(85) 99645-7140 nelclassico@gmail.com Praça Coronel Melquiades, 124
Tecnologia

Construindo Ridgeline: Projetando uma experiência 3D em tempo real em Webflow | Codrops

Ridgeline é um site de caminhadas e fotografia: um passeio cinematográfico por três verdadeiras trilhas alpinas. É um projeto pessoal e é realmente a minha maneira de decidir como quero construir sites. Tudo começou com uma pergunta: um site Webflow pode hospedar terreno 3D genuinamente real, ou seja, dados de elevação reais (DEMs) dispostos como uma malha de contorno de pesquisa e renderizados ao vivo em Three.js, não um loop de vídeo e não uma folha de sprite preparada, sem abrir mão da editabilidade do Webflow?

A única regra que estabeleci foi uma única frase: “Não me importo se é um componente integrado ou JS externo, quero VER.” Essa regra decidiu silenciosamente toda a arquitetura. Eu olhei para duas maneiras de obter 3D em tempo real no Webflow antes de me comprometer com uma, e essa decisão (compromissos e tudo) é onde está a verdadeira história.

O local finalizado é composto por três cenas de “condição”, cada uma uma verdadeira jornada desenhada em seu próprio terreno: Amanhecer (Tre Cime, uma tempestade), Nascer do sol (Mont Blanc, hora azul em rosa) e Neve (Annapurna, noite azul com neve caindo). Eles estão interligados com fotografia controlada por rolagem e som ambiente, além de uma página inicial do atlas que voa pelo terreno com três visualizações de cenas clicáveis. Tudo isso é geometria real e tudo é construído e mantido a partir do código por meio do Webflow MCP enquanto permanece um projeto de Webflow normal e editável por humanos.

A jornada Dawn é minha. Gravei a caminhada acima de Cortina no Tre Cime, exportei a atividade do Strava como uma trilha GPS (GPX), limpei os picos habituais do GPS e coloquei o traço real na verdadeira encosta SRTM. Apenas uma versão normalizada e sem coordenadas dos navios de rastreamento, de modo que o terreno seja reconhecível, mas a rota exata não seja exposta. (Sunrise e Snow usam rotas sintetizadas plausíveis, já que eu não tinha uma trilha gravada para elas.)

Mais uma coisa que vale a pena dizer desde o início, porque moldou a rapidez com que eu poderia me mover: todo o site foi codificado com Claude (Opus 4.8 e Fable 5), conduzindo o Webflow MCP, e ficou pronto em cerca de uma semana.

Também mantive um registro de construção desde o primeiro dia: as decisões, as vitórias e os erros. Este artigo foi extraído diretamente dele, então os erros também estão aqui, não apenas no resultado final arrumado.

A pilha

  • Three.js + React Three Fiber + drei para as cenas 3D.
  • GSAP (ScrollTrigger) + Lenis para rolagem e animação.
  • Blender (conduzido através do Blender MCP) para cada malha, modelada ou cozida, e então exportada para glTF (compactado Draco).
  • Cloudflare R2 para hospedar o pacote JS mais o GLB e os ativos de textura (um CDN, com um carregador bloqueado por cache).
  • Webflow para estrutura, estilo, páginas e CMS: a superfície editável por humanos.
  • Webflow MCP  (1.3 no momento da escrita) como a camada de construção, conduzida por Claude: páginas, componentes, classes, variáveis, código personalizado, publicação.
  • Strava como fonte de dados para a verdadeira jornada Dawn, exportada como uma pista GPX e colocada no terreno SRTM.

1. Duas maneiras de colocar React no Webflow e por que escolhi incorporar

O Webflow possui uma rota nativa adequada para isso: um componente de código (implantado via DevLink ou uma biblioteca compartilhada) que reside no Designer como um elemento de primeira classe. É uma ótima opção quando seu componente tem o formato de UI. O meu era um animal diferente. Um único pacote WebGL pesado com sua própria etapa de construção, Three.js, um pipeline de ativos preparado no Blender e GLBs hospedados em R2 é um monte de máquinas para rotear através de qualquer sistema de componentes. Então comparei isso com uma incorporação JS auto-hospedada e, para este projeto, a incorporação venceu. A compensação é a parte interessante:

  • Construção independente. Three.js, o bundler e os ativos ficam todos em um lugar que eu controlo de ponta a ponta.
  • Itera em segundos. Implante o pacote, atualize, pronto. Nenhuma republicação para uma mudança de comportamento.
  • Portátil. O mesmo embed roda em qualquer site ou CMS, que atenda a um pacote tão especializado.

A única coisa que uma incorporação não é é um elemento do Designer de arrastar e soltar. Resolvi isso com a montagem baseada em atributos. A incorporação procura elementos de host que o designer coloca (como [data-terrain-scene] ou [data-terrain-card]) e é montado neles. O designer permanece no controle de onde ; o código controla o que .

###PRE_d34609607b95be3de18f6bfc789563ad###

Conclusões

  1. Combine o caminho de integração com o componente. Valide se ele se adapta ao seu caso de uso antes de confirmar.
  2. Uma incorporação auto-hospedada troca a conveniência do Designer por uma construção independente e iteração instantânea.
  3. A montagem orientada por atributos mantém a divisão limpa: o fluxo da Web é dono do layout, o código é dono do comportamento.

2. Construindo todo o site a partir do código: o fluxo de trabalho Webflow MCP

Essa é a parte que mais me surpreendeu. Todo o site (páginas, componentes, classes, CSS, variáveis, código personalizado, SEO, publicação) é construído e mantido por um agente através do servidor Webflow MCP (Model Context Protocol). O agente aqui é Claude. E a saída ainda é um projeto de Webflow completamente normal que um ser humano pode abrir e editar.

A regra que estabeleci:

Marcação e CSS residem no Webflow Designer como componentes e classes nomeados. Comportamento, 3D e animação ao vivo em JS versionado em R2. O Designer mantém a estrutura; o CDN mantém o comportamento.

Uma coisa que vale a pena saber com antecedência: o Webflow oferece dois locais para código personalizado, e eles são para trabalhos diferentes. Existem scripts registrados (a API de scripts, JS injetado) e há código personalizado de cabeçalho/rodapé (HTML/CSS bruto). Qualquer coisa que deva existir na primeira pintura pertence à segunda, e explicarei exatamente o porquê na seção flash.

Conclusões

  1. Um agente pode conduzir uma construção de fluxo da Web real via MCP e deixar para trás um projeto totalmente editável por humanos.
  2. Mantenha uma linha dura: estrutura no Webflow, comportamento no código. É isso que mantém ambas as metades sustentáveis.

3. Tornando o 3D real: Blender para glTF para Three.js

O inegociável é que a geometria é real, não um shader inteligente em um primitivo. Cada terreno é um DEM real (um conjunto de dados de elevação SRTM) para um maciço real, modelado no Blender, exportado para glTF e carregado em Three.js. Se for “um monólito de cristal”, a resposta começa no Blender, não no JSX.

O pipeline:

  1. Modele ou asse no Blender. Eu uso um build.py paramétrico para formas definidas por matemática e modelagem interativa para formas direcionadas à arte. De qualquer forma, existe um .blend real para que a geometria possa ser iterada posteriormente.
  2. Exporte para glTF com os sinalizadores importantes:

###PRE_1575360fb4b9bc4f105bededbb2ffed1###

  1. Carregue com useGLTF, percorra para encontrar objetos nomeados, aplicar materiais e capturar o estado por instância no carregamento. Não recalcule a geometria em tempo de execução.

O Blender também executou seu próprio MCP, a mesma configuração de agente do Webflow, para que ajustes de modelo pudessem acontecer diretamente no Blender quando uma cena precisasse, sem nenhuma viagem manual de ida e volta pela UI.

Os três GLBs de terreno chegam a aproximadamente 540-580 KB cada após Draco, pequenos o suficiente para que o download nunca seja o gargalo. Os gargalos estavam todos na GPU e no thread principal, que é o restante deste artigo.

A aparência do mapa de levantamento é um shader, sem texturas

A estética do mapa de contorno não é uma textura. É um shader de fragmento que lê a altura do espaço mundial da malha. O truque que mantém as linhas nítidas a qualquer distância da câmera é fwidth(): ele deriva a largura do anti-alias no espaço da tela da taxa de mudança do índice de banda, de modo que as linhas tenham um pixel de largura, independentemente de você aumentar ou diminuir o zoom.

###PRE_40152fa7fec9d8d8b006565d8d1199d6###

Cada cena tem o mesmo shader com um conjunto diferente de uniformes (cores de fundo e contorno, intensidade da neve, direção da luz), que é o que faz com que Dawn, Sunrise e Snow pareçam três lugares enquanto compartilham um programa. Um pequeno toque que supera seu peso: um minúsculo pontilhamento por pixel ((hash(gl_FragCoord.xy) – 0,5) * 0,0045) elimina as faixas de 8 bits que, de outra forma, apareceriam como manchas granuladas nos gradientes quase pretos do amanhecer.

Conclusões

  1. A geometria real é lida de maneira diferente de uma primitiva falsa. Vale a pena o pipeline.
  2. export_yup, export_apply, export_extras, e Draco são as quatro bandeiras que você sempre deseja.
  3. fwidth() oferece largura de linha independente de resolução, a chave para contornos procedimentais nítidos em qualquer zoom.
  4. Um shader mais uniformes por cena supera três shaders, e um dither barato supera faixas visíveis de 8 bits.

4. Animação: role sem renderizar novamente React

Todo o movimento é controlado pela rolagem, e a regra fundamental é que o React nunca é renderizado novamente na rolagem. O progresso da rolagem entra em uma referência e é lido dentro do loop de renderização.

  • Lenis conduz uma rolagem suave e alimenta o ScrollTrigger.
  • GSAP ScrollTrigger possui fixação e limpeza.
  • O progresso vai para um ref, leia cada quadro em useFrame. Nenhum estado React no caminho de rolagem.

###PRE_2eddce34705f5d29522943f968b20f51###

Dois padrões fizeram o trabalho pesado.

Primeiro, a seção de recolhimento de quadros. Uma imagem em tela inteira se transforma em uma pequena placa 4:5 enquanto as colunas da imagem ao redor fluem. A armadilha: a animação width e a altura da tela inteira para a placa altera a proporção de aspecto de cada quadro, o que força object-fit para recortar cada quadro, e esse recortar é um salto visível. A correção foi tornar o elemento uma caixa fixa de 4:5, dimensionada uma vez para cobrir a janela de visualização, animada exclusivamente por transform: scale. Caixa constante mais aspecto constante significa que o navegador calcula o corte uma vez e nunca recorta, com zero layout por quadro.

###PRE_b1a2edb997403709f05b5911a473205d###

Depois veio a armadilha, que me custou muito tempo. Uma animação de desvio CSS cujos quadros-chave começaram a partir de um estado de deslocamento (scale(1.04) translate(...)). No instante em que foi acionado, o elemento se ajustou a esse deslocamento. Um segundo “salto” que sobreviveu a todas as correções da matemática do colapso, porque não foi o colapso. A lição: qualquer animação que alterna no meio da rolagem deve começar no estado de repouso do elemento (identidade) ou ele irá aparecer.

Conclusões

  1. Nunca renderize novamente o React na rolagem. Progresso em uma referência, lido no loop do quadro.
  2. Animando width e height re-cortes object-fit cada quadro. Prefira transformar: é combinável, sem layout.
  3. Um quadro-chave que não começa na identidade será quebrado quando for ativado e se ocultará do código que você acha que é responsável.

5. As costuras: pré-carregador, flash de primeira pintura, porta de áudio

O 3D nunca foi a parte difícil. As costuras, os momentos entre estados, comiam na maior parte do tempo. Cada um é uma pegadinha ensinável.

Comece com o flash da primeira pintura. Em uma recarga difícil, você capturaria uma fração de segundo de um fundo simples antes que o pré-carregador escuro aparecesse. É um problema de tempo: o JS do site é injetado por um carregador, então ele executa após o navegador já ter pintado o HTML bruto, o que significa que o JS não pode impedir seu próprio flash de pré-carregamento. A capa deve existir no código customizado do head, de forma síncrona, antes de mais nada:

###PRE_19831e375a55e3fddd122229e039bb36###

O JS remove #topo-fp-guard em init() e aumenta o pré-carregador real no mesmo tick, para que não haja flash na saída também. Esse body>*{visibility:hidden} é agressivo, então o tempo limite à prova de falhas é o que impede que uma falha de JS deixe a página em branco.

Em seguida, o portão de áudio. Os navegadores bloqueiam a reprodução automática até um gesto do usuário, de modo que o pré-carregador termina com uma escolha explícita de Enter ou Enter-mudo, que também funciona como o gesto que desbloqueia o barramento de áudio ambiente. Tentar a reprodução automática antes disso é um erro garantido do console.

Som: uma camada ambiente que rastreia a cena

Assim que o portão destranca o ônibus, cada cena executa seu próprio áudio silencioso, de modo que parece uma atmosfera em vez de uma trilha sonora.

  • Uma cama ambiente por cena. O nascer do sol recebe um suave ciclo florestal, a neve um vento fino de neve e o amanhecer permanece “silencioso” porque sua tempestade carrega o som. As camas fazem cross-fade com a troca de cena, então não há cortes na navegação.
  • Eventos, não um loop. Alcançar um cume na linha de caminhada toca um breve sinal sonoro de chegada. A tempestade Dawn é a peça principal: cada raio programa seu trovão por distância, setTimeout(() => audio.thunder(d), 350 + d * 3200), então um raio próximo estala quase imediatamente e um distante ressoa segundos depois, exatamente como o real. Ruídos baixos de fundo acontecem a cada 7-19s para manter a tempestade viva entre os ataques.
  • Um interruptor controla tudo isso. A opção Enter ou Enter-mute do portão define um único sinalizador window.__topoSound.enabled, transmitido em um evento topo:sound e o áudio de cada cena lê dele. O silenciamento é instantâneo e sobrevive às transições de página.

A regra que segui: o som deve ser aquilo que você sentiria falta se ele desaparecesse, não aquilo que você nota quando ele está lá.

Conclusões

  1. Um script injetado pelo carregador não pode impedir seu próprio flash de primeira pintura. A guarda pertence ao código principal síncrono.
  2. Qualquer proteção de página inteira precisa de um tempo limite à prova de falhas, ou uma falha de carregamento significa uma página permanentemente em branco.
  3. Dobre o gesto de desbloqueio de áudio em um momento de interface do usuário que você já possui (o botão Enter). Não siga um prompt separado.
  4. Vincule o som à cena. Camas por cena e eventos como trovões cronometrados vendem o lugar, e uma bandeira global de mudo o mantém instantâneo e seguro para navegação.

6. Transições de página sem queda de quadro

Navegar entre as cenas é uma troca PJAX sob uma cobertura de “inundação” WebGL, uma revelação de contorno topográfico que varre, segura e depois drena. A principal chamada arquitetônica: a tela do terreno é um único contexto WebGL persistente. A navegação troca os dados da cena (setScene), ela não desmonta e recria a tela. Criar um novo contexto por navegação é como você obtém contextos despejados e flashes pretos.

O problema residual era mais sutil. A inundação já foi drenada em um evento pronto para cena, mas esse evento foi disparado quando a nova malha foi analisada , antes que o shader de seu material fosse compilado. Portanto, a compilação travou o thread principal durante a revelação visível . A correção é compileAsync:

###PRE_22aacb32f21391f62edd2c83cd36c0ac###

compileAsync usa a extensão de compilação de shader paralelo, então o trabalho pesado acontece fora do thread e a revelação desenha uma cena que já está quente. O mesmo truque pré-aquece a cena do cume antes de aparecer e mata um congelamento de aproximadamente um segundo na primeira revelação.

Uma observação honesta que se encaixa no tema “sem almoço grátis”: ainda há cerca de 110 ms de trabalho síncrono (a troca do innerHTML mais a religação do JS da nova página), mas ele é executado enquanto o Flood está totalmente opaco e retido, por isso é invisível. Deixei isso aí em vez de reestruturar uma delicada função PJAX para um ganho que ninguém pode ver. Perfeito onde é visto, pragmático onde não é.

Conclusões

  1. Persista um contexto WebGL em toda a navegação. Troque dados, não a tela.
  2. Coloque sua revelação pronta para GPU, não analisada. compileAsync move a compilação para fora do thread principal.
  3. Saiba quais barracas estão visíveis. Esconda os inevitáveis ​​sob uma cobertura opaca em vez de combatê-los.

7. O desempenho geralmente não é animar as coisas

Esta é a seção que o título promete. Chegar a 60fps bloqueados foi uma série de edições do tipo “pare de fazer um trabalho que não altera um pixel”. Criei primeiro um pequeno perfilador FPS na página. Você alterna com um pressionamento de tecla e ele agrupa a taxa de quadros por profundidade de rolagem e rotula a seção, de modo que uma gota é lida como min 34 @ 46% (Frames) em vez de um vago “parece estranho”. Cada correção abaixo visava um número real e uma localização real:

onde antes depois
Introdução à página inicial (telas de terreno) 46fps, min 33 60 , min 56
Cena de neve (neve caindo) 37fps 60
Summit revelado (primeira rolagem) ~1000ms congelar desaparecido
Entrada do rodapé min 7 min 57

As vitórias, em ordem de impacto. Nenhum deles custa qualidade visual:

  • Congele o mapa de sombras. A luz principal e o terreno são estáticos; apenas a câmera voa. No entanto, um mapa de sombras 2048 PCF renderizou novamente a cena inteira em cada quadro. Asse uma vez e defina gl.shadowMap.autoUpdate = false. As sombras são independentes da câmera, então nada muda.
  • Elimine MSAA desperdiçado. A tela de fundo é renderizada por meio de um compositor de pós-processamento, portanto, seu próprio buffer antialias:true só recebe um blit de tela cheia, uma alocação MSAA e resolve cada quadro para nada. E a passagem de desfoque executou multisampling:4, que é MSAA em um desfoque. Ambos desligados.
  • Animar partículas na GPU. A neve caindo executou um loop de CPU por quadro em aproximadamente 2.000 pontos e reenviou todo o buffer de posição, três vezes, uma por camada. Essas paralisações de sincronização de CPU para GPU foram a única cena que perdeu quadros em uma GPU rápida (que parece travar mais, não menos). Posições estáticas mais um sombreador de vértice que calcula a queda do uTime fornecem uma aparência idêntica com custo quase zero por quadro.

###PRE_36905aea8acc92831a2acab3fb2018cc###

  • Pare as caminhadas na árvore por quadro. Alguns scene.traverse() loops reescreveram a opacidade do material em cada quadro, mesmo quando o valor não havia mudado. Armazene em cache a lista de materiais uma vez e pule totalmente quando o valor estiver estável.
  • Substitua a “atmosfera” cara por fades baratos. Uma cobertura de “névoa” com gradiente animado de quatro camadas que se levantava com uma tela cheia blur(9px) estava sendo repintada continuamente para o que parece ser um fade. Um desbotamento de opacidade escura tem a mesma aparência e não custa nada.

A linha direta, emprestada de todo post de bom desempenho: o quadro mais rápido é aquele que não funciona. Quase todas as vitórias aqui foram por remoção, não por inteligência.

Conclusões

  1. Crie um criador de perfil antes de otimizar. Classifique por local para que as correções sejam direcionadas.
  2. Luz estática mais uma câmera em movimento significam que você pode congelar o mapa de sombras. Livre.
  3. Os reuploads do buffer são os que mais travam as GPUs rápidas. Envie a animação por partícula para o sombreador de vértice.
  4. Auditoria antialias e MSAA quando um compositor está envolvido. Muitas vezes é pago e nunca usado.

8. Conteúdo: editável e construído para ser encontrado

As páginas de cena são estáticas, mas o site possui uma coleção CMS (a galeria) para que o conteúdo editorial permaneça editável no Webflow, não bloqueado em código. O padrão que mantém um CMS e um código incorporado cooperando:

  • A Coleção contém o conteúdo: títulos, coordenadas, cópia, imagens.
  • A incorporação lê os atributos e o DOM que o modelo CMS renderiza (como um [data-terrain-card="{slug}"]) em vez de buscar a própria API do CMS, de modo que o 3D se vincula a tudo o que o editor publica sem uma segunda fonte de dados.
  • Os tokens de design (cores, escala de tipo, espaçamento) permanecem como variáveis ​​do fluxo da Web, portanto, os acentos por cena são editáveis ​​no Designer, e não codificados no sombreador.

Manter o conteúdo real e editável também compensa em termos de descoberta. Marcação semântica limpa, títulos por página e meta descrições, e os rápidos Core Web Vitals que falharam no trabalho de desempenho são exatamente o que os rastreadores de pesquisa e os mecanismos de resposta de IA (AEO) analisam melhor. Conteúdo real em HTML real, com carregamento rápido e boa leitura para pessoas e máquinas.

Conclusões

  1. Deixe o próprio conteúdo do CMS e deixe a incorporação ler o DOM renderizado, não uma API paralela.
  2. Mantenha acentos e tokens como variáveis ​​​​do Webflow para que a aparência permaneça editável.
  3. SEO e AEO vêm principalmente de graça da mesma disciplina: marcação semântica, metadados por página e uma página genuinamente rápida.

Reflexões

Se eu fizesse isso de novo:

  • Eu procuraria a incorporação auto-hospedada no primeiro dia para um pacote tão especializado, embora pesar os dois caminhos de integração tenha sido um tempo bem gasto.
  • Eu trataria o orçamento do quadro como uma restrição de design desde o início, não como uma passagem de limpeza. As maiores vitórias aqui foram arquitetônicas (um contexto WebGL, um mapa de sombra congelado, compilações de shader off-thread), e essas são muito mais baratas para projetar do que para modernizar.
  • Eu observaria os dois canais de código personalizado antecipadamente. Uma pequena coisa que teria me salvado um pouco.

A lição recorrente, e aquela que vale a pena deixar para você: “compila” não é “está feito”. Quase todas as correções nesta compilação vieram da observação da página renderizada real em uma guia focada. Os saltos, os flashes, as quedas de quadro eram invisíveis no código e óbvios na tela.

Minha pilha

  • Three.js, React Three Fiber e drei
  • GSAP com ScrollTrigger
  • Lenis
  • Blender via Blender MCP (glTF e Draco)
  • Cloudflare R2
  • Pista Strava GPX para a verdadeira rota Dawn
  • Webflow com o Webflow MCP
  • Codificado com Claude (Opus 4.8 e Fable 5)

Deixe um comentário

Seu email não será publicado. Campos obrigatórios marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.