Incorporar o viewport de cena 3D
Incorpore o viewport de cena 3D do Nodaro em um iframe e controle-o com postMessage: handshake, mensagens de estado, eventos de edição, assets e limites.
O viewport de cena 3D é o visualizador 3D de pré-visualização do Nodaro, que você pode incorporar em um iframe dentro do seu próprio app, em /embed/scene3d. É o mesmo visualizador que o editor do Nodaro usa: um viewport three.js, controles de reprodução, uma lista de objetos, editores numéricos de pose e um histórico de revisões. O iframe é controlado inteiramente por postMessage: sua página guarda a cena, e o iframe a desenha e informa o que o usuário fez.
Esta não é a incorporação de miniapp. Um miniapp executa um workflow publicado; o viewport não executa nada. Veja Incorporações para a diferença, e Formato de cena 3D para os dados que ele desenha.
O que o iframe faz e não faz
| Lê | Só o parentOrigin e o channel da própria URL, e as mensagens state dessa origem exata. |
| Desenha | A cena que você envia: o viewport, a reprodução e a navegação na linha do tempo, a lista de objetos, os editores de pose de cada objeto e da câmera, o histórico de revisões e o aviso de revisão pendente. Para uma cena pré-calculada (versão 2): a câmera pré-calculada, a navegação entre tomadas, as entidades semânticas com as cores de identificação delas e os controles de sobreposição. |
| Envia | ready quando começa a escutar, depois um event por ação do usuário e um asset-request por asset declarado de uma cena da versão 2. |
| Nunca faz | Autenticar-se, ler ou gravar no armazenamento, chamar um endpoint /v1, guardar algo de um carregamento para outro, ou avançar a própria visualização depois de uma edição. |
O iframe é sem estado por design. Quando o usuário confirma uma edição, o iframe envia para você uma nova revisão imutável e continua mostrando a antiga até que você envie o resultado de volta. Um iframe que se adiantasse à página pai poderia mostrar uma revisão que nunca chega a ser salva.
O iframe não guarda nenhum segredo e pode ser carregado com segurança em uma página que você não controla totalmente. Os dados de cena que você envia a ele só são tão privados quanto a origem para a qual você os endereça, então sempre envie para uma origem exata; veja o checklist da página pai.
Verificar se o viewport está disponível
Em uma implantação que inclui o viewport, o nó Gerar cena 3D (Generate 3D Scene) anuncia o recurso scene3d-embed-v1 em GET /v1/nodes. Verifique esse recurso antes de oferecer o editor incorporado.
A URL
https://app.nodaro.ai/embed/scene3d?parentOrigin=<origin>&channel=<uuid>| Parâmetro | Obrigatório | Regra |
|---|---|---|
parentOrigin | Sim | A origem exata e normalizada da página que incorpora o iframe: um esquema, um host e uma porta opcional, nada mais. new URL(value).origin precisa ser igual ao valor. Só http: e https:, até 255 caracteres. Uma barra no final, um caminho, uma query, informações de usuário, null ou qualquer outro esquema são recusados. |
channel | Sim | Um UUID aleatório novo, de crypto.randomUUID(), um por iframe montado. Ele impede que dois viewports na mesma página, ou um iframe antigo de uma caixa de diálogo fechada, redesenhem um ao outro. |
MessageEvent.origin chega normalizado pelo navegador. Exigir o parâmetro com a mesma grafia permite que o iframe compare os dois exatamente, em vez de adivinhar. Se um parâmetro estiver ausente ou malformado, o iframe mostra um erro, não envia nada e não escuta nada.
O handshake
- Sua página adiciona o iframe com a URL dele.
- O iframe é montado e começa a escutar. Ele envia
nodaro:scene3d:readye repete o envio até você responder. - Sua página responde com uma mensagem
nodaro:scene3d:state. O iframe valida a cena e a desenha. - Quando o usuário age, o iframe envia um
nodaro:scene3d:event. - Sua página verifica
expectedRevisionId, aplica o evento e envia um novostate: a nova verdade.
Envie o seu primeiro state em resposta ao ready, não no evento load do iframe. O load dispara antes de o listener do iframe estar pronto, então uma mensagem enviada nesse momento pode ser descartada. O iframe repete o ready em um intervalo curto e limitado, o que cobre uma página pai que começa a escutar um pouco tarde. Ele para depois da sua primeira mensagem endereçada a ele, mesmo uma que ele recuse: uma recusa ainda prova que você está escutando.
Da página pai para o iframe: state
Existe um único tipo de mensagem, e ele carrega um snapshot completo. Não existe atualização parcial: sempre envie o estado completo que você quer desenhado.
{
type: "nodaro:scene3d:state",
version: 1 | 2, // 2 enables baked (version 2) scenes, see below
channel: string, // must equal the channel in the URL
scenePlan: Scene3DPlan, // required, validated
selectedObjectIds?: string[], // default []
lockedObjectIds?: string[], // default []
history?: Scene3DRevisionEntry[], // default []
pendingPlan?: Scene3DPlan, // default absent
isGenerating?: boolean, // default false
readOnly?: boolean, // default TRUE
}Uma entrada de histórico:
{
revisionId: string, // UUID
scenePlan: Scene3DPlan, // validated like the active plan
source: "generate" | "edit" | "manual" | "upstream",
createdAt: string, // your timestamp, shown as it is
changeSummary?: string,
context?: { prompt?: string }, // only prompt is kept
}scenePlan é o Scene3DPlan público do pacote @nodaro/shared: o mesmo formato que um job de Gerar cena 3D ou de Editar cena 3D (Edit 3D Scene) retorna em output_data.scenePlan.
readOnlyvem comotruepor padrão. O silêncio significa: olhe, mas não toque. Um iframe editável precisa dizerreadOnly: false.contextmantém sóprompt, que vira a dica (tooltip) do botão de restaurar. Outras chaves dentro decontextsão descartadas em silêncio. Chaves desconhecidas em qualquer outro lugar são recusadas; veja Versionamento.isGenerating: truesó adiciona um aviso de que uma revisão está sendo gerada. A cena continua ativa e, quando não é somente leitura, editável.pendingPlancobre um job que terminou depois que o usuário editou. O iframe mostra um aviso e, quando editável, dois botões para escolher uma revisão.
O que o iframe ignora e o que ele recusa
| O iframe recebe | Ele |
|---|---|
Uma mensagem cujo event.source não é a janela que incorpora o iframe | Ignora a mensagem, em silêncio. |
Uma mensagem cujo event.origin não é parentOrigin | Ignora a mensagem, em silêncio. |
Outro type, outro channel ou um payload que não é um objeto | Ignora a mensagem, em silêncio. |
Uma version desconhecida | Recusa a mensagem, de forma visível. |
| Um envelope malformado, uma chave desconhecida ou uma lista acima do limite | Recusa a mensagem, de forma visível. |
Um scenePlan, um pendingPlan ou qualquer history[].scenePlan que falhe na validação | Recusa a mensagem, de forma visível. |
Ignorar é silencioso, porque o tráfego de outro iframe não é problema do usuário. Recusar é visível, porque foi a sua mensagem que estava errada. Uma recusa nunca destrói dados aceitos: o último snapshot aceito continua na tela, sob um banner que informa o motivo, e o próximo state válido o remove.
Do iframe para a página pai: ready e event
O iframe envia as duas mensagens para o parentOrigin exato da URL dele, nunca para "*".
{
type: "nodaro:scene3d:ready",
version: 1, // the baseline, always 1
channel: string,
protocolVersions: [1, 2], // every version this frame accepts
capabilities: {
assetTransport: true, // it can ask you for asset bytes
sceneSchemaVersions: [1, 2], // the scene versions it can draw
},
}Identifique o ready pelo type e pelo channel, e leia protocolVersions. Não compare a mensagem inteira. version continua 1, então uma página pai escrita para a versão 1 continua funcionando; tudo o que a versão 2 acrescenta é anunciado em campos que essa página pai não lê.
{
type: "nodaro:scene3d:event",
version: 1 | 2, // 2 ONLY for edit-operations
channel: string,
expectedRevisionId: string | null,
event:
| { kind: "plan", plan: Scene3DPlan, changeSummary: string }
| { kind: "selection", objectIds: string[] }
| { kind: "locks", objectIds: string[] }
| { kind: "restore", revisionId: string }
| { kind: "resolve-pending", adopt: boolean }
// version 2 scenes only, see "Baked scenes" below
| { kind: "edit-operations", operations: Scene3DV2EditOperation[], expectedContentHash: string },
}O envelope diz version: 2 só para edit-operations. Todos os outros tipos de evento continuam dizendo 1, então uma página pai da versão 1 nunca encontra uma versão desconhecida em uma mensagem que ela entende.
expectedRevisionId é a revisão que o iframe mostrava quando o usuário agiu, a base a partir da qual a ação foi calculada. Aplique o evento só se ele ainda for igual ao seu scenePlan.revisionId ativo; caso contrário, descarte-o. Essa única verificação faz com que um clique em um snapshot que você já substituiu não tenha efeito, em vez de causar uma reversão silenciosa. Ele só é null antes de o iframe aceitar algum estado.
kind | Significado | O que você faz |
|---|---|---|
plan | Uma edição local, como uma pose numérica ou uma mudança de cor, produziu uma nova revisão imutável. plan.parentRevisionId é expectedRevisionId. Nenhum modelo foi executado e nada foi cobrado. | Torne plan a revisão ativa, adicione-a ao histórico e envie um novo state. |
selection | O usuário selecionou ou desmarcou objetos. | Reproduza a seleção e envie state, ou apenas guarde-a. |
locks | O usuário bloqueou ou desbloqueou objetos. Um objeto bloqueado é um objeto que um job de edição precisa deixar idêntico byte a byte. | Salve a alteração e envie state. |
restore | O usuário pediu uma revisão anterior do histórico que você enviou. | Torne essa revisão ativa e envie state. |
resolve-pending | adopt: true usa o pendingPlan; false mantém o plano atual. | Resolva e envie state sem pendingPlan. |
edit-operations | Uma edição de sobreposição da versão 2, na forma de operações. O iframe não a aplica. | Aplique a edição com o aplicador compartilhado, salve o resultado e envie de volta o plano salvo. Veja Editar uma cena pré-calculada. |
Modo somente leitura
readOnly, que é true por padrão, mantém o visualizador inteiro e bloqueia todas as gravações:
| Continua funcionando | Bloqueado |
|---|---|
| Reproduzir, pausar e percorrer a linha do tempo | Confirmações numéricas de pose, de objetos e da câmera |
| Selecionar no viewport e na lista | Cores de objetos e do fundo |
| Ler a lista de objetos, os selos de bloqueio e o histórico de revisões | Botões de bloqueio |
| O aviso de revisão pendente | Restaurar e os botões de revisão pendente |
O único evento que um iframe somente leitura emite é selection. A regra vale nos dois sentidos: os controles que alterariam a cena ficam desativados ou ocultos, e o iframe se recusa a emitir uma alteração mesmo que um controle seja acionado diretamente. Somente leitura é uma propriedade do iframe, não um estilo.
Cenas pré-calculadas e o transporte de assets
Uma cena da versão 1 é autossuficiente: primitivas, quadros-chave e uma câmera, tudo dentro do plano que você envia. Uma cena da versão 2 é geometria pré-calculada (baked). O manifesto dela traz entidades semânticas, tomadas, uma trilha de câmera pré-calculada e uma lista de ids de assets com hashes SHA-256, enquanto os bytes em si ficam atrás da API autenticada do Nodaro.
O iframe continua sem sessão e não faz nenhuma chamada de rede. Em vez disso, ele pede a você exatamente os assets que o manifesto declara, e você os busca com a sua própria sessão.
Ativar
Envie version: 2 na sua mensagem state. Uma cena da versão 2 em uma mensagem version: 1 é recusada com scenePlan — this scene uses schema version 2, which needs embed protocol version 2 (asset transport): uma página pai da versão 1 não tem tratamento para pedidos de assets, e o iframe ficaria esperando para sempre. As cenas da versão 1 funcionam com qualquer uma das versões.
As mensagens
// frame to parent
{
type: "nodaro:scene3d:asset-request",
version: 2,
channel: string,
requestId: string, // new for each request; send it back unchanged
revisionId: string, // plan.revisionId
assetId: string, // an opaque id from plan.assets
kind: "glb" | "camera-track-json",
byteLength: number, // what the manifest declares
sha256: string, // 64 lower-case hex characters
}
// parent to frame, success
{
type: "nodaro:scene3d:asset-response",
version: 2,
channel: string,
requestId: string, // echoed
revisionId: string, // echoed
assetId: string, // echoed
ok: true,
bytes: ArrayBuffer, // a structured clone: NOT a URL, NOT base64
}
// parent to frame, failure
{ /* same envelope */ ok: false, error: "short reason" }revisionId é a revisão armazenada atual do plano. Cada revisão armazenada fixa todos os seus assets, incluindo os bytes que reaproveita de uma revisão anterior.
O que o iframe verifica
O iframe ignora em silêncio o tráfego normal que não é para ele:
- Uma resposta de outra janela, de outra origem ou de outro
channel. - Um
requestIdque não está pendente, como uma resposta atrasada depois de um tempo limite, uma duplicata ou bytes que ninguém pediu.
Ele faz aquele asset falhar, de forma visível, nestes casos:
versionnão é2, ou um campo é desconhecido ou está ausente.revisionIdouassetIdnão é o que foi pedido.- A resposta é
ok: false. O iframe mostra o seuerror, encurtado. bytesnão é umArrayBuffer.- O tamanho não é o
byteLengthdo manifesto. - O hash SHA-256 não é o hash do manifesto.
A verificação do hash é a que importa: ela separa “o host entregou alguns bytes” de “estes são os bytes de que esta revisão é feita”. O renderizador verifica o hash uma segunda vez antes do parsing, então nenhum caminho desenha geometria não verificada.
O iframe também impõe limites a si mesmo:
- No máximo 4 pedidos ficam em andamento, e o resto espera em uma fila.
- No máximo 64 pedidos e 64 MiB são permitidos por revisão.
- Cada pedido tem 20 segundos para ser respondido.
- Todo pedido pendente é rejeitado quando chega uma nova revisão ou quando o iframe é desmontado, então uma resposta para uma cena que o usuário deixou nunca é desenhada.
- Assets idênticos, com o mesmo id e o mesmo hash, são pedidos uma vez, então uma revisão de sobreposição que reaproveita um GLB não o baixa de novo.
Regras para a sua página
- Autorize com base no que você enviou. Só responda quando
request.revisionIdfor a revisão que você enviou (plan.revisionId) erequest.assetIdestiver nosassetsdesse plano, com o mesmobyteLengthe o mesmosha256. Caso contrário, respondaok: false. Se você pular essa verificação, a sua sessão vira um oráculo: quem controlar a página dentro do iframe pode pedir qualquer revisão ou asset e ler a resposta. - Nunca envie uma credencial. Nem token, nem cookie, nem URL assinada: uma URL que concede acesso é um token bearer escrito de outro jeito. Envie bytes.
- Envie para a origem exata do iframe, nunca para
"*". - Não transfira um buffer que você mantém. Um structured clone o copia; uma lista de transferência o tiraria de você.
- Descarte pedidos de uma revisão que você já substituiu, e mantenha no máximo uma busca em andamento por revisão e asset.
import { createClient } from "@nodaro/sdk"
import type { Scene3DPlanV2 } from "@nodaro/shared"
const client = createClient({ baseUrl: "https://app.nodaro.ai", auth: myAuth })
/** The plan you sent most recently. */
let active: Scene3DPlanV2
window.addEventListener("message", async (event) => {
if (event.source !== iframe.contentWindow) return
if (event.origin !== NODARO_ORIGIN) return
const data = event.data
if (data?.type !== "nodaro:scene3d:asset-request") return
if (data.channel !== channel || data.version !== 2) return
const reply = (body: Record<string, unknown>) =>
iframe.contentWindow?.postMessage(
{
type: "nodaro:scene3d:asset-response",
version: 2,
channel,
requestId: data.requestId,
revisionId: data.revisionId,
assetId: data.assetId,
...body,
},
NODARO_ORIGIN,
)
// 1. Authorize: the asset must belong to the plan we sent, and the request
// must name the revision that plan pins the bytes to.
const asset = active.assets.find((a) => a.assetId === data.assetId)
const pinnedTo = active.revisionId
if (
!asset ||
data.revisionId !== pinnedTo ||
asset.byteLength !== data.byteLength ||
asset.sha256 !== data.sha256
) {
reply({ ok: false, error: "unknown asset" })
return
}
// 2. Fetch with OUR session, and send the bytes, never the URL or the token.
try {
const bytes = await client.scene3d.assetBytes(pinnedTo, asset)
reply({ ok: true, bytes })
} catch (error) {
reply({ ok: false, error: error instanceof Error ? error.message : "fetch failed" })
}
})Editar uma cena pré-calculada
As edições da versão 2 são sobreposições, e o iframe nunca aplica uma sozinho. Uma revisão da versão 2 que o seu servidor ainda não armazenou pareceria salva enquanto os assets dela ainda pertencem à revisão de origem. Por isso, o iframe envia as operações e continua mostrando a revisão que você enviou:
// event.event
{
kind: "edit-operations",
operations: [
{ op: "set-override", override: { kind: "entity-transform", entityId: "hero", space: "local", position: [3, 0.5, 0] } },
],
expectedContentHash: "<64 hex characters>", // plan.provenance.contentHash
}Passe os três valores de verificação de revisão desatualizada para o aplicador compartilhado, o mesmo que a API usa, e depois salve o resultado e envie-o de volta:
import { applyScene3DV2EditOperations } from "@nodaro/shared"
const result = await applyScene3DV2EditOperations(active, message.event.operations, {
expectedRevisionId: message.expectedRevisionId, // from the envelope
expectedContentHash: message.event.expectedContentHash,
lockedObjectIds,
})
if (!result.ok) return showError(result.message) // stale_revision, locked, ...Uma substituição nomeia só os canais que altera, e o iframe a monta assim de propósito. Em uma entidade de asset, a transformação do nó no arquivo GLB é a referência, então uma edição que moveu algo não deve também repetir uma rotação que nunca tocou. Para salvar uma edição sem um job de geração, chame POST /v1/3d-scene/revisions/:revisionId/edits, ou client.scene3d.applyEdits() no SDK; veja a API de cenas 3D.
Validação e limites
scenePlan, pendingPlan e cada history[].scenePlan são analisados com o scene3DPlanSchema público de @nodaro/shared. A verificação cobre a estrutura e as regras entre campos: ciclos de pais, pais ausentes, quadros-chave depois do último quadro e o teto de duração. Um plano que falha é recusado por inteiro.
| Limite | Valor |
|---|---|
Entradas de history | 12 |
Entradas de selectedObjectIds ou de lockedObjectIds | 100 |
| Tamanho do id de um objeto | 64 caracteres |
Tamanho de changeSummary e de context.prompt | 2.000 caracteres |
Tamanho de createdAt | 64 caracteres |
Tamanho de parentOrigin | 255 caracteres |
| Largura e altura do quadro | 100 a 2560 px em cada eixo |
| Objetos por cena | 100 |
| Quadros-chave por objeto ou por trilha de câmera | 240 |
| Entidades em uma cena da versão 2 | 100 |
| Assets, tomadas e substituições em uma cena da versão 2 | 64, 32 e 200 |
| Bytes de assets que uma cena da versão 2 pode declarar | 64 MiB |
O iframe verifica o tamanho das listas antes de analisar qualquer coisa, então um payload grande demais é recusado sem ser lido. Um manifesto da versão 2 é uma promessa sobre bytes, então os totais declarados são verificados antes de qualquer asset ser pedido. Um quadro com mais de 1920 px no lado mais longo é renderizado em MP4 por um preço maior; veja Renderizar vídeo (Render Video).
Checklist da página pai
O iframe protege a própria caixa de entrada. Só a sua página pode proteger a sua.
- Crie um
channelnovo por iframe comcrypto.randomUUID(), e guarde-o. - Monte a URL com a sua própria origem exata, a partir de
window.location.origin, não de uma string que você montou. - Verifique quatro coisas em toda mensagem que você recebe.
event.source === iframe.contentWindow.event.originé exatamente a origem do Nodaro que você incorporou.data.channelé o seu channel.data.versioné uma versão que você implementa:1, ou2se você fornece assets.
- Compare
expectedRevisionIdcom a sua revisão ativa antes de aplicar um eventoplan,restoreouresolve-pending, e descarte o evento se não corresponder. - Valide
plande novo comscene3DPlanSchemaantes de armazená-lo. O iframe valida o que desenha; você é responsável pelo que salva. - Envie
stateao receberready, e de novo depois de cada alteração que você aceitar. - Nunca envie para
"*". Enderece a origem do Nodaro de forma explícita. - Não coloque nada secreto em uma mensagem. O protocolo transporta geometria de cena e ids de revisão: nada de tokens, de identificadores de usuário ou de URLs que você não colocaria em uma referência de cena.
- Remova o listener quando a caixa de diálogo fechar. Um listener antigo somado a um channel reutilizado é como duas caixas de diálogo começam a responder uma à outra.
- Se você fornece cenas da versão 2, autorize cada pedido de asset com base no plano que você enviou, como descrito em Regras para a sua página.
Exemplo completo
import { scene3DPlanSchema, type Scene3DPlan } from "@nodaro/shared"
const NODARO_ORIGIN = "https://app.nodaro.ai"
const channel = crypto.randomUUID()
const iframe = document.createElement("iframe")
iframe.src =
`${NODARO_ORIGIN}/embed/scene3d` +
`?parentOrigin=${encodeURIComponent(window.location.origin)}` +
`&channel=${channel}`
iframe.allow = "" // the frame needs no permissions
let active: Scene3DPlan = initialPlan // your current revision
let history: RevisionEntry[] = [] // newest last, at most 12
function pushState() {
iframe.contentWindow?.postMessage(
{
type: "nodaro:scene3d:state",
version: 1,
channel,
scenePlan: active,
selectedObjectIds: selection,
lockedObjectIds: locks,
history: history.slice(-12),
readOnly: false, // leave it out and the frame is view-only
},
NODARO_ORIGIN, // never "*"
)
}
function onMessage(e: MessageEvent) {
if (e.source !== iframe.contentWindow) return
if (e.origin !== NODARO_ORIGIN) return
const data = e.data
if (!data || typeof data !== "object") return
if (data.channel !== channel || data.version !== 1) return
if (data.type === "nodaro:scene3d:ready") {
pushState() // the frame listens: send the truth
return
}
if (data.type !== "nodaro:scene3d:event") return
// The action was computed from a revision we may have replaced already.
const mutates = data.event.kind !== "selection"
if (mutates && data.expectedRevisionId !== active.revisionId) return
switch (data.event.kind) {
case "selection":
selection = data.event.objectIds
return // no new state needed; the frame shows it already
case "locks":
locks = data.event.objectIds
break
case "plan": {
// Trust nothing you save.
const parsed = scene3DPlanSchema.safeParse(data.event.plan)
if (!parsed.success) return
active = parsed.data
history = [...history, {
revisionId: active.revisionId,
scenePlan: active,
source: "manual",
changeSummary: data.event.changeSummary,
createdAt: new Date().toISOString(),
}].slice(-12)
break
}
case "restore": {
const entry = history.find((h) => h.revisionId === data.event.revisionId)
if (!entry) return
active = entry.scenePlan
break
}
case "resolve-pending":
active = data.event.adopt ? pending! : active
pending = undefined
break
}
pushState()
}
window.addEventListener("message", onMessage)
document.body.appendChild(iframe)
// On teardown:
// window.removeEventListener("message", onMessage)
// iframe.remove()Gerar e editar cenas com um modelo, e renderizar uma cena em MP4, são jobs comuns da API: veja Gerar cena 3D, Editar cena 3D e Renderizar vídeo. O viewport é só o visualizador: ele nunca inicia um job e nunca gasta nada.
Versionamento
version: 1 é o primeiro contrato, congelado. O envelope state é estrito: uma chave desconhecida no nível superior, ou dentro de uma entrada de histórico, é recusada em vez de ignorada. Assim, uma página pai e um iframe nunca concordam só pela metade sobre o significado de uma mensagem. O protocolo cresce aumentando a version, e um iframe que não implementa uma versão recusa a mensagem de forma visível, em vez de adivinhar.
version: 2 acrescenta à versão 1 e não substitui nada. Ela traz as cenas pré-calculadas, o transporte de assets e o evento edit-operations, e uma página pai que só fala a versão 1 continua funcionando sem mudanças. O iframe anuncia o que consegue fazer em ready.protocolVersions e ready.capabilities: leia esses campos em vez de supor, o que mantém a próxima versão também aditiva. Se você precisar de um campo que não existe, abra uma issue no repositório público em vez de enviá-lo mesmo assim: uma mensagem recusada é o contrato funcionando.
Perguntas frequentes
Páginas relacionadas
Formato de cena 3D
Incorporações
Gerar cena 3D
Cenas 3D
Cenas 3D
Última atualização
Incorporar um miniapp
Execute um miniapp do Nodaro no seu produto com o código de incorporação ou um formulário próprio na API REST: entradas, execuções, resultados e erros.
Formato de cena 3D
Como é um plano de cena 3D do Nodaro: primitivas e quadros-chave na versão 1; assets GLB, entidades, trilha de câmera, tomadas e sobreposições na versão 2.