lccWorker.ts
examples/lcc/lccWorker.ts · raw · open the example · all examples · site home
Exports: LccRequestLccReply
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
// The decode worker of LccDecoders.ts: it reads a block of an LCC scene
// (its range of data.bin and, with SH, twice that range of shcoef.bin), or
// the whole environment.bin, and decodes it into ext splats with Radix's
// CPU encoder (lccDecode.ts). A module worker has no import map, so Radix
// core comes from dist/radix-core.module.js, at the URL the page
// resolves.
// Messages (LccRequest / LccReply):
// main -> worker
// { type: "init", coreUrl }
// { type: "decode", id, source, block, sh, ranges, normal? }
// { type: "environment", id, source, sh, ranges, normal? }
// { type: "chunk", id, source, paths, sh } an LCC2 .spz / .ply chunk
// (the first of `paths` there)
// { type: "lciHeader", id, source } collision.lci's header
// { type: "lciMesh", id, source, mesh } one of its meshes
// { type: "lcc2Collision", id, source, node, ply, btree }
// an LCC2 node's mesh + BVH
// { type: "cancel", id }
// worker -> main
// { type: "decoded", id, data, ms, bytes } data: TransferableArrayData,
// its arrays transferred (a
// chunk's meta.lccSh: whether
// it has SH, in extSh)
// { type: "missing", id } no environment.bin (or
// collision.lci)
// { type: "value", id, value } a header, or a CollisionChunk
// (its arrays transferred)
// { type: "error", id, message } ("AbortError" when cancelled)
import type * as Core from "@sparkjsdev/radix/core";
import { lciHeaderLength, readLcc2Collision, readLciHeader, readLciMesh, type CollisionChunk, type LciMesh } from "./collision.js";
import { decodeChunk } from "./lccChunk.js";
import { decodeBlock, splitEnvironment } from "./lccDecode.js";
import { readOptional, readRange, type LccBlock, type LccRanges, type LccSource } from "./LccScene.js";
type Normal = { min: number[]; max: number[] };
export type LccRequest =
| { type: "init"; coreUrl: string }
| { type: "decode"; id: number; source: LccSource; block: LccBlock; sh: boolean; ranges: LccRanges; normal?: Normal }
| { type: "environment"; id: number; source: LccSource; sh: boolean; ranges: LccRanges; normal?: Normal }
| { type: "chunk"; id: number; source: LccSource; paths: string[]; sh: boolean }
| { type: "lciHeader"; id: number; source: LccSource }
| { type: "lciMesh"; id: number; source: LccSource; mesh: LciMesh }
| { type: "lcc2Collision"; id: number; source: LccSource; node: string; ply: string[]; btree: string[] }
| { type: "cancel"; id: number };
export type LccReply =
| { type: "decoded"; id: number; data: Core.TransferableArrayData; ms: number; bytes: number }
| { type: "missing"; id: number }
| { type: "value"; id: number; value: unknown }
| { type: "error"; id: number; message: string };
type DecodeRequest = Extract<LccRequest, { type: "decode" | "environment" }>;
type CollisionRequest = Extract<LccRequest, { type: "lciHeader" | "lciMesh" | "lcc2Collision" }>;
const LCI = "collision.lci";
// A collision request's answer: undefined when the scene has no collision.lci
async function collision(request: CollisionRequest, signal: AbortSignal): Promise<{ value?: unknown; transfer: ArrayBuffer[] }> {
const { source } = request;
if (request.type === "lciHeader") {
let head: Uint8Array;
try {
head = await readRange(source, LCI, 0, 4096, signal);
} catch (error) {
if (error instanceof Error && /HTTP 40[34]|No collision\.lci/i.test(error.message)) {
return { transfer: [] };
}
throw error;
}
const length = lciHeaderLength(head);
const bytes = head.byteLength >= length ? head : await readRange(source, LCI, 0, length, signal);
return { value: readLciHeader(bytes), transfer: [] };
}
let chunk: CollisionChunk;
if (request.type === "lciMesh") {
const { mesh } = request;
chunk = readLciMesh(await readRange(source, LCI, mesh.offset, mesh.offset + mesh.bytes, signal), mesh);
} else {
const [ply, btree] = await Promise.all([readFirst(source, request.ply, signal), readFirst(source, request.btree, signal)]);
chunk = readLcc2Collision(request.node, ply.bytes, btree.bytes);
}
const buffers = new Set([chunk.vertices.buffer, chunk.faces.buffer, chunk.bvh.buffer, chunk.box.buffer] as ArrayBuffer[]);
return { value: chunk, transfer: [...buffers] };
}
// The first of `paths` the source has
async function readFirst(source: LccSource, paths: string[], signal: AbortSignal) {
for (const path of paths) {
const bytes = await readOptional(source, path, signal);
if (bytes) {
return { bytes, path };
}
}
throw new Error(`${paths[0]}: not found`);
}
const scope = globalThis as unknown as { postMessage(message: unknown, transfer: ArrayBuffer[]): void; onmessage: ((event: MessageEvent) => void) | null };
let core: Promise<typeof Core> | undefined;
const pending = new Map<number, AbortController>();
// The splats and SH bytes a request reads, or undefined for a missing environment
async function read(request: DecodeRequest, signal: AbortSignal) {
const { source, sh } = request;
if (request.type === "environment") {
const bytes = await readOptional(source, "environment.bin", signal);
return bytes && { ...splitEnvironment(bytes, sh), bytes: bytes.byteLength };
}
const { block } = request;
const [data, shBytes] = await Promise.all([
readRange(source, "data.bin", block.offset, block.offset + block.size, signal),
sh ? readRange(source, "shcoef.bin", block.offset * 2, (block.offset + block.size) * 2, signal) : undefined,
]);
return { count: block.count, data, sh: shBytes, bytes: data.byteLength + (shBytes?.byteLength ?? 0) };
}
async function decode(request: Exclude<LccRequest, { type: "init" | "cancel" }>) {
const { id } = request;
const controller = new AbortController();
pending.set(id, controller);
const aborted = () => {
if (controller.signal.aborted) {
throw new DOMException("Cancelled", "AbortError");
}
};
try {
const start = performance.now();
let data: Core.ArrayData;
let bytes: number;
if (request.type === "lciHeader" || request.type === "lciMesh" || request.type === "lcc2Collision") {
const { value, transfer } = await collision(request, controller.signal);
aborted();
scope.postMessage(value === undefined ? { type: "missing", id } satisfies LccReply : { type: "value", id, value } satisfies LccReply, transfer);
return;
}
const module = await core!;
if (request.type === "chunk") {
const file = await readFirst(request.source, request.paths, controller.signal);
aborted();
const chunk = await decodeChunk(module, file.bytes, file.path, request.sh);
data = chunk.data;
data.meta.lccSh = chunk.sh;
bytes = file.bytes.byteLength;
} else {
const input = await read(request, controller.signal);
aborted();
if (!input) {
scope.postMessage({ type: "missing", id } satisfies LccReply, []);
return;
}
data = decodeBlock(module, input.data, input.sh, input.count, request.ranges, request.normal);
bytes = input.bytes;
}
const decoded = data.toTransferable();
const reply: LccReply = { type: "decoded", id, data: decoded, ms: performance.now() - start, bytes };
scope.postMessage(reply, module.ArrayData.transferables(decoded));
} catch (error) {
const message = error instanceof DOMException && error.name === "AbortError" ? "AbortError" : error instanceof Error ? error.message : String(error);
scope.postMessage({ type: "error", id, message } satisfies LccReply, []);
} finally {
pending.delete(id);
}
}
scope.onmessage = (event: MessageEvent<LccRequest>) => {
const request = event.data;
if (request.type === "init") {
core = import(request.coreUrl) as Promise<typeof Core>;
} else if (request.type === "cancel") {
pending.get(request.id)?.abort();
} else {
void decode(request);
}
};