Files
yawn/addons/handles/src/importers/gltf.ts
T
Ampandheaust f324ff6bb2 handles: one interleaved vertices/indices arena
Geometry becomes a span in a single 80-byte interleaved `vertices` array
plus a span in a single `indices` array, allocated through the core's
span allocator instead of one arena array per mesh attribute. Draws are
always indexed with baseVertex; face-material overrides emit one draw per
run of same-material faces rather than one per face. Vertex data is
validated as a whole before any span is touched, same-geometry mutations
are serialised, and render signal 4 is held across a span change until
the graph reflecting it is published. Sibling arrays grow to the primary
array's capacity in one batch; reserve() takes vertex/index totals.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-09-18 07:00:36 +00:00

142 lines
5.4 KiB
TypeScript

import { Mesh } from "../Mesh";
import type { Scene } from "../Scene";
import { DirectionalLight } from "../lights/DirectionalLight";
import { PointLight } from "../lights/PointLight";
import { SpotLight } from "../lights/SpotLight";
import { PBRMaterial } from "../materials/PBRMaterial";
import { Texture } from "../materials/Texture";
let worker: Worker | undefined;
let nextRequest = 1;
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.addEventListener("message", ({ data }) => {
const request = pending.get(data.request);
if (!request) return;
pending.delete(data.request);
if (data.error) request.reject(new Error(data.error));
else request.resolve(data.result);
});
worker.addEventListener("error", () => {
for (const request of pending.values())
request.reject(new Error("IMPORT_WORKER_ERROR"));
pending.clear();
});
return worker;
}
/** Imports glTF/GLB off-thread, then hydrates conventional handles backed by Scene SAB rows. */
export async function importGltf(scene: Scene, url: string | URL) {
await scene.ready;
const request = nextRequest++;
const result = await new Promise<any>((resolve, reject) => {
pending.set(request, { resolve, reject });
importer().postMessage({ request, url: String(url) });
});
await scene.reserve({
nodes: result.primitives.length,
materials: result.materials.length,
vertices: result.primitives.reduce(
(total: number, primitive: any) => total + primitive.positions.length / 3,
0,
),
// Each geometry's index span is padded to whole rows of four.
indices: result.primitives.reduce(
(total: number, primitive: any) =>
total +
Math.ceil(
(primitive.indices?.length ?? primitive.positions.length / 3) / 4,
) *
4,
0,
),
});
let textures: Texture[] = [];
const imported = await scene.batchGraphUpdates(async () => {
const srgb = new Set<number>();
for (const material of result.materials) {
if (material.baseColorTexture >= 0) srgb.add(material.baseColorTexture);
if (material.emissiveTexture >= 0) srgb.add(material.emissiveTexture);
if (material.flags & 16 && material.metallicRoughnessTexture >= 0)
srgb.add(material.metallicRoughnessTexture);
if (material.specularColorTexture >= 0)
srgb.add(material.specularColorTexture);
}
textures = result.textures.map(
(
{ image, sampler }: { image: ImageBitmap; sampler: any },
index: number,
) =>
new Texture(scene, {
source: image,
format: srgb.has(index) ? "rgba8unorm-srgb" : "rgba8unorm",
sampler,
}),
);
await Promise.all(textures.map((texture: Texture) => texture.ready));
const materials = result.materials.map(
(options: any) =>
new PBRMaterial(scene, {
...options,
baseColorTexture: textures[options.baseColorTexture],
metallicRoughnessTexture: textures[options.metallicRoughnessTexture],
normalTexture: textures[options.normalTexture],
emissiveTexture: textures[options.emissiveTexture],
occlusionTexture: textures[options.occlusionTexture],
transmissionTexture: textures[options.transmissionTexture],
thicknessTexture: textures[options.thicknessTexture],
clearcoatTexture: textures[options.clearcoatTexture],
iridescenceTexture: textures[options.iridescenceTexture],
iridescenceThicknessTexture:
textures[options.iridescenceThicknessTexture],
anisotropyTexture: textures[options.anisotropyTexture],
specularTexture: textures[options.specularTexture],
specularColorTexture: textures[options.specularColorTexture],
}),
);
await Promise.all(materials.map((material: PBRMaterial) => material.ready));
const meshes: Mesh[] = result.primitives.map(
(primitive: any) =>
new Mesh(scene, {
position: primitive.position,
rotor: primitive.rotor,
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.uvs2 ? { uvs2: primitive.uvs2 } : {}),
...(primitive.colors ? { colors: primitive.colors } : {}),
},
}),
);
await Promise.all(meshes.map((mesh) => mesh.ready));
const lights = (result.lights ?? []).map((options: any) => {
if (options.type === "directional")
return new DirectionalLight(scene, options);
if (options.type === "spot") return new SpotLight(scene, options);
return new PointLight(scene, options);
});
await Promise.all(lights.map((light: any) => light.ready));
return Object.assign(meshes, { camera: result.camera });
});
await Promise.all(
result.textures.map((_: unknown, index: number) =>
scene.core.deleteTexture(textures[index].resource),
),
);
return imported;
}