From ded70a3cb46c0eeb52bd55b82a03eb4712bed94f Mon Sep 17 00:00:00 2001 From: Amp Date: Thu, 20 Aug 2026 09:21:21 +0000 Subject: [PATCH] Add interactive tutorial playgrounds Embed editable split-view examples throughout the guide, restore the full LFS-backed Sponza demo, batch glTF hydration, honor hierarchical transforms, and report transformed BVH picks with a live FPS counter. Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure --- .agents/setup | 6 + .gitattributes | 1 + addons/handles/src/Scene.ts | 484 ++++++++++++++++++++----- addons/handles/src/bvh/picking.ts | 40 +- addons/handles/src/bvh/worker.ts | 97 ++++- addons/handles/src/importers/gltf.ts | 64 ++-- addons/handles/src/importers/worker.ts | 235 ++++++++++-- docs/.vitepress/Playground.vue | 370 +++++++++++++++---- docs/.vitepress/playgrounds.js | 388 ++++++++++++++++++++ docs/guide/cameras.md | 6 + docs/guide/core.md | 6 + docs/guide/importing-and-picking.md | 10 +- docs/guide/scene-and-sab.md | 6 + docs/public/models/sponza.glb | 3 + 14 files changed, 1466 insertions(+), 250 deletions(-) create mode 100644 .gitattributes create mode 100644 docs/.vitepress/playgrounds.js create mode 100644 docs/public/models/sponza.glb diff --git a/.agents/setup b/.agents/setup index f2b5b05..cde975b 100755 --- a/.agents/setup +++ b/.agents/setup @@ -10,5 +10,11 @@ rustup toolchain install "$toolchain" --profile minimal --component rust-src,rus if ! command -v wasm-pack >/dev/null; then cargo install wasm-pack --version 0.15.0 --locked fi +if ! command -v git-lfs >/dev/null; then + sudo apt-get update -qq + sudo apt-get install -y git-lfs +fi +git lfs install --local +git lfs pull --include="docs/public/models/sponza.glb" npm ci amp orb services ensure diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..015c046 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/public/models/sponza.glb filter=lfs diff=lfs merge=lfs -text diff --git a/addons/handles/src/Scene.ts b/addons/handles/src/Scene.ts index 15408eb..5a0d227 100644 --- a/addons/handles/src/Scene.ts +++ b/addons/handles/src/Scene.ts @@ -20,8 +20,15 @@ type ShaderLike = { vertexEntry: string; fragmentEntry: string; }; -type PostProcessState = { id: string; kind: string; options: Record }; -type TextureState = GraphTexture & { number: number; source?: string | ImageBitmap }; +type PostProcessState = { + id: string; + kind: string; + options: Record; +}; +type TextureState = GraphTexture & { + number: number; + source?: string | ImageBitmap; +}; const rows = [ ["nodes", 16, "u32"], @@ -193,11 +200,12 @@ function effectFragment(kind: string, options: Record) { } function presentShader(toneMap: string) { - const tone = toneMap === "reinhard" - ? "color / (color + vec3(1.0))" - : toneMap === "linear" - ? "clamp(color, vec3(0.0), vec3(1.0))" - : "clamp((color * (2.51 * color + vec3(0.03))) / (color * (2.43 * color + vec3(0.59)) + vec3(0.14)), vec3(0.0), vec3(1.0))"; + const tone = + toneMap === "reinhard" + ? "color / (color + vec3(1.0))" + : toneMap === "linear" + ? "clamp(color, vec3(0.0), vec3(1.0))" + : "clamp((color * (2.51 * color + vec3(0.03))) / (color * (2.43 * color + vec3(0.59)) + vec3(0.14)), vec3(0.0), vec3(1.0))"; return `${fullscreenVertex} @group(0) @binding(0) var source: texture_2d; @group(0) @binding(1) var sourceSampler: sampler; @@ -209,11 +217,19 @@ function presentShader(toneMap: string) { } function encode(value: unknown): string { - if (value === null || typeof value === "boolean" || typeof value === "number") return String(value); + if (value === null || typeof value === "boolean" || typeof value === "number") + return String(value); if (typeof value === "string") return JSON.stringify(value); - if (Array.isArray(value)) return `(array${value.map((item) => ` ${encode(item)}`).join("")})`; - if (value && value.constructor === Object) return `(object${Object.keys(value as object).sort().map((key) => - ` (field ${JSON.stringify(key)} ${encode((value as Record)[key])})`).join("")})`; + if (Array.isArray(value)) + return `(array${value.map((item) => ` ${encode(item)}`).join("")})`; + if (value && value.constructor === Object) + return `(object${Object.keys(value as object) + .sort() + .map( + (key) => + ` (field ${JSON.stringify(key)} ${encode((value as Record)[key])})`, + ) + .join("")})`; throw new TypeError("Render graph values must be plain data"); } @@ -236,8 +252,13 @@ export class Scene { #geometryRefs = new Map(); #nextGeometry = 1; #nextTexture = 0; + #graphBatchDepth = 0; + #graphBatchDirty = false; - constructor(canvas: HTMLCanvasElement, options: { arenaBytes?: number; fps?: number; hdr?: boolean } = {}) { + constructor( + canvas: HTMLCanvasElement, + options: { arenaBytes?: number; fps?: number; hdr?: boolean } = {}, + ) { this.hdr = options.hdr ?? true; this.core = new YawnCore(canvas, { arenaBytes: options.arenaBytes }); this.ready = this.#initialize(options.fps ?? 60); @@ -247,12 +268,19 @@ export class Scene { await this.core.ready; for (const [name, stride, format] of rows) await this.core.createRows({ name, rows: 1, stride, format }); - await this.core.createRows({ name: "clusters", rows: 256, stride: 16, format: "u32" }); + await this.core.createRows({ + name: "clusters", + rows: 256, + stride: 16, + format: "u32", + }); this.core.array("nodeQuaternions").write(0, [0, 0, 0, 1]); this.core.array("nodeScales").write(0, [1, 1, 1, 0]); this.core.array("sceneAccent").write(0, [0.28, 0.72, 1, 1]); const material = await this.core.allocateObject("materials"); - this.core.array("materials").write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 0, 1, 0.5]); + this.core + .array("materials") + .write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 0, 1, 0.5]); await this.core.setFps(fps); await this.#compileRenderGraph(); return this; @@ -262,16 +290,80 @@ export class Scene { return this.core.array(name); } - async ensureRows(name: string, rowCount: number, stride: number, format: RowFormat) { + async ensureRows( + name: string, + rowCount: number, + stride: number, + format: RowFormat, + ) { await this.core.ready; try { const current = this.core.array(name); - if (current.stride !== stride || current.format !== format) throw new Error(`ROW_LAYOUT: ${name}`); + if (current.stride !== stride || current.format !== format) + throw new Error(`ROW_LAYOUT: ${name}`); if (current.rows >= rowCount) return current; } catch (error) { - if (!(error instanceof Error) || !error.message.startsWith("UNKNOWN_ARRAY")) throw error; + if ( + !(error instanceof Error) || + !error.message.startsWith("UNKNOWN_ARRAY") + ) + throw error; + } + return this.core.createRows({ + name, + rows: Math.max(1, rowCount), + stride, + format, + }); + } + + async reserve(additional: { nodes?: number; materials?: number }) { + await this.ready; + const nodes = additional.nodes ?? 0; + const materials = additional.materials ?? 0; + if ( + ![nodes, materials].every( + (value) => Number.isInteger(value) && value >= 0, + ) + ) + throw new RangeError("additional"); + const nodeCapacity = this.array("nodes").rows + nodes; + const materialCapacity = this.array("materials").rows + materials; + const growth = [ + ...rows.slice(0, 6).map(([name, stride, format]) => ({ + name, + rows: nodeCapacity, + stride, + format, + })), + { + name: "materials", + rows: materialCapacity, + stride: 48, + format: "f32" as const, + }, + { + name: "materialTextures", + rows: materialCapacity, + stride: 32, + format: "u32" as const, + }, + ].filter((request) => this.array(request.name).rows < request.rows); + if (growth.length) await this.core.createRowsBatch(growth); + } + + async batchGraphUpdates(operation: () => T | Promise) { + await this.ready; + this.#graphBatchDepth++; + try { + return await operation(); + } finally { + this.#graphBatchDepth--; + if (!this.#graphBatchDepth && this.#graphBatchDirty) { + this.#graphBatchDirty = false; + await this.updateRenderGraph(); + } } - return this.core.createRows({ name, rows: Math.max(1, rowCount), stride, format }); } async allocateNode() { @@ -279,7 +371,9 @@ export class Scene { const id = await this.core.allocateObject("nodes"); const growth = rows.slice(1, 6).flatMap(([name, stride, format]) => { const current = this.array(name); - return current.rows < id + 1 ? [{ name, rows: id + 1, stride, format }] : []; + return current.rows < id + 1 + ? [{ name, rows: id + 1, stride, format }] + : []; }); if (growth.length) await this.core.createRowsBatch(growth); this.array("nodeQuaternions").write(id, [0, 0, 0, 1]); @@ -289,7 +383,14 @@ export class Scene { } async releaseNode(id: number) { - for (const name of ["nodes", "nodePositions", "nodeQuaternions", "nodeScales", "meshInfo", "bounds"]) + for (const name of [ + "nodes", + "nodePositions", + "nodeQuaternions", + "nodeScales", + "meshInfo", + "bounds", + ]) this.array(name).row(id).fill(0); await this.core.deleteObject("nodes", id); } @@ -302,7 +403,8 @@ export class Scene { } addComputePass(pass: ComputePass) { - if (this.#computePasses.has(pass.id)) throw new Error(`COMPUTE_PASS_EXISTS: ${pass.id}`); + if (this.#computePasses.has(pass.id)) + throw new Error(`COMPUTE_PASS_EXISTS: ${pass.id}`); this.#computePasses.set(pass.id, pass); pass.attach(this); return this.updateRenderGraph(); @@ -318,13 +420,19 @@ export class Scene { registerMesh(mesh: MeshLike) { this.#meshes.set(mesh.id, mesh); - this.#geometryRefs.set(mesh.geometryId, (this.#geometryRefs.get(mesh.geometryId) ?? 0) + 1); + this.#geometryRefs.set( + mesh.geometryId, + (this.#geometryRefs.get(mesh.geometryId) ?? 0) + 1, + ); return this.updateRenderGraph(); } async unregisterMesh(mesh: MeshLike) { this.#meshes.delete(mesh.id); - const references = Math.max(0, (this.#geometryRefs.get(mesh.geometryId) ?? 1) - 1); + const references = Math.max( + 0, + (this.#geometryRefs.get(mesh.geometryId) ?? 1) - 1, + ); this.#geometryRefs.set(mesh.geometryId, references); await this.updateRenderGraph(); if (!references) { @@ -351,31 +459,62 @@ export class Scene { } releaseGeometry(id: number) { - this.#geometryRefs.set(id, Math.max(0, (this.#geometryRefs.get(id) ?? 1) - 1)); + this.#geometryRefs.set( + id, + Math.max(0, (this.#geometryRefs.get(id) ?? 1) - 1), + ); } async cloneGeometry(id: number) { const clone = this.createGeometry(); for (const [kind, data] of this.#geometry.get(id) ?? []) - await this.setVertexData(clone, kind, data.slice() as Float32Array | Uint32Array, false); + await this.setVertexData( + clone, + kind, + data.slice() as Float32Array | Uint32Array, + false, + ); return clone; } - async setVertexData(geometry: number, kind: string, source: ArrayLike, updateGraph = true) { - const components: Record = { positions: 3, normals: 3, tangents: 4, uvs: 2, colors: 4, indices: 1 }; + async setVertexData( + geometry: number, + kind: string, + source: ArrayLike, + updateGraph = true, + ) { + const components: Record = { + positions: 3, + normals: 3, + tangents: 4, + uvs: 2, + colors: 4, + indices: 1, + }; const width = components[kind]; - if (!width || source.length % width) throw new RangeError(`VERTEX_DATA: ${kind}`); + if (!width || source.length % width) + throw new RangeError(`VERTEX_DATA: ${kind}`); const integer = kind === "indices"; const data = integer ? Uint32Array.from(source) : Float32Array.from(source); const name = `geometry.${geometry}.${kind}`; const rowCount = integer ? Math.ceil(data.length / 4) : data.length / width; - const target = await this.ensureRows(name, rowCount, 16, integer ? "u32" : "f32"); - target.view.fill(0); - if (integer) target.view.set(data); - else for (let row = 0; row < rowCount; row++) - target.row(row).set(data.subarray(row * width, (row + 1) * width)); - (this.#geometry.get(geometry) ?? this.#geometry.set(geometry, new Map()).get(geometry)!) - .set(kind, data); + const target = await this.ensureRows( + name, + rowCount, + 16, + integer ? "u32" : "f32", + ); + const view = target.view; + view.fill(0); + if (integer || width === 4) view.set(data); + else + for (let row = 0; row < rowCount; row++) + for (let lane = 0; lane < width; lane++) + view[row * 4 + lane] = data[row * width + lane]; + ( + this.#geometry.get(geometry) ?? + this.#geometry.set(geometry, new Map()).get(geometry)! + ).set(kind, data); if (updateGraph) await this.updateRenderGraph(); } @@ -411,6 +550,10 @@ export class Scene { } updateRenderGraph() { + if (this.#graphBatchDepth) { + this.#graphBatchDirty = true; + return Promise.resolve(); + } const update = this.#graphUpdates.then(async () => { await this.ready; await this.#compileRenderGraph(); @@ -431,24 +574,45 @@ export class Scene { const addSampler = (value: GraphSampler) => samplers.set(value.id, value); for (const [id, array] of [ - ["point-lights", "pointLights"], ["rect-lights", "rectAreaLights"], - ["spot-lights", "spotLights"], ["directional-lights", "directionalLights"], - ["ambient-lights", "ambientLights"], ["clusters", "clusters"], - ]) addBuffer({ id, array, usage: ["storage"] }); + ["point-lights", "pointLights"], + ["rect-lights", "rectAreaLights"], + ["spot-lights", "spotLights"], + ["directional-lights", "directionalLights"], + ["ambient-lights", "ambientLights"], + ["clusters", "clusters"], + ]) + addBuffer({ id, array, usage: ["storage"] }); addBuffer({ id: "accent", array: "sceneAccent", usage: ["uniform"] }); - computePipelines.push({ id: "cluster-lights", code: clusterShader, entry: "main" }); + computePipelines.push({ + id: "cluster-lights", + code: clusterShader, + entry: "main", + }); passes.push({ - id: "cluster-lights", type: "compute", pipeline: "cluster-lights", dispatch: [4, 1, 1], - bindings: ["point-lights", "rect-lights", "spot-lights", "directional-lights", "ambient-lights", "clusters"] - .map((resource, binding) => ({ group: 0, binding, resource })), + id: "cluster-lights", + type: "compute", + pipeline: "cluster-lights", + dispatch: [4, 1, 1], + bindings: [ + "point-lights", + "rect-lights", + "spot-lights", + "directional-lights", + "ambient-lights", + "clusters", + ].map((resource, binding) => ({ group: 0, binding, resource })), }); for (const pass of this.#computePasses.values()) { pass.buffers.forEach(addBuffer); pass.textures.forEach(addTexture); pass.samplers.forEach(addSampler); - computePipelines.push({ id: pass.id, code: pass.code, entry: pass.entry }); + computePipelines.push({ + id: pass.id, + code: pass.code, + entry: pass.entry, + }); passes.push({ id: pass.id, type: "compute", @@ -460,57 +624,146 @@ export class Scene { } const computeIds = [...this.#computePasses.keys()]; - const renderedMeshes = [...this.#meshes.values()].filter((mesh) => mesh.vertexCount > 0); + const renderedMeshes = [...this.#meshes.values()].filter( + (mesh) => mesh.vertexCount > 0, + ); const hdrFormat = this.hdr ? "rgba16float" : "rgba8unorm"; - addTexture({ id: "hdr", format: hdrFormat, size: ["canvas", "canvas", 1], usage: ["render", "sampled"], transient: false }); + addTexture({ + id: "hdr", + format: hdrFormat, + size: ["canvas", "canvas", 1], + usage: ["render", "sampled"], + transient: false, + }); addSampler({ id: "linear", magFilter: "linear", minFilter: "linear" }); let previous = computeIds.length ? computeIds : ["cluster-lights"]; if (!renderedMeshes.length) { - renderPipelines.push({ id: "empty-forward", code: emptyForwardShader, vertex: { entry: "vertex" }, fragment: { entry: "fragment", targets: [{ format: hdrFormat }] } }); + renderPipelines.push({ + id: "empty-forward", + code: emptyForwardShader, + vertex: { entry: "vertex" }, + fragment: { entry: "fragment", targets: [{ format: hdrFormat }] }, + }); passes.push({ - id: "forward-empty", type: "render", pipeline: "empty-forward", after: previous, + id: "forward-empty", + type: "render", + pipeline: "empty-forward", + after: previous, bindings: [{ group: 0, binding: 0, resource: "accent" }], - color: [{ resource: "hdr", clear: [0.015, 0.025, 0.05, 1] }], draw: { vertices: 3 }, + color: [{ resource: "hdr", clear: [0.015, 0.025, 0.05, 1] }], + draw: { vertices: 3 }, }); previous = ["forward-empty"]; } else { for (const [id, array] of [ - ["node-positions", "nodePositions"], ["node-quaternions", "nodeQuaternions"], - ["node-scales", "nodeScales"], ["mesh-info", "meshInfo"], ["materials", "materials"], ["cameras", "cameras"], - ]) addBuffer({ id, array, usage: ["storage"] }); + ["node-positions", "nodePositions"], + ["node-quaternions", "nodeQuaternions"], + ["node-scales", "nodeScales"], + ["mesh-info", "meshInfo"], + ["materials", "materials"], + ["cameras", "cameras"], + ]) + addBuffer({ id, array, usage: ["storage"] }); renderPipelines.push({ - id: "forward-pbr", code: forwardShader, - vertex: { entry: "vertex", buffers: [{ arrayStride: 16, attributes: [{ format: "float32x3", offset: 0, shaderLocation: 0 }] }] }, + id: "forward-pbr", + code: forwardShader, + vertex: { + entry: "vertex", + buffers: [ + { + arrayStride: 16, + attributes: [ + { format: "float32x3", offset: 0, shaderLocation: 0 }, + ], + }, + ], + }, fragment: { entry: "fragment", targets: [{ format: hdrFormat }] }, }); let firstRender = true; for (const mesh of renderedMeshes) { const vertex = `geometry-${mesh.geometryId}-positions`; - addBuffer({ id: vertex, array: `geometry.${mesh.geometryId}.positions`, usage: ["vertex"] }); + addBuffer({ + id: vertex, + array: `geometry.${mesh.geometryId}.positions`, + usage: ["vertex"], + }); const indexed = mesh.indexCount > 0; - if (indexed) addBuffer({ id: `geometry-${mesh.geometryId}-indices`, array: `geometry.${mesh.geometryId}.indices`, usage: ["index"] }); - const draws = indexed && mesh.faceMaterials.size - ? Array.from({ length: Math.floor(mesh.indexCount / 3) }, (_, face) => ({ - face, - count: 3, - firstIndex: face * 3, - material: mesh.faceMaterials.get(face), - })) - : [{ face: -1, count: indexed ? mesh.indexCount : mesh.vertexCount, firstIndex: 0, material: undefined }]; + if (indexed) + addBuffer({ + id: `geometry-${mesh.geometryId}-indices`, + array: `geometry.${mesh.geometryId}.indices`, + usage: ["index"], + }); + const draws = + indexed && mesh.faceMaterials.size + ? Array.from( + { length: Math.floor(mesh.indexCount / 3) }, + (_, face) => ({ + face, + count: 3, + firstIndex: face * 3, + material: mesh.faceMaterials.get(face), + }), + ) + : [ + { + face: -1, + count: indexed ? mesh.indexCount : mesh.vertexCount, + firstIndex: 0, + material: undefined, + }, + ]; for (const draw of draws) { const id = `forward-${mesh.id}-${draw.face}`; - if (mesh.id > 65535 || (draw.material ?? 0) > 65534) throw new RangeError("Scene handle limit"); - const instance = (mesh.id + (draw.material === undefined ? 0 : (draw.material + 1) * 65536)) >>> 0; + if (mesh.id > 65535 || (draw.material ?? 0) > 65534) + throw new RangeError("Scene handle limit"); + const instance = + (mesh.id + + (draw.material === undefined + ? 0 + : (draw.material + 1) * 65536)) >>> + 0; passes.push({ - id, type: "render", pipeline: "forward-pbr", after: previous, - bindings: ["clusters", "accent", "node-positions", "node-quaternions", "node-scales", "mesh-info", "materials", "cameras"] - .map((resource, binding) => ({ group: 0, binding, resource })), - color: [{ resource: "hdr", ...(firstRender ? { clear: [0.015, 0.025, 0.05, 1] } : { load: "load" }) }], + id, + type: "render", + pipeline: "forward-pbr", + after: previous, + bindings: [ + "clusters", + "accent", + "node-positions", + "node-quaternions", + "node-scales", + "mesh-info", + "materials", + "cameras", + ].map((resource, binding) => ({ group: 0, binding, resource })), + color: [ + { + resource: "hdr", + ...(firstRender + ? { clear: [0.015, 0.025, 0.05, 1] } + : { load: "load" }), + }, + ], vertexBuffers: [{ slot: 0, resource: vertex }], - ...(indexed ? { indexBuffer: { resource: `geometry-${mesh.geometryId}-indices`, format: "uint32" } } : {}), + ...(indexed + ? { + indexBuffer: { + resource: `geometry-${mesh.geometryId}-indices`, + format: "uint32", + }, + } + : {}), draw: indexed - ? { indices: draw.count, firstIndex: draw.firstIndex, instances: 1, firstInstance: instance } + ? { + indices: draw.count, + firstIndex: draw.firstIndex, + instances: 1, + firstInstance: instance, + } : { vertices: draw.count, instances: 1, firstInstance: instance }, }); firstRender = false; @@ -522,11 +775,22 @@ export class Scene { for (const material of this.#shaders.values()) { const id = `shader-${material.id}`; renderPipelines.push({ - id, code: material.code, + id, + code: material.code, vertex: { entry: material.vertexEntry }, - fragment: { entry: material.fragmentEntry, targets: [{ format: hdrFormat }] }, + fragment: { + entry: material.fragmentEntry, + targets: [{ format: hdrFormat }], + }, + }); + passes.push({ + id, + type: "render", + pipeline: id, + after: previous, + color: [{ resource: "hdr", load: "load" }], + draw: { vertices: 3 }, }); - passes.push({ id, type: "render", pipeline: id, after: previous, color: [{ resource: "hdr", load: "load" }], draw: { vertices: 3 } }); previous = [id]; } @@ -534,12 +798,30 @@ export class Scene { for (const [index, effect] of [...this.#effects.values()].entries()) { const output = `post-${index}`; const pipeline = `post-${effect.id}`; - addTexture({ id: output, format: hdrFormat, size: ["canvas", "canvas", 1], usage: ["render", "sampled"], transient: true }); - renderPipelines.push({ id: pipeline, code: effectFragment(effect.kind, effect.options), vertex: { entry: "vertex" }, fragment: { entry: "fragment", targets: [{ format: hdrFormat }] } }); + addTexture({ + id: output, + format: hdrFormat, + size: ["canvas", "canvas", 1], + usage: ["render", "sampled"], + transient: true, + }); + renderPipelines.push({ + id: pipeline, + code: effectFragment(effect.kind, effect.options), + vertex: { entry: "vertex" }, + fragment: { entry: "fragment", targets: [{ format: hdrFormat }] }, + }); passes.push({ - id: pipeline, type: "render", pipeline, after: previous, - bindings: [{ group: 0, binding: 0, resource: input }, { group: 0, binding: 1, resource: "linear" }], - color: [{ resource: output, clear: [0, 0, 0, 1] }], draw: { vertices: 3 }, + id: pipeline, + type: "render", + pipeline, + after: previous, + bindings: [ + { group: 0, binding: 0, resource: input }, + { group: 0, binding: 1, resource: "linear" }, + ], + color: [{ resource: output, clear: [0, 0, 0, 1] }], + draw: { vertices: 3 }, }); input = output; previous = [pipeline]; @@ -554,16 +836,39 @@ export class Scene { entry: "main", }); passes.push({ - id, type: "compute", pipeline: id, after: ["cluster-lights"], dispatch: [1, 1, 1], - bindings: [{ group: 0, binding: 0, resource: texture.id }, { group: 0, binding: 1, resource: "clusters" }], + id, + type: "compute", + pipeline: id, + after: ["cluster-lights"], + dispatch: [1, 1, 1], + bindings: [ + { group: 0, binding: 0, resource: texture.id }, + { group: 0, binding: 1, resource: "clusters" }, + ], }); } - const toneMap = String([...this.#effects.values()].find((effect) => effect.kind === "colorGrading")?.options.toneMap ?? "aces"); - renderPipelines.push({ id: "present", code: presentShader(toneMap), vertex: { entry: "vertex" }, fragment: { entry: "fragment", targets: [{ format: "canvas" }] } }); + const toneMap = String( + [...this.#effects.values()].find( + (effect) => effect.kind === "colorGrading", + )?.options.toneMap ?? "aces", + ); + renderPipelines.push({ + id: "present", + code: presentShader(toneMap), + vertex: { entry: "vertex" }, + fragment: { entry: "fragment", targets: [{ format: "canvas" }] }, + }); passes.push({ - id: "present", type: "render", pipeline: "present", after: previous, - bindings: [{ group: 0, binding: 0, resource: input }, { group: 0, binding: 1, resource: "linear" }], - color: [{ resource: "canvas", clear: [0, 0, 0, 1] }], draw: { vertices: 3 }, + id: "present", + type: "render", + pipeline: "present", + after: previous, + bindings: [ + { group: 0, binding: 0, resource: input }, + { group: 0, binding: 1, resource: "linear" }, + ], + color: [{ resource: "canvas", clear: [0, 0, 0, 1] }], + draw: { vertices: 3 }, }); const graph = { @@ -571,7 +876,10 @@ export class Scene { resources: { buffers: [...buffers.values()], textures: [...textures.values()], - samplers: [...samplers.values()].map(({ id, ...descriptor }) => ({ id, descriptor })), + samplers: [...samplers.values()].map(({ id, ...descriptor }) => ({ + id, + descriptor, + })), }, pipelines: { render: renderPipelines, compute: computePipelines }, passes, diff --git a/addons/handles/src/bvh/picking.ts b/addons/handles/src/bvh/picking.ts index 0f6d87f..4dceeed 100644 --- a/addons/handles/src/bvh/picking.ts +++ b/addons/handles/src/bvh/picking.ts @@ -8,32 +8,56 @@ export class Picking { readonly ready: Promise; #worker: Worker; #next = 1; - #pending = new Map void; reject: (error: Error) => void }>(); + #pending = new Map< + number, + { resolve: (value: any) => void; reject: (error: Error) => void } + >(); constructor(scene: Scene) { this.scene = scene; - this.#worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", name: "yawn-bvh" }); + this.#worker = new Worker(new URL("./worker.ts", import.meta.url), { + type: "module", + name: "yawn-bvh", + }); this.#worker.addEventListener("message", ({ data }) => { const pending = this.#pending.get(data.request); if (!pending) return; this.#pending.delete(data.request); pending.resolve(data.hits); }); - this.#worker.addEventListener("error", () => this.#fail(new Error("BVH_WORKER_ERROR"))); + this.#worker.addEventListener("error", () => + this.#fail(new Error("BVH_WORKER_ERROR")), + ); this.ready = scene.ready.then(() => this.refresh()); } refresh() { return this.#request("sync", { - shares: Object.fromEntries(["info", "nodes", "nodePositions", "meshInfo", "bounds"] - .map((name) => [name, this.scene.array(name).share()])), + shares: Object.fromEntries( + [ + "info", + "nodes", + "nodePositions", + "nodeQuaternions", + "nodeScales", + "meshInfo", + "bounds", + ].map((name) => [name, this.scene.array(name).share()]), + ), }).then(() => undefined); } - async pick(origin: ArrayLike, direction: ArrayLike): Promise { + async pick( + origin: ArrayLike, + direction: ArrayLike, + ): Promise { await this.ready; - if (origin.length !== 3 || direction.length !== 3) throw new TypeError("Pick rays have three lanes"); - return this.#request("pick", { origin: Array.from(origin), direction: Array.from(direction) }); + if (origin.length !== 3 || direction.length !== 3) + throw new TypeError("Pick rays have three lanes"); + return this.#request("pick", { + origin: Array.from(origin), + direction: Array.from(direction), + }); } #request(type: string, payload: object) { diff --git a/addons/handles/src/bvh/worker.ts b/addons/handles/src/bvh/worker.ts index f9a126a..f67e12f 100644 --- a/addons/handles/src/bvh/worker.ts +++ b/addons/handles/src/bvh/worker.ts @@ -1,6 +1,17 @@ -type SharedRows = { buffer: SharedArrayBuffer; descriptor: { offset: number; rows: number; stride: number; format: string } }; +export {}; + +type SharedRows = { + buffer: SharedArrayBuffer; + descriptor: { offset: number; rows: number; stride: number; format: string }; +}; type Box = { id: number; min: number[]; max: number[] }; -type Branch = { min: number[]; max: number[]; boxes?: Box[]; left?: Branch; right?: Branch }; +type Branch = { + min: number[]; + max: number[]; + boxes?: Box[]; + left?: Branch; + right?: Branch; +}; let shares: Record = {}; let root: Branch | undefined; @@ -9,7 +20,7 @@ let builtFrame = -1; function view(name: string) { const share = shares[name]; if (!share) return undefined; - const length = share.descriptor.rows * share.descriptor.stride / 4; + const length = (share.descriptor.rows * share.descriptor.stride) / 4; return share.descriptor.format === "u32" ? new Uint32Array(share.buffer, share.descriptor.offset, length) : new Float32Array(share.buffer, share.descriptor.offset, length); @@ -18,10 +29,11 @@ function view(name: string) { function merge(boxes: Box[]) { const min = [Infinity, Infinity, Infinity]; const max = [-Infinity, -Infinity, -Infinity]; - for (const box of boxes) for (let lane = 0; lane < 3; lane++) { - min[lane] = Math.min(min[lane], box.min[lane]); - max[lane] = Math.max(max[lane], box.max[lane]); - } + for (const box of boxes) + for (let lane = 0; lane < 3; lane++) { + min[lane] = Math.min(min[lane], box.min[lane]); + max[lane] = Math.max(max[lane], box.max[lane]); + } return { min, max }; } @@ -31,32 +43,74 @@ function build(boxes: Box[]): Branch | undefined { if (boxes.length <= 4) return { ...bounds, boxes }; const extents = bounds.max.map((value, lane) => value - bounds.min[lane]); const axis = extents.indexOf(Math.max(...extents)); - boxes.sort((a, b) => (a.min[axis] + a.max[axis]) - (b.min[axis] + b.max[axis])); + boxes.sort((a, b) => a.min[axis] + a.max[axis] - (b.min[axis] + b.max[axis])); const middle = Math.ceil(boxes.length / 2); - return { ...bounds, left: build(boxes.slice(0, middle)), right: build(boxes.slice(middle)) }; + return { + ...bounds, + left: build(boxes.slice(0, middle)), + right: build(boxes.slice(middle)), + }; +} + +function rotate(quaternion: number[], value: number[]) { + const [qx, qy, qz, qw] = quaternion; + const [x, y, z] = value; + const tx = 2 * (qy * z - qz * y); + const ty = 2 * (qz * x - qx * z); + const tz = 2 * (qx * y - qy * x); + return [ + x + qw * tx + qy * tz - qz * ty, + y + qw * ty + qz * tx - qx * tz, + z + qw * tz + qx * ty - qy * tx, + ]; } function rebuild() { const bounds = view("bounds"); const positions = view("nodePositions"); + const quaternions = view("nodeQuaternions"); + const scales = view("nodeScales"); const meshes = view("meshInfo"); const nodes = view("nodes"); - if (!bounds || !positions || !meshes || !nodes) return; + if (!bounds || !positions || !quaternions || !scales || !meshes || !nodes) + return; const count = shares.bounds.descriptor.rows; const boxes: Box[] = []; for (let id = 0; id < count; id++) { if (!nodes[id * 4] || !meshes[id * 4 + 2]) continue; const offset = id * 8; - const translation = id * 4; - const min = [0, 1, 2].map((lane) => Number(bounds[offset + lane]) + Number(positions[translation + lane])); - const max = [0, 1, 2].map((lane) => Number(bounds[offset + 4 + lane]) + Number(positions[translation + lane])); - if (min.every(Number.isFinite) && max.every(Number.isFinite)) boxes.push({ id, min, max }); + const transform = id * 4; + const min = [Infinity, Infinity, Infinity]; + const max = [-Infinity, -Infinity, -Infinity]; + const quaternion = [0, 1, 2, 3].map((lane) => + Number(quaternions[transform + lane]), + ); + for (let corner = 0; corner < 8; corner++) { + const local = [0, 1, 2].map( + (lane) => + Number(bounds[offset + (corner & (1 << lane) ? 4 : 0) + lane]) * + Number(scales[transform + lane]), + ); + const rotated = rotate(quaternion, local); + for (let lane = 0; lane < 3; lane++) { + const value = rotated[lane] + Number(positions[transform + lane]); + min[lane] = Math.min(min[lane], value); + max[lane] = Math.max(max[lane], value); + } + } + if (min.every(Number.isFinite) && max.every(Number.isFinite)) + boxes.push({ id, min, max }); } root = build(boxes); builtFrame = Number(view("info")?.[1] ?? builtFrame + 1); } -function intersection(origin: number[], inverse: number[], min: number[], max: number[]) { +function intersection( + origin: number[], + inverse: number[], + min: number[], + max: number[], +) { let near = -Infinity; let far = Infinity; for (let lane = 0; lane < 3; lane++) { @@ -68,8 +122,17 @@ function intersection(origin: number[], inverse: number[], min: number[], max: n return far >= Math.max(near, 0) ? Math.max(near, 0) : Infinity; } -function trace(branch: Branch | undefined, origin: number[], inverse: number[], hits: { id: number; distance: number }[]) { - if (!branch || !Number.isFinite(intersection(origin, inverse, branch.min, branch.max))) return; +function trace( + branch: Branch | undefined, + origin: number[], + inverse: number[], + hits: { id: number; distance: number }[], +) { + if ( + !branch || + !Number.isFinite(intersection(origin, inverse, branch.min, branch.max)) + ) + return; for (const box of branch.boxes ?? []) { const distance = intersection(origin, inverse, box.min, box.max); if (Number.isFinite(distance)) hits.push({ id: box.id, distance }); diff --git a/addons/handles/src/importers/gltf.ts b/addons/handles/src/importers/gltf.ts index c0dc1b2..66fff50 100644 --- a/addons/handles/src/importers/gltf.ts +++ b/addons/handles/src/importers/gltf.ts @@ -4,11 +4,17 @@ import { PBRMaterial } from "../materials/PBRMaterial"; let worker: Worker | undefined; let nextRequest = 1; -const pending = new Map void; reject: (error: Error) => void }>(); +const pending = new Map< + number, + { resolve: (value: any) => void; reject: (error: Error) => void } +>(); function importer() { if (worker) return worker; - worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", name: "yawn-importer" }); + worker = new Worker(new URL("./worker.ts", import.meta.url), { + type: "module", + name: "yawn-importer", + }); worker.addEventListener("message", ({ data }) => { const request = pending.get(data.request); if (!request) return; @@ -17,7 +23,8 @@ function importer() { else request.resolve(data.result); }); worker.addEventListener("error", () => { - for (const request of pending.values()) request.reject(new Error("IMPORT_WORKER_ERROR")); + for (const request of pending.values()) + request.reject(new Error("IMPORT_WORKER_ERROR")); pending.clear(); }); return worker; @@ -31,26 +38,33 @@ export async function importGltf(scene: Scene, url: string | URL) { pending.set(request, { resolve, reject }); importer().postMessage({ request, url: String(url) }); }); - const materials = result.materials.map((options: any) => new PBRMaterial(scene, options)); - await Promise.all(materials.map((material: PBRMaterial) => material.ready)); - const meshes: Mesh[] = []; - for (const primitive of result.primitives) { - const mesh = new Mesh(scene, { - position: primitive.position, - quaternion: primitive.quaternion, - scale: primitive.scale, - material: materials[primitive.material], - vertexData: { - positions: primitive.positions, - indices: primitive.indices, - ...(primitive.normals ? { normals: primitive.normals } : {}), - ...(primitive.tangents ? { tangents: primitive.tangents } : {}), - ...(primitive.uvs ? { uvs: primitive.uvs } : {}), - ...(primitive.colors ? { colors: primitive.colors } : {}), - }, - }); - await mesh.ready; - meshes.push(mesh); - } - return meshes; + await scene.reserve({ + nodes: result.primitives.length, + materials: result.materials.length, + }); + return scene.batchGraphUpdates(async () => { + const materials = result.materials.map( + (options: any) => new PBRMaterial(scene, options), + ); + await Promise.all(materials.map((material: PBRMaterial) => material.ready)); + const meshes: Mesh[] = result.primitives.map( + (primitive: any) => + new Mesh(scene, { + position: primitive.position, + quaternion: primitive.quaternion, + scale: primitive.scale, + material: materials[primitive.material], + vertexData: { + positions: primitive.positions, + indices: primitive.indices, + ...(primitive.normals ? { normals: primitive.normals } : {}), + ...(primitive.tangents ? { tangents: primitive.tangents } : {}), + ...(primitive.uvs ? { uvs: primitive.uvs } : {}), + ...(primitive.colors ? { colors: primitive.colors } : {}), + }, + }), + ); + await Promise.all(meshes.map((mesh) => mesh.ready)); + return meshes; + }); } diff --git a/addons/handles/src/importers/worker.ts b/addons/handles/src/importers/worker.ts index abe7d9f..d3a7225 100644 --- a/addons/handles/src/importers/worker.ts +++ b/addons/handles/src/importers/worker.ts @@ -1,6 +1,23 @@ +export {}; + const decoder = new TextDecoder(); const widths: Record = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }; -const sizes: Record = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }; +const sizes: Record = { + 5120: 1, + 5121: 1, + 5122: 2, + 5123: 2, + 5125: 4, + 5126: 4, +}; +const arrays: Record = { + 5120: Int8Array, + 5121: Uint8Array, + 5122: Int16Array, + 5123: Uint16Array, + 5125: Uint32Array, + 5126: Float32Array, +}; function component(view: DataView, offset: number, type: number) { if (type === 5120) return view.getInt8(offset); @@ -12,10 +29,88 @@ function component(view: DataView, offset: number, type: number) { throw new Error("GLTF_COMPONENT"); } +function rotate(quaternion: number[], value: number[]) { + const [qx, qy, qz, qw] = quaternion; + const [x, y, z] = value; + const tx = 2 * (qy * z - qz * y); + const ty = 2 * (qz * x - qx * z); + const tz = 2 * (qx * y - qy * x); + return [ + x + qw * tx + qy * tz - qz * ty, + y + qw * ty + qz * tx - qx * tz, + z + qw * tz + qx * ty - qy * tx, + ]; +} + +function multiply(a: number[], b: number[]) { + return [ + a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1], + a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0], + a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3], + a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2], + ]; +} + +function decompose(matrix: number[]) { + const scale = [ + Math.hypot(matrix[0], matrix[1], matrix[2]), + Math.hypot(matrix[4], matrix[5], matrix[6]), + Math.hypot(matrix[8], matrix[9], matrix[10]), + ]; + const [sx, sy, sz] = scale; + const [m00, m01, m02] = [matrix[0] / sx, matrix[4] / sy, matrix[8] / sz]; + const [m10, m11, m12] = [matrix[1] / sx, matrix[5] / sy, matrix[9] / sz]; + const [m20, m21, m22] = [matrix[2] / sx, matrix[6] / sy, matrix[10] / sz]; + let quaternion: number[]; + if (m00 + m11 + m22 > 0) { + const s = Math.sqrt(1 + m00 + m11 + m22) * 2; + quaternion = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4]; + } else if (m00 > m11 && m00 > m22) { + const s = Math.sqrt(1 + m00 - m11 - m22) * 2; + quaternion = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s]; + } else if (m11 > m22) { + const s = Math.sqrt(1 + m11 - m00 - m22) * 2; + quaternion = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s]; + } else { + const s = Math.sqrt(1 + m22 - m00 - m11) * 2; + quaternion = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s]; + } + return { position: matrix.slice(12, 15), quaternion, scale }; +} + +function transform(node: any) { + return node.matrix + ? decompose(node.matrix) + : { + position: node.translation ?? [0, 0, 0], + quaternion: node.rotation ?? [0, 0, 0, 1], + scale: node.scale ?? [1, 1, 1], + }; +} + +function compose(parent: any, local: any) { + const position = rotate( + parent.quaternion, + local.position.map( + (value: number, lane: number) => value * parent.scale[lane], + ), + ); + return { + position: position.map((value, lane) => value + parent.position[lane]), + quaternion: multiply(parent.quaternion, local.quaternion), + scale: local.scale.map( + (value: number, lane: number) => value * parent.scale[lane], + ), + }; +} + function parse(bytes: Uint8Array) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (view.getUint32(0, true) !== 0x46546c67) - return { document: JSON.parse(decoder.decode(bytes)), binary: undefined as Uint8Array | undefined }; + return { + document: JSON.parse(decoder.decode(bytes)), + binary: undefined as Uint8Array | undefined, + }; let offset = 12; let document: any; let binary: Uint8Array | undefined; @@ -23,7 +118,8 @@ function parse(bytes: Uint8Array) { const length = view.getUint32(offset, true); const type = view.getUint32(offset + 4, true); const chunk = bytes.subarray(offset + 8, offset + 8 + length); - if (type === 0x4e4f534a) document = JSON.parse(decoder.decode(chunk).replace(/\0+$/u, "")); + if (type === 0x4e4f534a) + document = JSON.parse(decoder.decode(chunk).replace(/\0+$/u, "")); if (type === 0x004e4942) binary = chunk; offset += 8 + length; } @@ -34,16 +130,20 @@ function parse(bytes: Uint8Array) { async function load(url: string) { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP_${response.status}`); - const { document, binary } = parse(new Uint8Array(await response.arrayBuffer())); - const buffers = await Promise.all((document.buffers ?? []).map(async (buffer: any, index: number) => { - if (buffer.uri === undefined) { - if (index || !binary) throw new Error("GLTF_BUFFER"); - return binary; - } - const result = await fetch(new URL(buffer.uri, url)); - if (!result.ok) throw new Error(`HTTP_${result.status}`); - return new Uint8Array(await result.arrayBuffer()); - })); + const { document, binary } = parse( + new Uint8Array(await response.arrayBuffer()), + ); + const buffers = await Promise.all( + (document.buffers ?? []).map(async (buffer: any, index: number) => { + if (buffer.uri === undefined) { + if (index || !binary) throw new Error("GLTF_BUFFER"); + return binary; + } + const result = await fetch(new URL(buffer.uri, url)); + if (!result.ok) throw new Error(`HTTP_${result.status}`); + return new Uint8Array(await result.arrayBuffer()); + }), + ); const accessor = (id: number, integer = false) => { const source = document.accessors[id]; @@ -54,15 +154,40 @@ async function load(url: string) { const start = (bufferView.byteOffset ?? 0) + (source.byteOffset ?? 0); const stride = bufferView.byteStride ?? width * size; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - const values = integer ? new Uint32Array(source.count * width) : new Float32Array(source.count * width); - for (let item = 0; item < source.count; item++) for (let lane = 0; lane < width; lane++) { - let value = component(view, start + item * stride + lane * size, source.componentType); - if (!integer && source.normalized) { - const maximum = source.componentType === 5121 ? 255 : source.componentType === 5123 ? 65535 : 1; - value /= maximum; - } - values[item * width + lane] = value; + const values = integer + ? new Uint32Array(source.count * width) + : new Float32Array(source.count * width); + if ( + !source.normalized && + stride === width * size && + (bytes.byteOffset + start) % size === 0 + ) { + const packed = new arrays[source.componentType]( + bytes.buffer, + bytes.byteOffset + start, + source.count * width, + ); + values.set(packed); + return values; } + for (let item = 0; item < source.count; item++) + for (let lane = 0; lane < width; lane++) { + let value = component( + view, + start + item * stride + lane * size, + source.componentType, + ); + if (!integer && source.normalized) { + const maximum = + source.componentType === 5121 + ? 255 + : source.componentType === 5123 + ? 65535 + : 1; + value /= maximum; + } + values[item * width + lane] = value; + } return values; }; @@ -80,18 +205,34 @@ async function load(url: string) { const emitMesh = (meshId: number, transform: any) => { const mesh = document.meshes?.[meshId]; for (const primitive of mesh?.primitives ?? []) { - if ((primitive.mode ?? 4) !== 4 || primitive.attributes.POSITION === undefined) continue; + if ( + (primitive.mode ?? 4) !== 4 || + primitive.attributes.POSITION === undefined + ) + continue; const positions = accessor(primitive.attributes.POSITION); - const indices = primitive.indices === undefined - ? Uint32Array.from({ length: positions.length / 3 }, (_, index) => index) - : accessor(primitive.indices, true); + const indices = + primitive.indices === undefined + ? Uint32Array.from( + { length: positions.length / 3 }, + (_, index) => index, + ) + : accessor(primitive.indices, true); primitives.push({ positions, indices, - ...(primitive.attributes.NORMAL === undefined ? {} : { normals: accessor(primitive.attributes.NORMAL) }), - ...(primitive.attributes.TANGENT === undefined ? {} : { tangents: accessor(primitive.attributes.TANGENT) }), - ...(primitive.attributes.TEXCOORD_0 === undefined ? {} : { uvs: accessor(primitive.attributes.TEXCOORD_0) }), - ...(primitive.attributes.COLOR_0 === undefined ? {} : { colors: accessor(primitive.attributes.COLOR_0) }), + ...(primitive.attributes.NORMAL === undefined + ? {} + : { normals: accessor(primitive.attributes.NORMAL) }), + ...(primitive.attributes.TANGENT === undefined + ? {} + : { tangents: accessor(primitive.attributes.TANGENT) }), + ...(primitive.attributes.TEXCOORD_0 === undefined + ? {} + : { uvs: accessor(primitive.attributes.TEXCOORD_0) }), + ...(primitive.attributes.COLOR_0 === undefined + ? {} + : { colors: accessor(primitive.attributes.COLOR_0) }), material: primitive.material ?? -1, ...transform, }); @@ -101,17 +242,28 @@ async function load(url: string) { const nodes = document.nodes ?? []; const scene = document.scenes?.[document.scene ?? 0]; const children = new Set(nodes.flatMap((node: any) => node.children ?? [])); - const roots = scene?.nodes ?? nodes.map((_: any, id: number) => id).filter((id: number) => !children.has(id)); - const visit = (id: number, parent = { position: [0, 0, 0], scale: [1, 1, 1] }) => { + const roots = + scene?.nodes ?? + nodes + .map((_: any, id: number) => id) + .filter((id: number) => !children.has(id)); + const visit = ( + id: number, + parent = { + position: [0, 0, 0], + quaternion: [0, 0, 0, 1], + scale: [1, 1, 1], + }, + ) => { const node = nodes[id] ?? {}; - const position = (node.translation ?? [0, 0, 0]).map((value: number, lane: number) => value + parent.position[lane]); - const scale = (node.scale ?? [1, 1, 1]).map((value: number, lane: number) => value * parent.scale[lane]); - const transform = { position, scale, quaternion: node.rotation ?? [0, 0, 0, 1] }; - if (node.mesh !== undefined) emitMesh(node.mesh, transform); - for (const child of node.children ?? []) visit(child, transform); + const world = compose(parent, transform(node)); + if (node.mesh !== undefined) emitMesh(node.mesh, world); + for (const child of node.children ?? []) visit(child, world); }; for (const root of roots) visit(root); - if (!nodes.length) for (let id = 0; id < (document.meshes ?? []).length; id++) emitMesh(id, {}); + if (!nodes.length) + for (let id = 0; id < (document.meshes ?? []).length; id++) + emitMesh(id, {}); return { materials, primitives }; } @@ -120,9 +272,14 @@ addEventListener("message", async ({ data }) => { const result = await load(data.url); const transfers = result.primitives.flatMap((primitive: any) => ["positions", "indices", "normals", "tangents", "uvs", "colors"] - .map((name) => primitive[name]?.buffer).filter(Boolean)); + .map((name) => primitive[name]?.buffer) + .filter(Boolean), + ); (postMessage as any)({ request: data.request, result }, transfers); } catch (error) { - postMessage({ request: data.request, error: error instanceof Error ? error.message : "GLTF_IMPORT" }); + postMessage({ + request: data.request, + error: error instanceof Error ? error.message : "GLTF_IMPORT", + }); } }); diff --git a/docs/.vitepress/Playground.vue b/docs/.vitepress/Playground.vue index 2d7bf94..31bc9b7 100644 --- a/docs/.vitepress/Playground.vue +++ b/docs/.vitepress/Playground.vue @@ -1,98 +1,324 @@