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.
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
| Ferramenta | O que é | Tamanho típico da primeira carga | Brilha em |
|---|---|---|---|
<model-viewer> | Componente web que mostra um glTF com rotação, AR e anotações | Muito pequeno (a biblioteca + o modelo) | "Ver o produto em 3D" sem lógica |
| Three.js | Biblioteca JavaScript de renderização 3D | Pequeno; cresce com o que você usa | Experiências de marca, efeitos, integração fina com o DOM |
| Babylon.js / PlayCanvas | Motores 3D nativos da web, com editor (PlayCanvas) e física | Pequeno a médio | Jogos e apps 3D feitos para a web desde o início |
| Unity (build Web) | O motor completo compilado para WebAssembly | Vários MB mesmo num projeto vazio | Ló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?
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.
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.
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.
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:
| Arquivo | Papel |
|---|---|
*.loader.js | O script que você inclui na página. Define createUnityInstance |
*.framework.js | O código JavaScript de suporte do runtime (incluindo o que vier dos seus .jslib) |
*.wasm | O seu C# e o motor, compilados para WebAssembly |
*.data | Cenas 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.
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.
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.
JavaScript (página)
C# (Unity)
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.
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.
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.
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.
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.
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.
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" });
});
- Strings passam como ponteiros. O que chega ao
.jslibé um número (endereço na memória do WebAssembly); semUTF8ToString, você vê números estranhos em vez do texto. - No editor,
__Internalnão existe. Sem o#if UNITY_WEBGL && !UNITY_EDITORe uma implementação alternativa, o Play Mode lançaEntryPointNotFoundException. - Exceções no .jslib derrubam a instância. Um
JSON.parsede texto inválido dentro do plugin pode interromper o runtime. Envolva o que pode falhar emtry/catche reporte o erro, em vez de deixar propagar.
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.
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
- O Unity precisa de
window,canvase WebAssembly: nunca renderiza no servidor. Marque o componente com"use client"e carregue-o comdynamic(() => import("./Configurador"), { ssr: false }). - Coloque a pasta
Build/empublic/(ou numa CDN) e confira os cabeçalhos de compressão no servidor ou na plataforma de hospedagem — um arquivo.brservido semContent-Encodingé a falha mais comum depois do deploy. - Mais sobre o framework em Next.js.
5.3 Desmontar sem vazar
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.
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
| Tela | devicePixelRatio | Pixels renderizados num canvas de 400×300 CSS |
|---|---|---|
| Monitor comum | 1 | 120 000 |
| Notebook de alta densidade | 2 | 480 000 (4×) |
| Celular topo de linha | 3 | 1 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.
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
| Recurso | Exige gesto do usuário? | Consequência |
|---|---|---|
| Áudio | Sim (política de autoplay) | Sem clique/toque prévio, o som fica mudo; comece silencioso e ofereça um botão "ativar som" |
| Tela cheia | Sim | Cap. 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ão | O 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.
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
- Memória do WebAssembly: as Player Settings controlam o tamanho inicial e máximo da memória (heap). Um máximo generoso demais pode falhar a alocação no celular; um inicial pequeno com crescimento gradual costuma ser mais seguro.
- Formato de textura: desktops usam formatos DXT/BC; celulares, ASTC ou ETC2. Uma textura num formato que a GPU não suporta é descomprimida pelo navegador e ocupa várias vezes mais memória. Gere o build com o formato do público principal, ou builds diferentes para desktop e mobile servidos conforme o aparelho.
- Menos coisa carregada ao mesmo tempo: Addressables e carregamento por partes — detalhados em Serious Games na Web, cap. 8.
- Uma instância por página (cap. 5.3) — duas instâncias raramente cabem num celular.
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.
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 2 | WebGPU | |
|---|---|---|
| Modelo | Máquina de estados do OpenGL ES 3.0 | API moderna, próxima de Vulkan, Metal e Direct3D 12 |
| Compute shaders | Não | Sim — partículas na GPU, simulação, pós-processamento avançado |
| Custo por draw call na CPU | Alto | Menor, com pipelines pré-compilados |
| Suporte | Praticamente universal | Chrome 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.
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.
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
- Nomeie a região: o contêiner do canvas com
role="region"e umaria-label("Visualizador 3D do sofá, girável"). - Anuncie mudanças: um elemento com
aria-live="polite", atualizado pela página quando o Unity emite eventos ("Cor alterada para vermelho. Preço: R$ 4.290"). - Tudo operável sem o 3D: toda escolha feita arrastando no canvas precisa de um equivalente em controle HTML.
- Movimento reduzido: leia
matchMedia("(prefers-reduced-motion: reduce)")na página e envie ao Unity pela ponte, para desligar rotação automática e animações de câmera. - Alternativa sem WebGL: se o carregamento falhar, mostre imagens do produto e o conteúdo em HTML — nunca uma área vazia.
- Busca e compartilhamento: título, descrição, texto do produto e imagem de pré-visualização vivem no HTML; o 3D é um complemento.
Critérios de acessibilidade em detalhe estão na apostila de Acessibilidade (WCAG).
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.
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
Debug.Logvai para o console do navegador. Em builds de desenvolvimento, as mensagens vêm com pilha de chamadas mais legível.- Development Build + Autoconnect Profiler permite ligar o Profiler do editor ao build rodando no navegador.
- O painel de memória e de desempenho das ferramentas de desenvolvedor do navegador mostra o que o Profiler do Unity não vê: o tempo de download e compilação, o JavaScript da página, o layout.
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).
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 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
| Item | Verificação |
|---|---|
| Compressão e cabeçalhos | Os arquivos .br/.gz chegam com Content-Encoding correto (aba Rede do navegador) |
| Cache | Nomes com hash; um deploy novo nunca mistura versões |
| Teclado | Os campos HTML da página digitam com o Unity carregado |
| Memória | Cinco navegações de ida e volta no iPhone sem a aba cair |
| Falha | Com WebGL desativado, a página mostra a alternativa e não uma área vazia |
| Acessibilidade | Todas as opções operáveis por teclado e anunciadas por leitor de tela |
| Medição | Eventos de carregamento, pronto e erro chegando, respeitando o consentimento |
12.3 Projetos de portfólio
- Configurador completo seguindo o roteiro acima, com um README mostrando o funil e o tempo de carga medido em celular.
- 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.
- Componente React reutilizável que encapsula loader, fila, eventos e desmontagem segura, publicado como pacote ou repositório.
- 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
- Manual do Unity, secção da plataforma Web: interação com o navegador (
.jslib,SendMessage), templates, configurações de memória, WebGPU. - react-unity-webgl: documentação da versão que for usar.
- MDN: WebAssembly, Canvas, WebGPU, políticas de autoplay e Fullscreen API.
- Na Uniana: Serious Games na Web (tamanho e carregamento), Unity para Aplicações, WebGPU, Three.js.
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.