Replace addons with conventional handles
Provide the single-loadout Scene API, SAB-backed meshes, cameras, materials, lights, workers, post effects, and tutorial playgrounds. Batch matching row growth so active GPU loadouts refresh once. Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { Mesh } from "../Mesh";
|
||||
import type { Scene } from "../Scene";
|
||||
import { PBRMaterial } from "../materials/PBRMaterial";
|
||||
|
||||
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) });
|
||||
});
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
const decoder = new TextDecoder();
|
||||
const widths: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
|
||||
const sizes: Record<number, number> = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
||||
|
||||
function component(view: DataView, offset: number, type: number) {
|
||||
if (type === 5120) return view.getInt8(offset);
|
||||
if (type === 5121) return view.getUint8(offset);
|
||||
if (type === 5122) return view.getInt16(offset, true);
|
||||
if (type === 5123) return view.getUint16(offset, true);
|
||||
if (type === 5125) return view.getUint32(offset, true);
|
||||
if (type === 5126) return view.getFloat32(offset, true);
|
||||
throw new Error("GLTF_COMPONENT");
|
||||
}
|
||||
|
||||
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 };
|
||||
let offset = 12;
|
||||
let document: any;
|
||||
let binary: Uint8Array | undefined;
|
||||
while (offset < bytes.length) {
|
||||
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 === 0x004e4942) binary = chunk;
|
||||
offset += 8 + length;
|
||||
}
|
||||
if (!document) throw new Error("GLTF_JSON");
|
||||
return { document, binary };
|
||||
}
|
||||
|
||||
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 accessor = (id: number, integer = false) => {
|
||||
const source = document.accessors[id];
|
||||
const width = widths[source.type];
|
||||
const size = sizes[source.componentType];
|
||||
const bufferView = document.bufferViews[source.bufferView];
|
||||
const bytes = buffers[bufferView.buffer];
|
||||
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;
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
const materials = (document.materials ?? []).map((material: any) => {
|
||||
const pbr = material.pbrMetallicRoughness ?? {};
|
||||
return {
|
||||
baseColor: pbr.baseColorFactor ?? [1, 1, 1, 1],
|
||||
metallic: pbr.metallicFactor ?? 0,
|
||||
roughness: pbr.roughnessFactor ?? 0.7,
|
||||
emissive: material.emissiveFactor ?? [0, 0, 0],
|
||||
alphaCutoff: material.alphaCutoff ?? 0.5,
|
||||
};
|
||||
});
|
||||
const primitives: any[] = [];
|
||||
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;
|
||||
const positions = accessor(primitive.attributes.POSITION);
|
||||
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) }),
|
||||
material: primitive.material ?? -1,
|
||||
...transform,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 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);
|
||||
};
|
||||
for (const root of roots) visit(root);
|
||||
if (!nodes.length) for (let id = 0; id < (document.meshes ?? []).length; id++) emitMesh(id, {});
|
||||
return { materials, primitives };
|
||||
}
|
||||
|
||||
addEventListener("message", async ({ data }) => {
|
||||
try {
|
||||
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));
|
||||
(postMessage as any)({ request: data.request, result }, transfers);
|
||||
} catch (error) {
|
||||
postMessage({ request: data.request, error: error instanceof Error ? error.message : "GLTF_IMPORT" });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user