Apostila profissional · Web & 3D Interativo

Unity na web:
da página ao motor
e de volta

Um guia completo — do básico ao muito avançado — para colocar Unity dentro de sites e aplicações web de verdade: a ponte entre JavaScript e C# nos dois sentidos, React e Next.js, canvas responsivo, input que não briga com a página, navegadores móveis, WebGPU, acessibilidade e medição — e quando é melhor não usar Unity.

12 capítulosníveis básico → muito avançado
20 exercícioscom gabaritos
Código pronto.jslib, C#, JS, React e template
2 demos ao vivoo caminho de um clique e o gerador de ponte

↓ role para começar — a barra no topo é o seu progresso

Capítulo 01 Básico

Unity na web: quando faz sentido

Unity no navegador é poderoso e pesado. A primeira competência é saber quando o peso compensa — e quando uma biblioteca web de 3D faz o mesmo com um décimo do custo.

Esta apostila trata de experiências interativas dentro de páginas web: configuradores de produto, visualizadores 3D em sites de marca, demonstrações de produto, mini-jogos promocionais, visualizações de dados em 3D, simuladores embutidos em portais. O denominador comum é que o Unity não é a página — é uma peça dentro dela, que precisa conversar com o resto.

Onde esta apostila se encaixa

Reduzir o tamanho do build, compressão, configuração do servidor, cache e tela de carregamento estão em Serious Games na Web — aqui não se repete isso, remete-se. Unity para aplicações não-web (desktop, mobile, XR, gêmeos digitais) está em Unity para Aplicações. O básico do motor está em Unity Essencial.

1.1 As alternativas que você precisa conhecer

FerramentaO que éTamanho típico da primeira cargaBrilha em
<model-viewer>Componente web que mostra um glTF com rotação, AR e anotaçõesMuito pequeno (a biblioteca + o modelo)"Ver o produto em 3D" sem lógica
Three.jsBiblioteca JavaScript de renderização 3DPequeno; cresce com o que você usaExperiências de marca, efeitos, integração fina com o DOM
Babylon.js / PlayCanvasMotores 3D nativos da web, com editor (PlayCanvas) e físicaPequeno a médioJogos e apps 3D feitos para a web desde o início
Unity (build Web)O motor completo compilado para WebAssemblyVários MB mesmo num projeto vazioLógica complexa, física, conteúdo já feito em Unity, equipe que já sabe Unity, multiplataforma

Para aprofundar as alternativas, veja Computação Gráfica com Three.js e 3D em Tempo Real e Motion Graphics na Web.

1.2 A pergunta que decide

O que você perderia se reescrevesse isto em Three.js?
— a pergunta de triagem

Se a resposta for "quase nada" (um modelo girando, trocar cor, algumas anotações), use a alternativa web: vai carregar mais rápido, pesar menos no celular e ser mais fácil de indexar e tornar acessível. Se a resposta for "a física, o sistema de interação, seis meses de trabalho já feito em Unity, e a versão mobile e desktop do mesmo produto", o Unity se paga.

O custo que não aparece no protótipo

No computador do desenvolvedor, com fibra e cache quente, um build Unity abre em dois segundos. Numa landing page acessada por celular em 4G, é frequentemente o elemento que faz o visitante desistir antes de ver qualquer coisa. Meça sempre com rede limitada e cache vazio antes de decidir — e decida com o dado, não com a demo.

Na prática · mercado de trabalho

Agências e estúdios que fazem experiências web 3D valorizam quem sabe as duas pontas: C# no Unity e JavaScript/React na página. A maioria dos desenvolvedores Unity não sabe integrar com um front-end moderno, e a maioria dos front-ends não sabe Unity — quem faz a ponte é raro.

Exercício 1.1 — Unity ou web nativa?

Escolha a ferramenta: (a) página de produto de um tênis com rotação 360° e troca de cor; (b) simulador de montagem de uma máquina, com física de encaixe, já existente como app desktop em Unity; (c) mini-jogo promocional de 30 segundos numa campanha de redes sociais, acessado quase só por celular; (d) configurador de cozinha com 400 peças, regras de compatibilidade e orçamento.

Ver gabarito

(a) <model-viewer> ou Three.js — nada ali justifica o motor. (b) Unity — o trabalho já existe e a física é o produto. (c) Web nativa (Three.js/PlayCanvas): público em celular, sessão curta, cada segundo de carga custa visitantes. (d) Qualquer um dos dois pode servir; Unity ganha se a equipe já trabalha nele ou se o configurador também será app desktop/mobile — e aí o cap. 12 mostra como fazer.

Capítulo 02 Básico

Anatomia de um build Web e do loader

Antes de integrar, entenda o que o Unity entrega e qual é o único ponto de contato entre a página e o motor.

2.1 O que sai do build

No Unity 6 a plataforma chama-se Web (antes, WebGL). Um build produz uma pasta com o index.html do template e uma subpasta Build/ com quatro arquivos principais:

ArquivoPapel
*.loader.jsO script que você inclui na página. Define createUnityInstance
*.framework.jsO código JavaScript de suporte do runtime (incluindo o que vier dos seus .jslib)
*.wasmO seu C# e o motor, compilados para WebAssembly
*.dataCenas e assets

2.2 O único ponto de entrada

<canvas id="unity-canvas" tabindex="-1"></canvas>
<script src="Build/app.loader.js"></script>
<script>
  createUnityInstance(document.querySelector("#unity-canvas"), {
    dataUrl:      "Build/app.data",
    frameworkUrl: "Build/app.framework.js",
    codeUrl:      "Build/app.wasm",
    companyName:  "Uniana",
    productName:  "Configurador",
    productVersion: "1.0",
  }, (progresso) => {
    barra.style.width = (progresso * 100) + "%";   // 0 a 1
  }).then((unityInstance) => {
    window.unityInstance = unityInstance;           // guarde: é por aqui que se fala com o motor
  }).catch((erro) => {
    mostrarErro(erro);                              // navegador sem suporte, falta de memória…
  });
</script>

createUnityInstance devolve uma promise que resolve com a instância. Tudo o que a página faz com o motor passa por esse objeto: SendMessage (cap. 3), SetFullscreen (cap. 6) e Quit (cap. 5).

2.3 Templates

O index.html gerado vem de um template. Os embutidos (Default, Minimal, PWA) servem para testar; para produção, crie o seu em Assets/WebGLTemplates/NomeDoTemplate/index.html e escolha-o nas Player Settings. O template aceita variáveis que o Unity substitui no build, como {{{ LOADER_FILENAME }}}, {{{ DATA_FILENAME }}} e {{{ PRODUCT_NAME }}} — assim o nome dos arquivos não fica escrito à mão.

Mas numa integração real você frequentemente não usa o index.html do Unity: copia a pasta Build/ para o site e chama createUnityInstance a partir da sua própria página ou componente (cap. 5). O template passa a ser só o ambiente de teste.

Nomes de arquivo e cache

Se o build tiver sempre o mesmo nome (app.wasm) e o servidor mandar guardar em cache, os visitantes podem ficar com uma versão antiga do .wasm e uma nova do .data — e o resultado é um erro obscuro. Ative a opção de nomes com hash do conteúdo (Name Files As Hashes) ou versione a pasta. Compressão e cabeçalhos do servidor estão em Serious Games na Web, cap. 7.

Exercício 2.1 — Monte o mínimo

Faça um build Web de uma cena com um cubo, apague o index.html gerado e escreva o seu, do zero, só com o canvas, o loader, uma barra de progresso em HTML e uma mensagem de erro amigável se createUnityInstance falhar. Sirva a pasta com um servidor local (abrir o arquivo direto do disco não funciona).

Ver o que esperar

Se a página abrir em branco, abra o console: os erros mais comuns são caminho errado dos arquivos em Build/, arquivos comprimidos servidos sem o cabeçalho Content-Encoding, e o protocolo file://. Guardar este HTML mínimo é útil: é o ponto de partida de todos os exercícios seguintes.

Capítulo 03 Intermediário · Interativo

A ponte, parte 1: da página para o Unity

A página manda, o Unity obedece. O canal é estreito — um objeto, um método, um valor — e isso é uma vantagem, se você desenhar o contrato.

3.1 SendMessage

// JavaScript, na página
unityInstance.SendMessage("Ponte", "TrocarCor", "#C0392B");
// C#, num componente do GameObject chamado "Ponte"
public class Ponte : MonoBehaviour
{
    [SerializeField] Configurador configurador;

    // chamado pela página — público, um parâmetro (string, int ou float) ou nenhum
    public void TrocarCor(string hex)
    {
        if (ColorUtility.TryParseHtmlString(hex, out var cor))
            configurador.AplicarCor(cor);
    }
}

Três regras: o nome do GameObject tem de existir na cena e ser único (é procurado por nome); o método tem de ser público e aceitar no máximo um parâmetro, do tipo string ou número; e não há valor de retorno — se a página precisa de uma resposta, o Unity responde pelo caminho inverso (cap. 4).

3.2 Um só objeto, um contrato em JSON

Espalhar SendMessage para vinte objetos diferentes acopla a página à hierarquia da cena: renomeou um objeto, a página quebra em silêncio. Melhor: um único GameObject "Ponte", persistente, com poucos métodos que recebem JSON, e que distribui internamente. O contrato fica documentado num lugar só.

// JavaScript
function enviar(comando, dados) {
  unityInstance.SendMessage("Ponte", "Receber",
    JSON.stringify({ v: 1, comando, dados }));
}
enviar("trocarCor", { peca: "porta", cor: "#C0392B" });

// C#
[System.Serializable] class Mensagem { public int v; public string comando; public string dados; }
public void Receber(string json) { /* desserializa e despacha por 'comando' */ }

O campo v (versão do contrato) parece excessivo até o dia em que o site e o build são publicados em momentos diferentes e um lado passa a mandar um formato que o outro não conhece.

3.3 A mensagem antes do motor estar pronto

O erro mais frequente: a página chama SendMessage antes de createUnityInstance resolver, ou antes de a cena com o objeto "Ponte" ter carregado. A mensagem se perde. Solução: uma fila na página que acumula comandos até o Unity avisar que está pronto (o aviso vem pelo cap. 4), e só então os despacha.

🔧 Gerador de ponte — escreva o contrato, receba os dois lados

JavaScript (página)


    

C# (Unity)


    

Exercício 3.1 — Onde a mensagem morreu?

A página chama unityInstance.SendMessage("Carro", "Pintar", "red") e nada acontece, sem erro visível. Liste quatro causas possíveis.

Ver gabarito

(1) Não existe objeto chamado exatamente "Carro" na cena carregada (ou o nome mudou, ou é "Carro(Clone)" porque foi instanciado). (2) O método é privado ou tem assinatura incompatível. (3) A mensagem foi enviada antes de a instância ou a cena estarem prontas. (4) O método recebe "red", mas espera um hexadecimal e ignora a falha de conversão. O console do navegador costuma mostrar um aviso para (1) e (2) — por isso ele fica sempre aberto durante o desenvolvimento.

Exercício 3.2 — A fila

Escreva, em JavaScript, uma função enviar(comando, dados) que funcione tanto antes quanto depois de o Unity estar pronto, e uma função unityPronto() que o Unity chamará quando a cena carregar.

Ver exemplo resolvido
const fila = []; let pronto = false;
function enviar(comando, dados) {
  const msg = JSON.stringify({ v: 1, comando, dados });
  if (pronto) unityInstance.SendMessage("Ponte", "Receber", msg);
  else fila.push(msg);
}
window.unityPronto = function () {
  pronto = true;
  fila.splice(0).forEach(m => unityInstance.SendMessage("Ponte", "Receber", m));
};

Capítulo 04 Intermediário · Interativo

A ponte, parte 2: do Unity para a página

O C# não enxerga o window. Para falar com a página, é preciso um plugin em JavaScript que o build embute — o .jslib.

4.1 O caminho de um clique, ida e volta

Role o bloco abaixo: o painel escuro acompanha cada etapa de uma interação completa — o visitante clica num botão HTML, a peça muda no 3D, e o painel HTML mostra o novo preço.

Etapa 1 · página

O controle mora no HTML

Botões, seletores e formulários em HTML são mais acessíveis, mais rápidos de fazer, indexáveis e estilizados com o CSS do site. Deixe no canvas só o que precisa ser 3D.

Etapa 2 · ida

Um canal, um contrato

Tudo passa pela função enviar() com fila (exercício 3.2). A página não conhece a hierarquia da cena — só o contrato.

Etapa 3 · motor

O Unity é dono do estado 3D

A regra de negócio (preço, compatibilidade) pode estar no C# ou num backend — mas precisa estar num lugar só. Se a página e o Unity calculam o preço cada um à sua maneira, um dia vão discordar.

Etapa 4 · volta

O plugin .jslib

Funções declaradas em C# com [DllImport("__Internal")] são implementadas num arquivo .jslib. O build junta esse JavaScript ao framework.js.

Etapa 5 · página

Eventos, não funções globais soltas

Em vez de o .jslib chamar funções específicas da página, ele dispara um CustomEvent no window. A página escuta o que quiser — e o build não precisa saber que framework está do outro lado.

4.2 O plugin .jslib

// Assets/Plugins/WebGL/Ponte.jslib
mergeInto(LibraryManager.library, {

  PonteEmitir: function (nomePtr, jsonPtr) {
    var nome = UTF8ToString(nomePtr);          // ponteiro de memória → string JS
    var dados = JSON.parse(UTF8ToString(jsonPtr));
    window.dispatchEvent(new CustomEvent("unity:" + nome, { detail: dados }));
  },

  PonteLerQuery: function (chavePtr) {        // devolver uma string ao C#
    var valor = new URLSearchParams(location.search).get(UTF8ToString(chavePtr)) || "";
    var tamanho = lengthBytesUTF8(valor) + 1;
    var buffer = _malloc(tamanho);
    stringToUTF8(valor, buffer, tamanho);
    return buffer;                              // o runtime converte e libera
  }
});
// C#
using System.Runtime.InteropServices;

public static class PonteWeb
{
#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")] static extern void PonteEmitir(string nome, string json);
    [DllImport("__Internal")] static extern string PonteLerQuery(string chave);
#else
    static void PonteEmitir(string nome, string json) => UnityEngine.Debug.Log($"[web] {nome}: {json}");
    static string PonteLerQuery(string chave) => "";
#endif
    public static void Emitir(string nome, object dados) =>
        PonteEmitir(nome, UnityEngine.JsonUtility.ToJson(dados));
    public static string LerQuery(string chave) => PonteLerQuery(chave);
}
// JavaScript, na página
window.addEventListener("unity:precoMudou", (e) => {
  document.querySelector("#preco").textContent =
    e.detail.total.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
});
Três armadilhas da volta
  • Strings passam como ponteiros. O que chega ao .jslib é um número (endereço na memória do WebAssembly); sem UTF8ToString, você vê números estranhos em vez do texto.
  • No editor, __Internal não existe. Sem o #if UNITY_WEBGL && !UNITY_EDITOR e uma implementação alternativa, o Play Mode lança EntryPointNotFoundException.
  • Exceções no .jslib derrubam a instância. Um JSON.parse de texto inválido dentro do plugin pode interromper o runtime. Envolva o que pode falhar em try/catch e reporte o erro, em vez de deixar propagar.
Exercício 4.1 — O aviso de pronto

Usando PonteWeb.Emitir, faça o Unity avisar a página de que está pronto quando a cena principal terminar de carregar, e ligue esse aviso à fila do exercício 3.2.

Ver exemplo resolvido

No C#, num Start do objeto Ponte (que só roda depois de a cena carregar): PonteWeb.Emitir("pronto", new Vazio());. Na página: window.addEventListener("unity:pronto", () => window.unityPronto());. Assim a fila é drenada exatamente quando o objeto que recebe as mensagens existe — nem antes, nem depois.

Exercício 4.2 — Link compartilhável

Um cliente quer mandar por WhatsApp o link de uma configuração ("porta vermelha, puxador preto"). Desenhe o fluxo nos dois sentidos.

Ver gabarito

Ida: a cada mudança, o Unity emite configMudou com o estado; a página atualiza a URL com history.replaceState (?porta=vermelha&puxador=preto). Volta: ao abrir um link, o Unity lê a query (com PonteLerQuery, ou a página envia a configuração depois do aviso de pronto) e aplica. A segunda opção é mais simples e mantém a URL como assunto da página.

Capítulo 05 Intermediário

Dentro de React e Next.js

Frameworks montam e desmontam componentes o tempo todo. O Unity não gosta de ser desmontado — e é aqui que nascem os vazamentos de memória.

5.1 A biblioteca react-unity-webgl

O pacote comunitário react-unity-webgl envolve o loader num hook e num componente, com estado de carregamento, envio de mensagens e escuta de eventos:

import { Unity, useUnityContext } from "react-unity-webgl";

export default function Configurador() {
  const { unityProvider, isLoaded, loadingProgression, sendMessage,
          addEventListener, removeEventListener } = useUnityContext({
    loaderUrl:    "/unity/Build/app.loader.js",
    dataUrl:      "/unity/Build/app.data",
    frameworkUrl: "/unity/Build/app.framework.js",
    codeUrl:      "/unity/Build/app.wasm",
  });

  useEffect(() => {
    const aoMudarPreco = (json) => setPreco(JSON.parse(json).total);
    addEventListener("precoMudou", aoMudarPreco);
    return () => removeEventListener("precoMudou", aoMudarPreco);
  }, [addEventListener, removeEventListener]);

  return (<>
    {!isLoaded && <Progresso valor={loadingProgression} />}
    <Unity unityProvider={unityProvider} style={{ width: "100%", aspectRatio: "16/9" }} />
    <button onClick={() => sendMessage("Ponte", "Receber", JSON.stringify({ v:1, comando:"trocarCor", dados:{ cor:"#C0392B" } }))}>
      Vermelho
    </button>
  </>);
}

Para o sentido Unity → React, a biblioteca usa um despachante próprio chamado a partir do .jslib (dispatchReactUnityEvent), em vez do CustomEvent do cap. 4. Os dois modelos são equivalentes; escolha um e seja consistente. Confira a documentação da versão que instalar — a API mudou entre versões principais.

5.2 Next.js: só no cliente

5.3 Desmontar sem vazar

Uma instância de cada vez

Se o visitante navega para outra rota e o componente é desmontado sem encerrar o Unity, o runtime continua na memória. Ao voltar, uma segunda instância é criada — e em celulares a aba cai na terceira ou quarta navegação. Antes de desmontar, chame unityInstance.Quit() (ou o unload() da biblioteca) e aguarde a promise. Mesmo assim, parte da memória só volta quando a página é recarregada; se o Unity aparece em várias rotas, considere mantê-lo montado num layout persistente e apenas escondê-lo.

Exercício 5.1 — O vazamento

Num site Next.js, a rota /produto/[id] mostra o configurador Unity. Usuários de iPhone reclamam que "a página fecha sozinha" depois de ver quatro ou cinco produtos. Diagnóstico e duas soluções possíveis.

Ver gabarito

Cada navegação entre produtos desmonta e remonta o componente, criando uma nova instância sem liberar a anterior; o Safari mata a aba ao estourar o limite de memória. Soluções: (1) chamar e aguardar Quit() no desmonte; (2) melhor: manter uma só instância num layout comum a todas as rotas de produto e, ao trocar de produto, enviar um comando carregarProduto em vez de recriar o Unity.

Capítulo 06 Aplicado

Canvas responsivo, DPI e tela cheia

O canvas é um elemento da página como outro qualquer — até você esquecer que numa tela de alta densidade ele pode renderizar nove vezes mais pixels do que parece.

6.1 Tamanho: o CSS decide

Por padrão (matchWebGLToCanvasSize: true), o Unity ajusta a resolução de renderização ao tamanho do canvas na página. Portanto, o layout é assunto do CSS: largura 100%, aspect-ratio para manter a proporção, e o Unity acompanha. No C#, Screen.width e Screen.height refletem esse tamanho — use-os, e não valores fixos, para posicionar UI.

6.2 Densidade de pixels: o custo escondido

TeladevicePixelRatioPixels renderizados num canvas de 400×300 CSS
Monitor comum1120 000
Notebook de alta densidade2480 000 (4×)
Celular topo de linha31 080 000 (9×)

O aparelho que tem a GPU mais fraca (o celular) é o que pede mais pixels. A configuração devicePixelRatio no objeto passado a createUnityInstance permite limitar:

const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
createUnityInstance(canvas, { ...config, devicePixelRatio: dpr });

A diferença visual entre 1,5 e 3 num celular é pequena; a diferença de desempenho e aquecimento, não.

6.3 Tela cheia

unityInstance.SetFullscreen(1) coloca o canvas em tela cheia — mas navegadores só permitem isso em resposta a um gesto do usuário (clique, toque). Chamado a partir de um temporizador ou logo no carregamento, é ignorado. Ligue-o a um botão. No iPhone, o suporte a tela cheia de elementos é limitado; ofereça um layout que ocupe a viewport inteira como alternativa.

Exercício 6.1 — Orçamento de pixels

Um configurador ocupa a largura toda de um celular com 390 px de largura CSS e proporção 4:3, com devicePixelRatio 3. Quantos pixels renderiza por frame? E com o limite de 1,5?

Ver gabarito

Tamanho CSS: 390 × 292,5. Com DPR 3: 1170 × 877,5 ≈ 1,03 milhão de pixels. Com DPR 1,5: 585 × 438,75 ≈ 257 mil — um quarto do trabalho de fragmentos, por uma perda de nitidez pouco perceptível.

Capítulo 07 Aplicado

Input na web: teclado, toque, rolagem e áudio

Um jogo em tela cheia pode capturar tudo. Um canvas no meio de uma página tem de dividir o teclado, o mouse e o dedo com o resto do site.

7.1 O teclado roubado

Por padrão o build Web captura o teclado da página inteira — e o campo de busca, o formulário de contato e o chat do site deixam de receber teclas. A correção fica no C#:

#if UNITY_WEBGL && !UNITY_EDITOR
    WebGLInput.captureAllKeyboardInput = false;   // o Unity só recebe teclas quando o canvas tem foco
#endif

Com isso, o canvas precisa de foco para receber teclado: dê-lhe tabindex e deixe o foco visível, para que quem navega por teclado saiba onde está.

7.2 A rolagem sequestrada

Se o Unity usa a roda do mouse para zoom, o visitante que só queria rolar a página fica "preso" no canvas. Padrões que funcionam: zoom só com Ctrl/⌘ + roda, ou só depois de um clique que "ativa" o 3D; e botões de zoom na interface. No toque, o mesmo dilema com o gesto de arrastar: o arrastar com um dedo rola a página e o de dois dedos gira o modelo, ou há um modo de interação explícito. Não existe resposta universal — mas "o canvas captura tudo" quase nunca é a certa numa página com conteúdo abaixo dele.

7.3 Gestos obrigatórios

RecursoExige gesto do usuário?Consequência
ÁudioSim (política de autoplay)Sem clique/toque prévio, o som fica mudo; comece silencioso e ofereça um botão "ativar som"
Tela cheiaSimCap. 6.3
Pointer lock (esconder e travar o cursor)SimÚtil para navegação em primeira pessoa; nunca ative sem o visitante pedir
Sensores de movimento (iOS)Sim, com permissãoO pedido de permissão tem de sair de um toque

7.4 Teclado virtual no celular

Campos de texto dentro do Unity não abrem o teclado do celular sozinhos; há suporte (WebGLInput.mobileKeyboardSupport), mas a experiência é inferior à de um <input> HTML. Para qualquer coisa além de uma palavra — nome, e-mail, observações — use um campo HTML sobreposto ou fora do canvas e envie o valor pela ponte.

Exercício 7.1 — Revisão de UX

Um visualizador de imóvel em Unity está no meio de uma página longa. Relatos: "não consigo rolar a página no celular", "o formulário de contato não digita", "tem uma música que não toca". Uma correção para cada.

Ver gabarito

Rolagem: arrastar com um dedo rola a página; girar o modelo só com dois dedos ou depois de tocar em "Explorar em 3D". Formulário: WebGLInput.captureAllKeyboardInput = false. Música: começar mudo e tocar só depois do primeiro gesto do usuário, com um botão de som visível.

Capítulo 08 Aplicado

Navegadores móveis e memória

Durante anos, "Unity na web" significava "no computador". O suporte oficial a navegadores móveis chegou no ciclo do Unity 2023 e do Unity 6 — e trouxe o limite de memória para o centro do projeto.

8.1 O que mudou

Versões anteriores exibiam um aviso de que navegadores móveis não eram suportados. As versões recentes suportam oficialmente o navegador do celular — o que não significa que qualquer projeto de desktop rode bem nele. A restrição dominante passa a ser a memória que a aba pode usar, especialmente no Safari do iOS, que encerra abas que passam do limite sem aviso ao usuário além de um recarregamento.

8.2 As alavancas

Teste no aparelho real, não no emulador

O modo de dispositivo das ferramentas de desenvolvedor do navegador simula o tamanho da tela, não a memória nem a GPU do celular. Um build que roda no emulador pode derrubar a aba de um iPhone de três anos. Tenha pelo menos um iPhone e um Android intermediário na bancada de testes.

Exercício 8.1 — Estratégia por aparelho

Metade do público de um configurador vem de celular. O build atual tem texturas 4K em DXT e abre bem no desktop, mas cai no iPhone. Proponha uma estratégia.

Ver gabarito

Dois builds: desktop (DXT, texturas altas) e mobile (ASTC, texturas reduzidas para 1K–2K, qualidade gráfica menor, DPR limitado). A página detecta o tipo de aparelho e carrega o build adequado. Em paralelo, medir a memória de pico no iPhone mais antigo que se quer suportar e ajustar o heap máximo a partir desse dado.

Capítulo 09 Avançado

WebGPU e o futuro do gráfico no navegador

WebGL é uma API de 2011 baseada em OpenGL ES. WebGPU é a sua sucessora — e muda o que um build Unity pode fazer no navegador.

9.1 O que o WebGPU traz

WebGL 2WebGPU
ModeloMáquina de estados do OpenGL ES 3.0API moderna, próxima de Vulkan, Metal e Direct3D 12
Compute shadersNãoSim — partículas na GPU, simulação, pós-processamento avançado
Custo por draw call na CPUAltoMenor, com pipelines pré-compilados
SuportePraticamente universalChrome e Edge desde 2023; Safari e Firefox passaram a ativá-lo a partir de 2025, com diferenças por sistema operacional

9.2 No Unity

O Unity 6 inclui um backend gráfico WebGPU, marcado como experimental/acesso antecipado à data desta apostila. Ele abre a porta a recursos que dependem de compute shaders (como o VFX Graph na GPU) em builds Web. A estratégia sensata hoje: WebGPU com fallback para WebGL 2 — as Player Settings permitem listar as APIs gráficas por ordem de preferência — e testar os dois caminhos, porque o visitante com navegador sem WebGPU verá a versão WebGL.

Não prometa o que o fallback não entrega

Se a experiência depende de um efeito que só existe em WebGPU, o visitante sem suporte vê uma versão pior ou quebrada. Desenhe a experiência para funcionar bem em WebGL 2 e trate o WebGPU como melhoria progressiva — o mesmo princípio que a web aplica a qualquer recurso novo. Para a API em si, veja WebGPU & GPU Compute.

Exercício 9.1 — Detecção

Escreva a verificação em JavaScript que diz se o navegador expõe WebGPU e obtém um adaptador, e explique por que "ter navigator.gpu" não basta.

Ver gabarito
async function temWebGPU() {
  if (!("gpu" in navigator)) return false;
  try { return !!(await navigator.gpu.requestAdapter()); }
  catch { return false; }
}

O objeto pode existir e, ainda assim, não haver adaptador disponível (GPU na lista de bloqueio, drivers, modo de economia). Só o requestAdapter() bem-sucedido confirma suporte real.

Capítulo 10 Avançado

Acessibilidade e a página ao redor do canvas

Para um leitor de tela e para um buscador, o canvas é uma caixa preta. Tudo o que importa precisa existir também fora dele.

10.1 O que o canvas esconde

O conteúdo desenhado no canvas não existe no DOM: não é lido por leitores de tela, não é indexado, não é traduzível pelo navegador, não aceita "buscar na página". Por isso, a arquitetura recomendada no cap. 4 — controles e informação em HTML, só o 3D no canvas — não é só conveniência: é o que torna a experiência acessível e encontrável.

10.2 Práticas

Critérios de acessibilidade em detalhe estão na apostila de Acessibilidade (WCAG).

Exercício 10.1 — Auditoria rápida

Passe um configurador Unity por um leitor de tela (VoiceOver ou NVDA) e pela navegação só por teclado. Liste o que não é possível fazer.

Ver o que costuma acontecer

Quase sempre: o canvas é anunciado como "gráfico" sem nome; opções que só existem na UI interna do Unity são inalcançáveis; mudanças de preço não são anunciadas; e o foco fica preso no canvas depois do primeiro clique. Cada item tem a correção listada em 10.2 — e todas ficam do lado da página, não do Unity.

Capítulo 11 Muito avançado

Medir, depurar e sobreviver em produção

Depois do lançamento, a pergunta deixa de ser "funciona?" e passa a ser "para quantos visitantes não funcionou, e onde eles desistiram?".

11.1 Eventos de analytics pela ponte

O Unity não precisa de um SDK de analytics próprio: emite eventos pela ponte e a página os entrega à ferramenta que o site já usa.

window.addEventListener("unity:analytics", (e) => {
  if (typeof gtag === "function")
    gtag("event", e.detail.nome, e.detail.params);   // respeita o modo de consentimento já configurado no site
});

Eventos que valem ouro: unity_carregamento_inicio, unity_pronto (com o tempo decorrido), unity_erro, e as ações de negócio (config_cor, config_concluida). A diferença entre quem começou a carregar e quem chegou a unity_pronto é a métrica mais honesta sobre o custo do Unity na página.

Consentimento

Eventos enviados pelo Unity são eventos do site como quaisquer outros: seguem o banner de cookies e o modo de consentimento. Não crie um canal paralelo de medição que ignore a escolha do visitante.

11.2 Depurar

11.3 Erros em produção

Capture as falhas no catch de createUnityInstance, em window.onerror e pelo evento unity:erro emitido a partir de Application.logMessageReceived no C# (filtrando por exceções), e envie a um serviço de monitoramento. As causas mais comuns, em ordem: falta de memória em celular, navegador sem WebGL 2 ou com GPU bloqueada, arquivos comprimidos mal servidos após um deploy, e versões misturadas de arquivos em cache (cap. 2).

Exercício 11.1 — O funil

Dados de uma semana: 10 000 visitantes viram a página, 9 200 iniciaram o carregamento, 6 100 chegaram a unity_pronto, 2 300 mudaram alguma configuração, 410 concluíram. Onde está o maior problema e o que você investigaria primeiro?

Ver gabarito

Entre iniciar o carregamento e ficar pronto perdem-se 3 100 pessoas (34%) — antes de verem qualquer coisa. É o maior vazamento e é técnico: segmentar por aparelho e rede, verificar tempo de carga e erros (memória no celular?). Só depois olhar a conversão de "pronto" para "configurou" (38%), que é pergunta de UX.

Capítulo 12 Prática

Projeto guiado: configurador na página

Um configurador de produto usa quase tudo desta apostila numa só peça. Construí-lo é o melhor exercício de integração — e um ótimo item de portfólio.

12.1 O roteiro

Passo 1 Decidir se é Unity (cap. 1) e medir o build vazio no celular
Passo 2 Página própria com createUnityInstance, barra de progresso e alternativa em imagens (caps. 2 e 10)
Passo 3 Contrato JSON versionado, objeto Ponte único, fila até o aviso de pronto (caps. 3 e 4)
Passo 4 Controles em HTML: cores, opcionais, preço — o canvas só para o 3D (caps. 4 e 10)
Passo 5 Estado na URL para compartilhar a configuração (exercício 4.2)
Passo 6 Canvas responsivo com DPR limitado, teclado liberado, rolagem respeitada (caps. 6 e 7)
Passo 7 Build mobile separado e teste num iPhone e num Android reais (cap. 8)
Passo 8 Eventos de analytics com consentimento e captura de erros (cap. 11)
Passo 9 Integração no framework do site com desmontagem segura (cap. 5)

12.2 Checklist de publicação

ItemVerificação
Compressão e cabeçalhosOs arquivos .br/.gz chegam com Content-Encoding correto (aba Rede do navegador)
CacheNomes com hash; um deploy novo nunca mistura versões
TecladoOs campos HTML da página digitam com o Unity carregado
MemóriaCinco navegações de ida e volta no iPhone sem a aba cair
FalhaCom WebGL desativado, a página mostra a alternativa e não uma área vazia
AcessibilidadeTodas as opções operáveis por teclado e anunciadas por leitor de tela
MediçãoEventos de carregamento, pronto e erro chegando, respeitando o consentimento

12.3 Projetos de portfólio

  1. Configurador completo seguindo o roteiro acima, com um README mostrando o funil e o tempo de carga medido em celular.
  2. O mesmo produto em Unity e em Three.js, com comparação medida (tamanho, tempo até interativo, memória no celular) e a sua recomendação — mostra que você sabe escolher, não só executar.
  3. Componente React reutilizável que encapsula loader, fila, eventos e desmontagem segura, publicado como pacote ou repositório.
  4. Visualização de dados em 3D alimentada por uma API, com os controles e a tabela de dados em HTML acessível.

12.4 Fontes para continuar

🏁 Síntese final da apostila

Quatro ideias sustentam Unity na web: (1) o Unity é uma peça da página, não a página — controles, texto e informação em HTML; só o 3D no canvas; (2) a ponte é um contrato — um objeto, JSON versionado, fila até o pronto, eventos na volta; (3) o celular define o projeto — pixels, memória, texturas e uma instância de cada vez; (4) o custo do Unity se mede no funil — quantos visitantes desistem antes do pronto é o número que decide se ele se paga. E antes de tudo: pergunte o que você perderia se reescrevesse em Three.js.