Português

Documentação xr/studio

Entidade Custom Code

Construa uma entidade com o seu próprio código: o scope do Three.js, params com controlos no inspector, carregar assets, interfaces a partir de markup e screen space.

Entidade Custom Code

Uma entidade Custom Code é uma entidade que constrói com o seu próprio código em vez do inspector. Fica na cena como qualquer outra entidade, e tudo o que faz de uma entidade uma entidade continua a aplicar-se: tem transform, pode ser escondida, pode ser animada na timeline, pode ter filhos, e pode ser transformada num prefab.

Para adicionar, use Add Entities → Advanced → Custom Code. Vem com um script funcional, por isso consegue ver a forma do bloco antes de mudar seja o que for.

A diferença para uma ação Custom Code está em quando corre. Uma ação dispara uma vez num evento. O bloco de uma entidade corre quando a entidade aparece, pode voltar a correr em cada frame, e limpa tudo quando a entidade desaparece.

O que o seu código recebe

O Three.js já está em scope como THREE. Nunca importe a sua própria cópia: um segundo Three.js quebra as verificações instanceof em toda a cena.

Em scopeO que é
THREEA instância de Three.js da aplicação
groupO objeto da própria entidade. Tudo o que constrói pertence aqui
paramsValores do painel Params, cada um com um controlo no inspector
entityEsta entidade, para o nome, id e tags
scene, camera, rendererO scene graph ativo
loadersLoaders de GLTF, texturas e HDR já configurados
uikitml()Transforma markup numa interface
cyangoA mesma API da história que uma ação Custom Code recebe, incluindo cyango.utils.getAssetUrl()
console, timersLogging com namespace, e timers que se limpam sozinhos

A forma de um bloco

const geometry = new THREE.TorusKnotGeometry(0.4, 0.14, 128, 32);
const material = new THREE.MeshStandardMaterial({ color: params.color });
const mesh = new THREE.Mesh(geometry, material);

group.add(mesh);

return {
  update: (delta, elapsed) => {
    mesh.rotation.y += delta * params.speed;
  },
  dispose: () => {
    geometry.dispose();
    material.dispose();
  },
};

As duas funções devolvidas são opcionais. Um bloco que só constrói geometria estática pode não devolver nada.

  • update(delta, elapsed) corre em cada frame. delta são os segundos desde o último frame, elapsed os segundos desde o início da história.
  • dispose() corre antes do bloco voltar a correr e quando a entidade desaparece.

Tudo o que deixar em group é removido e libertado por si, incluindo geometrias, materiais e texturas. Escreva dispose para o que criou fora do group, como um render target. Os timers que iniciar são limpos automaticamente.

Params

Cada chave em Params torna-se um controlo no inspector, por isso uma história pode ser afinada sem abrir o código. Os params são uma lista, e cada entrada carrega o seu valor e o seu controlo:

[
  { "key": "color", "value": "#6366f1", "label": "Cor" },
  { "key": "speed", "value": 1, "type": "SLIDER", "min": 0, "max": 5, "step": 0.1 }
]

Só key é obrigatório. Deixe type de fora e é inferido a partir do valor: um número dá um campo numérico, um booleano um switch, uma string #rrggbb um seletor de cor, qualquer outra string um campo de texto, e [x, y, z] três campos numéricos.

Defina type quando quiser algo que o valor não consegue implicar. Os tipos disponíveis são NUMBER, SLIDER, BOOLEAN, TEXT, COLOR, SELECT e VECTOR3, refinados com label, help, min, max, step e options.

Mudar qualquer param volta a correr o bloco todo, por isso o seu código lê sempre os valores atuais.

Carregar assets

Atribua um asset no painel Asset e leia-o com cyango.utils.getAssetUrl(). O loader de GLTF já traz os mesmos decoders DRACO, KTX2 e meshopt que a entidade 3D Model usa.

const url = cyango.utils.getAssetUrl();
if (!url) return;

const gltf = await loaders.gltf.loadAsync(url);
group.add(gltf.scene);

const mixer = new THREE.AnimationMixer(gltf.scene);
if (gltf.animations[0]) mixer.clipAction(gltf.animations[0]).play();

return { update: (delta) => mixer.update(delta) };

cyango.utils.getAssetUrl() sem argumento devolve o asset da própria entidade. Passe um id de asset para chegar a qualquer outro asset da história.

Note que um asset carregado desta forma não segura o loading screen da cena, por isso um modelo grande aparece depois da cena já estar interativa.

Interfaces a partir de markup

uikitml() transforma um subconjunto de HTML numa interface 3D:

await uikitml(`
  <div style="flex-direction: column; padding: 20px; background-color: #111827">
    <span style="color: white; font-size: 20px">${params.title}</span>
  </div>
`);

As tags são div, p, span, li, h1–h6, ol, ul, a, button, img, svg, video, input e textarea. Qualquer outra falha a leitura do markup e a interface inteira é ignorada, com o motivo registado na consola do browser.

Os estilos são escritos com um valor por propriedade: padding: 12px 20px não é aceite, por isso use padding-x e padding-y, ou um único padding. Os nomes das propriedades são os do uikit escritos com traços, ou seja background-color e flex-direction, não backgroundColor.

Mantenha todas as propriedades no style do próprio elemento. Um bloco <style> e um atributo class são aceites mas nunca aplicados, por isso o elemento fica sem estilo.

O resultado é anexado, atualizado e libertado por si, por isso não o adicione nem o atualize manualmente.

Para pôr uma interface no ecrã, arraste a entidade para cima da entidade Screen na árvore da cena. O mesmo markup passa a ser desenhado à frente da câmara, com o layout feito pelo GUI. Deixada na raiz da cena, é desenhada em world space na própria entidade.

Só o markup se move entre os dois. Uma mesh precisa de world space, porque um parent de GUI faz layout de componentes de interface e não tem caixa para um objeto 3D simples.

No ecrã, a caixa da própria entidade começa transparente, por isso não bloqueia os cliques destinados à cena que está por trás, e os elementos que o seu markup constrói mantêm os cliques deles de qualquer forma. Para forçar num sentido ou no outro, use Pointer → Clicks no inspetor em vez do markup. Veja Entidades de interface para saber como essa definição funciona.

Chegar à história

cyango é o mesmo objeto que uma ação Custom Code recebe, por isso um bloco pode ler o estado da história, mudar de cena, ou partilhar valores com ações através de variáveis globais. Veja a API Custom Code para o namespace completo.

cyango.utils.setGlobalVar('spawnedAt', Date.now());

Quando usar outra coisa

As entidades incorporadas cobrem quase tudo o que uma história precisa, e vêm com quality levels, física, localização e suporte de prefabs que o seu código teria de reproduzir. Recorra a uma entidade Custom Code quando as incorporadas mesmo não chegam: instanciar milhares de objetos, shaders personalizados, geometria generativa, ou uma interface gerada a partir de dados.

Para um único modelo animado, use a entidade 3D Model. Para uma interface que desenha à mão, use as entidades GUI.