flowchart TD
ini["05_demo.ts chama importarAtivo('/ativos/morsa.gltf', ajuste)"] --> carga["new GLTFLoader().loadAsync(url)"]
carga --> bruta["new Box3().setFromObject(no)"]
bruta --> chegou["dimensoes(caixaBruta) devolve comoChegou: 18.00 x 12.00 x 17.00"]
chegou --> giro["1. orientacao DECLARADA: MathUtils.degToRad(ajuste.giroEmXGraus), -90 graus em X"]
giro --> remede["2. escala MEDIDA: maiorLado(dimensoes(new Box3().setFromObject(no))) devolve 18.00"]
remede --> zero{"medidoAposGirar === 0"}
zero -->|"true"| um["fator = 1: sem medida nao ha divisao possivel"]
zero -->|"false"| div["fator = ajuste.maiorLadoRealM / medidoAposGirar = 0.18 / 18.00 = 0.0100"]
um --> aplica["no.scale.setScalar(fator)"]
div --> aplica
aplica --> res["3. origem MEDIDA: caixaAjustada.getCenter(centro) sobre a caixa ja girada e escalada"]
res --> base{"ajuste.assentarNaBase"}
base -->|"true"| sobe["y da correcao = -caixaAjustada.min.y"]
base -->|"false"| nada["y da correcao = 0"]
sobe --> plano{"ajuste.centrarNoPlano"}
nada --> plano
plano -->|"true"| centra["x e z da correcao = -centro.x e -centro.z"]
plano -->|"false"| fica["x e z da correcao = 0"]
centra --> soma["no.position.add(correcao), deslocamento de 0.30 m"]
fica --> soma
soma --> ficou["dimensoes(new Box3().setFromObject(no)) devolve comoFicou: 18.0 x 17.0 x 12.0 cm"]
ficou --> censo["no.traverse conta o que chegou"]
censo --> emalha{"filho instanceof Mesh"}
emalha -->|"sim"| conta["contarTriangulos(filho.geometry) e materiais.add(item.name)"]
emalha -->|"nao"| segue["nada a somar neste no"]
conta --> censo
segue --> censo
censo -->|"arvore percorrida"| total["4 malhas, 48 triangulos, 2 materiais: FerroFundido e AcoUsinado"]
total --> mistura["new AnimationMixer(no), que avancar(delta) usara no laco"]
mistura --> saida["AtivoImportado com no, diagnostico, animacoes e avancar()"]
1 Ativos, formatos de troca e orçamento de cena — Projeto do Professor
Este é o projeto de referência resolvido pelo professor: a implementação que serve de modelo do que cada grupo deve produzir no Projeto Integrador. Ele existe para ser estudado, discutido e criticado — não para ser copiado. O que está aqui é uma solução defensável, com as decisões explicadas uma a uma; a solução de cada grupo será outra, e terá de ser defendida do mesmo jeito.
1.1 Visão geral
A oficina passa de 440 triângulos para mais de 7.000, e nenhum deles entra sem justificativa escrita.
Até aqui toda a forma da Bancada nasceu dentro do projeto. As cinco peças do mecanismo somavam 440 triângulos, e esse número pequeno era decisão, não acaso: ele existia para servir de linha de base.
Agora a linha de base é gasta. Entra conteúdo de fora, entra repetição em quantidade, e entra a medição que decide se o resultado cabe na máquina que a turma tem. Os três assuntos são de engenharia de desempenho, e nenhum deles se resolve com impressão sobre fluidez.
O código deste módulo ocupa três arquivos novos em content/, mais o ativo em public/. A cena do módulo anterior não é reescrita: ela ganha uma borda parafusada, um objeto importado e uma prateleira ao fundo.
1.2 O arquivo que chega não sabe o tamanho da peça
Um formato de troca aberto transporta geometria, material e animação — e não transporta a única informação que decide a escala.
O formato usado aqui fixa duas convenções na própria especificação: a unidade é o metro e o vertical é o eixo Y. Isso deveria encerrar o assunto. Não encerra, porque quem exporta nem sempre obedece, e o arquivo continua formalmente válido.
Um exportador que escreveu centímetro produz um arquivo correto e mentiroso ao mesmo tempo. Nenhum campo do cabeçalho denuncia a troca. O que denuncia é alguém saber que a peça tem 18 centímetros de boca e comparar esse número com o que veio.
A convenção desta cena, declarada uma vez
Unidade: metro. Vertical: eixo Y. Origem de cada peça: no ponto em que ela toca a superfície de apoio. Frente: o sentido positivo de Z.
Cena sem convenção escrita não tem contra o que acusar divergência. Cada ativo importado passa a trazer a sua, e a oficina vira uma colcha de escalas que ninguém consegue reconciliar depois.
1.3 Tarefa 1: Trazer ativos externos e ajustá-los ao projeto
A morsa chega deitada, gigante e a 30 centímetros de onde deveria estar — e os três defeitos são previsíveis.
Enunciado da tarefa
Parte do conteúdo da cena chega de fora, num formato de troca aberto que carrega geometria, materiais e animação. Trazer não basta: escala, orientação, unidade de medida e posição na hierarquia são decisões a tomar aqui, e um modelo importado sem ajuste costuma aparecer deitado, gigante ou a dez metros da origem.
O que fica pronto é o caminho completo entre o arquivo de origem e o objeto integrado à cena, com cada ajuste registrado. É esse caminho que distingue um ambiente construído de uma cena de demonstração aberta e publicada como está.
1.3.1 O ativo, e por que ele chega errado de propósito
A morsa aparafusada ao tampo é o ativo externo desta cena. Ela tem quatro malhas, dois materiais e uma animação de abrir e fechar o mordente móvel.
O arquivo foi autorado fora do projeto, e foi autorado em centímetros, com o vertical em Z e com a base 30 centímetros acima da origem. Os três desvios são deliberados. Eles são exatamente o que chega de uma ferramenta de modelagem configurada por outra pessoa, e um ativo perfeito não ensinaria a corrigir nada.
Quem monta a cena não controla a origem do conteúdo. Controla o que faz com ele na chegada, e é essa fronteira que o arquivo abaixo materializa.
src/bancada/content/ativos.ts
// ---------------------------------------------------------------------------
// O caminho entre um arquivo produzido fora e um objeto integrado à cena.
//
// Este arquivo existe porque "carregar" é a parte fácil e a parte curta. Uma
// linha pede o arquivo, a biblioteca devolve a árvore pronta, e a peça aparece
// deitada, gigante e a trinta centímetros de onde deveria estar. Nenhuma dessas
// três coisas é defeito do arquivo: são convenções diferentes entre quem autorou
// e quem recebe, e alguém precisa declarar de que lado a conversão acontece.
//
// A convenção desta cena está declarada abaixo, uma vez, e é contra ela que tudo
// o que chega é medido. Cena sem convenção escrita não tem como acusar
// divergência — cada ativo importado passa a trazer a sua, e a oficina vira uma
// colcha de escalas.
//
// A separação que este arquivo mantém: o que é MEDIDO e o que é DECLARADO. O
// fator de escala e o assentamento na base são derivados da caixa envolvente do
// que chegou, e por isso não têm como estar errados por distração. A orientação
// e o tamanho real da peça são declaração de quem importa, porque o arquivo não
// os carrega — o formato de troca fixa metro e vertical em Y, de modo que um
// exportador que escreveu centímetro produz um arquivo formalmente correto e
// materialmente mentiroso. Quem sabe que a morsa tem dezoito centímetros de boca
// é uma pessoa, não o cabeçalho.
// ---------------------------------------------------------------------------
import {
AnimationClip,
AnimationMixer,
Box3,
Material,
MathUtils,
Mesh,
Object3D,
Vector3,
} from 'three';
import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { contarTriangulos } from './malha';
/**
* A convenção da cena, escrita uma vez. Toda importação é medida contra ela, e é
* ela que este projeto não negocia com o ativo que chega.
*/
// recorte:inicio convencao-da-cena-declarada
export const CONVENCAO_DA_CENA = {
unidade: 'metro',
eixoVertical: 'Y',
origemDaPeca: 'no ponto em que ela toca a superfície de apoio',
frente: '+Z',
} as const;
// recorte:fim convencao-da-cena-declarada
// recorte:inicio escala-nao-se-digita
/**
* O que quem importa precisa declarar, porque o arquivo não responde.
*
* `maiorLadoRealM` é a medida do objeto no mundo, em metros, obtida por quem
* conhece a peça. Dela sai o fator de escala, por divisão contra o que foi
* medido no arquivo — e é por isso que o fator não é digitado: digitar 0.01
* funciona até o dia em que chega um ativo em milímetro.
*/
export interface AjusteDeImportacao {
readonly maiorLadoRealM: number;
// recorte:fim escala-nao-se-digita
/** Giro em torno de X, em graus, para trazer o vertical do autor ao vertical da cena. */
readonly giroEmXGraus: number;
/** Assenta a base do objeto no zero local do pai. Derivado da caixa envolvente. */
readonly assentarNaBase: boolean;
/** Centra o objeto no plano horizontal do pai. Também derivado. */
readonly centrarNoPlano: boolean;
}
export interface DimensoesM {
readonly largura: number;
readonly altura: number;
readonly profundidade: number;
}
/**
* O laudo da importação. Ele não existe para enfeitar o relatório: é o que
* permite a alguém conferir, sem abrir o arquivo, que a peça na cena tem o
* tamanho que a peça no mundo tem.
*/
// recorte:inicio diagnostico-como-chegou-como-ficou
export interface DiagnosticoDeImportacao {
readonly arquivo: string;
readonly comoChegou: DimensoesM;
readonly comoFicou: DimensoesM;
readonly fatorDeEscala: number;
readonly giroEmXGraus: number;
readonly deslocamentoCorrigidoM: Vector3;
readonly divergencias: readonly string[];
}
// recorte:fim diagnostico-como-chegou-como-ficou
export interface AtivoImportado {
readonly no: Object3D;
readonly diagnostico: DiagnosticoDeImportacao;
readonly animacoes: readonly AnimationClip[];
readonly malhas: number;
readonly triangulos: number;
readonly materiais: readonly string[];
/** Avança as animações do ativo. Chamado pelo laço, com o delta em segundos. */
avancar(delta: number): void;
/** Liga a animação de nome dado. Devolve falso quando ela não existe no arquivo. */
tocar(nome: string): boolean;
}
function dimensoes(caixa: Box3): DimensoesM {
const tamanho: Vector3 = new Vector3();
caixa.getSize(tamanho);
return { largura: tamanho.x, altura: tamanho.y, profundidade: tamanho.z };
}
function maiorLado(d: DimensoesM): number {
return Math.max(d.largura, d.altura, d.profundidade);
}
/**
* Traz o arquivo, mede o que chegou, aplica os ajustes e devolve o laudo.
*
* A ordem dos ajustes não é livre, e trocá-la põe o objeto no lugar errado sem
* qualquer aviso. Gira-se primeiro, escala-se depois e só então se mede o
* deslocamento residual: a caixa envolvente de um objeto deitado tem outra base
* que não a do mesmo objeto em pé, e assentar antes de girar assenta a peça pelo
* lado que vai virar lateral.
*/
export async function importarAtivo(
url: string,
ajuste: AjusteDeImportacao,
): Promise<AtivoImportado> {
const gltf: GLTF = await new GLTFLoader().loadAsync(url);
const no: Object3D = gltf.scene;
no.name = url.split('/').pop() ?? 'ativo';
const caixaBruta: Box3 = new Box3().setFromObject(no);
const comoChegou: DimensoesM = dimensoes(caixaBruta);
// 1. Orientação. Declarada, porque o formato de troca já fixa o vertical em Y
// e o arquivo não tem onde dizer que o autor usou outro.
no.rotation.x = MathUtils.degToRad(ajuste.giroEmXGraus);
no.updateMatrixWorld(true);
// 2. Escala. Medida: o maior lado do que chegou contra o maior lado real.
const medidoAposGirar: number = maiorLado(dimensoes(new Box3().setFromObject(no)));
const fator: number = medidoAposGirar === 0 ? 1 : ajuste.maiorLadoRealM / medidoAposGirar;
no.scale.setScalar(fator);
no.updateMatrixWorld(true);
// 3. Origem. Medida: onde a caixa envolvente ficou depois dos dois anteriores.
const caixaAjustada: Box3 = new Box3().setFromObject(no);
const centro: Vector3 = new Vector3();
caixaAjustada.getCenter(centro);
const correcao: Vector3 = new Vector3(
ajuste.centrarNoPlano ? -centro.x : 0,
ajuste.assentarNaBase ? -caixaAjustada.min.y : 0,
ajuste.centrarNoPlano ? -centro.z : 0,
);
no.position.add(correcao);
no.updateMatrixWorld(true);
const comoFicou: DimensoesM = dimensoes(new Box3().setFromObject(no));
let malhas: number = 0;
let triangulos: number = 0;
const materiais: Set<string> = new Set<string>();
no.traverse((filho: Object3D) => {
if (filho instanceof Mesh) {
malhas += 1;
triangulos += contarTriangulos(filho.geometry);
const material: Material | Material[] = filho.material;
for (const item of Array.isArray(material) ? material : [material]) {
materiais.add(item.name === '' ? '(sem nome)' : item.name);
}
}
});
const mistura: AnimationMixer = new AnimationMixer(no);
return {
no,
diagnostico: {
arquivo: url,
comoChegou,
comoFicou,
fatorDeEscala: fator,
giroEmXGraus: ajuste.giroEmXGraus,
deslocamentoCorrigidoM: correcao,
divergencias: apontarDivergencias(comoChegou, fator, ajuste, correcao),
},
animacoes: gltf.animations,
malhas,
triangulos,
materiais: [...materiais],
avancar(delta: number): void {
mistura.update(delta);
},
tocar(nome: string): boolean {
const trilha: AnimationClip | undefined = gltf.animations.find((a) => a.name === nome);
if (trilha === undefined) {
return false;
}
mistura.clipAction(trilha).play();
return true;
},
};
}
/**
* Nomeia, em uma frase por item, cada divergência entre o que chegou e a
* convenção da cena.
*
* O critério de divergente é de tolerância, e não de igualdade: um fator de
* 0,998 é ruído de exportação e não interessa a ninguém. Os limiares abaixo são
* frouxos de propósito — a lista existe para acusar troca de unidade e de eixo,
* que erram por ordens de grandeza, e não para reclamar de milímetro.
*/
function apontarDivergencias(
comoChegou: DimensoesM,
fator: number,
ajuste: AjusteDeImportacao,
correcao: Vector3,
): string[] {
const divergencias: string[] = [];
if (fator < 0.5 || fator > 2) {
const unidadesPorMetro: number = 1 / fator;
divergencias.push(
`Unidade divergente: o arquivo mede ${maiorLado(comoChegou).toFixed(2)} para uma peça de ` +
`${ajuste.maiorLadoRealM.toFixed(2)} m, o que dá ${unidadesPorMetro.toFixed(0)} unidades ` +
`por metro. O formato de troca fixa metro, e o exportador não obedeceu.`,
);
}
if (ajuste.giroEmXGraus !== 0) {
divergencias.push(
`Eixo vertical divergente: o ativo veio com o vertical em outro eixo, e foi girado ` +
`${ajuste.giroEmXGraus} graus em X para o vertical da cena ser ` +
`${CONVENCAO_DA_CENA.eixoVertical}.`,
);
}
if (correcao.length() > 0.01) {
divergencias.push(
`Origem divergente: a peça vinha deslocada ${correcao.length().toFixed(2)} m da própria ` +
`base, e a correção a assentou na superfície de apoio.`,
);
}
if (divergencias.length === 0) {
divergencias.push('Nenhuma divergência acima da tolerância: o ativo chegou na convenção.');
}
return divergencias;
}
/** As linhas do laudo, para o relatório da página e para a conferência em tutoria. */
export function linhasDoAtivo(ativo: AtivoImportado): string[] {
const d: DiagnosticoDeImportacao = ativo.diagnostico;
const cm = (v: number): string => (v * 100).toFixed(1);
return [
`Arquivo: ${d.arquivo}`,
`Como chegou: ${d.comoChegou.largura.toFixed(2)} x ${d.comoChegou.altura.toFixed(2)} x ` +
`${d.comoChegou.profundidade.toFixed(2)} unidades do arquivo`,
`Como ficou: ${cm(d.comoFicou.largura)} x ${cm(d.comoFicou.altura)} x ` +
`${cm(d.comoFicou.profundidade)} cm`,
`Fator de escala aplicado: ${d.fatorDeEscala.toFixed(4)} (medido, não digitado)`,
`Conteúdo: ${ativo.malhas} malhas, ${ativo.triangulos} triângulos, ` +
`${ativo.materiais.length} materiais (${ativo.materiais.join(', ')})`,
`Animações no arquivo: ${
ativo.animacoes.length === 0
? 'nenhuma'
: ativo.animacoes.map((a) => `${a.name} (${a.duration.toFixed(1)} s)`).join(', ')
}`,
...d.divergencias,
];
}1.3.2 O que é medido e o que é declarado
A divisão que sustenta este arquivo tem duas colunas, e confundi-las é o defeito mais comum da importação feita às pressas.
Do lado medido ficam o fator de escala e o assentamento na base. Os dois saem da caixa envolvente do que chegou, por divisão e por subtração. Não há como errá-los por distração, e trocar o ativo por outro os recalcula sozinho.
Do lado declarado ficam a orientação e o tamanho real da peça. O arquivo não os carrega, e não tem onde carregá-los. Quem sabe que a morsa tem 18 centímetros de boca é uma pessoa, e essa pessoa precisa escrever o número em algum lugar.
Repare no que isso evita. Digitar 0.01 como fator de escala funciona até o dia em que chega um ativo em milímetro, e nesse dia o defeito é silencioso: a peça aparece dez vezes maior e alguém a corrige à mão, num terceiro lugar.
1.3.3 A ordem dos ajustes, que não é livre
Gira-se primeiro, escala-se depois, e só então se mede o deslocamento residual.
A razão é geométrica e cabe em uma frase. A caixa envolvente de um objeto deitado tem outra base que não a do mesmo objeto em pé. Assentar antes de girar assenta a peça pelo lado que vai virar lateral, e o resultado é um objeto flutuando ou enterrado — sem erro, sem aviso, e com todos os números do laudo parecendo corretos.
1.3.4 O laudo, e por que ele existe
Cada importação devolve um laudo com o que chegou, o que ficou e o que foi corrigido. Ele não é enfeite de relatório.
O laudo é o que permite a alguém conferir, sem abrir o arquivo, que a peça na cena tem o tamanho que a peça no mundo tem. Na tutoria, é a primeira coisa a pedir: um grupo que importou e não sabe dizer o fator aplicado não importou, arrastou.
A tolerância do laudo é frouxa de propósito. Um fator de 0,998 é ruído de exportação e não interessa a ninguém; a lista existe para acusar troca de unidade e de eixo, que erram por ordens de grandeza.
1.3.5 Onde é fácil errar, e como conferir
O erro previsível aqui é a animação com relógio próprio.
A trilha que veio dentro do arquivo é avançada pelo mesmo intervalo que move o resto da cena. Fosse por um relógio dela, andaria mais rápido na máquina que desenha mais rápido, e a peça importada passaria a viver noutro tempo. O sintoma aparece só quando alguém compara dois computadores, o que raramente acontece antes da apresentação.
A conferência do ajuste é de olho e leva segundos: a morsa tem de encostar no tampo, e não atravessá-lo nem pairar sobre ele. A conferência do laudo é de leitura, e ela é a que a tutoria cobra.
1.4 Tarefa 2: Tratar repetição como repetição
206 parafusos idênticos custam 206 conversas com a placa, ou uma só — e a diferença não aparece na contagem de triângulos.
Enunciado da tarefa
Uma cena com quatrocentos parafusos pode custar quatrocentas chamadas de desenho ou uma só. Objetos repetidos passam a ser desenhados como repetição, e os que existem para serem vistos de longe ganham versão simplificada.
A tarefa fecha quando a cena completa do domínio escolhido está montada com esses dois recursos aplicados onde eles compensam, e com o critério de onde compensa escrito. Simplificar o objeto que fica sempre perto do olho é trabalho jogado fora.
src/bancada/content/repeticao.ts
// ---------------------------------------------------------------------------
// Repetição tratada como repetição, e detalhe cobrado por distância.
//
// Os dois recursos deste arquivo resolvem problemas diferentes e são
// confundidos o tempo todo. Instanciar ataca a CHAMADA DE DESENHO: duzentos e
// vinte parafusos idênticos são duzentas e vinte conversas com a placa, e cada
// conversa custa mesmo quando o objeto é minúsculo. Nível de detalhe ataca o
// TRIÂNGULO: o que está longe não entrega ao olho a densidade que tem, e paga
// por ela do mesmo jeito.
//
// Quem confunde os dois simplifica o parafuso — que já era barato em triângulo e
// caro em chamada — e instancia o que aparece uma vez só. Os dois trabalhos
// somem no ruído da medição, e o quadro continua estourado.
//
// O critério deste arquivo, e ele é uma frase: instancia-se o que se repete, e
// simplifica-se o que está longe do olho. A mesma engrenagem aparece nos dois
// lados dessa frase — a da bancada, que a mão vai apanhar, não recebe nível de
// detalhe nenhum; as vinte e quatro da prateleira do fundo recebem, e são a
// mesma malha. O que decide é a distância, não o objeto.
// ---------------------------------------------------------------------------
import {
BufferGeometry,
Camera,
Group,
InstancedMesh,
Material,
Matrix4,
Object3D,
Quaternion,
Vector3,
} from 'three';
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
import { contarTriangulos, contarVertices } from './malha';
/** Espaçamento entre parafusos ao longo da borda do tampo, em metros. */
export const ESPACAMENTO_DOS_PARAFUSOS: number = 0.02;
/** Distância, em metros, a partir da qual a prateleira do fundo troca de nível. */
export const DISTANCIA_DE_TROCA: number = 2.2;
/**
* Posições ao longo do retângulo da borda de uma superfície, em coordenadas
* locais dela. A conta é de perímetro dividido por espaçamento, e o número de
* parafusos sai daí em vez de ser digitado — trocar o tampo por um maior passa a
* acrescentar parafusos sozinho, que é o comportamento que se espera de uma
* borda parafusada.
*/
export function disporNaBorda(
largura: number,
profundidade: number,
espacamento: number,
recuo: number,
altura: number,
): Vector3[] {
const x: number = largura / 2 - recuo;
const z: number = profundidade / 2 - recuo;
const porLado: number = Math.max(2, Math.floor((x * 2) / espacamento));
const porTopo: number = Math.max(2, Math.floor((z * 2) / espacamento));
const posicoes: Vector3[] = [];
for (let i: number = 0; i < porLado; i += 1) {
const t: number = -x + (i * (x * 2)) / (porLado - 1);
posicoes.push(new Vector3(t, altura, -z));
posicoes.push(new Vector3(t, altura, z));
}
for (let i: number = 1; i < porTopo - 1; i += 1) {
const t: number = -z + (i * (z * 2)) / (porTopo - 1);
posicoes.push(new Vector3(-x, altura, t));
posicoes.push(new Vector3(x, altura, t));
}
return posicoes;
}
export interface CustoDaRepeticao {
readonly objetos: number;
readonly chamadasSemInstanciar: number;
readonly chamadasInstanciando: number;
readonly triangulosPorObjeto: number;
readonly triangulosTotais: number;
}
// recorte:inicio matriz-escrita-uma-vez
/**
* Monta um lote instanciado a partir de uma malha e de uma lista de posições.
*
* A matriz de cada cópia é escrita uma vez, na montagem, e não a cada quadro. O
* aviso de que a matriz não muda mais é o que autoriza a biblioteca a mandar o
* lote inteiro para a placa e não voltar a tocá-lo — e é ele que transforma
* duzentas e vinte chamadas em uma.
*/
export function montarLoteInstanciado(
// recorte:fim matriz-escrita-uma-vez
geometria: BufferGeometry,
material: Material,
posicoes: readonly Vector3[],
nome: string,
): InstancedMesh {
const lote: InstancedMesh = new InstancedMesh(geometria, material, posicoes.length);
lote.name = nome;
const matriz: Matrix4 = new Matrix4();
const semGiro: Quaternion = new Quaternion();
const semEscala: Vector3 = new Vector3(1, 1, 1);
for (let i: number = 0; i < posicoes.length; i += 1) {
const posicao: Vector3 | undefined = posicoes[i];
if (posicao === undefined) {
continue;
}
matriz.compose(posicao, semGiro, semEscala);
lote.setMatrixAt(i, matriz);
}
lote.instanceMatrix.needsUpdate = true;
return lote;
}
/**
* O que a instanciação comprou, em número.
*
* A chamada de desenho é a unidade certa aqui, e não o triângulo: os triângulos
* continuam os mesmos, um por um, e a placa os desenha igual. O que sai da conta
* é a conversa — o custo fixo de preparar cada objeto, que independe de quão
* pequeno ele seja e que é justamente o que faz a cena de mil parafusos
* minúsculos engasgar sem que a contagem de triângulos assuste ninguém.
*/
export function custoDaRepeticao(
geometria: BufferGeometry,
quantidade: number,
): CustoDaRepeticao {
const porObjeto: number = contarTriangulos(geometria);
return {
objetos: quantidade,
chamadasSemInstanciar: quantidade,
chamadasInstanciando: 1,
triangulosPorObjeto: porObjeto,
triangulosTotais: porObjeto * quantidade,
};
}
export interface NivelSimplificado {
readonly geometria: BufferGeometry;
readonly triangulos: number;
readonly vertices: number;
/** Falso quando a ferramenta não entregou resultado utilizável e a malha original ficou. */
readonly simplificou: boolean;
readonly observacao: string;
}
/**
* Simplifica uma malha pela ferramenta, e não à mão.
*
* Dois passos, e o primeiro é o que quase todo mundo esquece. A malha da oficina
* duplica vértice em toda face, de propósito, para ter aresta viva. Para o
* algoritmo de simplificação, vértices duplicados são pontos que não se tocam:
* ele enxerga a peça como um monte de triângulos soltos e não tem que colapsar.
* Unir os vértices coincidentes antes é o que devolve a ele uma superfície.
*
* Unir custa a aresta viva, e no nível distante isso é exatamente o que se quer
* pagar: a três metros, ninguém distingue canto duro de canto macio, e a
* diferença é toda em triângulo.
*
* O resultado é conferido. A ferramenta pode devolver malha vazia ou com
* coordenada não finita em geometrias que ela não digere, e uma peça que some da
* cena é um defeito muito pior do que um nível distante mais pesado que o ideal.
*/
export function simplificar(
geometria: BufferGeometry,
proporcaoRemovida: number,
): NivelSimplificado {
const unida: BufferGeometry = mergeVertices(geometria);
const aRemover: number = Math.floor(contarVertices(unida) * proporcaoRemovida);
if (aRemover < 1) {
return {
geometria,
triangulos: contarTriangulos(geometria),
vertices: contarVertices(geometria),
simplificou: false,
observacao: 'malha pequena demais para valer a simplificação; o original ficou',
};
}
const candidata: BufferGeometry = new SimplifyModifier().modify(unida, aRemover);
const posicoes: ArrayLike<number> = candidata.getAttribute('position').array;
let naoFinitos: number = 0;
for (let i: number = 0; i < posicoes.length; i += 1) {
if (!Number.isFinite(posicoes[i] ?? Number.NaN)) {
naoFinitos += 1;
}
}
if (contarTriangulos(candidata) === 0 || naoFinitos > 0) {
candidata.dispose();
return {
geometria,
triangulos: contarTriangulos(geometria),
vertices: contarVertices(geometria),
simplificou: false,
observacao: 'a ferramenta não devolveu malha utilizável; o original ficou no lugar',
};
}
// A normal precisa ser recalculada: os triângulos que sobraram têm outra
// inclinação, e manter a normal antiga produz iluminação que não corresponde à
// forma — o defeito é sutil e aparece como mancha, não como buraco.
candidata.computeVertexNormals();
candidata.computeBoundingSphere();
candidata.computeBoundingBox();
candidata.name = `${geometria.name}-simplificada`;
return {
geometria: candidata,
triangulos: contarTriangulos(candidata),
vertices: contarVertices(candidata),
simplificou: true,
observacao: 'simplificada pela ferramenta, com as normais recalculadas',
};
}
export interface Prateleira {
readonly no: Object3D;
readonly quantidade: number;
readonly triangulosPerto: number;
readonly triangulosLonge: number;
readonly nivel: NivelSimplificado;
/** Escolhe o nível pela distância da câmera. Devolve verdadeiro quando está no nível distante. */
atualizar(camera: Camera): boolean;
linhas(): string[];
}
/**
* A prateleira de peças de reposição do fundo da oficina: as duas técnicas
* juntas, que é como elas aparecem em ambiente real.
*
* Cada nível é um lote instanciado inteiro, e a troca liga um e desliga o outro.
* Alternar o objeto um a um, como faz o mecanismo pronto de nível de detalhe,
* devolveria uma chamada de desenho por peça e desfaria a instanciação — as duas
* técnicas se anulariam, e o ganho de uma pagaria a perda da outra.
*
* A troca é por distância da câmera ao centro da prateleira, e não por peça. A
* prateleira tem meio metro de largura: a diferença de distância entre a peça
* mais próxima e a mais distante dela não muda a decisão.
*/
export function montarPrateleira(
geometria: BufferGeometry,
material: Material,
quantidade: number,
proporcaoRemovida: number,
): Prateleira {
const nivel: NivelSimplificado = simplificar(geometria, proporcaoRemovida);
const posicoes: Vector3[] = [];
const porFileira: number = 6;
for (let i: number = 0; i < quantidade; i += 1) {
const coluna: number = i % porFileira;
const fileira: number = Math.floor(i / porFileira);
posicoes.push(new Vector3(-0.25 + coluna * 0.1, 0.02 + fileira * 0.18, 0));
}
const perto: InstancedMesh = montarLoteInstanciado(
geometria,
material,
posicoes,
'reposicao-perto',
);
const longe: InstancedMesh = montarLoteInstanciado(
nivel.geometria,
material,
posicoes,
'reposicao-longe',
);
longe.visible = false;
const no: Group = new Group();
no.name = 'prateleira-de-reposicao';
no.add(perto);
no.add(longe);
const centro: Vector3 = new Vector3();
const daCamera: Vector3 = new Vector3();
return {
no,
quantidade,
triangulosPerto: contarTriangulos(geometria) * quantidade,
triangulosLonge: nivel.triangulos * quantidade,
nivel,
atualizar(camera: Camera): boolean {
no.getWorldPosition(centro);
camera.getWorldPosition(daCamera);
const distante: boolean = daCamera.distanceTo(centro) > DISTANCIA_DE_TROCA;
perto.visible = !distante;
longe.visible = distante;
return distante;
},
linhas(): string[] {
const economia: number =
this.triangulosPerto === 0
? 0
: ((this.triangulosPerto - this.triangulosLonge) / this.triangulosPerto) * 100;
return [
`Peças de reposição na prateleira: ${quantidade}, em dois níveis instanciados`,
`Nível de perto: ${contarTriangulos(geometria)} triângulos por peça, ` +
`${this.triangulosPerto} no total`,
`Nível de longe: ${nivel.triangulos} triângulos por peça, ${this.triangulosLonge} no total ` +
`(${economia.toFixed(0)}% a menos)`,
`Troca a ${DISTANCIA_DE_TROCA.toFixed(1)} m da câmera — ${nivel.observacao}`,
'A engrenagem da bancada é a mesma malha e não recebe nível nenhum: ela fica a meio metro ' +
'do olho, e simplificar o que está perto é trabalho jogado fora.',
];
},
};
}
/** As linhas do lote de parafusos, para o relatório e para a tutoria. */
export function linhasDaRepeticao(custo: CustoDaRepeticao): string[] {
return [
`Parafusos na borda: ${custo.objetos}, a cada ` +
`${(ESPACAMENTO_DOS_PARAFUSOS * 100).toFixed(0)} cm de perímetro`,
`Chamadas de desenho sem instanciar: ${custo.chamadasSemInstanciar}`,
`Chamadas de desenho instanciando: ${custo.chamadasInstanciando}`,
`Triângulos: ${custo.triangulosPorObjeto} por parafuso, ${custo.triangulosTotais} no total — ` +
'e este número não muda com a instanciação, porque ela não é sobre triângulo',
];
}1.4.1 Duas técnicas, dois problemas diferentes
Instanciar e simplificar são confundidos o tempo todo, e o motivo é que ambos aparecem sob o rótulo de otimização de cena.
Instanciar ataca a chamada de desenho. Cada objeto desenhado é uma conversa com a placa, com custo fixo que independe do tamanho do objeto. É por isso que uma cena de mil parafusos minúsculos engasga sem que a contagem de triângulos assuste ninguém.
Simplificar ataca o triângulo. O que está longe não entrega ao olho a densidade que tem, e paga por ela do mesmo jeito em todo quadro.
Quem troca os dois simplifica o parafuso, que já era barato em triângulo, e instancia o que aparece uma vez só. Os dois trabalhos somem no ruído da medição, e o quadro continua estourado.
1.4.2 A borda parafusada, e o número que ela produz
O tampo mede 1,4 por 0,8 metro, e os parafusos ficam a cada 2 centímetros de perímetro. Dessa conta saem 206 cabeças sextavadas, e o número não é digitado: trocar o tampo por um maior acrescenta parafusos sozinho.
Cada cabeça tem 24 triângulos, o que dá 4.944 no lote inteiro. Esse número não muda com a instanciação, e dizer isso em voz alta em aula economiza meia dúvida. O que muda é a coluna ao lado: 206 chamadas de desenho passam a ser uma.
O parafuso é a cabeça e nada mais. O corpo dele está dentro da madeira, e modelar o que ninguém vê é orçamento gasto em nada.
1.4.3 A mesma engrenagem, duas decisões opostas
A prateleira do fundo guarda 24 peças de reposição. Elas são a mesma malha da engrenagem que está sobre a bancada, e essa identidade é o argumento inteiro.
A engrenagem da bancada fica a meio metro do olho e a mão vai apanhá-la. Ela não recebe nível de detalhe nenhum. As 24 da prateleira ficam a quase três metros, e recebem. O que decide é a distância.
A ferramenta corta a malha de 224 para 82 triângulos, uma redução de 63% que a três metros ninguém enxerga. Nas 24 peças, isso são 5.376 triângulos que viram 1.968.
1.4.4 Unir vértices antes de simplificar
Por que o algoritmo de simplificação não funciona direto na nossa malha?
A malha da oficina duplica vértice em toda face, de propósito, para ter aresta viva. Para o algoritmo, vértices duplicados são pontos que não se tocam: ele enxerga a peça como um monte de triângulos soltos e não encontra aresta nenhuma para colapsar.
Unir os vértices coincidentes antes devolve a ele uma superfície. Unir custa a aresta viva, e no nível distante isso é exatamente o que se quer pagar — a três metros ninguém distingue canto duro de canto macio, e a diferença é toda em triângulo.
Depois de simplificar, as normais são recalculadas. Os triângulos que sobraram têm outra inclinação, e manter a normal antiga produz iluminação que não corresponde à forma. O defeito é sutil e aparece como mancha, não como buraco.
1.4.5 Onde é fácil errar, e como conferir
O erro previsível desta tarefa é usar o mecanismo pronto de nível de detalhe por objeto sobre um lote instanciado.
Ele alterna a malha peça por peça, e ao fazer isso devolve uma chamada de desenho por peça. As duas técnicas se anulam, o ganho de uma paga a perda da outra, e o relatório mostra o mesmo número de antes. Aqui cada nível é um lote inteiro, e a troca liga um e desliga o outro.
A conferência é o botão que aproxima a câmera da prateleira. A contagem de triângulos sobe ao chegar perto e desce ao afastar, e o número na tela é a prova de que a troca acontece.
A ferramenta pode falhar, e a falha precisa ser tratada. O algoritmo devolve malha vazia ou com coordenada não finita em geometrias que ele não digere. Uma peça que some da cena é defeito mais grave do que um nível distante mais pesado que o ideal, e por isso o resultado é conferido antes de entrar. Quando não passa, a malha original fica no lugar e a observação vai para o relatório — em vez de sumir do relatório junto com a peça.
1.5 Tarefa 3: Medir o custo em número
“Está rodando bem” é uma frase sem sujeito, e é a resposta que a tutoria recusa.
Enunciado da tarefa
Impressão sobre desempenho não sustenta decisão alguma. Triângulos e chamadas de desenho passam a ser contados, e o número passa a caber num teto declarado antes da medição.
O que fica pronto é a medição registrada junto da máquina em que foi obtida, tomando a máquina mais modesta prevista como referência. Um número sem a máquina ao lado não permite a ninguém repetir a conta.
src/bancada/content/medicao.ts
// ---------------------------------------------------------------------------
// A medição, e a máquina ao lado do número.
//
// Este arquivo separa duas contas que costumam ser dadas como a mesma. O CENSO é
// estático: percorre a árvore da cena e soma o que existe, independentemente de
// a câmera estar olhando. Ele responde "quanto esta cena pesa". A LEITURA do
// orçamento é dinâmica e vem do módulo do laço: conta o que a placa desenhou no
// quadro que acabou de sair. Ela responde "quanto custou o que estava à vista".
//
// A diferença entre os dois números não é erro: é o descarte do que ficou fora
// do campo de visão. Um censo alto com leitura baixa é uma cena grande vista por
// um buraco de fechadura, e vira problema no instante em que alguém gira a
// cabeça — o que, num visor, acontece o tempo todo.
//
// A máquina viaja junto do número por uma razão prosaica: "roda a sessenta" é
// uma frase sem sujeito. Sessenta na máquina de quem tem placa dedicada e vinte
// e dois na do laboratório é o mesmo ambiente, e a decisão que se toma a partir
// dos dois é oposta.
// ---------------------------------------------------------------------------
import { InstancedMesh, Material, Mesh, Object3D, WebGLRenderer } from 'three';
import { contarTriangulos } from './malha';
import type { LeituraDoOrcamento } from '../core/orcamento';
/**
* A máquina contra a qual esta cena é orçada. É a mais modesta do parque
* previsto, e não a do professor.
*
* Medir na máquina mais forte da sala produz um teto que a turma não alcança, e
* o resultado prático é o ambiente estourando justamente nos computadores em que
* ele vai ser apresentado.
*/
// recorte:inicio maquina-de-referencia-declarada
export const MAQUINA_DE_REFERENCIA = {
descricao: 'desktop x64 do laboratório, com vídeo integrado e sem placa dedicada',
navegador: 'Chrome com WebGL 2 habilitado',
observacao:
'é o limite duro do parque: nenhum número deste ambiente pode depender de placa dedicada',
} as const;
// recorte:fim maquina-de-referencia-declarada
// recorte:inicio censo-conta-lote-nao-instancia
export interface Censo {
readonly malhas: number;
readonly lotesInstanciados: number;
readonly instancias: number;
readonly triangulos: number;
readonly materiaisDistintos: number;
/** Piso de chamadas de desenho: uma por malha, uma por lote — não uma por instância. */
readonly chamadasPrevistas: number;
}
// recorte:fim censo-conta-lote-nao-instancia
/**
* Percorre a árvore e soma o que existe.
*
* O lote instanciado conta como UMA chamada e como o produto de triângulos por
* instância. É essa assimetria que a instanciação compra, e é ela que o censo
* precisa expor separada — somar tudo num número só esconderia exatamente a
* decisão que o módulo ensina a tomar.
*/
export function censoDaCena(raiz: Object3D): Censo {
let malhas: number = 0;
let lotes: number = 0;
let instancias: number = 0;
let triangulos: number = 0;
const materiais: Set<string> = new Set<string>();
raiz.traverse((no: Object3D) => {
if (!(no instanceof Mesh)) {
return;
}
const porMalha: number = contarTriangulos(no.geometry);
if (no instanceof InstancedMesh) {
lotes += 1;
instancias += no.count;
triangulos += porMalha * no.count;
} else {
malhas += 1;
triangulos += porMalha;
}
const material: Material | Material[] = no.material;
for (const item of Array.isArray(material) ? material : [material]) {
materiais.add(item.uuid);
}
});
return {
malhas,
lotesInstanciados: lotes,
instancias,
triangulos,
materiaisDistintos: materiais.size,
chamadasPrevistas: malhas + lotes,
};
}
/**
* O que o navegador conta sobre a placa, quando conta.
*
* A extensão que revela o nome do adaptador é opcional e frequentemente
* bloqueada por privacidade, e nesse caso a resposta honesta é dizer que não se
* sabe. Inventar um nome de placa num relatório de desempenho é pior do que não
* ter nome nenhum: quem lê passa a acreditar que a medição é reproduzível.
*/
export function identificarPlaca(renderer: WebGLRenderer): string {
const contexto: WebGLRenderingContext | WebGL2RenderingContext = renderer.getContext();
const extensao: unknown = contexto.getExtension('WEBGL_debug_renderer_info');
if (extensao === null || typeof extensao !== 'object') {
return 'placa não identificada (o navegador não expõe o nome do adaptador)';
}
const chave: unknown = (extensao as Record<string, unknown>)['UNMASKED_RENDERER_WEBGL'];
if (typeof chave !== 'number') {
return 'placa não identificada (o navegador não expõe o nome do adaptador)';
}
const nome: unknown = contexto.getParameter(chave);
return typeof nome === 'string' && nome !== '' ? nome : 'placa não identificada';
}
/**
* As linhas da medição: o censo, a leitura e a máquina, na mesma folha.
*
* Estar na mesma folha é o requisito, e não detalhe de apresentação. Número de
* desempenho sem a máquina ao lado não permite a ninguém repetir a conta, e um
* relatório que não se repete não é medição — é impressão com casas decimais.
*/
export function linhasDaMedicao(
censo: Censo,
leitura: LeituraDoOrcamento,
placa: string,
): string[] {
const linhas: string[] = [
`Máquina de referência: ${MAQUINA_DE_REFERENCIA.descricao}`,
`Navegador previsto: ${MAQUINA_DE_REFERENCIA.navegador}`,
`Placa relatada por este aparelho: ${placa}`,
`Teto declarado por quadro: ${leitura.tetoMs.toFixed(1)} ms`,
'',
`Censo estático da cena: ${censo.triangulos} triângulos em ${censo.malhas} malhas soltas e ` +
`${censo.lotesInstanciados} lotes instanciados (${censo.instancias} instâncias)`,
`Chamadas de desenho previstas pelo censo: ${censo.chamadasPrevistas}`,
`Materiais distintos: ${censo.materiaisDistintos}`,
];
if (leitura.quadrosMedidos === 0) {
linhas.push('', 'Ainda sem quadros medidos: a leitura dinâmica entra depois do primeiro quadro.');
return linhas;
}
const folga: number = leitura.tetoMs - leitura.intervaloMedioMs;
linhas.push(
'',
`Medido em ${leitura.quadrosMedidos} quadros: ${leitura.chamadasDeDesenho} chamadas e ` +
`${leitura.triangulos} triângulos efetivamente desenhados`,
`Intervalo médio: ${leitura.intervaloMedioMs.toFixed(1)} ms — ` +
`${folga >= 0 ? `${folga.toFixed(1)} ms de folga` : `${(-folga).toFixed(1)} ms acima do teto`}`,
`Pior intervalo da janela: ${leitura.piorIntervaloMs.toFixed(1)} ms`,
`Diferença entre censo e medição: ${censo.triangulos - leitura.triangulos} triângulos ficaram ` +
'fora do campo de visão neste quadro, e voltam assim que a câmera girar',
);
return linhas;
}1.5.1 Censo e leitura são contas diferentes
O censo é estático. Ele percorre a árvore da cena e soma o que existe, esteja ou não à vista. Responde quanto esta cena pesa.
A leitura é dinâmica e vem do laço. Conta o que a placa desenhou no quadro que acabou de sair. Responde quanto custou o que estava no campo de visão.
A diferença entre os dois é o descarte do que ficou fora do enquadramento. Um censo alto com leitura baixa é uma cena grande vista por um buraco de fechadura, e vira problema no instante em que alguém gira a cabeça — o que, dentro de um visor, acontece o tempo todo.
O censo também separa o lote instanciado do resto. Um lote conta como uma chamada e como o produto de triângulos por instância. Somar tudo num número só esconderia justamente a decisão que este módulo ensina a tomar.
1.5.2 O envelope, agora com o que gastá-lo
Cada quadro é um envelope de onze milissegundos, e o que não couber nele não fica para depois: some.
O envelope foi declarado antes de existir conteúdo pesado a que atribuir culpa. Era um teto sem nada contra o que ser gasto. Agora há: mais de 7.000 triângulos, um ativo importado com animação própria e dois lotes instanciados.
O teto adotado continua sendo o do visor, e não o do computador de mesa. A mesma cena vai subir nos dois, e orçar pelo aparelho mais folgado garante estouro no outro — justamente aquele em que o estouro produz mal-estar físico, e não só uma animação menos lisa.
1.5.3 A máquina viaja junto do número
Sessenta quadros por segundo na máquina de quem tem placa dedicada e vinte e dois na do laboratório são o mesmo ambiente. A decisão que se toma a partir de cada um é oposta.
A máquina de referência declarada aqui é a mais modesta do parque previsto: o computador de mesa do laboratório, com vídeo integrado e sem placa dedicada. Medir na máquina mais forte da sala produz um teto que a turma não alcança, e o ambiente estoura exatamente nos computadores em que vai ser apresentado.
O nome da placa é pedido ao navegador, e o navegador frequentemente recusa por privacidade. Quando recusa, o relatório diz que não sabe. Inventar um nome de placa é pior do que não ter nome nenhum: quem lê passa a acreditar que a medição é reproduzível.
1.5.4 Onde é fácil errar, e como conferir
O erro previsível é medir a média e parar aí.
A média dilui o engasgo até ele desaparecer do número, e o engasgo é o que o corpo sente. Por isso a leitura traz também o pior intervalo da janela observada e a proporção de quadros acima do teto. Uma cena com média confortável e 12% dos quadros estourados é desconfortável no visor, e o relatório precisa dizer isso.
A conferência é o botão de medir com a câmera em duas posições: junto à bancada e junto à prateleira. Se os dois números forem iguais, alguma coisa não está sendo contada.
1.6 Por dentro da implementação
As seções acima explicam as decisões. Esta explica o percurso: qual função chama qual, com que valor, e o que volta. A execução de referência é a página deste módulo com a morsa chegando, e os números citados abaixo são os que ela imprimiu.
| alteração | o que ela passou a fazer |
|---|---|
importacao-medida |
um arquivo autorado fora entra na cena com escala, orientação e origem resolvidas na chegada |
laudo-de-divergencias |
cada desvio entre o que chegou e a convenção vira uma frase, por tolerância |
censo-estatico |
a árvore inteira passa a ser contada, separando malha solta de lote |
medicao-com-maquina |
o número de desempenho passa a sair acompanhado do aparelho que o produziu |
A costura com o que já existia cabe em duas frases. importarAtivo() devolve um nó, e quem o pendura é cena.bancada.add(), o mesmo agrupador que montarCena() criou no módulo da estrutura — nenhum nó é reparentado aqui. E censoDaCena() percorre cena.sala, isto é, a árvore que vestirCena() acabou de vestir, sem saber de nada que este módulo tenha acrescentado.
1.6.1 A chegada, medida e não digitada
O cenário é a morsa. importarAtivo('/ativos/morsa.gltf', ...) recebe maiorLadoRealM 0,18 e giroEmXGraus −90; a caixa envolvente do que veio mede 18,00 × 12,00 × 17,00 unidades do arquivo, e o objeto sai da função medindo 18,0 × 17,0 × 12,0 cm.
O fator aparece numa divisão só: ajuste.maiorLadoRealM / medidoAposGirar, com 0,18 sobre 18,00, que dá 0,0100. Repare de onde vem o denominador — maiorLado(dimensoes(...)) é chamado depois do giro, sobre uma caixa remedida, e não sobre a caixa bruta guardada em comoChegou. É essa segunda medição que faz a ordem dos três ajustes ser obrigatória em vez de recomendada.
sequenceDiagram
autonumber
participant demo as demos/05_demo.ts
participant ativos as content/ativos.ts
participant loader as GLTFLoader
participant malha as content/malha.ts
demo->>ativos: importarAtivo('/ativos/morsa.gltf', maiorLadoRealM 0.18, giroEmXGraus -90)
ativos->>loader: loadAsync('/ativos/morsa.gltf')
loader-->>ativos: GLTF com scene, 4 malhas e a trilha AbrirFechar
ativos->>ativos: dimensoes(caixaBruta)
ativos-->>ativos: comoChegou 18.00 x 12.00 x 17.00 unidades do arquivo
ativos->>ativos: MathUtils.degToRad(-90) e no.updateMatrixWorld(true)
ativos->>ativos: maiorLado(dimensoes(caixa apos girar))
ativos-->>ativos: 18.00, o maior lado ja em pe
ativos->>ativos: no.scale.setScalar(0.18 / 18.00)
ativos-->>ativos: fator 0.0100
ativos->>ativos: caixaAjustada.getCenter(centro) e no.position.add(correcao)
ativos-->>ativos: correcao de 0.30 m, medida depois de girar e escalar
loop um no por vez, na arvore do ativo
ativos->>malha: contarTriangulos(filho.geometry)
malha-->>ativos: triangulos daquela malha
end
ativos-->>demo: AtivoImportado: 48 triangulos, comoFicou 18.0 x 17.0 x 12.0 cm
demo->>demo: cena.bancada.add(importado.no)
A troca de eixo aparece no resultado sem estar escrita em lugar nenhum: 12,00 e 17,00 chegam nessa ordem e saem trocadas, porque o giro de −90° em X leva a profundidade para a altura. Quem confere o laudo em tutoria tem aí a evidência de que o giro aconteceu.
A guarda medidoAposGirar === 0, e o que a divisão nua custaria. A alternativa é dividir direto. O custo é um objeto vazio ou degenerado, que dá caixa de lado zero e fator infinito: escalar por infinito não lança exceção, e a peça some da cena sem uma linha de erro. Com a guarda, o fator vira 1 e o objeto fica visível no tamanho em que veio — errado, mas à vista.
Os quatro campos de AjusteDeImportacao, e o que um só custaria. A alternativa é receber uma matriz de correção pronta, o que é mais curto e é o que a maioria dos exemplos faz. O custo é que a matriz mistura numa coisa só o que foi medido e o que foi declarado: quem trocar o ativo por outro precisa recalcular a matriz inteira à mão, em vez de trocar um número de dezoito centímetros.
O contador de malhas dentro de importarAtivo(), e o que adiá-lo custaria. A alternativa é deixar censoDaCena() responder também por isso, já que ele percorre a árvore de qualquer jeito. O custo é perder a separação entre o que o ativo trouxe — 4 malhas, 48 triângulos, 2 materiais — e o que a cena tem no total: com um número só, ninguém consegue dizer se o orçamento estourou por causa do arquivo importado ou do resto.
1.6.2 As três divergências, por tolerância
O cenário é o mesmo, e a saída é a que a página imprime nas três últimas linhas do laudo: unidade divergente com 100 unidades por metro, eixo vertical divergente com o giro de −90°, e origem divergente com 0,30 m de deslocamento.
flowchart TD
ini["importarAtivo() monta o diagnostico e chama apontarDivergencias(comoChegou, fator, ajuste, correcao)"] --> uni{"fator < 0.5 || fator > 2"}
uni -->|"0.0100, fora da faixa"| conta["unidadesPorMetro = 1 / fator = 100"]
uni -->|"dentro da faixa"| pulaUni["nada a acusar sobre unidade"]
conta --> msgUni["'Unidade divergente: o arquivo mede 18.00 para uma peca de 0.18 m, o que da 100 unidades por metro'"]
msgUni --> eixo{"ajuste.giroEmXGraus !== 0"}
pulaUni --> eixo
eixo -->|"-90"| msgEixo["'Eixo vertical divergente: girado -90 graus em X para o vertical da cena ser Y', lido de CONVENCAO_DA_CENA.eixoVertical"]
eixo -->|"0"| pulaEixo["o autor ja usava o vertical da cena"]
msgEixo --> org{"correcao.length() > 0.01"}
pulaEixo --> org
org -->|"0.30 m"| msgOrg["'Origem divergente: a peca vinha deslocada 0.30 m da propria base'"]
org -->|"abaixo da tolerancia"| pulaOrg["ruido de exportacao, que a lista nao reclama"]
msgOrg --> vazia{"divergencias.length === 0"}
pulaOrg --> vazia
vazia -->|"true"| ok["'Nenhuma divergencia acima da tolerancia: o ativo chegou na convencao'"]
vazia -->|"false, tres itens"| lista["as tres frases, uma por divergencia"]
ok --> laudo["linhasDoAtivo(ativo) monta a folha"]
lista --> laudo
laudo --> cm["cm(v) converte comoFicou para centimetro; comoChegou fica na unidade do arquivo"]
cm --> fim["nove linhas na tela, terminadas pelas divergencias"]
Os três testes de apontarDivergencias() são independentes, e nenhum interrompe o seguinte: a lista sai com três itens porque as três condições deram verdadeiro no mesmo ativo. O último teste existe para o caso oposto — sem ele, um ativo correto produziria uma folha que termina em branco, e branco lê-se como “ninguém conferiu”.
sequenceDiagram autonumber participant demo as demos/05_demo.ts participant ativos as content/ativos.ts participant tela as div id ativo ativos->>ativos: apontarDivergencias(comoChegou, 0.0100, ajuste, correcao) ativos-->>ativos: 'Unidade divergente: 100 unidades por metro' ativos-->>ativos: 'Eixo vertical divergente: girado -90 graus em X' ativos-->>ativos: 'Origem divergente: deslocada 0.30 m da propria base' ativos-->>demo: DiagnosticoDeImportacao com tres divergencias demo->>ativos: linhasDoAtivo(importado) ativos->>ativos: cm(d.comoFicou.largura) ativos-->>ativos: '18.0', ja em centimetro ativos-->>demo: 'Fator de escala aplicado: 0.0100 (medido, nao digitado)' e mais oito linhas demo->>tela: escrever(raizAtivo, linhas)
linhasDoAtivo() mostra as duas colunas na mesma folha e em unidades diferentes de propósito: comoChegou fica na unidade do arquivo, e comoFicou passa por cm() antes de ser escrito. Pôr as duas em metro esconderia justamente o que se quer que salte aos olhos.
A faixa fator < 0.5 || fator > 2, e o que a igualdade custaria. A alternativa é acusar sempre que o fator for diferente de 1. O custo é uma folha em que todo ativo tem divergência de unidade, inclusive o correto que veio com 0,998 por arredondamento de exportação — e uma lista que acusa sempre deixa de ser lida na segunda semana.
Anunciar a divergência de eixo a partir de ajuste.giroEmXGraus, e o que detectá-la custaria. A alternativa é inferir o eixo vertical do próprio arquivo, comparando as proporções da caixa envolvente. O custo é adivinhação: uma peça mais larga que alta é comum e legítima, e o palpite erraria em silêncio. Aqui a frase só é escrita porque alguém declarou o giro, e ela relata uma decisão em vez de fingir uma medição.
1.6.3 O censo, que conta lote e não instância
O cenário é o clique em Medir agora, com a morsa já pendurada. censoDaCena(cena.sala) devolve 524 triângulos em 12 malhas soltas, 0 lotes instanciados, 8 materiais distintos e 12 chamadas previstas — as quatro malhas da morsa somadas às oito que a cena já tinha.
flowchart TD
ini["clique em 'Medir agora' chama censoDaCena(cena.sala)"] --> anda["raiz.traverse, um no por vez"]
anda --> ehMalha{"no instanceof Mesh"}
ehMalha -->|"false"| volta["nada a somar: luz, grupo ou eixo"]
volta --> anda
ehMalha -->|"true"| pm["porMalha = contarTriangulos(no.geometry)"]
pm --> lote{"no instanceof InstancedMesh"}
lote -->|"true"| somaLote["lotes += 1, instancias += no.count, triangulos += porMalha * no.count"]
lote -->|"false"| somaSolta["malhas += 1, triangulos += porMalha"]
somaLote --> mat["materiais.add(item.uuid), uma vez por material do no"]
somaSolta --> mat
mat --> anda
anda -->|"arvore percorrida"| saida["Censo: 524 triangulos, 12 malhas soltas, 0 lotes, 8 materiaisDistintos"]
saida --> prev["chamadasPrevistas = malhas + lotes = 12, uma por lote e nao uma por instancia"]
O ramo que decide está em no instanceof InstancedMesh, e ele é testado depois de no instanceof Mesh, não em vez dele: um lote também é uma malha, e inverter os dois testes faria todo lote cair na coluna das malhas soltas. chamadasPrevistas sai de malhas + lotes, sem instancias em lugar nenhum da soma — é essa linha que expõe o que a instanciação compra.
sequenceDiagram
autonumber
participant demo as demos/05_demo.ts
participant med as content/medicao.ts
participant sala as cena.sala
participant malha as content/malha.ts
demo->>med: censoDaCena(cena.sala)
med->>sala: traverse do grafo montado em montarCena() e vestido por vestirCena()
loop um no por vez
sala-->>med: no, ja com a morsa que importarAtivo() pendurou em cena.bancada
med->>malha: contarTriangulos(no.geometry)
malha-->>med: triangulos daquela malha
end
med-->>demo: Censo com 524 triangulos, 12 malhas, 0 lotesInstanciados, chamadasPrevistas 12
Contar material por identidade, e o que contar por nome custaria. A alternativa é usar o nome do material, que é legível e é o que se quer no relatório. O custo é contar dois materiais idênticos em valor como um só, e o número deixaria de responder à pergunta que importa: quantas trocas de estado a placa vai fazer.
Percorrer a árvore a cada clique, e o que um contador incremental custaria. A alternativa é manter os totais atualizados a cada nó adicionado. O custo é um número que diverge em silêncio na primeira vez que alguém remover uma peça sem avisar o contador — e a árvore desta cena tem doze malhas, de modo que a travessia inteira é mais barata que o defeito que ela evita.
1.6.4 A medição, com a máquina na mesma folha
O cenário é o mesmo clique. orcamento.ler() devolve 120 quadros medidos, o intervalo médio e o pior intervalo da janela, contra o teto de 16,7 ms que o computador de mesa declara; os dois intervalos variam com a máquina que roda a página, e por isso nenhum valor deles é transcrito aqui; identificarPlaca(palco.renderer) devolve o que aquele navegador quis dizer.
flowchart TD
ini["clique em 'Medir agora' chama identificarPlaca(palco.renderer)"] --> ctx["renderer.getContext() e getExtension('WEBGL_debug_renderer_info')"]
ctx --> temExt{"extensao === null || typeof extensao !== 'object'"}
temExt -->|"true"| naoSabe["'placa nao identificada (o navegador nao expoe o nome do adaptador)'"]
temExt -->|"false"| chave["chave = extensao['UNMASKED_RENDERER_WEBGL']"]
chave --> ehNum{"typeof chave !== 'number'"}
ehNum -->|"true"| naoSabe
ehNum -->|"false"| nome["contexto.getParameter(chave)"]
nome --> ehStr{"typeof nome === 'string' && nome !== ''"}
ehStr -->|"true"| revela["o nome relatado por este aparelho"]
ehStr -->|"false"| naoSabe
revela --> folha["linhasDaMedicao(censo, orcamento.ler(), placa)"]
naoSabe --> folha
folha --> cab["MAQUINA_DE_REFERENCIA.descricao, .navegador e o teto de 16.7 ms na mesma folha"]
cab --> temQuadro{"leitura.quadrosMedidos === 0"}
temQuadro -->|"true"| adiar["'Ainda sem quadros medidos: a leitura dinamica entra depois do primeiro quadro'"]
temQuadro -->|"false, 120 quadros"| folga["folga = leitura.tetoMs - leitura.intervaloMedioMs"]
folga --> sinal{"folga >= 0"}
sinal -->|"true"| ms["'<folga> ms de folga'"]
sinal -->|"false"| estouro["'acima do teto', com o sinal invertido para nao imprimir menos"]
ms --> pior["pior intervalo da janela, que a media esconderia"]
estouro --> pior
pior --> dif["censo.triangulos - leitura.triangulos = 524 - 524 = 0 fora do campo de visao"]
identificarPlaca() tem três saídas para o mesmo desconhecimento, e todas as três dizem a mesma frase: extensão ausente, chave que não é número, e nome vazio. Escrever a recusa por extenso, em vez de devolver cadeia vazia, é o que impede a folha de sair com um campo em branco que cada leitor preenche de cabeça.
sequenceDiagram autonumber participant botao as botaoMedir participant demo as demos/05_demo.ts participant med as content/medicao.ts participant orc as core/orcamento.ts participant palco as core/palco.ts botao->>demo: click, depois de a cena ja ter desenhado quadros demo->>orc: orcamento.ler() orc-->>demo: LeituraDoOrcamento: 120 quadrosMedidos, intervalo medio e pior intervalo da janela demo->>palco: identificarPlaca(palco.renderer) palco-->>demo: o nome que o navegador entregou, ou a recusa dita por extenso demo->>med: linhasDaMedicao(censo, leitura, placa) med-->>demo: 'Teto declarado por quadro: 16.7 ms' e 'Intervalo medio: <medio> ms - <folga> ms de folga' demo->>demo: escrever(raizCusto, linhas)
linhasDaMedicao() monta o cabeçalho — máquina, navegador, placa e teto — antes de olhar para leitura.quadrosMedidos. É por isso que a folha nunca sai sem a máquina ao lado, inclusive no caso em que ainda não há quadro medido e a função retorna cedo.
A saída antecipada em quadrosMedidos === 0, e o que a conta protegida custaria. A alternativa é calcular a média com uma guarda contra divisão por zero e imprimir 0,0 ms. O custo é uma linha que se lê como medição excelente quando é ausência de medição, e essa é a leitura que alguém vai levar para a apresentação.
A folga como subtração impressa, e o que o percentual custaria. A alternativa é dizer que sobrou um quarto do orçamento. O custo é perder a unidade em que a decisão é tomada: quem precisa acrescentar sombra ao quadro quer saber quantos milissegundos tem, e não que fração deles sobrou.
1.6.5 O que a página deste módulo ainda não exercita
A instanciação da borda e a troca de nível por distância estão escritas no código deste módulo, e as seções acima as explicam. A página de demonstração não passa por elas: quem monta a borda parafusada e a prateleira é a composição do ambiente completo, que não pertence a módulo nenhum do percurso. Sem execução não há cenário honesto, e por isso disporNaBorda(), montarLoteInstanciado(), simplificar() e montarPrateleira() não recebem diagrama aqui — recebem quando a demonstração que as exercita existir.
1.7 O que este módulo deixa pronto
A cena está completa, e pela primeira vez existe um número contra o qual as próximas decisões vão ser julgadas.
A oficina agora tem conteúdo de três procedências: geometria autorada dentro do projeto, um ativo trazido de fora e ajustado, e repetição tratada como repetição. O eixo de modelagem fecha aqui.
O que sobrevive deste módulo para todos os seguintes é a medição. A partir do módulo da câmera orbital entram interação, sessão imersiva e ancoragem, e cada um deles acrescenta trabalho por quadro. Sem a linha de base medida agora, a primeira queda de desempenho será atribuída ao último recurso adicionado — que quase nunca é o culpado.