/*! * WasmGPU v0.10.0 * Released on Monday, August 31, 2026 * WebGPU × WebAssembly rendering and computing engine for scientific workloads in the browser * Copyright (c) Zushah and contributors * SPDX-License-Identifier: MPL-2.0 * Source: https://github.com/Zushah/WasmGPU * Website: https://zushah.github.io/WasmGPU */ // typescript/utils/index.ts var assert = (cond, msg) => { if (!cond) throw new Error(msg); }; var alignTo = (n, alignment) => Math.ceil(n / alignment) * alignment; var clamp = (x, lo, hi) => x < lo ? lo : x > hi ? hi : x; var clamp01 = (x) => clamp(x, 0, 1); var clampInt = (value, min, max) => { if (!Number.isFinite(value)) return min; return Math.max(min, Math.min(max, Math.round(value))); }; var lerp = (a, b, t) => a + (b - a) * t; var ceilDiv = (n, d) => { assert(Number.isFinite(n) && Number.isFinite(d), "ceilDiv expects finite numbers"); assert(d !== 0, "ceilDiv divisor must be non-zero"); return Math.floor((n + d - 1) / d); }; var isPositiveInt = (n) => Number.isInteger(n) && n > 0; var isNonNegativeInt = (n) => Number.isInteger(n) && n >= 0; var finiteOr = (x, fallback) => typeof x === "number" && Number.isFinite(x) ? x : fallback; var intOr = (x, fallback) => typeof x === "number" && Number.isInteger(x) ? x : fallback; var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); var isGPUBuffer = (x) => typeof x === "object" && x !== null && x.mapState !== void 0; var resolveGPUBuffer = (x) => isGPUBuffer(x) ? x : x.buffer; var normalizePositiveIntShape = (shape, label = "shape") => { if (!shape) return null; const out = []; for (let i = 0; i < shape.length; i++) { const d = shape[i]; assert(Number.isInteger(d) && d > 0, `${label}[${i}] must be an integer > 0.`); out.push(d | 0); } return out.length > 0 ? out : null; }; var linearIndexToNdIndex = (shape, index) => { if (!shape || shape.length === 0) return null; if (!Number.isInteger(index) || index < 0) return null; let remaining = index | 0; const out = new Array(shape.length); for (let i = shape.length - 1; i >= 0; i--) { const dim = shape[i]; out[i] = remaining % dim; remaining = Math.floor(remaining / dim); } return remaining === 0 ? out : null; }; var normalizeColorStops = (stops, fallback = [[0, 0, 0, 1], [1, 1, 1, 1]], maxStops = 8) => { const source = !stops || stops.length === 0 ? fallback : stops; const limit = Math.max(0, maxStops | 0); const count = Math.min(limit, Math.max(2, source.length)); const out = []; for (let i = 0; i < count; i++) { const c = source[Math.min(i, source.length - 1)] ?? fallback[Math.min(i, fallback.length - 1)] ?? [0, 0, 0, 1]; out.push([c[0], c[1], c[2], c[3]]); } return out; }; var sampleColorStops = (tIn, stopsIn, maxStops = 8) => { const count = Math.min(Math.max(2, maxStops | 0), Math.max(2, stopsIn.length)); const stops = normalizeColorStops(stopsIn, [[0, 0, 0, 1], [1, 1, 1, 1]], count); const x = clamp01(tIn) * (count - 1); const i0 = Math.floor(x); const i1 = Math.min(count - 1, i0 + 1); const f = x - i0; if (i0 >= count - 1) return stops[count - 1]; return [ lerp(stops[i0][0], stops[i1][0], f), lerp(stops[i0][1], stops[i1][1], f), lerp(stops[i0][2], stops[i1][2], f), lerp(stops[i0][3], stops[i1][3], f) ]; }; var createBuffer = (device, data, usage, label) => { const buffer = device.createBuffer({ label, size: alignTo(data.byteLength, 4), usage, mappedAtCreation: true }); new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); buffer.unmap(); return buffer; }; var createDepthTexture = (device, width, height, sampleCount = 1) => device.createTexture({ size: { width, height, depthOrArrayLayers: 1 }, format: "depth24plus", sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT }); // typescript/wasm/driver.ts var HOST = null; var setWebAssemblyDriverHost = (wasm2, frameArena2) => { HOST = { wasm: wasm2, frameArena: frameArena2 }; }; var ensureHost = () => { if (!HOST) throw new Error("WebAssembly driver host not set. This is an internal error: WasmGPU/typescript/wasm/driver.ts should call setWebAssemblyDriverHost()."); return HOST; }; var HEAP_SLICE_FINALIZER = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((held) => { try { const { wasm: wasm2 } = ensureHost(); const ptr = held.ptr >>> 0; const len = held.length >>> 0; switch (held.dtype) { case "f32": wasm2.freeF32(ptr, len); break; case "f64": wasm2.freeF64(ptr, len); break; case "u32": wasm2.freeU32(ptr, len); break; case "i32": wasm2.freeU32(ptr, len); break; case "u8": wasm2.freeBytes(ptr, len); break; } } catch { } }) : null; var WasmSlice = class { kind; dtype; ptr; length; byteLength; ctor; epoch; epochProvider; freed = false; _buf = null; _view = null; constructor(kind, dtype, ptr, length, ctor, epoch, epochProvider) { this.kind = kind; this.dtype = dtype; this.ptr = ptr >>> 0; this.length = length >>> 0; this.ctor = ctor; this.epoch = epoch >>> 0; this.epochProvider = epochProvider; this.byteLength = this.length * (ctor.BYTES_PER_ELEMENT >>> 0) >>> 0; if (this.kind === "heap") HEAP_SLICE_FINALIZER?.register(this, { dtype: this.dtype, ptr: this.ptr, length: this.length }, this); } isAlive() { if (this.freed) return false; if (!this.epochProvider) return true; try { return this.epoch >>> 0 === this.epochProvider() >>> 0; } catch { return false; } } assertAlive() { if (this.isAlive()) return; if (this.freed) throw new Error(`WasmSlice<${this.dtype}> is no longer valid (freed).`); if (this.epochProvider) { let currentEpoch = 0; try { currentEpoch = this.epochProvider() >>> 0; } catch { } throw new Error(`WasmSlice<${this.dtype}> is no longer valid (epoch changed: allocEpoch=${this.epoch} currentEpoch=${currentEpoch}).`); } throw new Error(`WasmSlice<${this.dtype}> is no longer valid.`); } buffer() { this.assertAlive(); return ensureHost().wasm.memory().buffer; } view() { this.assertAlive(); const buf = ensureHost().wasm.memory().buffer; if (this._buf !== buf || !this._view) { this._buf = buf; this._view = new this.ctor(buf, this.ptr >>> 0, this.length >>> 0); } return this._view; } write(src, srcOffset = 0, zeroFill = true) { const v = this.view(); if (zeroFill) v.fill(0); if (!src) return; const dstLen = this.length >>> 0; const srcOff = srcOffset >>> 0; const srcLen = src.length >>> 0; const remaining = srcLen > srcOff ? srcLen - srcOff : 0; const n = Math.min(dstLen, remaining); if (n === 0) return; const s = src; if (ArrayBuffer.isView(s) && typeof s.subarray === "function") { v.set(s.subarray(srcOff, srcOff + n), 0); return; } for (let i = 0; i < n; i++) v[i] = src[srcOff + i]; } handle() { this.assertAlive(); const h = { kind: this.kind, dtype: this.dtype, ptr: this.ptr >>> 0, length: this.length >>> 0 }; if (this.epochProvider) h.epoch = this.epoch >>> 0; return h; } free() { if (this.kind !== "heap") throw new Error(`WasmSlice.free(): cannot free a ${this.kind} allocation. Use reset() for arena-like allocators (frameArena.reset() / WasmHeapArena.reset()).`); if (this.freed) return; const { wasm: wasm2 } = ensureHost(); const ptr = this.ptr >>> 0; const len = this.length >>> 0; switch (this.dtype) { case "f32": wasm2.freeF32(ptr, len); break; case "f64": wasm2.freeF64(ptr, len); break; case "u32": wasm2.freeU32(ptr, len); break; case "i32": wasm2.freeU32(ptr, len); break; case "u8": wasm2.freeBytes(ptr, len); break; } this.freed = true; HEAP_SLICE_FINALIZER?.unregister(this); this._buf = null; this._view = null; } }; var alignUp = (n, align) => { const a = align >>> 0; if (a === 0 || (a & a - 1) !== 0) throw new Error(`alignUp(${n}, ${align}): align must be a non-zero power of two`); return Math.ceil(n / a) * a; }; var WasmHeapArena = class { basePtr; capBytes; headBytes = 0; _epoch = 1; destroyed = false; constructor(capBytes, align = 16) { const cap = capBytes >>> 0; if (cap === 0) throw new Error("WasmHeapArena: capBytes must be > 0"); const { wasm: wasm2 } = ensureHost(); const base = wasm2.allocBytes(cap); if (!base) throw new Error(`WasmHeapArena(${capBytes}): wasm.allocBytes failed`); const a = align >>> 0; if (a !== 0 && (base & a - 1) !== 0) { wasm2.freeBytes(base, cap); throw new Error(`WasmHeapArena(${capBytes}): basePtr 0x${base.toString(16)} is not ${align}-byte aligned`); } this.basePtr = base >>> 0; this.capBytes = cap >>> 0; } epoch() { this.assertAlive(); return this._epoch >>> 0; } usedBytes() { this.assertAlive(); return this.headBytes >>> 0; } reset() { this.assertAlive(); this.headBytes = 0; this._epoch = this._epoch + 1 >>> 0; if (this._epoch === 0) this._epoch = 1; } destroy() { if (this.destroyed) return; const base = this.basePtr >>> 0; const cap = this.capBytes >>> 0; ensureHost().wasm.freeBytes(base, cap); this.destroyed = true; this.headBytes = 0; this._epoch = this._epoch + 1 >>> 0; if (this._epoch === 0) this._epoch = 1; } alloc(bytes, alignBytes = 16) { this.assertAlive(); const b = bytes >>> 0; const a = alignBytes >>> 0; const base = this.basePtr >>> 0; const head = this.headBytes >>> 0; const start = alignUp(base + head, a); const end = start + b; if (end - base > this.capBytes >>> 0) throw new Error(`WasmHeapArena.alloc(${bytes}, ${alignBytes}): out of memory (used=${head} cap=${this.capBytes})`); this.headBytes = end - base >>> 0; return start >>> 0; } allocF32(len) { const l = len >>> 0; const ptr = this.alloc(l * 4, 16); const epoch = this.epoch(); return new WasmSlice("arena", "f32", ptr, l, Float32Array, epoch, () => this.epoch()); } allocF64(len) { const l = len >>> 0; const ptr = this.alloc(l * 8, 16); const epoch = this.epoch(); return new WasmSlice("arena", "f64", ptr, l, Float64Array, epoch, () => this.epoch()); } allocU32(len) { const l = len >>> 0; const ptr = this.alloc(l * 4, 16); const epoch = this.epoch(); return new WasmSlice("arena", "u32", ptr, l, Uint32Array, epoch, () => this.epoch()); } allocI32(len) { const l = len >>> 0; const ptr = this.alloc(l * 4, 16); const epoch = this.epoch(); return new WasmSlice("arena", "i32", ptr, l, Int32Array, epoch, () => this.epoch()); } allocU8(len, alignBytes = 16) { const l = len >>> 0; const ptr = this.alloc(l, alignBytes); const epoch = this.epoch(); return new WasmSlice("arena", "u8", ptr, l, Uint8Array, epoch, () => this.epoch()); } assertAlive() { if (this.destroyed) throw new Error("WasmHeapArena has been destroyed."); } }; var cachedWasmBytesBuf = null; var cachedWasmBytes = null; var wasmBytesView = () => { const b = ensureHost().wasm.memory().buffer; if (b !== cachedWasmBytesBuf || !cachedWasmBytes) { cachedWasmBytesBuf = b; cachedWasmBytes = new Uint8Array(b); } return cachedWasmBytes; }; var driver = { buffer: () => ensureHost().wasm.memory().buffer, bytes: () => wasmBytesView(), isSharedMemory: () => { const b = ensureHost().wasm.memory().buffer; return typeof SharedArrayBuffer !== "undefined" && b instanceof SharedArrayBuffer; }, requireSharedMemory: () => { const b = ensureHost().wasm.memory().buffer; if (typeof SharedArrayBuffer !== "undefined" && b instanceof SharedArrayBuffer) return b; throw new Error("WebAssembly memory is not a SharedArrayBuffer. Build with WASMGPU_SHARED_MEMORY=1 and serve with cross-origin isolation to enable SharedArrayBuffer."); }, viewOn: (ctor, buffer, ptr, len) => { return new ctor(buffer, ptr >>> 0, len >>> 0); }, view: (ctor, ptr, len) => { return new ctor(ensureHost().wasm.memory().buffer, ptr >>> 0, len >>> 0); }, createHeapArena: (capBytes, align = 16) => { return new WasmHeapArena(capBytes, align); }, viewFromHandle: (buffer, handle) => { const ptr = handle.ptr >>> 0; const len = handle.length >>> 0; switch (handle.dtype) { case "f32": return new Float32Array(buffer, ptr, len); case "f64": return new Float64Array(buffer, ptr, len); case "u32": return new Uint32Array(buffer, ptr, len); case "i32": return new Int32Array(buffer, ptr, len); case "u8": return new Uint8Array(buffer, ptr, len); } }, heap: { allocF32: (len) => { const { wasm: wasm2 } = ensureHost(); const ptr = wasm2.allocF32(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.heap.allocF32(${len}) failed`); return new WasmSlice("heap", "f32", ptr, len, Float32Array, 0, null); }, allocF64: (len) => { const { wasm: wasm2 } = ensureHost(); const ptr = wasm2.allocF64(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.heap.allocF64(${len}) failed`); return new WasmSlice("heap", "f64", ptr, len, Float64Array, 0, null); }, allocU32: (len) => { const { wasm: wasm2 } = ensureHost(); const ptr = wasm2.allocU32(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.heap.allocU32(${len}) failed`); return new WasmSlice("heap", "u32", ptr, len, Uint32Array, 0, null); }, allocI32: (len) => { const { wasm: wasm2 } = ensureHost(); const ptr = wasm2.allocU32(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.heap.allocI32(${len}) failed`); return new WasmSlice("heap", "i32", ptr, len, Int32Array, 0, null); }, allocU8: (len, align = 16) => { const { wasm: wasm2 } = ensureHost(); const ptr = wasm2.allocBytes(len >>> 0); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.heap.allocU8(${len}) failed`); if (align !== 0) { if ((ptr & (align >>> 0) - 1) !== 0) { throw new Error(`driver.heap.allocU8(${len}): returned ptr 0x${ptr.toString(16)} is not ${align}-byte aligned`); } } return new WasmSlice("heap", "u8", ptr, len, Uint8Array, 0, null); } }, frame: { allocF32: (len) => { const { frameArena: frameArena2 } = ensureHost(); const ptr = frameArena2.allocF32(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.frame.allocF32(${len}) failed`); return new WasmSlice("frame", "f32", ptr, len, Float32Array, frameArena2.epoch(), () => frameArena2.epoch()); }, allocF64: (len) => { const { frameArena: frameArena2 } = ensureHost(); const ptr = frameArena2.allocF64(len); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.frame.allocF64(${len}) failed`); return new WasmSlice("frame", "f64", ptr, len, Float64Array, frameArena2.epoch(), () => frameArena2.epoch()); }, allocU32: (len) => { const { frameArena: frameArena2 } = ensureHost(); const ptr = frameArena2.alloc((len >>> 0) * 4, 16); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.frame.allocU32(${len}) failed`); return new WasmSlice("frame", "u32", ptr, len, Uint32Array, frameArena2.epoch(), () => frameArena2.epoch()); }, allocI32: (len) => { const { frameArena: frameArena2 } = ensureHost(); const ptr = frameArena2.alloc((len >>> 0) * 4, 16); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.frame.allocI32(${len}) failed`); return new WasmSlice("frame", "i32", ptr, len, Int32Array, frameArena2.epoch(), () => frameArena2.epoch()); }, allocU8: (len, align = 16) => { const { frameArena: frameArena2 } = ensureHost(); const ptr = frameArena2.alloc(len >>> 0, align >>> 0); if (!ptr && len >>> 0 !== 0) throw new Error(`driver.frame.allocU8(${len}) failed`); return new WasmSlice("frame", "u8", ptr, len, Uint8Array, frameArena2.epoch(), () => frameArena2.epoch()); } } }; var modPromise = null; var mod = null; var frameArenaEpoch = 0; var DEFAULT_FRAME_ARENA_BYTES = 8 * 1024 * 1024; var IIFE_SCRIPT_URL = (() => { if (typeof document === "undefined") return null; const cs = document.currentScript; const src = cs?.src; return src && src.length > 0 ? src : null; })(); var defaultBaseURL = () => { if (import.meta.url !== "__CURRENT_SCRIPT__") return new URL(".", import.meta.url).toString(); const base = IIFE_SCRIPT_URL ?? location.href; return new URL(".", base).toString(); }; var initWebAssembly = async (baseURL) => { if (mod) return; const base = baseURL ?? defaultBaseURL(); const wasmURL = new URL("wasm.js", base).toString(); modPromise ??= import(wasmURL); mod = await modPromise; mod.wasmgpu_frame_arena_init(DEFAULT_FRAME_ARENA_BYTES); frameArenaEpoch = mod.wasmgpu_frame_arena_epoch() >>> 0; }; var ensure = () => { if (!mod) throw new Error("WebAssembly driver not initialized. Call await initWebAssembly() first."); return mod; }; var refreshFrameArenaEpoch = () => { frameArenaEpoch = ensure().wasmgpu_frame_arena_epoch() >>> 0; return frameArenaEpoch; }; var bool = (x) => !!x; var wasm = { memory: () => ensure().memory, seed: (seed) => { ensure().wasmgpu_seed(seed >>> 0); }, allocF32: (len) => ensure().wasmgpu_alloc_f32(len >>> 0) >>> 0, freeF32: (ptr, len) => ensure().wasmgpu_free_f32(ptr >>> 0, len >>> 0), allocF64: (len) => ensure().wasmgpu_alloc_f64(len >>> 0) >>> 0, freeF64: (ptr, len) => ensure().wasmgpu_free_f64(ptr >>> 0, len >>> 0), allocU32: (len) => ensure().wasmgpu_alloc_u32(len >>> 0) >>> 0, freeU32: (ptr, len) => ensure().wasmgpu_free_u32(ptr >>> 0, len >>> 0), allocBytes: (bytes) => ensure().wasmgpu_alloc(bytes >>> 0) >>> 0, freeBytes: (ptr, bytes) => ensure().wasmgpu_free(ptr >>> 0, bytes >>> 0), f32view: (ptr, len) => ensure().f32view(ptr >>> 0, len >>> 0), f64view: (ptr, len) => ensure().f64view(ptr >>> 0, len >>> 0), u32view: (ptr, len) => ensure().u32view(ptr >>> 0, len >>> 0), i32view: (ptr, len) => ensure().i32view(ptr >>> 0, len >>> 0), u8view: (ptr, len) => ensure().u8view(ptr >>> 0, len >>> 0), writeF32: (ptr, len, src) => { const v = ensure().f32view(ptr >>> 0, len >>> 0), n = Math.min(len >>> 0, src ? src.length >>> 0 : 0); for (let i = 0; i < n; i++) v[i] = src[i]; for (let i = n; i < len >>> 0; i++) v[i] = 0; }, writeF64: (ptr, len, src) => { const v = ensure().f64view(ptr >>> 0, len >>> 0), n = Math.min(len >>> 0, src ? src.length >>> 0 : 0); for (let i = 0; i < n; i++) v[i] = src[i]; for (let i = n; i < len >>> 0; i++) v[i] = 0; }, readF32Array: (ptr, len) => Array.from(ensure().f32view(ptr >>> 0, len >>> 0)), readF64Array: (ptr, len) => Array.from(ensure().f64view(ptr >>> 0, len >>> 0)) }; var frameArena = { init: (capBytes = DEFAULT_FRAME_ARENA_BYTES) => { const base = ensure().wasmgpu_frame_arena_init(capBytes >>> 0) >>> 0; if (!base) throw new Error(`wasmgpu_frame_arena_init(${capBytes}) failed`); refreshFrameArenaEpoch(); return base; }, reset: () => { ensure().wasmgpu_frame_arena_reset(); refreshFrameArenaEpoch(); }, alloc: (bytes, align = 16) => { const ptr = ensure().wasmgpu_frame_alloc(bytes >>> 0, align >>> 0) >>> 0; if (!ptr) throw new Error(`wasmgpu_frame_alloc(${bytes}, ${align}) failed`); return ptr; }, allocF32: (len) => { const ptr = ensure().wasmgpu_frame_alloc_f32(len >>> 0) >>> 0; if (!ptr) throw new Error(`wasmgpu_frame_alloc_f32(${len}) failed`); return ptr; }, allocF64: (len) => { const ptr = ensure().wasmgpu_frame_alloc_f64(len >>> 0) >>> 0; if (!ptr) throw new Error(`wasmgpu_frame_alloc_f64(${len}) failed`); return ptr; }, epoch: () => { if (!frameArenaEpoch) refreshFrameArenaEpoch(); return frameArenaEpoch; }, usedBytes: () => ensure().wasmgpu_frame_arena_used() >>> 0, capBytes: () => ensure().wasmgpu_frame_arena_cap() >>> 0 }; setWebAssemblyDriverHost(wasm, frameArena); var accessorf = { compact: (outPtr, srcPtr, count, rows, columns, componentBytes, elementStride) => { const logicalComponents = rows * columns >>> 0; const encodedComponents = columns > 1 ? logicalComponents | 2147483648 : logicalComponents; ensure().accessor_deinterleave(outPtr >>> 0, srcPtr >>> 0, count >>> 0, encodedComponents >>> 0, componentBytes >>> 0, elementStride >>> 0); }, applySparse: (outPtr, outComponentCount, componentType, numComponents, indicesPtr, indicesComponentType, valuesPtr, sparseCount) => { ensure().accessor_apply_sparse(outPtr >>> 0, outComponentCount >>> 0, componentType >>> 0, numComponents >>> 0, indicesPtr >>> 0, indicesComponentType >>> 0, valuesPtr >>> 0, sparseCount >>> 0); }, convertToF32: (outPtr, srcPtr, componentCount, componentType, normalized) => { ensure().accessor_convert_to_f32(outPtr >>> 0, srcPtr >>> 0, componentCount >>> 0, componentType >>> 0, normalized ? 1 : 0); }, convertToU16: (outPtr, srcPtr, componentCount, componentType) => { ensure().accessor_convert_to_u16(outPtr >>> 0, srcPtr >>> 0, componentCount >>> 0, componentType >>> 0); }, convertToU32: (outPtr, srcPtr, componentCount, componentType) => { ensure().accessor_convert_to_u32(outPtr >>> 0, srcPtr >>> 0, componentCount >>> 0, componentType >>> 0); } }; var animf = { sampleClipTRS: (posPtr, rotPtr, sclPtr, transformCount, samplersPtr, samplerCount, channelsPtr, channelCount, time) => { ensure().anim_sample_clip_trs(posPtr >>> 0, rotPtr >>> 0, sclPtr >>> 0, transformCount >>> 0, samplersPtr >>> 0, samplerCount >>> 0, channelsPtr >>> 0, channelCount >>> 0, time); }, computeJointMatricesTo: (outPtr, jointIndicesPtr, jointCount, invBindPtr, worldBasePtr, meshWorldPtr) => { ensure().anim_compute_joint_matrices_to(outPtr >>> 0, jointIndicesPtr >>> 0, jointCount >>> 0, invBindPtr >>> 0, worldBasePtr >>> 0, meshWorldPtr >>> 0); } }; var boundsf = { pointcloudXYZS: (outBoxMinPtr, outBoxMaxPtr, outSphereCenterPtr, outSphereRadiusPtr, pointsPtr, pointCount, strideF32) => { ensure().bounds_pointcloud_xyzs(outBoxMinPtr >>> 0, outBoxMaxPtr >>> 0, outSphereCenterPtr >>> 0, outSphereRadiusPtr >>> 0, pointsPtr >>> 0, pointCount >>> 0, strideF32 >>> 0); }, glyphInstances: (outBoxMinPtr, outBoxMaxPtr, outSphereCenterPtr, outSphereRadiusPtr, positionsPtr, scalesPtr, rotationsPtr, instanceCount, glyphCenterPtr, glyphRadius) => { ensure().bounds_glyph_instances(outBoxMinPtr >>> 0, outBoxMaxPtr >>> 0, outSphereCenterPtr >>> 0, outSphereRadiusPtr >>> 0, positionsPtr >>> 0, scalesPtr >>> 0, rotationsPtr >>> 0, instanceCount >>> 0, glyphCenterPtr >>> 0, glyphRadius); }, geometryPositions: (outBoxMinPtr, outBoxMaxPtr, outSphereCenterPtr, outSphereRadiusPtr, positionsPtr, vertexCount) => { ensure().bounds_geometry_positions(outBoxMinPtr >>> 0, outBoxMaxPtr >>> 0, outSphereCenterPtr >>> 0, outSphereRadiusPtr >>> 0, positionsPtr >>> 0, vertexCount >>> 0); } }; var cullf = { writePlanesFromViewProjection: (outPlanesPtr, viewProjPtr) => { ensure().cull_write_planes_from_view_projection(outPlanesPtr >>> 0, viewProjPtr >>> 0); }, prepareWorldSpheresFromPtrs: (outCentersPtr, outRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, count) => { ensure().cull_prepare_world_spheres_from_ptrs(outCentersPtr >>> 0, outRadiiPtr >>> 0, worldPtrsPtr >>> 0, localCentersPtr >>> 0, localRadiiPtr >>> 0, count >>> 0); }, spheresFrustum: (outIndicesPtr, centersPtr, radiiPtr, count, frustumPlanesPtr) => { return ensure().cull_spheres_frustum(outIndicesPtr >>> 0, centersPtr >>> 0, radiiPtr >>> 0, count >>> 0, frustumPlanesPtr >>> 0) >>> 0; }, spheresOcclusion: (outIndicesPtr, outStatsPtr, centersPtr, radiiPtr, count, viewProjPtr, viewportWidth, viewportHeight, mipOffsetsPtr, mipWidthsPtr, mipHeightsPtr, mipCount, depthValuesPtr, depthValuesLen, nearPlaneEpsilon, maxScreenCoverage, depthBias) => { return ensure().cull_spheres_occlusion(outIndicesPtr >>> 0, outStatsPtr >>> 0, centersPtr >>> 0, radiiPtr >>> 0, count >>> 0, viewProjPtr >>> 0, viewportWidth, viewportHeight, mipOffsetsPtr >>> 0, mipWidthsPtr >>> 0, mipHeightsPtr >>> 0, mipCount >>> 0, depthValuesPtr >>> 0, depthValuesLen >>> 0, nearPlaneEpsilon, maxScreenCoverage, depthBias) >>> 0; } }; var frustumf = { writePlanesFromViewProjection: (outPlanesPtr, viewProj) => { if (typeof viewProj === "number") { ensure().cull_write_planes_from_view_projection(outPlanesPtr >>> 0, viewProj >>> 0); return; } const vpPtr = frameArena.allocF32(16); wasm.writeF32(vpPtr, 16, viewProj); ensure().cull_write_planes_from_view_projection(outPlanesPtr >>> 0, vpPtr >>> 0); } }; var mat4f = { alloc: () => wasm.allocF32(16), view: (ptr) => wasm.f32view(ptr, 16), set: (ptr, src) => wasm.writeF32(ptr, 16, src), abs: (out, m) => { ensure().mat4f_abs(out >>> 0, m >>> 0); }, add: (out, a, b) => { ensure().mat4f_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, m) => { ensure().mat4f_copy(out >>> 0, m >>> 0); }, decomposeTRS: (outTrs, m) => { ensure().mat4f_decompose_trs(outTrs >>> 0, m >>> 0); }, det: (m) => ensure().mat4f_det(m >>> 0), identity: (out) => { ensure().mat4f_identity(out >>> 0); }, init: (out, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) => { ensure().mat4f_init(out >>> 0, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15); }, invert: (out, m) => { ensure().mat4f_invert(out >>> 0, m >>> 0); }, isEqual: (a, b) => bool(ensure().mat4f_isEqual(a >>> 0, b >>> 0)), isIdentity: (m) => bool(ensure().mat4f_isIdentity(m >>> 0)), isInverse: (a, b) => bool(ensure().mat4f_isInverse(a >>> 0, b >>> 0)), isZero: (m) => bool(ensure().mat4f_isZero(m >>> 0)), lookAt: (out, eye3, center3, up3) => { ensure().mat4f_lookAt(out >>> 0, eye3 >>> 0, center3 >>> 0, up3 >>> 0); }, mul: (out, a, b) => { ensure().mat4f_mul(out >>> 0, a >>> 0, b >>> 0); }, mulVec4: (outVec4, m, v4) => { ensure().mat4f_mul_vec4(outVec4 >>> 0, m >>> 0, v4 >>> 0); }, neg: (out, m) => { ensure().mat4f_neg(out >>> 0, m >>> 0); }, norm: (m) => ensure().mat4f_norm(m >>> 0), normalize: (out, m) => { ensure().mat4f_normalize(out >>> 0, m >>> 0); }, normsq: (m) => ensure().mat4f_normsq(m >>> 0), perspective: (out, fovY, aspect, near, far) => { ensure().mat4f_perspective(out >>> 0, fovY, aspect, near, far); }, random: (out) => { ensure().mat4f_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().mat4f_random_range(out >>> 0, min, max); }, rotateX: (out, m, angle) => { ensure().mat4f_rotateX(out >>> 0, m >>> 0, angle); }, rotateY: (out, m, angle) => { ensure().mat4f_rotateY(out >>> 0, m >>> 0, angle); }, rotateZ: (out, m, angle) => { ensure().mat4f_rotateZ(out >>> 0, m >>> 0, angle); }, round: (out, m) => { ensure().mat4f_round(out >>> 0, m >>> 0); }, scl: (out, m, scalar) => { ensure().mat4f_scl(out >>> 0, m >>> 0, scalar); }, sub: (out, a, b) => { ensure().mat4f_sub(out >>> 0, a >>> 0, b >>> 0); }, trace: (m) => ensure().mat4f_trace(m >>> 0), translate: (out, m, v3) => { ensure().mat4f_translate(out >>> 0, m >>> 0, v3 >>> 0); }, transpose: (out, m) => { ensure().mat4f_transpose(out >>> 0, m >>> 0); }, print: (m) => { const a = wasm.f32view(m, 16); console.log(`[ ${a[0]} ${a[1]} ${a[2]} ${a[3]} ] [ ${a[4]} ${a[5]} ${a[6]} ${a[7]} ] [ ${a[8]} ${a[9]} ${a[10]} ${a[11]} ] [ ${a[12]} ${a[13]} ${a[14]} ${a[15]} ]`); } }; var mat4d = { alloc: () => wasm.allocF64(16), view: (ptr) => wasm.f64view(ptr, 16), set: (ptr, src) => wasm.writeF64(ptr, 16, src), abs: (out, m) => { ensure().mat4d_abs(out >>> 0, m >>> 0); }, add: (out, a, b) => { ensure().mat4d_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, m) => { ensure().mat4d_copy(out >>> 0, m >>> 0); }, decomposeTRS: (outTrs, m) => { ensure().mat4d_decompose_trs(outTrs >>> 0, m >>> 0); }, det: (m) => ensure().mat4d_det(m >>> 0), identity: (out) => { ensure().mat4d_identity(out >>> 0); }, init: (out, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) => { ensure().mat4d_init(out >>> 0, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15); }, invert: (out, m) => { ensure().mat4d_invert(out >>> 0, m >>> 0); }, isEqual: (a, b) => bool(ensure().mat4d_isEqual(a >>> 0, b >>> 0)), isIdentity: (m) => bool(ensure().mat4d_isIdentity(m >>> 0)), isInverse: (a, b) => bool(ensure().mat4d_isInverse(a >>> 0, b >>> 0)), isZero: (m) => bool(ensure().mat4d_isZero(m >>> 0)), lookAt: (out, eye3, center3, up3) => { ensure().mat4d_lookAt(out >>> 0, eye3 >>> 0, center3 >>> 0, up3 >>> 0); }, mul: (out, a, b) => { ensure().mat4d_mul(out >>> 0, a >>> 0, b >>> 0); }, mulVec4: (outVec4, m, v4) => { ensure().mat4d_mul_vec4(outVec4 >>> 0, m >>> 0, v4 >>> 0); }, neg: (out, m) => { ensure().mat4d_neg(out >>> 0, m >>> 0); }, norm: (m) => ensure().mat4d_norm(m >>> 0), normalize: (out, m) => { ensure().mat4d_normalize(out >>> 0, m >>> 0); }, normsq: (m) => ensure().mat4d_normsq(m >>> 0), perspective: (out, fovY, aspect, near, far) => { ensure().mat4d_perspective(out >>> 0, fovY, aspect, near, far); }, random: (out) => { ensure().mat4d_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().mat4d_random_range(out >>> 0, min, max); }, rotateX: (out, m, angle) => { ensure().mat4d_rotateX(out >>> 0, m >>> 0, angle); }, rotateY: (out, m, angle) => { ensure().mat4d_rotateY(out >>> 0, m >>> 0, angle); }, rotateZ: (out, m, angle) => { ensure().mat4d_rotateZ(out >>> 0, m >>> 0, angle); }, round: (out, m) => { ensure().mat4d_round(out >>> 0, m >>> 0); }, scl: (out, m, scalar) => { ensure().mat4d_scl(out >>> 0, m >>> 0, scalar); }, sub: (out, a, b) => { ensure().mat4d_sub(out >>> 0, a >>> 0, b >>> 0); }, trace: (m) => ensure().mat4d_trace(m >>> 0), translate: (out, m, v3) => { ensure().mat4d_translate(out >>> 0, m >>> 0, v3 >>> 0); }, transpose: (out, m) => { ensure().mat4d_transpose(out >>> 0, m >>> 0); }, print: (m) => { const a = wasm.f64view(m, 16); console.log(`[ ${a[0]} ${a[1]} ${a[2]} ${a[3]} ] [ ${a[4]} ${a[5]} ${a[6]} ${a[7]} ] [ ${a[8]} ${a[9]} ${a[10]} ${a[11]} ] [ ${a[12]} ${a[13]} ${a[14]} ${a[15]} ]`); } }; var meshf = { computeVertexNormals: (outNormalsPtr, positionsPtr, vertexCount, indicesPtr, indexCount) => { ensure().mesh_compute_vertex_normals(outNormalsPtr >>> 0, positionsPtr >>> 0, vertexCount >>> 0, indicesPtr >>> 0, indexCount >>> 0); } }; var ndarrayf = { numel: (shapePtr, ndim) => { return ensure().ndarray_numel(shapePtr >>> 0, ndim >>> 0) >>> 0; }, stridesRowMajorTo: (outStridesPtr, shapePtr, ndim, elemBytes) => { return !!ensure().ndarray_strides_row_major(outStridesPtr >>> 0, shapePtr >>> 0, ndim >>> 0, elemBytes >>> 0); }, offsetBytes: (shapePtr, stridesPtr, indicesPtr, ndim, baseOffsetBytes) => { return ensure().ndarray_offset_bytes(shapePtr >>> 0, stridesPtr >>> 0, indicesPtr >>> 0, ndim >>> 0, baseOffsetBytes >>> 0) >>> 0; } }; var quatf = { alloc: () => wasm.allocF32(4), view: (ptr) => wasm.f32view(ptr, 4), set: (ptr, src) => wasm.writeF32(ptr, 4, src), abs: (out, q) => { ensure().quatf_abs(out >>> 0, q >>> 0); }, add: (out, a, b) => { ensure().quatf_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, q) => { ensure().quatf_copy(out >>> 0, q >>> 0); }, dist: (a, b) => ensure().quatf_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().quatf_distsq(a >>> 0, b >>> 0), fromAxisAngle: (out, axis3, angle) => { ensure().quatf_fromAxisAngle(out >>> 0, axis3 >>> 0, angle); }, init: (out, x, y, z, w) => { ensure().quatf_init(out >>> 0, x, y, z, w); }, invert: (out, q) => { ensure().quatf_invert(out >>> 0, q >>> 0); }, isEqual: (a, b) => bool(ensure().quatf_isEqual(a >>> 0, b >>> 0)), isNormalized: (q) => bool(ensure().quatf_isNormalized(q >>> 0)), isZero: (q) => bool(ensure().quatf_isZero(q >>> 0)), mul: (out, a, b) => { ensure().quatf_mul(out >>> 0, a >>> 0, b >>> 0); }, neg: (out, q) => { ensure().quatf_neg(out >>> 0, q >>> 0); }, norm: (q) => ensure().quatf_norm(q >>> 0), normalize: (out, q) => { ensure().quatf_normalize(out >>> 0, q >>> 0); }, normscl: (out, q, scalar) => { ensure().quatf_normscl(out >>> 0, q >>> 0, scalar); }, normsq: (q) => ensure().quatf_normsq(q >>> 0), random: (out) => { ensure().quatf_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().quatf_random_range(out >>> 0, min, max); }, round: (out, q) => { ensure().quatf_round(out >>> 0, q >>> 0); }, scl: (out, q, scalar) => { ensure().quatf_scl(out >>> 0, q >>> 0, scalar); }, slerp: (out, a, b, t) => { ensure().quatf_slerp(out >>> 0, a >>> 0, b >>> 0, t); }, sub: (out, a, b) => { ensure().quatf_sub(out >>> 0, a >>> 0, b >>> 0); }, toRotation: (outVec3, q, v3) => { ensure().quatf_toRotation(outVec3 >>> 0, q >>> 0, v3 >>> 0); }, print: (q) => { const a = wasm.f32view(q, 4); console.log(`${a[0]} ${a[1] < 0 ? "-" : "+"} ${Math.abs(a[1])}i ${a[2] < 0 ? "-" : "+"} ${Math.abs(a[2])}j ${a[3] < 0 ? "-" : "+"} ${Math.abs(a[3])}k`); } }; var quatd = { alloc: () => wasm.allocF64(4), view: (ptr) => wasm.f64view(ptr, 4), set: (ptr, src) => wasm.writeF64(ptr, 4, src), abs: (out, q) => { ensure().quatd_abs(out >>> 0, q >>> 0); }, add: (out, a, b) => { ensure().quatd_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, q) => { ensure().quatd_copy(out >>> 0, q >>> 0); }, dist: (a, b) => ensure().quatd_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().quatd_distsq(a >>> 0, b >>> 0), fromAxisAngle: (out, axis3, angle) => { ensure().quatd_fromAxisAngle(out >>> 0, axis3 >>> 0, angle); }, init: (out, x, y, z, w) => { ensure().quatd_init(out >>> 0, x, y, z, w); }, invert: (out, q) => { ensure().quatd_invert(out >>> 0, q >>> 0); }, isEqual: (a, b) => bool(ensure().quatd_isEqual(a >>> 0, b >>> 0)), isNormalized: (q) => bool(ensure().quatd_isNormalized(q >>> 0)), isZero: (q) => bool(ensure().quatd_isZero(q >>> 0)), mul: (out, a, b) => { ensure().quatd_mul(out >>> 0, a >>> 0, b >>> 0); }, neg: (out, q) => { ensure().quatd_neg(out >>> 0, q >>> 0); }, norm: (q) => ensure().quatd_norm(q >>> 0), normalize: (out, q) => { ensure().quatd_normalize(out >>> 0, q >>> 0); }, normscl: (out, q, scalar) => { ensure().quatd_normscl(out >>> 0, q >>> 0, scalar); }, normsq: (q) => ensure().quatd_normsq(q >>> 0), random: (out) => { ensure().quatd_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().quatd_random_range(out >>> 0, min, max); }, round: (out, q) => { ensure().quatd_round(out >>> 0, q >>> 0); }, scl: (out, q, scalar) => { ensure().quatd_scl(out >>> 0, q >>> 0, scalar); }, slerp: (out, a, b, t) => { ensure().quatd_slerp(out >>> 0, a >>> 0, b >>> 0, t); }, sub: (out, a, b) => { ensure().quatd_sub(out >>> 0, a >>> 0, b >>> 0); }, toRotation: (outVec3, q, v3) => { ensure().quatd_toRotation(outVec3 >>> 0, q >>> 0, v3 >>> 0); }, print: (q) => { const a = wasm.f64view(q, 4); console.log(`${a[0]} ${a[1] < 0 ? "-" : "+"} ${Math.abs(a[1])}i ${a[2] < 0 ? "-" : "+"} ${Math.abs(a[2])}j ${a[3] < 0 ? "-" : "+"} ${Math.abs(a[3])}k`); } }; var transformf = { composeLocalMany: (outLocalPtr, posPtr, rotPtr, sclPtr, count) => { ensure().transform_compose_local_many(outLocalPtr >>> 0, posPtr >>> 0, rotPtr >>> 0, sclPtr >>> 0, count >>> 0); }, updateWorldOrdered: (outWorldPtr, localPtr, parentPtr, orderPtr, count) => { ensure().transform_update_world_ordered(outWorldPtr >>> 0, localPtr >>> 0, parentPtr >>> 0, orderPtr >>> 0, count >>> 0); }, updatePartialOrdered: (outWorldPtr, outLocalPtr, posPtr, rotPtr, sclPtr, parentPtr, orderPtr, dirtyIndicesPtr, dirtyCount, count) => { ensure().transform_update_partial_ordered(outWorldPtr >>> 0, outLocalPtr >>> 0, posPtr >>> 0, rotPtr >>> 0, sclPtr >>> 0, parentPtr >>> 0, orderPtr >>> 0, dirtyIndicesPtr >>> 0, dirtyCount >>> 0, count >>> 0); }, packModelNormalMat4FromPtrs: (outPtr, matPtrsPtr, count) => { ensure().transform_pack_model_normal_mat4_from_ptrs(outPtr >>> 0, matPtrsPtr >>> 0, count >>> 0); } }; var vec3f = { alloc: () => wasm.allocF32(3), view3: (ptr) => wasm.f32view(ptr, 3), set3: (ptr, src) => wasm.writeF32(ptr, 3, src), abs: (out, v) => { ensure().vec3f_abs(out >>> 0, v >>> 0); }, add: (out, a, b) => { ensure().vec3f_add(out >>> 0, a >>> 0, b >>> 0); }, ang: (out, v) => { ensure().vec3f_ang(out >>> 0, v >>> 0); }, angBetween: (a, b) => ensure().vec3f_angBetween(a >>> 0, b >>> 0), copy: (out, v) => { ensure().vec3f_copy(out >>> 0, v >>> 0); }, cross: (out, a, b) => { ensure().vec3f_cross(out >>> 0, a >>> 0, b >>> 0); }, dist: (a, b) => ensure().vec3f_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().vec3f_distsq(a >>> 0, b >>> 0), dot: (a, b) => ensure().vec3f_dot(a >>> 0, b >>> 0), init: (out, x, y, z) => { ensure().vec3f_init(out >>> 0, x, y, z); }, interp: (out, v, a, b, c) => { ensure().vec3f_interp(out >>> 0, v >>> 0, a, b, c); }, isEqual: (a, b) => bool(ensure().vec3f_isEqual(a >>> 0, b >>> 0)), isNormalized: (v) => bool(ensure().vec3f_isNormalized(v >>> 0)), isOrthogonal: (a, b) => bool(ensure().vec3f_isOrthogonal(a >>> 0, b >>> 0)), isParallel: (a, b) => bool(ensure().vec3f_isParallel(a >>> 0, b >>> 0)), isZero: (v) => bool(ensure().vec3f_isZero(v >>> 0)), neg: (out, v) => { ensure().vec3f_neg(out >>> 0, v >>> 0); }, norm: (v) => ensure().vec3f_norm(v >>> 0), normalize: (out, v) => { ensure().vec3f_normalize(out >>> 0, v >>> 0); }, normscl: (out, v, scalar) => { ensure().vec3f_normscl(out >>> 0, v >>> 0, scalar); }, normsq: (v) => ensure().vec3f_normsq(v >>> 0), oproj: (out, a, b) => { ensure().vec3f_oproj(out >>> 0, a >>> 0, b >>> 0); }, proj: (out, a, b) => { ensure().vec3f_proj(out >>> 0, a >>> 0, b >>> 0); }, random: (out) => { ensure().vec3f_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().vec3f_random_range(out >>> 0, min, max); }, reflect: (out, a, b) => { ensure().vec3f_reflect(out >>> 0, a >>> 0, b >>> 0); }, refract: (out, a, b, refractiveIndex) => { ensure().vec3f_refract(out >>> 0, a >>> 0, b >>> 0, refractiveIndex); }, round: (out, v) => { ensure().vec3f_round(out >>> 0, v >>> 0); }, scl: (out, v, scalar) => { ensure().vec3f_scl(out >>> 0, v >>> 0, scalar); }, sub: (out, a, b) => { ensure().vec3f_sub(out >>> 0, a >>> 0, b >>> 0); }, print: (v) => { const a = wasm.f32view(v, 3); console.log(`(${a[0]}, ${a[1]}, ${a[2]})`); } }; var vec3d = { alloc: () => wasm.allocF64(3), view3: (ptr) => wasm.f64view(ptr, 3), set3: (ptr, src) => wasm.writeF64(ptr, 3, src), abs: (out, v) => { ensure().vec3d_abs(out >>> 0, v >>> 0); }, add: (out, a, b) => { ensure().vec3d_add(out >>> 0, a >>> 0, b >>> 0); }, ang: (out, v) => { ensure().vec3d_ang(out >>> 0, v >>> 0); }, angBetween: (a, b) => ensure().vec3d_angBetween(a >>> 0, b >>> 0), copy: (out, v) => { ensure().vec3d_copy(out >>> 0, v >>> 0); }, cross: (out, a, b) => { ensure().vec3d_cross(out >>> 0, a >>> 0, b >>> 0); }, dist: (a, b) => ensure().vec3d_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().vec3d_distsq(a >>> 0, b >>> 0), dot: (a, b) => ensure().vec3d_dot(a >>> 0, b >>> 0), init: (out, x, y, z) => { ensure().vec3d_init(out >>> 0, x, y, z); }, interp: (out, v, a, b, c) => { ensure().vec3d_interp(out >>> 0, v >>> 0, a, b, c); }, isEqual: (a, b) => bool(ensure().vec3d_isEqual(a >>> 0, b >>> 0)), isNormalized: (v) => bool(ensure().vec3d_isNormalized(v >>> 0)), isOrthogonal: (a, b) => bool(ensure().vec3d_isOrthogonal(a >>> 0, b >>> 0)), isParallel: (a, b) => bool(ensure().vec3d_isParallel(a >>> 0, b >>> 0)), isZero: (v) => bool(ensure().vec3d_isZero(v >>> 0)), neg: (out, v) => { ensure().vec3d_neg(out >>> 0, v >>> 0); }, norm: (v) => ensure().vec3d_norm(v >>> 0), normalize: (out, v) => { ensure().vec3d_normalize(out >>> 0, v >>> 0); }, normscl: (out, v, scalar) => { ensure().vec3d_normscl(out >>> 0, v >>> 0, scalar); }, normsq: (v) => ensure().vec3d_normsq(v >>> 0), oproj: (out, a, b) => { ensure().vec3d_oproj(out >>> 0, a >>> 0, b >>> 0); }, proj: (out, a, b) => { ensure().vec3d_proj(out >>> 0, a >>> 0, b >>> 0); }, random: (out) => { ensure().vec3d_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().vec3d_random_range(out >>> 0, min, max); }, reflect: (out, a, b) => { ensure().vec3d_reflect(out >>> 0, a >>> 0, b >>> 0); }, refract: (out, a, b, refractiveIndex) => { ensure().vec3d_refract(out >>> 0, a >>> 0, b >>> 0, refractiveIndex); }, round: (out, v) => { ensure().vec3d_round(out >>> 0, v >>> 0); }, scl: (out, v, scalar) => { ensure().vec3d_scl(out >>> 0, v >>> 0, scalar); }, sub: (out, a, b) => { ensure().vec3d_sub(out >>> 0, a >>> 0, b >>> 0); }, print: (v) => { const a = wasm.f64view(v, 3); console.log(`(${a[0]}, ${a[1]}, ${a[2]})`); } }; var mat4 = { abs: (matr) => ensure().mat4abs(matr), add: (matr1, matr2) => ensure().mat4add(matr1, matr2), copy: (matr) => ensure().mat4copy(matr), det: (matr) => ensure().mat4det(matr), identity: () => ensure().mat4identity(), init: (m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) => ensure().mat4init(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15), invert: (matr) => ensure().mat4invert(matr), isEqual: (matr1, matr2) => ensure().mat4isEqual(matr1, matr2), isIdentity: (matr) => ensure().mat4isIdentity(matr), isInverse: (matr1, matr2) => ensure().mat4isInverse(matr1, matr2), isZero: (matr) => ensure().mat4isZero(matr), lookAt: (eye, center, up) => ensure().mat4lookAt(eye, center, up), mul: (matr1, matr2ORvect) => ensure().mat4mul(matr1, matr2ORvect), neg: (matr) => ensure().mat4neg(matr), norm: (matr) => ensure().mat4norm(matr), normalize: (matr) => ensure().mat4normalize(matr), normsq: (matr) => ensure().mat4normsq(matr), perspective: (fovY, aspect, near, far) => ensure().mat4perspective(fovY, aspect, near, far), print: (matr) => ensure().mat4print(matr), random: (min, max) => ensure().mat4random(min, max), rotateX: (matr, angle) => ensure().mat4rotateX(matr, angle), rotateY: (matr, angle) => ensure().mat4rotateY(matr, angle), rotateZ: (matr, angle) => ensure().mat4rotateZ(matr, angle), round: (matr) => ensure().mat4round(matr), scl: (matr, scalar) => ensure().mat4scl(matr, scalar), sub: (matr1, matr2) => ensure().mat4sub(matr1, matr2), trace: (matr) => ensure().mat4trace(matr), translate: (matr, vect) => ensure().mat4translate(matr, vect), transpose: (matr) => ensure().mat4transpose(matr) }; var quat = { abs: (q) => ensure().quatabs(q), add: (q1, q2) => ensure().quatadd(q1, q2), copy: (q) => ensure().quatcopy(q), dist: (q1, q2) => ensure().quatdist(q1, q2), distsq: (q1, q2) => ensure().quatdistsq(q1, q2), fromAxisAngle: (axis, angle) => ensure().quatfromAxisAngle(axis, angle), init: (a, b, c, d) => ensure().quatinit(a, b, c, d), invert: (q) => ensure().quatinvert(q), isEqual: (q1, q2) => ensure().quatisEqual(q1, q2), isNormalized: (q) => ensure().quatisNormalized(q), isZero: (q) => ensure().quatisZero(q), mul: (q1, q2) => ensure().quatmul(q1, q2), neg: (q) => ensure().quatneg(q), norm: (q) => ensure().quatnorm(q), normalize: (q) => ensure().quatnormalize(q), normscl: (q, scalar) => ensure().quatnormscl(q, scalar), normsq: (q) => ensure().quatnormsq(q), print: (q) => ensure().quatprint(q), random: (min, max) => ensure().quatrandom(min, max), round: (q) => ensure().quatround(q), scl: (q, scalar) => ensure().quatscl(q, scalar), slerp: (q1, q2, t) => ensure().quatslerp(q1, q2, t), sub: (q1, q2) => ensure().quatsub(q1, q2), toRotation: (q, v) => ensure().quattoRotation(q, v) }; var vec3 = { abs: (v) => ensure().vec3abs(v), add: (v1, v2) => ensure().vec3add(v1, v2), ang: (v) => ensure().vec3ang(v), angBetween: (v1, v2) => ensure().vec3angBetween(v1, v2), copy: (v) => ensure().vec3copy(v), cross: (v1, v2) => ensure().vec3cross(v1, v2), dist: (v1, v2) => ensure().vec3dist(v1, v2), distsq: (v1, v2) => ensure().vec3distsq(v1, v2), dot: (v1, v2) => ensure().vec3dot(v1, v2), init: (x, y, z) => ensure().vec3init(x, y, z), interp: (v, a, b, c) => ensure().vec3interp(v, a, b, c), isEqual: (v1, v2) => ensure().vec3isEqual(v1, v2), isNormalized: (v) => ensure().vec3isNormalized(v), isOrthogonal: (v1, v2) => ensure().vec3isOrthogonal(v1, v2), isParallel: (v1, v2) => ensure().vec3isParallel(v1, v2), isZero: (v) => ensure().vec3isZero(v), neg: (v) => ensure().vec3neg(v), norm: (v) => ensure().vec3norm(v), normalize: (v) => ensure().vec3normalize(v), normscl: (v, scalar) => ensure().vec3normscl(v, scalar), normsq: (v) => ensure().vec3normsq(v), oproj: (v1, v2) => ensure().vec3oproj(v1, v2), print: (v) => ensure().vec3print(v), proj: (v1, v2) => ensure().vec3proj(v1, v2), random: (min, max) => ensure().vec3random(min, max), reflect: (v1, v2) => ensure().vec3reflect(v1, v2), refract: (v1, v2, refractiveIndex) => ensure().vec3refract(v1, v2, refractiveIndex), round: (v) => ensure().vec3round(v), scl: (v, scalar) => ensure().vec3scl(v, scalar), sub: (v1, v2) => ensure().vec3sub(v1, v2) }; // typescript/compute/buffer.ts var isArrayBufferView = (x) => { return ArrayBuffer.isView(x); }; var resolveSourceRange = (data, srcOffsetBytes = 0, sizeBytes) => { if (isArrayBufferView(data)) { const baseOffset = data.byteOffset + srcOffsetBytes; const maxSize2 = data.byteLength - srcOffsetBytes; const size2 = sizeBytes === void 0 ? maxSize2 : Math.min(maxSize2, sizeBytes); return { buffer: data.buffer, offset: baseOffset, size: size2 }; } const maxSize = data.byteLength - srcOffsetBytes; const size = sizeBytes === void 0 ? maxSize : Math.min(maxSize, sizeBytes); return { buffer: data, offset: srcOffsetBytes, size }; }; var queueWriteBufferAligned = (queue, dst, dstOffsetBytes, data, srcOffsetBytes = 0, sizeBytes) => { assert(Number.isInteger(dstOffsetBytes) && dstOffsetBytes >= 0, `dstOffsetBytes must be an integer >= 0 (got ${dstOffsetBytes})`); const src = resolveSourceRange(data, srcOffsetBytes, sizeBytes); assert((dstOffsetBytes & 3) === 0, `dstOffsetBytes must be 4-byte aligned (got ${dstOffsetBytes})`); assert((src.offset & 3) === 0, `srcOffsetBytes must be 4-byte aligned (got ${src.offset})`); const alignedSize = alignTo(src.size, 4); if (alignedSize === src.size) { queue.writeBuffer(dst, dstOffsetBytes, src.buffer, src.offset, src.size); return; } const tmp = new Uint8Array(alignedSize); tmp.set(new Uint8Array(src.buffer, src.offset, src.size)); queue.writeBuffer(dst, dstOffsetBytes, tmp, 0, alignedSize); }; var GpuBuffer = class { device; queue; buffer; byteLength; usage; constructor(device, queue, buffer, byteLength, usage) { this.device = device; this.queue = queue; this.buffer = buffer; this.byteLength = byteLength; this.usage = usage; } destroy() { this.buffer.destroy(); } write(data, dstOffsetBytes = 0, srcOffsetBytes = 0, sizeBytes) { queueWriteBufferAligned(this.queue, this.buffer, dstOffsetBytes, data, srcOffsetBytes, sizeBytes); } writeFromArrayBuffer(src, srcOffsetBytes, sizeBytes, dstOffsetBytes = 0) { this.write(src, dstOffsetBytes, srcOffsetBytes, sizeBytes); } writeFromWasmMemory(mem, srcPtrBytes, sizeBytes, dstOffsetBytes = 0) { const view = new Uint8Array(mem.buffer, srcPtrBytes >>> 0, sizeBytes >>> 0); this.write(view, dstOffsetBytes, 0, sizeBytes); } }; var StorageBuffer = class extends GpuBuffer { label; constructor(device, queue, desc) { const byteLength = desc.data ? resolveSourceRange(desc.data).size : desc.byteLength ?? 0; assert(Number.isInteger(byteLength) && byteLength >= 0, `StorageBuffer.byteLength must be an integer >= 0 (got ${byteLength})`); const size = alignTo(byteLength, 4); let usage = GPUBufferUsage.STORAGE; if (desc.copyDst !== false) usage |= GPUBufferUsage.COPY_DST; if (desc.copySrc) usage |= GPUBufferUsage.COPY_SRC; if (desc.usage) usage |= desc.usage; const buffer = device.createBuffer({ label: desc.label, size: Math.max(4, size), usage, mappedAtCreation: !!desc.data }); if (desc.data) { const src = resolveSourceRange(desc.data); const dstBytes = new Uint8Array(buffer.getMappedRange()); dstBytes.set(new Uint8Array(src.buffer, src.offset, src.size), 0); if (src.size < dstBytes.byteLength) dstBytes.fill(0, src.size); buffer.unmap(); } super(device, queue, buffer, byteLength, usage); this.label = desc.label ?? null; } get canReadback() { return (this.usage & GPUBufferUsage.COPY_SRC) !== 0; } async read(srcOffsetBytes = 0, sizeBytes) { assert(this.canReadback, "StorageBuffer.read() requires the buffer to be created with copySrc: true"); assert(Number.isInteger(srcOffsetBytes) && srcOffsetBytes >= 0, `srcOffsetBytes must be an integer >= 0 (got ${srcOffsetBytes})`); const size = sizeBytes ?? this.byteLength - srcOffsetBytes; assert(Number.isInteger(size) && size >= 0, `sizeBytes must be an integer >= 0 (got ${size})`); assert(srcOffsetBytes + size <= this.byteLength, `read range out of bounds (offset ${srcOffsetBytes}, size ${size}, byteLength ${this.byteLength})`); const alignedSize = alignTo(size, 4); const srcOffsetAligned = alignTo(srcOffsetBytes, 4); assert(srcOffsetAligned === srcOffsetBytes, `srcOffsetBytes must be 4-byte aligned for readback (got ${srcOffsetBytes})`); const staging = this.device.createBuffer({ size: Math.max(4, alignedSize), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); const encoder = this.device.createCommandEncoder(); encoder.copyBufferToBuffer(this.buffer, srcOffsetBytes, staging, 0, alignedSize); this.queue.submit([encoder.finish()]); await staging.mapAsync(GPUMapMode.READ, 0, alignedSize); const mapped = staging.getMappedRange(0, alignedSize); const out = mapped.slice(0, size); staging.unmap(); staging.destroy(); return out; } async readAs(ctor, srcOffsetBytes = 0, sizeBytes) { const bytes = await this.read(srcOffsetBytes, sizeBytes); const bpe = ctor.BYTES_PER_ELEMENT; assert(bytes.byteLength % bpe === 0, `readAs: byteLength (${bytes.byteLength}) is not divisible by BYTES_PER_ELEMENT (${bpe})`); const len = bytes.byteLength / bpe; return new ctor(bytes, 0, len); } }; var UniformBuffer = class extends GpuBuffer { label; constructor(device, queue, desc) { const byteLength = desc.data ? resolveSourceRange(desc.data).size : desc.byteLength ?? 0; assert(Number.isInteger(byteLength) && byteLength >= 0, `UniformBuffer.byteLength must be an integer >= 0 (got ${byteLength})`); const size = alignTo(byteLength, 4); let usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST; if (desc.usage) usage |= desc.usage; const buffer = device.createBuffer({ label: desc.label, size: Math.max(4, size), usage, mappedAtCreation: !!desc.data }); if (desc.data) { const src = resolveSourceRange(desc.data); const dstBytes = new Uint8Array(buffer.getMappedRange()); dstBytes.set(new Uint8Array(src.buffer, src.offset, src.size), 0); if (src.size < dstBytes.byteLength) dstBytes.fill(0, src.size); buffer.unmap(); } super(device, queue, buffer, byteLength, usage); this.label = desc.label ?? null; } }; // typescript/compute/ndarray.ts var DTYPE_TABLE = { i8: { dtype: "i8", ctor: Int8Array, bytesPerElement: 1, wgslScalarType: null }, u8: { dtype: "u8", ctor: Uint8Array, bytesPerElement: 1, wgslScalarType: null }, i16: { dtype: "i16", ctor: Int16Array, bytesPerElement: 2, wgslScalarType: null }, u16: { dtype: "u16", ctor: Uint16Array, bytesPerElement: 2, wgslScalarType: null }, i32: { dtype: "i32", ctor: Int32Array, bytesPerElement: 4, wgslScalarType: "i32" }, u32: { dtype: "u32", ctor: Uint32Array, bytesPerElement: 4, wgslScalarType: "u32" }, f32: { dtype: "f32", ctor: Float32Array, bytesPerElement: 4, wgslScalarType: "f32" }, f64: { dtype: "f64", ctor: Float64Array, bytesPerElement: 8, wgslScalarType: null } }; var dtypeInfo = (dtype) => { const info = DTYPE_TABLE[dtype]; if (!info) throw new Error(`Unknown dtype: ${String(dtype)}`); return info; }; var validateShape = (shape) => { assert(Array.isArray(shape), "shape must be an array of dimension sizes"); const out = new Array(shape.length); for (let i = 0; i < shape.length; i++) { const d = shape[i]; assert(Number.isInteger(d) && d >= 0, `shape[${i}] must be an integer >= 0 (got ${d})`); assert(d <= 4294967295, `shape[${i}] must fit in u32 (got ${d})`); out[i] = d; } return out; }; var defaultRowMajorStridesBytes = (shape, bytesPerElement2) => { const ndim = shape.length; const strides = new Array(ndim); let stride = bytesPerElement2; for (let i = ndim - 1; i >= 0; i--) { assert(Number.isInteger(stride) && stride >= 0, "Stride overflow while computing row-major strides"); assert(stride <= 2147483647, `row-major stride exceeds i32 range (got ${stride})`); strides[i] = stride; stride = stride * shape[i]; assert(Number.isFinite(stride) && stride >= 0, "Stride overflow while computing row-major strides"); assert(stride <= Number.MAX_SAFE_INTEGER, "Stride overflow while computing row-major strides"); } return strides; }; var validateStridesBytes = (stridesBytes, ndim, bytesPerElement2) => { assert(Array.isArray(stridesBytes), "stridesBytes must be an array"); assert(stridesBytes.length === ndim, `stridesBytes length (${stridesBytes.length}) must equal shape length (${ndim})`); const out = new Array(ndim); for (let i = 0; i < ndim; i++) { const s = stridesBytes[i]; assert(Number.isInteger(s), `stridesBytes[${i}] must be an integer (got ${s})`); assert(s >= -2147483648 && s <= 2147483647, `stridesBytes[${i}] must fit in i32 (got ${s})`); assert(s % bytesPerElement2 === 0, `stridesBytes[${i}] (${s}) must be a multiple of bytesPerElement (${bytesPerElement2})`); out[i] = s; } return out; }; var validateOffsetBytes = (offsetBytes, bytesPerElement2) => { const off = offsetBytes ?? 0; assert(Number.isInteger(off) && off >= 0, `offsetBytes must be an integer >= 0 (got ${off})`); assert(off % bytesPerElement2 === 0, `offsetBytes (${off}) must be a multiple of bytesPerElement (${bytesPerElement2})`); return off; }; var numelFromShape = (shape) => { let n = 1; for (let i = 0; i < shape.length; i++) { n *= shape[i]; if (shape[i] === 0) return 0; assert(Number.isFinite(n), "numel overflow"); } return n; }; var requiredBackingBytes = (shape, stridesBytes, offsetBytes, bytesPerElement2) => { if (shape.length === 0) { const req2 = offsetBytes + bytesPerElement2; assert(req2 <= 4294967295, `required backing bytes exceeds wasm32 address space (got ${req2})`); return req2; } if (numelFromShape(shape) === 0) return 0; let min = BigInt(offsetBytes); let max = BigInt(offsetBytes); for (let i = 0; i < shape.length; i++) { const dim = shape[i]; const s = BigInt(stridesBytes[i]); const extent = BigInt(dim - 1) * s; if (extent < 0n) min += extent; else max += extent; } assert(min >= 0n, `layout underflows: minimum byte offset is ${min} (offsetBytes is too small for negative strides)`); const req = max + BigInt(bytesPerElement2); assert(req <= BigInt(4294967295), `required backing bytes exceeds wasm32 address space (got ${req})`); return Number(req); }; var isContiguousRowMajor = (shape, stridesBytes, offsetBytes, bytesPerElement2) => { if (offsetBytes !== 0) return false; if (shape.length === 0) return true; if (numelFromShape(shape) === 0) return true; const expected = defaultRowMajorStridesBytes(shape, bytesPerElement2); for (let i = 0; i < shape.length; i++) if (stridesBytes[i] !== expected[i]) return false; return true; }; var Ndarray = class { dtype; shape; stridesBytes; offsetBytes; bytesPerElement; numel; byteLength; constructor(dtype, shape, stridesBytes, offsetBytes, byteLength) { this.dtype = dtype; this.shape = shape; this.stridesBytes = stridesBytes; this.offsetBytes = offsetBytes; this.bytesPerElement = dtypeInfo(dtype).bytesPerElement; this.numel = numelFromShape(shape); this.byteLength = byteLength; } get ndim() { return this.shape.length; } get wgslScalarType() { return dtypeInfo(this.dtype).wgslScalarType; } get isContiguousC() { return isContiguousRowMajor(this.shape, this.stridesBytes, this.offsetBytes, this.bytesPerElement); } layout() { return { shape: this.shape.slice(), stridesBytes: this.stridesBytes.slice(), offsetBytes: this.offsetBytes }; } }; var CPUndarray = class _CPUndarray extends Ndarray { _basePtrBytes; _shapePtr; _stridesPtr; _destroyed = false; _buf = null; _all = null; constructor(dtype, shape, stridesBytes, offsetBytes, byteLength, basePtrBytes, shapePtr, stridesPtr) { super(dtype, shape, stridesBytes, offsetBytes, byteLength); this._basePtrBytes = basePtrBytes; this._shapePtr = shapePtr; this._stridesPtr = stridesPtr; } static empty(dtype, layout) { wasm.memory(); const info = dtypeInfo(dtype); const shape = validateShape(layout.shape); const offsetBytes = validateOffsetBytes(layout.offsetBytes, info.bytesPerElement); const stridesBytes = layout.stridesBytes ? validateStridesBytes(layout.stridesBytes, shape.length, info.bytesPerElement) : defaultRowMajorStridesBytes(shape, info.bytesPerElement); const byteLength = requiredBackingBytes(shape, stridesBytes, offsetBytes, info.bytesPerElement); const ndim = shape.length >>> 0; let shapePtr = 0; let stridesPtr = 0; let basePtrBytes = 0; try { shapePtr = wasm.allocU32(ndim); assert(shapePtr !== 0 || ndim === 0, `CPUndarray.empty(): shape allocation failed (${ndim} elements)`); stridesPtr = wasm.allocU32(ndim); assert(stridesPtr !== 0 || ndim === 0, `CPUndarray.empty(): strides allocation failed (${ndim} elements)`); const shapeView = wasm.u32view(shapePtr, ndim); for (let i = 0; i < shape.length; i++) shapeView[i] = shape[i] >>> 0; const strideView = wasm.i32view(stridesPtr, ndim); for (let i = 0; i < stridesBytes.length; i++) strideView[i] = stridesBytes[i] | 0; basePtrBytes = byteLength > 0 ? wasm.allocBytes(byteLength >>> 0) : 0; assert(basePtrBytes !== 0 || byteLength === 0, `CPUndarray.empty(): backing allocation failed (${byteLength} bytes)`); return new _CPUndarray(dtype, shape, stridesBytes, offsetBytes, byteLength, basePtrBytes, shapePtr, stridesPtr); } catch (error) { if (basePtrBytes) wasm.freeBytes(basePtrBytes, byteLength >>> 0); if (stridesPtr) wasm.freeU32(stridesPtr, ndim); if (shapePtr) wasm.freeU32(shapePtr, ndim); throw error; } } static zeros(dtype, layout) { const a = _CPUndarray.empty(dtype, layout); try { a.zero_(); return a; } catch (error) { a.destroy(); throw error; } } static fromArray(dtype, shape, src) { const dst = _CPUndarray.empty(dtype, { shape }); try { assert(dst.isContiguousC, "CPUndarray.fromArray currently requires a contiguous row-major layout"); assert(src.length >= dst.numel, `source length (${src.length}) must be >= numel (${dst.numel})`); const data = dst.data(); for (let i = 0; i < dst.numel; i++) data[i] = src[i]; return dst; } catch (error) { dst.destroy(); throw error; } } get residency() { return "cpu-webassembly"; } get destroyed() { return this._destroyed; } get basePtrBytes() { this.assertAlive(); return this._basePtrBytes; } get shapePtr() { this.assertAlive(); return this._shapePtr; } get stridesPtr() { this.assertAlive(); return this._stridesPtr; } assertAlive() { assert(!this._destroyed, "CPUndarray has been destroyed"); } ensureAllView() { this.assertAlive(); const buf = wasm.memory().buffer; if (this._buf !== buf) { this._buf = buf; const ctor = dtypeInfo(this.dtype).ctor; this._all = new ctor(buf); } return this._all; } backingBytes() { this.assertAlive(); if (this.byteLength === 0) return new Uint8Array(wasm.memory().buffer, 0, 0); return wasm.u8view(this._basePtrBytes, this.byteLength >>> 0); } data() { this.assertAlive(); assert(this.isContiguousC, "CPUndarray.data() requires a contiguous row-major layout (use backingBytes() for raw backing storage)"); if (this.numel === 0) { const buf = wasm.memory().buffer; const ctor = dtypeInfo(this.dtype).ctor; return new ctor(buf, 0, 0); } return new (dtypeInfo(this.dtype)).ctor(wasm.memory().buffer, this._basePtrBytes + this.offsetBytes >>> 0, this.numel >>> 0); } offsetBytesAt(indices) { this.assertAlive(); assert(indices.length === this.ndim, `expected ${this.ndim} indices, got ${indices.length}`); if (this.ndim === 0) return this.offsetBytes; let off = this.offsetBytes; for (let i = 0; i < this.ndim; i++) { const v = indices[i]; assert(Number.isInteger(v) && v >= 0, `index[${i}] must be an integer >= 0 (got ${v})`); assert(v <= 4294967295, `index[${i}] must fit in u32 (got ${v})`); assert(v < this.shape[i], "index out of bounds (or offset overflow)"); off += this.stridesBytes[i] * v; } assert(Number.isSafeInteger(off) && off >= 0 && off <= 4294967295, "index out of bounds (or offset overflow)"); assert(off + this.bytesPerElement <= this.byteLength, "computed byte offset is outside backing storage"); return off; } get(...indices) { const off = this.offsetBytesAt(indices); const abs = this._basePtrBytes + off >>> 0; assert(abs % this.bytesPerElement === 0, "internal error: misaligned element address"); const i = abs / this.bytesPerElement; const all = this.ensureAllView(); return all[i]; } set(value, ...indices) { const off = this.offsetBytesAt(indices); const abs = this._basePtrBytes + off >>> 0; assert(abs % this.bytesPerElement === 0, "internal error: misaligned element address"); const i = abs / this.bytesPerElement; const all = this.ensureAllView(); all[i] = value; } zero_() { this.assertAlive(); if (this.byteLength === 0) return; this.backingBytes().fill(0); } uploadToGPU(ctx, desc = {}) { const bytes = this.backingBytes(); const sb = new StorageBuffer(ctx.device, ctx.queue, { label: desc.label, byteLength: this.byteLength, data: bytes, copyDst: desc.copyDst, copySrc: desc.copySrc, usage: desc.usage }); return new GPUndarray(this.dtype, this.shape.slice(), this.stridesBytes.slice(), this.offsetBytes, this.byteLength, sb, 0, true, ctx.readback ?? null); } destroy() { if (this._destroyed) return; const ndim = this.ndim >>> 0; if (this._basePtrBytes) wasm.freeBytes(this._basePtrBytes, this.byteLength >>> 0); if (this._stridesPtr) wasm.freeU32(this._stridesPtr, ndim); if (this._shapePtr) wasm.freeU32(this._shapePtr, ndim); this._destroyed = true; this._basePtrBytes = 0; this._shapePtr = 0; this._stridesPtr = 0; this._buf = null; this._all = null; } }; var GPUndarray = class _GPUndarray extends Ndarray { buffer; baseOffsetBytes; owned; readback; constructor(dtype, shape, stridesBytes, offsetBytes, byteLength, buffer, baseOffsetBytes = 0, owned = false, readback = null) { super(dtype, shape, stridesBytes, offsetBytes, byteLength); assert(Number.isInteger(baseOffsetBytes) && baseOffsetBytes >= 0, `baseOffsetBytes must be an integer >= 0 (got ${baseOffsetBytes})`); assert((baseOffsetBytes & 3) === 0, `baseOffsetBytes must be 4-byte aligned for storage buffers (got ${baseOffsetBytes})`); this.buffer = buffer; this.baseOffsetBytes = baseOffsetBytes; this.owned = owned; this.readback = readback; } static empty(ctx, dtype, layout, desc = {}) { const info = dtypeInfo(dtype); const shape = validateShape(layout.shape); const offsetBytes = validateOffsetBytes(layout.offsetBytes, info.bytesPerElement); const stridesBytes = layout.stridesBytes ? validateStridesBytes(layout.stridesBytes, shape.length, info.bytesPerElement) : defaultRowMajorStridesBytes(shape, info.bytesPerElement); const byteLength = requiredBackingBytes(shape, stridesBytes, offsetBytes, info.bytesPerElement); const sb = new StorageBuffer(ctx.device, ctx.queue, { label: desc.label, byteLength, copyDst: desc.copyDst, copySrc: desc.copySrc, usage: desc.usage }); return new _GPUndarray(dtype, shape, stridesBytes, offsetBytes, byteLength, sb, 0, true, ctx.readback ?? null); } static wrap(buffer, dtype, layout, baseOffsetBytes = 0) { const info = dtypeInfo(dtype); const shape = validateShape(layout.shape); const offsetBytes = validateOffsetBytes(layout.offsetBytes, info.bytesPerElement); const stridesBytes = layout.stridesBytes ? validateStridesBytes(layout.stridesBytes, shape.length, info.bytesPerElement) : defaultRowMajorStridesBytes(shape, info.bytesPerElement); const byteLength = requiredBackingBytes(shape, stridesBytes, offsetBytes, info.bytesPerElement); return new _GPUndarray(dtype, shape, stridesBytes, offsetBytes, byteLength, buffer, baseOffsetBytes, false); } get residency() { return "gpu-storagebuffer"; } bindingResource() { return { buffer: this.buffer, offset: this.baseOffsetBytes, size: alignTo(this.byteLength, 4) }; } async readbackToCPU() { assert(this.buffer.canReadback, "GPUndarray.readbackToCPU() requires the underlying StorageBuffer to be created with copySrc: true"); const cpu = CPUndarray.empty(this.dtype, { shape: this.shape, stridesBytes: this.stridesBytes, offsetBytes: this.offsetBytes }); try { if (this.readback && !this.readback.isDestroyed) { await this.readback.readIntoWasmMemory(wasm.memory(), cpu.basePtrBytes, this.buffer, this.baseOffsetBytes, this.byteLength, { label: "GPUndarray:readbackToCPU" }); } else { const bytes = await this.buffer.read(this.baseOffsetBytes, this.byteLength); cpu.backingBytes().set(new Uint8Array(bytes), 0); } return cpu; } catch (error) { cpu.destroy(); throw error; } } destroy() { if (this.owned) this.buffer.destroy(); } }; // typescript/wasm/interop.ts var assertWasmMemoryView = (source, label) => { assert(source instanceof WasmMemoryView, `${label} must be a WasmMemoryView.`); return source; }; var assertWasmViewDType = (source, dtype, label) => { const view = assertWasmMemoryView(source, label); assert(view.dtype === dtype, `${label} dtype must be '${dtype}'.`); return view; }; var assertWasmF32View = (source, label) => assertWasmViewDType(source, "f32", label); var assertWasmU16View = (source, label) => assertWasmViewDType(source, "u16", label); var assertWasmU32View = (source, label) => assertWasmViewDType(source, "u32", label); var assertWasmRecordCount = (value, label = "record count") => { assert(typeof value === "number" && Number.isFinite(value), `${label} must be a finite number.`); assert(Number.isInteger(value) && value >= 0, `${label} must be an integer >= 0.`); assert(Number.isSafeInteger(value), `${label} must be a safe integer.`); return value; }; var assertWasmCapacity = (value, label = "wasmCapacity") => { if (value === void 0) return 0; assert(typeof value === "number" && Number.isFinite(value), `${label} must be a finite number.`); assert(Number.isInteger(value) && value >= 0, `${label} must be an integer >= 0.`); assert(Number.isSafeInteger(value), `${label} must be a safe integer.`); return value; }; var validateWasmRecordRange = (source, count, componentsPerRecord, sourceLabel, countTerm = "count") => { const safeCount = assertWasmRecordCount(count, countTerm); assert(typeof componentsPerRecord === "number" && Number.isFinite(componentsPerRecord), `${sourceLabel} componentsPerRecord must be finite.`); assert(Number.isInteger(componentsPerRecord) && componentsPerRecord > 0, `${sourceLabel} componentsPerRecord must be an integer > 0.`); assert(Number.isSafeInteger(componentsPerRecord), `${sourceLabel} componentsPerRecord must be a safe integer.`); assert(safeCount <= Math.floor(Number.MAX_SAFE_INTEGER / componentsPerRecord), `${sourceLabel} ${countTerm}*${componentsPerRecord} exceeds Number.MAX_SAFE_INTEGER.`); const requiredLength = safeCount * componentsPerRecord; assert(source.length >= requiredLength, `${sourceLabel} length must be at least ${countTerm}*${componentsPerRecord}.`); }; var resolveWasmRecordCount = (source, explicitCount, componentsPerRecord, sourceLabel, countLabel = "record count", countTerm = "count") => { assert(Number.isInteger(componentsPerRecord) && componentsPerRecord > 0, `${sourceLabel} componentsPerRecord must be an integer > 0.`); if (explicitCount !== void 0) { const count = assertWasmRecordCount(explicitCount, countLabel); validateWasmRecordRange(source, count, componentsPerRecord, sourceLabel, countTerm); return count; } assert(source.length % componentsPerRecord === 0, `${sourceLabel} length must be a multiple of ${componentsPerRecord} when ${countTerm} is not provided.`); return source.length / componentsPerRecord; }; var growWasmCapacity = (requiredCount, currentCapacity = 0) => { const required = assertWasmRecordCount(requiredCount, "wasm required capacity"); const current = assertWasmCapacity(currentCapacity, "wasm current capacity"); if (required <= current) return current; let capacity = current > 0 ? current : 1; while (capacity < required) { const next = capacity * 2; assert(Number.isSafeInteger(next) && next <= Number.MAX_SAFE_INTEGER, "wasm capacity growth exceeds Number.MAX_SAFE_INTEGER."); capacity = next; } return capacity; }; var isWebAssemblyMemory = (x) => typeof WebAssembly !== "undefined" && typeof WebAssembly.Memory !== "undefined" && x instanceof WebAssembly.Memory; var isWebAssemblyGlobal = (x) => typeof WebAssembly !== "undefined" && typeof WebAssembly.Global !== "undefined" && x instanceof WebAssembly.Global; var describeLabel = (label, name) => name ? `${label} '${name}'` : label; var normalizeName = (name) => { if (!name) return null; return name; }; var assertNonNegativeInteger = (value, label) => { if (typeof value === "bigint") { assert(value >= 0n, `${label} must be >= 0 (got ${value.toString()})`); assert(value <= BigInt(Number.MAX_SAFE_INTEGER), `${label} exceeds Number.MAX_SAFE_INTEGER (got ${value.toString()})`); return Number(value); } assert(typeof value === "number", `${label} must be a number or bigint (got ${typeof value})`); assert(Number.isFinite(value), `${label} must be finite (got ${value})`); assert(Number.isInteger(value), `${label} must be an integer (got ${value})`); assert(value >= 0, `${label} must be >= 0 (got ${value})`); assert(Number.isSafeInteger(value), `${label} must be a safe integer (got ${value})`); return value; }; var assertCallArg = (arg, label) => { if (typeof arg === "bigint") return arg; assert(typeof arg === "number", `${label} must be a number or bigint (got ${typeof arg})`); assert(Number.isFinite(arg), `${label} must be finite (got ${arg})`); return arg; }; var resolveCallArgs = (args, label) => { if (args === void 0) return []; assert(Array.isArray(args), `${label} args must be an array when provided.`); if (args.length === 0) return []; const out = new Array(args.length); for (let i = 0; i < args.length; i++) out[i] = assertCallArg(args[i], `${label} args[${i}]`); return out; }; var assertByteOffset = (byteOffset, label) => { if (byteOffset === void 0) return 0; assert(Number.isFinite(byteOffset), `${label} byteOffset must be finite (got ${byteOffset})`); assert(Number.isInteger(byteOffset), `${label} byteOffset must be an integer (got ${byteOffset})`); assert(byteOffset >= 0, `${label} byteOffset must be >= 0 (got ${byteOffset})`); assert(Number.isSafeInteger(byteOffset), `${label} byteOffset must be a safe integer (got ${byteOffset})`); return byteOffset; }; var checkedAdd = (a, b, label) => { const out = a + b; assert(Number.isSafeInteger(out), `${label} overflowed Number.MAX_SAFE_INTEGER`); return out; }; var checkedMul = (a, b, label) => { const out = a * b; assert(Number.isSafeInteger(out), `${label} overflowed Number.MAX_SAFE_INTEGER`); return out; }; var resolveTypedArrayCtor = (dtype) => { const info = dtypeInfo(dtype); return info.ctor; }; var WasmModule = class _WasmModule { exportsObject; defaultMemorySource; name; constructor(exportsObject = {}, options = {}) { this.exportsObject = exportsObject; this.defaultMemorySource = options.memory; this.name = normalizeName(options.name); } static fromInstance(instance, options = {}) { assert(typeof instance === "object" && instance !== null, "WasmModule.fromInstance(): instance must be an object with an exports field."); const exportsObject = instance.exports; assert(typeof exportsObject === "object" && exportsObject !== null, "WasmModule.fromInstance(): instance.exports must be an object."); return new _WasmModule(exportsObject, options); } static fromExports(exportsObject, options = {}) { assert(typeof exportsObject === "object" && exportsObject !== null, "WasmModule.fromExports(): exports must be an object."); return new _WasmModule(exportsObject, options); } static fromMemory(memory, options = {}) { assert(isWebAssemblyMemory(memory), "WasmModule.fromMemory(): memory must be a WebAssembly.Memory."); return new _WasmModule({}, { ...options, memory }); } getExport(name) { assert(typeof name === "string" && name.length > 0, "WasmModule.getExport(): name must be a non-empty string."); assert(Object.prototype.hasOwnProperty.call(this.exportsObject, name), `${describeLabel("WasmModule export", this.name)} does not contain export '${name}'.`); return this.exportsObject[name]; } getFunction(name) { const value = this.getExport(name); assert(typeof value === "function", `${describeLabel("WasmModule export", this.name)} '${name}' is not a function.`); return value; } getGlobal(name) { const value = this.getExport(name); assert(isWebAssemblyGlobal(value), `${describeLabel("WasmModule export", this.name)} '${name}' is not a WebAssembly.Global.`); return value; } memory(nameOrMemory) { const source = nameOrMemory !== void 0 ? nameOrMemory : this.defaultMemorySource; if (isWebAssemblyMemory(source)) return source; if (typeof source === "string") { const value = this.getExport(source); assert(isWebAssemblyMemory(value), `${describeLabel("WasmModule export", this.name)} '${source}' is not a WebAssembly.Memory.`); return value; } if (source !== void 0 && source !== null) throw new Error(`${describeLabel("WasmModule", this.name)} received an invalid memory source. Expected a WebAssembly.Memory or memory export name.`); const memoryExports = this.memoryExportNames(); if (memoryExports.length === 1) return this.memory(memoryExports[0]); if (memoryExports.length === 0) throw new Error(`${describeLabel("WasmModule", this.name)} could not resolve a WebAssembly.Memory. Pass memory explicitly or export one memory.`); throw new Error(`${describeLabel("WasmModule", this.name)} has multiple memory exports (${memoryExports.join(", ")}). Pass memory explicitly.`); } view(descriptor) { return new WasmMemoryView(this, descriptor); } readBytes(descriptor) { const resolved = this._resolveBytes(descriptor); return new Uint8Array(resolved.memory.buffer, resolved.ptr >>> 0, resolved.byteLength >>> 0); } readUtf8(ptr, length, options = {}) { const name = normalizeName(options.name); const resolved = this._resolveBytes({ memory: options.memory, ptr, byteLength: length, byteOffset: options.byteOffset, name: name ?? void 0 }); const decoder = new TextDecoder("utf-8", { fatal: options.fatal ?? false, ignoreBOM: options.ignoreBOM ?? false }); return decoder.decode(new Uint8Array(resolved.memory.buffer, resolved.ptr >>> 0, resolved.byteLength >>> 0)); } dataView(descriptor) { const resolved = this._resolveBytes(descriptor); return new DataView(resolved.memory.buffer, resolved.ptr >>> 0, resolved.byteLength >>> 0); } _resolveView(descriptor) { const name = normalizeName(descriptor.name); const label = describeLabel("WasmMemoryView", name); const dtype = descriptor.dtype; const bytesPerElement2 = dtypeInfo(dtype).bytesPerElement >>> 0; const length = this.resolveValue(descriptor.length, `${label} length`); const byteLength = checkedMul(length, bytesPerElement2, `${label} byteLength`); const resolved = this.resolveByteRange(descriptor.memory, descriptor.ptr, descriptor.byteOffset, byteLength, label); assert(resolved.ptr % bytesPerElement2 === 0, `${label} ptr ${resolved.ptr} is not aligned for dtype '${dtype}' (${bytesPerElement2} bytes).`); return { memory: resolved.memory, ptr: resolved.ptr >>> 0, length: length >>> 0, byteLength: byteLength >>> 0, dtype, name }; } _resolveBytes(descriptor) { const name = normalizeName(descriptor.name); const label = describeLabel("external WebAssembly memory range", name); const byteLength = this.resolveValue(descriptor.byteLength, `${label} byteLength`); return this.resolveByteRange(descriptor.memory, descriptor.ptr, descriptor.byteOffset, byteLength, label); } memoryExportNames() { const names = []; for (const [name, value] of Object.entries(this.exportsObject)) if (isWebAssemblyMemory(value)) names.push(name); return names; } resolveByteRange(memorySource, ptrDescriptor, byteOffset, byteLength, label) { const memory = this.memory(memorySource); assert(isWebAssemblyMemory(memory), `${label} requires a valid WebAssembly.Memory.`); const basePtr = this.resolveValue(ptrDescriptor, `${label} ptr`); const extraOffset = assertByteOffset(byteOffset, label); const ptr = checkedAdd(basePtr, extraOffset, `${label} ptr + byteOffset`); const end = checkedAdd(ptr, byteLength, `${label} ptr + byteLength`); const bufferByteLength = memory.buffer.byteLength >>> 0; assert(end <= bufferByteLength, `${label} range [${ptr}, ${end}) is out of bounds for memory byteLength ${bufferByteLength}.`); return { memory, ptr: ptr >>> 0, byteLength: byteLength >>> 0 }; } resolveValue(descriptor, label) { if (typeof descriptor === "number" || typeof descriptor === "bigint") return assertNonNegativeInteger(descriptor, label); if (typeof descriptor === "string") { const value2 = this.getExport(descriptor); if (typeof value2 === "function") return assertNonNegativeInteger(value2(), `${label} export '${descriptor}' result`); if (isWebAssemblyGlobal(value2)) return assertNonNegativeInteger(value2.value, `${label} export '${descriptor}' value`); throw new Error(`${label} export '${descriptor}' must be a function or WebAssembly.Global.`); } if (typeof descriptor === "function") return assertNonNegativeInteger(descriptor(), `${label} callback result`); assert(typeof descriptor === "object" && descriptor !== null, `${label} descriptor must be a number, bigint, export name string, callback, or descriptor object.`); if (Object.prototype.hasOwnProperty.call(descriptor, "function")) { const functionDescriptor = descriptor; assert(typeof functionDescriptor.function === "function", `${label} function descriptor must contain a callable function.`); const args = resolveCallArgs(functionDescriptor.args, label); return assertNonNegativeInteger(functionDescriptor.function(...args), `${label} function result`); } if (Object.prototype.hasOwnProperty.call(descriptor, "global")) { const globalDescriptor = descriptor; assert(isWebAssemblyGlobal(globalDescriptor.global), `${label} global descriptor must contain a WebAssembly.Global.`); return assertNonNegativeInteger(globalDescriptor.global.value, `${label} global value`); } assert(Object.prototype.hasOwnProperty.call(descriptor, "export"), `${label} descriptor object requires one of 'function', 'global', or 'export'.`); const exportDescriptor = descriptor; assert(typeof exportDescriptor.export === "string" && exportDescriptor.export.length > 0, `${label} export descriptor requires a non-empty export name.`); assert(exportDescriptor.kind === void 0 || exportDescriptor.kind === "function" || exportDescriptor.kind === "global", `${label} export descriptor kind must be 'function' or 'global' when provided.`); if (exportDescriptor.kind === "global") return assertNonNegativeInteger(this.getGlobal(exportDescriptor.export).value, `${label} export '${exportDescriptor.export}' value`); if (exportDescriptor.kind === "function") return assertNonNegativeInteger(this.getFunction(exportDescriptor.export)(...resolveCallArgs(exportDescriptor.args, label)), `${label} export '${exportDescriptor.export}' result`); const value = this.getExport(exportDescriptor.export); if (typeof value === "function") return assertNonNegativeInteger(value(...resolveCallArgs(exportDescriptor.args, label)), `${label} export '${exportDescriptor.export}' result`); if (isWebAssemblyGlobal(value)) { assert(resolveCallArgs(exportDescriptor.args, label).length === 0, `${label} export '${exportDescriptor.export}' is a global and does not accept args.`); return assertNonNegativeInteger(value.value, `${label} export '${exportDescriptor.export}' value`); } throw new Error(`${label} export '${exportDescriptor.export}' must be a function or WebAssembly.Global.`); } }; var WasmMemoryView = class { moduleRef; descriptor; state; cachedBuffer = null; cachedArray = null; cachedBytes = null; cachedDataView = null; constructor(moduleRef, descriptor) { this.moduleRef = moduleRef; this.descriptor = descriptor; this.state = this.moduleRef._resolveView(this.descriptor); } get memory() { return this.state.memory; } get ptr() { return this.state.ptr >>> 0; } get length() { return this.state.length >>> 0; } get byteLength() { return this.state.byteLength >>> 0; } get dtype() { return this.state.dtype; } get name() { return this.state.name; } refresh() { this.state = this.moduleRef._resolveView(this.descriptor); this.cachedBuffer = null; this.cachedArray = null; this.cachedBytes = null; this.cachedDataView = null; return this; } array() { this.ensureCachedViews(); return this.cachedArray; } bytes() { this.ensureCachedViews(); return this.cachedBytes; } dataView() { this.ensureCachedViews(); return this.cachedDataView; } copy() { const src = this.array(); const ctor = resolveTypedArrayCtor(this.dtype); const out = new ctor(new ArrayBuffer(this.byteLength >>> 0), 0, this.length >>> 0); out.set(src); return out; } copyInto(target) { const label = describeLabel("WasmMemoryView", this.name); assert(ArrayBuffer.isView(target) && !(target instanceof DataView), `${label} copyInto target must be a numeric TypedArray.`); if (target instanceof Uint8Array || target instanceof Int8Array) { assert(target.byteLength >>> 0 >= this.byteLength >>> 0, `${label} copyInto target is too small for ${this.byteLength} bytes.`); new Uint8Array(target.buffer, target.byteOffset >>> 0, this.byteLength >>> 0).set(this.bytes()); return; } const expectedCtor = resolveTypedArrayCtor(this.dtype); assert(target.constructor === expectedCtor, `${label} copyInto target must be dtype-compatible with '${this.dtype}'. Use Uint8Array or Int8Array for raw byte copies.`); assert(target.length >>> 0 >= this.length >>> 0, `${label} copyInto target is too small for ${this.length} elements.`); const dst = target; dst.set(this.array(), 0); } ensureCachedViews() { const buffer = this.state.memory.buffer; if (this.cachedBuffer === buffer && this.cachedArray && this.cachedBytes && this.cachedDataView) return; const ctor = resolveTypedArrayCtor(this.state.dtype); this.cachedBuffer = buffer; this.cachedArray = new ctor(buffer, this.state.ptr >>> 0, this.state.length >>> 0); this.cachedBytes = new Uint8Array(buffer, this.state.ptr >>> 0, this.state.byteLength >>> 0); this.cachedDataView = new DataView(buffer, this.state.ptr >>> 0, this.state.byteLength >>> 0); } }; var webassemblyInterop = Object.freeze({ fromInstance: (instance, options = {}) => WasmModule.fromInstance(instance, options), fromExports: (exportsObject, options = {}) => WasmModule.fromExports(exportsObject, options), fromMemory: (memory, options = {}) => WasmModule.fromMemory(memory, options) }); // typescript/core/transform.ts var NO_PARENT = 4294967295; var allocF32Checked = (len, label) => { const length = len >>> 0; const ptr = wasm.allocF32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly f32 allocation failed (${length} elements).`); return ptr; }; var allocU32Checked = (len, label) => { const length = len >>> 0; const ptr = wasm.allocU32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly u32 allocation failed (${length} elements).`); return ptr; }; var TransformStore = class _TransformStore { static _global = null; static global() { if (!_TransformStore._global) _TransformStore._global = new _TransformStore(16384); return _TransformStore._global; } cap; count = 0; posPtr = 0; rotPtr = 0; sclPtr = 0; localPtr = 0; worldPtr = 0; parentPtr = 0; orderPtr = 0; tmpAxisPtr = 0; tmpQuatPtr = 0; dirtyIndicesPtr = 0; dirtyIndicesCap = 0; _buf = null; _f32 = null; _u32 = null; _dirty = true; _orderDirty = true; _dirtyAll = true; _dirtyList = []; _dirtyMark = new Uint8Array(0); _nodes = []; _freeList = []; _visited = new Uint8Array(0); _stack = []; constructor(initialCap) { this.cap = Math.max(1, initialCap | 0); this.allocateArrays(this.cap); } allocateArrays(cap) { const f32Allocs = []; const u32Allocs = []; const allocF32 = (len, name) => { const ptr = allocF32Checked(len, `TransformStore.${name}`); if (ptr) f32Allocs.push({ ptr, len }); return ptr; }; const allocU32 = (len, name) => { const ptr = allocU32Checked(len, `TransformStore.${name}`); if (ptr) u32Allocs.push({ ptr, len }); return ptr; }; try { const posPtr = allocF32(cap * 3, "positions"); const rotPtr = allocF32(cap * 4, "rotations"); const sclPtr = allocF32(cap * 3, "scales"); const localPtr = allocF32(cap * 16, "localMatrices"); const worldPtr = allocF32(cap * 16, "worldMatrices"); const parentPtr = allocU32(cap, "parents"); const orderPtr = allocU32(cap, "order"); const tmpAxisPtr = this.tmpAxisPtr || allocF32(4, "temporaryAxis"); const tmpQuatPtr = this.tmpQuatPtr || allocF32(4, "temporaryQuaternion"); wasm.u32view(parentPtr, cap).fill(NO_PARENT); this.posPtr = posPtr; this.rotPtr = rotPtr; this.sclPtr = sclPtr; this.localPtr = localPtr; this.worldPtr = worldPtr; this.parentPtr = parentPtr; this.orderPtr = orderPtr; this.tmpAxisPtr = tmpAxisPtr; this.tmpQuatPtr = tmpQuatPtr; this._buf = null; } catch (error) { for (let i = u32Allocs.length - 1; i >= 0; i--) wasm.freeU32(u32Allocs[i].ptr, u32Allocs[i].len); for (let i = f32Allocs.length - 1; i >= 0; i--) wasm.freeF32(f32Allocs[i].ptr, f32Allocs[i].len); throw error; } } ensureViews() { const buf = wasm.memory().buffer; if (this._buf !== buf) { this._buf = buf; this._f32 = new Float32Array(buf); this._u32 = new Uint32Array(buf); } } f32() { this.ensureViews(); return this._f32; } u32() { this.ensureViews(); return this._u32; } ensureDirtyMarkCapacity() { if (this._dirtyMark.length >= this.cap) return; const next = new Uint8Array(this.cap); for (let i = 0; i < this._dirtyList.length; i++) next[this._dirtyList[i]] = 1; this._dirtyMark = next; } ensureDirtyIndexCapacity(minLen) { if (this.dirtyIndicesCap >= minLen) return; let cap = Math.max(1, this.dirtyIndicesCap | 0); while (cap < minLen) cap *= 2; const nextPtr = allocU32Checked(cap, "TransformStore.dirtyIndices"); const oldPtr = this.dirtyIndicesPtr; const oldCap = this.dirtyIndicesCap; this.dirtyIndicesPtr = nextPtr; this.dirtyIndicesCap = cap; if (oldPtr) wasm.freeU32(oldPtr, oldCap); } clearDirtyList() { for (let i = 0; i < this._dirtyList.length; i++) this._dirtyMark[this._dirtyList[i]] = 0; this._dirtyList.length = 0; } markDirty() { this._dirty = true; this._dirtyAll = true; this.clearDirtyList(); } markOrderDirty() { this._orderDirty = true; this._dirty = true; this._dirtyAll = true; this.clearDirtyList(); } markIndexDirty(index) { if (index < 0 || index >= this.count) return; this._dirty = true; if (this._dirtyAll) return; this.ensureDirtyMarkCapacity(); if (this._dirtyMark[index]) return; this._dirtyMark[index] = 1; this._dirtyList.push(index); } alloc(node) { let index; if (this._freeList.length > 0) { index = this._freeList.pop(); if (index < 0) throw new Error("TransformStore.alloc: corrupted free list (negative index)."); if (index >= this.count) this.count = index + 1; if (this._nodes[index] !== null && this._nodes[index] !== void 0) throw new Error(`TransformStore.alloc: free list returned an in-use slot ${index}.`); } else { if (this.count >= this.cap) this.growTo(this.cap * 2); index = this.count++; } this._nodes[index] = node; this.initDefaults(index); this._orderDirty = true; this._dirty = true; this._dirtyAll = true; return index; } initDefaults(index) { this.ensureViews(); const f32 = this.f32(); const u32 = this.u32(); let p = (this.posPtr >>> 2) + index * 3; f32[p + 0] = 0; f32[p + 1] = 0; f32[p + 2] = 0; let r = (this.rotPtr >>> 2) + index * 4; f32[r + 0] = 0; f32[r + 1] = 0; f32[r + 2] = 0; f32[r + 3] = 1; let s = (this.sclPtr >>> 2) + index * 3; f32[s + 0] = 1; f32[s + 1] = 1; f32[s + 2] = 1; u32[(this.parentPtr >>> 2) + index] = NO_PARENT; } setParent(childIndex, parentIndex) { this.ensureViews(); const u32 = this.u32(); u32[(this.parentPtr >>> 2) + childIndex] = parentIndex === null ? NO_PARENT : parentIndex >>> 0; this._orderDirty = true; this._dirty = true; this._dirtyAll = true; } free(index) { if (index < 0 || index >= this.count) throw new Error(`TransformStore.free: index out of range: ${index} (count=${this.count})`); const node = this._nodes[index]; if (!node) throw new Error(`TransformStore.free: double free or invalid slot: ${index}`); this._nodes[index] = null; this.ensureViews(); const f32 = this.f32(); const u32 = this.u32(); let p = (this.posPtr >>> 2) + index * 3; f32[p + 0] = 0; f32[p + 1] = 0; f32[p + 2] = 0; let r = (this.rotPtr >>> 2) + index * 4; f32[r + 0] = 0; f32[r + 1] = 0; f32[r + 2] = 0; f32[r + 3] = 1; let s = (this.sclPtr >>> 2) + index * 3; f32[s + 0] = 1; f32[s + 1] = 1; f32[s + 2] = 1; u32[(this.parentPtr >>> 2) + index] = NO_PARENT; const localBase = (this.localPtr >>> 2) + index * 16; const worldBase = (this.worldPtr >>> 2) + index * 16; for (let i = 0; i < 16; i++) { f32[localBase + i] = 0; f32[worldBase + i] = 0; } f32[localBase + 0] = 1; f32[localBase + 5] = 1; f32[localBase + 10] = 1; f32[localBase + 15] = 1; f32[worldBase + 0] = 1; f32[worldBase + 5] = 1; f32[worldBase + 10] = 1; f32[worldBase + 15] = 1; this._freeList.push(index); this._orderDirty = true; this._dirty = true; this._dirtyAll = true; while (this.count > 0) { const last = this.count - 1; if (this._nodes[last]) break; this.count--; } } updateIfNeeded() { if (!this._dirty) return; this.update(); } update() { const count = this.count | 0; if (count === 0) { this._dirty = false; this._dirtyAll = false; this._orderDirty = false; this.clearDirtyList(); return; } this.ensureDirtyMarkCapacity(); const dirtyCount = this._dirtyAll ? count : this._dirtyList.length | 0; const useFull = this._orderDirty || this._dirtyAll || dirtyCount > count >>> 2; if (useFull) { if (this._orderDirty) this.buildOrder(); transformf.composeLocalMany(this.localPtr, this.posPtr, this.rotPtr, this.sclPtr, count); transformf.updateWorldOrdered(this.worldPtr, this.localPtr, this.parentPtr, this.orderPtr, count); this._dirty = false; this._dirtyAll = false; this._orderDirty = false; this.clearDirtyList(); return; } if (this._dirtyList.length === 0) { this._dirty = false; return; } this.ensureViews(); this.ensureDirtyIndexCapacity(this._dirtyList.length); const dirty = this.u32().subarray(this.dirtyIndicesPtr >>> 2, (this.dirtyIndicesPtr >>> 2) + this._dirtyList.length); for (let i = 0; i < this._dirtyList.length; i++) dirty[i] = this._dirtyList[i] >>> 0; transformf.updatePartialOrdered(this.worldPtr, this.localPtr, this.posPtr, this.rotPtr, this.sclPtr, this.parentPtr, this.orderPtr, this.dirtyIndicesPtr, this._dirtyList.length, count); this._dirty = false; this.clearDirtyList(); } buildOrder() { const count = this.count; if (this._visited.length < count) this._visited = new Uint8Array(count); this._visited.fill(0, 0, count); const u32 = this.u32(); const parentBase = this.parentPtr >>> 2; const orderBase = this.orderPtr >>> 2; let out = 0; const stack = this._stack; stack.length = 0; for (let i = 0; i < count; i++) { if (this._visited[i]) continue; if (u32[parentBase + i] !== NO_PARENT) continue; stack.push(i); while (stack.length) { const idx = stack.pop(); if (this._visited[idx]) continue; this._visited[idx] = 1; u32[orderBase + out++] = idx >>> 0; const node = this._nodes[idx]; const children = node?.children ?? []; for (let c = children.length - 1; c >= 0; c--) stack.push(children[c].index); } } for (let i = 0; i < count; i++) { if (this._visited[i]) continue; this._visited[i] = 1; u32[orderBase + out++] = i >>> 0; } this._orderDirty = false; } growTo(minCap) { let newCap = this.cap; while (newCap < minCap) newCap *= 2; const oldCap = this.cap; const oldCount = this.count; const oldPosPtr = this.posPtr; const oldRotPtr = this.rotPtr; const oldSclPtr = this.sclPtr; const oldLocalPtr = this.localPtr; const oldWorldPtr = this.worldPtr; const oldParentPtr = this.parentPtr; const oldOrderPtr = this.orderPtr; this.allocateArrays(newCap); this.cap = newCap; this.ensureViews(); const f32 = this.f32(); const u32 = this.u32(); f32.set(f32.subarray(oldPosPtr >>> 2, (oldPosPtr >>> 2) + oldCount * 3), this.posPtr >>> 2); f32.set(f32.subarray(oldRotPtr >>> 2, (oldRotPtr >>> 2) + oldCount * 4), this.rotPtr >>> 2); f32.set(f32.subarray(oldSclPtr >>> 2, (oldSclPtr >>> 2) + oldCount * 3), this.sclPtr >>> 2); f32.set(f32.subarray(oldLocalPtr >>> 2, (oldLocalPtr >>> 2) + oldCount * 16), this.localPtr >>> 2); f32.set(f32.subarray(oldWorldPtr >>> 2, (oldWorldPtr >>> 2) + oldCount * 16), this.worldPtr >>> 2); u32.set(u32.subarray(oldParentPtr >>> 2, (oldParentPtr >>> 2) + oldCount), this.parentPtr >>> 2); u32.set(u32.subarray(oldOrderPtr >>> 2, (oldOrderPtr >>> 2) + oldCount), this.orderPtr >>> 2); wasm.freeF32(oldPosPtr, oldCap * 3); wasm.freeF32(oldRotPtr, oldCap * 4); wasm.freeF32(oldSclPtr, oldCap * 3); wasm.freeF32(oldLocalPtr, oldCap * 16); wasm.freeF32(oldWorldPtr, oldCap * 16); wasm.freeU32(oldParentPtr, oldCap); wasm.freeU32(oldOrderPtr, oldCap); this._orderDirty = true; this._dirty = true; this._dirtyAll = true; } }; var Transform = class _Transform { index; _parent = null; _children = []; _position = [0, 0, 0]; _rotation = [0, 0, 0, 1]; _scale = [1, 1, 1]; _localMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; _worldMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; _disposed = false; static _chainScratch = []; constructor() { const store = TransformStore.global(); this.index = store.alloc(this); } static updateAll() { TransformStore.global().updateIfNeeded(); } assertAlive() { if (this._disposed) throw new Error("Transform is disposed (use-after-dispose)."); } get disposed() { return this._disposed; } get parent() { return this._parent; } get children() { return this._children; } get root() { let t = this; while (t._parent) t = t._parent; return t; } traverse(callback) { callback(this); for (const child of this._children) child.traverse(callback); } readVec3FromStore(ptrBaseF32, out) { const store = TransformStore.global(); const f32 = store.f32(); out[0] = f32[ptrBaseF32 + 0]; out[1] = f32[ptrBaseF32 + 1]; out[2] = f32[ptrBaseF32 + 2]; } readQuatFromStore(ptrBaseF32, out) { const store = TransformStore.global(); const f32 = store.f32(); out[0] = f32[ptrBaseF32 + 0]; out[1] = f32[ptrBaseF32 + 1]; out[2] = f32[ptrBaseF32 + 2]; out[3] = f32[ptrBaseF32 + 3]; } readMat4FromStore(ptrBaseF32, out) { const store = TransformStore.global(); const f32 = store.f32(); for (let i = 0; i < 16; i++) out[i] = f32[ptrBaseF32 + i]; } get positionPtr() { this.assertAlive(); const T = TransformStore.global(); return T.posPtr + this.index * 3 * 4 >>> 0; } get rotationPtr() { this.assertAlive(); const T = TransformStore.global(); return T.rotPtr + this.index * 4 * 4 >>> 0; } get scalePtr() { this.assertAlive(); const T = TransformStore.global(); return T.sclPtr + this.index * 3 * 4 >>> 0; } get localMatrixPtr() { this.assertAlive(); const T = TransformStore.global(); return T.localPtr + this.index * 16 * 4 >>> 0; } get worldMatrixPtr() { this.assertAlive(); const T = TransformStore.global(); return T.worldPtr + this.index * 16 * 4 >>> 0; } get position() { return this._position; } setPosition(x, y, z) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const base = (T.posPtr >>> 2) + this.index * 3; f32[base + 0] = x; f32[base + 1] = y; f32[base + 2] = z; this._position[0] = x; this._position[1] = y; this._position[2] = z; T.markIndexDirty(this.index); return this; } translate(x, y, z) { this.assertAlive(); return this.setPosition(this._position[0] + x, this._position[1] + y, this._position[2] + z); } get rotation() { return this._rotation; } setRotation(x, y, z, w) { this.assertAlive(); const T = TransformStore.global(); const rotPtr = this.rotationPtr; quatf.init(rotPtr, x, y, z, w); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } setRotationFromAxisAngle(axis, angle) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const a = T.tmpAxisPtr >>> 2; f32[a + 0] = axis[0]; f32[a + 1] = axis[1]; f32[a + 2] = axis[2]; vec3f.normalize(T.tmpAxisPtr, T.tmpAxisPtr); const rotPtr = this.rotationPtr; quatf.fromAxisAngle(rotPtr, T.tmpAxisPtr, angle); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } setRotationFromEuler(x, y, z) { this.assertAlive(); const hx = x * 0.5; const hy = y * 0.5; const hz = z * 0.5; const sx = Math.sin(hx); const cx = Math.cos(hx); const sy = Math.sin(hy); const cy = Math.cos(hy); const sz = Math.sin(hz); const cz = Math.cos(hz); const qx = sx * cy * cz + cx * sy * sz; const qy = cx * sy * cz - sx * cy * sz; const qz = cx * cy * sz + sx * sy * cz; const qw = cx * cy * cz - sx * sy * sz; return this.setRotation(qx, qy, qz, qw); } rotateOnAxis(axis, angle) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const a = T.tmpAxisPtr >>> 2; f32[a + 0] = axis[0]; f32[a + 1] = axis[1]; f32[a + 2] = axis[2]; vec3f.normalize(T.tmpAxisPtr, T.tmpAxisPtr); quatf.fromAxisAngle(T.tmpQuatPtr, T.tmpAxisPtr, angle); const rotPtr = this.rotationPtr; quatf.mul(rotPtr, rotPtr, T.tmpQuatPtr); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } rotateX(angle) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const a = T.tmpAxisPtr >>> 2; f32[a + 0] = 1; f32[a + 1] = 0; f32[a + 2] = 0; vec3f.normalize(T.tmpAxisPtr, T.tmpAxisPtr); quatf.fromAxisAngle(T.tmpQuatPtr, T.tmpAxisPtr, angle); const rotPtr = this.rotationPtr; quatf.mul(rotPtr, rotPtr, T.tmpQuatPtr); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } rotateY(angle) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const a = T.tmpAxisPtr >>> 2; f32[a + 0] = 0; f32[a + 1] = 1; f32[a + 2] = 0; vec3f.normalize(T.tmpAxisPtr, T.tmpAxisPtr); quatf.fromAxisAngle(T.tmpQuatPtr, T.tmpAxisPtr, angle); const rotPtr = this.rotationPtr; quatf.mul(rotPtr, rotPtr, T.tmpQuatPtr); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } rotateZ(angle) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const a = T.tmpAxisPtr >>> 2; f32[a + 0] = 0; f32[a + 1] = 0; f32[a + 2] = 1; vec3f.normalize(T.tmpAxisPtr, T.tmpAxisPtr); quatf.fromAxisAngle(T.tmpQuatPtr, T.tmpAxisPtr, angle); const rotPtr = this.rotationPtr; quatf.mul(rotPtr, rotPtr, T.tmpQuatPtr); quatf.normalize(rotPtr, rotPtr); const base = (T.rotPtr >>> 2) + this.index * 4; this.readQuatFromStore(base, this._rotation); T.markIndexDirty(this.index); return this; } get scale() { return this._scale; } setScale(x, y, z) { this.assertAlive(); const T = TransformStore.global(); const f32 = T.f32(); const base = (T.sclPtr >>> 2) + this.index * 3; f32[base + 0] = x; f32[base + 1] = y; f32[base + 2] = z; this._scale[0] = x; this._scale[1] = y; this._scale[2] = z; T.markIndexDirty(this.index); return this; } setUniformScale(scalar) { this.assertAlive(); return this.setScale(scalar, scalar, scalar); } get localMatrix() { this.assertAlive(); const T = TransformStore.global(); T.updateIfNeeded(); const base = (T.localPtr >>> 2) + this.index * 16; this.readMat4FromStore(base, this._localMatrix); return this._localMatrix; } get worldMatrix() { this.assertAlive(); const T = TransformStore.global(); T.updateIfNeeded(); const base = (T.worldPtr >>> 2) + this.index * 16; this.readMat4FromStore(base, this._worldMatrix); return this._worldMatrix; } getWorldPosition(out) { this.assertAlive(); const T = TransformStore.global(); T.updateIfNeeded(); const base = (T.worldPtr >>> 2) + this.index * 16; const f32 = T.f32(); if (out) { out[0] = f32[base + 12]; out[1] = f32[base + 13]; out[2] = f32[base + 14]; return out; } return [f32[base + 12], f32[base + 13], f32[base + 14]]; } get worldPosition() { return this.getWorldPosition(); } getWorldRotation(out) { this.assertAlive(); TransformStore.global().updateIfNeeded(); _Transform._chainScratch.length = 0; let curr = this; while (curr) { _Transform._chainScratch.push(curr); curr = curr._parent; } let x = 0, y = 0, z = 0, w = 1; for (let i = _Transform._chainScratch.length - 1; i >= 0; i--) { const t = _Transform._chainScratch[i]; const r = t._rotation; let rx = r[0] ?? 0; let ry = r[1] ?? 0; let rz = r[2] ?? 0; let rw = r[3] ?? 1; let rlen2 = rx * rx + ry * ry + rz * rz + rw * rw; if (!Number.isFinite(rlen2) || rlen2 <= 1e-12) { rx = 0; ry = 0; rz = 0; rw = 1; } else { const inv = 1 / Math.sqrt(rlen2); rx *= inv; ry *= inv; rz *= inv; rw *= inv; } const nx = w * rx + x * rw + y * rz - z * ry; const ny = w * ry - x * rz + y * rw + z * rx; const nz = w * rz + x * ry - y * rx + z * rw; const nw = w * rw - x * rx - y * ry - z * rz; x = nx; y = ny; z = nz; w = nw; } _Transform._chainScratch.length = 0; let len2 = x * x + y * y + z * z + w * w; if (!Number.isFinite(len2) || len2 <= 1e-12) { x = 0; y = 0; z = 0; w = 1; } else { const inv = 1 / Math.sqrt(len2); x *= inv; y *= inv; z *= inv; w *= inv; } const res = out ?? [0, 0, 0, 1]; res[0] = x; res[1] = y; res[2] = z; res[3] = w; return res; } get worldRotation() { return this.getWorldRotation(); } setParent(parent) { this.assertAlive(); if (parent === this._parent) return this; if (parent === this) throw new Error("Transform cannot be parented to itself."); for (let p = parent; p; p = p._parent) if (p === this) throw new Error("Transform parenting would create a cycle."); this.removeFromParent(); this._parent = parent; if (parent) { parent._children.push(this); TransformStore.global().setParent(this.index, parent.index); } else { TransformStore.global().setParent(this.index, null); } return this; } addChild(child) { this.assertAlive(); child.setParent(this); return this; } removeChild(child) { this.assertAlive(); if (child._parent !== this) return this; child.setParent(null); return this; } removeFromParent() { this.assertAlive(); if (!this._parent) return this; const p = this._parent; const i = p._children.indexOf(this); if (i >= 0) p._children.splice(i, 1); this._parent = null; TransformStore.global().setParent(this.index, null); return this; } reset() { this.assertAlive(); this._parent = null; this._children.length = 0; TransformStore.global().setParent(this.index, null); this.setPosition(0, 0, 0); this.setRotation(0, 0, 0, 1); this.setScale(1, 1, 1); return this; } copyFrom(other) { this.assertAlive(); this.setPosition(other._position[0], other._position[1], other._position[2]); this.setRotation(other._rotation[0], other._rotation[1], other._rotation[2], other._rotation[3]); this.setScale(other._scale[0], other._scale[1], other._scale[2]); return this; } clone() { this.assertAlive(); const T = new _Transform(); T.copyFrom(this); return T; } dispose() { if (this._disposed) return; const children = this._children.slice(); for (const child of children) child.setParent(null); this._children.length = 0; this.removeFromParent(); TransformStore.global().free(this.index); this._disposed = true; } }; // typescript/graphics/geometry.ts var allocGeometryScratchF32 = (allocations, len, label) => { const length = len >>> 0; const ptr = wasm.allocF32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly f32 allocation failed (${length} elements).`); if (ptr) allocations.push({ ptr, len: length }); return ptr; }; var allocGeometryScratchU32 = (allocations, len, label) => { const length = len >>> 0; const ptr = wasm.allocU32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly u32 allocation failed (${length} elements).`); if (ptr) allocations.push({ ptr, len: length }); return ptr; }; var computeGeometryBounds = (positions) => { const vertexCount = Math.floor(positions.length / 3); if (vertexCount <= 0) return { boxMin: [0, 0, 0], boxMax: [0, 0, 0], sphereCenter: [0, 0, 0], sphereRadius: 0 }; const allocations = []; try { const positionsPtr = allocGeometryScratchF32(allocations, positions.length, "computeGeometryBounds.positions"); const boxMinPtr = allocGeometryScratchF32(allocations, 3, "computeGeometryBounds.boxMin"); const boxMaxPtr = allocGeometryScratchF32(allocations, 3, "computeGeometryBounds.boxMax"); const sphereCenterPtr = allocGeometryScratchF32(allocations, 3, "computeGeometryBounds.sphereCenter"); const sphereRadiusPtr = allocGeometryScratchF32(allocations, 1, "computeGeometryBounds.sphereRadius"); wasm.f32view(positionsPtr, positions.length).set(positions); boundsf.geometryPositions(boxMinPtr, boxMaxPtr, sphereCenterPtr, sphereRadiusPtr, positionsPtr, vertexCount); const boxMin = wasm.f32view(boxMinPtr, 3); const boxMax = wasm.f32view(boxMaxPtr, 3); const sphereCenter = wasm.f32view(sphereCenterPtr, 3); const sphereRadius = wasm.f32view(sphereRadiusPtr, 1); return { boxMin: [boxMin[0], boxMin[1], boxMin[2]], boxMax: [boxMax[0], boxMax[1], boxMax[2]], sphereCenter: [sphereCenter[0], sphereCenter[1], sphereCenter[2]], sphereRadius: sphereRadius[0] }; } finally { for (let i = allocations.length - 1; i >= 0; i--) wasm.freeF32(allocations[i].ptr, allocations[i].len); } }; var computeGeometryVertexNormals = (positions, indices) => { const vertexCount = Math.floor(positions.length / 3); const idxLen = indices ? indices.length : 0; const f32Allocations = []; const u32Allocations = []; try { const positionsPtr = allocGeometryScratchF32(f32Allocations, positions.length, "computeGeometryVertexNormals.positions"); const outputPtr = allocGeometryScratchF32(f32Allocations, positions.length, "computeGeometryVertexNormals.output"); const indicesPtr = idxLen > 0 ? allocGeometryScratchU32(u32Allocations, idxLen, "computeGeometryVertexNormals.indices") : 0; wasm.f32view(positionsPtr, positions.length).set(positions); if (indices && idxLen > 0) wasm.u32view(indicesPtr, idxLen).set(indices); meshf.computeVertexNormals(outputPtr, positionsPtr, vertexCount, indicesPtr, idxLen); const out = new Float32Array(positions.length); out.set(wasm.f32view(outputPtr, positions.length)); return out; } finally { for (let i = u32Allocations.length - 1; i >= 0; i--) wasm.freeU32(u32Allocations[i].ptr, u32Allocations[i].len); for (let i = f32Allocations.length - 1; i >= 0; i--) wasm.freeF32(f32Allocations[i].ptr, f32Allocations[i].len); } }; var normalizeVec3At = (data, offset, fallback) => { let x = data[offset + 0] ?? fallback[0], y = data[offset + 1] ?? fallback[1], z = data[offset + 2] ?? fallback[2]; const len = Math.hypot(x, y, z); if (len <= 1e-12) return fallback; x /= len; y /= len; z /= len; return [x, y, z]; }; var fallbackTangentForNormal = (nx, ny, nz) => { const ax = Math.abs(nx) < 0.9 ? 1 : 0, ay = ax === 1 ? 0 : 1, az = 0; let tx = ay * nz - az * ny, ty = az * nx - ax * nz, tz = ax * ny - ay * nx; const len = Math.hypot(tx, ty, tz); if (len <= 1e-12) return [1, 0, 0]; tx /= len; ty /= len; tz /= len; return [tx, ty, tz]; }; var computeGeometryTangents = (positions, normals, uvs, indices) => { const vertexCount = positions.length / 3 | 0; const out = new Float32Array(vertexCount * 4), tan1 = new Float32Array(vertexCount * 3), tan2 = new Float32Array(vertexCount * 3); const indexCount = indices ? indices.length : vertexCount; const indexAt = (i) => indices ? indices[i] : i; for (let i = 0; i + 2 < indexCount; i += 3) { const i0 = indexAt(i + 0), i1 = indexAt(i + 1), i2 = indexAt(i + 2); const p0 = i0 * 3, p1 = i1 * 3, p2 = i2 * 3; const uv0 = i0 * 2, uv1 = i1 * 2, uv2 = i2 * 2; const x1 = positions[p1 + 0] - positions[p0 + 0], y1 = positions[p1 + 1] - positions[p0 + 1], z1 = positions[p1 + 2] - positions[p0 + 2], x2 = positions[p2 + 0] - positions[p0 + 0], y2 = positions[p2 + 1] - positions[p0 + 1], z2 = positions[p2 + 2] - positions[p0 + 2]; const s1 = uvs[uv1 + 0] - uvs[uv0 + 0], t1 = uvs[uv1 + 1] - uvs[uv0 + 1], s2 = uvs[uv2 + 0] - uvs[uv0 + 0], t2 = uvs[uv2 + 1] - uvs[uv0 + 1]; const denom = s1 * t2 - s2 * t1; if (Math.abs(denom) <= 1e-12) continue; const r = 1 / denom; const sx = (t2 * x1 - t1 * x2) * r, sy = (t2 * y1 - t1 * y2) * r, sz = (t2 * z1 - t1 * z2) * r, tx = (s1 * x2 - s2 * x1) * r, ty = (s1 * y2 - s2 * y1) * r, tz = (s1 * z2 - s2 * z1) * r; const o0 = i0 * 3; tan1[o0 + 0] += sx; tan1[o0 + 1] += sy; tan1[o0 + 2] += sz; tan2[o0 + 0] += tx; tan2[o0 + 1] += ty; tan2[o0 + 2] += tz; const o1 = i1 * 3; tan1[o1 + 0] += sx; tan1[o1 + 1] += sy; tan1[o1 + 2] += sz; tan2[o1 + 0] += tx; tan2[o1 + 1] += ty; tan2[o1 + 2] += tz; const o2 = i2 * 3; tan1[o2 + 0] += sx; tan1[o2 + 1] += sy; tan1[o2 + 2] += sz; tan2[o2 + 0] += tx; tan2[o2 + 1] += ty; tan2[o2 + 2] += tz; } for (let i = 0; i < vertexCount; i++) { const nOff = i * 3, tOff = i * 3, o = i * 4; const [nx, ny, nz] = normalizeVec3At(normals, nOff, [0, 1, 0]); let tx = tan1[tOff + 0], ty = tan1[tOff + 1], tz = tan1[tOff + 2]; const ndott = nx * tx + ny * ty + nz * tz; tx -= nx * ndott; ty -= ny * ndott; tz -= nz * ndott; const tLen = Math.hypot(tx, ty, tz); if (tLen <= 1e-12) [tx, ty, tz] = fallbackTangentForNormal(nx, ny, nz); else { tx /= tLen; ty /= tLen; tz /= tLen; } const bx = ny * tz - nz * ty, by = nz * tx - nx * tz, bz = nx * ty - ny * tx; const cx = tan2[tOff + 0], cy = tan2[tOff + 1], cz = tan2[tOff + 2]; const handedness = bx * cx + by * cy + bz * cz < 0 ? -1 : 1; out[o + 0] = tx; out[o + 1] = ty; out[o + 2] = tz; out[o + 3] = handedness; } return out; }; var createDerivativeFallbackTangents = (vertexCount) => { const out = new Float32Array(vertexCount * 4); for (let i = 0; i < vertexCount; i++) out[i * 4 + 3] = 1; return out; }; var packSkinInfluences = (joints, weights, joints1, weights1) => { const vertexCount = joints.length / 4 | 0; const hasSecondSet = joints1 !== null && weights1 !== null; const stride = hasSecondSet ? 48 : 24; const out = new Uint8Array(vertexCount * stride); const view = new DataView(out.buffer); for (let i = 0; i < vertexCount; i++) { const vertexBase = i * stride; const src = i * 4; for (let c = 0; c < 4; c++) view.setUint16(vertexBase + c * 2, joints[src + c] ?? 0, true); for (let c = 0; c < 4; c++) view.setFloat32(vertexBase + 8 + c * 4, weights[src + c] ?? 0, true); if (hasSecondSet) { for (let c = 0; c < 4; c++) view.setUint16(vertexBase + 24 + c * 2, joints1[src + c] ?? 0, true); for (let c = 0; c < 4; c++) view.setFloat32(vertexBase + 32 + c * 4, weights1[src + c] ?? 0, true); } } return out; }; var GEOMETRY_WASM_VERTEX_CHANNELS = ["positions", "normals", "tangents", "colors", "uvs", "uvs1", "joints", "weights", "joints1", "weights1"]; var makeGeometryWasmState = () => ({ source: null, dirty: false, managed: false, capacity: 0, capacityHint: 0 }); var geometryWasmFieldName = (channel) => { switch (channel) { case "positions": return "wasmPositions"; case "normals": return "wasmNormals"; case "tangents": return "wasmTangents"; case "colors": return "wasmColors"; case "uvs": return "wasmUvs"; case "uvs1": return "wasmUvs1"; case "joints": return "wasmJoints"; case "weights": return "wasmWeights"; case "joints1": return "wasmJoints1"; case "weights1": return "wasmWeights1"; case "indices": return "wasmIndices"; } }; var geometryWasmComponents = (channel) => { switch (channel) { case "positions": case "normals": return 3; case "tangents": case "colors": case "joints": case "weights": case "joints1": case "weights1": return 4; case "uvs": case "uvs1": return 2; case "indices": return 1; } }; var geometryWasmBytesPerElement = (channel) => channel === "joints" || channel === "joints1" ? 2 : 4; var isGeometryWasmVertexChannel = (channel) => channel !== "indices"; var hasGeometryWasmInputs = (desc) => !!(desc.wasmPositions || desc.wasmNormals || desc.wasmTangents || desc.wasmColors || desc.wasmUvs || desc.wasmUvs1 || desc.wasmJoints || desc.wasmWeights || desc.wasmJoints1 || desc.wasmWeights1 || desc.wasmIndices); var assertNoDuplicateGeometrySource = (cpuSource, wasmSource, cpuLabel, wasmLabel) => { assert(!(cpuSource && wasmSource), `Geometry: ${cpuLabel} and ${wasmLabel} cannot both be provided for the same attribute.`); }; var createFallbackNormals = (vertexCount) => { const out = new Float32Array(vertexCount * 3); for (let i = 1; i < out.length; i += 3) out[i] = 1; return out; }; var createFallbackColors = (vertexCount) => { const out = new Float32Array(vertexCount * 4); out.fill(1); return out; }; var Geometry = class _Geometry { positions; normals; tangents; colors; uvs; uvs1; joints = null; weights = null; joints1 = null; weights1 = null; _jointsBuffer = null; _weightsBuffer = null; _joints1Buffer = null; _weights1Buffer = null; _skinInfluenceBuffer = null; indices = null; morphTargets; authoredNormals = false; vertexCount = 0; indexCount = 0; _boundsMin = [0, 0, 0]; _boundsMax = [0, 0, 0]; _boundsCenter = [0, 0, 0]; _boundsRadius = 0; _boundsSource = "none"; _positionBuffer = null; _normalBuffer = null; _tangentBuffer = null; _colorBuffer = null; _uvBuffer = null; _uv1Buffer = null; _indexBuffer = null; _device = null; _refCount = 1; _destroyed = false; _keepCPUData = false; _morphBaseRevision = 0; _skinInfluenceDirty = true; _wasm = { positions: makeGeometryWasmState(), normals: makeGeometryWasmState(), tangents: makeGeometryWasmState(), colors: makeGeometryWasmState(), uvs: makeGeometryWasmState(), uvs1: makeGeometryWasmState(), joints: makeGeometryWasmState(), weights: makeGeometryWasmState(), joints1: makeGeometryWasmState(), weights1: makeGeometryWasmState(), indices: makeGeometryWasmState() }; _cpuProvided = { positions: false, normals: false, tangents: false, colors: false, uvs: false, uvs1: false, joints: false, weights: false, joints1: false, weights1: false, indices: false }; _cpuDirty = { positions: true, normals: true, tangents: true, colors: true, uvs: true, uvs1: true, joints: true, weights: true, joints1: true, weights1: true, indices: true }; constructor(descriptor) { assertNoDuplicateGeometrySource(descriptor.positions, descriptor.wasmPositions, "positions", "wasmPositions"); assertNoDuplicateGeometrySource(descriptor.normals, descriptor.wasmNormals, "normals", "wasmNormals"); assertNoDuplicateGeometrySource(descriptor.tangents, descriptor.wasmTangents, "tangents", "wasmTangents"); assertNoDuplicateGeometrySource(descriptor.colors, descriptor.wasmColors, "colors", "wasmColors"); assertNoDuplicateGeometrySource(descriptor.uvs, descriptor.wasmUvs, "uvs", "wasmUvs"); assertNoDuplicateGeometrySource(descriptor.uvs1, descriptor.wasmUvs1, "uvs1", "wasmUvs1"); assertNoDuplicateGeometrySource(descriptor.joints, descriptor.wasmJoints, "joints", "wasmJoints"); assertNoDuplicateGeometrySource(descriptor.weights, descriptor.wasmWeights, "weights", "wasmWeights"); assertNoDuplicateGeometrySource(descriptor.joints1, descriptor.wasmJoints1, "joints1", "wasmJoints1"); assertNoDuplicateGeometrySource(descriptor.weights1, descriptor.wasmWeights1, "weights1", "wasmWeights1"); assertNoDuplicateGeometrySource(descriptor.indices, descriptor.wasmIndices, "indices", "wasmIndices"); assert(!!descriptor.positions || !!descriptor.wasmPositions, "Geometry: positions or wasmPositions are required."); this._keepCPUData = !!descriptor.keepCPUData; this.morphTargets = descriptor.morphTargets ?? []; assert(!(this.morphTargets.length > 0 && descriptor.wasmPositions), "Geometry: wasmPositions with morphTargets are not supported yet; provide CPU positions for morph-target geometry."); const wasmPositions = descriptor.wasmPositions ? assertWasmF32View(descriptor.wasmPositions, "Geometry: wasmPositions").refresh() : null; if (descriptor.vertexCount !== void 0) this.vertexCount = assertWasmRecordCount(descriptor.vertexCount, "Geometry: vertexCount"); else if (descriptor.positions) this.vertexCount = Math.floor(descriptor.positions.length / 3); else this.vertexCount = resolveWasmRecordCount(wasmPositions, void 0, 3, "Geometry: wasmPositions", "Geometry: vertexCount", "vertexCount"); if (descriptor.positions) { this.positions = descriptor.positions; this._cpuProvided.positions = true; if (this.positions.length !== Math.floor(this.positions.length / 3) * 3) console.warn(`[Geometry] positions length ${this.positions.length} is not divisible by 3; trailing components ignored.`); this.validateCPUVertexChannel("positions", this.vertexCount); } else { validateWasmRecordRange(wasmPositions, this.vertexCount, 3, "Geometry: wasmPositions", "vertexCount"); this.positions = this._keepCPUData ? this.copyWasmActiveRange(wasmPositions, this.vertexCount * 3) : new Float32Array(0); } const expectedNormalLength = this.vertexCount * 3; let authoredNormals = descriptor.authoredNormals ?? (!!descriptor.normals || !!descriptor.wasmNormals); let normals = descriptor.normals ?? (descriptor.wasmNormals ? new Float32Array(0) : createFallbackNormals(this.vertexCount)); if (descriptor.normals && normals.length !== expectedNormalLength) { console.warn(`[Geometry] normals length mismatch (got ${normals.length}, expected ${expectedNormalLength}). Using fallback normals.`); normals = createFallbackNormals(this.vertexCount); authoredNormals = false; } else if (descriptor.normals) this._cpuProvided.normals = true; if (!descriptor.normals && !descriptor.wasmNormals) { normals = createFallbackNormals(this.vertexCount); if (descriptor.authoredNormals === void 0) authoredNormals = false; } this.authoredNormals = authoredNormals; this.normals = normals; const expectedTangentLength = this.vertexCount * 4; let tangents = descriptor.tangents ?? (descriptor.wasmTangents ? new Float32Array(0) : null); if (tangents && tangents.length !== expectedTangentLength) { console.warn(`[Geometry] tangents length mismatch (got ${tangents.length}, expected ${expectedTangentLength}). Using fallback tangents.`); tangents = null; } else if (descriptor.tangents) this._cpuProvided.tangents = true; const expectedUvLength = this.vertexCount * 2; let uvs = descriptor.uvs ?? (descriptor.wasmUvs ? new Float32Array(0) : new Float32Array(expectedUvLength)); if (descriptor.uvs && uvs.length !== expectedUvLength) { console.warn(`[Geometry] uvs length mismatch (got ${uvs.length}, expected ${expectedUvLength}). TEXCOORD_0 disabled.`); uvs = new Float32Array(expectedUvLength); } else if (descriptor.uvs) this._cpuProvided.uvs = true; this.uvs = uvs; this.tangents = tangents ?? (descriptor.wasmTangents ? new Float32Array(0) : createDerivativeFallbackTangents(this.vertexCount)); const expectedColorLength = this.vertexCount * 4; let colors = descriptor.colors ?? (descriptor.wasmColors ? new Float32Array(0) : createFallbackColors(this.vertexCount)); if (descriptor.colors && colors.length !== expectedColorLength) { console.warn(`[Geometry] colors length mismatch (got ${colors.length}, expected ${expectedColorLength}). Using default white colors.`); colors = createFallbackColors(this.vertexCount); } else if (descriptor.colors) this._cpuProvided.colors = true; this.colors = colors; let uvs1 = descriptor.uvs1 ?? (descriptor.wasmUvs1 ? new Float32Array(0) : new Float32Array(expectedUvLength)); if (descriptor.uvs1 && uvs1.length !== expectedUvLength) { console.warn(`[Geometry] uvs1 length mismatch (got ${uvs1.length}, expected ${expectedUvLength}). TEXCOORD_1 disabled.`); uvs1 = new Float32Array(expectedUvLength); } else if (descriptor.uvs1) this._cpuProvided.uvs1 = true; this.uvs1 = uvs1; let joints = descriptor.joints ?? null; let weights = descriptor.weights ?? null; const expected = this.vertexCount * 4; if (joints && !weights && !descriptor.wasmWeights || !joints && weights && !descriptor.wasmJoints) { console.warn(`[Geometry] JOINTS_0/WEIGHTS_0 must be provided together. Skinning disabled for this geometry.`); joints = null; weights = null; } if (joints && joints.length !== expected) { console.warn(`[Geometry] joints length mismatch (got ${joints.length}, expected ${expected}). Skinning disabled.`); joints = null; if (!descriptor.wasmJoints) weights = null; } if (weights && weights.length !== expected) { console.warn(`[Geometry] weights length mismatch (got ${weights.length}, expected ${expected}). Skinning disabled.`); weights = null; if (!descriptor.wasmWeights) joints = null; } this.joints = joints; this.weights = weights; if (joints) this._cpuProvided.joints = true; if (weights) this._cpuProvided.weights = true; let joints1 = descriptor.joints1 ?? null; let weights1 = descriptor.weights1 ?? null; const hasBaseSkin = !!(joints || descriptor.wasmJoints) && !!(weights || descriptor.wasmWeights); if (joints1 && !weights1 && !descriptor.wasmWeights1 || !joints1 && weights1 && !descriptor.wasmJoints1) { console.warn(`[Geometry] JOINTS_1/WEIGHTS_1 must be provided together. Ignoring additional influences.`); joints1 = null; weights1 = null; } if ((joints1 || weights1 || descriptor.wasmJoints1 || descriptor.wasmWeights1) && !hasBaseSkin) { console.warn(`[Geometry] JOINTS_1/WEIGHTS_1 provided without JOINTS_0/WEIGHTS_0. Ignoring additional influences.`); joints1 = null; weights1 = null; } if (joints1 && joints1.length !== expected) { console.warn(`[Geometry] joints1 length mismatch (got ${joints1.length}, expected ${expected}). Ignoring additional influences.`); joints1 = null; if (!descriptor.wasmJoints1) weights1 = null; } if (weights1 && weights1.length !== expected) { console.warn(`[Geometry] weights1 length mismatch (got ${weights1.length}, expected ${expected}). Ignoring additional influences.`); weights1 = null; if (!descriptor.wasmWeights1) joints1 = null; } this.joints1 = joints1; this.weights1 = weights1; if (joints1) this._cpuProvided.joints1 = true; if (weights1) this._cpuProvided.weights1 = true; this.indices = descriptor.indices ?? null; if (this.indices) this._cpuProvided.indices = true; if (descriptor.indexCount !== void 0) { this.indexCount = assertWasmRecordCount(descriptor.indexCount, "Geometry: indexCount"); if (this.indices) assert(this.indices.length >= this.indexCount, `Geometry: indices length must be at least indexCount.`); } else if (this.indices) this.indexCount = this.indices.length; else if (descriptor.wasmIndices) this.indexCount = resolveWasmRecordCount(assertWasmU32View(descriptor.wasmIndices, "Geometry: wasmIndices").refresh(), void 0, 1, "Geometry: wasmIndices", "Geometry: indexCount", "indexCount"); else this.indexCount = this.vertexCount; if (descriptor.bounds) this.setBounds(descriptor.bounds, "explicit"); else if (descriptor.positions) this.setBounds(computeGeometryBounds(this.positions), "computed"); else this.setBounds(computeGeometryBounds(wasmPositions.array().subarray(0, this.vertexCount * 3)), "computed"); if (hasGeometryWasmInputs(descriptor)) { this.setWasmAttributes({ positions: descriptor.wasmPositions ?? null, normals: descriptor.wasmNormals ?? null, tangents: descriptor.wasmTangents ?? null, colors: descriptor.wasmColors ?? null, uvs: descriptor.wasmUvs ?? null, uvs1: descriptor.wasmUvs1 ?? null, joints: descriptor.wasmJoints ?? null, weights: descriptor.wasmWeights ?? null, joints1: descriptor.wasmJoints1 ?? null, weights1: descriptor.wasmWeights1 ?? null, indices: descriptor.wasmIndices ?? null }, { vertexCount: this.vertexCount, indexCount: this.indexCount, keepCPUData: this._keepCPUData, recomputeBounds: this._boundsSource !== "explicit" && !!descriptor.wasmPositions, vertexCapacity: assertWasmCapacity(descriptor.wasmVertexCapacity, "Geometry: wasmVertexCapacity"), indexCapacity: assertWasmCapacity(descriptor.wasmIndexCapacity, "Geometry: wasmIndexCapacity") }); } } assertAlive(action) { if (this._destroyed) throw new Error(`Geometry: cannot ${action}; resource has already been released.`); } setBounds(bounds, source) { this._boundsMin = [bounds.boxMin[0], bounds.boxMin[1], bounds.boxMin[2]]; this._boundsMax = [bounds.boxMax[0], bounds.boxMax[1], bounds.boxMax[2]]; this._boundsCenter = [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]]; this._boundsRadius = Math.max(0, bounds.sphereRadius); this._boundsSource = source; } wasmState(channel) { return this._wasm[channel]; } assertWasmSource(channel, source) { const label = `Geometry: ${geometryWasmFieldName(channel)}`; if (channel === "indices") return assertWasmU32View(source, label); if (channel === "joints" || channel === "joints1") return assertWasmU16View(source, label); return assertWasmF32View(source, label); } copyWasmActiveRange(source, elementCount) { const data = source.array().subarray(0, elementCount); if (data instanceof Uint16Array) return new Uint16Array(data); if (data instanceof Uint32Array) return new Uint32Array(data); return new Float32Array(data); } getCPUChannelData(channel) { switch (channel) { case "positions": return this.positions; case "normals": return this.normals; case "tangents": return this.tangents; case "colors": return this.colors; case "uvs": return this.uvs; case "uvs1": return this.uvs1; case "joints": return this.joints; case "weights": return this.weights; case "joints1": return this.joints1; case "weights1": return this.weights1; case "indices": return this.indices; } } setCPUChannelData(channel, data) { switch (channel) { case "positions": this.positions = data ?? new Float32Array(0); break; case "normals": this.normals = data ?? createFallbackNormals(this.vertexCount); break; case "tangents": this.tangents = data ?? createDerivativeFallbackTangents(this.vertexCount); break; case "colors": this.colors = data ?? createFallbackColors(this.vertexCount); break; case "uvs": this.uvs = data ?? new Float32Array(this.vertexCount * 2); break; case "uvs1": this.uvs1 = data ?? new Float32Array(this.vertexCount * 2); break; case "joints": this.joints = data; break; case "weights": this.weights = data; break; case "joints1": this.joints1 = data; break; case "weights1": this.weights1 = data; break; case "indices": this.indices = data; break; } if (data) this._cpuProvided[channel] = true; this._cpuDirty[channel] = true; if (channel === "joints" || channel === "weights" || channel === "joints1" || channel === "weights1") this._skinInfluenceDirty = true; } dropCPUChannelDataForWasm(channel) { switch (channel) { case "positions": this.positions = new Float32Array(0); break; case "normals": this.normals = new Float32Array(0); this.authoredNormals = false; break; case "tangents": this.tangents = new Float32Array(0); break; case "colors": this.colors = new Float32Array(0); break; case "uvs": this.uvs = new Float32Array(0); break; case "uvs1": this.uvs1 = new Float32Array(0); break; case "joints": this.joints = null; break; case "weights": this.weights = null; break; case "joints1": this.joints1 = null; break; case "weights1": this.weights1 = null; break; case "indices": this.indices = null; break; } this._cpuProvided[channel] = false; } getChannelBuffer(channel) { switch (channel) { case "positions": return this._positionBuffer; case "normals": return this._normalBuffer; case "tangents": return this._tangentBuffer; case "colors": return this._colorBuffer; case "uvs": return this._uvBuffer; case "uvs1": return this._uv1Buffer; case "joints": return this._jointsBuffer; case "weights": return this._weightsBuffer; case "joints1": return this._joints1Buffer; case "weights1": return this._weights1Buffer; case "indices": return this._indexBuffer; } } setChannelBuffer(channel, buffer) { switch (channel) { case "positions": this._positionBuffer = buffer; break; case "normals": this._normalBuffer = buffer; break; case "tangents": this._tangentBuffer = buffer; break; case "colors": this._colorBuffer = buffer; break; case "uvs": this._uvBuffer = buffer; break; case "uvs1": this._uv1Buffer = buffer; break; case "joints": this._jointsBuffer = buffer; break; case "weights": this._weightsBuffer = buffer; break; case "joints1": this._joints1Buffer = buffer; break; case "weights1": this._weights1Buffer = buffer; break; case "indices": this._indexBuffer = buffer; break; } } replaceChannelBuffer(channel, buffer) { const current = this.getChannelBuffer(channel); if (current && current !== buffer) current.destroy(); this.setChannelBuffer(channel, buffer); } validateCPUVertexChannel(channel, vertexCount) { if (this.wasmState(channel).source) return; const data = this.getCPUChannelData(channel); if (!data) return; if (!this._cpuProvided[channel] && channel !== "positions") return; const expected = vertexCount * geometryWasmComponents(channel); assert(data.length >= expected, `Geometry: ${channel} length must match vertexCount when wasm vertex count changes.`); } validateNonWasmVertexChannels(vertexCount) { for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) this.validateCPUVertexChannel(channel, vertexCount); } resizeFallbackVertexChannels(vertexCount) { if (!this._wasm.normals.source && !this._cpuProvided.normals) { this.normals = createFallbackNormals(vertexCount); this.authoredNormals = false; this._cpuDirty.normals = true; } if (!this._wasm.tangents.source && !this._cpuProvided.tangents) { this.tangents = createDerivativeFallbackTangents(vertexCount); this._cpuDirty.tangents = true; } if (!this._wasm.colors.source && !this._cpuProvided.colors) { this.colors = createFallbackColors(vertexCount); this._cpuDirty.colors = true; } if (!this._wasm.uvs.source && !this._cpuProvided.uvs) { this.uvs = new Float32Array(vertexCount * 2); this._cpuDirty.uvs = true; } if (!this._wasm.uvs1.source && !this._cpuProvided.uvs1) { this.uvs1 = new Float32Array(vertexCount * 2); this._cpuDirty.uvs1 = true; } } restoreCPUFallbackAfterWasmClear(channel) { const expected = this.vertexCount * geometryWasmComponents(channel); const current = this.getCPUChannelData(channel); if (channel === "positions") return; if (channel === "indices") { if (this._cpuProvided.indices && this.indices && this.indices.length >= this.indexCount) return; this.indices = null; this.indexCount = this.vertexCount; this._cpuProvided.indices = false; return; } if (channel === "normals") { if (this._cpuProvided.normals && current && current.length >= expected) return; this.normals = createFallbackNormals(this.vertexCount); this.authoredNormals = false; this._cpuProvided.normals = false; return; } if (channel === "tangents") { if (this._cpuProvided.tangents && current && current.length >= expected) return; this.tangents = createDerivativeFallbackTangents(this.vertexCount); this._cpuProvided.tangents = false; return; } if (channel === "colors") { if (this._cpuProvided.colors && current && current.length >= expected) return; this.colors = createFallbackColors(this.vertexCount); this._cpuProvided.colors = false; return; } if (channel === "uvs") { if (this._cpuProvided.uvs && current && current.length >= expected) return; this.uvs = new Float32Array(this.vertexCount * 2); this._cpuProvided.uvs = false; return; } if (channel === "uvs1") { if (this._cpuProvided.uvs1 && current && current.length >= expected) return; this.uvs1 = new Float32Array(this.vertexCount * 2); this._cpuProvided.uvs1 = false; } } clearSkinChannelBuffer(channel) { this.replaceChannelBuffer(channel, null); this._cpuDirty[channel] = false; this.wasmState(channel).dirty = false; } normalizeSkinPairsAfterWasmClear() { const hasBaseSkin = !!(this.joints || this._wasm.joints.source) && !!(this.weights || this._wasm.weights.source); if (!hasBaseSkin) { this.joints = null; this.weights = null; this.joints1 = null; this.weights1 = null; this._cpuProvided.joints = false; this._cpuProvided.weights = false; this._cpuProvided.joints1 = false; this._cpuProvided.weights1 = false; this.clearSkinChannelBuffer("joints"); this.clearSkinChannelBuffer("weights"); this.clearSkinChannelBuffer("joints1"); this.clearSkinChannelBuffer("weights1"); this._skinInfluenceDirty = true; return; } const hasExtraSkin = !!(this.joints1 || this._wasm.joints1.source) && !!(this.weights1 || this._wasm.weights1.source); if (hasExtraSkin) return; this.joints1 = null; this.weights1 = null; this._cpuProvided.joints1 = false; this._cpuProvided.weights1 = false; this.clearSkinChannelBuffer("joints1"); this.clearSkinChannelBuffer("weights1"); this._skinInfluenceDirty = true; } validateWasmSourceBeforeSet(channel, source, explicitCount) { const countTerm = isGeometryWasmVertexChannel(channel) ? "vertexCount" : "indexCount"; const countLabel = isGeometryWasmVertexChannel(channel) ? "Geometry: vertexCount" : "Geometry: indexCount"; if (explicitCount !== void 0) { const count = assertWasmRecordCount(explicitCount, countLabel); validateWasmRecordRange(source, count, geometryWasmComponents(channel), `Geometry: ${geometryWasmFieldName(channel)}`, countTerm); return; } if (channel === "positions") { resolveWasmRecordCount(source, void 0, 3, "Geometry: wasmPositions", "Geometry: vertexCount", "vertexCount"); return; } if (channel === "indices") { resolveWasmRecordCount(source, void 0, 1, "Geometry: wasmIndices", "Geometry: indexCount", "indexCount"); return; } if (this.vertexCount > 0) validateWasmRecordRange(source, this.vertexCount, geometryWasmComponents(channel), `Geometry: ${geometryWasmFieldName(channel)}`, "vertexCount"); } setVertexCountFromWasm(vertexCount) { const count = assertWasmRecordCount(vertexCount, "Geometry: vertexCount"); this.validateNonWasmVertexChannels(count); if (count !== this.vertexCount) { this.vertexCount = count; this.resizeFallbackVertexChannels(count); if (!this.indices && !this._wasm.indices.source) this.indexCount = count; for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) if (this.wasmState(channel).source) this.wasmState(channel).dirty = true; this._skinInfluenceDirty = true; } } setIndexCountFromWasm(indexCount) { this.indexCount = assertWasmRecordCount(indexCount, "Geometry: indexCount"); } hasWasmVertexSources() { return GEOMETRY_WASM_VERTEX_CHANNELS.some((channel) => !!this.wasmState(channel).source); } hasDirtyWasmSources() { if (this._wasm.indices.source && this._wasm.indices.dirty) return true; return GEOMETRY_WASM_VERTEX_CHANNELS.some((channel) => !!this.wasmState(channel).source && this.wasmState(channel).dirty); } hasDirtyCPUSources() { if (this._skinInfluenceDirty) return true; if (this._cpuDirty.indices && !this._wasm.indices.source) return true; return GEOMETRY_WASM_VERTEX_CHANNELS.some((channel) => this._cpuDirty[channel] && !this.wasmState(channel).source); } markAllCPUSourcesDirty() { for (const channel of [...GEOMETRY_WASM_VERTEX_CHANNELS, "indices"]) this._cpuDirty[channel] = true; this._skinInfluenceDirty = true; } markAllWasmSourcesDirty() { for (const channel of [...GEOMETRY_WASM_VERTEX_CHANNELS, "indices"]) if (this.wasmState(channel).source) this.wasmState(channel).dirty = true; } clearWasmChannel(channel, destroyManagedBuffer) { const state = this.wasmState(channel); state.source = null; state.dirty = false; state.capacityHint = 0; if (destroyManagedBuffer && state.managed) this.replaceChannelBuffer(channel, null); state.managed = false; state.capacity = 0; this.restoreCPUFallbackAfterWasmClear(channel); this._cpuDirty[channel] = true; if (channel === "positions" || channel === "normals" || channel === "colors" || channel === "indices") this._morphBaseRevision = this._morphBaseRevision + 1 >>> 0; if (channel === "joints" || channel === "weights" || channel === "joints1" || channel === "weights1") { this.normalizeSkinPairsAfterWasmClear(); this._skinInfluenceDirty = true; } } setWasmVertexSource(channel, source, capacity, keepCPUData, vertexCount) { if (source === null) { this.clearWasmChannel(channel, true); return false; } assert(!(this.morphTargets.length > 0 && channel === "positions"), "Geometry: wasmPositions with morphTargets are not supported yet; provide CPU positions for morph-target geometry."); const state = this.wasmState(channel); const wasmSource = this.assertWasmSource(channel, source); wasmSource.refresh(); this.validateWasmSourceBeforeSet(channel, wasmSource, vertexCount); state.capacityHint = assertWasmCapacity(capacity, `Geometry: ${geometryWasmFieldName(channel)} capacity`); if (!state.managed) { this.replaceChannelBuffer(channel, null); state.capacity = 0; } if (!(keepCPUData ?? this._keepCPUData)) this.dropCPUChannelDataForWasm(channel); state.source = wasmSource; state.dirty = true; this._cpuDirty[channel] = false; if (channel === "joints" || channel === "weights" || channel === "joints1" || channel === "weights1") this._skinInfluenceDirty = true; return true; } setWasmIndexSource(source, capacity, keepCPUData, indexCount) { if (source === null) { this.clearWasmChannel("indices", true); return false; } const state = this.wasmState("indices"); const wasmSource = assertWasmU32View(source, "Geometry: wasmIndices"); wasmSource.refresh(); this.validateWasmSourceBeforeSet("indices", wasmSource, indexCount); state.capacityHint = assertWasmCapacity(capacity, "Geometry: wasmIndices capacity"); if (!state.managed) { this.replaceChannelBuffer("indices", null); state.capacity = 0; } if (!(keepCPUData ?? this._keepCPUData)) this.dropCPUChannelDataForWasm("indices"); state.source = wasmSource; state.dirty = true; this._cpuDirty.indices = false; return true; } updateWasmBounds(options) { if (!options.recomputeBounds || this._boundsSource === "explicit") return; const source = this._wasm.positions.source; if (!source) return; validateWasmRecordRange(source, this.vertexCount, 3, "Geometry: wasmPositions", "vertexCount"); this.setBounds(computeGeometryBounds(source.array().subarray(0, this.vertexCount * 3)), "computed"); } setWasmPositions(source, options = {}) { if (this.setWasmVertexSource("positions", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmNormals(source, options = {}) { if (this.setWasmVertexSource("normals", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmTangents(source, options = {}) { if (this.setWasmVertexSource("tangents", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmColors(source, options = {}) { if (this.setWasmVertexSource("colors", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmUvs(source, options = {}) { if (this.setWasmVertexSource("uvs", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmUvs1(source, options = {}) { if (this.setWasmVertexSource("uvs1", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmJoints(source, options = {}) { if (this.setWasmVertexSource("joints", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmWeights(source, options = {}) { if (this.setWasmVertexSource("weights", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmJoints1(source, options = {}) { if (this.setWasmVertexSource("joints1", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmWeights1(source, options = {}) { if (this.setWasmVertexSource("weights1", source, options.capacity, options.keepCPUData, options.vertexCount)) this.refreshWasmVertices(options); } setWasmIndices(source, options = {}) { if (this.setWasmIndexSource(source, options.capacity, options.keepCPUData, options.indexCount)) this.refreshWasmIndices(options); } setWasmAttributes(sources, options = {}) { const vertexCapacity = options.vertexCapacity ?? options.capacity; const indexCapacity = options.indexCapacity ?? options.capacity; let touchedVertex = false; let touchedIndex = false; if (Object.prototype.hasOwnProperty.call(sources, "positions")) touchedVertex = this.setWasmVertexSource("positions", sources.positions, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "normals")) touchedVertex = this.setWasmVertexSource("normals", sources.normals, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "tangents")) touchedVertex = this.setWasmVertexSource("tangents", sources.tangents, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "colors")) touchedVertex = this.setWasmVertexSource("colors", sources.colors, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "uvs")) touchedVertex = this.setWasmVertexSource("uvs", sources.uvs, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "uvs1")) touchedVertex = this.setWasmVertexSource("uvs1", sources.uvs1, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "joints")) touchedVertex = this.setWasmVertexSource("joints", sources.joints, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "weights")) touchedVertex = this.setWasmVertexSource("weights", sources.weights, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "joints1")) touchedVertex = this.setWasmVertexSource("joints1", sources.joints1, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "weights1")) touchedVertex = this.setWasmVertexSource("weights1", sources.weights1, vertexCapacity, options.keepCPUData, options.vertexCount) || touchedVertex; if (Object.prototype.hasOwnProperty.call(sources, "indices")) touchedIndex = this.setWasmIndexSource(sources.indices ?? null, indexCapacity, options.keepCPUData, options.indexCount); if (touchedVertex) this.refreshWasmVertices(options); if (touchedIndex) this.refreshWasmIndices(options); } refreshWasmVertices(options = {}) { assert(this.hasWasmVertexSources(), "Geometry: refreshWasmVertices() requires at least one wasm vertex source."); for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) { const source = this.wasmState(channel).source; if (!source) continue; source.refresh(); this.assertWasmSource(channel, source); } let count = options.vertexCount; if (count === void 0 && this._wasm.positions.source) count = resolveWasmRecordCount(this._wasm.positions.source, void 0, 3, "Geometry: wasmPositions", "Geometry: vertexCount", "vertexCount"); if (count === void 0) count = this.vertexCount; this.setVertexCountFromWasm(count); for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) { const source = this.wasmState(channel).source; if (!source) continue; validateWasmRecordRange(source, this.vertexCount, geometryWasmComponents(channel), `Geometry: ${geometryWasmFieldName(channel)}`, "vertexCount"); this.wasmState(channel).dirty = true; if (options.keepCPUData ?? this._keepCPUData) this.setCPUChannelData(channel, this.copyWasmActiveRange(source, this.vertexCount * geometryWasmComponents(channel))); } this._morphBaseRevision = this._morphBaseRevision + 1 >>> 0; this.updateWasmBounds(options); } refreshWasmIndices(options = {}) { const source = this._wasm.indices.source; assert(!!source, "Geometry: refreshWasmIndices() requires wasmIndices."); source.refresh(); assertWasmU32View(source, "Geometry: wasmIndices"); const count = resolveWasmRecordCount(source, options.indexCount, 1, "Geometry: wasmIndices", "Geometry: indexCount", "indexCount"); this.setIndexCountFromWasm(count); validateWasmRecordRange(source, this.indexCount, 1, "Geometry: wasmIndices", "indexCount"); this._wasm.indices.dirty = true; if (options.keepCPUData ?? this._keepCPUData) this.setCPUChannelData("indices", this.copyWasmActiveRange(source, this.indexCount)); this._morphBaseRevision = this._morphBaseRevision + 1 >>> 0; } refreshFromWasm(options = {}) { if (this.hasWasmVertexSources()) this.refreshWasmVertices(options); if (this._wasm.indices.source) this.refreshWasmIndices(options); } get morphBaseRevision() { return this._morphBaseRevision; } getMorphBaseChannel(channel) { this.assertAlive(`access ${channel} morph base data`); const expected = this.vertexCount * geometryWasmComponents(channel); const source = this.wasmState(channel).source; if (source) { source.refresh(); validateWasmRecordRange(source, this.vertexCount, geometryWasmComponents(channel), `Geometry: ${geometryWasmFieldName(channel)}`, "vertexCount"); return this.copyWasmActiveRange(source, expected); } const data = this.getCPUChannelData(channel); if (data && data.length >= expected) return data.subarray(0, expected); if (channel === "positions") throw new Error("Geometry: positions are required for morph base data."); return channel === "colors" ? createFallbackColors(this.vertexCount) : createFallbackNormals(this.vertexCount); } getMorphIndices() { this.assertAlive("access morph index data"); const source = this._wasm.indices.source; if (source) { source.refresh(); validateWasmRecordRange(source, this.indexCount, 1, "Geometry: wasmIndices", "indexCount"); return this.copyWasmActiveRange(source, this.indexCount); } return this.indices ? this.indices.subarray(0, this.indexCount) : null; } clearWasmSources() { for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) this.clearWasmChannel(channel, true); this.clearWasmChannel("indices", true); } retain() { this.assertAlive("retain"); this._refCount++; return this; } release() { if (this._destroyed) throw new Error("Geometry: release() called after the resource was already released."); if (this._refCount <= 0) throw new Error("Geometry: reference count underflow."); this._refCount--; if (this._refCount > 0) return; this._destroyed = true; this.disposeResources(); } upload(device) { this.assertAlive("upload"); const deviceChanged = this._device !== device; if (deviceChanged) { if (this._device) this.disposeResources(); this._device = device; this.markAllCPUSourcesDirty(); this.markAllWasmSourcesDirty(); } if (!deviceChanged && !this.hasDirtyWasmSources() && !this.hasDirtyCPUSources()) return; const queue = device.queue; for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) this.uploadCPUChannel(device, channel); this.uploadCPUChannel(device, "indices"); for (const channel of GEOMETRY_WASM_VERTEX_CHANNELS) this.uploadWasmChannel(device, queue, channel); this.uploadWasmChannel(device, queue, "indices"); this.uploadSkinInfluenceBuffer(device); this._device = device; } uploadCPUChannel(device, channel) { if (this.wasmState(channel).source || !this._cpuDirty[channel]) return; const data = this.getCPUChannelData(channel); if (!data) { this.replaceChannelBuffer(channel, null); this._cpuDirty[channel] = false; return; } if (channel === "positions" && data.length < this.vertexCount * 3) throw new Error("Geometry: positions are required after clearing wasmPositions; provide CPU positions with keepCPUData or setWasmPositions()."); const usage = channel === "indices" ? GPUBufferUsage.INDEX : GPUBufferUsage.VERTEX; this.replaceChannelBuffer(channel, createBuffer(device, data, usage, `Geometry.${channel}`)); this._cpuDirty[channel] = false; } ensureWasmBuffer(device, channel, count) { const state = this.wasmState(channel); const required = Math.max(count, state.capacityHint); if (required <= 0) return; if (state.managed && this.getChannelBuffer(channel) && state.capacity >= required) return; const capacity = growWasmCapacity(required, state.capacity); const size = capacity * geometryWasmComponents(channel) * geometryWasmBytesPerElement(channel); const usage = (channel === "indices" ? GPUBufferUsage.INDEX : GPUBufferUsage.VERTEX) | GPUBufferUsage.COPY_DST; this.replaceChannelBuffer(channel, device.createBuffer({ label: `Geometry.${geometryWasmFieldName(channel)}`, size, usage })); state.managed = true; state.capacity = capacity; } uploadWasmChannel(device, queue, channel) { const state = this.wasmState(channel); const source = state.source; if (!source || !state.dirty) return; source.refresh(); this.assertWasmSource(channel, source); const count = isGeometryWasmVertexChannel(channel) ? this.vertexCount : this.indexCount; validateWasmRecordRange(source, count, geometryWasmComponents(channel), `Geometry: ${geometryWasmFieldName(channel)}`, isGeometryWasmVertexChannel(channel) ? "vertexCount" : "indexCount"); if (count <= 0) { state.dirty = false; return; } this.ensureWasmBuffer(device, channel, count); const buffer = this.getChannelBuffer(channel); assert(!!buffer, `Geometry: ${geometryWasmFieldName(channel)} upload requires a GPU buffer.`); const byteLength = count * geometryWasmComponents(channel) * geometryWasmBytesPerElement(channel); const data = source.array(); queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, byteLength); state.dirty = false; if (channel === "joints" || channel === "weights" || channel === "joints1" || channel === "weights1") this._skinInfluenceDirty = true; } resolveSkinArray(channel) { const source = this.wasmState(channel).source; if (source) return source.array().subarray(0, this.vertexCount * 4); return this.getCPUChannelData(channel); } uploadSkinInfluenceBuffer(device) { if (!this._skinInfluenceDirty) return; if (!this.hasSkinAttributes) { this._skinInfluenceBuffer?.destroy(); this._skinInfluenceBuffer = null; this._skinInfluenceDirty = false; return; } const joints = this.resolveSkinArray("joints"); const weights = this.resolveSkinArray("weights"); assert(!!joints && !!weights, "Geometry: skin influence upload requires JOINTS_0/WEIGHTS_0."); const joints1 = this.resolveSkinArray("joints1"); const weights1 = this.resolveSkinArray("weights1"); this._skinInfluenceBuffer?.destroy(); this._skinInfluenceBuffer = createBuffer(device, packSkinInfluences(joints, weights, joints1, weights1), GPUBufferUsage.VERTEX, "Geometry.skinInfluences"); this._skinInfluenceDirty = false; } get positionBuffer() { this.assertAlive("access positionBuffer"); if (!this._positionBuffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._positionBuffer; } get normalBuffer() { this.assertAlive("access normalBuffer"); if (!this._normalBuffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._normalBuffer; } get tangentBuffer() { this.assertAlive("access tangentBuffer"); if (!this._tangentBuffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._tangentBuffer; } get colorBuffer() { this.assertAlive("access colorBuffer"); if (!this._colorBuffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._colorBuffer; } get uvBuffer() { this.assertAlive("access uvBuffer"); if (!this._uvBuffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._uvBuffer; } get uv1Buffer() { this.assertAlive("access uv1Buffer"); if (!this._uv1Buffer) throw new Error("Geometry not uploaded. Call upload(device) first."); return this._uv1Buffer; } get jointsBuffer() { return this._jointsBuffer; } get weightsBuffer() { return this._weightsBuffer; } get joints1Buffer() { return this._joints1Buffer; } get weights1Buffer() { return this._weights1Buffer; } get skinInfluenceBuffer() { return this._skinInfluenceBuffer; } get indexBuffer() { return this._indexBuffer; } get isIndexed() { return this._indexBuffer !== null; } get isSkinned() { return this._jointsBuffer !== null && this._weightsBuffer !== null; } get isSkinned8() { return this._jointsBuffer !== null && this._weightsBuffer !== null && this._joints1Buffer !== null && this._weights1Buffer !== null; } get hasSkinAttributes() { return !!(this.joints || this._wasm.joints.source) && !!(this.weights || this._wasm.weights.source); } get hasSkin8Attributes() { return this.hasSkinAttributes && !!(this.joints1 || this._wasm.joints1.source) && !!(this.weights1 || this._wasm.weights1.source); } get boundsMin() { return this._boundsMin; } get boundsMax() { return this._boundsMax; } get boundsCenter() { return this._boundsCenter; } get boundsRadius() { return this._boundsRadius; } destroy() { this.release(); } disposeResources() { this._positionBuffer?.destroy(); this._normalBuffer?.destroy(); this._tangentBuffer?.destroy(); this._colorBuffer?.destroy(); this._uvBuffer?.destroy(); this._uv1Buffer?.destroy(); this._jointsBuffer?.destroy(); this._weightsBuffer?.destroy(); this._joints1Buffer?.destroy(); this._weights1Buffer?.destroy(); this._skinInfluenceBuffer?.destroy(); this._jointsBuffer = null; this._weightsBuffer = null; this._joints1Buffer = null; this._weights1Buffer = null; this._skinInfluenceBuffer = null; this._indexBuffer?.destroy(); this._positionBuffer = null; this._normalBuffer = null; this._tangentBuffer = null; this._colorBuffer = null; this._uvBuffer = null; this._uv1Buffer = null; this._indexBuffer = null; this._device = null; for (const channel of [...GEOMETRY_WASM_VERTEX_CHANNELS, "indices"]) { const state = this.wasmState(channel); state.managed = false; state.capacity = 0; } } static point(size = 1, plane = "xy", doubleSided = false) { return _Geometry.rectangle(size, size, plane, doubleSided); } static line(length = 1, thickness = 0.01, plane = "xy", doubleSided = false) { return _Geometry.rectangle(length, thickness, plane, doubleSided); } static plane(width = 1, height = 1, widthSegments = 1, heightSegments = 1) { const w = width / 2, h = height / 2; const gridX = widthSegments, gridY = heightSegments; const gridX1 = gridX + 1, gridY1 = gridY + 1; const segmentWidth = width / gridX; const segmentHeight = height / gridY; const positions = []; const normals = []; const uvs = []; const indices = []; for (let iy = 0; iy < gridY1; iy++) { const y = iy * segmentHeight - h; for (let ix = 0; ix < gridX1; ix++) { const x = ix * segmentWidth - w; positions.push(x, 0, y); normals.push(0, 1, 0); uvs.push(ix / gridX, 1 - iy / gridY); } } for (let iy = 0; iy < gridY; iy++) { for (let ix = 0; ix < gridX; ix++) { const a = ix + gridX1 * iy; const b = ix + gridX1 * (iy + 1); const c = ix + 1 + gridX1 * (iy + 1); const d = ix + 1 + gridX1 * iy; indices.push(a, b, d, b, c, d); } } return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static triangle(width = 1, height = 1, plane = "xy", doubleSided = false) { const w = width / 2; const h = height / 2; const positions = []; const normals = []; const uvs = []; const indices = []; const flipWinding = plane === "xz"; let nx = 0, ny = 0, nz = 0; switch (plane) { case "xy": nz = 1; break; case "xz": ny = 1; break; case "yz": nx = 1; break; } const uFor = (x) => width !== 0 ? x / width + 0.5 : 0.5; const vFor = (y) => height !== 0 ? -y / height + 0.5 : 0.5; const pushVertex = (x, y) => { switch (plane) { case "xy": positions.push(x, y, 0); break; case "xz": positions.push(x, 0, y); break; case "yz": positions.push(0, x, y); break; } normals.push(nx, ny, nz); uvs.push(uFor(x), vFor(y)); }; pushVertex(-w, -h); pushVertex(w, -h); pushVertex(0, h); if (flipWinding) { indices.push(0, 2, 1); } else { indices.push(0, 1, 2); } const base = { positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static rectangle(width = 1, height = 1, plane = "xy", doubleSided = false) { const w = width / 2; const h = height / 2; const positions = []; const normals = []; const uvs = []; const indices = []; const flipWinding = plane === "xz"; let nx = 0, ny = 0, nz = 0; switch (plane) { case "xy": nz = 1; break; case "xz": ny = 1; break; case "yz": nx = 1; break; } const pushVertex = (x, y, u, v) => { switch (plane) { case "xy": positions.push(x, y, 0); break; case "xz": positions.push(x, 0, y); break; case "yz": positions.push(0, x, y); break; } normals.push(nx, ny, nz); uvs.push(u, v); }; pushVertex(-w, -h, 0, 1); pushVertex(w, -h, 1, 1); pushVertex(w, h, 1, 0); pushVertex(-w, h, 0, 0); if (flipWinding) { indices.push(0, 2, 1, 0, 3, 2); } else { indices.push(0, 1, 2, 0, 2, 3); } const base = { positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static circle(radius = 0.5, segments = 64, plane = "xy", doubleSided = false) { const seg = Math.max(3, Math.floor(segments)); const positions = []; const normals = []; const uvs = []; const indices = []; const flipWinding = plane === "xz"; let nx = 0, ny = 0, nz = 0; switch (plane) { case "xy": nz = 1; break; case "xz": ny = 1; break; case "yz": nx = 1; break; } const inv2r = radius !== 0 ? 1 / (2 * radius) : 0; const pushVertex = (x, y) => { switch (plane) { case "xy": positions.push(x, y, 0); break; case "xz": positions.push(x, 0, y); break; case "yz": positions.push(0, x, y); break; } normals.push(nx, ny, nz); const u = radius !== 0 ? 0.5 + x * inv2r : 0.5; const v = radius !== 0 ? 0.5 - y * inv2r : 0.5; uvs.push(u, v); }; pushVertex(0, 0); for (let i = 0; i < seg; i++) { const t = i / seg * Math.PI * 2; const x = Math.cos(t) * radius; const y = Math.sin(t) * radius; pushVertex(x, y); } for (let i = 0; i < seg; i++) { const a = 0; const b = 1 + i; const c = 1 + (i + 1) % seg; if (flipWinding) { indices.push(a, c, b); } else { indices.push(a, b, c); } } const base = { positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static ellipse(radiusX = 0.5, radiusY = 0.5, segments = 64, plane = "xy", doubleSided = false) { const seg = Math.max(3, Math.floor(segments)); const positions = []; const normals = []; const uvs = []; const indices = []; const flipWinding = plane === "xz"; let nx = 0, ny = 0, nz = 0; switch (plane) { case "xy": nz = 1; break; case "xz": ny = 1; break; case "yz": nx = 1; break; } const inv2rx = radiusX !== 0 ? 1 / (2 * radiusX) : 0; const inv2ry = radiusY !== 0 ? 1 / (2 * radiusY) : 0; const pushVertex = (x, y) => { switch (plane) { case "xy": positions.push(x, y, 0); break; case "xz": positions.push(x, 0, y); break; case "yz": positions.push(0, x, y); break; } normals.push(nx, ny, nz); const u = radiusX !== 0 ? 0.5 + x * inv2rx : 0.5; const v = radiusY !== 0 ? 0.5 - y * inv2ry : 0.5; uvs.push(u, v); }; pushVertex(0, 0); for (let i = 0; i < seg; i++) { const t = i / seg * Math.PI * 2; const x = Math.cos(t) * radiusX; const y = Math.sin(t) * radiusY; pushVertex(x, y); } for (let i = 0; i < seg; i++) { const a = 0; const b = 1 + i; const c = 1 + (i + 1) % seg; if (flipWinding) { indices.push(a, c, b); } else { indices.push(a, b, c); } } const base = { positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static box(width = 1, height = 1, depth = 1) { const w = width / 2, h = height / 2, d = depth / 2; const positions = new Float32Array([ -w, -h, d, w, -h, d, w, h, d, -w, h, d, w, -h, -d, -w, -h, -d, -w, h, -d, w, h, -d, -w, h, d, w, h, d, w, h, -d, -w, h, -d, -w, -h, -d, w, -h, -d, w, -h, d, -w, -h, d, w, -h, d, w, -h, -d, w, h, -d, w, h, d, -w, -h, -d, -w, -h, d, -w, h, d, -w, h, -d ]); const normals = new Float32Array([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0 ]); const uvs = new Float32Array([ 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0 ]); const indices = new Uint32Array([ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 ]); return new _Geometry({ positions, normals, uvs, indices }); } static sphere(radius = 0.5, widthSegments = 32, heightSegments = 16) { const positions = []; const normals = []; const uvs = []; const indices = []; for (let iy = 0; iy <= heightSegments; iy++) { const v = iy / heightSegments; const phi = v * Math.PI; for (let ix = 0; ix <= widthSegments; ix++) { const u = ix / widthSegments; const theta = u * Math.PI * 2; const x = -Math.cos(theta) * Math.sin(phi); const y = Math.cos(phi); const z = Math.sin(theta) * Math.sin(phi); positions.push(radius * x, radius * y, radius * z); normals.push(x, y, z); uvs.push(u, v); } } for (let iy = 0; iy < heightSegments; iy++) { for (let ix = 0; ix < widthSegments; ix++) { const a = ix + (widthSegments + 1) * iy; const b = ix + (widthSegments + 1) * (iy + 1); const c = ix + 1 + (widthSegments + 1) * (iy + 1); const d = ix + 1 + (widthSegments + 1) * iy; if (iy !== 0) indices.push(a, b, d); if (iy !== heightSegments - 1) indices.push(b, c, d); } } return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static cylinder(radiusTop = 0.5, radiusBottom = 0.5, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false) { const positions = []; const normals = []; const uvs = []; const indices = []; let index = 0; const halfHeight = height / 2; const slope = (radiusBottom - radiusTop) / height; for (let iy = 0; iy <= heightSegments; iy++) { const v = iy / heightSegments; const y = v * height - halfHeight; const radius = v * (radiusTop - radiusBottom) + radiusBottom; for (let ix = 0; ix <= radialSegments; ix++) { const u = ix / radialSegments; const theta = u * Math.PI * 2; const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); positions.push(radius * sinTheta, y, radius * cosTheta); const nLen = Math.sqrt(1 + slope * slope); normals.push(sinTheta / nLen, slope / nLen, cosTheta / nLen); uvs.push(u, 1 - v); } } for (let iy = 0; iy < heightSegments; iy++) { for (let ix = 0; ix < radialSegments; ix++) { const a = ix + (radialSegments + 1) * iy; const b = ix + (radialSegments + 1) * (iy + 1); const c = ix + 1 + (radialSegments + 1) * (iy + 1); const d = ix + 1 + (radialSegments + 1) * iy; indices.push(a, d, b, b, d, c); } } index = positions.length / 3; const generateTopCap = () => { const centerIndex = index; positions.push(0, halfHeight, 0); normals.push(0, 1, 0); uvs.push(0.5, 0.5); index++; for (let ix = 0; ix <= radialSegments; ix++) { const u = ix / radialSegments; const theta = u * Math.PI * 2; const x = radiusTop * Math.sin(theta); const z = radiusTop * Math.cos(theta); positions.push(x, halfHeight, z); normals.push(0, 1, 0); uvs.push(Math.sin(theta) * 0.5 + 0.5, Math.cos(theta) * 0.5 + 0.5); if (ix > 0) indices.push(centerIndex, index - 1, index); index++; } }; const generateBottomCap = () => { const centerIndex = index; positions.push(0, -halfHeight, 0); normals.push(0, -1, 0); uvs.push(0.5, 0.5); index++; for (let ix = 0; ix <= radialSegments; ix++) { const u = ix / radialSegments; const theta = u * Math.PI * 2; const x = radiusBottom * Math.sin(theta); const z = radiusBottom * Math.cos(theta); positions.push(x, -halfHeight, z); normals.push(0, -1, 0); uvs.push(Math.sin(theta) * 0.5 + 0.5, Math.cos(theta) * 0.5 + 0.5); if (ix > 0) indices.push(centerIndex, index, index - 1); index++; } }; if (!openEnded) { generateTopCap(); generateBottomCap(); } return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static pyramid(baseWidth = 1, baseDepth = 1, height = 1) { const w = baseWidth / 2, d = baseDepth / 2; const h = height; const apex = [0, h, 0]; const bl = [-w, 0, -d]; const br = [w, 0, -d]; const fr = [w, 0, d]; const fl = [-w, 0, d]; const faceNormal = (v0, v1, v2) => { const ax = v1[0] - v0[0], ay = v1[1] - v0[1], az = v1[2] - v0[2]; const bx = v2[0] - v0[0], by = v2[1] - v0[1], bz = v2[2] - v0[2]; const nx = ay * bz - az * by; const ny = az * bx - ax * bz; const nz = ax * by - ay * bx; const len = Math.sqrt(nx * nx + ny * ny + nz * nz); return [nx / len, ny / len, nz / len]; }; const positions = []; const normals = []; const uvs = []; const indices = []; let idx = 0; const addFace = (v0, v1, v2) => { const n = faceNormal(v0, v1, v2); positions.push(...v0, ...v1, ...v2); normals.push(...n, ...n, ...n); uvs.push(0.5, 0, 0, 1, 1, 1); indices.push(idx, idx + 1, idx + 2); idx += 3; }; addFace(apex, fl, fr); addFace(apex, fr, br); addFace(apex, br, bl); addFace(apex, bl, fl); const baseNormal = [0, -1, 0]; positions.push(...bl, ...br, ...fr, ...fl); normals.push(...baseNormal, ...baseNormal, ...baseNormal, ...baseNormal); uvs.push(0, 0, 1, 0, 1, 1, 0, 1); indices.push(idx, idx + 1, idx + 2, idx, idx + 2, idx + 3); return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static torus(radius = 0.5, tube = 0.2, radialSegments = 32, tubularSegments = 24) { const positions = []; const normals = []; const uvs = []; const indices = []; for (let j = 0; j <= radialSegments; j++) { for (let i = 0; i <= tubularSegments; i++) { const u = i / tubularSegments * Math.PI * 2; const v = j / radialSegments * Math.PI * 2; const x = (radius + tube * Math.cos(v)) * Math.cos(u); const y = tube * Math.sin(v); const z = (radius + tube * Math.cos(v)) * Math.sin(u); positions.push(x, y, z); const cx = radius * Math.cos(u); const cz = radius * Math.sin(u); const nx = x - cx; const ny = y; const nz = z - cz; const len = Math.sqrt(nx * nx + ny * ny + nz * nz); normals.push(nx / len, ny / len, nz / len); uvs.push(i / tubularSegments, j / radialSegments); } } for (let j = 0; j < radialSegments; j++) { for (let i = 0; i < tubularSegments; i++) { const a = i + (tubularSegments + 1) * j; const b = i + (tubularSegments + 1) * (j + 1); const c = i + 1 + (tubularSegments + 1) * (j + 1); const d = i + 1 + (tubularSegments + 1) * j; indices.push(a, b, d, b, c, d); } } return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static prism(radius = 0.5, height = 1, sides = 6) { if (sides < 3) sides = 3; const positions = []; const normals = []; const uvs = []; const indices = []; const halfHeight = height / 2; let idx = 0; const topRing = []; const bottomRing = []; for (let i = 0; i < sides; i++) { const theta = i / sides * Math.PI * 2; const x = radius * Math.cos(theta); const z = radius * Math.sin(theta); topRing.push([x, halfHeight, z]); bottomRing.push([x, -halfHeight, z]); } const faceNormal = (v0, v1, v2) => { const ax = v1[0] - v0[0], ay = v1[1] - v0[1], az = v1[2] - v0[2]; const bx = v2[0] - v0[0], by = v2[1] - v0[1], bz = v2[2] - v0[2]; const nx = ay * bz - az * by; const ny = az * bx - ax * bz; const nz = ax * by - ay * bx; const len = Math.sqrt(nx * nx + ny * ny + nz * nz); return [nx / len, ny / len, nz / len]; }; for (let i = 0; i < sides; i++) { const next = (i + 1) % sides; const t0 = topRing[i]; const t1 = topRing[next]; const b0 = bottomRing[i]; const b1 = bottomRing[next]; const n = faceNormal(t0, t1, b0); positions.push(...t0, ...b0, ...b1, ...t1); normals.push(...n, ...n, ...n, ...n); const u0 = i / sides; const u1 = (i + 1) / sides; uvs.push(u0, 0, u0, 1, u1, 1, u1, 0); indices.push(idx, idx + 2, idx + 1, idx, idx + 3, idx + 2); idx += 4; } const topCenter = [0, halfHeight, 0]; const topNormal = [0, 1, 0]; const topCenterIdx = idx; positions.push(...topCenter); normals.push(...topNormal); uvs.push(0.5, 0.5); idx++; for (let i = 0; i < sides; i++) { const t = topRing[i]; positions.push(...t); normals.push(...topNormal); const u = 0.5 + 0.5 * Math.cos(i / sides * Math.PI * 2); const v = 0.5 + 0.5 * Math.sin(i / sides * Math.PI * 2); uvs.push(u, v); } for (let i = 0; i < sides; i++) { const next = (i + 1) % sides; indices.push(topCenterIdx, topCenterIdx + 1 + next, topCenterIdx + 1 + i); } idx += sides; const bottomCenter = [0, -halfHeight, 0]; const bottomNormal = [0, -1, 0]; const bottomCenterIdx = idx; positions.push(...bottomCenter); normals.push(...bottomNormal); uvs.push(0.5, 0.5); idx++; for (let i = 0; i < sides; i++) { const b = bottomRing[i]; positions.push(...b); normals.push(...bottomNormal); const u = 0.5 + 0.5 * Math.cos(i / sides * Math.PI * 2); const v = 0.5 + 0.5 * Math.sin(i / sides * Math.PI * 2); uvs.push(u, v); } for (let i = 0; i < sides; i++) { const next = (i + 1) % sides; indices.push(bottomCenterIdx, bottomCenterIdx + 1 + i, bottomCenterIdx + 1 + next); } return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static cartesianCurve(descriptor) { const f = descriptor.f; const xMin = descriptor.xMin ?? -1; const xMax = descriptor.xMax ?? 1; const segments = Math.max(2, Math.floor(descriptor.segments ?? 256)); const radius = descriptor.radius ?? 0.01; const radialSegments = Math.max(3, Math.floor(descriptor.radialSegments ?? 8)); const closed = descriptor.closed ?? false; const plane = descriptor.plane ?? "xy"; const upLocal = descriptor.up ?? [0, 0, 1]; let up; switch (plane) { case "xy": up = upLocal; break; case "xz": up = [upLocal[0], upLocal[2], upLocal[1]]; break; case "yz": up = [upLocal[2], upLocal[0], upLocal[1]]; break; } const breakOnInvalid = descriptor.breakOnInvalid ?? true; const positions = []; const normals = []; const uvs = []; const indices = []; let vertexOffset = 0; const sampleCount = closed ? segments : segments + 1; let segmentPoints = []; let anyInvalid = false; const flushSegment = (close) => { const pointCount = segmentPoints.length / 3; if (pointCount >= 2) { vertexOffset = _Geometry._appendTubeSegment(new Float32Array(segmentPoints), radius, radialSegments, close, up, positions, normals, uvs, indices, vertexOffset); } segmentPoints = []; }; const range = xMax - xMin; for (let i = 0; i < sampleCount; i++) { const u = segments > 0 ? i / segments : 0; const x = xMin + range * u; const y = f(x); if (!Number.isFinite(y)) { anyInvalid = true; if (breakOnInvalid) flushSegment(false); continue; } let wx; let wy; let wz; switch (plane) { case "xy": wx = x; wy = y; wz = 0; break; case "xz": wx = x; wy = 0; wz = y; break; case "yz": wx = 0; wy = x; wz = y; break; } segmentPoints.push(wx, wy, wz); } flushSegment(closed && !anyInvalid); if (positions.length === 0) return new _Geometry({ positions: new Float32Array(0) }); return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static cartesianSurface(descriptor) { const f = descriptor.f; const xMin = descriptor.xMin ?? -1; const xMax = descriptor.xMax ?? 1; const zMin = descriptor.zMin ?? -1; const zMax = descriptor.zMax ?? 1; const xSegments = Math.max(1, Math.floor(descriptor.xSegments ?? 128)); const zSegments = Math.max(1, Math.floor(descriptor.zSegments ?? 128)); const skipInvalid = descriptor.skipInvalid ?? true; const doubleSided = descriptor.doubleSided ?? false; const plane = descriptor.plane ?? "xz"; const gridX = xSegments; const gridZ = zSegments; const gridX1 = gridX + 1; const gridZ1 = gridZ + 1; const positions = new Float32Array(gridX1 * gridZ1 * 3); const normals = new Float32Array(gridX1 * gridZ1 * 3); const uvs = new Float32Array(gridX1 * gridZ1 * 2); const valid = new Uint8Array(gridX1 * gridZ1); const xRange = xMax - xMin; const zRange = zMax - zMin; for (let iz = 0; iz < gridZ1; iz++) { const vz = gridZ > 0 ? iz / gridZ : 0; const z = zMin + zRange * vz; for (let ix = 0; ix < gridX1; ix++) { const ux = gridX > 0 ? ix / gridX : 0; const x = xMin + xRange * ux; const i = ix + gridX1 * iz; const y = f(x, z); const ok = Number.isFinite(y); valid[i] = ok ? 1 : 0; const p = i * 3; const height = ok ? y : 0; let wx; let wy; let wz; switch (plane) { case "xy": wx = x; wy = z; wz = height; break; case "xz": wx = x; wy = height; wz = z; break; case "yz": wx = height; wy = x; wz = z; break; } positions[p + 0] = wx; positions[p + 1] = wy; positions[p + 2] = wz; const t = i * 2; uvs[t + 0] = ux; uvs[t + 1] = 1 - vz; } } _Geometry._computeGridNormals(positions, valid, gridX, gridZ, normals); const indices = []; for (let iz = 0; iz < gridZ; iz++) { for (let ix = 0; ix < gridX; ix++) { const a = ix + gridX1 * iz; const b = ix + gridX1 * (iz + 1); const c = ix + 1 + gridX1 * (iz + 1); const d = ix + 1 + gridX1 * iz; if (skipInvalid && (!valid[a] || !valid[b] || !valid[c] || !valid[d])) continue; indices.push(a, b, d, b, c, d); } } if (indices.length === 0) return new _Geometry({ positions: new Float32Array(0) }); const base = { positions, normals, uvs, indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static parametricCurve(descriptor) { const f = descriptor.f; const tMin = descriptor.tMin ?? 0; const tMax = descriptor.tMax ?? 1; const segments = Math.max(2, Math.floor(descriptor.segments ?? 256)); const radius = descriptor.radius ?? 0.01; const radialSegments = Math.max(3, Math.floor(descriptor.radialSegments ?? 8)); const closed = descriptor.closed ?? false; const breakOnInvalid = descriptor.breakOnInvalid ?? true; const plane = descriptor.plane ?? "xy"; const positions = []; const normals = []; const uvs = []; const indices = []; let vertexOffset = 0; const sampleCount = closed ? segments : segments + 1; let segmentPoints = []; let anyInvalid = false; let upLocal = descriptor.up ?? null; let upWorld = null; if (upLocal) { switch (plane) { case "xy": upWorld = upLocal; break; case "xz": upWorld = [upLocal[0], upLocal[2], upLocal[1]]; break; case "yz": upWorld = [upLocal[2], upLocal[0], upLocal[1]]; break; } } const flushSegment = (close) => { const pointCount = segmentPoints.length / 3; if (pointCount >= 2) { const upVec = upWorld ?? [0, 1, 0]; vertexOffset = _Geometry._appendTubeSegment(new Float32Array(segmentPoints), radius, radialSegments, close, upVec, positions, normals, uvs, indices, vertexOffset); } segmentPoints = []; }; const range = tMax - tMin; for (let i = 0; i < sampleCount; i++) { const s = segments > 0 ? i / segments : 0; const t = tMin + range * s; const p = f(t); let x; let y; let z; if (p.length === 2) { x = p[0]; y = p[1]; z = 0; if (!upLocal) { upLocal = [0, 0, 1]; switch (plane) { case "xy": upWorld = upLocal; break; case "xz": upWorld = [upLocal[0], upLocal[2], upLocal[1]]; break; case "yz": upWorld = [upLocal[2], upLocal[0], upLocal[1]]; break; } } } else { x = p[0]; y = p[1]; z = p[2]; if (!upLocal) { upLocal = [0, 1, 0]; switch (plane) { case "xy": upWorld = upLocal; break; case "xz": upWorld = [upLocal[0], upLocal[2], upLocal[1]]; break; case "yz": upWorld = [upLocal[2], upLocal[0], upLocal[1]]; break; } } } if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) { anyInvalid = true; if (breakOnInvalid) flushSegment(false); continue; } let wx; let wy; let wz; switch (plane) { case "xy": wx = x; wy = y; wz = z; break; case "xz": wx = x; wy = z; wz = y; break; case "yz": wx = z; wy = x; wz = y; break; } segmentPoints.push(wx, wy, wz); } flushSegment(closed && !anyInvalid); if (positions.length === 0) return new _Geometry({ positions: new Float32Array(0) }); return new _Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); } static parametricSurface(descriptor) { const f = descriptor.f; const uMin = descriptor.uMin ?? 0; const uMax = descriptor.uMax ?? 1; const vMin = descriptor.vMin ?? 0; const vMax = descriptor.vMax ?? 1; const uSegments = Math.max(1, Math.floor(descriptor.uSegments ?? 128)); const vSegments = Math.max(1, Math.floor(descriptor.vSegments ?? 128)); const skipInvalid = descriptor.skipInvalid ?? true; const doubleSided = descriptor.doubleSided ?? false; const plane = descriptor.plane ?? "xy"; const gridU = uSegments; const gridV = vSegments; const gridU1 = gridU + 1; const gridV1 = gridV + 1; const positions = new Float32Array(gridU1 * gridV1 * 3); const normals = new Float32Array(gridU1 * gridV1 * 3); const uvs = new Float32Array(gridU1 * gridV1 * 2); const valid = new Uint8Array(gridU1 * gridV1); const uRange = uMax - uMin; const vRange = vMax - vMin; for (let iv = 0; iv < gridV1; iv++) { const vv = gridV > 0 ? iv / gridV : 0; const v = vMin + vRange * vv; for (let iu = 0; iu < gridU1; iu++) { const uu = gridU > 0 ? iu / gridU : 0; const u = uMin + uRange * uu; const i = iu + gridU1 * iv; const p = f(u, v); const x = p[0]; const y = p[1]; const z = p[2]; const ok = Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z); valid[i] = ok ? 1 : 0; const o = i * 3; if (ok) { let wx; let wy; let wz; switch (plane) { case "xy": wx = x; wy = y; wz = z; break; case "xz": wx = x; wy = z; wz = y; break; case "yz": wx = z; wy = x; wz = y; break; } positions[o + 0] = wx; positions[o + 1] = wy; positions[o + 2] = wz; } else { positions[o + 0] = 0; positions[o + 1] = 0; positions[o + 2] = 0; } const t = i * 2; uvs[t + 0] = uu; uvs[t + 1] = 1 - vv; } } _Geometry._computeGridNormals(positions, valid, gridU, gridV, normals); const indices = []; for (let iv = 0; iv < gridV; iv++) { for (let iu = 0; iu < gridU; iu++) { const a = iu + gridU1 * iv; const b = iu + gridU1 * (iv + 1); const c = iu + 1 + gridU1 * (iv + 1); const d = iu + 1 + gridU1 * iv; if (skipInvalid && (!valid[a] || !valid[b] || !valid[c] || !valid[d])) continue; indices.push(a, b, d, b, c, d); } } if (indices.length === 0) return new _Geometry({ positions: new Float32Array(0) }); const base = { positions, normals, uvs, indices: new Uint32Array(indices) }; return new _Geometry(doubleSided ? _Geometry._makeDoubleSided(base) : base); } static _appendTubeSegment(points, radius, radialSegments, closed, up, outPositions, outNormals, outUvs, outIndices, vertexOffset) { const pointCount = points.length / 3; if (pointCount < 2) return vertexOffset; const tangents = new Float32Array(pointCount * 3); for (let i = 0; i < pointCount; i++) { const prev = closed ? (i - 1 + pointCount) % pointCount : Math.max(i - 1, 0); const next = closed ? (i + 1) % pointCount : Math.min(i + 1, pointCount - 1); let tx = points[next * 3 + 0] - points[prev * 3 + 0]; let ty = points[next * 3 + 1] - points[prev * 3 + 1]; let tz = points[next * 3 + 2] - points[prev * 3 + 2]; const tLen = Math.sqrt(tx * tx + ty * ty + tz * tz); if (tLen > 1e-12) { tx /= tLen; ty /= tLen; tz /= tLen; } else { tx = 0; ty = 1; tz = 0; } tangents[i * 3 + 0] = tx; tangents[i * 3 + 1] = ty; tangents[i * 3 + 2] = tz; } const normals = new Float32Array(pointCount * 3); const binormals = new Float32Array(pointCount * 3); let upX = up[0], upY = up[1], upZ = up[2]; const t0x = tangents[0], t0y = tangents[1], t0z = tangents[2]; let n0x = t0y * upZ - t0z * upY; let n0y = t0z * upX - t0x * upZ; let n0z = t0x * upY - t0y * upX; let n0Len = Math.sqrt(n0x * n0x + n0y * n0y + n0z * n0z); if (n0Len < 1e-6) { if (Math.abs(t0x) < 0.9) { upX = 1; upY = 0; upZ = 0; } else { upX = 0; upY = 1; upZ = 0; } n0x = t0y * upZ - t0z * upY; n0y = t0z * upX - t0x * upZ; n0z = t0x * upY - t0y * upX; n0Len = Math.sqrt(n0x * n0x + n0y * n0y + n0z * n0z); } if (n0Len > 1e-12) { n0x /= n0Len; n0y /= n0Len; n0z /= n0Len; } else { n0x = 1; n0y = 0; n0z = 0; } normals[0] = n0x; normals[1] = n0y; normals[2] = n0z; let b0x = t0y * n0z - t0z * n0y; let b0y = t0z * n0x - t0x * n0z; let b0z = t0x * n0y - t0y * n0x; const b0Len = Math.sqrt(b0x * b0x + b0y * b0y + b0z * b0z); if (b0Len > 1e-12) { b0x /= b0Len; b0y /= b0Len; b0z /= b0Len; } binormals[0] = b0x; binormals[1] = b0y; binormals[2] = b0z; for (let i = 1; i < pointCount; i++) { const tPrevX = tangents[(i - 1) * 3 + 0]; const tPrevY = tangents[(i - 1) * 3 + 1]; const tPrevZ = tangents[(i - 1) * 3 + 2]; const tCurX = tangents[i * 3 + 0]; const tCurY = tangents[i * 3 + 1]; const tCurZ = tangents[i * 3 + 2]; let ax = tPrevY * tCurZ - tPrevZ * tCurY; let ay = tPrevZ * tCurX - tPrevX * tCurZ; let az = tPrevX * tCurY - tPrevY * tCurX; const aLen = Math.sqrt(ax * ax + ay * ay + az * az); let nx = normals[(i - 1) * 3 + 0]; let ny = normals[(i - 1) * 3 + 1]; let nz = normals[(i - 1) * 3 + 2]; if (aLen > 1e-6) { ax /= aLen; ay /= aLen; az /= aLen; const dot = Math.max(-1, Math.min(1, tPrevX * tCurX + tPrevY * tCurY + tPrevZ * tCurZ)); const angle = Math.acos(dot); const c = Math.cos(angle); const s = Math.sin(angle); const oneMinusC = 1 - c; const crossX = ay * nz - az * ny; const crossY = az * nx - ax * nz; const crossZ = ax * ny - ay * nx; const aDotN = ax * nx + ay * ny + az * nz; const rx = nx * c + crossX * s + ax * aDotN * oneMinusC; const ry = ny * c + crossY * s + ay * aDotN * oneMinusC; const rz = nz * c + crossZ * s + az * aDotN * oneMinusC; nx = rx; ny = ry; nz = rz; } const nDotT = nx * tCurX + ny * tCurY + nz * tCurZ; nx -= tCurX * nDotT; ny -= tCurY * nDotT; nz -= tCurZ * nDotT; const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz); if (nLen > 1e-12) { nx /= nLen; ny /= nLen; nz /= nLen; } else { nx = normals[0]; ny = normals[1]; nz = normals[2]; } normals[i * 3 + 0] = nx; normals[i * 3 + 1] = ny; normals[i * 3 + 2] = nz; let bx = tCurY * nz - tCurZ * ny; let by = tCurZ * nx - tCurX * nz; let bz = tCurX * ny - tCurY * nx; const bLen = Math.sqrt(bx * bx + by * by + bz * bz); if (bLen > 1e-12) { bx /= bLen; by /= bLen; bz /= bLen; } binormals[i * 3 + 0] = bx; binormals[i * 3 + 1] = by; binormals[i * 3 + 2] = bz; } const ring = radialSegments + 1; const denomU = closed ? pointCount : pointCount - 1; for (let i = 0; i < pointCount; i++) { const u = denomU > 0 ? i / denomU : 0; const px = points[i * 3 + 0]; const py = points[i * 3 + 1]; const pz = points[i * 3 + 2]; const nx0 = normals[i * 3 + 0]; const ny0 = normals[i * 3 + 1]; const nz0 = normals[i * 3 + 2]; const bx0 = binormals[i * 3 + 0]; const by0 = binormals[i * 3 + 1]; const bz0 = binormals[i * 3 + 2]; for (let j = 0; j <= radialSegments; j++) { const v = radialSegments > 0 ? j / radialSegments : 0; const theta = v * Math.PI * 2; const cosT = Math.cos(theta); const sinT = Math.sin(theta); const rx = cosT * nx0 + sinT * bx0; const ry = cosT * ny0 + sinT * by0; const rz = cosT * nz0 + sinT * bz0; outPositions.push(px + radius * rx, py + radius * ry, pz + radius * rz); outNormals.push(rx, ry, rz); outUvs.push(u, v); } } const segmentCount = closed ? pointCount : pointCount - 1; for (let i = 0; i < segmentCount; i++) { const next = closed ? (i + 1) % pointCount : i + 1; for (let j = 0; j < radialSegments; j++) { const a = vertexOffset + ring * i + j; const b = vertexOffset + ring * next + j; const c = vertexOffset + ring * next + j + 1; const d = vertexOffset + ring * i + j + 1; outIndices.push(a, d, b, b, d, c); } } return vertexOffset + ring * pointCount; } static _computeGridNormals(positions, valid, gridX, gridY, outNormals) { const gridX1 = gridX + 1; const gridY1 = gridY + 1; for (let iy = 0; iy < gridY1; iy++) { for (let ix = 0; ix < gridX1; ix++) { const i = ix + gridX1 * iy; const o = i * 3; if (!valid[i]) { outNormals[o + 0] = 0; outNormals[o + 1] = 0; outNormals[o + 2] = 0; continue; } const iL = ix > 0 ? i - 1 : i; const iR = ix < gridX ? i + 1 : i; const iD = iy > 0 ? i - gridX1 : i; const iU = iy < gridY ? i + gridX1 : i; const lx = valid[iL] ? positions[iL * 3 + 0] : positions[o + 0]; const ly = valid[iL] ? positions[iL * 3 + 1] : positions[o + 1]; const lz = valid[iL] ? positions[iL * 3 + 2] : positions[o + 2]; const rx = valid[iR] ? positions[iR * 3 + 0] : positions[o + 0]; const ry = valid[iR] ? positions[iR * 3 + 1] : positions[o + 1]; const rz = valid[iR] ? positions[iR * 3 + 2] : positions[o + 2]; const dx = valid[iD] ? positions[iD * 3 + 0] : positions[o + 0]; const dy = valid[iD] ? positions[iD * 3 + 1] : positions[o + 1]; const dz = valid[iD] ? positions[iD * 3 + 2] : positions[o + 2]; const ux = valid[iU] ? positions[iU * 3 + 0] : positions[o + 0]; const uy = valid[iU] ? positions[iU * 3 + 1] : positions[o + 1]; const uz = valid[iU] ? positions[iU * 3 + 2] : positions[o + 2]; const pux = rx - lx; const puy = ry - ly; const puz = rz - lz; const pvx = ux - dx; const pvy = uy - dy; const pvz = uz - dz; let nx = pvy * puz - pvz * puy; let ny = pvz * pux - pvx * puz; let nz = pvx * puy - pvy * pux; const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz); if (nLen > 1e-12) { nx /= nLen; ny /= nLen; nz /= nLen; } else { nx = 0; ny = 1; nz = 0; } outNormals[o + 0] = nx; outNormals[o + 1] = ny; outNormals[o + 2] = nz; } } } static _makeDoubleSided(descriptor) { const positions = descriptor.positions; assert(!!positions, "Geometry: positions are required for double-sided geometry generation."); const normals = descriptor.normals ?? new Float32Array(positions.length / 3 * 3); const tangents = descriptor.tangents ?? null; const uvs = descriptor.uvs ?? new Float32Array(positions.length / 3 * 2); const uvs1 = descriptor.uvs1 ?? new Float32Array(positions.length / 3 * 2); const indices = descriptor.indices; if (!indices) return descriptor; const baseVertexCount = positions.length / 3; const outPositions = new Float32Array(positions.length * 2); outPositions.set(positions, 0); outPositions.set(positions, positions.length); const outNormals = new Float32Array(normals.length * 2); outNormals.set(normals, 0); for (let i = 0; i < baseVertexCount; i++) { const o = i * 3; outNormals[normals.length + o + 0] = -normals[o + 0]; outNormals[normals.length + o + 1] = -normals[o + 1]; outNormals[normals.length + o + 2] = -normals[o + 2]; } const outUvs = new Float32Array(uvs.length * 2); outUvs.set(uvs, 0); outUvs.set(uvs, uvs.length); const outUvs1 = new Float32Array(uvs1.length * 2); outUvs1.set(uvs1, 0); outUvs1.set(uvs1, uvs1.length); let outTangents; if (tangents) { outTangents = new Float32Array(tangents.length * 2); outTangents.set(tangents, 0); for (let i = 0; i < baseVertexCount; i++) { const o = i * 4; outTangents[tangents.length + o + 0] = tangents[o + 0]; outTangents[tangents.length + o + 1] = tangents[o + 1]; outTangents[tangents.length + o + 2] = tangents[o + 2]; outTangents[tangents.length + o + 3] = -tangents[o + 3]; } } const outIndices = new Uint32Array(indices.length * 2); outIndices.set(indices, 0); for (let i = 0; i < indices.length; i += 3) { const i0 = indices[i + 0]; const i1 = indices[i + 1]; const i2 = indices[i + 2]; const o = indices.length + i; outIndices[o + 0] = baseVertexCount + i0; outIndices[o + 1] = baseVertexCount + i2; outIndices[o + 2] = baseVertexCount + i1; } return { ...descriptor, positions: outPositions, normals: outNormals, tangents: outTangents, uvs: outUvs, uvs1: outUvs1, indices: outIndices }; } }; // typescript/world/bounds.ts var cloneVec3 = (v) => { return [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0]; }; var emptyBounds = (partial = false) => { return { boxMin: [0, 0, 0], boxMax: [0, 0, 0], sphereCenter: [0, 0, 0], sphereRadius: 0, empty: true, partial }; }; var cloneBounds = (bounds) => { return { boxMin: cloneVec3(bounds.boxMin), boxMax: cloneVec3(bounds.boxMax), sphereCenter: cloneVec3(bounds.sphereCenter), sphereRadius: bounds.sphereRadius, empty: bounds.empty, partial: bounds.partial }; }; var boundsFromBox = (boxMin, boxMax, partial = false) => { const min = cloneVec3(boxMin); const max = cloneVec3(boxMax); const cx = (min[0] + max[0]) * 0.5; const cy = (min[1] + max[1]) * 0.5; const cz = (min[2] + max[2]) * 0.5; const ex = max[0] - cx; const ey = max[1] - cy; const ez = max[2] - cz; return { boxMin: min, boxMax: max, sphereCenter: [cx, cy, cz], sphereRadius: Math.sqrt(ex * ex + ey * ey + ez * ez), empty: false, partial }; }; var boundsFromSphere = (center, radius, partial = false) => { const c = cloneVec3(center); const r = Math.max(0, radius); return { boxMin: [c[0] - r, c[1] - r, c[2] - r], boxMax: [c[0] + r, c[1] + r, c[2] + r], sphereCenter: c, sphereRadius: r, empty: false, partial }; }; var boundsFromBoxAndSphere = (boxMin, boxMax, sphereCenter, sphereRadius, partial = false) => { return { boxMin: cloneVec3(boxMin), boxMax: cloneVec3(boxMax), sphereCenter: cloneVec3(sphereCenter), sphereRadius: Math.max(0, sphereRadius), empty: false, partial }; }; var normalizeBounds = (source) => { if ("getBounds" in source && typeof source.getBounds === "function") return source.getBounds(); return source; }; var unionBounds = (a, b) => { if (a.empty) { const out = cloneBounds(b); out.partial = a.partial || b.partial; return out; } if (b.empty) { const out = cloneBounds(a); out.partial = a.partial || b.partial; return out; } return boundsFromBox([Math.min(a.boxMin[0], b.boxMin[0]), Math.min(a.boxMin[1], b.boxMin[1]), Math.min(a.boxMin[2], b.boxMin[2])], [Math.max(a.boxMax[0], b.boxMax[0]), Math.max(a.boxMax[1], b.boxMax[1]), Math.max(a.boxMax[2], b.boxMax[2])], a.partial || b.partial); }; var getBoundsCenter = (bounds) => { if (bounds.empty) return [0, 0, 0]; return [(bounds.boxMin[0] + bounds.boxMax[0]) * 0.5, (bounds.boxMin[1] + bounds.boxMax[1]) * 0.5, (bounds.boxMin[2] + bounds.boxMax[2]) * 0.5]; }; var getBoundsSize = (bounds) => { if (bounds.empty) return [0, 0, 0]; return [bounds.boxMax[0] - bounds.boxMin[0], bounds.boxMax[1] - bounds.boxMin[1], bounds.boxMax[2] - bounds.boxMin[2]]; }; var expandBounds = (bounds, padding) => { if (bounds.empty) return cloneBounds(bounds); const scale = Math.max(1, padding); const center = getBoundsCenter(bounds); const ex = (bounds.boxMax[0] - bounds.boxMin[0]) * 0.5 * scale; const ey = (bounds.boxMax[1] - bounds.boxMin[1]) * 0.5 * scale; const ez = (bounds.boxMax[2] - bounds.boxMin[2]) * 0.5 * scale; return boundsFromBox([center[0] - ex, center[1] - ey, center[2] - ez], [center[0] + ex, center[1] + ey, center[2] + ez], bounds.partial); }; var getBoundsCorners = (bounds) => { if (bounds.empty) return []; const min = bounds.boxMin; const max = bounds.boxMax; return [[min[0], min[1], min[2]], [max[0], min[1], min[2]], [min[0], max[1], min[2]], [max[0], max[1], min[2]], [min[0], min[1], max[2]], [max[0], min[1], max[2]], [min[0], max[1], max[2]], [max[0], max[1], max[2]]]; }; var transformBounds = (bounds, matrix) => { if (bounds.empty) return cloneBounds(bounds); const cx = (bounds.boxMin[0] + bounds.boxMax[0]) * 0.5; const cy = (bounds.boxMin[1] + bounds.boxMax[1]) * 0.5; const cz = (bounds.boxMin[2] + bounds.boxMax[2]) * 0.5; const ex = (bounds.boxMax[0] - bounds.boxMin[0]) * 0.5; const ey = (bounds.boxMax[1] - bounds.boxMin[1]) * 0.5; const ez = (bounds.boxMax[2] - bounds.boxMin[2]) * 0.5; const tcx = matrix[0] * cx + matrix[4] * cy + matrix[8] * cz + matrix[12]; const tcy = matrix[1] * cx + matrix[5] * cy + matrix[9] * cz + matrix[13]; const tcz = matrix[2] * cx + matrix[6] * cy + matrix[10] * cz + matrix[14]; const tex = Math.abs(matrix[0]) * ex + Math.abs(matrix[4]) * ey + Math.abs(matrix[8]) * ez; const tey = Math.abs(matrix[1]) * ex + Math.abs(matrix[5]) * ey + Math.abs(matrix[9]) * ez; const tez = Math.abs(matrix[2]) * ex + Math.abs(matrix[6]) * ey + Math.abs(matrix[10]) * ez; const sx = Math.hypot(matrix[0], matrix[1], matrix[2]); const sy = Math.hypot(matrix[4], matrix[5], matrix[6]); const sz = Math.hypot(matrix[8], matrix[9], matrix[10]); const smax = Math.max(sx, sy, sz); const scx = matrix[0] * bounds.sphereCenter[0] + matrix[4] * bounds.sphereCenter[1] + matrix[8] * bounds.sphereCenter[2] + matrix[12]; const scy = matrix[1] * bounds.sphereCenter[0] + matrix[5] * bounds.sphereCenter[1] + matrix[9] * bounds.sphereCenter[2] + matrix[13]; const scz = matrix[2] * bounds.sphereCenter[0] + matrix[6] * bounds.sphereCenter[1] + matrix[10] * bounds.sphereCenter[2] + matrix[14]; return boundsFromBoxAndSphere([tcx - tex, tcy - tey, tcz - tez], [tcx + tex, tcy + tey, tcz + tez], [scx, scy, scz], bounds.sphereRadius * smax, bounds.partial); }; // typescript/world/mesh.ts var meshMorphRuntimes = /* @__PURE__ */ new WeakMap(); var resolveWeights = (weights, targetCount) => { const out = new Float32Array(targetCount); if (!weights) return out; const count = Math.min(targetCount, weights.length | 0); for (let i = 0; i < count; i++) out[i] = Number(weights[i] ?? 0) || 0; return out; }; var updateMeshMorphCPUState = (runtime, geometry) => { const sourceRevision = geometry.morphBaseRevision; const sourceChanged = runtime.sourceRevision !== sourceRevision; if (!runtime.dirty && !sourceChanged) return false; runtime.positions.set(geometry.getMorphBaseChannel("positions")); runtime.colors.set(geometry.getMorphBaseChannel("colors")); runtime.positionDirty = sourceChanged || runtime.hasPositionTargets; runtime.normalDirty = sourceChanged || (runtime.recomputeNormals ? runtime.hasPositionTargets : runtime.hasNormalTargets); runtime.colorDirty = sourceChanged || runtime.hasColorTargets; for (let i = 0; i < runtime.targetCount; i++) { const weight = runtime.weights[i] ?? 0; if (weight === 0) continue; const target = geometry.morphTargets[i]; const pos = target?.positions; if (pos) for (let j = 0; j < pos.length; j++) runtime.positions[j] += pos[j] * weight; const colors = target?.colors; if (colors) for (let j = 0; j < colors.length; j++) runtime.colors[j] += colors[j] * weight; } if (runtime.recomputeNormals) runtime.normals.set(computeGeometryVertexNormals(runtime.positions, geometry.getMorphIndices())); else if (runtime.hasNormalTargets) { runtime.normals.set(geometry.getMorphBaseChannel("normals")); for (let i = 0; i < runtime.targetCount; i++) { const weight = runtime.weights[i] ?? 0; if (weight === 0) continue; const target = geometry.morphTargets[i]; const normals = target?.normals; if (!normals) continue; for (let j = 0; j < normals.length; j++) runtime.normals[j] += normals[j] * weight; } } else runtime.normals.set(geometry.getMorphBaseChannel("normals")); if (runtime.hasColorTargets) for (let i = 0; i < runtime.colors.length; i++) { const value = runtime.colors[i] ?? 0; runtime.colors[i] = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0; } const bounds = computeGeometryBounds(runtime.positions); runtime.boundsMin = bounds.boxMin; runtime.boundsMax = bounds.boxMax; runtime.boundsCenter = bounds.sphereCenter; runtime.boundsRadius = bounds.sphereRadius; runtime.dirty = false; runtime.sourceRevision = sourceRevision; if (sourceChanged) runtime.revision++; runtime.gpuDirty = runtime.positionDirty || runtime.normalDirty || runtime.colorDirty; return true; }; var destroyMeshMorphBuffers = (runtime) => { runtime.positionBuffer?.destroy(); runtime.normalBuffer?.destroy(); runtime.colorBuffer?.destroy(); runtime.positionBuffer = null; runtime.normalBuffer = null; runtime.colorBuffer = null; runtime.device = null; }; var initializeMeshMorphRuntime = (mesh, weights) => { const targetCount = mesh.geometry.morphTargets.length | 0; if (targetCount <= 0) return; const runtime = { revision: 1, targetCount, weights: resolveWeights(weights, targetCount), sourceRevision: mesh.geometry.morphBaseRevision, positions: new Float32Array(mesh.geometry.getMorphBaseChannel("positions")), normals: new Float32Array(mesh.geometry.getMorphBaseChannel("normals")), colors: new Float32Array(mesh.geometry.getMorphBaseChannel("colors")), device: null, positionBuffer: null, normalBuffer: null, colorBuffer: null, dirty: true, gpuDirty: true, positionDirty: true, normalDirty: true, colorDirty: true, hasPositionTargets: mesh.geometry.morphTargets.some((target) => !!target.positions), hasNormalTargets: mesh.geometry.morphTargets.some((target) => !!target.normals), hasColorTargets: mesh.geometry.morphTargets.some((target) => !!target.colors), recomputeNormals: !mesh.geometry.authoredNormals && mesh.geometry.morphTargets.some((target) => !!target.positions), boundsMin: mesh.geometry.boundsMin, boundsMax: mesh.geometry.boundsMax, boundsCenter: mesh.geometry.boundsCenter, boundsRadius: mesh.geometry.boundsRadius }; meshMorphRuntimes.set(mesh, runtime); }; var copyMeshMorphRuntime = (source, target) => { const runtime = meshMorphRuntimes.get(source); if (!runtime) return; initializeMeshMorphRuntime(target, runtime.weights); }; var destroyMeshMorphRuntime = (mesh) => { const runtime = meshMorphRuntimes.get(mesh); if (!runtime) return; destroyMeshMorphBuffers(runtime); meshMorphRuntimes.delete(mesh); }; var hasMeshMorphRuntime = (mesh) => { return meshMorphRuntimes.has(mesh); }; var setMeshMorphWeights = (mesh, weights) => { const runtime = meshMorphRuntimes.get(mesh); if (!runtime) return; const next = resolveWeights(weights, runtime.targetCount); let changed = false; for (let i = 0; i < runtime.targetCount; i++) if (runtime.weights[i] !== next[i]) { changed = true; break; } if (!changed) return; runtime.weights.set(next); runtime.dirty = true; runtime.revision++; }; var setMeshMorphWeight = (mesh, index, weight) => { const runtime = meshMorphRuntimes.get(mesh); if (!runtime) return; const slot = index | 0; if (slot < 0 || slot >= runtime.targetCount) return; const next = Number(weight) || 0; if (runtime.weights[slot] === next) return; runtime.weights[slot] = next; runtime.dirty = true; runtime.revision++; }; var getMeshMorphRevision = (mesh) => meshMorphRuntimes.get(mesh)?.revision ?? 0; var getMeshLocalBoundsSource = (mesh) => { const runtime = meshMorphRuntimes.get(mesh); if (!runtime) return mesh.geometry; updateMeshMorphCPUState(runtime, mesh.geometry); return runtime; }; var getMeshVertexSource = (mesh) => { return meshMorphRuntimes.has(mesh) ? mesh : mesh.geometry; }; var getMeshVertexBuffers = (mesh, device, queue) => { const runtime = meshMorphRuntimes.get(mesh); if (!runtime) { mesh.geometry.upload(device); return { positionBuffer: mesh.geometry.positionBuffer, normalBuffer: mesh.geometry.normalBuffer, colorBuffer: mesh.geometry.colorBuffer }; } const updated = updateMeshMorphCPUState(runtime, mesh.geometry); if (runtime.device !== device || !runtime.positionBuffer || !runtime.normalBuffer || !runtime.colorBuffer) { destroyMeshMorphBuffers(runtime); runtime.positionBuffer = createBuffer(device, runtime.positions, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST); runtime.normalBuffer = createBuffer(device, runtime.normals, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST); runtime.colorBuffer = createBuffer(device, runtime.colors, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST); runtime.device = device; runtime.positionDirty = false; runtime.normalDirty = false; runtime.colorDirty = false; runtime.gpuDirty = false; } else if (updated || runtime.gpuDirty) { if (runtime.positionDirty) queue.writeBuffer(runtime.positionBuffer, 0, runtime.positions.buffer, runtime.positions.byteOffset, runtime.positions.byteLength); if (runtime.normalDirty) queue.writeBuffer(runtime.normalBuffer, 0, runtime.normals.buffer, runtime.normals.byteOffset, runtime.normals.byteLength); if (runtime.colorDirty) queue.writeBuffer(runtime.colorBuffer, 0, runtime.colors.buffer, runtime.colors.byteOffset, runtime.colors.byteLength); runtime.positionDirty = false; runtime.normalDirty = false; runtime.colorDirty = false; runtime.gpuDirty = false; } return { positionBuffer: runtime.positionBuffer, normalBuffer: runtime.normalBuffer, colorBuffer: runtime.colorBuffer }; }; var Mesh = class _Mesh { geometry; transform; _material; _visible = true; _castShadow = true; _receiveShadow = true; _destroyed = false; name = ""; userData = {}; skin = null; constructor(geometry, material) { this.geometry = geometry; this._material = material; this.transform = new Transform(); } get material() { return this._material; } get destroyed() { return this._destroyed; } get visible() { return this._visible; } set visible(value) { this._visible = value; } get castShadow() { return this._castShadow; } set castShadow(value) { this._castShadow = value; } get receiveShadow() { return this._receiveShadow; } set receiveShadow(value) { this._receiveShadow = value; } assertAlive(action) { if (this._destroyed) throw new Error(`Mesh: cannot ${action}; mesh has already been destroyed.`); } setMaterial(material) { this.assertAlive("set material"); if (material === this._material) return this; const previous = this._material; this._material = material; previous.release(); return this; } setParent(parent) { this.assertAlive("set parent"); this.transform.setParent(parent?.transform ?? null); return this; } addChild(child) { this.assertAlive("add child"); this.transform.addChild(child.transform); return this; } removeChild(child) { this.assertAlive("remove child"); this.transform.removeChild(child.transform); return this; } get worldMatrix() { this.assertAlive("access world matrix"); return this.transform.worldMatrix; } getLocalBounds() { const bounds = this.getLocalBoundsSource(); return boundsFromBoxAndSphere(bounds.boundsMin, bounds.boundsMax, bounds.boundsCenter, bounds.boundsRadius); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } destroy() { if (this._destroyed) return; this._destroyed = true; detachMeshFromSceneOwners(this); destroyMeshMorphRuntime(this); this.skin?.dispose(); this.skin = null; this.transform.dispose(); this.geometry.release(); this._material.release(); } clone() { this.assertAlive("clone"); this.geometry.retain(); this.material.retain(); const mesh = new _Mesh(this.geometry, this.material); mesh.transform.copyFrom(this.transform); mesh.name = this.name; mesh.visible = this.visible; mesh.castShadow = this.castShadow; mesh.receiveShadow = this.receiveShadow; copyMeshMorphRuntime(this, mesh); return mesh; } cloneWithMaterial(material) { this.assertAlive("clone with material"); this.geometry.retain(); const mesh = new _Mesh(this.geometry, material); mesh.transform.copyFrom(this.transform); mesh.name = this.name; mesh.visible = this.visible; mesh.castShadow = this.castShadow; mesh.receiveShadow = this.receiveShadow; copyMeshMorphRuntime(this, mesh); return mesh; } getLocalBoundsSource() { return getMeshLocalBoundsSource(this); } }; var meshSceneOwners = /* @__PURE__ */ new WeakMap(); var registerMeshSceneOwner = (mesh, owner) => { let owners = meshSceneOwners.get(mesh); if (!owners) { owners = /* @__PURE__ */ new Set(); meshSceneOwners.set(mesh, owners); } owners.add(owner); }; var unregisterMeshSceneOwner = (mesh, owner) => { const owners = meshSceneOwners.get(mesh); if (!owners) return; owners.delete(owner); if (owners.size === 0) meshSceneOwners.delete(mesh); }; var detachMeshFromSceneOwners = (mesh) => { const owners = meshSceneOwners.get(mesh); if (!owners) return; for (const owner of [...owners]) owner.remove(mesh); }; // typescript/graphics/colormap.ts var BUILTIN_RESOLUTION = 256; var BUILTIN_RGBA8_BASE64 = { grayscale: "AAAA/wEBAf8CAgL/AwMD/wQEBP8FBQX/BgYG/wcHB/8ICAj/CQkJ/woKCv8LCwv/DAwM/w0NDf8ODg7/Dw8P/xAQEP8RERH/EhIS/xMTE/8UFBT/FRUV/xYWFv8XFxf/GBgY/xkZGf8aGhr/Gxsb/xwcHP8dHR3/Hh4e/x8fH/8gICD/ICAg/yIiIv8jIyP/JCQk/yQkJP8mJib/Jycn/ygoKP8oKCj/Kioq/ysrK/8sLCz/LCws/y4uLv8vLy//MDAw/zAwMP8yMjL/MzMz/zQ0NP80NDT/NjY2/zc3N/84ODj/ODg4/zo6Ov87Ozv/PDw8/zw8PP8+Pj7/Pz8//0BAQP9BQUH/QUFB/0NDQ/9ERET/RUVF/0ZGRv9HR0f/SEhI/0lJSf9JSUn/S0tL/0xMTP9NTU3/Tk5O/09PT/9QUFD/UVFR/1FRUf9TU1P/VFRU/1VVVf9WVlb/V1dX/1hYWP9ZWVn/WVlZ/1tbW/9cXFz/XV1d/15eXv9fX1//YGBg/2FhYf9hYWH/Y2Nj/2RkZP9lZWX/ZmZm/2dnZ/9oaGj/aWlp/2lpaf9ra2v/bGxs/21tbf9ubm7/b29v/3BwcP9xcXH/cXFx/3Nzc/90dHT/dXV1/3Z2dv93d3f/eHh4/3l5ef95eXn/e3t7/3x8fP99fX3/fn5+/39/f/+AgID/gYGB/4KCgv+Dg4P/g4OD/4WFhf+Ghob/h4eH/4iIiP+JiYn/ioqK/4uLi/+MjIz/jY2N/46Ojv+Pj4//kJCQ/5GRkf+SkpL/k5OT/5OTk/+VlZX/lpaW/5eXl/+YmJj/mZmZ/5qamv+bm5v/nJyc/52dnf+enp7/n5+f/6CgoP+hoaH/oqKi/6Ojo/+jo6P/paWl/6ampv+np6f/qKio/6mpqf+qqqr/q6ur/6ysrP+tra3/rq6u/6+vr/+wsLD/sbGx/7Kysv+zs7P/s7Oz/7W1tf+2trb/t7e3/7i4uP+5ubn/urq6/7u7u/+8vLz/vb29/76+vv+/v7//wMDA/8HBwf/CwsL/w8PD/8PDw//FxcX/xsbG/8fHx//IyMj/ycnJ/8rKyv/Ly8v/zMzM/83Nzf/Ozs7/z8/P/9DQ0P/R0dH/0tLS/9PT0//T09P/1dXV/9bW1v/X19f/2NjY/9nZ2f/a2tr/29vb/9zc3P/d3d3/3t7e/9/f3//g4OD/4eHh/+Li4v/j4+P/4+Pj/+Xl5f/m5ub/5+fn/+jo6P/p6en/6urq/+vr6//s7Oz/7e3t/+7u7v/v7+//8PDw//Hx8f/y8vL/8/Pz//Pz8//19fX/9vb2//f39//4+Pj/+fn5//r6+v/7+/v//Pz8//39/f/+/v7//////w==", turbo: "MBI7/zEVQv8yGEr/NBtR/zUeWP82IV//NyNl/zgmbP85KXL/Oix5/zsvf/88MoX/PDWL/z03kf8+Opb/Pz2c/0BAof9AQ6b/QUWr/0FIsP9CS7X/Q066/0NQvv9DU8L/RFbH/0RYy/9FW87/RV7S/0Vg1v9FY9n/Rmbd/0Zo4P9Ga+P/Rm3m/0Zw6P9Gc+v/RnXt/0Z48P9GevL/Rn30/0Z/9v9Ggvj/RYT5/0WH+/9Fifz/RIz9/0OO/f9Ckf7/QZP+/0CW/v8/mP7/Ppv+/zyd/f87oPz/OaL8/zil+/82qPn/NKr4/zOs9v8xr/X/L7Hz/y208f8rtu//Krnt/yi76/8mven/JcDm/yPC5P8hxOH/IMbf/x7J3P8dy9r/HM3X/xvP1P8a0dL/GdPP/xjVzP8Y18r/F9nH/xfaxP8X3ML/F96//xjgvf8Y4br/GeO4/xrktv8b5bT/Heex/x7or/8g6az/Iuup/yTspv8n7aP/Ke6g/yzvnf8v8Jr/MvGX/zXzlP849JH/O/SN/z/1iv9C9of/RveD/0r4gP9N+Xz/Ufl5/1X6dv9Z+3L/Xftv/2H8bP9l/Gj/af1l/239Yv9x/V//dP5c/3j+Wf98/lb/gP5T/4T+UP+H/k3/i/5L/47+SP+S/kb/lf5E/5j+Qv+b/UD/nv0+/6H8Pf+k/Dv/pvs6/6n7Of+s+jf/rvk3/7H4Nv+z+DX/tvc1/7n1NP+79DT/vvM0/8DyM//D8TP/xe8z/8juM//K7TP/zes0/8/qNP/R6DT/1Oc1/9blNf/Y4zX/2uI2/93gNv/f3jb/4dw3/+PaN//l2Dj/59c4/+jVOP/q0zn/7NE5/+3POf/vzTn/8Ms6//LIOv/zxjr/9MQ6//bCOv/3wDn/+L45//m8Of/5ujj/+rc3//u1N//7szb//LA1//yuNP/9qzP//aky//2mMf/9ozD//qEv//6eLv/+my3//pgs//2VK//9kin//Y8o//2MJ//8iSb//IYk//uDI//7gCL/+n0g//p6H//5dx7/+HQc//dxG//3bhr/9msY//VoF//0ZRb/82MV//JgFP/xXRP/71oR/+5YEP/tVQ//7FIO/+pQDf/pTQ3/6EsM/+ZJC//lRgr/40QK/+JCCf/gQAj/3j4I/908B//bOgf/2TgG/9c2Bv/WNAX/1DIF/9IwBf/QLwT/zi0E/8srA//JKQP/xygD/8UmAv/DJAL/wCMC/74hAv+7HwH/uR4B/7YcAf+0GwH/sRkB/64YAf+sFgH/qRUB/6YUAf+jEgH/oBEB/50QAf+aDgH/lw0B/5QMAf+RCwH/jgoB/4sJAf+HCAH/hAcB/4EGAv99BQL/egQC/w==", viridis: "RAFU/0QCVf9EA1f/RQVY/0UGWv9FCFv/Rglc/0YLXv9GDF//Rg5h/0cPYv9HEWP/RxJl/0cUZv9HFWf/RxZp/0cYav9IGWv/SBps/0gcbv9IHW//SB5w/0ggcf9IIXL/SCJz/0gjdP9HJXX/RyZ2/0cnd/9HKHj/Ryp5/0crev9HLHv/Ri18/0YvfP9GMH3/RjF+/0Uyf/9FNH//RTWA/0U2gf9EN4H/RDmC/0M6g/9DO4P/QzyE/0I9hP9CPoX/QkCF/0FBhv9BQob/QEOH/0BEh/8/RYf/P0eI/z5IiP8+SYn/PUqJ/z1Lif89TIn/PE2K/zxOiv87UIr/O1GK/zpSi/86U4v/OVSL/zlVi/84Vov/OFeM/zdYjP83WYz/NlqM/zZbjP81XIz/NV2M/zRejf80X43/M2CN/zNhjf8yYo3/MmON/zFkjf8xZY3/MWaN/zBnjf8waI3/L2mN/y9qjf8ua47/LmyO/y5tjv8tbo7/LW+O/yxwjv8scY7/LHKO/ytzjv8rdI7/KnWO/yp2jv8qd47/KXiO/yl5jv8oeo7/KHqO/yh7jv8nfI7/J32O/yd+jv8mf47/JoCO/yaBjv8lgo7/JYON/ySEjf8khY3/JIaN/yOHjf8jiI3/I4mN/yKJjf8iio3/IouN/yGMjf8hjYz/IY6M/yCPjP8gkIz/IJGM/x+SjP8fk4v/H5SL/x+Vi/8flov/HpeK/x6Yiv8emYr/HpmK/x6aif8em4n/HpyJ/x6diP8enoj/Hp+I/x6gh/8foYf/H6KG/x+jhv8gpIX/IKWF/yGmhf8hp4T/IqeE/yOog/8jqYL/JKqC/yWrgf8mrIH/J62A/yiuf/8pr3//KrB+/yuxff8ssX3/LrJ8/y+ze/8wtHr/MrV6/zO2ef81t3j/Nrh3/zi5dv85uXb/O7p1/z27dP8+vHP/QL1y/0K+cf9EvnD/Rb9v/0fAbv9JwW3/S8Js/03Ca/9Pw2n/UcRo/1PFZ/9Vxmb/V8Zl/1nHZP9byGL/Xslh/2DJYP9iyl//ZMtd/2fMXP9pzFv/a81Z/23OWP9wzlb/cs9V/3TQVP930FL/edFR/3zST/9+0k7/gdNM/4PTS/+G1En/iNVH/4vVRv+N1kT/kNZD/5LXQf+V1z//l9g+/5rYPP+d2Tr/n9k4/6LaN/+l2jX/p9sz/6rbMv+t3DD/r9wu/7LdLP+13Sv/t90p/7reJ/+93ib/v98k/8LfIv/F3yH/x+Af/8rgHv/N4B3/z+Ec/9LhG//U4Rr/1+IZ/9riGP/c4hj/3+MY/+HjGP/k4xj/5+QZ/+nkGf/s5Br/7uUb//HlHP/z5R7/9uYf//jmIf/65iL//eck/w==", magma: "AAAD/wAABP8AAAb/AQAH/wEBCf8BAQv/AgIN/wICD/8DAxH/BAMT/wQEFf8FBBf/BgUZ/wcFG/8IBh3/CQcf/woHIv8LCCT/DAkm/w0KKP8OCir/Dwss/xAML/8RDDH/Eg0z/xQNNf8VDjj/Fg46/xcPPP8YDz//GhBB/xsQRP8cEEb/HhBJ/x8RS/8gEU3/IhFQ/yMRUv8lEVX/JhFX/ygRWf8qEVz/KxFe/y0QYP8vEGL/MBBl/zIQZ/80EGj/NQ9q/zcPbP85D27/Ow9v/zwPcf8+D3L/QA9z/0IPdP9DD3X/RQ92/0cPd/9IEHj/ShB5/0sQef9NEXr/TxF7/1ASe/9SEnz/UxN8/1UTff9XFH3/WBV+/1oVfv9bFn7/XRd+/14Xf/9gGH//YRh//2MZf/9lGoD/ZhqA/2gbgP9pHID/axyA/2wdgP9uHoH/bx6B/3Efgf9zH4H/dCCB/3Yhgf93IYH/eSKB/3oigf98I4H/fiSB/38kgf+BJYH/giWB/4Qmgf+FJoH/hyeB/4kogf+KKIH/jCmA/40pgP+PKoD/kSqA/5IrgP+UK4D/lSyA/5csf/+ZLX//mi1//5wuf/+eLn7/ny9+/6Evfv+jMH7/pDB9/6Yxff+nMX3/qTJ8/6szfP+sM3v/rjR7/7A0e/+xNXr/szV6/7U2ef+2Nnn/uDd4/7k3eP+7OHf/vTl3/745dv/AOnX/wjp1/8M7dP/FPHT/xjxz/8g9cv/KPnL/yz5x/80/cP/OQHD/0EFv/9FCbv/TQm3/1ENt/9ZEbP/XRWv/2UZq/9pHaf/cSGn/3Ulo/95KZ//gS2b/4Uxm/+JNZf/kTmT/5VBj/+ZRYv/nUmL/6FRh/+pVYP/rVmD/7Fhf/+1ZX//uW17/7l1d/+9eXf/wYF3/8WFc//JjXP/zZVz/82db//RoW//1alv/9Wxb//ZuW//2cFv/93Fb//dzXP/4dVz/+Hdc//l5XP/5e13/+X1d//p/Xv/6gF7/+oJf//uEYP/7hmD/+4hh//uKYv/8jGP//I5j//yQZP/8kmX//JNm//2VZ//9l2j//Zlp//2bav/9nWv//Z9s//2hbv/9om///aRw//6mcf/+qHP//qp0//6sdf/+rnb//q94//6xef/+s3v//rV8//63ff/+uX///ruA//68gv/+voP//sCF//7Chv/+xIj//saJ//7Hi//+yY3//suO//3NkP/9z5L//dGT//3Slf/91Jf//daY//3Ymv/92pz//dyd//3dn//936H//eGj//zjpf/85ab//Oao//zoqv/86qz//Oyu//zusP/88LH//PGz//zztf/89bf/+/e5//v5u//7+r3/+/y//w==", plasma: "DAeG/xAHh/8TBon/FQaK/xgGi/8bBoz/HQaN/x8Fjv8hBY//IwWQ/yUFkf8nBZL/KQWT/ysFlP8tBJT/LwSV/zEElv8zBJf/NASY/zYEmP84BJn/OgSa/zsDmv89A5v/PwOc/0ADnP9CA53/RAOe/0UDnv9HAp//SQKf/0oCoP9MAqH/TgKh/08Cov9RAaL/UgGj/1QBo/9WAaP/VwGk/1kBpP9aAKX/XACl/14Apf9fAKb/YQCm/2IApv9kAKf/ZQCn/2cAp/9oAKf/agCn/2wAqP9tAKj/bwCo/3AAqP9yAKj/cwCo/3UAqP92Aaj/eAGo/3kBqP97Aqj/fAKn/34Dp/9/A6f/gQSn/4IEp/+EBab/hQam/4YHpv+IB6X/iQil/4sJpP+MCqT/jgyk/48No/+QDqP/kg+i/5MQof+VEaH/lhKg/5cToP+ZFJ//mhWe/5sXnv+dGJ3/nhmc/58am/+gG5v/ohya/6Mdmf+kHpj/pR+X/6chl/+oIpb/qSOV/6oklP+sJZP/rSaS/64nkf+vKJD/sCqP/7Erj/+yLI7/tC2N/7UujP+2L4v/tzCK/7gyif+5M4j/ujSH/7s1hv+8NoX/vTeE/744g/+/OYL/wDuB/8E8gP/CPYD/wz5//8Q/fv/FQH3/xkF8/8dCe//IRHr/yUV5/8pGeP/LR3f/zEh2/81Jdf/OSnX/z0t0/9BNc//RTnL/0U9x/9JQcP/TUW//1FJu/9VTbf/WVW3/11Zs/9dXa//YWGr/2Vlp/9paaP/bW2f/3F1m/9xeZv/dX2X/3mBk/99hY//fYmL/4GRh/+FlYP/iZmD/42df/+NoXv/kal3/5Wtc/+VsW//mbVr/525a/+hwWf/ocVj/6XJX/+pzVv/qdFX/63ZU/+x3VP/seFP/7XlS/+17Uf/ufFD/731P/+9+Tv/wgE3/8IFN//GCTP/yhEv/8oVK//OGSf/zh0j/9IlH//SKR//1i0b/9Y1F//aORP/2j0P/9pFC//eSQf/3k0H/+JVA//iWP//4mD7/+Zk9//maPP/6nDv/+p06//qfOv/6oDn/+6I4//ujN//7pDb//KY1//ynNf/8qTT//Koz//ysMv/8rTH//a8x//2wMP/9si///bMu//21Lf/9ti3//bgs//25K//9uyv//bwq//2+Kf/9wCn//cEo//3DKP/9xCf//cYm//zHJv/8ySb//Msl//zMJf/8ziX/+9Ak//vRJP/70yT/+tUk//rWJP/62CT/+dkk//nbJP/43ST/+N8k//fgJP/34iX/9uQl//blJf/15yb/9ekm//TqJv/z7Cb/8+4m//LwJv/y8Sb/8fMm//D1Jf/w9iP/7/gh/w==", inferno: "AAAD/wAABP8AAAb/AQAH/wEBCf8BAQv/AgEO/wICEP8DAhL/BAMU/wQDFv8FBBj/BgQb/wcFHf8IBh//CQYh/woHI/8LByb/DQgo/w4IKv8PCS3/EAkv/xIKMv8TCjT/FAs2/xYLOf8XCzv/GQs+/xoLQP8cDEP/HQxF/x8MR/8gDEr/IgtM/yQLTv8mC1D/JwtS/ykLVP8rClb/LQpY/y4KWv8wClz/Mgld/zQJX/81CWD/Nwlh/zkJYv87CWT/PAll/z4JZv9ACWb/QQln/0MKaP9FCmn/Rgpp/0gLav9KC2r/Swxr/00Ma/9PDWz/UA1s/1IObP9TDm3/VQ9t/1cPbf9YEG3/WhFt/1sRbv9dEm7/XxJu/2ATbv9iFG7/YxRu/2UVbv9mFW7/aBZu/2oXbv9rF27/bRhu/24Ybv9wGW7/chlt/3Mabf91G23/dhtt/3gcbf96HG3/ex1s/30dbP9+Hmz/gB9r/4Efa/+DIGv/hSBq/4Yhav+IIWr/iSJp/4siaf+NI2n/jiRo/5AkaP+RJWf/kyVn/5UmZv+WJmb/mCdl/5koZP+bKGT/nClj/54pY/+gKmL/oSth/6MrYf+kLGD/pixf/6ctX/+pLl7/qy5d/6wvXP+uMFv/rzFb/7ExWv+yMln/tDNY/7UzV/+3NFb/uDVW/7o2Vf+7N1T/vTdT/744Uv+/OVH/wTpQ/8I7T//EPE7/xT1N/8c+TP/IPkv/yT9K/8tASf/MQUj/zUJH/89ERv/QRUT/0UZD/9JHQv/USEH/1UlA/9ZKP//XSz7/2U09/9pOO//bTzr/3FA5/91SOP/eUzf/31Q2/+BWNP/iVzP/41gy/+RaMf/lWzD/5lwu/+ZeLf/nXyz/6GEr/+liKv/qZCj/62Un/+xnJv/taCX/7Woj/+5sIv/vbSH/8G8f//BwHv/xch3/8nQc//J1Gv/zdxn/83kY//R6Fv/1fBX/9X4U//aAEv/2gRH/94MQ//eFDv/4hw3/+IgM//iKC//5jAn/+Y4I//mQCP/6kQf/+pMG//qVBv/6lwb/+5kG//ubBv/7nQb/+54H//ugB//7ogj/+6QK//umC//7qA3/+6oO//usEP/7rhL/+7AU//uxFv/7sxj/+7Ua//u3HP/7uR7/+rsh//q9I//6vyX/+sEo//nDKv/5xSz/+ccv//jJMf/4yzT/+M03//fPOv/30Tz/9tM///bVQv/110X/9dlI//TbS//03E//895S//PgVv/z4ln/8uRd//LmYP/x6GT/8elo//HrbP/x7XD/8e50//Hwef/x8n3/8vOB//L0hf/z9on/9PeN//X4kf/2+pX/9/uZ//n8nf/6/aD//P6k/w==" }; var srgbToLinearChannel = (c) => { if (c <= 0.04045) return c / 12.92; return Math.pow((c + 0.055) / 1.055, 2.4); }; var decodeBase64ToU8 = (b64) => { if (typeof globalThis.atob === "function") { const bin = globalThis.atob(b64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i) & 255; return out; } const B = globalThis.Buffer; if (B && typeof B.from === "function") return Uint8Array.from(B.from(b64, "base64")); throw new Error("Colormap: No base64 decoder available (expected atob() or Buffer)."); }; var ensureBuiltinRGBA8Linear = (name) => { const anyMap = BUILTIN_RGBA8_BASE64; const cached = anyMap[name]; if (cached instanceof Uint8Array) return cached; const decoded = decodeBase64ToU8(BUILTIN_RGBA8_BASE64[name]); anyMap[name] = decoded; return decoded; }; var normalizeStops = (stops) => { assert(stops.length >= 2, "Colormap: expected at least 2 stops."); const out = []; let implicitIndex = 0; for (const s of stops) { if (Array.isArray(s)) { out.push({ t: stops.length <= 1 ? 0 : implicitIndex / (stops.length - 1), color: [s[0], s[1], s[2], s[3]] }); implicitIndex++; } else out.push({ t: s.t, color: [s.color[0], s.color[1], s.color[2], s.color[3]] }); } out.sort((a, b) => a.t - b.t); for (const s of out) s.t = clamp01(s.t); if (out[0].t > 0) out.unshift({ t: 0, color: out[0].color }); const last = out.length - 1; if (out[last].t < 1) out.push({ t: 1, color: out[last].color }); return out; }; var sampleStopsLinear = (stops, t) => { const x = clamp01(t); if (x <= stops[0].t) return stops[0].color; const last = stops.length - 1; if (x >= stops[last].t) return stops[last].color; for (let i = 0; i < last; i++) { const a = stops[i]; const b = stops[i + 1]; if (x >= a.t && x <= b.t) { const denom = b.t - a.t || 1e-6; const u = (x - a.t) / denom; return [ lerp(a.color[0], b.color[0], u), lerp(a.color[1], b.color[1], u), lerp(a.color[2], b.color[2], u), lerp(a.color[3], b.color[3], u) ]; } } return stops[last].color; }; var toRGBA8Linear = (colors, colorSpace) => { const out = new Uint8Array(colors.length * 4); for (let i = 0; i < colors.length; i++) { let r = clamp01(colors[i][0]); let g = clamp01(colors[i][1]); let b = clamp01(colors[i][2]); const a = clamp01(colors[i][3]); if (colorSpace === "srgb") { r = srgbToLinearChannel(r); g = srgbToLinearChannel(g); b = srgbToLinearChannel(b); } out[i * 4 + 0] = Math.max(0, Math.min(255, Math.round(r * 255))); out[i * 4 + 1] = Math.max(0, Math.min(255, Math.round(g * 255))); out[i * 4 + 2] = Math.max(0, Math.min(255, Math.round(b * 255))); out[i * 4 + 3] = Math.max(0, Math.min(255, Math.round(a * 255))); } return out; }; var sampleRGBA8Nearest = (rgba8, width, t) => { const x = Math.min(width - 1, Math.max(0, Math.round(clamp01(t) * (width - 1)))); return [ rgba8[x * 4 + 0] / 255, rgba8[x * 4 + 1] / 255, rgba8[x * 4 + 2] / 255, rgba8[x * 4 + 3] / 255 ]; }; var sampleRGBA8Linear = (rgba8, width, t) => { const tx = clamp01(t) * Math.max(0, width - 1); const i0 = Math.min(width - 1, Math.max(0, Math.floor(tx))); const i1 = Math.min(width - 1, i0 + 1); const f = tx - i0; const o0 = i0 * 4; const o1 = i1 * 4; return [ lerp(rgba8[o0 + 0] / 255, rgba8[o1 + 0] / 255, f), lerp(rgba8[o0 + 1] / 255, rgba8[o1 + 1] / 255, f), lerp(rgba8[o0 + 2] / 255, rgba8[o1 + 2] / 255, f), lerp(rgba8[o0 + 3] / 255, rgba8[o1 + 3] / 255, f) ]; }; var createTexture1DFromRGBA8 = (device, queue, rgba8, width, label) => { assert(width > 0, "Colormap: width must be > 0."); assert(rgba8.length >>> 0 === width * 4, "Colormap: rgba8 length must be width*4."); const texture = device.createTexture({ label, size: { width, height: 1, depthOrArrayLayers: 1 }, dimension: "1d", format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST }); const bytesPerRowUnaligned = width * 4; const bytesPerRow = alignTo(bytesPerRowUnaligned, 256); const data = bytesPerRow === bytesPerRowUnaligned ? rgba8 : (() => { const padded = new Uint8Array(bytesPerRow); padded.set(rgba8); return padded; })(); queue.writeTexture( { texture }, new Uint8Array(data), { bytesPerRow, rowsPerImage: 1 }, { width, height: 1, depthOrArrayLayers: 1 } ); return texture; }; var createSampler = (device, filter) => { return device.createSampler({ addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge", addressModeW: "clamp-to-edge", magFilter: filter === "linear" ? "linear" : "nearest", minFilter: filter === "linear" ? "linear" : "nearest", mipmapFilter: "nearest" }); }; var _nextColormapId = 1; var Colormap = class _Colormap { id = _nextColormapId++; _label; _filter; _width; _rgba8Linear; _external; _gpuByDevice = /* @__PURE__ */ new WeakMap(); constructor(opts) { this._label = opts.label; this._width = opts.width; this._filter = opts.filter; this._rgba8Linear = opts.rgba8Linear ?? null; this._external = opts.external ?? null; } static builtin(name) { return BUILTIN_SINGLETONS[name]; } static fromStops(stops, desc = {}) { const resolution = Math.max(2, Math.floor(desc.resolution ?? BUILTIN_RESOLUTION)); const filter = desc.filter ?? "linear"; const colorSpace = desc.colorSpace ?? "srgb"; const normalized = normalizeStops(stops); const samples = new Array(resolution); for (let i = 0; i < resolution; i++) { const t = resolution === 1 ? 0 : i / (resolution - 1); samples[i] = sampleStopsLinear(normalized, t); } const rgba8Linear = toRGBA8Linear(samples, colorSpace); return new _Colormap({ label: "Colormap.customStops", width: resolution, filter, rgba8Linear }); } static fromPalette(colors, desc = {}) { assert(colors.length >= 1, "Colormap.fromPalette: expected at least 1 color."); const filter = desc.filter ?? "nearest"; const colorSpace = desc.colorSpace ?? "srgb"; const rgba8Linear = toRGBA8Linear(colors, colorSpace); return new _Colormap({ label: "Colormap.palette", width: colors.length, filter, rgba8Linear }); } static fromGPUTextureView(device, view, sampler, width, filter = "linear") { assert(width > 0, "Colormap.fromGPUTextureView: width must be > 0."); return new _Colormap({ label: "Colormap.external", width, filter, external: { device, view, sampler, width, filter } }); } get width() { return this._width; } get filter() { return this._filter; } get canSampleCPU() { return this._rgba8Linear !== null; } getRGBA8LinearLUT() { if (!this._rgba8Linear) throw new Error("Colormap: CPU sampling is unavailable for external GPU-only colormaps."); return this._rgba8Linear.slice(); } sampleCPU(t) { const rgba8 = this._rgba8Linear; if (!rgba8) throw new Error("Colormap: CPU sampling is unavailable for external GPU-only colormaps."); if (this._filter === "nearest") return sampleRGBA8Nearest(rgba8, this._width, t); return sampleRGBA8Linear(rgba8, this._width, t); } getGPUResources(device, queue) { if (this._external) { assert(this._external.device === device, "Colormap: external texture was created with a different GPUDevice."); return { texture: null, view: this._external.view, sampler: this._external.sampler, width: this._external.width, filter: this._external.filter }; } const cached = this._gpuByDevice.get(device); if (cached) return cached; const rgba8 = this._rgba8Linear ?? (() => { throw new Error("Colormap: no LUT data to upload."); })(); const texture = createTexture1DFromRGBA8(device, queue, rgba8, this._width, this._label); const view = texture.createView({ dimension: "1d" }); const sampler = createSampler(device, this._filter); const res = { texture, view, sampler, width: this._width, filter: this._filter }; this._gpuByDevice.set(device, res); return res; } toUniformStops(maxStops = 8, colorSpace = "linear") { const n = Math.max(2, Math.min(8, Math.floor(maxStops))); const rgba8 = this._rgba8Linear; if (!rgba8) { return [ [0, 0, 0, 1], [1, 1, 1, 1] ]; } const out = new Array(n); for (let i = 0; i < n; i++) { const t = n === 1 ? 0 : i / (n - 1); const x = Math.min(this._width - 1, Math.max(0, Math.round(t * (this._width - 1)))); const r = rgba8[x * 4 + 0] / 255; const g = rgba8[x * 4 + 1] / 255; const b = rgba8[x * 4 + 2] / 255; const a = rgba8[x * 4 + 3] / 255; if (colorSpace === "linear") out[i] = [r, g, b, a]; else { const toSrgb = (v) => { if (v <= 31308e-7) return 12.92 * v; return 1.055 * Math.pow(v, 1 / 2.4) - 0.055; }; out[i] = [toSrgb(r), toSrgb(g), toSrgb(b), a]; } } return out; } }; var BUILTIN_SINGLETONS = { grayscale: new Colormap({ label: "Colormap.grayscale", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("grayscale") }), turbo: new Colormap({ label: "Colormap.turbo", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("turbo") }), viridis: new Colormap({ label: "Colormap.viridis", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("viridis") }), magma: new Colormap({ label: "Colormap.magma", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("magma") }), plasma: new Colormap({ label: "Colormap.plasma", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("plasma") }), inferno: new Colormap({ label: "Colormap.inferno", width: BUILTIN_RESOLUTION, filter: "linear", rgba8Linear: ensureBuiltinRGBA8Linear("inferno") }) }; // wgsl/graphics/unlit.wgsl var unlit_default = "struct MaterialUniforms { color: vec4, params: vec4, base_color_transform0: vec4, base_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(13) color: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) normal: vec3, @location(1) uv: vec2, @location(2) uv1: vec2, @location(3) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_sampler: sampler; @group(1) @binding(2) var base_tex: texture_2d; fn linear_to_srgb(c: vec3) -> vec3 { return pow(c, vec3(1.0 / 2.2)); } fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; out.position = camera.view_projection * model.model * vec4(in.position, 1.0); out.normal = (model.normal_matrix * vec4(in.normal, 0.0)).xyz; out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let tex_color = textureSample(base_tex, base_sampler, base_uv); var out_color = material.color * tex_color * in.color; let alpha_cutoff = material.params.x; if (alpha_cutoff > 0.0 && out_color.a < alpha_cutoff) { discard; } return vec4(linear_to_srgb(out_color.rgb), out_color.a); }"; // wgsl/graphics/unlit-instanced.wgsl var unlit_instanced_default = "struct MaterialUniforms { color: vec4, params: vec4, base_color_transform0: vec4, base_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(13) color: vec4, @location(3) m0: vec4, @location(4) m1: vec4, @location(5) m2: vec4, @location(6) m3: vec4, @location(7) n0: vec4, @location(8) n1: vec4, @location(9) n2: vec4, @location(10) n3: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) normal: vec3, @location(1) uv: vec2, @location(2) uv1: vec2, @location(3) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_sampler: sampler; @group(1) @binding(2) var base_tex: texture_2d; fn linear_to_srgb(c: vec3) -> vec3 { return pow(c, vec3(1.0 / 2.2)); } fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let model_m = mat4x4(in.m0, in.m1, in.m2, in.m3); let normal_m = mat4x4(in.n0, in.n1, in.n2, in.n3); out.position = camera.view_projection * model_m * vec4(in.position, 1.0); out.normal = (normal_m * vec4(in.normal, 0.0)).xyz; out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let tex_color = textureSample(base_tex, base_sampler, base_uv); var out_color = material.color * tex_color * in.color; let alpha_cutoff = material.params.x; if (alpha_cutoff > 0.0 && out_color.a < alpha_cutoff) { discard; } return vec4(linear_to_srgb(out_color.rgb), out_color.a); }"; // wgsl/graphics/unlit-skinned.wgsl var unlit_skinned_default = "struct MaterialUniforms { color: vec4, params: vec4, base_color_transform0: vec4, base_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(13) color: vec4, @location(3) joints: vec4, @location(4) weights: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, @location(1) uv1: vec2, @location(2) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec4, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct SkinBuffer { joints: array>, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_texture: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn linear_to_srgb(c: vec3) -> vec3 { return pow(c, vec3(1.0 / 2.2)); } fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j = in.joints; let w = in.weights; let m = skin.joints[j.x] * w.x + skin.joints[j.y] * w.y + skin.joints[j.z] * w.z + skin.joints[j.w] * w.w; let local_pos = m * vec4(in.position, 1.0); out.position = camera.view_projection * model.model * local_pos; out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let base_color_sample = textureSample(base_color_texture, base_color_sampler, base_uv); var out_color = material.color * base_color_sample * in.color; let alpha_cutoff = material.params.x; if (alpha_cutoff > 0.0 && out_color.a < alpha_cutoff) { discard; } return vec4(linear_to_srgb(out_color.rgb), out_color.a); }"; // wgsl/graphics/unlit-skinned8.wgsl var unlit_skinned8_default = "struct MaterialUniforms { color: vec4, params: vec4, base_color_transform0: vec4, base_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(13) color: vec4, @location(3) joints0: vec4, @location(4) weights0: vec4, @location(5) joints1: vec4, @location(6) weights1: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, @location(1) uv1: vec2, @location(2) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec4, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct SkinBuffer { joints: array>, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_texture: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn linear_to_srgb(c: vec3) -> vec3 { return pow(c, vec3(1.0 / 2.2)); } fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j0 = in.joints0; let w0 = in.weights0; let j1 = in.joints1; let w1 = in.weights1; let m = skin.joints[j0.x] * w0.x + skin.joints[j0.y] * w0.y + skin.joints[j0.z] * w0.z + skin.joints[j0.w] * w0.w + skin.joints[j1.x] * w1.x + skin.joints[j1.y] * w1.y + skin.joints[j1.z] * w1.z + skin.joints[j1.w] * w1.w; let local_pos = m * vec4(in.position, 1.0); out.position = camera.view_projection * model.model * local_pos; out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let base_color_sample = textureSample(base_color_texture, base_color_sampler, base_uv); var out_color = material.color * base_color_sample * in.color; let alpha_cutoff = material.params.x; if (alpha_cutoff > 0.0 && out_color.a < alpha_cutoff) { discard; } return vec4(linear_to_srgb(out_color.rgb), out_color.a); }"; // wgsl/graphics/standard.wgsl var standard_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let world_pos4 = model.model * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(in.normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(in.tangent.xyz, 0.0)).xyz, in.tangent.w, ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let base_contribution = (k_d * albedo / PI + specular_brdf + sheen_brdf) * radiance * n_dot_l; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-defaults.wgsl var standard_defaults_default = "const standard_default_base_color = vec4(1.0); const standard_default_metallic_roughness = vec4(1.0); const standard_default_normal = vec4(0.5, 0.5, 1.0, 1.0); const standard_default_occlusion = vec4(1.0); const standard_default_emissive = vec4(1.0); const standard_default_clearcoat = vec4(1.0); const standard_default_clearcoat_roughness = vec4(1.0); const standard_default_clearcoat_normal = vec4(0.5, 0.5, 1.0, 1.0); const standard_default_specular = vec4(1.0); const standard_default_specular_color = vec4(1.0); const standard_default_sheen_color = vec4(1.0); const standard_default_sheen_roughness = vec4(1.0); const standard_default_iridescence = vec4(1.0); const standard_default_iridescence_thickness = vec4(1.0); const standard_default_anisotropy = vec4(1.0, 0.5, 1.0, 1.0); const standard_default_transmission = vec4(1.0); const standard_default_volume_thickness = vec4(1.0); const standard_default_diffuse_transmission = vec4(1.0); const standard_default_diffuse_transmission_color = vec4(1.0);"; // wgsl/graphics/standard-instanced.wgsl var standard_instanced_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) m0: vec4, @location(4) m1: vec4, @location(5) m2: vec4, @location(6) m3: vec4, @location(7) n0: vec4, @location(8) n1: vec4, @location(9) n2: vec4, @location(10) n3: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let model_m = mat4x4(in.m0, in.m1, in.m2, in.m3); let normal_m = mat4x4(in.n0, in.n1, in.n2, in.n3); let world_pos4 = model_m * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((normal_m * vec4(in.normal, 0.0)).xyz); out.tangent = vec4((normal_m * vec4(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let base_contribution = (k_d * albedo / PI + specular_brdf + sheen_brdf) * radiance * n_dot_l; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-skinned.wgsl var standard_skinned_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) joints: vec4, @location(4) weights: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct SkinBuffer { joints: array>, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j = in.joints; let w = in.weights; let skin_matrix = skin.joints[j.x] * w.x + skin.joints[j.y] * w.y + skin.joints[j.z] * w.z + skin.joints[j.w] * w.w; let local_pos = skin_matrix * vec4(in.position, 1.0); let local_normal = (skin_matrix * vec4(in.normal, 0.0)).xyz; let local_tangent = (skin_matrix * vec4(in.tangent.xyz, 0.0)).xyz; let world_pos4 = model.model * local_pos; out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(local_normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(local_tangent, 0.0)).xyz, in.tangent.w, ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let base_contribution = (k_d * albedo / PI + specular_brdf + sheen_brdf) * radiance * n_dot_l; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-skinned8.wgsl var standard_skinned8_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) joints0: vec4, @location(4) weights0: vec4, @location(5) joints1: vec4, @location(6) weights1: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct SkinBuffer { joints: array>, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j0 = in.joints0; let w0 = in.weights0; let j1 = in.joints1; let w1 = in.weights1; let skin_matrix = skin.joints[j0.x] * w0.x + skin.joints[j0.y] * w0.y + skin.joints[j0.z] * w0.z + skin.joints[j0.w] * w0.w + skin.joints[j1.x] * w1.x + skin.joints[j1.y] * w1.y + skin.joints[j1.z] * w1.z + skin.joints[j1.w] * w1.w; let local_pos = skin_matrix * vec4(in.position, 1.0); let local_normal = (skin_matrix * vec4(in.normal, 0.0)).xyz; let local_tangent = (skin_matrix * vec4(in.tangent.xyz, 0.0)).xyz; let world_pos4 = model.model * local_pos; out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(local_normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(local_tangent, 0.0)).xyz, in.tangent.w, ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let base_contribution = (k_d * albedo / PI + specular_brdf + sheen_brdf) * radiance * n_dot_l; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-transmission.wgsl var standard_transmission_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, transmission_params: vec4, diffuse_transmission_color: vec4, volume_attenuation: vec4, transmission_transform0: vec4, transmission_transform1: vec4, volume_thickness_transform0: vec4, volume_thickness_transform1: vec4, diffuse_transmission_transform0: vec4, diffuse_transmission_transform1: vec4, diffuse_transmission_color_transform0: vec4, diffuse_transmission_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, @location(6) model_scale: vec3, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(1) @binding(31) var transmission_sampler: sampler; @group(1) @binding(32) var transmission_tex: texture_2d; @group(1) @binding(33) var volume_thickness_sampler: sampler; @group(1) @binding(34) var volume_thickness_tex: texture_2d; @group(1) @binding(35) var diffuse_transmission_sampler: sampler; @group(1) @binding(36) var diffuse_transmission_tex: texture_2d; @group(1) @binding(37) var diffuse_transmission_color_sampler: sampler; @group(1) @binding(38) var diffuse_transmission_color_tex: texture_2d; @group(1) @binding(39) var transmission_source_sampler: sampler; @group(1) @binding(40) var transmission_source_tex: texture_2d; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } fn screen_uv_from_fragment(position: vec4) -> vec2 { let dims = vec2(textureDimensions(transmission_source_tex, 0)); return clamp(position.xy / max(dims, vec2(1.0)), vec2(0.0), vec2(1.0)); } fn project_world_to_screen_uv(world_pos: vec3) -> vec2 { let clip = camera.view_projection * vec4(world_pos, 1.0); let inv_w = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * inv_w * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2(0.0), vec2(1.0)); } fn transmission_screen_uv( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, thickness: f32, model_scale: vec3, ) -> vec2 { let base_uv = screen_uv_from_fragment(position); if (thickness <= 1e-5) { return base_uv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-v, n, eta); let ray_length2 = dot(ray, ray); if (ray_length2 <= 1e-8) { ray = -v; } else { ray = ray * inverseSqrt(ray_length2); } let transmission_ray = ray * max(thickness, 0.0) * max(model_scale, vec3(1e-4)); return project_world_to_screen_uv(world_pos + transmission_ray); } fn transmission_source_to_linear(color: vec3) -> vec3 { return pow(clamp(color, vec3(0.0), vec3(1.0)), vec3(2.2)); } fn sample_transmission_source_at(uv: vec2) -> vec3 { let source_color = textureSampleLevel( transmission_source_tex, transmission_source_sampler, clamp(uv, vec2(0.0), vec2(1.0)), 0.0, ).rgb; return transmission_source_to_linear(source_color); } fn dispersion_iors(ior: f32, dispersion: f32) -> vec3 { let half_spread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3(ior - half_spread, ior, ior + half_spread), vec3(1.0)); } fn sample_transmission_source( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, dispersion: f32, thickness: f32, model_scale: vec3, ) -> vec3 { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, ior, thickness, model_scale), ); } let iors = dispersion_iors(ior, dispersion); let r = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.r, thickness, model_scale), ).r; let g = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.g, thickness, model_scale), ).g; let b = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.b, thickness, model_scale), ).b; return vec3(r, g, b); } fn volume_transmission_attenuation( thickness: f32, attenuation_distance: f32, attenuation_color: vec3, ) -> vec3 { if (thickness <= 1e-5 || attenuation_distance <= 1e-5) { return vec3(1.0); } return pow( max(attenuation_color, vec3(1e-4)), vec3(thickness / attenuation_distance), ); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let world_pos4 = model.model * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(in.normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(in.tangent.xyz, 0.0)).xyz, in.tangent.w, ); out.model_scale = vec3( length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz), ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let transmission_uv = apply_texture_transform( in.uv, in.uv1, material.transmission_transform0, material.transmission_transform1, ); let volume_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.volume_thickness_transform0, material.volume_thickness_transform1, ); let diffuse_transmission_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_transform0, material.diffuse_transmission_transform1, ); let diffuse_transmission_color_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_color_transform0, material.diffuse_transmission_color_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let transmission = clamp( material.transmission_params.x * textureSample(transmission_tex, transmission_sampler, transmission_uv).r, 0.0, 1.0, ); let diffuse_transmission = clamp( material.transmission_params.y * textureSample( diffuse_transmission_tex, diffuse_transmission_sampler, diffuse_transmission_uv, ).a, 0.0, 1.0, ); let volume_thickness = max( material.transmission_params.z * textureSample(volume_thickness_tex, volume_thickness_sampler, volume_thickness_uv).g, 0.0, ); let dispersion = max(material.transmission_params.w, 0.0); let diffuse_transmission_color = material.diffuse_transmission_color.rgb * textureSample( diffuse_transmission_color_tex, diffuse_transmission_color_sampler, diffuse_transmission_color_uv, ).rgb; let volume_attenuation = volume_transmission_attenuation( volume_thickness, material.diffuse_transmission_color.w, material.volume_attenuation.rgb, ); let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } var view_fresnel = fresnel_schlick(view_ndot_v, f0, f90); if (iridescence > 1e-5) { view_fresnel = mix(view_fresnel, iridescence_fresnel_color, iridescence); } let transmission_weight = transmission * (1.0 - metallic) * max(1.0 - max_component(view_fresnel), 0.0); let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let front_diffuse = (1.0 - diffuse_transmission) * k_d * albedo * radiance * n_dot_l / PI; let back_diffuse = diffuse_transmission * k_d * diffuse_transmission_color * radiance * max(dot(-n, l), 0.0) / PI; let diffuse_contribution = (front_diffuse + back_diffuse) * (1.0 - transmission); let specular_contribution = (specular_brdf + sheen_brdf) * radiance * n_dot_l; let base_contribution = diffuse_contribution + specular_contribution; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } if (transmission_weight > 1e-5) { let transmitted_source = sample_transmission_source( in.position, in.world_pos, n, v, material.extension_params.x, dispersion, volume_thickness, in.model_scale, ); lo += transmitted_source * albedo * volume_attenuation * transmission_weight * (1.0 - clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-transmission-instanced.wgsl var standard_transmission_instanced_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, transmission_params: vec4, diffuse_transmission_color: vec4, volume_attenuation: vec4, transmission_transform0: vec4, transmission_transform1: vec4, volume_thickness_transform0: vec4, volume_thickness_transform1: vec4, diffuse_transmission_transform0: vec4, diffuse_transmission_transform1: vec4, diffuse_transmission_color_transform0: vec4, diffuse_transmission_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) m0: vec4, @location(4) m1: vec4, @location(5) m2: vec4, @location(6) m3: vec4, @location(7) n0: vec4, @location(8) n1: vec4, @location(9) n2: vec4, @location(10) n3: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, @location(6) model_scale: vec3, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(1) @binding(31) var transmission_sampler: sampler; @group(1) @binding(32) var transmission_tex: texture_2d; @group(1) @binding(33) var volume_thickness_sampler: sampler; @group(1) @binding(34) var volume_thickness_tex: texture_2d; @group(1) @binding(35) var diffuse_transmission_sampler: sampler; @group(1) @binding(36) var diffuse_transmission_tex: texture_2d; @group(1) @binding(37) var diffuse_transmission_color_sampler: sampler; @group(1) @binding(38) var diffuse_transmission_color_tex: texture_2d; @group(1) @binding(39) var transmission_source_sampler: sampler; @group(1) @binding(40) var transmission_source_tex: texture_2d; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } fn screen_uv_from_fragment(position: vec4) -> vec2 { let dims = vec2(textureDimensions(transmission_source_tex, 0)); return clamp(position.xy / max(dims, vec2(1.0)), vec2(0.0), vec2(1.0)); } fn project_world_to_screen_uv(world_pos: vec3) -> vec2 { let clip = camera.view_projection * vec4(world_pos, 1.0); let inv_w = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * inv_w * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2(0.0), vec2(1.0)); } fn transmission_screen_uv( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, thickness: f32, model_scale: vec3, ) -> vec2 { let base_uv = screen_uv_from_fragment(position); if (thickness <= 1e-5) { return base_uv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-v, n, eta); let ray_length2 = dot(ray, ray); if (ray_length2 <= 1e-8) { ray = -v; } else { ray = ray * inverseSqrt(ray_length2); } let transmission_ray = ray * max(thickness, 0.0) * max(model_scale, vec3(1e-4)); return project_world_to_screen_uv(world_pos + transmission_ray); } fn transmission_source_to_linear(color: vec3) -> vec3 { return pow(clamp(color, vec3(0.0), vec3(1.0)), vec3(2.2)); } fn sample_transmission_source_at(uv: vec2) -> vec3 { let source_color = textureSampleLevel( transmission_source_tex, transmission_source_sampler, clamp(uv, vec2(0.0), vec2(1.0)), 0.0, ).rgb; return transmission_source_to_linear(source_color); } fn dispersion_iors(ior: f32, dispersion: f32) -> vec3 { let half_spread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3(ior - half_spread, ior, ior + half_spread), vec3(1.0)); } fn sample_transmission_source( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, dispersion: f32, thickness: f32, model_scale: vec3, ) -> vec3 { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, ior, thickness, model_scale), ); } let iors = dispersion_iors(ior, dispersion); let r = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.r, thickness, model_scale), ).r; let g = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.g, thickness, model_scale), ).g; let b = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.b, thickness, model_scale), ).b; return vec3(r, g, b); } fn volume_transmission_attenuation( thickness: f32, attenuation_distance: f32, attenuation_color: vec3, ) -> vec3 { if (thickness <= 1e-5 || attenuation_distance <= 1e-5) { return vec3(1.0); } return pow( max(attenuation_color, vec3(1e-4)), vec3(thickness / attenuation_distance), ); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let model_m = mat4x4(in.m0, in.m1, in.m2, in.m3); let normal_m = mat4x4(in.n0, in.n1, in.n2, in.n3); let world_pos4 = model_m * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((normal_m * vec4(in.normal, 0.0)).xyz); out.tangent = vec4((normal_m * vec4(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.model_scale = vec3( length(model_m[0].xyz), length(model_m[1].xyz), length(model_m[2].xyz), ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let transmission_uv = apply_texture_transform( in.uv, in.uv1, material.transmission_transform0, material.transmission_transform1, ); let volume_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.volume_thickness_transform0, material.volume_thickness_transform1, ); let diffuse_transmission_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_transform0, material.diffuse_transmission_transform1, ); let diffuse_transmission_color_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_color_transform0, material.diffuse_transmission_color_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let transmission = clamp( material.transmission_params.x * textureSample(transmission_tex, transmission_sampler, transmission_uv).r, 0.0, 1.0, ); let diffuse_transmission = clamp( material.transmission_params.y * textureSample( diffuse_transmission_tex, diffuse_transmission_sampler, diffuse_transmission_uv, ).a, 0.0, 1.0, ); let volume_thickness = max( material.transmission_params.z * textureSample(volume_thickness_tex, volume_thickness_sampler, volume_thickness_uv).g, 0.0, ); let dispersion = max(material.transmission_params.w, 0.0); let diffuse_transmission_color = material.diffuse_transmission_color.rgb * textureSample( diffuse_transmission_color_tex, diffuse_transmission_color_sampler, diffuse_transmission_color_uv, ).rgb; let volume_attenuation = volume_transmission_attenuation( volume_thickness, material.diffuse_transmission_color.w, material.volume_attenuation.rgb, ); let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } var view_fresnel = fresnel_schlick(view_ndot_v, f0, f90); if (iridescence > 1e-5) { view_fresnel = mix(view_fresnel, iridescence_fresnel_color, iridescence); } let transmission_weight = transmission * (1.0 - metallic) * max(1.0 - max_component(view_fresnel), 0.0); let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let front_diffuse = (1.0 - diffuse_transmission) * k_d * albedo * radiance * n_dot_l / PI; let back_diffuse = diffuse_transmission * k_d * diffuse_transmission_color * radiance * max(dot(-n, l), 0.0) / PI; let diffuse_contribution = (front_diffuse + back_diffuse) * (1.0 - transmission); let specular_contribution = (specular_brdf + sheen_brdf) * radiance * n_dot_l; let base_contribution = diffuse_contribution + specular_contribution; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } if (transmission_weight > 1e-5) { let transmitted_source = sample_transmission_source( in.position, in.world_pos, n, v, material.extension_params.x, dispersion, volume_thickness, in.model_scale, ); lo += transmitted_source * albedo * volume_attenuation * transmission_weight * (1.0 - clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-transmission-skinned.wgsl var standard_transmission_skinned_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, transmission_params: vec4, diffuse_transmission_color: vec4, volume_attenuation: vec4, transmission_transform0: vec4, transmission_transform1: vec4, volume_thickness_transform0: vec4, volume_thickness_transform1: vec4, diffuse_transmission_transform0: vec4, diffuse_transmission_transform1: vec4, diffuse_transmission_color_transform0: vec4, diffuse_transmission_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) joints: vec4, @location(4) weights: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, @location(6) model_scale: vec3, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct SkinBuffer { joints: array>, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(1) @binding(31) var transmission_sampler: sampler; @group(1) @binding(32) var transmission_tex: texture_2d; @group(1) @binding(33) var volume_thickness_sampler: sampler; @group(1) @binding(34) var volume_thickness_tex: texture_2d; @group(1) @binding(35) var diffuse_transmission_sampler: sampler; @group(1) @binding(36) var diffuse_transmission_tex: texture_2d; @group(1) @binding(37) var diffuse_transmission_color_sampler: sampler; @group(1) @binding(38) var diffuse_transmission_color_tex: texture_2d; @group(1) @binding(39) var transmission_source_sampler: sampler; @group(1) @binding(40) var transmission_source_tex: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } fn screen_uv_from_fragment(position: vec4) -> vec2 { let dims = vec2(textureDimensions(transmission_source_tex, 0)); return clamp(position.xy / max(dims, vec2(1.0)), vec2(0.0), vec2(1.0)); } fn project_world_to_screen_uv(world_pos: vec3) -> vec2 { let clip = camera.view_projection * vec4(world_pos, 1.0); let inv_w = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * inv_w * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2(0.0), vec2(1.0)); } fn transmission_screen_uv( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, thickness: f32, model_scale: vec3, ) -> vec2 { let base_uv = screen_uv_from_fragment(position); if (thickness <= 1e-5) { return base_uv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-v, n, eta); let ray_length2 = dot(ray, ray); if (ray_length2 <= 1e-8) { ray = -v; } else { ray = ray * inverseSqrt(ray_length2); } let transmission_ray = ray * max(thickness, 0.0) * max(model_scale, vec3(1e-4)); return project_world_to_screen_uv(world_pos + transmission_ray); } fn transmission_source_to_linear(color: vec3) -> vec3 { return pow(clamp(color, vec3(0.0), vec3(1.0)), vec3(2.2)); } fn sample_transmission_source_at(uv: vec2) -> vec3 { let source_color = textureSampleLevel( transmission_source_tex, transmission_source_sampler, clamp(uv, vec2(0.0), vec2(1.0)), 0.0, ).rgb; return transmission_source_to_linear(source_color); } fn dispersion_iors(ior: f32, dispersion: f32) -> vec3 { let half_spread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3(ior - half_spread, ior, ior + half_spread), vec3(1.0)); } fn sample_transmission_source( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, dispersion: f32, thickness: f32, model_scale: vec3, ) -> vec3 { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, ior, thickness, model_scale), ); } let iors = dispersion_iors(ior, dispersion); let r = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.r, thickness, model_scale), ).r; let g = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.g, thickness, model_scale), ).g; let b = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.b, thickness, model_scale), ).b; return vec3(r, g, b); } fn volume_transmission_attenuation( thickness: f32, attenuation_distance: f32, attenuation_color: vec3, ) -> vec3 { if (thickness <= 1e-5 || attenuation_distance <= 1e-5) { return vec3(1.0); } return pow( max(attenuation_color, vec3(1e-4)), vec3(thickness / attenuation_distance), ); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j = in.joints; let w = in.weights; let skin_matrix = skin.joints[j.x] * w.x + skin.joints[j.y] * w.y + skin.joints[j.z] * w.z + skin.joints[j.w] * w.w; let local_pos = skin_matrix * vec4(in.position, 1.0); let local_normal = (skin_matrix * vec4(in.normal, 0.0)).xyz; let local_tangent = (skin_matrix * vec4(in.tangent.xyz, 0.0)).xyz; let world_pos4 = model.model * local_pos; out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(local_normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(local_tangent, 0.0)).xyz, in.tangent.w, ); out.model_scale = vec3( length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz), ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let transmission_uv = apply_texture_transform( in.uv, in.uv1, material.transmission_transform0, material.transmission_transform1, ); let volume_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.volume_thickness_transform0, material.volume_thickness_transform1, ); let diffuse_transmission_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_transform0, material.diffuse_transmission_transform1, ); let diffuse_transmission_color_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_color_transform0, material.diffuse_transmission_color_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let transmission = clamp( material.transmission_params.x * textureSample(transmission_tex, transmission_sampler, transmission_uv).r, 0.0, 1.0, ); let diffuse_transmission = clamp( material.transmission_params.y * textureSample( diffuse_transmission_tex, diffuse_transmission_sampler, diffuse_transmission_uv, ).a, 0.0, 1.0, ); let volume_thickness = max( material.transmission_params.z * textureSample(volume_thickness_tex, volume_thickness_sampler, volume_thickness_uv).g, 0.0, ); let dispersion = max(material.transmission_params.w, 0.0); let diffuse_transmission_color = material.diffuse_transmission_color.rgb * textureSample( diffuse_transmission_color_tex, diffuse_transmission_color_sampler, diffuse_transmission_color_uv, ).rgb; let volume_attenuation = volume_transmission_attenuation( volume_thickness, material.diffuse_transmission_color.w, material.volume_attenuation.rgb, ); let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } var view_fresnel = fresnel_schlick(view_ndot_v, f0, f90); if (iridescence > 1e-5) { view_fresnel = mix(view_fresnel, iridescence_fresnel_color, iridescence); } let transmission_weight = transmission * (1.0 - metallic) * max(1.0 - max_component(view_fresnel), 0.0); let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let front_diffuse = (1.0 - diffuse_transmission) * k_d * albedo * radiance * n_dot_l / PI; let back_diffuse = diffuse_transmission * k_d * diffuse_transmission_color * radiance * max(dot(-n, l), 0.0) / PI; let diffuse_contribution = (front_diffuse + back_diffuse) * (1.0 - transmission); let specular_contribution = (specular_brdf + sheen_brdf) * radiance * n_dot_l; let base_contribution = diffuse_contribution + specular_contribution; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } if (transmission_weight > 1e-5) { let transmitted_source = sample_transmission_source( in.position, in.world_pos, n, v, material.extension_params.x, dispersion, volume_thickness, in.model_scale, ); lo += transmitted_source * albedo * volume_attenuation * transmission_weight * (1.0 - clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/standard-transmission-skinned8.wgsl var standard_transmission_skinned8_default = "const PI: f32 = 3.14159265359; struct MaterialUniforms { color: vec4, emissive: vec4, params: vec4, params2: vec4, base_color_transform0: vec4, base_color_transform1: vec4, metallic_roughness_transform0: vec4, metallic_roughness_transform1: vec4, normal_transform0: vec4, normal_transform1: vec4, occlusion_transform0: vec4, occlusion_transform1: vec4, emissive_transform0: vec4, emissive_transform1: vec4, clearcoat_params: vec4, specular_params: vec4, extension_params: vec4, clearcoat_transform0: vec4, clearcoat_transform1: vec4, clearcoat_roughness_transform0: vec4, clearcoat_roughness_transform1: vec4, clearcoat_normal_transform0: vec4, clearcoat_normal_transform1: vec4, specular_transform0: vec4, specular_transform1: vec4, specular_color_transform0: vec4, specular_color_transform1: vec4, sheen_params: vec4, iridescence_params: vec4, anisotropy_params: vec4, sheen_color_transform0: vec4, sheen_color_transform1: vec4, sheen_roughness_transform0: vec4, sheen_roughness_transform1: vec4, iridescence_transform0: vec4, iridescence_transform1: vec4, iridescence_thickness_transform0: vec4, iridescence_thickness_transform1: vec4, anisotropy_transform0: vec4, anisotropy_transform1: vec4, transmission_params: vec4, diffuse_transmission_color: vec4, volume_attenuation: vec4, transmission_transform0: vec4, transmission_transform1: vec4, volume_thickness_transform0: vec4, volume_thickness_transform1: vec4, diffuse_transmission_transform0: vec4, diffuse_transmission_transform1: vec4, diffuse_transmission_color_transform0: vec4, diffuse_transmission_color_transform1: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(11) uv1: vec2, @location(12) tangent: vec4, @location(13) color: vec4, @location(3) joints0: vec4, @location(4) weights0: vec4, @location(5) joints1: vec4, @location(6) weights1: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) uv1: vec2, @location(4) tangent: vec4, @location(5) color: vec4, @location(6) model_scale: vec3, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, direction: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct SkinBuffer { joints: array>, } struct TangentFrame { t: vec3, b: vec3, n: vec3, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var base_color_sampler: sampler; @group(1) @binding(2) var base_color_tex: texture_2d; @group(1) @binding(3) var metallic_roughness_sampler: sampler; @group(1) @binding(4) var metallic_roughness_tex: texture_2d; @group(1) @binding(5) var normal_sampler: sampler; @group(1) @binding(6) var normal_tex: texture_2d; @group(1) @binding(7) var occlusion_sampler: sampler; @group(1) @binding(8) var occlusion_tex: texture_2d; @group(1) @binding(9) var emissive_sampler: sampler; @group(1) @binding(10) var emissive_tex: texture_2d; @group(1) @binding(11) var clearcoat_sampler: sampler; @group(1) @binding(12) var clearcoat_tex: texture_2d; @group(1) @binding(13) var clearcoat_roughness_sampler: sampler; @group(1) @binding(14) var clearcoat_roughness_tex: texture_2d; @group(1) @binding(15) var clearcoat_normal_sampler: sampler; @group(1) @binding(16) var clearcoat_normal_tex: texture_2d; @group(1) @binding(17) var specular_sampler: sampler; @group(1) @binding(18) var specular_tex: texture_2d; @group(1) @binding(19) var specular_color_sampler: sampler; @group(1) @binding(20) var specular_color_tex: texture_2d; @group(1) @binding(21) var sheen_color_sampler: sampler; @group(1) @binding(22) var sheen_color_tex: texture_2d; @group(1) @binding(23) var sheen_roughness_sampler: sampler; @group(1) @binding(24) var sheen_roughness_tex: texture_2d; @group(1) @binding(25) var iridescence_sampler: sampler; @group(1) @binding(26) var iridescence_tex: texture_2d; @group(1) @binding(27) var iridescence_thickness_sampler: sampler; @group(1) @binding(28) var iridescence_thickness_tex: texture_2d; @group(1) @binding(29) var anisotropy_sampler: sampler; @group(1) @binding(30) var anisotropy_tex: texture_2d; @group(1) @binding(31) var transmission_sampler: sampler; @group(1) @binding(32) var transmission_tex: texture_2d; @group(1) @binding(33) var volume_thickness_sampler: sampler; @group(1) @binding(34) var volume_thickness_tex: texture_2d; @group(1) @binding(35) var diffuse_transmission_sampler: sampler; @group(1) @binding(36) var diffuse_transmission_tex: texture_2d; @group(1) @binding(37) var diffuse_transmission_color_sampler: sampler; @group(1) @binding(38) var diffuse_transmission_color_tex: texture_2d; @group(1) @binding(39) var transmission_source_sampler: sampler; @group(1) @binding(40) var transmission_source_tex: texture_2d; @group(2) @binding(0) var skin: SkinBuffer; fn apply_texture_transform( uv0: vec2, uv1: vec2, transform0: vec4, transform1: vec4, ) -> vec2 { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y, ); return rotated + transform0.xy; } fn fresnel_schlick(cos_theta: f32, f0: vec3, f90: vec3) -> vec3 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (f90 - f0) * pow(one_minus_cos, 5.0); } fn max_component(v: vec3) -> f32 { return max(max(v.x, v.y), v.z); } fn distribution_ggx(n: vec3, h: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(n, h), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = n_dot_h2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / (n_dot_v * (1.0 - k) + k); } fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { let n_dot_v = max(dot(n, v), 0.0); let n_dot_l = max(dot(n, l), 0.0); return geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness); } fn fallback_tangent_frame(normal: vec3) -> TangentFrame { let n = normalize(normal); let axis = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(n.x) < 0.9); let t = normalize(cross(axis, n)); let b = cross(n, t); return TangentFrame(t, b, n); } fn derivative_tangent_frame( normal: vec3, world_pos: vec3, uv: vec2, ) -> TangentFrame { let n = normalize(normal); let dp1 = dpdx(world_pos); let dp2 = dpdy(world_pos); let duv1 = dpdx(uv); let duv2 = dpdy(uv); let dp2perp = cross(dp2, n); let dp1perp = cross(n, dp1); let t = (dp2perp * duv1.x) + (dp1perp * duv2.x); let b = (dp2perp * duv1.y) + (dp1perp * duv2.y); let frame_length2 = max(dot(t, t), dot(b, b)); if (frame_length2 <= 1e-20) { return fallback_tangent_frame(n); } let frame_scale = 1.0 / sqrt(frame_length2); return TangentFrame(t * frame_scale, b * frame_scale, n); } fn build_tangent_frame( normal: vec3, tangent: vec4, world_pos: vec3, uv: vec2, face_sign: f32, ) -> TangentFrame { let n = normalize(normal); let derivative_frame = derivative_tangent_frame(n, world_pos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let t_len2 = dot(t, t); if (t_len2 <= 1e-20) { return TangentFrame( derivative_frame.t, derivative_frame.b * face_sign, derivative_frame.n * face_sign, ); } t = t * inverseSqrt(t_len2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0) * face_sign; return TangentFrame(t, b, n * face_sign); } fn apply_normal_map( n: vec3, tangent: vec4, world_pos: vec3, uv: vec2, normal_sample: vec3, normal_scale: f32, face_sign: f32, ) -> vec3 { if (normal_scale == 0.0) { return normalize(n) * face_sign; } let frame = build_tangent_frame(n, tangent, world_pos, uv, face_sign); var ns = normal_sample * 2.0 - vec3(1.0); ns = vec3(ns.x * normal_scale, ns.y * normal_scale, ns.z); return normalize(frame.t * ns.x + frame.b * ns.y + frame.n * ns.z); } fn sqr(v: f32) -> f32 { return v * v; } fn ior_to_fresnel0(transmitted_ior: f32, incident_ior: f32) -> f32 { let r = (transmitted_ior - incident_ior) / (transmitted_ior + incident_ior); return r * r; } fn ior_to_fresnel0_vec(transmitted_ior: vec3, incident_ior: f32) -> vec3 { let r = (transmitted_ior - vec3(incident_ior)) / (transmitted_ior + vec3(incident_ior)); return r * r; } fn fresnel0_to_ior(f0: vec3) -> vec3 { let sqrt_f0 = sqrt(clamp(f0, vec3(0.0), vec3(0.9999))); return (vec3(1.0) + sqrt_f0) / max(vec3(1.0) - sqrt_f0, vec3(1e-4)); } fn fresnel_schlick_scalar(cos_theta: f32, f0: f32) -> f32 { let one_minus_cos = 1.0 - clamp(cos_theta, 0.0, 1.0); return f0 + (1.0 - f0) * pow(one_minus_cos, 5.0); } fn sanitize_reflectance(value: vec3, fallback: vec3) -> vec3 { var result = clamp(fallback, vec3(0.0), vec3(1.0)); if (value.x == value.x && abs(value.x) < 1.0e6) { result.x = clamp(value.x, 0.0, 1.0); } if (value.y == value.y && abs(value.y) < 1.0e6) { result.y = clamp(value.y, 0.0, 1.0); } if (value.z == value.z && abs(value.z) < 1.0e6) { result.z = clamp(value.z, 0.0, 1.0); } return result; } fn eval_iridescence_sensitivity(opd: f32, shift: vec3) -> vec3 { let phase = 2.0 * PI * opd * 1.0e-9; let phase2 = phase * phase; let val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz = val * sqrt(2.0 * PI * variance) * cos(pos * phase + shift) * exp(-phase2 * variance); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase2); xyz /= 1.0685e-7; return vec3( 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, ); } fn iridescent_fresnel( outside_ior: f32, iridescence_ior: f32, base_f0: vec3, thickness: f32, cos_theta1: f32, ) -> vec3 { let safe_cos_theta1 = clamp(cos_theta1, 0.0, 1.0); let thin_film_ior = mix(outside_ior, iridescence_ior, smoothstep(0.0, 0.03, thickness)); let r0 = ior_to_fresnel0(thin_film_ior, outside_ior); let r12 = fresnel_schlick_scalar(safe_cos_theta1, r0); let t121 = 1.0 - r12; let base_ior = fresnel0_to_ior(base_f0); let r1 = ior_to_fresnel0_vec(base_ior, thin_film_ior); let eta = outside_ior / thin_film_ior; let sin_theta2_sq = eta * eta * (1.0 - safe_cos_theta1 * safe_cos_theta1); let cos_theta2_sq = 1.0 - sin_theta2_sq; if (cos_theta2_sq < 0.0) { return vec3(1.0); } let cos_theta2 = sqrt(cos_theta2_sq); let r23 = fresnel_schlick(cos_theta2, r1, vec3(1.0)); let phi12 = select(0.0, PI, thin_film_ior < outside_ior); let phi21 = PI - phi12; let phi23 = vec3( select(0.0, PI, base_ior.x < thin_film_ior), select(0.0, PI, base_ior.y < thin_film_ior), select(0.0, PI, base_ior.z < thin_film_ior), ); let phi = vec3(phi21) + phi23; let opd = 2.0 * thin_film_ior * thickness * cos_theta2; let r123_product = clamp(vec3(r12) * r23, vec3(1e-5), vec3(0.9999)); let r123 = sqrt(r123_product); let rs = sqr(t121) * r23 / (vec3(1.0) - r123_product); var i = vec3(r12) + rs; var cm = rs - vec3(t121); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(opd, phi); cm *= r123; i += cm * 2.0 * eval_iridescence_sensitivity(2.0 * opd, 2.0 * phi); return sanitize_reflectance(i, base_f0); } fn distribution_ggx_anisotropic(n_dot_h: f32, t_dot_h: f32, b_dot_h: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3(ab * t_dot_h, at * b_dot_h, a2 * n_dot_h); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibility_ggx_anisotropic( n_dot_l: f32, n_dot_v: f32, b_dot_v: f32, t_dot_v: f32, t_dot_l: f32, b_dot_l: f32, at: f32, ab: f32, ) -> f32 { let ggx_v = n_dot_l * length(vec3(at * t_dot_v, ab * b_dot_v, n_dot_v)); let ggx_l = n_dot_v * length(vec3(at * t_dot_l, ab * b_dot_l, n_dot_l)); return clamp(0.5 / max(ggx_v + ggx_l, 1e-8), 0.0, 1.0); } fn sheen_distribution(n_dot_h: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let inv_r = 1.0 / alpha_g; let sin2h = max(1.0 - n_dot_h * n_dot_h, 0.0); return (2.0 + inv_r) * pow(sin2h, inv_r * 0.5) / (2.0 * PI); } fn sheen_l(cos_theta: f32, alpha_g: f32) -> f32 { let one_minus_alpha_sq = sqr(1.0 - alpha_g); let a = mix(21.5473, 25.3245, one_minus_alpha_sq); let b = mix(3.82987, 3.32435, one_minus_alpha_sq); let c = mix(0.19823, 0.16801, one_minus_alpha_sq); let d = mix(-1.97760, -1.27393, one_minus_alpha_sq); let e = mix(-4.32054, -4.85967, one_minus_alpha_sq); return a / (1.0 + b * pow(cos_theta, c)) + d * cos_theta + e; } fn sheen_lambda(cos_theta: f32, alpha_g: f32) -> f32 { let safe_cos_theta = clamp(cos_theta, 1e-4, 1.0); if (safe_cos_theta < 0.5) { return exp(sheen_l(safe_cos_theta, alpha_g)); } return exp(2.0 * sheen_l(0.5, alpha_g) - sheen_l(1.0 - safe_cos_theta, alpha_g)); } fn sheen_visibility(n_dot_l: f32, n_dot_v: f32, sheen_roughness: f32) -> f32 { let alpha_g = max(sheen_roughness * sheen_roughness, 1e-4); let visibility = 1.0 + sheen_lambda(n_dot_v, alpha_g) + sheen_lambda(n_dot_l, alpha_g); return clamp(1.0 / max(visibility * 4.0 * n_dot_v * n_dot_l, 1e-6), 0.0, 1.0); } fn dielectric_f0_from_ior(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safe_ior = max(ior, 1.0); let r = (safe_ior - 1.0) / (safe_ior + 1.0); return r * r; } fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; } fn compute_range_attenuation(distance: f32, range: f32) -> f32 { let inv_sq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return inv_sq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return inv_sq * fade * fade; } fn compute_spot_factor(l: vec3, direction: vec3, cos_inner: f32, cos_outer: f32) -> f32 { let angle_cos = dot(-l, normalize(direction)); if (cos_inner <= cos_outer) { return select(0.0, 1.0, angle_cos >= cos_outer); } return clamp((angle_cos - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0); } fn screen_uv_from_fragment(position: vec4) -> vec2 { let dims = vec2(textureDimensions(transmission_source_tex, 0)); return clamp(position.xy / max(dims, vec2(1.0)), vec2(0.0), vec2(1.0)); } fn project_world_to_screen_uv(world_pos: vec3) -> vec2 { let clip = camera.view_projection * vec4(world_pos, 1.0); let inv_w = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * inv_w * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2(0.0), vec2(1.0)); } fn transmission_screen_uv( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, thickness: f32, model_scale: vec3, ) -> vec2 { let base_uv = screen_uv_from_fragment(position); if (thickness <= 1e-5) { return base_uv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-v, n, eta); let ray_length2 = dot(ray, ray); if (ray_length2 <= 1e-8) { ray = -v; } else { ray = ray * inverseSqrt(ray_length2); } let transmission_ray = ray * max(thickness, 0.0) * max(model_scale, vec3(1e-4)); return project_world_to_screen_uv(world_pos + transmission_ray); } fn transmission_source_to_linear(color: vec3) -> vec3 { return pow(clamp(color, vec3(0.0), vec3(1.0)), vec3(2.2)); } fn sample_transmission_source_at(uv: vec2) -> vec3 { let source_color = textureSampleLevel( transmission_source_tex, transmission_source_sampler, clamp(uv, vec2(0.0), vec2(1.0)), 0.0, ).rgb; return transmission_source_to_linear(source_color); } fn dispersion_iors(ior: f32, dispersion: f32) -> vec3 { let half_spread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3(ior - half_spread, ior, ior + half_spread), vec3(1.0)); } fn sample_transmission_source( position: vec4, world_pos: vec3, n: vec3, v: vec3, ior: f32, dispersion: f32, thickness: f32, model_scale: vec3, ) -> vec3 { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, ior, thickness, model_scale), ); } let iors = dispersion_iors(ior, dispersion); let r = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.r, thickness, model_scale), ).r; let g = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.g, thickness, model_scale), ).g; let b = sample_transmission_source_at( transmission_screen_uv(position, world_pos, n, v, iors.b, thickness, model_scale), ).b; return vec3(r, g, b); } fn volume_transmission_attenuation( thickness: f32, attenuation_distance: f32, attenuation_color: vec3, ) -> vec3 { if (thickness <= 1e-5 || attenuation_distance <= 1e-5) { return vec3(1.0); } return pow( max(attenuation_color, vec3(1e-4)), vec3(thickness / attenuation_distance), ); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j0 = in.joints0; let w0 = in.weights0; let j1 = in.joints1; let w1 = in.weights1; let skin_matrix = skin.joints[j0.x] * w0.x + skin.joints[j0.y] * w0.y + skin.joints[j0.z] * w0.z + skin.joints[j0.w] * w0.w + skin.joints[j1.x] * w1.x + skin.joints[j1.y] * w1.y + skin.joints[j1.z] * w1.z + skin.joints[j1.w] * w1.w; let local_pos = skin_matrix * vec4(in.position, 1.0); let local_normal = (skin_matrix * vec4(in.normal, 0.0)).xyz; let local_tangent = (skin_matrix * vec4(in.tangent.xyz, 0.0)).xyz; let world_pos4 = model.model * local_pos; out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(local_normal, 0.0)).xyz); out.tangent = vec4( (model.normal_matrix * vec4(local_tangent, 0.0)).xyz, in.tangent.w, ); out.model_scale = vec3( length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz), ); out.uv = in.uv; out.uv1 = in.uv1; out.color = in.color; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(front_facing) is_front: bool) -> @location(0) vec4 { let face_sign = select(-1.0, 1.0, is_front); let front_geom_normal = normalize(in.normal); let geom_normal = front_geom_normal * face_sign; let shadow_world_dx = dpdx(in.world_pos); let shadow_world_dy = dpdy(in.world_pos); let base_uv = apply_texture_transform( in.uv, in.uv1, material.base_color_transform0, material.base_color_transform1, ); let mr_uv = apply_texture_transform( in.uv, in.uv1, material.metallic_roughness_transform0, material.metallic_roughness_transform1, ); let normal_uv = apply_texture_transform( in.uv, in.uv1, material.normal_transform0, material.normal_transform1, ); let occlusion_uv = apply_texture_transform( in.uv, in.uv1, material.occlusion_transform0, material.occlusion_transform1, ); let emissive_uv = apply_texture_transform( in.uv, in.uv1, material.emissive_transform0, material.emissive_transform1, ); let clearcoat_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_transform0, material.clearcoat_transform1, ); let clearcoat_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_roughness_transform0, material.clearcoat_roughness_transform1, ); let clearcoat_normal_uv = apply_texture_transform( in.uv, in.uv1, material.clearcoat_normal_transform0, material.clearcoat_normal_transform1, ); let specular_uv = apply_texture_transform( in.uv, in.uv1, material.specular_transform0, material.specular_transform1, ); let specular_color_uv = apply_texture_transform( in.uv, in.uv1, material.specular_color_transform0, material.specular_color_transform1, ); let sheen_color_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_color_transform0, material.sheen_color_transform1, ); let sheen_roughness_uv = apply_texture_transform( in.uv, in.uv1, material.sheen_roughness_transform0, material.sheen_roughness_transform1, ); let iridescence_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_transform0, material.iridescence_transform1, ); let iridescence_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.iridescence_thickness_transform0, material.iridescence_thickness_transform1, ); let anisotropy_uv = apply_texture_transform( in.uv, in.uv1, material.anisotropy_transform0, material.anisotropy_transform1, ); let transmission_uv = apply_texture_transform( in.uv, in.uv1, material.transmission_transform0, material.transmission_transform1, ); let volume_thickness_uv = apply_texture_transform( in.uv, in.uv1, material.volume_thickness_transform0, material.volume_thickness_transform1, ); let diffuse_transmission_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_transform0, material.diffuse_transmission_transform1, ); let diffuse_transmission_color_uv = apply_texture_transform( in.uv, in.uv1, material.diffuse_transmission_color_transform0, material.diffuse_transmission_color_transform1, ); let base_sample = textureSample(base_color_tex, base_color_sampler, base_uv); let base_color = material.color * base_sample * in.color; let alpha_cutoff = material.params2.x; if (alpha_cutoff > 0.0 && base_color.a < alpha_cutoff) { discard; } let mr_sample = textureSample(metallic_roughness_tex, metallic_roughness_sampler, mr_uv); let metallic = clamp(material.params.x * mr_sample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mr_sample.g, 0.04, 1.0); let normal_sample = textureSample(normal_tex, normal_sampler, normal_uv).xyz; let n = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, normal_uv, normal_sample, material.params.z, face_sign, ); let occl_sample = textureSample(occlusion_tex, occlusion_sampler, occlusion_uv).r; let ao = 1.0 + material.params.w * (occl_sample - 1.0); let emissive_sample = textureSample(emissive_tex, emissive_sampler, emissive_uv).rgb; let emissive = emissive_sample * material.emissive.rgb * material.emissive.a * material.extension_params.y; let clearcoat = clamp( material.clearcoat_params.x * textureSample(clearcoat_tex, clearcoat_sampler, clearcoat_uv).r, 0.0, 1.0, ); let clearcoat_roughness = clamp( material.clearcoat_params.y * textureSample( clearcoat_roughness_tex, clearcoat_roughness_sampler, clearcoat_roughness_uv, ).g, 0.04, 1.0, ); let clearcoat_normal_sample = textureSample( clearcoat_normal_tex, clearcoat_normal_sampler, clearcoat_normal_uv, ).xyz; let clearcoat_normal = apply_normal_map( front_geom_normal, in.tangent, in.world_pos, clearcoat_normal_uv, clearcoat_normal_sample, material.clearcoat_params.z, face_sign, ); let specular_strength = clamp( material.specular_params.x * textureSample(specular_tex, specular_sampler, specular_uv).a, 0.0, 1.0, ); let specular_color = material.specular_params.yzw * textureSample(specular_color_tex, specular_color_sampler, specular_color_uv).rgb; let sheen_color = material.sheen_params.rgb * textureSample(sheen_color_tex, sheen_color_sampler, sheen_color_uv).rgb; let sheen_roughness = clamp( material.sheen_params.w * textureSample(sheen_roughness_tex, sheen_roughness_sampler, sheen_roughness_uv).a, 0.0, 1.0, ); let iridescence = clamp( material.iridescence_params.x * textureSample(iridescence_tex, iridescence_sampler, iridescence_uv).r, 0.0, 1.0, ); let iridescence_thickness_sample = textureSample( iridescence_thickness_tex, iridescence_thickness_sampler, iridescence_thickness_uv, ).g; let iridescence_thickness = mix( material.iridescence_params.z, material.iridescence_params.w, iridescence_thickness_sample, ); let anisotropy_sample = textureSample(anisotropy_tex, anisotropy_sampler, anisotropy_uv).rgb; let transmission = clamp( material.transmission_params.x * textureSample(transmission_tex, transmission_sampler, transmission_uv).r, 0.0, 1.0, ); let diffuse_transmission = clamp( material.transmission_params.y * textureSample( diffuse_transmission_tex, diffuse_transmission_sampler, diffuse_transmission_uv, ).a, 0.0, 1.0, ); let volume_thickness = max( material.transmission_params.z * textureSample(volume_thickness_tex, volume_thickness_sampler, volume_thickness_uv).g, 0.0, ); let dispersion = max(material.transmission_params.w, 0.0); let diffuse_transmission_color = material.diffuse_transmission_color.rgb * textureSample( diffuse_transmission_color_tex, diffuse_transmission_color_sampler, diffuse_transmission_color_uv, ).rgb; let volume_attenuation = volume_transmission_attenuation( volume_thickness, material.diffuse_transmission_color.w, material.volume_attenuation.rgb, ); let anisotropy_strength = clamp(material.anisotropy_params.x * anisotropy_sample.b, 0.0, 1.0); var anisotropy_direction = anisotropy_sample.rg * 2.0 - vec2(1.0); let anisotropy_direction_length2 = dot(anisotropy_direction, anisotropy_direction); anisotropy_direction = select( vec2(1.0, 0.0), anisotropy_direction * inverseSqrt(max(anisotropy_direction_length2, 1e-8)), anisotropy_direction_length2 > 1e-8, ); anisotropy_direction = vec2( material.anisotropy_params.y * anisotropy_direction.x - material.anisotropy_params.z * anisotropy_direction.y, material.anisotropy_params.z * anisotropy_direction.x + material.anisotropy_params.y * anisotropy_direction.y, ); let albedo = base_color.rgb; let v = normalize(camera.position - in.world_pos); let dielectric_f0 = dielectric_f0_from_ior(material.extension_params.x); let dielectric_f0_color = min(vec3(dielectric_f0) * specular_color, vec3(1.0)) * specular_strength; let f0 = mix(dielectric_f0_color, albedo, metallic); let f90 = mix(vec3(specular_strength), vec3(1.0), metallic); let view_ndot_v = max(dot(n, v), 0.0); var iridescence_fresnel_color = f0; if (iridescence > 1e-5 && iridescence_thickness > 0.0) { iridescence_fresnel_color = iridescent_fresnel( 1.0, material.iridescence_params.y, f0, iridescence_thickness, view_ndot_v, ); } var view_fresnel = fresnel_schlick(view_ndot_v, f0, f90); if (iridescence > 1e-5) { view_fresnel = mix(view_fresnel, iridescence_fresnel_color, iridescence); } let transmission_weight = transmission * (1.0 - metallic) * max(1.0 - max_component(view_fresnel), 0.0); let geometric_n = geom_normal; let anisotropy_frame = build_tangent_frame( front_geom_normal, in.tangent, in.world_pos, anisotropy_uv, face_sign, ); let clearcoat_view_fresnel = clamp( clearcoat * fresnel_schlick_scalar(abs(dot(v, clearcoat_normal)), 0.04), 0.0, 1.0, ); var lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let distance = length(light_dir); if (distance <= 1e-5) { continue; } l = light_dir / distance; attenuation = compute_range_attenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * compute_spot_factor( l, light.direction.xyz, light.params.y, light.params.z, ); } } if (attenuation <= 0.0) { continue; } let h = normalize(v + l); let radiance = light.color.rgb * light.color.a * attenuation * standard_direct_visibility(i, in.world_pos, geom_normal, l, shadow_world_dx, shadow_world_dy); let n_dot_l = max(dot(n, l), 0.0); let n_dot_v = view_ndot_v; let n_dot_h = max(dot(n, h), 0.0); let vdot_h = max(dot(v, h), 0.0); let base_f = fresnel_schlick(vdot_h, f0, f90); var f = base_f; if (iridescence > 1e-5) { f = mix(base_f, iridescence_fresnel_color, iridescence); } var specular_brdf: vec3; if (anisotropy_strength > 1e-5) { let anisotropic_t = normalize( anisotropy_frame.t * anisotropy_direction.x + anisotropy_frame.b * anisotropy_direction.y, ); let anisotropic_b = normalize(cross(geometric_n, anisotropic_t)); let t_dot_v = dot(anisotropic_t, v); let b_dot_v = dot(anisotropic_b, v); let t_dot_l = dot(anisotropic_t, l); let b_dot_l = dot(anisotropic_b, l); let t_dot_h = dot(anisotropic_t, h); let b_dot_h = dot(anisotropic_b, h); let alpha_roughness = max(roughness * roughness, 0.001); let at = mix(alpha_roughness, 1.0, anisotropy_strength * anisotropy_strength); let ab = alpha_roughness; let d = distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, at, ab); let vg = visibility_ggx_anisotropic( n_dot_l, n_dot_v, b_dot_v, t_dot_v, t_dot_l, b_dot_l, at, ab, ); specular_brdf = f * d * vg; } else { let ndf = distribution_ggx(n, h, roughness); let g = geometry_smith(n, v, l, roughness); let numerator = ndf * g * f; let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; specular_brdf = numerator / denominator; } let sheen_d = sheen_distribution(n_dot_h, sheen_roughness); let sheen_v = sheen_visibility(n_dot_l, n_dot_v, sheen_roughness); let sheen_brdf = sheen_color * sheen_d * sheen_v; let diffuse_energy = max(1.0 - max_component(f), 0.0); let k_d = vec3(diffuse_energy) * (1.0 - metallic); let front_diffuse = (1.0 - diffuse_transmission) * k_d * albedo * radiance * n_dot_l / PI; let back_diffuse = diffuse_transmission * k_d * diffuse_transmission_color * radiance * max(dot(-n, l), 0.0) / PI; let diffuse_contribution = (front_diffuse + back_diffuse) * (1.0 - transmission); let specular_contribution = (specular_brdf + sheen_brdf) * radiance * n_dot_l; let base_contribution = diffuse_contribution + specular_contribution; let clearcoat_ndot_l = max(dot(clearcoat_normal, l), 0.0); let clearcoat_ndot_v = max(dot(clearcoat_normal, v), 0.0); let clearcoat_ndf = distribution_ggx(clearcoat_normal, h, clearcoat_roughness); let clearcoat_g = geometry_smith(clearcoat_normal, v, l, clearcoat_roughness); let clearcoat_brdf = clearcoat_ndf * clearcoat_g / (4.0 * clearcoat_ndot_v * clearcoat_ndot_l + 0.0001); let clearcoat_contribution = vec3(clearcoat_brdf) * radiance * clearcoat_ndot_l; lo += mix(base_contribution, clearcoat_contribution, clearcoat_view_fresnel); } if (transmission_weight > 1e-5) { let transmitted_source = sample_transmission_source( in.position, in.world_pos, n, v, material.extension_params.x, dispersion, volume_thickness, in.model_scale, ); lo += transmitted_source * albedo * volume_attenuation * transmission_weight * (1.0 - clearcoat_view_fresnel); } lo += emissive * (1.0 - clearcoat_view_fresnel); lo = lo / (lo + vec3(1.0)); lo = pow(lo, vec3(1.0 / 2.2)); return vec4(lo, base_color.a); }"; // wgsl/graphics/data.wgsl var data_default = "struct MaterialUniforms { scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, color_params: vec4, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) data_value: vec4, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } struct Light { position: vec4, color: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var data: array; @group(1) @binding(2) var colormap_sampler: sampler; @group(1) @binding(3) var colormap_tex: texture_1d; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } fn srgb_from_linear(c: vec3) -> vec3 { let a = vec3(0.055); return select( 12.92 * c, (1.0 + a) * pow(c, vec3(1.0 / 2.4)) - a, c > vec3(0.0031308), ); } fn luminance(rgb: vec3) -> f32 { return dot(rgb, vec3(0.2126, 0.7152, 0.0722)); } @vertex fn vs_main(in: VertexInput, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { var out: VertexOutput; let world_pos4 = model.model * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos4; out.world_pos = world_pos4.xyz; out.normal = normalize((model.normal_matrix * vec4(in.normal, 0.0)).xyz); let component_count = max(1u, min(4u, u32(material.scale_source.x + 0.5))); let stride = max(1u, u32(material.scale_source.w + 0.5)); let data_offset = u32(material.scale_domain.z + 0.5); let base = vertex_index * stride + data_offset; var x: f32 = data[base + 0u]; var y: f32 = 0.0; var z: f32 = 0.0; var w: f32 = 0.0; if (component_count > 1u) { y = data[base + 1u]; } if (component_count > 2u) { z = data[base + 2u]; } if (component_count > 3u) { w = data[base + 3u]; } out.data_value = vec4(x, y, z, w); return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let component_count = max(1u, min(4u, u32(material.scale_source.x + 0.5))); let component_index = min(3u, u32(material.scale_source.y + 0.5)); let value_mode = u32(material.scale_source.z + 0.5); let v = scale_select_value(in.data_value, component_count, component_index, value_mode); if (!scale_is_finite(v)) { discard; } let t = scale_apply_transform( v, vec4(material.scale_domain.x, material.scale_domain.y, 0.0, material.scale_domain.w), material.scale_clamp, material.scale_params, material.scale_flags, ); var cmap = textureSample(colormap_tex, colormap_sampler, t); let shading = scale_clamp01(material.color_params.y); if (shading > 0.0) { let n = normalize(in.normal); var light_factor: f32 = luminance(lighting.ambient.rgb); for (var i = 0u; i < lighting.light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - in.world_pos; let dist = length(light_dir); l = normalize(light_dir); attenuation = 1.0 / max(1e-6, dist * dist); } let ndotl = max(dot(n, l), 0.0); let lum = luminance(light.color.rgb) * light.color.a; light_factor += lum * attenuation * ndotl; } let shaded_rgb = cmap.rgb * light_factor; cmap = vec4(mix(cmap.rgb, shaded_rgb, shading), cmap.a); } let opacity = scale_clamp01(material.color_params.x); let final_a = cmap.a * opacity; let final_rgb = clamp(cmap.rgb, vec3(0.0), vec3(1.0)); cmap = vec4(final_rgb, final_a); return vec4(srgb_from_linear(cmap.rgb), cmap.a); }"; // wgsl/graphics/custom-default-vertex.wgsl var custom_default_vertex_default = "struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, } struct CameraUniforms { view_projection: mat4x4, position: vec3, } struct ModelUniforms { model: mat4x4, normal_matrix: mat4x4, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let world_pos = model.model * vec4(in.position, 1.0); out.position = camera.view_projection * world_pos; out.world_pos = world_pos.xyz; out.normal = normalize((model.normal_matrix * vec4(in.normal, 0.0)).xyz); out.uv = in.uv; return out; }"; // wgsl/effects/shadow-receiver.wgsl var shadow_receiver_default = "struct ShadowMetadata { view_projection: mat4x4, params: vec4, } struct ShadowUniforms { views: array, } @group(2) @binding(0) var shadow_maps: texture_depth_2d_array; @group(2) @binding(1) var shadow_sampler: sampler_comparison; @group(2) @binding(2) var shadows: ShadowUniforms; fn shadow_visibility( light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3, ) -> f32 { let metadata = shadows.views[light_index]; if (metadata.params.x < 0.0) { return 1.0; } let normalized_normal = normalize(geometric_normal); let normalized_light = normalize(light_direction); let offset_normal = select( -normalized_normal, normalized_normal, dot(normalized_normal, normalized_light) >= 0.0 ); let shadow_position = metadata.view_projection * vec4(world_position + offset_normal * metadata.params.z, 1.0); let projected = shadow_position.xyz / shadow_position.w; let uv = vec2(projected.x * 0.5 + 0.5, 0.5 - projected.y * 0.5); if (projected.z < 0.0 || projected.z > 1.0 || any(uv < vec2(0.0)) || any(uv > vec2(1.0)) ) { return 1.0; } let angle = 1.0 - clamp(abs(dot(normalized_normal, normalized_light)), 0.0, 1.0); let reference_depth = projected.z - metadata.params.y * (1.0 + angle * 2.0); let layer = i32(metadata.params.x); if (metadata.params.w < 0.5) { return textureSampleCompareLevel(shadow_maps, shadow_sampler, uv, layer, reference_depth); } let dimensions = vec2(textureDimensions(shadow_maps)); let texel = 1.0 / dimensions; let shadow_dx = metadata.view_projection * vec4(world_position_dx, 0.0); let shadow_dy = metadata.view_projection * vec4(world_position_dy, 0.0); let uv_dx = vec2(shadow_dx.x * 0.5, shadow_dx.y * -0.5); let uv_dy = vec2(shadow_dy.x * 0.5, shadow_dy.y * -0.5); let depth_dx = shadow_dx.z; let depth_dy = shadow_dy.z; let determinant = uv_dx.x * uv_dy.y - uv_dx.y * uv_dy.x; var depth_gradient = vec2(0.0); if (abs(determinant) > 1e-8) { depth_gradient = vec2( (depth_dx * uv_dy.y - depth_dy * uv_dx.y) / determinant, (uv_dx.x * depth_dy - uv_dy.x * depth_dx) / determinant ); } var visibility = 0.0; for (var y = -1; y <= 1; y++) { for (var x = -1; x <= 1; x++) { let offset = vec2(f32(x), f32(y)) * texel; visibility += textureSampleCompareLevel( shadow_maps, shadow_sampler, uv + offset, layer, reference_depth + dot(depth_gradient, offset) ); } } return visibility / 9.0; }"; // typescript/scaling/transform.ts var SCALE_UNIFORM_FLOAT_COUNT = 20; var modeToIdMap = { linear: 0, log: 1, symlog: 2 }; var clampModeToIdMap = { none: 0, range: 1, percentile: 2 }; var valueModeToIdMap = { component: 0, magnitude: 1 }; var scaleModeToId = (mode) => modeToIdMap[mode]; var scaleClampModeToId = (mode) => clampModeToIdMap[mode]; var scaleValueModeToId = (mode) => valueModeToIdMap[mode]; var cloneScaleTransform = (transform) => { return { mode: transform.mode, clampMode: transform.clampMode, valueMode: transform.valueMode, componentCount: transform.componentCount, componentIndex: transform.componentIndex, stride: transform.stride, offset: transform.offset, domainMin: transform.domainMin, domainMax: transform.domainMax, clampMin: transform.clampMin, clampMax: transform.clampMax, percentileLow: transform.percentileLow, percentileHigh: transform.percentileHigh, logBase: transform.logBase, symlogLinThresh: transform.symlogLinThresh, gamma: transform.gamma, invert: transform.invert }; }; var defaultScaleTransform = () => { return { mode: "linear", clampMode: "none", valueMode: "component", componentCount: 1, componentIndex: 0, stride: 1, offset: 0, domainMin: 0, domainMax: 1, clampMin: 0, clampMax: 1, percentileLow: 2, percentileHigh: 98, logBase: 10, symlogLinThresh: 1, gamma: 1, invert: false }; }; var normalizeScaleTransform = (descriptor) => { const defaults = defaultScaleTransform(); const mode = descriptor.mode ?? defaults.mode; const clampMode = descriptor.clampMode ?? defaults.clampMode; const valueMode = descriptor.valueMode ?? defaults.valueMode; assert(mode === "linear" || mode === "log" || mode === "symlog", `Invalid scale mode: ${String(mode)}`); assert(clampMode === "none" || clampMode === "range" || clampMode === "percentile", `Invalid scale clamp mode: ${String(clampMode)}`); assert(valueMode === "component" || valueMode === "magnitude", `Invalid scale value mode: ${String(valueMode)}`); const componentCount = clamp(intOr(descriptor.componentCount, defaults.componentCount), 1, 4); const componentIndex = clamp(intOr(descriptor.componentIndex, defaults.componentIndex), 0, 3); const stride = Math.max(componentCount, intOr(descriptor.stride, defaults.stride)); const offset = Math.max(0, intOr(descriptor.offset, defaults.offset)); const domainMin = finiteOr(descriptor.domainMin, defaults.domainMin); const domainMax = finiteOr(descriptor.domainMax, defaults.domainMax); const clampMin = finiteOr(descriptor.clampMin, defaults.clampMin); const clampMax = finiteOr(descriptor.clampMax, defaults.clampMax); const percentileLow = clamp(finiteOr(descriptor.percentileLow, defaults.percentileLow), 0, 100); const percentileHigh = clamp(finiteOr(descriptor.percentileHigh, defaults.percentileHigh), 0, 100); assert(percentileHigh > percentileLow, `Scale transform requires percentileHigh > percentileLow (got ${percentileLow}, ${percentileHigh})`); const logBase2 = Math.max(1.000001, finiteOr(descriptor.logBase, defaults.logBase)); const symlogLinThresh = Math.max(1e-20, finiteOr(descriptor.symlogLinThresh, defaults.symlogLinThresh)); const gamma = Math.max(1e-6, finiteOr(descriptor.gamma, defaults.gamma)); const invert = !!descriptor.invert; return { mode, clampMode, valueMode, componentCount, componentIndex, stride, offset, domainMin, domainMax, clampMin, clampMax, percentileLow, percentileHigh, logBase: logBase2, symlogLinThresh, gamma, invert }; }; var packScaleTransform = (transformIn, out, offset = 0) => { const transform = normalizeScaleTransform(transformIn); out[offset + 0] = transform.componentCount; out[offset + 1] = transform.componentIndex; out[offset + 2] = scaleValueModeToId(transform.valueMode); out[offset + 3] = transform.stride; out[offset + 4] = transform.domainMin; out[offset + 5] = transform.domainMax; out[offset + 6] = transform.offset; out[offset + 7] = scaleClampModeToId(transform.clampMode); out[offset + 8] = transform.clampMin; out[offset + 9] = transform.clampMax; out[offset + 10] = transform.percentileLow; out[offset + 11] = transform.percentileHigh; out[offset + 12] = scaleModeToId(transform.mode); out[offset + 13] = transform.logBase; out[offset + 14] = transform.symlogLinThresh; out[offset + 15] = transform.gamma; out[offset + 16] = transform.invert ? 1 : 0; out[offset + 17] = 0; out[offset + 18] = 0; out[offset + 19] = 0; }; var logBase = (x, base) => { const b = Math.max(1.000001, base); return Math.log(x) / Math.log(b); }; var applyScaleMode = (x, mode, symlogLinThresh, base) => { if (mode === "linear") return x; if (mode === "log") return logBase(Math.max(x, 1e-20), base); const lt = Math.max(symlogLinThresh, 1e-20); const sign = x >= 0 ? 1 : -1; const y = logBase(1 + Math.abs(x) / lt, base); return sign * y; }; var invertScaleMode = (x, mode, symlogLinThresh, base) => { if (mode === "linear") return x; if (mode === "log") return Math.pow(Math.max(base, 1.000001), x); const lt = Math.max(symlogLinThresh, 1e-20); const sign = x >= 0 ? 1 : -1; const y = Math.pow(Math.max(base, 1.000001), Math.abs(x)) - 1; return sign * (y * lt); }; var resolveScaleDomain = (transform) => { const hasClamp = transform.clampMode !== "none" && transform.clampMax > transform.clampMin; let domainMin = transform.domainMin; let domainMax = transform.domainMax; if (domainMax <= domainMin && hasClamp) { domainMin = transform.clampMin; domainMax = transform.clampMax; } return { domainMin, domainMax, clampMin: transform.clampMin, clampMax: transform.clampMax, hasClamp }; }; var resolveScaleTransformDomainCPU = (transformIn) => { return resolveScaleDomain(normalizeScaleTransform(transformIn)); }; var applyScaleTransformCPU = (value, transformIn) => { const transform = normalizeScaleTransform(transformIn); if (!Number.isFinite(value)) return Number.NaN; let v = value; const domain = resolveScaleDomain(transform); if (domain.hasClamp) v = clamp(v, domain.clampMin, domain.clampMax); const d0 = domain.domainMin; const d1 = domain.domainMax; const a = applyScaleMode(d0, transform.mode, transform.symlogLinThresh, transform.logBase); const b = applyScaleMode(d1, transform.mode, transform.symlogLinThresh, transform.logBase); const x = applyScaleMode(v, transform.mode, transform.symlogLinThresh, transform.logBase); const denom = Math.max(1e-20, b - a); let t = clamp01((x - a) / denom); t = Math.pow(t, transform.gamma); if (transform.invert) t = 1 - t; return clamp01(t); }; var invertScaleTransformCPU = (tIn, transformIn) => { const transform = normalizeScaleTransform(transformIn); const domain = resolveScaleDomain(transform); let t = clamp01(tIn); if (transform.invert) t = 1 - t; t = Math.pow(t, 1 / Math.max(transform.gamma, 1e-6)); const a = applyScaleMode(domain.domainMin, transform.mode, transform.symlogLinThresh, transform.logBase); const b = applyScaleMode(domain.domainMax, transform.mode, transform.symlogLinThresh, transform.logBase); const x = a + (b - a) * t; let v = invertScaleMode(x, transform.mode, transform.symlogLinThresh, transform.logBase); if (domain.hasClamp) v = clamp(v, domain.clampMin, domain.clampMax); return v; }; // typescript/scaling/service.ts var unwrapSourceBuffer = (source) => { return isGPUBuffer(source) ? source : source.buffer; }; var resolveByteLength = (source) => { if (isGPUBuffer(source)) return Number(source.size); return typeof source.byteLength === "number" ? source.byteLength : null; }; var percentileFromHistogram = (bins, percentile, minValue, maxValue, total) => { if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) return Number.NaN; if (total <= 0) return Number.NaN; if (maxValue <= minValue) return minValue; const p = clamp(percentile, 0, 100); const target = p / 100 * Math.max(0, total - 1); const binWidth = (maxValue - minValue) / bins.length; let cumulative = 0; for (let i = 0; i < bins.length; i++) { const c = bins[i] >>> 0; const next = cumulative + c; if (target < next) { const left = minValue + i * binWidth; if (c === 0) return left; const frac = (target - cumulative) / c; return left + clamp(frac, 0, 1) * binWidth; } cumulative = next; } return maxValue; }; var normalizeSource = (source) => { assert(Number.isInteger(source.count) && source.count >= 0, `Scale stats source.count must be an integer >= 0 (got ${source.count})`); const componentCountRaw = typeof source.componentCount === "number" && Number.isInteger(source.componentCount) ? source.componentCount : 1; const componentCount = clamp(componentCountRaw, 1, 4); const componentIndexRaw = typeof source.componentIndex === "number" && Number.isInteger(source.componentIndex) ? source.componentIndex : 0; const componentIndex = clamp(componentIndexRaw, 0, 3); const strideRaw = typeof source.stride === "number" && Number.isInteger(source.stride) ? source.stride : componentCount; const stride = Math.max(componentCount, strideRaw); const offsetRaw = typeof source.offset === "number" && Number.isInteger(source.offset) ? source.offset : 0; const offset = Math.max(0, offsetRaw); const revisionRaw = typeof source.revision === "number" && Number.isInteger(source.revision) ? source.revision : 0; const revision = Math.max(0, revisionRaw); const valueMode = source.valueMode ?? "component"; assert(valueMode === "component" || valueMode === "magnitude", `Invalid scale value mode: ${String(valueMode)}`); const byteLength = resolveByteLength(source.buffer); if (byteLength !== null) { const capacity = Math.floor(byteLength / 4); const required = source.count > 0 ? offset + (source.count - 1) * stride + componentCount : 0; assert(required <= capacity, `Scale stats source range exceeds source buffer capacity (required ${required} f32, capacity ${capacity} f32)`); } return { buffer: source.buffer, count: source.count, componentCount, componentIndex, valueMode, stride, offset, revision }; }; var ScaleService = class { compute; sourceIds = /* @__PURE__ */ new WeakMap(); cache = /* @__PURE__ */ new Map(); sourceCacheKeys = /* @__PURE__ */ new Map(); nextSourceId = 1; constructor(compute) { this.compute = compute; } createTransform(descriptor) { return normalizeScaleTransform(descriptor); } invalidate(sourceOrDescriptor) { const source = sourceOrDescriptor.buffer ? sourceOrDescriptor.buffer : sourceOrDescriptor; const keyObj = unwrapSourceBuffer(source); const sourceId = this.sourceIds.get(keyObj); if (sourceId === void 0) return; const keys = this.sourceCacheKeys.get(sourceId); if (!keys) return; for (const key of keys) this.cache.delete(key); this.sourceCacheKeys.delete(sourceId); } clearCache() { this.cache.clear(); this.sourceCacheKeys.clear(); } requestStats(request) { const source = normalizeSource(request.source); const low = clamp(request.percentiles?.low ?? 2, 0, 100); const high = clamp(request.percentiles?.high ?? 98, 0, 100); assert(high > low, `Scale stats requires percentile.high > percentile.low (got ${low}, ${high})`); const binsRaw = request.percentiles?.bins ?? 2048; const bins = Math.max(2, Math.floor(Number.isFinite(binsRaw) ? binsRaw : 2048)); const sourceId = this.getSourceId(unwrapSourceBuffer(source.buffer)); const key = [ sourceId, source.revision, source.count, source.componentCount, source.componentIndex, source.valueMode, source.stride, source.offset, request.percentiles ? 1 : 0, low, high, bins ].join("|"); const existing = this.cache.get(key); if (existing) return existing.promise; const job = this.computeStats(source, request.percentiles ? { low, high, bins } : null).catch((error) => { this.cache.delete(key); const sourceKeys = this.sourceCacheKeys.get(sourceId); sourceKeys?.delete(key); if (sourceKeys && sourceKeys.size === 0) this.sourceCacheKeys.delete(sourceId); throw error; }); this.cache.set(key, { promise: job }); let set = this.sourceCacheKeys.get(sourceId); if (!set) { set = /* @__PURE__ */ new Set(); this.sourceCacheKeys.set(sourceId, set); } set.add(key); return job; } getSourceId(obj) { const existing = this.sourceIds.get(obj); if (existing !== void 0) return existing; const id = this.nextSourceId++; this.sourceIds.set(obj, id); return id; } async computeStats(source, percentile) { const sourceBuffer = unwrapSourceBuffer(source.buffer); const extracted = this.compute.kernels.extractScaleValuesF32(sourceBuffer, { count: source.count, componentCount: source.componentCount, componentIndex: source.componentIndex, valueMode: source.valueMode, stride: source.stride, offset: source.offset }); const compact = this.compute.kernels.compactF32(extracted.values, extracted.flags, { count: source.count }); const finiteCount = await this.compute.readback.readScalarU32(compact.count); if (finiteCount === 0) { extracted.values.destroy(); extracted.flags.destroy(); compact.output.destroy(); compact.count.destroy(); return { count: source.count, finiteCount: 0, min: Number.NaN, max: Number.NaN, percentileMin: null, percentileMax: null, histogramBins: null }; } const minBuffer = this.compute.kernels.minF32(compact.output, { count: finiteCount }); const maxBuffer = this.compute.kernels.maxF32(compact.output, { count: finiteCount }); const min = await this.compute.readback.readScalarF32(minBuffer); const max = await this.compute.readback.readScalarF32(maxBuffer); let percentileMin = null; let percentileMax = null; let histogramBins = null; if (percentile) { const hist = this.compute.kernels.histogramF32(compact.output, percentile.bins, { count: finiteCount, minValue: min, maxValue: max, clear: true }); const binsData = await this.compute.readback.readAs(Uint32Array, hist); percentileMin = percentileFromHistogram(binsData, percentile.low, min, max, finiteCount); percentileMax = percentileFromHistogram(binsData, percentile.high, min, max, finiteCount); histogramBins = percentile.bins; hist.destroy(); } extracted.values.destroy(); extracted.flags.destroy(); compact.output.destroy(); compact.count.destroy(); minBuffer.destroy(); maxBuffer.destroy(); return { count: source.count, finiteCount, min, max, percentileMin, percentileMax, histogramBins }; } }; // typescript/wgsl/interop.ts var validateBinding = (binding, context) => { if (!Number.isInteger(binding) || binding < 0) throw new Error(`${context}: binding must be a non-negative integer (got ${binding})`); }; var storageBufferLayout = (opts) => { validateBinding(opts.binding, "webgpuInterop.storageBufferLayout"); return { binding: opts.binding, visibility: opts.visibility ?? GPUShaderStage.COMPUTE, buffer: { type: opts.readOnly ? "read-only-storage" : "storage", hasDynamicOffset: opts.hasDynamicOffset ?? false, minBindingSize: opts.minBindingSize } }; }; var uniformBufferLayout = (opts) => { validateBinding(opts.binding, "webgpuInterop.uniformBufferLayout"); return { binding: opts.binding, visibility: opts.visibility ?? GPUShaderStage.COMPUTE, buffer: { type: "uniform", hasDynamicOffset: opts.hasDynamicOffset ?? false, minBindingSize: opts.minBindingSize } }; }; var samplerLayout = (opts) => { validateBinding(opts.binding, "webgpuInterop.samplerLayout"); return { binding: opts.binding, visibility: opts.visibility ?? GPUShaderStage.FRAGMENT, sampler: { type: opts.type ?? "filtering" } }; }; var textureLayout = (opts) => { validateBinding(opts.binding, "webgpuInterop.textureLayout"); return { binding: opts.binding, visibility: opts.visibility ?? GPUShaderStage.FRAGMENT, texture: { sampleType: opts.sampleType ?? "float", viewDimension: opts.viewDimension ?? "2d", multisampled: opts.multisampled ?? false } }; }; var normalizeBindGroupLayout = (descriptor, context = "WebGPU bind group layout") => { if (!descriptor || !Array.isArray(descriptor.entries)) throw new Error(`${context}: entries must be an array`); const seen = /* @__PURE__ */ new Set(); const entries = descriptor.entries.map((entry) => { validateBinding(entry.binding, context); if (seen.has(entry.binding)) throw new Error(`${context}: duplicate binding ${entry.binding}`); seen.add(entry.binding); const kindCount = Number(!!entry.buffer) + Number(!!entry.sampler) + Number(!!entry.texture) + Number(!!entry.storageTexture) + Number(!!entry.externalTexture); if (kindCount !== 1) throw new Error(`${context}: binding ${entry.binding} must define exactly one WebGPU resource layout`); return { ...entry, buffer: entry.buffer ? { ...entry.buffer } : void 0, sampler: entry.sampler ? { ...entry.sampler } : void 0, texture: entry.texture ? { ...entry.texture } : void 0, storageTexture: entry.storageTexture ? { ...entry.storageTexture } : void 0, externalTexture: entry.externalTexture ? { ...entry.externalTexture } : void 0 }; }); return { label: descriptor.label, entries }; }; var resolveBuffer = (resource) => { if (isGPUBuffer(resource)) return resource; const buffer = resource.buffer; if (!isGPUBuffer(buffer)) throw new Error("WebGPU binding resource: expected a GPUBuffer or WasmGPU buffer wrapper"); return buffer; }; var normalizeBindingResource = (resource) => { if (!resource || typeof resource !== "object") throw new Error("WebGPU binding resource: resource must be a WebGPU object or buffer binding"); if (isGPUBuffer(resource)) return { buffer: resource }; if ("buffer" in resource) { const binding = resource; if (binding.offset !== void 0 && (!Number.isInteger(binding.offset) || binding.offset < 0)) throw new Error(`WebGPU buffer binding: offset must be a non-negative integer (got ${binding.offset})`); if (binding.size !== void 0 && (!Number.isInteger(binding.size) || binding.size <= 0)) throw new Error(`WebGPU buffer binding: size must be a positive integer (got ${binding.size})`); return { buffer: resolveBuffer(binding.buffer), offset: binding.offset, size: binding.size }; } return resource; }; var normalizeBindGroupResources = (resources, context = "WebGPU bind group resources") => { if (!resources || typeof resources !== "object") throw new Error(`${context}: resources must be a binding record or array`); const seen = /* @__PURE__ */ new Set(); const entries = []; const add = (binding, resource) => { validateBinding(binding, context); if (seen.has(binding)) throw new Error(`${context}: duplicate binding ${binding}`); seen.add(binding); entries.push({ binding, resource: normalizeBindingResource(resource) }); }; if (Array.isArray(resources)) for (const entry of resources) add(entry.binding, entry.resource); else for (const key of Object.keys(resources)) { const binding = Number(key); if (!Number.isInteger(binding) || String(binding) !== key) throw new Error(`${context}: invalid binding key '${key}'`); add(binding, resources[binding]); } return entries; }; var validateResourcesForLayout = (layout, resources, context = "WebGPU bind group") => { const normalizedLayout = normalizeBindGroupLayout(layout, `${context} layout`); const entries = normalizeBindGroupResources(resources, `${context} resources`); const expected = new Set(normalizedLayout.entries.map((entry) => entry.binding)); for (const entry of entries) if (!expected.delete(entry.binding)) throw new Error(`${context}: resource binding ${entry.binding} is not declared by the layout`); if (expected.size > 0) throw new Error(`${context}: missing resources for bindings ${Array.from(expected).join(", ")}`); return entries; }; var webgpuInterop = Object.freeze({ storageBufferLayout, uniformBufferLayout, samplerLayout, textureLayout, bindGroupLayout: normalizeBindGroupLayout, bindingResource: normalizeBindingResource, bindGroupResources: normalizeBindGroupResources }); // typescript/graphics/material.ts var normalizeTextureTransform = (descriptor) => { const texCoord = descriptor?.texCoord === 1 ? 1 : 0; return { offset: [descriptor?.offset?.[0] ?? 0, descriptor?.offset?.[1] ?? 0], rotation: descriptor?.rotation ?? 0, scale: [descriptor?.scale?.[0] ?? 1, descriptor?.scale?.[1] ?? 1], texCoord }; }; var cloneTextureTransform = (transform) => { return { offset: [transform.offset[0], transform.offset[1]], rotation: transform.rotation, scale: [transform.scale[0], transform.scale[1]], texCoord: transform.texCoord }; }; var DEFAULT_TEXTURE_TRANSFORM = normalizeTextureTransform(null); var packTextureTransform = (f, offset, transform) => { const cos = Math.cos(transform.rotation); const sin = Math.sin(transform.rotation); f[offset + 0] = transform.offset[0]; f[offset + 1] = transform.offset[1]; f[offset + 2] = cos; f[offset + 3] = sin; f[offset + 4] = transform.scale[0]; f[offset + 5] = transform.scale[1]; f[offset + 6] = transform.texCoord; f[offset + 7] = 0; }; var BlendMode = /* @__PURE__ */ ((BlendMode2) => { BlendMode2["Opaque"] = "opaque"; BlendMode2["Transparent"] = "transparent"; BlendMode2["Additive"] = "additive"; return BlendMode2; })(BlendMode || {}); var CullMode = /* @__PURE__ */ ((CullMode2) => { CullMode2["None"] = "none"; CullMode2["Back"] = "back"; CullMode2["Front"] = "front"; return CullMode2; })(CullMode || {}); var Material = class { label; blendMode; cullMode; depthWrite; depthTest; pipeline = null; bindGroup = null; bindGroupKey = null; uniformBuffer = null; _uniformDataCache = null; _dirty = true; _refCount = 1; _destroyed = false; constructor(descriptor = {}) { this.label = descriptor.label; this.blendMode = descriptor.blendMode ?? "opaque" /* Opaque */; this.cullMode = descriptor.cullMode ?? "back" /* Back */; this.depthWrite = descriptor.depthWrite ?? true; this.depthTest = descriptor.depthTest ?? true; } get dirty() { return this._dirty; } assertAlive(action) { if (this._destroyed) throw new Error(`Material: cannot ${action}; resource has already been released.`); } retain() { this.assertAlive("retain"); this._refCount++; return this; } release() { if (this._destroyed) throw new Error("Material: release() called after the resource was already released."); if (this._refCount <= 0) throw new Error("Material: reference count underflow."); this._refCount--; if (this._refCount > 0) return; this._destroyed = true; this.disposeResources(); } markClean() { this.assertAlive("markClean"); this._dirty = false; } getUniformDataCache(floatCount) { this.assertAlive("build uniform data"); if (!this._uniformDataCache || this._uniformDataCache.length !== floatCount) this._uniformDataCache = new Float32Array(floatCount); return this._uniformDataCache; } destroy() { this.release(); } disposeResources() { this.uniformBuffer?.destroy(); this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this.pipeline = null; this._uniformDataCache = null; this._dirty = true; } }; var UnlitMaterial = class _UnlitMaterial extends Material { _color; _opacity; _baseColorTexture; _baseColorTextureTransform; _alphaCutoff; static _cachedBindGroupLayout = null; static _cachedLayoutDevice = null; constructor(descriptor = {}) { super({ ...descriptor, blendMode: descriptor.blendMode ?? ((descriptor.opacity ?? 1) < 1 ? "transparent" /* Transparent */ : "opaque" /* Opaque */) }); this._color = descriptor.color ?? [1, 1, 1]; this._opacity = descriptor.opacity ?? 1; this._baseColorTexture = descriptor.baseColorTexture ?? null; this._baseColorTextureTransform = normalizeTextureTransform(descriptor.baseColorTextureTransform); this._alphaCutoff = descriptor.alphaCutoff ?? 0; } get color() { return this._color; } set color(value) { this._color = value; this._dirty = true; } get opacity() { return this._opacity; } set opacity(value) { this._opacity = value; this._dirty = true; } get baseColorTexture() { return this._baseColorTexture; } set baseColorTexture(value) { this._baseColorTexture = value; this._dirty = true; } get baseColorTextureTransform() { return cloneTextureTransform(this._baseColorTextureTransform); } set baseColorTextureTransform(value) { this._baseColorTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get alphaCutoff() { return this._alphaCutoff; } set alphaCutoff(value) { this._alphaCutoff = value; this._dirty = true; } getUniformBufferSize() { return 64; } getUniformData() { const f = this.getUniformDataCache(16); f[0] = this._color[0]; f[1] = this._color[1]; f[2] = this._color[2]; f[3] = this._opacity; f[4] = this._alphaCutoff; f[5] = 0; f[6] = 0; f[7] = 0; packTextureTransform(f, 8, this._baseColorTextureTransform); return f; } createBindGroupLayout(device) { if (_UnlitMaterial._cachedBindGroupLayout && _UnlitMaterial._cachedLayoutDevice === device) return _UnlitMaterial._cachedBindGroupLayout; const layout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } } ] }); _UnlitMaterial._cachedBindGroupLayout = layout; _UnlitMaterial._cachedLayoutDevice = device; return layout; } getShaderCode(opts = {}) { if (opts.instanced) return unlit_instanced_default; if (opts.skinned8) return unlit_skinned8_default; if (opts.skinned) return unlit_skinned_default; return unlit_default; } }; var cloneColor = (value, fallback) => { return [value?.[0] ?? fallback[0], value?.[1] ?? fallback[1], value?.[2] ?? fallback[2]]; }; var normalizeStandardMaterialExtensions = (descriptor) => { return { clearcoat: descriptor?.clearcoat ? { factor: descriptor.clearcoat.factor ?? 0, texture: descriptor.clearcoat.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.clearcoat.textureTransform), roughness: descriptor.clearcoat.roughness ?? 0, roughnessTexture: descriptor.clearcoat.roughnessTexture ?? null, roughnessTextureTransform: normalizeTextureTransform(descriptor.clearcoat.roughnessTextureTransform), normalTexture: descriptor.clearcoat.normalTexture ?? null, normalTextureTransform: normalizeTextureTransform(descriptor.clearcoat.normalTextureTransform), normalScale: descriptor.clearcoat.normalScale ?? 1 } : null, transmission: descriptor?.transmission ? { factor: descriptor.transmission.factor ?? 0, texture: descriptor.transmission.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.transmission.textureTransform) } : null, volume: descriptor?.volume ? { thicknessFactor: descriptor.volume.thicknessFactor ?? 0, thicknessTexture: descriptor.volume.thicknessTexture ?? null, thicknessTextureTransform: normalizeTextureTransform(descriptor.volume.thicknessTextureTransform), attenuationDistance: descriptor.volume.attenuationDistance ?? Infinity, attenuationColor: cloneColor(descriptor.volume.attenuationColor, [1, 1, 1]) } : null, specular: descriptor?.specular ? { factor: descriptor.specular.factor ?? 1, texture: descriptor.specular.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.specular.textureTransform), color: cloneColor(descriptor.specular.color, [1, 1, 1]), colorTexture: descriptor.specular.colorTexture ?? null, colorTextureTransform: normalizeTextureTransform(descriptor.specular.colorTextureTransform) } : null, sheen: descriptor?.sheen ? { color: cloneColor(descriptor.sheen.color, [0, 0, 0]), colorTexture: descriptor.sheen.colorTexture ?? null, colorTextureTransform: normalizeTextureTransform(descriptor.sheen.colorTextureTransform), roughness: descriptor.sheen.roughness ?? 0, roughnessTexture: descriptor.sheen.roughnessTexture ?? null, roughnessTextureTransform: normalizeTextureTransform(descriptor.sheen.roughnessTextureTransform) } : null, iridescence: descriptor?.iridescence ? { factor: descriptor.iridescence.factor ?? 0, texture: descriptor.iridescence.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.iridescence.textureTransform), ior: descriptor.iridescence.ior ?? 1.3, thicknessMinimum: descriptor.iridescence.thicknessMinimum ?? 100, thicknessMaximum: descriptor.iridescence.thicknessMaximum ?? 400, thicknessTexture: descriptor.iridescence.thicknessTexture ?? null, thicknessTextureTransform: normalizeTextureTransform(descriptor.iridescence.thicknessTextureTransform) } : null, anisotropy: descriptor?.anisotropy ? { strength: descriptor.anisotropy.strength ?? 0, rotation: descriptor.anisotropy.rotation ?? 0, texture: descriptor.anisotropy.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.anisotropy.textureTransform) } : null, diffuseTransmission: descriptor?.diffuseTransmission ? { factor: descriptor.diffuseTransmission.factor ?? 0, texture: descriptor.diffuseTransmission.texture ?? null, textureTransform: normalizeTextureTransform(descriptor.diffuseTransmission.textureTransform), color: cloneColor(descriptor.diffuseTransmission.color, [1, 1, 1]), colorTexture: descriptor.diffuseTransmission.colorTexture ?? null, colorTextureTransform: normalizeTextureTransform(descriptor.diffuseTransmission.colorTextureTransform) } : null, dispersion: descriptor?.dispersion ? { dispersion: descriptor.dispersion.dispersion ?? 0 } : null, ior: descriptor?.ior ? { ior: descriptor.ior.ior ?? 1.5 } : null, emissiveStrength: descriptor?.emissiveStrength ? { strength: descriptor.emissiveStrength.strength ?? 1 } : null }; }; var cloneStandardMaterialExtensions = (extensions) => normalizeStandardMaterialExtensions(extensions); var WEBGPU_BASELINE_MAX_SAMPLED_TEXTURES_PER_SHADER_STAGE = 16; var WEBGPU_BASELINE_MAX_SAMPLERS_PER_SHADER_STAGE = 16; var STANDARD_MATERIAL_TEXTURE_SLOTS = [ { slot: "baseColor", feature: 1 /* BaseColorTexture */, shaderName: "base_color", colorSpace: "srgb" }, { slot: "metallicRoughness", feature: 2 /* MetallicRoughnessTexture */, shaderName: "metallic_roughness", colorSpace: "linear" }, { slot: "normal", feature: 4 /* NormalTexture */, shaderName: "normal", colorSpace: "linear" }, { slot: "occlusion", feature: 8 /* OcclusionTexture */, shaderName: "occlusion", colorSpace: "linear" }, { slot: "emissive", feature: 16 /* EmissiveTexture */, shaderName: "emissive", colorSpace: "srgb" }, { slot: "clearcoat", feature: 64 /* ClearcoatTexture */, shaderName: "clearcoat", colorSpace: "linear" }, { slot: "clearcoatRoughness", feature: 128 /* ClearcoatRoughnessTexture */, shaderName: "clearcoat_roughness", colorSpace: "linear" }, { slot: "clearcoatNormal", feature: 256 /* ClearcoatNormalTexture */, shaderName: "clearcoat_normal", colorSpace: "linear" }, { slot: "specular", feature: 16384 /* SpecularTexture */, shaderName: "specular", colorSpace: "linear" }, { slot: "specularColor", feature: 32768 /* SpecularColorTexture */, shaderName: "specular_color", colorSpace: "srgb" }, { slot: "sheenColor", feature: 131072 /* SheenColorTexture */, shaderName: "sheen_color", colorSpace: "srgb" }, { slot: "sheenRoughness", feature: 262144 /* SheenRoughnessTexture */, shaderName: "sheen_roughness", colorSpace: "linear" }, { slot: "iridescence", feature: 1048576 /* IridescenceTexture */, shaderName: "iridescence", colorSpace: "linear" }, { slot: "iridescenceThickness", feature: 2097152 /* IridescenceThicknessTexture */, shaderName: "iridescence_thickness", colorSpace: "linear" }, { slot: "anisotropy", feature: 8388608 /* AnisotropyTexture */, shaderName: "anisotropy", colorSpace: "linear" }, { slot: "transmission", feature: 1024 /* TransmissionTexture */, shaderName: "transmission", colorSpace: "linear" }, { slot: "volumeThickness", feature: 4096 /* ThicknessTexture */, shaderName: "volume_thickness", colorSpace: "linear" }, { slot: "diffuseTransmission", feature: 134217728 /* DiffuseTransmissionTexture */, shaderName: "diffuse_transmission", colorSpace: "linear" }, { slot: "diffuseTransmissionColor", feature: 268435456 /* DiffuseTransmissionColorTexture */, shaderName: "diffuse_transmission_color", colorSpace: "srgb" }, { slot: "transmissionSource", feature: null, shaderName: "transmission_source", colorSpace: "linear" } ]; var STANDARD_MATERIAL_TEXTURE_SLOT_BY_SHADER_NAME = new Map(STANDARD_MATERIAL_TEXTURE_SLOTS.map((definition) => [definition.shaderName, definition])); var STANDARD_MATERIAL_TEXTURE_SLOT_BY_NAME = new Map(STANDARD_MATERIAL_TEXTURE_SLOTS.map((definition) => [definition.slot, definition])); var getStandardMaterialTextureColorSpace = (slot) => { const definition = STANDARD_MATERIAL_TEXTURE_SLOT_BY_NAME.get(slot); if (!definition) throw new Error(`StandardMaterial: unknown texture slot '${slot}'.`); return definition.colorSpace; }; var planStandardMaterialLayout = (featureMask) => { const mask = featureMask >>> 0; const usesTransmission = (mask & (512 /* Transmission */ | 67108864 /* DiffuseTransmission */)) !== 0; const bindings = []; for (let index = 0; index < STANDARD_MATERIAL_TEXTURE_SLOTS.length; index++) { const definition = STANDARD_MATERIAL_TEXTURE_SLOTS[index]; const active = definition.feature === null ? usesTransmission : (mask & definition.feature) !== 0; if (!active) continue; const binding = Object.freeze({ slot: definition.slot, samplerBinding: 1 + index * 2, textureBinding: 2 + index * 2, colorSpace: definition.colorSpace }); bindings.push(binding); } const featureKey = `${usesTransmission ? "transmission" : "standard"}:${bindings.map((binding) => binding.slot).join(",")}`; return Object.freeze({ featureKey, bindings: Object.freeze(bindings), sampledTextureCount: bindings.length, samplerCount: bindings.length, usesTransmission }); }; var STANDARD_SHADER_SAMPLE_DEFAULTS = /* @__PURE__ */ new Map(); for (const match of standard_defaults_default.matchAll(/const\s+(standard_default_([A-Za-z0-9_]+))\s*=/g)) STANDARD_SHADER_SAMPLE_DEFAULTS.set(match[2], match[1]); var standardShaderSourceCache = /* @__PURE__ */ new Map(); var STANDARD_DIRECT_VISIBILITY_HOOK = "fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return 1.0; }"; var STANDARD_SHADOW_VISIBILITY_HOOK = "fn standard_direct_visibility(light_index: u32, world_position: vec3, geometric_normal: vec3, light_direction: vec3, world_position_dx: vec3, world_position_dy: vec3) -> f32 { return shadow_visibility(light_index, world_position, geometric_normal, light_direction, world_position_dx, world_position_dy); }"; var specializeStandardShader = (source, plan, variant, shadows = false, shadowGroup = 2) => { const cacheKey = `${plan.featureKey}|${variant}|${shadows ? `shadows:${shadowGroup}` : "no-shadows"}`; const cached = standardShaderSourceCache.get(cacheKey); if (cached) return cached; const activeSlots = new Set(plan.bindings.map((binding) => binding.slot)); const samplePattern = /textureSample\(\s*([A-Za-z0-9_]+)_tex\s*,\s*\1_sampler\s*,\s*[A-Za-z0-9_]+\s*,?\s*\)/g; let specialized = source.replace(samplePattern, (sample, shaderName) => { const definition = STANDARD_MATERIAL_TEXTURE_SLOT_BY_SHADER_NAME.get(shaderName); if (!definition || activeSlots.has(definition.slot)) return sample; const fallback = STANDARD_SHADER_SAMPLE_DEFAULTS.get(shaderName); if (!fallback) throw new Error(`StandardMaterial: canonical ${variant} WGSL has no imported default for ${definition.slot}.`); return fallback; }); for (const definition of STANDARD_MATERIAL_TEXTURE_SLOTS) { if (activeSlots.has(definition.slot) || definition.slot === "transmissionSource") continue; const unresolvedSample = new RegExp(`\\btextureSample\\s*\\(\\s*${definition.shaderName}_tex\\b`); if (unresolvedSample.test(specialized)) throw new Error(`StandardMaterial: canonical ${variant} WGSL contains an unsupported ${definition.slot} sampling path.`); } if (shadows) { const receiver = shadowGroup === 2 ? shadow_receiver_default : shadow_receiver_default.replaceAll("@group(2)", `@group(${shadowGroup})`); const hookCount = specialized.split(STANDARD_DIRECT_VISIBILITY_HOOK).length - 1; if (hookCount !== 1) throw new Error(`StandardMaterial: canonical ${variant} WGSL must contain exactly one direct-visibility hook; found ${hookCount}.`); specialized = receiver.concat(specialized.replace(STANDARD_DIRECT_VISIBILITY_HOOK, STANDARD_SHADOW_VISIBILITY_HOOK)); } specialized = standard_defaults_default.concat(specialized); standardShaderSourceCache.set(cacheKey, specialized); return specialized; }; var getSpecializedStandardShader = (plan, opts = {}) => { const transmission = plan.usesTransmission; const shadowGroup = opts.shadowGroup ?? (opts.skinned || opts.skinned8 ? 3 : 2); if (opts.instanced) return specializeStandardShader(transmission ? standard_transmission_instanced_default : standard_instanced_default, plan, transmission ? "transmission-instanced" : "instanced", opts.shadows, shadowGroup); if (opts.skinned8) return specializeStandardShader(transmission ? standard_transmission_skinned8_default : standard_skinned8_default, plan, transmission ? "transmission-skinned8" : "skinned8", opts.shadows, shadowGroup); if (opts.skinned) return specializeStandardShader(transmission ? standard_transmission_skinned_default : standard_skinned_default, plan, transmission ? "transmission-skinned" : "skinned", opts.shadows, shadowGroup); return specializeStandardShader(transmission ? standard_transmission_default : standard_default, plan, transmission ? "transmission" : "standard", opts.shadows, shadowGroup); }; var standardMaterialBindGroupLayouts = /* @__PURE__ */ new WeakMap(); var getMaterialTextureForSlot = (material, slot) => { const ext = material.extensions; switch (slot) { case "baseColor": return material.baseColorTexture; case "metallicRoughness": return material.metallicRoughnessTexture; case "normal": return material.normalTexture; case "occlusion": return material.occlusionTexture; case "emissive": return material.emissiveTexture; case "clearcoat": return ext.clearcoat?.texture ?? null; case "clearcoatRoughness": return ext.clearcoat?.roughnessTexture ?? null; case "clearcoatNormal": return ext.clearcoat?.normalTexture ?? null; case "specular": return ext.specular?.texture ?? null; case "specularColor": return ext.specular?.colorTexture ?? null; case "sheenColor": return ext.sheen?.colorTexture ?? null; case "sheenRoughness": return ext.sheen?.roughnessTexture ?? null; case "iridescence": return ext.iridescence?.texture ?? null; case "iridescenceThickness": return ext.iridescence?.thicknessTexture ?? null; case "anisotropy": return ext.anisotropy?.texture ?? null; case "transmission": return ext.transmission?.texture ?? null; case "volumeThickness": return ext.volume?.thicknessTexture ?? null; case "diffuseTransmission": return ext.diffuseTransmission?.texture ?? null; case "diffuseTransmissionColor": return ext.diffuseTransmission?.colorTexture ?? null; case "transmissionSource": return null; } }; var StandardMaterial = class _StandardMaterial extends Material { _color; _opacity; _metallic; _roughness; _emissive; _emissiveIntensity; _baseColorTexture; _metallicRoughnessTexture; _normalTexture; _occlusionTexture; _emissiveTexture; _baseColorTextureTransform; _metallicRoughnessTextureTransform; _normalTextureTransform; _occlusionTextureTransform; _emissiveTextureTransform; _normalScale; _occlusionStrength; _alphaCutoff; _extensions; _layoutPlan = null; static UNIFORM_FLOAT_COUNT = 204; constructor(descriptor = {}) { super({ ...descriptor, blendMode: descriptor.blendMode ?? ((descriptor.opacity ?? 1) < 1 ? "transparent" /* Transparent */ : "opaque" /* Opaque */) }); this._color = descriptor.color ?? [1, 1, 1]; this._opacity = descriptor.opacity ?? 1; this._metallic = descriptor.metallic ?? 0; this._roughness = descriptor.roughness ?? 1; this._emissive = descriptor.emissive ?? [0, 0, 0]; this._emissiveIntensity = descriptor.emissiveIntensity ?? 0; this._baseColorTexture = descriptor.baseColorTexture ?? null; this._metallicRoughnessTexture = descriptor.metallicRoughnessTexture ?? null; this._normalTexture = descriptor.normalTexture ?? null; this._occlusionTexture = descriptor.occlusionTexture ?? null; this._emissiveTexture = descriptor.emissiveTexture ?? null; this._baseColorTextureTransform = normalizeTextureTransform(descriptor.baseColorTextureTransform); this._metallicRoughnessTextureTransform = normalizeTextureTransform(descriptor.metallicRoughnessTextureTransform); this._normalTextureTransform = normalizeTextureTransform(descriptor.normalTextureTransform); this._occlusionTextureTransform = normalizeTextureTransform(descriptor.occlusionTextureTransform); this._emissiveTextureTransform = normalizeTextureTransform(descriptor.emissiveTextureTransform); this._normalScale = descriptor.normalScale ?? 1; this._occlusionStrength = descriptor.occlusionStrength ?? 1; this._alphaCutoff = descriptor.alphaCutoff ?? 0; this._extensions = normalizeStandardMaterialExtensions(descriptor.extensions); } invalidateBindings() { this._layoutPlan = null; this.bindGroupKey = null; this._dirty = true; } getLayoutPlan() { if (!this._layoutPlan) this._layoutPlan = planStandardMaterialLayout(this.getFeatureMask()); return this._layoutPlan; } get color() { return this._color; } set color(value) { this._color = value; this._dirty = true; } get opacity() { return this._opacity; } set opacity(value) { this._opacity = value; this._dirty = true; } get metallic() { return this._metallic; } set metallic(value) { this._metallic = Math.max(0, Math.min(1, value)); this._dirty = true; } get roughness() { return this._roughness; } set roughness(value) { this._roughness = Math.max(0, Math.min(1, value)); this._dirty = true; } get emissive() { return this._emissive; } set emissive(value) { this._emissive = value; this._dirty = true; } get emissiveIntensity() { return this._emissiveIntensity; } set emissiveIntensity(value) { this._emissiveIntensity = value; this._dirty = true; } get baseColorTexture() { return this._baseColorTexture; } set baseColorTexture(value) { this._baseColorTexture = value; this.invalidateBindings(); } get metallicRoughnessTexture() { return this._metallicRoughnessTexture; } set metallicRoughnessTexture(value) { this._metallicRoughnessTexture = value; this.invalidateBindings(); } get normalTexture() { return this._normalTexture; } set normalTexture(value) { this._normalTexture = value; this.invalidateBindings(); } get occlusionTexture() { return this._occlusionTexture; } set occlusionTexture(value) { this._occlusionTexture = value; this.invalidateBindings(); } get emissiveTexture() { return this._emissiveTexture; } set emissiveTexture(value) { this._emissiveTexture = value; this.invalidateBindings(); } get baseColorTextureTransform() { return cloneTextureTransform(this._baseColorTextureTransform); } set baseColorTextureTransform(value) { this._baseColorTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get metallicRoughnessTextureTransform() { return cloneTextureTransform(this._metallicRoughnessTextureTransform); } set metallicRoughnessTextureTransform(value) { this._metallicRoughnessTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get normalTextureTransform() { return cloneTextureTransform(this._normalTextureTransform); } set normalTextureTransform(value) { this._normalTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get occlusionTextureTransform() { return cloneTextureTransform(this._occlusionTextureTransform); } set occlusionTextureTransform(value) { this._occlusionTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get emissiveTextureTransform() { return cloneTextureTransform(this._emissiveTextureTransform); } set emissiveTextureTransform(value) { this._emissiveTextureTransform = normalizeTextureTransform(value); this._dirty = true; } get normalScale() { return this._normalScale; } set normalScale(value) { this._normalScale = value; this._dirty = true; } get occlusionStrength() { return this._occlusionStrength; } set occlusionStrength(value) { this._occlusionStrength = value; this._dirty = true; } get alphaCutoff() { return this._alphaCutoff; } set alphaCutoff(value) { this._alphaCutoff = value; this._dirty = true; } get extensions() { return cloneStandardMaterialExtensions(this._extensions); } setExtensions(descriptor) { const previousPlan = this.getLayoutPlan(); const previousTextures = previousPlan.bindings.map((binding) => getMaterialTextureForSlot(this, binding.slot)); this._extensions = normalizeStandardMaterialExtensions(descriptor); const nextPlan = planStandardMaterialLayout(this.getFeatureMask()); const sameLayout = previousPlan.featureKey === nextPlan.featureKey; const sameResources = sameLayout && nextPlan.bindings.every((binding, index) => getMaterialTextureForSlot(this, binding.slot) === previousTextures[index]); this._layoutPlan = nextPlan; if (!sameResources) this.bindGroupKey = null; this._dirty = true; return this; } getFeatureMask() { let mask = 0; if (this._baseColorTexture) mask |= 1 /* BaseColorTexture */; if (this._metallicRoughnessTexture) mask |= 2 /* MetallicRoughnessTexture */; if (this._normalTexture) mask |= 4 /* NormalTexture */; if (this._occlusionTexture) mask |= 8 /* OcclusionTexture */; if (this._emissiveTexture) mask |= 16 /* EmissiveTexture */; const clearcoat = this._extensions.clearcoat; if (clearcoat) { if (clearcoat.texture) mask |= 64 /* ClearcoatTexture */; if (clearcoat.roughnessTexture) mask |= 128 /* ClearcoatRoughnessTexture */; if (clearcoat.normalTexture) mask |= 256 /* ClearcoatNormalTexture */; } const transmission = this._extensions.transmission; if (transmission) { mask |= 512 /* Transmission */; if (transmission.texture) mask |= 1024 /* TransmissionTexture */; } const volume = this._extensions.volume; if (volume) { if (volume.thicknessTexture) mask |= 4096 /* ThicknessTexture */; } const specular = this._extensions.specular; if (specular) { if (specular.texture) mask |= 16384 /* SpecularTexture */; if (specular.colorTexture) mask |= 32768 /* SpecularColorTexture */; } const sheen = this._extensions.sheen; if (sheen) { if (sheen.colorTexture) mask |= 131072 /* SheenColorTexture */; if (sheen.roughnessTexture) mask |= 262144 /* SheenRoughnessTexture */; } const iridescence = this._extensions.iridescence; if (iridescence) { if (iridescence.texture) mask |= 1048576 /* IridescenceTexture */; if (iridescence.thicknessTexture) mask |= 2097152 /* IridescenceThicknessTexture */; } const anisotropy = this._extensions.anisotropy; if (anisotropy) { if (anisotropy.texture) mask |= 8388608 /* AnisotropyTexture */; } const diffuseTransmission = this._extensions.diffuseTransmission; if (diffuseTransmission) { mask |= 67108864 /* DiffuseTransmission */; if (diffuseTransmission.texture) mask |= 134217728 /* DiffuseTransmissionTexture */; if (diffuseTransmission.colorTexture) mask |= 268435456 /* DiffuseTransmissionColorTexture */; } return mask >>> 0; } getUniformBufferSize() { return _StandardMaterial.UNIFORM_FLOAT_COUNT * 4; } getUniformData() { const f = this.getUniformDataCache(_StandardMaterial.UNIFORM_FLOAT_COUNT); f[0] = this._color[0]; f[1] = this._color[1]; f[2] = this._color[2]; f[3] = this._opacity; f[4] = this._emissive[0]; f[5] = this._emissive[1]; f[6] = this._emissive[2]; f[7] = this._emissiveIntensity; f[8] = this._metallic; f[9] = this._roughness; f[10] = this._normalTexture ? this._normalScale : 0; f[11] = this._occlusionStrength; f[12] = this._alphaCutoff; f[13] = 0; f[14] = 0; f[15] = 0; packTextureTransform(f, 16, this._baseColorTextureTransform); packTextureTransform(f, 24, this._metallicRoughnessTextureTransform); packTextureTransform(f, 32, this._normalTextureTransform); packTextureTransform(f, 40, this._occlusionTextureTransform); packTextureTransform(f, 48, this._emissiveTextureTransform); const clearcoat = this._extensions.clearcoat; const specular = this._extensions.specular; const sheen = this._extensions.sheen; const iridescence = this._extensions.iridescence; const anisotropy = this._extensions.anisotropy; const transmission = this._extensions.transmission; const volume = this._extensions.volume; const diffuseTransmission = this._extensions.diffuseTransmission; const dispersion = this._extensions.dispersion; const ior = this._extensions.ior; const emissiveStrength = this._extensions.emissiveStrength; f[56] = clearcoat?.factor ?? 0; f[57] = clearcoat?.roughness ?? 0; f[58] = clearcoat?.normalTexture ? clearcoat.normalScale ?? 1 : 0; f[59] = 0; f[60] = specular?.factor ?? 1; f[61] = specular?.color[0] ?? 1; f[62] = specular?.color[1] ?? 1; f[63] = specular?.color[2] ?? 1; f[64] = ior?.ior ?? 1.5; f[65] = emissiveStrength?.strength ?? 1; f[66] = 0; f[67] = 0; packTextureTransform(f, 68, clearcoat?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 76, clearcoat?.roughnessTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 84, clearcoat?.normalTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 92, specular?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 100, specular?.colorTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); f[108] = sheen?.color[0] ?? 0; f[109] = sheen?.color[1] ?? 0; f[110] = sheen?.color[2] ?? 0; f[111] = sheen?.roughness ?? 0; f[112] = iridescence?.factor ?? 0; f[113] = iridescence?.ior ?? 1.3; f[114] = iridescence?.thicknessMinimum ?? 100; f[115] = iridescence?.thicknessMaximum ?? 400; f[116] = anisotropy?.strength ?? 0; f[117] = Math.cos(anisotropy?.rotation ?? 0); f[118] = Math.sin(anisotropy?.rotation ?? 0); f[119] = 0; packTextureTransform(f, 120, sheen?.colorTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 128, sheen?.roughnessTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 136, iridescence?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 144, iridescence?.thicknessTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 152, anisotropy?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); f[160] = transmission?.factor ?? 0; f[161] = diffuseTransmission?.factor ?? 0; f[162] = volume?.thicknessFactor ?? 0; f[163] = dispersion?.dispersion ?? 0; f[164] = diffuseTransmission?.color[0] ?? 1; f[165] = diffuseTransmission?.color[1] ?? 1; f[166] = diffuseTransmission?.color[2] ?? 1; f[167] = Number.isFinite(volume?.attenuationDistance ?? Infinity) ? volume?.attenuationDistance ?? 0 : 0; f[168] = volume?.attenuationColor[0] ?? 1; f[169] = volume?.attenuationColor[1] ?? 1; f[170] = volume?.attenuationColor[2] ?? 1; f[171] = 0; packTextureTransform(f, 172, transmission?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 180, volume?.thicknessTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 188, diffuseTransmission?.textureTransform ?? DEFAULT_TEXTURE_TRANSFORM); packTextureTransform(f, 196, diffuseTransmission?.colorTextureTransform ?? DEFAULT_TEXTURE_TRANSFORM); return f; } createBindGroupLayout(device) { const plan = this.getLayoutPlan(); const maxTextures = device.limits?.maxSampledTexturesPerShaderStage ?? WEBGPU_BASELINE_MAX_SAMPLED_TEXTURES_PER_SHADER_STAGE; const maxSamplers = device.limits?.maxSamplersPerShaderStage ?? WEBGPU_BASELINE_MAX_SAMPLERS_PER_SHADER_STAGE; const materialIdentity = this.label ? ` '${this.label}'` : ""; if (plan.sampledTextureCount > maxTextures || plan.samplerCount > maxSamplers) throw new Error(`StandardMaterial${materialIdentity}: required ${plan.sampledTextureCount} sampled textures (limit: ${maxTextures}) and ${plan.samplerCount} samplers (limit: ${maxSamplers}) for features [${plan.bindings.map((b) => b.slot).join(", ")}], which exceeds device limits.`); let deviceLayouts = standardMaterialBindGroupLayouts.get(device); if (!deviceLayouts) { deviceLayouts = /* @__PURE__ */ new Map(); standardMaterialBindGroupLayouts.set(device, deviceLayouts); } const cached = deviceLayouts.get(plan.featureKey); if (cached) return cached; const entries = [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]; for (const b of plan.bindings) { entries.push({ binding: b.samplerBinding, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }); entries.push({ binding: b.textureBinding, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } }); } const layout = device.createBindGroupLayout({ label: `StandardMaterial ${plan.featureKey}`, entries }); deviceLayouts.set(plan.featureKey, layout); return layout; } usesTransmissionLayout() { return this.getLayoutPlan().usesTransmission; } getShaderCode(opts = {}) { return getSpecializedStandardShader(this.getLayoutPlan(), opts); } }; var DataMaterial = class _DataMaterial extends Material { _CPUData = null; _keepCPUData = false; _dataDirty = false; _ownsDataBuffer = false; dataBuffer = null; _elementCount = 0; _scaleTransform; _opacity = 1; _shading = 0; _colormap = "viridis"; _scaleRevision = 0; _visualChangeListeners = /* @__PURE__ */ new Set(); static _cachedBindGroupLayout = null; static _cachedLayoutDevice = null; constructor(desc) { assert(!!desc && !!desc.scaleTransform, "DataMaterial: scaleTransform is required."); super({ ...desc, blendMode: desc.blendMode ?? ((desc.opacity ?? 1) < 1 ? "transparent" /* Transparent */ : "opaque" /* Opaque */) }); this._scaleTransform = normalizeScaleTransform(desc.scaleTransform); if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; if (desc.opacity !== void 0) this._opacity = desc.opacity; if (desc.shading !== void 0) this._shading = desc.shading; if (desc.colormap !== void 0) this._colormap = desc.colormap; if (desc.data) this.setData(desc.data, { keepCPUData: this._keepCPUData }); if (desc.dataBuffer !== void 0 && desc.dataBuffer !== null) { this.setDataBuffer(resolveGPUBuffer(desc.dataBuffer)); } } get scaleTransform() { return cloneScaleTransform(this._scaleTransform); } setScaleTransform(transform) { this._scaleTransform = normalizeScaleTransform(transform); this._elementCount = this.recomputeElementCount(); this._dirty = true; this.emitVisualChange("scale"); } get opacity() { return this._opacity; } set opacity(v) { if (v === this._opacity) return; this._opacity = v; this._dirty = true; } get shading() { return this._shading; } set shading(v) { if (v === this._shading) return; this._shading = v; this._dirty = true; } get colormap() { return this._colormap; } set colormap(v) { this._colormap = v; this.bindGroupKey = null; this.emitVisualChange("colormap"); } onVisualChange(listener) { this._visualChangeListeners.add(listener); return () => { this._visualChangeListeners.delete(listener); }; } getColormapKey() { const c = this._colormap; return c instanceof Colormap ? `cm:${c.id}` : `cm:${c}`; } getColormapForBinding() { const c = this._colormap; if (c instanceof Colormap) return c; return Colormap.builtin(c); } computeElementCountFromFloatLength(floatLength) { const stride = Math.max(1, Math.floor(this._scaleTransform.stride)); const offset = Math.max(0, Math.floor(this._scaleTransform.offset)); if (floatLength <= offset) return 0; return Math.max(0, Math.floor((floatLength - offset) / stride)); } recomputeElementCount() { if (this._CPUData) return this.computeElementCountFromFloatLength(this._CPUData.length); if (this.dataBuffer) return this.computeElementCountFromFloatLength(Math.floor(this.dataBuffer.size / 4)); return 0; } setData(data, opts = {}) { assert(data.length > 0, "DataMaterial: data must be non-empty."); this._CPUData = data; this._dataDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this._elementCount = this.computeElementCountFromFloatLength(data.length); this._scaleRevision++; this._dirty = true; this.bindGroupKey = null; } setDataBuffer(buffer) { this._CPUData = null; this.dataBuffer = buffer; this._ownsDataBuffer = false; this._dataDirty = false; this._elementCount = this.computeElementCountFromFloatLength(Math.floor(buffer.size / 4)); this._scaleRevision++; this._dirty = true; this.bindGroupKey = null; } dropCPUData() { this._CPUData = null; } getScaleSourceDescriptor(revision = this._scaleRevision) { if (!this.dataBuffer || this._elementCount <= 0) return null; return { buffer: this.dataBuffer, count: this._elementCount, componentCount: this._scaleTransform.componentCount, componentIndex: this._scaleTransform.componentIndex, valueMode: this._scaleTransform.valueMode, stride: this._scaleTransform.stride, offset: this._scaleTransform.offset, revision }; } upload(device, queue) { this.assertAlive("upload"); if (!this._dataDirty) return; if (this.dataBuffer && !this._CPUData) { this._dataDirty = false; return; } const data = this._CPUData; if (!data) { this._dataDirty = false; return; } const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; if (!this.dataBuffer || !this._ownsDataBuffer) { this.dataBuffer = createBuffer(device, data, usage); this._ownsDataBuffer = true; } else { try { queue.writeBuffer(this.dataBuffer, 0, data.buffer, data.byteOffset, data.byteLength); } catch { this.dataBuffer.destroy(); this.dataBuffer = createBuffer(device, data, usage); } } this._elementCount = this.computeElementCountFromFloatLength(data.length); if (!this._keepCPUData) this._CPUData = null; this._dataDirty = false; this.bindGroupKey = null; } getUniformBufferSize() { return (SCALE_UNIFORM_FLOAT_COUNT + 4) * 4; } getUniformData() { const f = this.getUniformDataCache(SCALE_UNIFORM_FLOAT_COUNT + 4); f.fill(0); packScaleTransform(this._scaleTransform, f, 0); f[SCALE_UNIFORM_FLOAT_COUNT + 0] = clamp01(this._opacity); f[SCALE_UNIFORM_FLOAT_COUNT + 1] = clamp01(this._shading); f[SCALE_UNIFORM_FLOAT_COUNT + 2] = 0; f[SCALE_UNIFORM_FLOAT_COUNT + 3] = 0; return f; } createBindGroupLayout(device) { if (_DataMaterial._cachedBindGroupLayout && _DataMaterial._cachedLayoutDevice === device) return _DataMaterial._cachedBindGroupLayout; const layout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } } ] }); _DataMaterial._cachedBindGroupLayout = layout; _DataMaterial._cachedLayoutDevice = device; return layout; } getShaderCode(_opts = {}) { return data_default; } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) { try { listener(kind); } catch { } } } disposeResources() { super.disposeResources(); if (this._ownsDataBuffer) this.dataBuffer?.destroy(); this.dataBuffer = null; this._CPUData = null; this._dataDirty = false; this._elementCount = 0; this._visualChangeListeners.clear(); } }; var snapshotCustomMaterialResource = (resource) => { normalizeBindingResource(resource); if (isGPUBuffer(resource) || !("buffer" in resource)) return resource; if ("device" in resource && "queue" in resource) return resource; const binding = resource; return { buffer: binding.buffer, offset: binding.offset, size: binding.size }; }; var CustomMaterial = class extends Material { _vertexShader; _fragmentShader; _bindGroupLayout; _resources = /* @__PURE__ */ new Map(); _cachedBindGroupLayout = null; _cachedLayoutDevice = null; constructor(descriptor) { super(descriptor); this._vertexShader = descriptor.vertexShader ?? this.defaultVertexShader(); this._fragmentShader = descriptor.fragmentShader; this._bindGroupLayout = normalizeBindGroupLayout(descriptor.bindGroupLayout ?? { entries: [] }, "CustomMaterial bind group layout"); const resources = descriptor.resources ?? {}; validateResourcesForLayout(this._bindGroupLayout, resources, "CustomMaterial"); if (Array.isArray(resources)) for (const entry of resources) this._resources.set(entry.binding, snapshotCustomMaterialResource(entry.resource)); else for (const key of Object.keys(resources)) this._resources.set(Number(key), snapshotCustomMaterialResource(resources[Number(key)])); } getUniformBufferSize() { return 0; } getUniformData() { this.assertAlive("build uniform data"); return new Float32Array(0); } getResource(binding) { this.assertAlive("get a resource"); const resource = this._resources.get(binding); return resource ? snapshotCustomMaterialResource(resource) : void 0; } setResource(binding, resource) { this.assertAlive("set a resource"); if (!this._bindGroupLayout.entries.some((entry) => entry.binding === binding)) throw new Error(`CustomMaterial: binding ${binding} is not declared by the immutable bind group layout`); this._resources.set(binding, snapshotCustomMaterialResource(resource)); this.bindGroup = null; this.bindGroupKey = null; } getBindGroupEntries() { this.assertAlive("build bind group entries"); return this._bindGroupLayout.entries.map((entry) => { const resource = this._resources.get(entry.binding); if (!resource) throw new Error(`CustomMaterial: missing resource for binding ${entry.binding}`); return { binding: entry.binding, resource: normalizeBindingResource(resource) }; }); } createBindGroupLayout(device) { if (this._cachedBindGroupLayout && this._cachedLayoutDevice === device) return this._cachedBindGroupLayout; const layout = device.createBindGroupLayout({ label: this._bindGroupLayout.label, entries: this._bindGroupLayout.entries }); this._cachedBindGroupLayout = layout; this._cachedLayoutDevice = device; return layout; } defaultVertexShader() { return custom_default_vertex_default; } getShaderCode(opts = {}) { return this._vertexShader + "\n" + this._fragmentShader; } disposeResources() { super.disposeResources(); this._resources.clear(); this._cachedBindGroupLayout = null; this._cachedLayoutDevice = null; } }; // typescript/world/pointcloud.ts var UNIFORM_FLOAT_COUNT = 4 + SCALE_UNIFORM_FLOAT_COUNT + 4 + 8 * 4; var UNIFORM_BYTE_SIZE = UNIFORM_FLOAT_COUNT * 4; var POINT_RECORD_FLOATS = 4; var POINT_RECORD_BYTES = POINT_RECORD_FLOATS * 4; var normalizePointCloudScaleTransform = (transform) => normalizeScaleTransform({ componentCount: 4, componentIndex: 3, stride: 4, ...transform }); var colorModeId = (mode) => mode === "rgba" ? 0 : 1; var pointCloudRevisionScratch = new ArrayBuffer(4); var pointCloudRevisionF32 = new Float32Array(pointCloudRevisionScratch); var pointCloudRevisionU32 = new Uint32Array(pointCloudRevisionScratch); var mixPointCloudRevision = (hash, value) => Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; var mixPointCloudRevisionF32 = (hash, value) => { pointCloudRevisionF32[0] = Number.isFinite(value) ? value : 0; return mixPointCloudRevision(hash, pointCloudRevisionU32[0] >>> 0); }; var resolveWasmDataPointCount = (source, explicitPointCount) => resolveWasmRecordCount(source, explicitPointCount, POINT_RECORD_FLOATS, "PointCloud: wasmData", "PointCloud: pointCount", "pointCount"); var validateWasmDataRange = (source, pointCount) => validateWasmRecordRange(source, pointCount, POINT_RECORD_FLOATS, "PointCloud: wasmData", "pointCount"); var validateWasmColorsRange = (source, pointCount) => validateWasmRecordRange(source, pointCount, POINT_RECORD_FLOATS, "PointCloud: wasmColors", "pointCount"); var PointCloud = class { transform = new Transform(); name = null; visible = true; boundsMin = [0, 0, 0]; boundsMax = [0, 0, 0]; boundsCenter = [0, 0, 0]; boundsRadius = 0; blendMode = "additive" /* Additive */; depthWrite = false; depthTest = true; _basePointSize = 2; _minPointSize = 1; _maxPointSize = 16; _sizeAttenuation = 1; _opacity = 1; _colorMode = "scalar"; _colormap = "viridis"; _colormapStops = [[0.267, 487e-5, 0.32942, 1], [0.99325, 0.90616, 0.14394, 1]]; _softness = 0.15; _scaleTransform; _CPUData = null; _colorsCPU = null; _wasmDataSource = null; _wasmColorsSource = null; _keepCPUData = false; _ndShape = null; _boundsSource = "none"; _scaleRevision = 0; _visualChangeListeners = /* @__PURE__ */ new Set(); pointsBuffer = null; colorsBuffer = null; uniformBuffer = null; bindGroup = null; bindGroupKey = null; _pointCount = 0; _uniformDirty = true; _pointsDirty = true; _colorsDirty = true; _pointsOwned = false; _colorsOwned = false; _ownExternalBuffers = false; _colorsExternal = false; _wasmDataDirty = false; _wasmColorsDirty = false; _pointsWasmManaged = false; _colorsWasmManaged = false; _wasmPointCapacity = 0; _wasmColorCapacity = 0; _wasmPointCapacityHint = 0; _wasmColorCapacityHint = 0; constructor(desc) { assert(!!desc && !!desc.scaleTransform, "PointCloud: scaleTransform is required."); this._scaleTransform = normalizePointCloudScaleTransform(desc.scaleTransform); if (desc.name !== void 0) this.name = desc.name; if (desc.visible !== void 0) this.visible = !!desc.visible; if (desc.blendMode !== void 0) this.blendMode = desc.blendMode; if (desc.depthWrite !== void 0) this.depthWrite = !!desc.depthWrite; if (desc.depthTest !== void 0) this.depthTest = !!desc.depthTest; if (desc.basePointSize !== void 0) this._basePointSize = desc.basePointSize; if (desc.minPointSize !== void 0) this._minPointSize = desc.minPointSize; if (desc.maxPointSize !== void 0) this._maxPointSize = desc.maxPointSize; if (desc.sizeAttenuation !== void 0) this._sizeAttenuation = desc.sizeAttenuation; if (desc.opacity !== void 0) this._opacity = desc.opacity; if (desc.colormap !== void 0) this._colormap = desc.colormap; if (desc.colormapStops !== void 0) this._colormapStops = normalizeColorStops(desc.colormapStops); if (desc.colorMode !== void 0) this._colorMode = desc.colorMode; else if (desc.colors || desc.colorsBuffer || desc.wasmColors) this._colorMode = "rgba"; if (desc.softness !== void 0) this._softness = desc.softness; if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; this._ownExternalBuffers = !!desc.ownBuffers; if (desc.ndShape !== void 0) this.ndShape = desc.ndShape; this.applyExplicitBounds(desc); const wasmCapacity = assertWasmCapacity(desc.wasmCapacity, "PointCloud: wasmCapacity"); if (desc.data) this.setData(desc.data, { keepCPUData: this._keepCPUData }); else if (desc.wasmData) this.setWasmData(desc.wasmData, { pointCount: desc.pointCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); else if (desc.pointsBuffer) { const buf = resolveGPUBuffer(desc.pointsBuffer); const count = desc.pointCount ?? 0; assert(count > 0, "PointCloud: pointCount is required when using pointsBuffer."); this.setPointsBuffer(buf, count, { ownBuffer: this._ownExternalBuffers }); } else if (desc.pointCount !== void 0) { this._pointCount = desc.pointCount; this._pointsDirty = false; } if (desc.colors) this.setColors(desc.colors, { keepCPUData: this._keepCPUData }); else if (desc.wasmColors) this.setWasmColors(desc.wasmColors, { pointCount: desc.pointCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); else if (desc.colorsBuffer) this.setColorsBuffer(resolveGPUBuffer(desc.colorsBuffer), { ownBuffer: this._ownExternalBuffers }); } applyExplicitBounds(desc) { if (desc.boundsMin && desc.boundsMax) { const bounds = boundsFromBox(desc.boundsMin, desc.boundsMax); this.setBounds(bounds, "explicit"); if (desc.boundsCenter) this.boundsCenter = [desc.boundsCenter[0], desc.boundsCenter[1], desc.boundsCenter[2]]; if (desc.boundsRadius !== void 0) this.boundsRadius = Math.max(0, desc.boundsRadius); return; } if (desc.boundsCenter || desc.boundsRadius !== void 0) { const center = desc.boundsCenter ?? [0, 0, 0]; const radius = desc.boundsRadius ?? 0; this.setBounds(boundsFromSphere(center, radius), "explicit"); } } setBounds(bounds, source) { this.boundsMin = [bounds.boxMin[0], bounds.boxMin[1], bounds.boxMin[2]]; this.boundsMax = [bounds.boxMax[0], bounds.boxMax[1], bounds.boxMax[2]]; this.boundsCenter = [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]]; this.boundsRadius = bounds.sphereRadius; this._boundsSource = source; } clearComputedBoundsIfNeeded() { if (this._boundsSource !== "computed") return; this._boundsSource = "none"; this.boundsMin = [0, 0, 0]; this.boundsMax = [0, 0, 0]; this.boundsCenter = [0, 0, 0]; this.boundsRadius = 0; } clearColorsIfCountMismatch() { if (!this._colorsCPU) return; if (this._colorsCPU.length / 4 === this._pointCount) return; this._colorsCPU = null; this._colorsDirty = false; this.bindGroupKey = null; } replacePointsBuffer(buffer, owned) { if (this.pointsBuffer && this.pointsBuffer !== buffer && this._pointsOwned) this.pointsBuffer.destroy(); this.pointsBuffer = buffer; this._pointsOwned = !!buffer && owned; } replaceColorsBuffer(buffer, owned) { if (this.colorsBuffer && this.colorsBuffer !== buffer && this._colorsOwned) this.colorsBuffer.destroy(); this.colorsBuffer = buffer; this._colorsOwned = !!buffer && owned; } clearWasmDataState(destroyManagedBuffer) { this._wasmDataSource = null; this._wasmDataDirty = false; this._wasmPointCapacityHint = 0; if (destroyManagedBuffer && this._pointsWasmManaged) { this.replacePointsBuffer(null, false); this.bindGroupKey = null; } this._pointsWasmManaged = false; this._wasmPointCapacity = 0; } clearWasmColorsState(destroyManagedBuffer) { this._wasmColorsSource = null; this._wasmColorsDirty = false; this._wasmColorCapacityHint = 0; if (destroyManagedBuffer && this._colorsWasmManaged) { this.replaceColorsBuffer(null, false); this.bindGroupKey = null; } this._colorsWasmManaged = false; this._wasmColorCapacity = 0; } setPointCountFromWasm(pointCount, bumpScaleRevision) { const count = assertWasmRecordCount(pointCount, "PointCloud: pointCount"); const changed = count !== this._pointCount; this._pointCount = count; if (changed) { this.clearColorsIfCountMismatch(); if (this._wasmColorsSource) { this._wasmColorsDirty = true; this._colorsDirty = true; } } if (bumpScaleRevision) this._scaleRevision++; } copyWasmActiveRange(source, pointCount) { const view = source.array(); return new Float32Array(view.subarray(0, pointCount * POINT_RECORD_FLOATS)); } computeBoundsFromPackedData(data, pointCount) { if (pointCount <= 0) return; let minX = data[0], minY = data[1], minZ = data[2]; let maxX = data[0], maxY = data[1], maxZ = data[2]; for (let i = 1; i < pointCount; i++) { const base = i * POINT_RECORD_FLOATS; const x = data[base + 0], y = data[base + 1], z = data[base + 2]; if (x < minX) minX = x; if (y < minY) minY = y; if (z < minZ) minZ = z; if (x > maxX) maxX = x; if (y > maxY) maxY = y; if (z > maxZ) maxZ = z; } const cx = 0.5 * (minX + maxX); const cy = 0.5 * (minY + maxY); const cz = 0.5 * (minZ + maxZ); let maxR2 = 0; for (let i = 0; i < pointCount; i++) { const base = i * POINT_RECORD_FLOATS; const dx = data[base + 0] - cx, dy = data[base + 1] - cy, dz = data[base + 2] - cz; const r2 = dx * dx + dy * dy + dz * dz; if (r2 > maxR2) maxR2 = r2; } this.setBounds(boundsFromBoxAndSphere([minX, minY, minZ], [maxX, maxY, maxZ], [cx, cy, cz], Math.sqrt(maxR2)), "computed"); } ensureWasmPointBuffer(device, pointCount) { const required = Math.max(pointCount, this._wasmPointCapacityHint); if (required <= 0) return; if (this.pointsBuffer && this._pointsWasmManaged && this._wasmPointCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmPointCapacity); const buffer = device.createBuffer({ label: "PointCloud.wasmData", size: capacity * POINT_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); this.replacePointsBuffer(buffer, true); this._pointsWasmManaged = true; this._wasmPointCapacity = capacity; this.bindGroupKey = null; } ensureWasmColorsBuffer(device, pointCount) { const required = Math.max(pointCount, this._wasmColorCapacityHint); if (required <= 0) return; if (this.colorsBuffer && this._colorsWasmManaged && this._wasmColorCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmColorCapacity); const buffer = device.createBuffer({ label: "PointCloud.wasmColors", size: capacity * POINT_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); this.replaceColorsBuffer(buffer, true); this._colorsWasmManaged = true; this._wasmColorCapacity = capacity; this.bindGroupKey = null; } get pointCount() { return this._pointCount; } get occluderRevision() { let hash = 2166136261 >>> 0; hash = mixPointCloudRevision(hash, this._pointCount >>> 0); hash = mixPointCloudRevision(hash, this._scaleRevision >>> 0); hash = mixPointCloudRevision(hash, this.blendMode === "opaque" /* Opaque */ ? 1 : this.blendMode === "transparent" /* Transparent */ ? 2 : 3); hash = mixPointCloudRevision(hash, this.depthWrite ? 1 : 0); hash = mixPointCloudRevision(hash, this.depthTest ? 1 : 0); hash = mixPointCloudRevision(hash, this._pointsDirty ? 1 : 0); hash = mixPointCloudRevision(hash, this._colorsDirty ? 1 : 0); hash = mixPointCloudRevision(hash, this.pointsBuffer ? 1 : 0); hash = mixPointCloudRevision(hash, this.colorsBuffer ? 1 : 0); hash = mixPointCloudRevision(hash, colorModeId(this._colorMode) >>> 0); hash = mixPointCloudRevisionF32(hash, this._basePointSize); hash = mixPointCloudRevisionF32(hash, this._minPointSize); hash = mixPointCloudRevisionF32(hash, this._maxPointSize); hash = mixPointCloudRevisionF32(hash, this._sizeAttenuation); return hash >>> 0; } get ndShape() { return this._ndShape ? this._ndShape.slice() : null; } set ndShape(shape) { this._ndShape = normalizePositiveIntShape(shape, "PointCloud: ndShape"); } get scaleTransform() { return cloneScaleTransform(this._scaleTransform); } setScaleTransform(transform) { this._scaleTransform = normalizePointCloudScaleTransform(transform); this._uniformDirty = true; this.emitVisualChange("scale"); } applyScaleStats(stats) { const next = cloneScaleTransform(this._scaleTransform); if (Number.isFinite(stats.min)) next.domainMin = stats.min; if (Number.isFinite(stats.max)) next.domainMax = stats.max; if (stats.percentileMin !== null && stats.percentileMax !== null) { next.clampMin = stats.percentileMin; next.clampMax = stats.percentileMax; } this._scaleTransform = normalizePointCloudScaleTransform(next); this._uniformDirty = true; this.emitVisualChange("scale"); } onVisualChange(listener) { this._visualChangeListeners.add(listener); return () => this._visualChangeListeners.delete(listener); } getScaleSourceDescriptor(revision = this._scaleRevision) { if (!this.pointsBuffer || this._pointCount <= 0) return null; return { buffer: this.pointsBuffer, count: this._pointCount, componentCount: this._scaleTransform.componentCount, componentIndex: this._scaleTransform.componentIndex, valueMode: this._scaleTransform.valueMode, stride: this._scaleTransform.stride, offset: this._scaleTransform.offset, revision }; } get basePointSize() { return this._basePointSize; } set basePointSize(v) { if (v === this._basePointSize) return; this._basePointSize = v; this._uniformDirty = true; } get minPointSize() { return this._minPointSize; } set minPointSize(v) { if (v === this._minPointSize) return; this._minPointSize = v; this._uniformDirty = true; } get maxPointSize() { return this._maxPointSize; } set maxPointSize(v) { if (v === this._maxPointSize) return; this._maxPointSize = v; this._uniformDirty = true; } get sizeAttenuation() { return this._sizeAttenuation; } set sizeAttenuation(v) { if (v === this._sizeAttenuation) return; this._sizeAttenuation = v; this._uniformDirty = true; } get opacity() { return this._opacity; } set opacity(v) { if (v === this._opacity) return; this._opacity = v; this._uniformDirty = true; } get colorMode() { return this._colorMode; } set colorMode(v) { if (v === this._colorMode) return; this._colorMode = v; this._uniformDirty = true; this.emitVisualChange("visual"); } get colormap() { return this._colormap; } set colormap(v) { this._colormap = v; this._uniformDirty = true; this.bindGroupKey = null; this.emitVisualChange("colormap"); } get colormapStops() { return this._colormapStops; } set colormapStops(stops) { this._colormapStops = normalizeColorStops(stops); this._uniformDirty = true; this.emitVisualChange("colormap"); } getColormapKey() { const c = this._colormap; return c instanceof Colormap ? `cm:${c.id}` : `cm:${c}`; } getColormapForBinding() { const c = this._colormap; if (c instanceof Colormap) return c; if (c === "custom") return Colormap.builtin("grayscale"); return Colormap.builtin(c); } get softness() { return this._softness; } set softness(v) { if (v === this._softness) return; this._softness = v; this._uniformDirty = true; } setData(data, opts = {}) { assert(data.length % 4 === 0, "PointCloud: data length must be a multiple of 4 (x,y,z,scalar per point)."); this.clearWasmDataState(true); this._CPUData = data; this._pointCount = data.length / 4; this.clearColorsIfCountMismatch(); if (this.pointsBuffer && !this._pointsOwned) this.pointsBuffer = null; this._pointsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this._scaleRevision++; this.bindGroupKey = null; this.clearComputedBoundsIfNeeded(); } setPointsBuffer(buffer, pointCount, opts = {}) { assert(pointCount > 0, "PointCloud: pointCount must be > 0."); this.clearWasmDataState(true); this._CPUData = null; this._pointCount = pointCount; this.clearColorsIfCountMismatch(); this.replacePointsBuffer(buffer, !!opts.ownBuffer); this._pointsDirty = false; this._scaleRevision++; this.bindGroupKey = null; this.clearComputedBoundsIfNeeded(); } setColors(data, opts = {}) { assert(data.length % 4 === 0, "PointCloud: colors length must be a multiple of 4 (r,g,b,a per point)."); assert(data.length / 4 === this._pointCount, "PointCloud: colors length must equal pointCount*4."); this.clearWasmColorsState(true); this._colorsCPU = new Float32Array(data); if (this.colorsBuffer && !this._colorsOwned) this.colorsBuffer = null; this._colorsExternal = false; this._colorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } setColorsBuffer(buffer, opts = {}) { if (buffer) assert(this._pointCount > 0, "PointCloud: pointCount must be > 0 when using colorsBuffer."); this.clearWasmColorsState(true); this.replaceColorsBuffer(buffer, !!buffer && !!opts.ownBuffer); this._colorsCPU = null; this._colorsExternal = !!buffer; this._colorsDirty = false; this.bindGroupKey = null; } setWasmData(source, options = {}) { if (source === null) { this.clearWasmDataState(true); return; } const wasmSource = assertWasmF32View(source, "PointCloud: wasmData"); this._wasmPointCapacityHint = assertWasmCapacity(options.capacity, "PointCloud: wasmData capacity"); if (!this._pointsWasmManaged) { this.replacePointsBuffer(null, false); this._wasmPointCapacity = 0; this.bindGroupKey = null; } this._wasmDataSource = wasmSource; this._CPUData = null; this.refreshWasmData(options); } setWasmColors(source, options = {}) { if (source === null) { this.clearWasmColorsState(true); return; } const wasmSource = assertWasmF32View(source, "PointCloud: wasmColors"); this._wasmColorCapacityHint = assertWasmCapacity(options.capacity, "PointCloud: wasmColors capacity"); if (!this._colorsWasmManaged) { this.replaceColorsBuffer(null, false); this._wasmColorCapacity = 0; this.bindGroupKey = null; } this._wasmColorsSource = wasmSource; this._colorsCPU = null; this._colorsExternal = false; this.refreshWasmColors(options); } refreshWasmData(options = {}) { const source = this._wasmDataSource; if (!source) return; source.refresh(); assertWasmF32View(source, "PointCloud: wasmData"); const count = resolveWasmDataPointCount(source, options.pointCount); this.setPointCountFromWasm(count, true); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._CPUData = this.copyWasmActiveRange(source, count); else this._CPUData = null; if (options.recomputeBounds && this._boundsSource !== "explicit") this.computeBoundsFromPackedData(source.array(), count); else this.clearComputedBoundsIfNeeded(); this._wasmDataDirty = true; this._pointsDirty = true; } refreshWasmColors(options = {}) { const source = this._wasmColorsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "PointCloud: wasmColors"); let count = this._pointCount; if (options.pointCount !== void 0) { const nextCount = assertWasmRecordCount(options.pointCount, "PointCloud: pointCount"); assert(!this._wasmDataSource || nextCount === this._pointCount, "PointCloud: refreshWasmColors pointCount must match the current pointCount when wasmData is active; call refreshWasmData() or refreshFromWasm() to update point count."); if (nextCount !== this._pointCount) { this._pointCount = nextCount; this.clearColorsIfCountMismatch(); this._scaleRevision++; } count = nextCount; } else assert(count > 0 || source.length === 0, "PointCloud: pointCount is required when using wasmColors without wasmData."); validateWasmColorsRange(source, count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._colorsCPU = this.copyWasmActiveRange(source, count); else this._colorsCPU = null; this._colorsExternal = false; this._wasmColorsDirty = true; this._colorsDirty = true; } refreshFromWasm(options = {}) { if (this._wasmDataSource) this.refreshWasmData(options); if (this._wasmColorsSource) this.refreshWasmColors(options); } clearWasmSources() { this.clearWasmDataState(true); this.clearWasmColorsState(true); } dropCPUData() { this._CPUData = null; this._colorsCPU = null; } getPointRecord(index) { const data = this._CPUData; if (!data) return null; if (!Number.isInteger(index) || index < 0 || index >= this._pointCount) return null; const o = index * 4; const color = this._colorsCPU ? [this._colorsCPU[o + 0], this._colorsCPU[o + 1], this._colorsCPU[o + 2], this._colorsCPU[o + 3]] : null; return { position: [data[o + 0], data[o + 1], data[o + 2]], scalar: data[o + 3], color, packed: [data[o + 0], data[o + 1], data[o + 2], data[o + 3]] }; } mapLinearIndexToNd(index) { return linearIndexToNdIndex(this._ndShape, index); } computeBoundsFromCPUData() { const data = this._CPUData; if (!data || data.length < 4) return; const pointCount = this._pointCount; if (pointCount <= 0) return; this.computeBoundsFromPackedData(data, pointCount); } getLocalBounds() { if (this._boundsSource === "none" && this._CPUData) this.computeBoundsFromCPUData(); if (this._boundsSource === "none") return emptyBounds(this._pointCount > 0); return boundsFromBoxAndSphere(this.boundsMin, this.boundsMax, this.boundsCenter, this.boundsRadius); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } uploadWasmData(device, queue) { const source = this._wasmDataSource; if (!source || !this._wasmDataDirty) return; source.refresh(); assertWasmF32View(source, "PointCloud: wasmData"); const count = this._pointCount; validateWasmDataRange(source, count); if (count <= 0) { this._wasmDataDirty = false; this._pointsDirty = false; return; } const data = source.array(); const byteLength = count * POINT_RECORD_BYTES; this.ensureWasmPointBuffer(device, count); const write = () => { assert(!!this.pointsBuffer, "PointCloud: wasmData upload requires a pointsBuffer."); queue.writeBuffer(this.pointsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replacePointsBuffer(null, false); this._pointsWasmManaged = false; this._wasmPointCapacity = 0; this.ensureWasmPointBuffer(device, count); write(); } if (this._keepCPUData) this._CPUData = new Float32Array(data.subarray(0, count * POINT_RECORD_FLOATS)); else this._CPUData = null; this._wasmDataDirty = false; this._pointsDirty = false; } uploadWasmColors(device, queue) { const source = this._wasmColorsSource; if (!source || !this._wasmColorsDirty) return; source.refresh(); assertWasmF32View(source, "PointCloud: wasmColors"); const count = this._pointCount; validateWasmColorsRange(source, count); if (count <= 0) { this._wasmColorsDirty = false; this._colorsDirty = false; return; } const colors = source.array(); const byteLength = count * POINT_RECORD_BYTES; this.ensureWasmColorsBuffer(device, count); const write = () => { assert(!!this.colorsBuffer, "PointCloud: wasmColors upload requires a colorsBuffer."); queue.writeBuffer(this.colorsBuffer, 0, colors.buffer, colors.byteOffset, byteLength); }; try { write(); } catch { this.replaceColorsBuffer(null, false); this._colorsWasmManaged = false; this._wasmColorCapacity = 0; this.ensureWasmColorsBuffer(device, count); write(); } if (this._keepCPUData) this._colorsCPU = new Float32Array(colors.subarray(0, count * POINT_RECORD_FLOATS)); else this._colorsCPU = null; this._colorsExternal = false; this._wasmColorsDirty = false; this._colorsDirty = false; } upload(device, queue) { if (this._wasmDataSource && this._wasmDataDirty) this.uploadWasmData(device, queue); else if (this._pointsDirty) { if (this.pointsBuffer && !this._CPUData) this._pointsDirty = false; else { const data = this._CPUData; if (!data) this._pointsDirty = false; else { const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; if (!this.pointsBuffer || !this._pointsOwned) this.replacePointsBuffer(createBuffer(device, data, usage), true); else try { queue.writeBuffer(this.pointsBuffer, 0, data.buffer, data.byteOffset, data.byteLength); } catch { this.replacePointsBuffer(createBuffer(device, data, usage), true); } this._pointsWasmManaged = false; this._wasmPointCapacity = 0; if (!this._keepCPUData) this._CPUData = null; this._pointsDirty = false; this.bindGroupKey = null; } } } if (this._wasmColorsSource && this._wasmColorsDirty) this.uploadWasmColors(device, queue); else if (!this._colorsExternal && this._colorsDirty) { const colors = this._colorsCPU; if (!colors) { this._colorsDirty = false; return; } const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; if (!this.colorsBuffer || !this._colorsOwned) this.replaceColorsBuffer(createBuffer(device, colors, usage), true); else try { queue.writeBuffer(this.colorsBuffer, 0, colors.buffer, colors.byteOffset, colors.byteLength); } catch { this.replaceColorsBuffer(createBuffer(device, colors, usage), true); } this._colorsWasmManaged = false; this._wasmColorCapacity = 0; if (!this._keepCPUData) this._colorsCPU = null; this._colorsDirty = false; this.bindGroupKey = null; } } getUniformBufferSize() { return UNIFORM_BYTE_SIZE; } getUniformData() { const out = new Float32Array(UNIFORM_FLOAT_COUNT); const base = Math.max(0, this._basePointSize); const minSize = Math.max(0, this._minPointSize); const maxSize = Math.max(minSize, this._maxPointSize); const atten = Math.max(0, this._sizeAttenuation); out[0] = base; out[1] = minSize; out[2] = maxSize; out[3] = atten; packScaleTransform(this._scaleTransform, out, 4); out[24] = clamp01(this._opacity); out[25] = clamp01(this._softness); out[26] = typeof this._colormap === "string" && this._colormap === "custom" ? Math.min(8, Math.max(2, this._colormapStops.length)) : 0; out[27] = colorModeId(this._colorMode); const stops = this._colormapStops; const nStops = Math.min(8, Math.max(2, stops.length)); for (let i = 0; i < 8; i++) { const src = stops[Math.min(i, nStops - 1)]; const o = 28 + i * 4; out[o + 0] = src[0]; out[o + 1] = src[1]; out[o + 2] = src[2]; out[o + 3] = src[3]; } return out; } get dirtyUniforms() { return this._uniformDirty; } markUniformsClean() { this._uniformDirty = false; } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) try { listener(kind); } catch { } } destroyOwnedBuffer(buffer, owned) { if (!buffer || !owned) return; buffer.destroy(); } destroy() { this.destroyOwnedBuffer(this.pointsBuffer, this._pointsOwned); this.destroyOwnedBuffer(this.colorsBuffer, this._colorsOwned); this.uniformBuffer?.destroy(); this.pointsBuffer = null; this.colorsBuffer = null; this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this._CPUData = null; this._colorsCPU = null; this._wasmDataSource = null; this._wasmColorsSource = null; this._ndShape = null; this._pointCount = 0; this._pointsOwned = false; this._colorsOwned = false; this._ownExternalBuffers = false; this._colorsExternal = false; this._wasmDataDirty = false; this._wasmColorsDirty = false; this._pointsWasmManaged = false; this._colorsWasmManaged = false; this._wasmPointCapacity = 0; this._wasmColorCapacity = 0; this._wasmPointCapacityHint = 0; this._wasmColorCapacityHint = 0; this._visualChangeListeners.clear(); this.transform.dispose(); } }; // typescript/world/glyphfield.ts var UNIFORM_FLOAT_COUNT2 = 5 * 4 + 4 + 4 + 8 * 4; var UNIFORM_BYTE_SIZE2 = UNIFORM_FLOAT_COUNT2 * 4; var GLYPH_RECORD_FLOATS = 4; var GLYPH_RECORD_BYTES = GLYPH_RECORD_FLOATS * 4; var resolveGlyphWasmRecordCount = (source, explicitInstanceCount, field) => resolveWasmRecordCount(source, explicitInstanceCount, GLYPH_RECORD_FLOATS, `GlyphField: ${field}`, "GlyphField: instanceCount", "instanceCount"); var validateGlyphWasmRecordRange = (source, instanceCount, field) => validateWasmRecordRange(source, instanceCount, GLYPH_RECORD_FLOATS, `GlyphField: ${field}`, "instanceCount"); var rotateGlyphOffset = (x, y, z, qx, qy, qz, qw) => { const tx = 2 * (qy * z - qz * y), ty = 2 * (qz * x - qx * z), 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)]; }; var normalizeGlyphScaleTransform = (transform) => normalizeScaleTransform({ componentCount: 4, componentIndex: 0, stride: 4, ...transform }); var colorModeId2 = (mode) => { switch (mode) { case "rgba": return 0; case "scalar": return 1; case "solid": return 2; } }; var glyphRevisionScratch = new ArrayBuffer(4); var glyphRevisionF32 = new Float32Array(glyphRevisionScratch); var glyphRevisionU32 = new Uint32Array(glyphRevisionScratch); var mixGlyphRevision = (hash, value) => Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; var mixGlyphRevisionF32 = (hash, value) => { glyphRevisionF32[0] = Number.isFinite(value) ? value : 0; return mixGlyphRevision(hash, glyphRevisionU32[0] >>> 0); }; var createUvEllipsoidGeometry = (latSegments = 8, lonSegments = 12) => { const lat = Math.max(3, latSegments | 0); const lon = Math.max(3, lonSegments | 0); const positions = []; const normals = []; const uvs = []; const indices = []; for (let y = 0; y <= lat; y++) { const v = y / lat; const theta = v * Math.PI; const sinT = Math.sin(theta); const cosT = Math.cos(theta); for (let x = 0; x <= lon; x++) { const u = x / lon; const phi = u * Math.PI * 2; const sinP = Math.sin(phi); const cosP = Math.cos(phi); const nx = cosP * sinT; const ny = cosT; const nz = sinP * sinT; positions.push(nx, ny, nz); normals.push(nx, ny, nz); uvs.push(u, 1 - v); } } const stride = lon + 1; for (let y = 0; y < lat; y++) { for (let x = 0; x < lon; x++) { const i0 = y * stride + x; const i1 = i0 + 1; const i2 = i0 + stride; const i3 = i2 + 1; indices.push(i0, i1, i2); indices.push(i1, i3, i2); } } return new Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); }; var createArrowGeometry = (radialSegments = 12) => { const seg = Math.max(3, radialSegments | 0); const positions = []; const normals = []; const uvs = []; const indices = []; const z0 = -0.5; const z1 = 0.15; const z2 = 0.2; const z3 = 0.5; const r0 = 0.05; const r1 = 0.1; const pushVertex = (px, py, pz, nx, ny, nz, u, v) => { const idx = positions.length / 3 | 0; positions.push(px, py, pz); normals.push(nx, ny, nz); uvs.push(u, v); return idx; }; const ring = (z, r, nz, forCap) => { const out = []; for (let i = 0; i <= seg; i++) { const t = i / seg; const a = t * Math.PI * 2; const c = Math.cos(a); const s = Math.sin(a); const px = c * r; const py = s * r; let nx = c; let ny = s; let nzz = nz; if (forCap) { nx = 0; ny = 0; nzz = nz; } out.push(pushVertex(px, py, z, nx, ny, nzz, t, z - z0)); } return out; }; const cylBottom = ring(z0, r0, 0, false); const cylTop = ring(z1, r0, 0, false); for (let i = 0; i < seg; i++) { const i0 = cylBottom[i]; const i1 = cylBottom[i + 1]; const i2 = cylTop[i]; const i3 = cylTop[i + 1]; indices.push(i0, i1, i2); indices.push(i1, i3, i2); } const fr0 = ring(z1, r0, 0, false); const fr1 = ring(z2, r1, 0, false); const slope = (r0 - r1) / (z2 - z1); for (let i = 0; i <= seg; i++) { const t = i / seg; const a = t * Math.PI * 2; const c = Math.cos(a); const s = Math.sin(a); const nx = c; const ny = s; const nz = slope; const inv = 1 / Math.hypot(nx, ny, nz); normals[fr0[i] * 3 + 0] = nx * inv; normals[fr0[i] * 3 + 1] = ny * inv; normals[fr0[i] * 3 + 2] = nz * inv; normals[fr1[i] * 3 + 0] = nx * inv; normals[fr1[i] * 3 + 1] = ny * inv; normals[fr1[i] * 3 + 2] = nz * inv; } for (let i = 0; i < seg; i++) { const i0 = fr0[i]; const i1 = fr0[i + 1]; const i2 = fr1[i]; const i3 = fr1[i + 1]; indices.push(i0, i1, i2); indices.push(i1, i3, i2); } const coneBase = ring(z2, r1, 0, false); const coneTipVerts = []; const coneH = z3 - z2; const coneNz = r1 / coneH; for (let i = 0; i <= seg; i++) { const t = i / seg; const a = t * Math.PI * 2; const c = Math.cos(a); const s = Math.sin(a); const nx = c; const ny = s; const nz = coneNz; const inv = 1 / Math.hypot(nx, ny, nz); coneTipVerts.push(pushVertex(0, 0, z3, nx * inv, ny * inv, nz * inv, t, z3 - z0)); } for (let i = 0; i <= seg; i++) { const t = i / seg; const a = t * Math.PI * 2; const c = Math.cos(a); const s = Math.sin(a); const nx = c; const ny = s; const nz = coneNz; const inv = 1 / Math.hypot(nx, ny, nz); normals[coneBase[i] * 3 + 0] = nx * inv; normals[coneBase[i] * 3 + 1] = ny * inv; normals[coneBase[i] * 3 + 2] = nz * inv; } for (let i = 0; i < seg; i++) { const b0 = coneBase[i]; const b1 = coneBase[i + 1]; const t0 = coneTipVerts[i]; indices.push(b0, b1, t0); } const capCenter = pushVertex(0, 0, z0, 0, 0, -1, 0.5, 0); const capRing = ring(z0, r0, -1, true); for (let i = 0; i < seg; i++) { const rA = capRing[i]; const rB = capRing[i + 1]; indices.push(capCenter, rB, rA); } return new Geometry({ positions: new Float32Array(positions), normals: new Float32Array(normals), uvs: new Float32Array(uvs), indices: new Uint32Array(indices) }); }; var cachedEllipsoid = null; var cachedArrow = null; var defaultGlyphGeometry = (shape) => { switch (shape) { case "ellipsoid": cachedEllipsoid ??= createUvEllipsoidGeometry(); return cachedEllipsoid; case "arrow": cachedArrow ??= createArrowGeometry(); return cachedArrow; default: cachedEllipsoid ??= createUvEllipsoidGeometry(); return cachedEllipsoid; } }; var GlyphField = class { transform = new Transform(); name = null; visible = true; boundsMin = [0, 0, 0]; boundsMax = [0, 0, 0]; blendMode = "opaque" /* Opaque */; cullMode = "back" /* Back */; depthWrite = true; depthTest = true; boundsCenter = [0, 0, 0]; boundsRadius = 0; shape = "ellipsoid"; geometry; positionsBuffer = null; rotationsBuffer = null; scalesBuffer = null; attributesBuffer = null; uniformBuffer = null; bindGroup = null; bindGroupKey = null; _instanceCount = 0; _positionsCPU = null; _rotationsCPU = null; _scalesCPU = null; _attributesCPU = null; _wasmPositionsSource = null; _wasmRotationsSource = null; _wasmScalesSource = null; _wasmAttributesSource = null; _positionsPtr = 0; _rotationsPtr = 0; _scalesPtr = 0; _attributesPtr = 0; _usingWasmPtrs = false; _usingExternalBuffers = false; _keepCPUData = false; _ndShape = null; _boundsSource = "none"; _dataDirty = true; _uniformDirty = true; _colorMode = "rgba"; _colormap = "viridis"; _colormapStops = [[0.267, 487e-5, 0.32942, 1], [0.99325, 0.90616, 0.14394, 1]]; _scaleTransform = normalizeGlyphScaleTransform({}); _scaleRevision = 0; _visualChangeListeners = /* @__PURE__ */ new Set(); _opacity = 1; _lit = false; _solidColor = [1, 1, 1, 1]; _positionsOwned = false; _rotationsOwned = false; _scalesOwned = false; _attributesOwned = false; _ownExternalBuffers = false; _wasmPositionsDirty = false; _wasmRotationsDirty = false; _wasmScalesDirty = false; _wasmAttributesDirty = false; _positionsWasmManaged = false; _rotationsWasmManaged = false; _scalesWasmManaged = false; _attributesWasmManaged = false; _wasmPositionsCapacity = 0; _wasmRotationsCapacity = 0; _wasmScalesCapacity = 0; _wasmAttributesCapacity = 0; _wasmPositionsCapacityHint = 0; _wasmRotationsCapacityHint = 0; _wasmScalesCapacityHint = 0; _wasmAttributesCapacityHint = 0; constructor(desc) { assert(!!desc && !!desc.scaleTransform, "GlyphField: scaleTransform is required."); this._scaleTransform = normalizeGlyphScaleTransform(desc.scaleTransform); if (desc.name !== void 0) this.name = desc.name; if (desc.visible !== void 0) this.visible = !!desc.visible; this.shape = desc.shape ?? "ellipsoid"; this.geometry = desc.geometry ?? defaultGlyphGeometry(this.shape); this.applyExplicitBounds(desc); if (desc.blendMode !== void 0) this.blendMode = desc.blendMode; if (desc.cullMode !== void 0) this.cullMode = desc.cullMode; if (desc.depthWrite !== void 0) this.depthWrite = !!desc.depthWrite; if (desc.depthTest !== void 0) this.depthTest = !!desc.depthTest; if (desc.colorMode !== void 0) this._colorMode = desc.colorMode; if (desc.colormap !== void 0) this._colormap = desc.colormap; if (desc.colormapStops !== void 0) this._colormapStops = normalizeColorStops(desc.colormapStops); if (desc.opacity !== void 0) this._opacity = desc.opacity; if (desc.lit !== void 0) this._lit = !!desc.lit; if (desc.solidColor !== void 0) this._solidColor = [desc.solidColor[0], desc.solidColor[1], desc.solidColor[2], desc.solidColor[3]]; if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; this._ownExternalBuffers = !!desc.ownBuffers; if (desc.ndShape !== void 0) this.ndShape = desc.ndShape; const positionsBuffer = desc.positionsBuffer ? resolveGPUBuffer(desc.positionsBuffer) : null; const rotationsBuffer = desc.rotationsBuffer ? resolveGPUBuffer(desc.rotationsBuffer) : null; const scalesBuffer = desc.scalesBuffer ? resolveGPUBuffer(desc.scalesBuffer) : null; const attributesBuffer = desc.attributesBuffer ? resolveGPUBuffer(desc.attributesBuffer) : null; const hasWasmSources = !!desc.wasmPositions || !!desc.wasmRotations || !!desc.wasmScales || !!desc.wasmAttributes; const wasmCapacity = assertWasmCapacity(desc.wasmCapacity, "GlyphField: wasmCapacity"); const hasCoreWasmSources = !!desc.wasmPositions || !!desc.wasmRotations || !!desc.wasmScales; if (hasWasmSources && hasCoreWasmSources) { this.initializeMixedDescriptorSources(desc, positionsBuffer, rotationsBuffer, scalesBuffer, attributesBuffer); this.setWasmInstances({ positions: desc.wasmPositions, rotations: desc.wasmRotations, scales: desc.wasmScales, attributes: desc.wasmAttributes }, { instanceCount: desc.instanceCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); } else if (positionsBuffer || rotationsBuffer || scalesBuffer || attributesBuffer) { assert(!!positionsBuffer && !!rotationsBuffer && !!scalesBuffer, "GlyphField: positionsBuffer, rotationsBuffer, and scalesBuffer are required when using external buffers."); const count = desc.instanceCount ?? 0; assert(count > 0, "GlyphField: instanceCount is required when using external buffers."); this.setBuffers(positionsBuffer, rotationsBuffer, scalesBuffer, attributesBuffer, count, { ownBuffers: this._ownExternalBuffers }); if (hasWasmSources) this.setWasmAttributes(desc.wasmAttributes ?? null, { instanceCount: desc.instanceCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); } else if (desc.positionsPtr || desc.rotationsPtr || desc.scalesPtr) { assert(!!desc.positionsPtr && !!desc.rotationsPtr && !!desc.scalesPtr, "GlyphField: positionsPtr, rotationsPtr, and scalesPtr are required when using wasm pointers."); const count = desc.instanceCount ?? 0; assert(count > 0, "GlyphField: instanceCount is required when using wasm pointers."); this.setWasmSoA(desc.positionsPtr, desc.rotationsPtr, desc.scalesPtr, desc.attributesPtr ?? 0, count); if (hasWasmSources) this.setWasmAttributes(desc.wasmAttributes ?? null, { instanceCount: desc.instanceCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); } else if (desc.positions || desc.rotations || desc.scales || desc.attributes) { this.setCPUData(desc.positions ?? null, desc.rotations ?? null, desc.scales ?? null, desc.attributes ?? null, { keepCPUData: this._keepCPUData, instanceCount: desc.instanceCount }); if (hasWasmSources) this.setWasmAttributes(desc.wasmAttributes ?? null, { instanceCount: desc.instanceCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); } else if (desc.instanceCount !== void 0) { this._instanceCount = desc.instanceCount | 0; this._dataDirty = false; if (hasWasmSources) this.setWasmAttributes(desc.wasmAttributes ?? null, { instanceCount: desc.instanceCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData }); } } applyExplicitBounds(desc) { if (desc.boundsMin && desc.boundsMax) { const bounds = boundsFromBox(desc.boundsMin, desc.boundsMax); this.setBounds(bounds, "explicit"); if (desc.boundsCenter) this.boundsCenter = [desc.boundsCenter[0], desc.boundsCenter[1], desc.boundsCenter[2]]; if (desc.boundsRadius !== void 0) this.boundsRadius = Math.max(0, desc.boundsRadius); return; } if (desc.boundsCenter || desc.boundsRadius !== void 0) { const center = desc.boundsCenter ?? [0, 0, 0]; const radius = desc.boundsRadius ?? 0; this.setBounds(boundsFromSphere(center, radius), "explicit"); } } setBounds(bounds, source) { this.boundsMin = [bounds.boxMin[0], bounds.boxMin[1], bounds.boxMin[2]]; this.boundsMax = [bounds.boxMax[0], bounds.boxMax[1], bounds.boxMax[2]]; this.boundsCenter = [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]]; this.boundsRadius = bounds.sphereRadius; this._boundsSource = source; } clearComputedBoundsIfNeeded() { if (this._boundsSource !== "computed") return; this._boundsSource = "none"; this.boundsMin = [0, 0, 0]; this.boundsMax = [0, 0, 0]; this.boundsCenter = [0, 0, 0]; this.boundsRadius = 0; } replacePositionsBuffer(buffer, owned) { if (this.positionsBuffer && this.positionsBuffer !== buffer && this._positionsOwned) this.positionsBuffer.destroy(); this.positionsBuffer = buffer; this._positionsOwned = !!buffer && owned; } replaceRotationsBuffer(buffer, owned) { if (this.rotationsBuffer && this.rotationsBuffer !== buffer && this._rotationsOwned) this.rotationsBuffer.destroy(); this.rotationsBuffer = buffer; this._rotationsOwned = !!buffer && owned; } replaceScalesBuffer(buffer, owned) { if (this.scalesBuffer && this.scalesBuffer !== buffer && this._scalesOwned) this.scalesBuffer.destroy(); this.scalesBuffer = buffer; this._scalesOwned = !!buffer && owned; } replaceAttributesBuffer(buffer, owned) { if (this.attributesBuffer && this.attributesBuffer !== buffer && this._attributesOwned) this.attributesBuffer.destroy(); this.attributesBuffer = buffer; this._attributesOwned = !!buffer && owned; } releaseInstanceBuffers() { this.replacePositionsBuffer(null, false); this.replaceRotationsBuffer(null, false); this.replaceScalesBuffer(null, false); this.replaceAttributesBuffer(null, false); } hasExternalWasmSources() { return !!(this._wasmPositionsSource || this._wasmRotationsSource || this._wasmScalesSource || this._wasmAttributesSource); } hasDirtyWasmSources() { return this._wasmPositionsDirty || this._wasmRotationsDirty || this._wasmScalesDirty || this._wasmAttributesDirty; } hasDirtyNonWasmSources() { return !!(!this._wasmPositionsSource && (this._positionsCPU || this._positionsPtr) || !this._wasmRotationsSource && (this._rotationsCPU || this._rotationsPtr) || !this._wasmScalesSource && (this._scalesCPU || this._scalesPtr) || !this._wasmAttributesSource && (this._attributesCPU || this._attributesPtr)); } hasCPUOrInternalPointerInputs() { return !!(this._positionsCPU || this._rotationsCPU || this._scalesCPU || this._attributesCPU || this._positionsPtr || this._rotationsPtr || this._scalesPtr || this._attributesPtr); } hasPositionsSource() { return !!(this._wasmPositionsSource || this._positionsCPU || this._positionsPtr || this.positionsBuffer); } hasRotationsSource() { return !!(this._wasmRotationsSource || this._rotationsCPU || this._rotationsPtr || this.rotationsBuffer); } hasScalesSource() { return !!(this._wasmScalesSource || this._scalesCPU || this._scalesPtr || this.scalesBuffer); } assertCoreSourcesAvailable(label) { if (this._instanceCount <= 0) return; assert(this.hasPositionsSource() && this.hasRotationsSource() && this.hasScalesSource(), `GlyphField: ${label} requires positions, rotations, and scales sources.`); } validateNonWasmCPUCount(count) { if (!this._wasmPositionsSource && this._positionsCPU) assert(this._positionsCPU.length / 4 === count, "GlyphField: positions length does not match instanceCount."); if (!this._wasmRotationsSource && this._rotationsCPU) assert(this._rotationsCPU.length / 4 === count, "GlyphField: rotations length does not match instanceCount."); if (!this._wasmScalesSource && this._scalesCPU) assert(this._scalesCPU.length / 4 === count, "GlyphField: scales length does not match instanceCount."); if (!this._wasmAttributesSource && this._attributesCPU) assert(this._attributesCPU.length / 4 === count, "GlyphField: attributes length does not match instanceCount."); } clearWasmPositionsState(destroyManagedBuffer) { this._wasmPositionsSource = null; this._wasmPositionsDirty = false; this._wasmPositionsCapacityHint = 0; if (destroyManagedBuffer && this._positionsWasmManaged) { this.replacePositionsBuffer(null, false); this.bindGroupKey = null; } this._positionsWasmManaged = false; this._wasmPositionsCapacity = 0; } clearWasmRotationsState(destroyManagedBuffer) { this._wasmRotationsSource = null; this._wasmRotationsDirty = false; this._wasmRotationsCapacityHint = 0; if (destroyManagedBuffer && this._rotationsWasmManaged) { this.replaceRotationsBuffer(null, false); this.bindGroupKey = null; } this._rotationsWasmManaged = false; this._wasmRotationsCapacity = 0; } clearWasmScalesState(destroyManagedBuffer) { this._wasmScalesSource = null; this._wasmScalesDirty = false; this._wasmScalesCapacityHint = 0; if (destroyManagedBuffer && this._scalesWasmManaged) { this.replaceScalesBuffer(null, false); this.bindGroupKey = null; } this._scalesWasmManaged = false; this._wasmScalesCapacity = 0; } clearWasmAttributesState(destroyManagedBuffer) { this._wasmAttributesSource = null; this._wasmAttributesDirty = false; this._wasmAttributesCapacityHint = 0; if (destroyManagedBuffer && this._attributesWasmManaged) { this.replaceAttributesBuffer(null, false); this.bindGroupKey = null; } this._attributesWasmManaged = false; this._wasmAttributesCapacity = 0; } clearAllWasmState(destroyManagedBuffers) { this.clearWasmPositionsState(destroyManagedBuffers); this.clearWasmRotationsState(destroyManagedBuffers); this.clearWasmScalesState(destroyManagedBuffers); this.clearWasmAttributesState(destroyManagedBuffers); } clearCPUAndPointerChannel(channel) { if (channel === "positions") { this._positionsCPU = null; this._positionsPtr = 0; } else if (channel === "rotations") { this._rotationsCPU = null; this._rotationsPtr = 0; } else if (channel === "scales") { this._scalesCPU = null; this._scalesPtr = 0; } else { this._attributesCPU = null; this._attributesPtr = 0; } this._usingWasmPtrs = false; this._usingExternalBuffers = false; } initializeMixedDescriptorSources(desc, positionsBuffer, rotationsBuffer, scalesBuffer, attributesBuffer) { let count = desc.instanceCount !== void 0 ? assertWasmRecordCount(desc.instanceCount, "GlyphField: instanceCount") : 0; let hasCount = desc.instanceCount !== void 0; let dataDirty = false; const acceptArrayCount = (data, label) => { assert(data.length % 4 === 0, `GlyphField: ${label} length must be a multiple of 4.`); const nextCount = data.length / 4; if (!hasCount) { count = nextCount; hasCount = true; return; } assert(nextCount === count, `GlyphField: ${label} length does not match instanceCount.`); }; const setCPUChannel = (channel, data) => { if (!data) return; acceptArrayCount(data, channel); if (channel === "positions") this._positionsCPU = data; else if (channel === "rotations") this._rotationsCPU = data; else if (channel === "scales") this._scalesCPU = data; else this._attributesCPU = data; dataDirty = true; }; const setPtrChannel = (channel, ptr) => { if (!ptr) return; if (channel === "positions") this._positionsPtr = ptr >>> 0; else if (channel === "rotations") this._rotationsPtr = ptr >>> 0; else if (channel === "scales") this._scalesPtr = ptr >>> 0; else this._attributesPtr = ptr >>> 0; dataDirty = true; }; const setBufferChannel = (channel, buffer) => { if (!buffer) return; if (channel === "positions") this.replacePositionsBuffer(buffer, this._ownExternalBuffers); else if (channel === "rotations") this.replaceRotationsBuffer(buffer, this._ownExternalBuffers); else if (channel === "scales") this.replaceScalesBuffer(buffer, this._ownExternalBuffers); else this.replaceAttributesBuffer(buffer, this._ownExternalBuffers); this.bindGroupKey = null; }; if (!desc.wasmPositions) { if (positionsBuffer) setBufferChannel("positions", positionsBuffer); else if (desc.positionsPtr) setPtrChannel("positions", desc.positionsPtr); else setCPUChannel("positions", desc.positions); } if (!desc.wasmRotations) { if (rotationsBuffer) setBufferChannel("rotations", rotationsBuffer); else if (desc.rotationsPtr) setPtrChannel("rotations", desc.rotationsPtr); else setCPUChannel("rotations", desc.rotations); } if (!desc.wasmScales) { if (scalesBuffer) setBufferChannel("scales", scalesBuffer); else if (desc.scalesPtr) setPtrChannel("scales", desc.scalesPtr); else setCPUChannel("scales", desc.scales); } if (!desc.wasmAttributes) { if (attributesBuffer) setBufferChannel("attributes", attributesBuffer); else if (desc.attributesPtr) setPtrChannel("attributes", desc.attributesPtr); else setCPUChannel("attributes", desc.attributes); } if (hasCount) this._instanceCount = count; this._usingWasmPtrs = false; this._usingExternalBuffers = false; this._keepCPUData = desc.keepCPUData ?? this._keepCPUData; this.clearComputedBoundsIfNeeded(); if (dataDirty) { this._dataDirty = true; this._scaleRevision++; } } primaryWasmCoreChannel() { if (this._wasmPositionsSource) return "positions"; if (this._wasmRotationsSource) return "rotations"; if (this._wasmScalesSource) return "scales"; return null; } setInstanceCountFromWasm(instanceCount) { const count = assertWasmRecordCount(instanceCount, "GlyphField: instanceCount"); const changed = count !== this._instanceCount; if (changed) this.validateNonWasmCPUCount(count); this._instanceCount = count; if (!changed) return; if (this._wasmPositionsSource && this._positionsCPU && this._positionsCPU.length / 4 !== count) this._positionsCPU = null; if (this._wasmRotationsSource && this._rotationsCPU && this._rotationsCPU.length / 4 !== count) this._rotationsCPU = null; if (this._wasmScalesSource && this._scalesCPU && this._scalesCPU.length / 4 !== count) this._scalesCPU = null; if (this._wasmAttributesSource && this._attributesCPU && this._attributesCPU.length / 4 !== count) this._attributesCPU = null; if (this._wasmPositionsSource) this._wasmPositionsDirty = true; if (this._wasmRotationsSource) this._wasmRotationsDirty = true; if (this._wasmScalesSource) this._wasmScalesDirty = true; if (this._wasmAttributesSource) this._wasmAttributesDirty = true; this._dataDirty = true; } resolveWasmChannelCount(channel, source, explicitInstanceCount) { const field = `wasm${channel[0].toUpperCase()}${channel.slice(1)}`; const primary = this.primaryWasmCoreChannel(); if (explicitInstanceCount !== void 0) { const count = assertWasmRecordCount(explicitInstanceCount, "GlyphField: instanceCount"); assert(!primary || channel === primary || count === this._instanceCount, `GlyphField: refreshWasm${channel[0].toUpperCase()}${channel.slice(1)} instanceCount must match the current instanceCount when another core wasm source is active; call refreshFromWasm() to update instance count.`); validateGlyphWasmRecordRange(source, count, field); return count; } if (channel === primary) return resolveGlyphWasmRecordCount(source, void 0, field); assert(this._instanceCount > 0 || source.length === 0, `GlyphField: instanceCount is required when using ${field} without a core wasm source.`); validateGlyphWasmRecordRange(source, this._instanceCount, field); return this._instanceCount; } setWasmChannelSource(channel, source, capacity) { if (source === null) { if (channel === "positions") this.clearWasmPositionsState(true); else if (channel === "rotations") this.clearWasmRotationsState(true); else if (channel === "scales") this.clearWasmScalesState(true); else this.clearWasmAttributesState(true); return false; } const field = `wasm${channel[0].toUpperCase()}${channel.slice(1)}`; const wasmSource = assertWasmF32View(source, `GlyphField: ${field}`); const capacityHint = assertWasmCapacity(capacity, `GlyphField: ${field} capacity`); if (channel === "positions") { this._wasmPositionsCapacityHint = capacityHint; if (!this._positionsWasmManaged) { this.replacePositionsBuffer(null, false); this._wasmPositionsCapacity = 0; this.bindGroupKey = null; } this._wasmPositionsSource = wasmSource; } else if (channel === "rotations") { this._wasmRotationsCapacityHint = capacityHint; if (!this._rotationsWasmManaged) { this.replaceRotationsBuffer(null, false); this._wasmRotationsCapacity = 0; this.bindGroupKey = null; } this._wasmRotationsSource = wasmSource; } else if (channel === "scales") { this._wasmScalesCapacityHint = capacityHint; if (!this._scalesWasmManaged) { this.replaceScalesBuffer(null, false); this._wasmScalesCapacity = 0; this.bindGroupKey = null; } this._wasmScalesSource = wasmSource; } else { this._wasmAttributesCapacityHint = capacityHint; if (!this._attributesWasmManaged) { this.replaceAttributesBuffer(null, false); this._wasmAttributesCapacity = 0; this.bindGroupKey = null; } this._wasmAttributesSource = wasmSource; } this.clearCPUAndPointerChannel(channel); return true; } copyWasmActiveRange(source, instanceCount) { const view = source.array(); return new Float32Array(view.subarray(0, instanceCount * GLYPH_RECORD_FLOATS)); } ensureWasmPositionsBuffer(device, instanceCount) { const required = Math.max(instanceCount, this._wasmPositionsCapacityHint); if (required <= 0) return; if (this.positionsBuffer && this._positionsWasmManaged && this._wasmPositionsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmPositionsCapacity); this.replacePositionsBuffer(device.createBuffer({ label: "GlyphField.wasmPositions", size: capacity * GLYPH_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._positionsWasmManaged = true; this._wasmPositionsCapacity = capacity; this.bindGroupKey = null; } ensureWasmRotationsBuffer(device, instanceCount) { const required = Math.max(instanceCount, this._wasmRotationsCapacityHint); if (required <= 0) return; if (this.rotationsBuffer && this._rotationsWasmManaged && this._wasmRotationsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmRotationsCapacity); this.replaceRotationsBuffer(device.createBuffer({ label: "GlyphField.wasmRotations", size: capacity * GLYPH_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._rotationsWasmManaged = true; this._wasmRotationsCapacity = capacity; this.bindGroupKey = null; } ensureWasmScalesBuffer(device, instanceCount) { const required = Math.max(instanceCount, this._wasmScalesCapacityHint); if (required <= 0) return; if (this.scalesBuffer && this._scalesWasmManaged && this._wasmScalesCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmScalesCapacity); this.replaceScalesBuffer(device.createBuffer({ label: "GlyphField.wasmScales", size: capacity * GLYPH_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._scalesWasmManaged = true; this._wasmScalesCapacity = capacity; this.bindGroupKey = null; } ensureWasmAttributesBuffer(device, instanceCount) { const required = Math.max(instanceCount, this._wasmAttributesCapacityHint); if (required <= 0) return; if (this.attributesBuffer && this._attributesWasmManaged && this._wasmAttributesCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmAttributesCapacity); this.replaceAttributesBuffer(device.createBuffer({ label: "GlyphField.wasmAttributes", size: capacity * GLYPH_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._attributesWasmManaged = true; this._wasmAttributesCapacity = capacity; this.bindGroupKey = null; } computeBoundsFromPackedData(positions, scales, rotations, instanceCount) { if (instanceCount <= 0) return; const glyphCenter = this.geometry.boundsCenter; const glyphRadius = this.geometry.boundsRadius; let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let i = 0; i < instanceCount; i++) { const base = i * GLYPH_RECORD_FLOATS; const sx = Math.abs(scales[base + 0]); const sy = Math.abs(scales[base + 1]); const sz = Math.abs(scales[base + 2]); let cx = glyphCenter[0] * sx; let cy = glyphCenter[1] * sy; let cz = glyphCenter[2] * sz; if (rotations) { const rotated = rotateGlyphOffset(cx, cy, cz, rotations[base + 0], rotations[base + 1], rotations[base + 2], rotations[base + 3]); cx = rotated[0]; cy = rotated[1]; cz = rotated[2]; } const x = positions[base + 0] + cx; const y = positions[base + 1] + cy; const z = positions[base + 2] + cz; const r = glyphRadius * Math.max(sx, sy, sz); if (x - r < minX) minX = x - r; if (y - r < minY) minY = y - r; if (z - r < minZ) minZ = z - r; if (x + r > maxX) maxX = x + r; if (y + r > maxY) maxY = y + r; if (z + r > maxZ) maxZ = z + r; } this.setBounds(boundsFromBox([minX, minY, minZ], [maxX, maxY, maxZ]), "computed"); } computeBoundsFromWasmSources(instanceCount) { const positionsSource = this._wasmPositionsSource; const scalesSource = this._wasmScalesSource; if (!positionsSource || !scalesSource || instanceCount <= 0) return false; positionsSource.refresh(); scalesSource.refresh(); assertWasmF32View(positionsSource, "GlyphField: wasmPositions"); assertWasmF32View(scalesSource, "GlyphField: wasmScales"); validateGlyphWasmRecordRange(positionsSource, instanceCount, "wasmPositions"); validateGlyphWasmRecordRange(scalesSource, instanceCount, "wasmScales"); let rotations = null; if (this._wasmRotationsSource) { this._wasmRotationsSource.refresh(); assertWasmF32View(this._wasmRotationsSource, "GlyphField: wasmRotations"); validateGlyphWasmRecordRange(this._wasmRotationsSource, instanceCount, "wasmRotations"); rotations = this._wasmRotationsSource.array(); } this.computeBoundsFromPackedData(positionsSource.array(), scalesSource.array(), rotations, instanceCount); return true; } updateWasmBounds(options) { if (options.recomputeBounds && this._boundsSource !== "explicit" && this.computeBoundsFromWasmSources(this._instanceCount)) return; this.clearComputedBoundsIfNeeded(); } get instanceCount() { return this._instanceCount; } get occluderRevision() { let hash = 2166136261 >>> 0; hash = mixGlyphRevision(hash, this._instanceCount >>> 0); hash = mixGlyphRevision(hash, this._scaleRevision >>> 0); hash = mixGlyphRevision(hash, this.blendMode === "opaque" /* Opaque */ ? 1 : this.blendMode === "transparent" /* Transparent */ ? 2 : 3); hash = mixGlyphRevision(hash, this.cullMode === "back" /* Back */ ? 1 : this.cullMode === "front" /* Front */ ? 2 : 3); hash = mixGlyphRevision(hash, this.depthWrite ? 1 : 0); hash = mixGlyphRevision(hash, this.depthTest ? 1 : 0); hash = mixGlyphRevision(hash, colorModeId2(this._colorMode) >>> 0); hash = mixGlyphRevision(hash, this._dataDirty ? 1 : 0); hash = mixGlyphRevision(hash, this.geometry.vertexCount >>> 0); hash = mixGlyphRevision(hash, this.geometry.indexCount >>> 0); hash = mixGlyphRevision(hash, this.attributesBuffer ? 1 : 0); hash = mixGlyphRevisionF32(hash, this._opacity); return hash >>> 0; } get ndShape() { return this._ndShape ? this._ndShape.slice() : null; } set ndShape(shape) { this._ndShape = normalizePositiveIntShape(shape, "GlyphField: ndShape"); } get scaleTransform() { return cloneScaleTransform(this._scaleTransform); } setScaleTransform(transform) { this._scaleTransform = normalizeGlyphScaleTransform(transform); this._uniformDirty = true; this.emitVisualChange("scale"); } applyScaleStats(stats) { const next = cloneScaleTransform(this._scaleTransform); if (Number.isFinite(stats.min)) next.domainMin = stats.min; if (Number.isFinite(stats.max)) next.domainMax = stats.max; if (stats.percentileMin !== null && stats.percentileMax !== null) { next.clampMin = stats.percentileMin; next.clampMax = stats.percentileMax; } this._scaleTransform = normalizeGlyphScaleTransform(next); this._uniformDirty = true; this.emitVisualChange("scale"); } onVisualChange(listener) { this._visualChangeListeners.add(listener); return () => this._visualChangeListeners.delete(listener); } getScaleSourceDescriptor(revision = this._scaleRevision) { if (!this.attributesBuffer || this._instanceCount <= 0) return null; return { buffer: this.attributesBuffer, count: this._instanceCount, componentCount: this._scaleTransform.componentCount, componentIndex: this._scaleTransform.componentIndex, valueMode: this._scaleTransform.valueMode, stride: this._scaleTransform.stride, offset: this._scaleTransform.offset, revision }; } set instanceCount(v) { const n = v | 0; if (n === this._instanceCount) return; assert(n >= 0, "GlyphField: instanceCount must be >= 0."); this._instanceCount = n; this._dataDirty = true; this._scaleRevision++; } get colorMode() { return this._colorMode; } set colorMode(v) { if (v === this._colorMode) return; this._colorMode = v; this._uniformDirty = true; } get colormap() { return this._colormap; } set colormap(v) { this._colormap = v; this._uniformDirty = true; this.bindGroupKey = null; this.emitVisualChange("colormap"); } get colormapStops() { return this._colormapStops; } set colormapStops(v) { this._colormapStops = normalizeColorStops(v); this._uniformDirty = true; this.emitVisualChange("colormap"); } getColormapKey() { const c = this._colormap; return c instanceof Colormap ? `cm:${c.id}` : `cm:${c}`; } getColormapForBinding() { const c = this._colormap; if (c instanceof Colormap) return c; if (c === "custom") return Colormap.builtin("grayscale"); return Colormap.builtin(c); } get opacity() { return this._opacity; } set opacity(v) { if (v === this._opacity) return; this._opacity = v; this._uniformDirty = true; } get lit() { return this._lit; } set lit(v) { const b = !!v; if (b === this._lit) return; this._lit = b; this._uniformDirty = true; } get solidColor() { return this._solidColor; } set solidColor(v) { this._solidColor = [v[0], v[1], v[2], v[3]]; this._uniformDirty = true; } markDataDirty() { if (!this._usingExternalBuffers) this._dataDirty = true; this._scaleRevision++; } markUniformsDirty() { this._uniformDirty = true; } getAttributeRecord(index) { const data = this._attributesCPU; if (!data) return null; if (!Number.isInteger(index) || index < 0 || index >= this._instanceCount) return null; const o = index * 4; return [data[o + 0], data[o + 1], data[o + 2], data[o + 3]]; } mapLinearIndexToNd(index) { return linearIndexToNdIndex(this._ndShape, index); } setCPUData(positions, rotations, scales, attributes, opts = {}) { if (positions) assert(positions.length % 4 === 0, "GlyphField: positions length must be a multiple of 4 (x,y,z,_ per instance)."); if (rotations) assert(rotations.length % 4 === 0, "GlyphField: rotations length must be a multiple of 4 (qx,qy,qz,qw per instance)."); if (scales) assert(scales.length % 4 === 0, "GlyphField: scales length must be a multiple of 4 (sx,sy,sz,_ per instance)."); if (attributes) assert(attributes.length % 4 === 0, "GlyphField: attributes length must be a multiple of 4 (a0,a1,a2,a3 per instance)."); const count = opts.instanceCount !== void 0 ? opts.instanceCount | 0 : positions ? positions.length / 4 : rotations ? rotations.length / 4 : scales ? scales.length / 4 : attributes ? attributes.length / 4 : 0; assert(count >= 0, "GlyphField: instanceCount must be >= 0."); if (count > 0) assert(!!positions && !!rotations && !!scales, "GlyphField: positions, rotations, and scales are required for CPU-backed glyph fields."); if (positions) assert(positions.length / 4 === count, "GlyphField: positions length does not match instanceCount."); if (rotations) assert(rotations.length / 4 === count, "GlyphField: rotations length does not match instanceCount."); if (scales) assert(scales.length / 4 === count, "GlyphField: scales length does not match instanceCount."); if (attributes) assert(attributes.length / 4 === count, "GlyphField: attributes length does not match instanceCount."); this.clearAllWasmState(true); if (this._usingExternalBuffers) this.releaseInstanceBuffers(); else if (!attributes) this.replaceAttributesBuffer(null, false); this._instanceCount = count; this._positionsCPU = positions; this._rotationsCPU = rotations; this._scalesCPU = scales; this._attributesCPU = attributes; this._positionsPtr = 0; this._rotationsPtr = 0; this._scalesPtr = 0; this._attributesPtr = 0; this._usingWasmPtrs = false; this._usingExternalBuffers = false; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.clearComputedBoundsIfNeeded(); this._dataDirty = true; this._scaleRevision++; this.bindGroupKey = null; } setWasmSoA(positionsPtr, rotationsPtr, scalesPtr, attributesPtr, instanceCount) { const count = instanceCount | 0; assert(count > 0, "GlyphField: instanceCount must be > 0."); this.clearAllWasmState(true); if (this._usingExternalBuffers) this.releaseInstanceBuffers(); else if (!attributesPtr) this.replaceAttributesBuffer(null, false); this._instanceCount = count; this._positionsCPU = null; this._rotationsCPU = null; this._scalesCPU = null; this._attributesCPU = null; this._positionsPtr = positionsPtr >>> 0; this._rotationsPtr = rotationsPtr >>> 0; this._scalesPtr = scalesPtr >>> 0; this._attributesPtr = attributesPtr >>> 0; this._usingWasmPtrs = true; this._usingExternalBuffers = false; this.clearComputedBoundsIfNeeded(); this._dataDirty = true; this._scaleRevision++; this.bindGroupKey = null; } setBuffers(positions, rotations, scales, attributes, instanceCount, opts = {}) { const count = instanceCount | 0; assert(count > 0, "GlyphField: instanceCount must be > 0."); const ownBuffers = !!opts.ownBuffers; this.clearAllWasmState(true); this._instanceCount = count; this.replacePositionsBuffer(positions, ownBuffers); this.replaceRotationsBuffer(rotations, ownBuffers); this.replaceScalesBuffer(scales, ownBuffers); this.replaceAttributesBuffer(attributes, ownBuffers && !!attributes); this._positionsCPU = null; this._rotationsCPU = null; this._scalesCPU = null; this._attributesCPU = null; this._positionsPtr = 0; this._rotationsPtr = 0; this._scalesPtr = 0; this._attributesPtr = 0; this._usingWasmPtrs = false; this._usingExternalBuffers = true; this.clearComputedBoundsIfNeeded(); this._dataDirty = false; this._scaleRevision++; this.bindGroupKey = null; } setWasmPositions(source, options = {}) { if (!this.setWasmChannelSource("positions", source, options.capacity)) return; this.refreshWasmPositions(options); } setWasmRotations(source, options = {}) { if (!this.setWasmChannelSource("rotations", source, options.capacity)) return; this.refreshWasmRotations(options); } setWasmScales(source, options = {}) { if (!this.setWasmChannelSource("scales", source, options.capacity)) return; this.refreshWasmScales(options); } setWasmAttributes(source, options = {}) { if (!this.setWasmChannelSource("attributes", source, options.capacity)) return; this.refreshWasmAttributes(options); } setWasmInstances(sources, options = {}) { if (Object.prototype.hasOwnProperty.call(sources, "positions")) this.setWasmChannelSource("positions", sources.positions ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "rotations")) this.setWasmChannelSource("rotations", sources.rotations ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "scales")) this.setWasmChannelSource("scales", sources.scales ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "attributes")) this.setWasmChannelSource("attributes", sources.attributes ?? null, options.capacity); this.refreshFromWasm(options); } refreshWasmPositions(options = {}) { const source = this._wasmPositionsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmPositions"); const count = this.resolveWasmChannelCount("positions", source, options.instanceCount); this.setInstanceCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._positionsCPU = this.copyWasmActiveRange(source, count); else this._positionsCPU = null; this.assertCoreSourcesAvailable("refreshWasmPositions"); this.updateWasmBounds(options); this._wasmPositionsDirty = true; this._dataDirty = true; } refreshWasmRotations(options = {}) { const source = this._wasmRotationsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmRotations"); const count = this.resolveWasmChannelCount("rotations", source, options.instanceCount); this.setInstanceCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._rotationsCPU = this.copyWasmActiveRange(source, count); else this._rotationsCPU = null; this.assertCoreSourcesAvailable("refreshWasmRotations"); this.updateWasmBounds(options); this._wasmRotationsDirty = true; this._dataDirty = true; } refreshWasmScales(options = {}) { const source = this._wasmScalesSource; if (!source) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmScales"); const count = this.resolveWasmChannelCount("scales", source, options.instanceCount); this.setInstanceCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._scalesCPU = this.copyWasmActiveRange(source, count); else this._scalesCPU = null; this.assertCoreSourcesAvailable("refreshWasmScales"); this.updateWasmBounds(options); this._wasmScalesDirty = true; this._dataDirty = true; } refreshWasmAttributes(options = {}) { const source = this._wasmAttributesSource; if (!source) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmAttributes"); const count = this.resolveWasmChannelCount("attributes", source, options.instanceCount); this.setInstanceCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._attributesCPU = this.copyWasmActiveRange(source, count); else this._attributesCPU = null; this._wasmAttributesDirty = true; this._dataDirty = true; this._scaleRevision++; } refreshFromWasm(options = {}) { if (this._wasmPositionsSource) this.refreshWasmPositions(options); if (this._wasmRotationsSource) this.refreshWasmRotations(options); if (this._wasmScalesSource) this.refreshWasmScales(options); if (this._wasmAttributesSource) this.refreshWasmAttributes(options); } clearWasmSources() { this.clearAllWasmState(true); if (!this.hasCPUOrInternalPointerInputs()) this._dataDirty = false; } computeBoundsFromCPUData() { const positions = this._positionsCPU; const scales = this._scalesCPU; const count = this._instanceCount; if (!positions || !scales || count <= 0) return; this.computeBoundsFromPackedData(positions, scales, this._rotationsCPU, count); } getLocalBounds() { if (this._boundsSource === "none" && this._positionsCPU && this._scalesCPU) this.computeBoundsFromCPUData(); if (this._boundsSource === "none") return emptyBounds(this._instanceCount > 0); return boundsFromBox(this.boundsMin, this.boundsMax); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } uploadWasmPositions(device, queue) { const source = this._wasmPositionsSource; if (!source || !this._wasmPositionsDirty) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmPositions"); const count = this._instanceCount; validateGlyphWasmRecordRange(source, count, "wasmPositions"); if (count <= 0) { this._wasmPositionsDirty = false; return; } const data = source.array(); const byteLength = count * GLYPH_RECORD_BYTES; this.ensureWasmPositionsBuffer(device, count); const write = () => { assert(!!this.positionsBuffer, "GlyphField: wasmPositions upload requires a positionsBuffer."); queue.writeBuffer(this.positionsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replacePositionsBuffer(null, false); this._positionsWasmManaged = false; this._wasmPositionsCapacity = 0; this.ensureWasmPositionsBuffer(device, count); write(); } if (this._keepCPUData) this._positionsCPU = new Float32Array(data.subarray(0, count * GLYPH_RECORD_FLOATS)); else this._positionsCPU = null; this._wasmPositionsDirty = false; } uploadWasmRotations(device, queue) { const source = this._wasmRotationsSource; if (!source || !this._wasmRotationsDirty) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmRotations"); const count = this._instanceCount; validateGlyphWasmRecordRange(source, count, "wasmRotations"); if (count <= 0) { this._wasmRotationsDirty = false; return; } const data = source.array(); const byteLength = count * GLYPH_RECORD_BYTES; this.ensureWasmRotationsBuffer(device, count); const write = () => { assert(!!this.rotationsBuffer, "GlyphField: wasmRotations upload requires a rotationsBuffer."); queue.writeBuffer(this.rotationsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceRotationsBuffer(null, false); this._rotationsWasmManaged = false; this._wasmRotationsCapacity = 0; this.ensureWasmRotationsBuffer(device, count); write(); } if (this._keepCPUData) this._rotationsCPU = new Float32Array(data.subarray(0, count * GLYPH_RECORD_FLOATS)); else this._rotationsCPU = null; this._wasmRotationsDirty = false; } uploadWasmScales(device, queue) { const source = this._wasmScalesSource; if (!source || !this._wasmScalesDirty) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmScales"); const count = this._instanceCount; validateGlyphWasmRecordRange(source, count, "wasmScales"); if (count <= 0) { this._wasmScalesDirty = false; return; } const data = source.array(); const byteLength = count * GLYPH_RECORD_BYTES; this.ensureWasmScalesBuffer(device, count); const write = () => { assert(!!this.scalesBuffer, "GlyphField: wasmScales upload requires a scalesBuffer."); queue.writeBuffer(this.scalesBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceScalesBuffer(null, false); this._scalesWasmManaged = false; this._wasmScalesCapacity = 0; this.ensureWasmScalesBuffer(device, count); write(); } if (this._keepCPUData) this._scalesCPU = new Float32Array(data.subarray(0, count * GLYPH_RECORD_FLOATS)); else this._scalesCPU = null; this._wasmScalesDirty = false; } uploadWasmAttributes(device, queue) { const source = this._wasmAttributesSource; if (!source || !this._wasmAttributesDirty) return; source.refresh(); assertWasmF32View(source, "GlyphField: wasmAttributes"); const count = this._instanceCount; validateGlyphWasmRecordRange(source, count, "wasmAttributes"); if (count <= 0) { this._wasmAttributesDirty = false; return; } const data = source.array(); const byteLength = count * GLYPH_RECORD_BYTES; this.ensureWasmAttributesBuffer(device, count); const write = () => { assert(!!this.attributesBuffer, "GlyphField: wasmAttributes upload requires an attributesBuffer."); queue.writeBuffer(this.attributesBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceAttributesBuffer(null, false); this._attributesWasmManaged = false; this._wasmAttributesCapacity = 0; this.ensureWasmAttributesBuffer(device, count); write(); } if (this._keepCPUData) this._attributesCPU = new Float32Array(data.subarray(0, count * GLYPH_RECORD_FLOATS)); else this._attributesCPU = null; this._wasmAttributesDirty = false; } uploadWasmSources(device, queue) { if (this._instanceCount <= 0) { this._wasmPositionsDirty = false; this._wasmRotationsDirty = false; this._wasmScalesDirty = false; this._wasmAttributesDirty = false; return; } this.uploadWasmPositions(device, queue); this.uploadWasmRotations(device, queue); this.uploadWasmScales(device, queue); this.uploadWasmAttributes(device, queue); if (!this._wasmAttributesSource && !this._attributesCPU && !this._attributesPtr && !this.attributesBuffer) { this.replaceAttributesBuffer(device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "GlyphField.attributesDummy" }), true); this._attributesWasmManaged = false; this._wasmAttributesCapacity = 0; this.bindGroupKey = null; } } upload(device, queue) { const hadDataDirty = this._dataDirty; if (this.hasDirtyWasmSources()) this.uploadWasmSources(device, queue); if (this._usingExternalBuffers && !this.hasExternalWasmSources()) return; const needsNonWasmUpload = hadDataDirty && this.hasDirtyNonWasmSources(); if (!needsNonWasmUpload) { this._dataDirty = this.hasDirtyWasmSources(); return; } if (this._instanceCount <= 0) { this._dataDirty = this.hasDirtyWasmSources(); return; } this.assertCoreSourcesAvailable("upload"); const bytes = driver.bytes(); const requiredBytes = this._instanceCount * 16; const uploadSoA = (buf, owned, cpu, ptr, label) => { if (!cpu && !ptr) return { buffer: buf, owned }; const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; const byteLength = requiredBytes; const source = cpu ?? new Uint8Array(bytes.buffer, ptr >>> 0, byteLength); if (!buf || !owned) return { buffer: createBuffer(device, source, usage, label), owned: true }; try { queue.writeBuffer(buf, 0, source.buffer, source.byteOffset, Math.min(source.byteLength, byteLength)); return { buffer: buf, owned: true }; } catch { return { buffer: createBuffer(device, source, usage, label), owned: true }; } }; const positions = this._wasmPositionsSource ? { buffer: this.positionsBuffer, owned: this._positionsOwned } : uploadSoA(this.positionsBuffer, this._positionsOwned, this._positionsCPU, this._positionsPtr, "GlyphField.positions"); const rotations = this._wasmRotationsSource ? { buffer: this.rotationsBuffer, owned: this._rotationsOwned } : uploadSoA(this.rotationsBuffer, this._rotationsOwned, this._rotationsCPU, this._rotationsPtr, "GlyphField.rotations"); const scales = this._wasmScalesSource ? { buffer: this.scalesBuffer, owned: this._scalesOwned } : uploadSoA(this.scalesBuffer, this._scalesOwned, this._scalesCPU, this._scalesPtr, "GlyphField.scales"); this.replacePositionsBuffer(positions.buffer, positions.owned); this.replaceRotationsBuffer(rotations.buffer, rotations.owned); this.replaceScalesBuffer(scales.buffer, scales.owned); if (!this._wasmAttributesSource && (this._attributesCPU || this._attributesPtr)) { const attributes = uploadSoA(this.attributesBuffer, this._attributesOwned, this._attributesCPU, this._attributesPtr, "GlyphField.attributes"); this.replaceAttributesBuffer(attributes.buffer, attributes.owned); } else if (!this._wasmAttributesSource && (!this.attributesBuffer || !this._attributesOwned)) this.replaceAttributesBuffer(device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "GlyphField.attributesDummy" }), true); if (!this._keepCPUData) { if (!this._wasmPositionsSource) this._positionsCPU = null; if (!this._wasmRotationsSource) this._rotationsCPU = null; if (!this._wasmScalesSource) this._scalesCPU = null; if (!this._wasmAttributesSource) this._attributesCPU = null; } this._dataDirty = this.hasDirtyWasmSources(); this.bindGroupKey = null; } getUniformBufferSize() { return UNIFORM_BYTE_SIZE2; } getUniformData() { const out = new Float32Array(UNIFORM_FLOAT_COUNT2); out.fill(0); packScaleTransform(this._scaleTransform, out, 0); out[20] = clamp01(this._opacity); out[21] = typeof this._colormap === "string" && this._colormap === "custom" ? Math.min(8, Math.max(2, this._colormapStops.length)) : 0; out[22] = colorModeId2(this._colorMode); out[23] = this._lit ? 1 : 0; out[24] = this._solidColor[0]; out[25] = this._solidColor[1]; out[26] = this._solidColor[2]; out[27] = this._solidColor[3]; const stops = this._colormapStops; const nStops = Math.min(8, Math.max(2, stops.length)); for (let i = 0; i < 8; i++) { const src = stops[Math.min(i, nStops - 1)]; const o = 28 + i * 4; out[o + 0] = src[0]; out[o + 1] = src[1]; out[o + 2] = src[2]; out[o + 3] = src[3]; } return out; } get dirtyUniforms() { return this._uniformDirty; } markUniformsClean() { this._uniformDirty = false; } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) { try { listener(kind); } catch { } } } destroyOwnedBuffer(buffer, owned) { if (!buffer || !owned) return; buffer.destroy(); } destroy() { this.destroyOwnedBuffer(this.positionsBuffer, this._positionsOwned); this.destroyOwnedBuffer(this.rotationsBuffer, this._rotationsOwned); this.destroyOwnedBuffer(this.scalesBuffer, this._scalesOwned); this.destroyOwnedBuffer(this.attributesBuffer, this._attributesOwned); this.uniformBuffer?.destroy(); this.positionsBuffer = null; this.rotationsBuffer = null; this.scalesBuffer = null; this.attributesBuffer = null; this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this._positionsCPU = null; this._rotationsCPU = null; this._scalesCPU = null; this._attributesCPU = null; this._wasmPositionsSource = null; this._wasmRotationsSource = null; this._wasmScalesSource = null; this._wasmAttributesSource = null; this._ndShape = null; this._instanceCount = 0; this._positionsOwned = false; this._rotationsOwned = false; this._scalesOwned = false; this._attributesOwned = false; this._ownExternalBuffers = false; this._wasmPositionsDirty = false; this._wasmRotationsDirty = false; this._wasmScalesDirty = false; this._wasmAttributesDirty = false; this._positionsWasmManaged = false; this._rotationsWasmManaged = false; this._scalesWasmManaged = false; this._attributesWasmManaged = false; this._wasmPositionsCapacity = 0; this._wasmRotationsCapacity = 0; this._wasmScalesCapacity = 0; this._wasmAttributesCapacity = 0; this._wasmPositionsCapacityHint = 0; this._wasmRotationsCapacityHint = 0; this._wasmScalesCapacityHint = 0; this._wasmAttributesCapacityHint = 0; this._visualChangeListeners.clear(); this.transform.dispose(); } }; // typescript/world/nodelink.ts var UNIFORM_FLOAT_COUNT3 = 128; var UNIFORM_BYTE_SIZE3 = UNIFORM_FLOAT_COUNT3 * 4; var NODELINK_VEC4_FLOATS = 4; var NODELINK_U32_EDGE_COMPONENTS = 2; var NODELINK_F32_BYTES = 4; var NODELINK_VEC4_BYTES = NODELINK_VEC4_FLOATS * NODELINK_F32_BYTES; var NODELINK_EDGE_BYTES = NODELINK_U32_EDGE_COMPONENTS * 4; var wasmNodeFieldName = (channel) => `wasm${channel[0].toUpperCase()}${channel.slice(1)}`; var wasmEdgeFieldName = (channel) => channel === "edges" ? "wasmEdges" : `wasm${channel[0].toUpperCase()}${channel.slice(1)}`; var nodeWasmComponents = (channel) => channel === "nodeScalars" ? 1 : NODELINK_VEC4_FLOATS; var edgeWasmComponents = (channel) => channel === "edges" ? NODELINK_U32_EDGE_COMPONENTS : channel === "edgeScalars" ? 1 : NODELINK_VEC4_FLOATS; var normalizeNodeScaleTransform = (transform) => normalizeScaleTransform({ componentCount: 1, componentIndex: 0, stride: 1, offset: 0, mode: "linear", clampMode: "range", domainMin: 0, domainMax: 1, clampMin: 0, clampMax: 1, gamma: 1, invert: false, ...transform ?? {} }); var normalizeEdgeScaleTransform = (transform) => normalizeScaleTransform({ componentCount: 1, componentIndex: 0, stride: 1, offset: 0, mode: "linear", clampMode: "range", domainMin: 0, domainMax: 1, clampMin: 0, clampMax: 1, gamma: 1, invert: false, ...transform ?? {} }); var colorModeId3 = (mode) => mode === "rgba" ? 0 : mode === "scalar" ? 1 : 2; var nodeGeometryModeId = (mode) => mode === "points" ? 0 : mode === "spheres" ? 1 : mode === "ellipsoids" ? 2 : 3; var edgeGeometryModeId = (mode) => mode === "lines" ? 0 : 1; var isNodeGeometryMode = (value) => value === "points" || value === "spheres" || value === "ellipsoids" || value === "cubes"; var isEdgeGeometryMode = (value) => value === "lines" || value === "cylinders"; var isColorMode = (value) => value === "rgba" || value === "scalar" || value === "solid"; var cloneBytes = (data) => new Uint8Array(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); var nodeLinkRevisionScratch = new ArrayBuffer(4); var nodeLinkRevisionF32 = new Float32Array(nodeLinkRevisionScratch); var nodeLinkRevisionU32 = new Uint32Array(nodeLinkRevisionScratch); var mixNodeLinkRevision = (hash, value) => Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; var mixNodeLinkRevisionF32 = (hash, value) => { nodeLinkRevisionF32[0] = Number.isFinite(value) ? value : 0; return mixNodeLinkRevision(hash, nodeLinkRevisionU32[0] >>> 0); }; var NodeLink = class { transform = new Transform(); name = null; visible = true; blendMode = "opaque" /* Opaque */; cullMode = "back" /* Back */; depthWrite = true; depthTest = true; boundsMin = [0, 0, 0]; boundsMax = [0, 0, 0]; boundsCenter = [0, 0, 0]; boundsRadius = 0; nodePositionsBuffer = null; nodeScalarsBuffer = null; nodeColorsBuffer = null; nodeRadiiBuffer = null; edgesBuffer = null; edgeScalarsBuffer = null; edgeColorsBuffer = null; uniformBuffer = null; bindGroup = null; bindGroupKey = null; _nodeCount = 0; _edgeCount = 0; _nodePositionsCPU = null; _nodeScalarsCPU = null; _nodeColorsCPU = null; _nodeRadiiCPU = null; _edgesCPU = null; _edgeScalarsCPU = null; _edgeColorsCPU = null; _wasmNodePositionsSource = null; _wasmNodeScalarsSource = null; _wasmNodeColorsSource = null; _wasmNodeRadiiSource = null; _wasmEdgesSource = null; _wasmEdgeScalarsSource = null; _wasmEdgeColorsSource = null; _nodePositionsExternal = false; _nodeScalarsExternal = false; _nodeColorsExternal = false; _nodeRadiiExternal = false; _edgesExternal = false; _edgeScalarsExternal = false; _edgeColorsExternal = false; _nodePositionsOwned = false; _nodeScalarsOwned = false; _nodeColorsOwned = false; _nodeRadiiOwned = false; _edgesOwned = false; _edgeScalarsOwned = false; _edgeColorsOwned = false; _nodePositionsDirty = true; _nodeScalarsDirty = true; _nodeColorsDirty = true; _nodeRadiiDirty = true; _edgesDirty = true; _edgeScalarsDirty = true; _edgeColorsDirty = true; _wasmNodePositionsDirty = false; _wasmNodeScalarsDirty = false; _wasmNodeColorsDirty = false; _wasmNodeRadiiDirty = false; _wasmEdgesDirty = false; _wasmEdgeScalarsDirty = false; _wasmEdgeColorsDirty = false; _nodePositionsWasmManaged = false; _nodeScalarsWasmManaged = false; _nodeColorsWasmManaged = false; _nodeRadiiWasmManaged = false; _edgesWasmManaged = false; _edgeScalarsWasmManaged = false; _edgeColorsWasmManaged = false; _wasmNodePositionsCapacity = 0; _wasmNodeScalarsCapacity = 0; _wasmNodeColorsCapacity = 0; _wasmNodeRadiiCapacity = 0; _wasmEdgesCapacity = 0; _wasmEdgeScalarsCapacity = 0; _wasmEdgeColorsCapacity = 0; _wasmNodePositionsCapacityHint = 0; _wasmNodeScalarsCapacityHint = 0; _wasmNodeColorsCapacityHint = 0; _wasmNodeRadiiCapacityHint = 0; _wasmEdgesCapacityHint = 0; _wasmEdgeScalarsCapacityHint = 0; _wasmEdgeColorsCapacityHint = 0; _uniformDirty = true; _boundsSource = "none"; _keepCPUData = false; _ownExternalBuffers = false; _ndShape = null; _nodeGeometryMode = "points"; _edgeGeometryMode = "lines"; _nodeColorMode = "scalar"; _edgeColorMode = "solid"; _nodeScaleTransform = normalizeNodeScaleTransform(void 0); _edgeScaleTransform = normalizeEdgeScaleTransform(void 0); _nodeScaleRevision = 0; _edgeScaleRevision = 0; _nodeColormap = "viridis"; _edgeColormap = "viridis"; _nodeColormapStops = [[0.267, 487e-5, 0.32942, 1], [0.99325, 0.90616, 0.14394, 1]]; _edgeColormapStops = [[0.267, 487e-5, 0.32942, 1], [0.99325, 0.90616, 0.14394, 1]]; _nodeSolidColor = [1, 1, 1, 1]; _edgeSolidColor = [0.8, 0.8, 0.8, 1]; _nodeSize = 1; _minPointSize = 1; _maxPointSize = 32; _pointSizeAttenuation = 1; _edgeSize = 0.06; _opacity = 1; _lit = false; _pendingWrites = []; _visualChangeListeners = /* @__PURE__ */ new Set(); constructor(desc = {}) { this._nodeScaleTransform = normalizeNodeScaleTransform(desc.nodeScaleTransform); this._edgeScaleTransform = normalizeEdgeScaleTransform(desc.edgeScaleTransform); if (desc.nodeGeometryMode !== void 0) assert(isNodeGeometryMode(desc.nodeGeometryMode), `NodeLink: invalid nodeGeometryMode '${String(desc.nodeGeometryMode)}'.`); if (desc.edgeGeometryMode !== void 0) assert(isEdgeGeometryMode(desc.edgeGeometryMode), `NodeLink: invalid edgeGeometryMode '${String(desc.edgeGeometryMode)}'.`); if (desc.nodeColorMode !== void 0) assert(isColorMode(desc.nodeColorMode), `NodeLink: invalid nodeColorMode '${String(desc.nodeColorMode)}'.`); if (desc.edgeColorMode !== void 0) assert(isColorMode(desc.edgeColorMode), `NodeLink: invalid edgeColorMode '${String(desc.edgeColorMode)}'.`); if (desc.nodePositionsBuffer !== void 0) assert(desc.nodeCount !== void 0, "NodeLink: nodeCount is required when nodePositionsBuffer is provided."); if (desc.edgesBuffer !== void 0) assert(desc.edgeCount !== void 0, "NodeLink: edgeCount is required when edgesBuffer is provided."); if (desc.name !== void 0) this.name = desc.name; if (desc.visible !== void 0) this.visible = !!desc.visible; if (desc.blendMode !== void 0) this.blendMode = desc.blendMode; if (desc.cullMode !== void 0) this.cullMode = desc.cullMode; if (desc.depthWrite !== void 0) this.depthWrite = !!desc.depthWrite; if (desc.depthTest !== void 0) this.depthTest = !!desc.depthTest; if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; this._ownExternalBuffers = !!desc.ownBuffers; if (desc.ndShape !== void 0) this.ndShape = desc.ndShape; if (desc.nodeGeometryMode !== void 0) this._nodeGeometryMode = desc.nodeGeometryMode; if (desc.edgeGeometryMode !== void 0) this._edgeGeometryMode = desc.edgeGeometryMode; if (desc.nodeColorMode !== void 0) this._nodeColorMode = desc.nodeColorMode; if (desc.edgeColorMode !== void 0) this._edgeColorMode = desc.edgeColorMode; if (desc.nodeColormap !== void 0) this._nodeColormap = desc.nodeColormap; if (desc.edgeColormap !== void 0) this._edgeColormap = desc.edgeColormap; if (desc.nodeColormapStops !== void 0) this._nodeColormapStops = normalizeColorStops(desc.nodeColormapStops); if (desc.edgeColormapStops !== void 0) this._edgeColormapStops = normalizeColorStops(desc.edgeColormapStops); if (desc.nodeSolidColor !== void 0) this._nodeSolidColor = [desc.nodeSolidColor[0], desc.nodeSolidColor[1], desc.nodeSolidColor[2], desc.nodeSolidColor[3]]; if (desc.edgeSolidColor !== void 0) this._edgeSolidColor = [desc.edgeSolidColor[0], desc.edgeSolidColor[1], desc.edgeSolidColor[2], desc.edgeSolidColor[3]]; if (desc.nodeSize !== void 0) this._nodeSize = Math.max(0, desc.nodeSize); if (desc.minPointSize !== void 0) this._minPointSize = Math.max(0, desc.minPointSize); if (desc.maxPointSize !== void 0) this._maxPointSize = Math.max(this._minPointSize, desc.maxPointSize); if (desc.pointSizeAttenuation !== void 0) this._pointSizeAttenuation = Math.max(0, desc.pointSizeAttenuation); if (desc.edgeSize !== void 0) this._edgeSize = Math.max(0, desc.edgeSize); if (desc.opacity !== void 0) this._opacity = clamp01(desc.opacity); if (desc.lit !== void 0) this._lit = !!desc.lit; this.applyExplicitBounds(desc); const wasmNodeCapacity = assertWasmCapacity(desc.wasmNodeCapacity, "NodeLink: wasmNodeCapacity"); const wasmEdgeCapacity = assertWasmCapacity(desc.wasmEdgeCapacity, "NodeLink: wasmEdgeCapacity"); if (desc.nodePositions) this.setNodePositions(desc.nodePositions, { stride: desc.nodePositionsStride ?? 3, keepCPUData: this._keepCPUData }); else if (desc.wasmNodePositions) this.setWasmNodePositions(desc.wasmNodePositions, { nodeCount: desc.nodeCount, capacity: wasmNodeCapacity, keepCPUData: this._keepCPUData }); else if (desc.nodePositionsBuffer) this.setNodePositionsBuffer(resolveGPUBuffer(desc.nodePositionsBuffer), desc.nodeCount ?? 0, { ownBuffer: this._ownExternalBuffers }); else if (desc.nodeCount !== void 0) this._nodeCount = Math.max(0, desc.nodeCount | 0); if (desc.edges) this.setEdges(desc.edges, { keepCPUData: this._keepCPUData }); else if (desc.wasmEdges) this.setWasmEdges(desc.wasmEdges, { edgeCount: desc.edgeCount, capacity: wasmEdgeCapacity, keepCPUData: this._keepCPUData }); else if (desc.edgesBuffer) this.setEdgesBuffer(resolveGPUBuffer(desc.edgesBuffer), desc.edgeCount ?? 0, { ownBuffer: this._ownExternalBuffers }); else if (desc.edgeCount !== void 0) this._edgeCount = Math.max(0, desc.edgeCount | 0); if (desc.nodeScalars) this.setNodeScalars(desc.nodeScalars, { keepCPUData: this._keepCPUData }); else if (desc.wasmNodeScalars) this.setWasmNodeScalars(desc.wasmNodeScalars, { nodeCount: desc.nodeCount, capacity: wasmNodeCapacity, keepCPUData: this._keepCPUData }); else if (desc.nodeScalarsBuffer) this.setNodeScalarsBuffer(resolveGPUBuffer(desc.nodeScalarsBuffer), { ownBuffer: this._ownExternalBuffers }); if (desc.nodeColors) this.setNodeColors(desc.nodeColors, { keepCPUData: this._keepCPUData }); else if (desc.wasmNodeColors) this.setWasmNodeColors(desc.wasmNodeColors, { nodeCount: desc.nodeCount, capacity: wasmNodeCapacity, keepCPUData: this._keepCPUData }); else if (desc.nodeColorsBuffer) this.setNodeColorsBuffer(resolveGPUBuffer(desc.nodeColorsBuffer), { ownBuffer: this._ownExternalBuffers }); if (desc.nodeRadii) this.setNodeRadii(desc.nodeRadii, { stride: desc.nodeRadiiStride ?? 3, keepCPUData: this._keepCPUData }); else if (desc.wasmNodeRadii) this.setWasmNodeRadii(desc.wasmNodeRadii, { nodeCount: desc.nodeCount, capacity: wasmNodeCapacity, keepCPUData: this._keepCPUData }); else if (desc.nodeRadiiBuffer) this.setNodeRadiiBuffer(resolveGPUBuffer(desc.nodeRadiiBuffer), { ownBuffer: this._ownExternalBuffers }); if (desc.edgeScalars) this.setEdgeScalars(desc.edgeScalars, { keepCPUData: this._keepCPUData }); else if (desc.wasmEdgeScalars) this.setWasmEdgeScalars(desc.wasmEdgeScalars, { edgeCount: desc.edgeCount, capacity: wasmEdgeCapacity, keepCPUData: this._keepCPUData }); else if (desc.edgeScalarsBuffer) this.setEdgeScalarsBuffer(resolveGPUBuffer(desc.edgeScalarsBuffer), { ownBuffer: this._ownExternalBuffers }); if (desc.edgeColors) this.setEdgeColors(desc.edgeColors, { keepCPUData: this._keepCPUData }); else if (desc.wasmEdgeColors) this.setWasmEdgeColors(desc.wasmEdgeColors, { edgeCount: desc.edgeCount, capacity: wasmEdgeCapacity, keepCPUData: this._keepCPUData }); else if (desc.edgeColorsBuffer) this.setEdgeColorsBuffer(resolveGPUBuffer(desc.edgeColorsBuffer), { ownBuffer: this._ownExternalBuffers }); } applyExplicitBounds(desc) { if (desc.boundsMin && desc.boundsMax) { this.setBounds(boundsFromBox(desc.boundsMin, desc.boundsMax), "explicit"); if (desc.boundsCenter) this.boundsCenter = [desc.boundsCenter[0], desc.boundsCenter[1], desc.boundsCenter[2]]; if (desc.boundsRadius !== void 0) this.boundsRadius = Math.max(0, desc.boundsRadius); return; } if (desc.boundsCenter || desc.boundsRadius !== void 0) this.setBounds(boundsFromSphere(desc.boundsCenter ?? [0, 0, 0], desc.boundsRadius ?? 0), "explicit"); } setBounds(bounds, source) { this.boundsMin = [bounds.boxMin[0], bounds.boxMin[1], bounds.boxMin[2]]; this.boundsMax = [bounds.boxMax[0], bounds.boxMax[1], bounds.boxMax[2]]; this.boundsCenter = [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]]; this.boundsRadius = bounds.sphereRadius; this._boundsSource = source; } clearComputedBoundsIfNeeded() { if (this._boundsSource !== "computed") return; this._boundsSource = "none"; this.boundsMin = [0, 0, 0]; this.boundsMax = [0, 0, 0]; this.boundsCenter = [0, 0, 0]; this.boundsRadius = 0; } clearPendingWrites(target) { let next = 0; for (let i = 0; i < this._pendingWrites.length; i++) { const write = this._pendingWrites[i]; if (write.target === target) continue; this._pendingWrites[next++] = write; } this._pendingWrites.length = next; } replaceNodePositionsBuffer(buffer, owned) { if (this.nodePositionsBuffer !== buffer) { this.clearPendingWrites("nodePositions"); if (this.nodePositionsBuffer && this._nodePositionsOwned) this.nodePositionsBuffer.destroy(); } this.nodePositionsBuffer = buffer; this._nodePositionsOwned = !!buffer && owned; } replaceNodeScalarsBuffer(buffer, owned) { if (this.nodeScalarsBuffer !== buffer) { this.clearPendingWrites("nodeScalars"); if (this.nodeScalarsBuffer && this._nodeScalarsOwned) this.nodeScalarsBuffer.destroy(); } this.nodeScalarsBuffer = buffer; this._nodeScalarsOwned = !!buffer && owned; } replaceNodeColorsBuffer(buffer, owned) { if (this.nodeColorsBuffer !== buffer) { this.clearPendingWrites("nodeColors"); if (this.nodeColorsBuffer && this._nodeColorsOwned) this.nodeColorsBuffer.destroy(); } this.nodeColorsBuffer = buffer; this._nodeColorsOwned = !!buffer && owned; } replaceNodeRadiiBuffer(buffer, owned) { if (this.nodeRadiiBuffer !== buffer) { this.clearPendingWrites("nodeRadii"); if (this.nodeRadiiBuffer && this._nodeRadiiOwned) this.nodeRadiiBuffer.destroy(); } this.nodeRadiiBuffer = buffer; this._nodeRadiiOwned = !!buffer && owned; } replaceEdgesBuffer(buffer, owned) { if (this.edgesBuffer !== buffer) { this.clearPendingWrites("edges"); if (this.edgesBuffer && this._edgesOwned) this.edgesBuffer.destroy(); } this.edgesBuffer = buffer; this._edgesOwned = !!buffer && owned; } replaceEdgeScalarsBuffer(buffer, owned) { if (this.edgeScalarsBuffer !== buffer) { this.clearPendingWrites("edgeScalars"); if (this.edgeScalarsBuffer && this._edgeScalarsOwned) this.edgeScalarsBuffer.destroy(); } this.edgeScalarsBuffer = buffer; this._edgeScalarsOwned = !!buffer && owned; } replaceEdgeColorsBuffer(buffer, owned) { if (this.edgeColorsBuffer !== buffer) { this.clearPendingWrites("edgeColors"); if (this.edgeColorsBuffer && this._edgeColorsOwned) this.edgeColorsBuffer.destroy(); } this.edgeColorsBuffer = buffer; this._edgeColorsOwned = !!buffer && owned; } validateNodeArrayLength(length, stride, label) { assert(stride === 3 || stride === 4, `NodeLink: ${label} stride must be 3 or 4.`); assert(length % stride === 0, `NodeLink: ${label} length must be a multiple of ${stride}.`); return length / stride | 0; } packVec4FromStride(data, stride) { if (stride === 4) return new Float32Array(data); const count = data.length / 3; const out = new Float32Array(count * 4); for (let i = 0; i < count; i++) { const si = i * 3; const di = i * 4; out[di + 0] = data[si + 0]; out[di + 1] = data[si + 1]; out[di + 2] = data[si + 2]; out[di + 3] = 0; } return out; } queueWrite(target, byteOffset, data) { this._pendingWrites.push({ target, byteOffset, bytes: cloneBytes(data) }); this.bindGroupKey = null; } flushQueuedWrites(queue) { for (const write of this._pendingWrites) { const buf = write.target === "nodePositions" ? this.nodePositionsBuffer : write.target === "nodeScalars" ? this.nodeScalarsBuffer : write.target === "nodeColors" ? this.nodeColorsBuffer : write.target === "nodeRadii" ? this.nodeRadiiBuffer : write.target === "edges" ? this.edgesBuffer : write.target === "edgeScalars" ? this.edgeScalarsBuffer : this.edgeColorsBuffer; if (!buf) continue; queue.writeBuffer(buf, write.byteOffset, write.bytes.buffer, write.bytes.byteOffset, write.bytes.byteLength); } this._pendingWrites.length = 0; } validateBufferCapacity(buffer, requiredBytes, label) { if (!buffer || requiredBytes <= 0) return; const size = Number(buffer.size ?? 0); if (Number.isFinite(size) && size > 0) assert(size >= requiredBytes, `NodeLink: ${label} buffer size must be at least ${requiredBytes} bytes for the active count.`); } hasDirtyWasmSources() { return this._wasmNodePositionsDirty || this._wasmNodeScalarsDirty || this._wasmNodeColorsDirty || this._wasmNodeRadiiDirty || this._wasmEdgesDirty || this._wasmEdgeScalarsDirty || this._wasmEdgeColorsDirty; } clearWasmNodePositionsState(destroyManagedBuffer) { this._wasmNodePositionsSource = null; this._wasmNodePositionsDirty = false; this._wasmNodePositionsCapacityHint = 0; if (destroyManagedBuffer && this._nodePositionsWasmManaged) { this.replaceNodePositionsBuffer(null, false); this.bindGroupKey = null; } this._nodePositionsWasmManaged = false; this._wasmNodePositionsCapacity = 0; } clearWasmNodeScalarsState(destroyManagedBuffer) { this._wasmNodeScalarsSource = null; this._wasmNodeScalarsDirty = false; this._wasmNodeScalarsCapacityHint = 0; if (destroyManagedBuffer && this._nodeScalarsWasmManaged) { this.replaceNodeScalarsBuffer(null, false); this.bindGroupKey = null; } this._nodeScalarsWasmManaged = false; this._wasmNodeScalarsCapacity = 0; } clearWasmNodeColorsState(destroyManagedBuffer) { this._wasmNodeColorsSource = null; this._wasmNodeColorsDirty = false; this._wasmNodeColorsCapacityHint = 0; if (destroyManagedBuffer && this._nodeColorsWasmManaged) { this.replaceNodeColorsBuffer(null, false); this.bindGroupKey = null; } this._nodeColorsWasmManaged = false; this._wasmNodeColorsCapacity = 0; } clearWasmNodeRadiiState(destroyManagedBuffer) { this._wasmNodeRadiiSource = null; this._wasmNodeRadiiDirty = false; this._wasmNodeRadiiCapacityHint = 0; if (destroyManagedBuffer && this._nodeRadiiWasmManaged) { this.replaceNodeRadiiBuffer(null, false); this.bindGroupKey = null; } this._nodeRadiiWasmManaged = false; this._wasmNodeRadiiCapacity = 0; } clearWasmEdgesState(destroyManagedBuffer) { this._wasmEdgesSource = null; this._wasmEdgesDirty = false; this._wasmEdgesCapacityHint = 0; if (destroyManagedBuffer && this._edgesWasmManaged) { this.replaceEdgesBuffer(null, false); this.bindGroupKey = null; } this._edgesWasmManaged = false; this._wasmEdgesCapacity = 0; } clearWasmEdgeScalarsState(destroyManagedBuffer) { this._wasmEdgeScalarsSource = null; this._wasmEdgeScalarsDirty = false; this._wasmEdgeScalarsCapacityHint = 0; if (destroyManagedBuffer && this._edgeScalarsWasmManaged) { this.replaceEdgeScalarsBuffer(null, false); this.bindGroupKey = null; } this._edgeScalarsWasmManaged = false; this._wasmEdgeScalarsCapacity = 0; } clearWasmEdgeColorsState(destroyManagedBuffer) { this._wasmEdgeColorsSource = null; this._wasmEdgeColorsDirty = false; this._wasmEdgeColorsCapacityHint = 0; if (destroyManagedBuffer && this._edgeColorsWasmManaged) { this.replaceEdgeColorsBuffer(null, false); this.bindGroupKey = null; } this._edgeColorsWasmManaged = false; this._wasmEdgeColorsCapacity = 0; } clearAllWasmState(destroyManagedBuffers) { this.clearWasmNodePositionsState(destroyManagedBuffers); this.clearWasmNodeScalarsState(destroyManagedBuffers); this.clearWasmNodeColorsState(destroyManagedBuffers); this.clearWasmNodeRadiiState(destroyManagedBuffers); this.clearWasmEdgesState(destroyManagedBuffers); this.clearWasmEdgeScalarsState(destroyManagedBuffers); this.clearWasmEdgeColorsState(destroyManagedBuffers); } primaryWasmNodeChannel() { if (this._wasmNodePositionsSource) return "nodePositions"; if (this._wasmNodeScalarsSource) return "nodeScalars"; if (this._wasmNodeColorsSource) return "nodeColors"; if (this._wasmNodeRadiiSource) return "nodeRadii"; return null; } primaryWasmEdgeChannel() { if (this._wasmEdgesSource) return "edges"; if (this._wasmEdgeScalarsSource) return "edgeScalars"; if (this._wasmEdgeColorsSource) return "edgeColors"; return null; } validateNonWasmNodeChannelsForCount(nodeCount) { const count = assertWasmRecordCount(nodeCount, "NodeLink: nodeCount"); if (!this._wasmNodePositionsSource && this._nodePositionsCPU) assert(this._nodePositionsCPU.length / 4 === count, "NodeLink: nodePositions length must equal nodeCount*4."); if (!this._wasmNodeScalarsSource && this._nodeScalarsCPU) assert(this._nodeScalarsCPU.length === count, "NodeLink: nodeScalars length must equal nodeCount."); if (!this._wasmNodeColorsSource && this._nodeColorsCPU) assert(this._nodeColorsCPU.length / 4 === count, "NodeLink: nodeColors length must equal nodeCount*4."); if (!this._wasmNodeRadiiSource && this._nodeRadiiCPU) assert(this._nodeRadiiCPU.length / 4 === count, "NodeLink: nodeRadii length must equal nodeCount*4."); if (!this._wasmNodePositionsSource) this.validateBufferCapacity(this.nodePositionsBuffer, count * NODELINK_VEC4_BYTES, "nodePositions"); if (!this._wasmNodeScalarsSource) this.validateBufferCapacity(this.nodeScalarsBuffer, count * NODELINK_F32_BYTES, "nodeScalars"); if (!this._wasmNodeColorsSource) this.validateBufferCapacity(this.nodeColorsBuffer, count * NODELINK_VEC4_BYTES, "nodeColors"); if (!this._wasmNodeRadiiSource) this.validateBufferCapacity(this.nodeRadiiBuffer, count * NODELINK_VEC4_BYTES, "nodeRadii"); } validateNonWasmEdgeChannelsForCount(edgeCount) { const count = assertWasmRecordCount(edgeCount, "NodeLink: edgeCount"); if (!this._wasmEdgesSource && this._edgesCPU) assert(this._edgesCPU.length / 2 === count, "NodeLink: edges length must equal edgeCount*2."); if (!this._wasmEdgeScalarsSource && this._edgeScalarsCPU) assert(this._edgeScalarsCPU.length === count, "NodeLink: edgeScalars length must equal edgeCount."); if (!this._wasmEdgeColorsSource && this._edgeColorsCPU) assert(this._edgeColorsCPU.length / 4 === count, "NodeLink: edgeColors length must equal edgeCount*4."); if (!this._wasmEdgesSource) this.validateBufferCapacity(this.edgesBuffer, count * NODELINK_EDGE_BYTES, "edges"); if (!this._wasmEdgeScalarsSource) this.validateBufferCapacity(this.edgeScalarsBuffer, count * NODELINK_F32_BYTES, "edgeScalars"); if (!this._wasmEdgeColorsSource) this.validateBufferCapacity(this.edgeColorsBuffer, count * NODELINK_VEC4_BYTES, "edgeColors"); } setNodeCountFromWasm(nodeCount) { const count = assertWasmRecordCount(nodeCount, "NodeLink: nodeCount"); const changed = count !== this._nodeCount; if (changed) this.validateNonWasmNodeChannelsForCount(count); this._nodeCount = count; if (!changed) return; if (this._wasmNodePositionsSource) { this._wasmNodePositionsDirty = true; this._nodePositionsDirty = true; } if (this._wasmNodeScalarsSource) { this._wasmNodeScalarsDirty = true; this._nodeScalarsDirty = true; } if (this._wasmNodeColorsSource) { this._wasmNodeColorsDirty = true; this._nodeColorsDirty = true; } if (this._wasmNodeRadiiSource) { this._wasmNodeRadiiDirty = true; this._nodeRadiiDirty = true; } } setEdgeCountFromWasm(edgeCount) { const count = assertWasmRecordCount(edgeCount, "NodeLink: edgeCount"); const changed = count !== this._edgeCount; if (changed) this.validateNonWasmEdgeChannelsForCount(count); this._edgeCount = count; if (!changed) return; if (this._wasmEdgesSource) { this._wasmEdgesDirty = true; this._edgesDirty = true; } if (this._wasmEdgeScalarsSource) { this._wasmEdgeScalarsDirty = true; this._edgeScalarsDirty = true; } if (this._wasmEdgeColorsSource) { this._wasmEdgeColorsDirty = true; this._edgeColorsDirty = true; } } resolveWasmNodeCount(channel, source, explicitNodeCount) { const field = wasmNodeFieldName(channel); const primary = this.primaryWasmNodeChannel(); const components = nodeWasmComponents(channel); if (explicitNodeCount !== void 0) { const count = assertWasmRecordCount(explicitNodeCount, "NodeLink: nodeCount"); assert(!primary || channel === primary || count === this._nodeCount, `NodeLink: refreshWasm${field.slice(4)} nodeCount must match the current nodeCount when another node wasm source is active; call refreshFromWasm() to update node count.`); validateWasmRecordRange(source, count, components, `NodeLink: ${field}`, "nodeCount"); return count; } if (channel === primary) return resolveWasmRecordCount(source, void 0, components, `NodeLink: ${field}`, "NodeLink: nodeCount", "nodeCount"); assert(this._nodeCount > 0 || source.length === 0, `NodeLink: nodeCount is required when using ${field} without a node wasm source.`); validateWasmRecordRange(source, this._nodeCount, components, `NodeLink: ${field}`, "nodeCount"); return this._nodeCount; } resolveWasmEdgeCount(channel, source, explicitEdgeCount) { const field = wasmEdgeFieldName(channel); const primary = this.primaryWasmEdgeChannel(); const components = edgeWasmComponents(channel); if (explicitEdgeCount !== void 0) { const count = assertWasmRecordCount(explicitEdgeCount, "NodeLink: edgeCount"); assert(!primary || channel === primary || count === this._edgeCount, `NodeLink: refreshWasm${field.slice(4)} edgeCount must match the current edgeCount when another edge wasm source is active; call refreshFromWasm() to update edge count.`); validateWasmRecordRange(source, count, components, `NodeLink: ${field}`, "edgeCount"); return count; } if (channel === primary) return resolveWasmRecordCount(source, void 0, components, `NodeLink: ${field}`, "NodeLink: edgeCount", "edgeCount"); assert(this._edgeCount > 0 || source.length === 0, `NodeLink: edgeCount is required when using ${field} without an edge wasm source.`); validateWasmRecordRange(source, this._edgeCount, components, `NodeLink: ${field}`, "edgeCount"); return this._edgeCount; } setWasmNodeChannelSource(channel, source, capacity) { if (source === null) { if (channel === "nodePositions") this.clearWasmNodePositionsState(true); else if (channel === "nodeScalars") this.clearWasmNodeScalarsState(true); else if (channel === "nodeColors") this.clearWasmNodeColorsState(true); else this.clearWasmNodeRadiiState(true); return false; } const field = wasmNodeFieldName(channel); const wasmSource = assertWasmF32View(source, `NodeLink: ${field}`); const capacityHint = assertWasmCapacity(capacity, `NodeLink: ${field} capacity`); if (channel === "nodePositions") { this._wasmNodePositionsCapacityHint = capacityHint; if (!this._nodePositionsWasmManaged) { this.replaceNodePositionsBuffer(null, false); this._wasmNodePositionsCapacity = 0; this.bindGroupKey = null; } this._wasmNodePositionsSource = wasmSource; this._nodePositionsCPU = null; this._nodePositionsExternal = false; } else if (channel === "nodeScalars") { this._wasmNodeScalarsCapacityHint = capacityHint; if (!this._nodeScalarsWasmManaged) { this.replaceNodeScalarsBuffer(null, false); this._wasmNodeScalarsCapacity = 0; this.bindGroupKey = null; } this._wasmNodeScalarsSource = wasmSource; this._nodeScalarsCPU = null; this._nodeScalarsExternal = false; } else if (channel === "nodeColors") { this._wasmNodeColorsCapacityHint = capacityHint; if (!this._nodeColorsWasmManaged) { this.replaceNodeColorsBuffer(null, false); this._wasmNodeColorsCapacity = 0; this.bindGroupKey = null; } this._wasmNodeColorsSource = wasmSource; this._nodeColorsCPU = null; this._nodeColorsExternal = false; } else { this._wasmNodeRadiiCapacityHint = capacityHint; if (!this._nodeRadiiWasmManaged) { this.replaceNodeRadiiBuffer(null, false); this._wasmNodeRadiiCapacity = 0; this.bindGroupKey = null; } this._wasmNodeRadiiSource = wasmSource; this._nodeRadiiCPU = null; this._nodeRadiiExternal = false; } this.clearPendingWrites(channel); return true; } setWasmEdgeChannelSource(channel, source, capacity) { if (source === null) { if (channel === "edges") this.clearWasmEdgesState(true); else if (channel === "edgeScalars") this.clearWasmEdgeScalarsState(true); else this.clearWasmEdgeColorsState(true); return false; } const field = wasmEdgeFieldName(channel); const capacityHint = assertWasmCapacity(capacity, `NodeLink: ${field} capacity`); if (channel === "edges") { const wasmSource = assertWasmU32View(source, "NodeLink: wasmEdges"); this._wasmEdgesCapacityHint = capacityHint; if (!this._edgesWasmManaged) { this.replaceEdgesBuffer(null, false); this._wasmEdgesCapacity = 0; this.bindGroupKey = null; } this._wasmEdgesSource = wasmSource; this._edgesCPU = null; this._edgesExternal = false; } else if (channel === "edgeScalars") { const wasmSource = assertWasmF32View(source, "NodeLink: wasmEdgeScalars"); this._wasmEdgeScalarsCapacityHint = capacityHint; if (!this._edgeScalarsWasmManaged) { this.replaceEdgeScalarsBuffer(null, false); this._wasmEdgeScalarsCapacity = 0; this.bindGroupKey = null; } this._wasmEdgeScalarsSource = wasmSource; this._edgeScalarsCPU = null; this._edgeScalarsExternal = false; } else { const wasmSource = assertWasmF32View(source, "NodeLink: wasmEdgeColors"); this._wasmEdgeColorsCapacityHint = capacityHint; if (!this._edgeColorsWasmManaged) { this.replaceEdgeColorsBuffer(null, false); this._wasmEdgeColorsCapacity = 0; this.bindGroupKey = null; } this._wasmEdgeColorsSource = wasmSource; this._edgeColorsCPU = null; this._edgeColorsExternal = false; } this.clearPendingWrites(channel); return true; } copyWasmF32Range(source, elementCount) { const view = source.array(); return new Float32Array(view.subarray(0, elementCount)); } copyWasmU32Range(source, elementCount) { const view = source.array(); return new Uint32Array(view.subarray(0, elementCount)); } computeBoundsFromPackedPositions(data, nodeCount) { if (nodeCount <= 0) return; let minX = data[0], minY = data[1], minZ = data[2]; let maxX = data[0], maxY = data[1], maxZ = data[2]; for (let i = 1; i < nodeCount; i++) { const o = i * 4; const x = data[o + 0], y = data[o + 1], z = data[o + 2]; if (x < minX) minX = x; if (y < minY) minY = y; if (z < minZ) minZ = z; if (x > maxX) maxX = x; if (y > maxY) maxY = y; if (z > maxZ) maxZ = z; } const cx = (minX + maxX) * 0.5; const cy = (minY + maxY) * 0.5; const cz = (minZ + maxZ) * 0.5; let radius = 0; for (let i = 0; i < nodeCount; i++) { const o = i * 4; const dx = data[o + 0] - cx, dy = data[o + 1] - cy, dz = data[o + 2] - cz; const d = Math.sqrt(dx * dx + dy * dy + dz * dz); if (d > radius) radius = d; } this.setBounds(boundsFromBox([minX, minY, minZ], [maxX, maxY, maxZ]), "computed"); this.boundsCenter = [cx, cy, cz]; this.boundsRadius = radius; } updateWasmNodeBounds(options, source, nodeCount) { if (options.recomputeBounds && this._boundsSource !== "explicit") { this.computeBoundsFromPackedPositions(source.array(), nodeCount); return; } this.clearComputedBoundsIfNeeded(); } ensureWasmNodePositionsBuffer(device, nodeCount) { const required = Math.max(nodeCount, this._wasmNodePositionsCapacityHint); if (required <= 0) return; if (this.nodePositionsBuffer && this._nodePositionsWasmManaged && this._wasmNodePositionsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmNodePositionsCapacity); this.replaceNodePositionsBuffer(device.createBuffer({ label: "NodeLink.wasmNodePositions", size: capacity * NODELINK_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._nodePositionsWasmManaged = true; this._wasmNodePositionsCapacity = capacity; this.bindGroupKey = null; } ensureWasmNodeScalarsBuffer(device, nodeCount) { const required = Math.max(nodeCount, this._wasmNodeScalarsCapacityHint); if (required <= 0) return; if (this.nodeScalarsBuffer && this._nodeScalarsWasmManaged && this._wasmNodeScalarsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmNodeScalarsCapacity); this.replaceNodeScalarsBuffer(device.createBuffer({ label: "NodeLink.wasmNodeScalars", size: capacity * NODELINK_F32_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._nodeScalarsWasmManaged = true; this._wasmNodeScalarsCapacity = capacity; this.bindGroupKey = null; } ensureWasmNodeColorsBuffer(device, nodeCount) { const required = Math.max(nodeCount, this._wasmNodeColorsCapacityHint); if (required <= 0) return; if (this.nodeColorsBuffer && this._nodeColorsWasmManaged && this._wasmNodeColorsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmNodeColorsCapacity); this.replaceNodeColorsBuffer(device.createBuffer({ label: "NodeLink.wasmNodeColors", size: capacity * NODELINK_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._nodeColorsWasmManaged = true; this._wasmNodeColorsCapacity = capacity; this.bindGroupKey = null; } ensureWasmNodeRadiiBuffer(device, nodeCount) { const required = Math.max(nodeCount, this._wasmNodeRadiiCapacityHint); if (required <= 0) return; if (this.nodeRadiiBuffer && this._nodeRadiiWasmManaged && this._wasmNodeRadiiCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmNodeRadiiCapacity); this.replaceNodeRadiiBuffer(device.createBuffer({ label: "NodeLink.wasmNodeRadii", size: capacity * NODELINK_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._nodeRadiiWasmManaged = true; this._wasmNodeRadiiCapacity = capacity; this.bindGroupKey = null; } ensureWasmEdgesBuffer(device, edgeCount) { const required = Math.max(edgeCount, this._wasmEdgesCapacityHint); if (required <= 0) return; if (this.edgesBuffer && this._edgesWasmManaged && this._wasmEdgesCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmEdgesCapacity); this.replaceEdgesBuffer(device.createBuffer({ label: "NodeLink.wasmEdges", size: capacity * NODELINK_EDGE_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._edgesWasmManaged = true; this._wasmEdgesCapacity = capacity; this.bindGroupKey = null; } ensureWasmEdgeScalarsBuffer(device, edgeCount) { const required = Math.max(edgeCount, this._wasmEdgeScalarsCapacityHint); if (required <= 0) return; if (this.edgeScalarsBuffer && this._edgeScalarsWasmManaged && this._wasmEdgeScalarsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmEdgeScalarsCapacity); this.replaceEdgeScalarsBuffer(device.createBuffer({ label: "NodeLink.wasmEdgeScalars", size: capacity * NODELINK_F32_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._edgeScalarsWasmManaged = true; this._wasmEdgeScalarsCapacity = capacity; this.bindGroupKey = null; } ensureWasmEdgeColorsBuffer(device, edgeCount) { const required = Math.max(edgeCount, this._wasmEdgeColorsCapacityHint); if (required <= 0) return; if (this.edgeColorsBuffer && this._edgeColorsWasmManaged && this._wasmEdgeColorsCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmEdgeColorsCapacity); this.replaceEdgeColorsBuffer(device.createBuffer({ label: "NodeLink.wasmEdgeColors", size: capacity * NODELINK_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._edgeColorsWasmManaged = true; this._wasmEdgeColorsCapacity = capacity; this.bindGroupKey = null; } get nodeCount() { return this._nodeCount; } get edgeCount() { return this._edgeCount; } get occluderRevision() { let hash = 2166136261 >>> 0; hash = mixNodeLinkRevision(hash, this._nodeCount >>> 0); hash = mixNodeLinkRevision(hash, this._edgeCount >>> 0); hash = mixNodeLinkRevision(hash, this._nodeScaleRevision >>> 0); hash = mixNodeLinkRevision(hash, this._edgeScaleRevision >>> 0); hash = mixNodeLinkRevision(hash, this.blendMode === "opaque" /* Opaque */ ? 1 : this.blendMode === "transparent" /* Transparent */ ? 2 : 3); hash = mixNodeLinkRevision(hash, this.cullMode === "back" /* Back */ ? 1 : this.cullMode === "front" /* Front */ ? 2 : 3); hash = mixNodeLinkRevision(hash, this.depthWrite ? 1 : 0); hash = mixNodeLinkRevision(hash, this.depthTest ? 1 : 0); hash = mixNodeLinkRevision(hash, nodeGeometryModeId(this._nodeGeometryMode) >>> 0); hash = mixNodeLinkRevision(hash, edgeGeometryModeId(this._edgeGeometryMode) >>> 0); hash = mixNodeLinkRevision(hash, colorModeId3(this._nodeColorMode) >>> 0); hash = mixNodeLinkRevision(hash, colorModeId3(this._edgeColorMode) >>> 0); hash = mixNodeLinkRevision(hash, this._nodePositionsDirty ? 1 : 0); hash = mixNodeLinkRevision(hash, this._nodeRadiiDirty ? 1 : 0); hash = mixNodeLinkRevision(hash, this._edgesDirty ? 1 : 0); hash = mixNodeLinkRevisionF32(hash, this._nodeSize); hash = mixNodeLinkRevisionF32(hash, this._edgeSize); hash = mixNodeLinkRevisionF32(hash, this._minPointSize); hash = mixNodeLinkRevisionF32(hash, this._maxPointSize); hash = mixNodeLinkRevisionF32(hash, this._pointSizeAttenuation); return hash >>> 0; } get ndShape() { return this._ndShape ? this._ndShape.slice() : null; } set ndShape(shape) { this._ndShape = normalizePositiveIntShape(shape, "NodeLink: ndShape"); } get nodeGeometryMode() { return this._nodeGeometryMode; } set nodeGeometryMode(v) { assert(isNodeGeometryMode(v), `NodeLink: invalid nodeGeometryMode '${String(v)}'.`); if (v !== this._nodeGeometryMode) { this._nodeGeometryMode = v; this._uniformDirty = true; this.emitVisualChange("visual"); } } get edgeGeometryMode() { return this._edgeGeometryMode; } set edgeGeometryMode(v) { assert(isEdgeGeometryMode(v), `NodeLink: invalid edgeGeometryMode '${String(v)}'.`); if (v !== this._edgeGeometryMode) { this._edgeGeometryMode = v; this._uniformDirty = true; this.emitVisualChange("visual"); } } get nodeColorMode() { return this._nodeColorMode; } set nodeColorMode(v) { assert(isColorMode(v), `NodeLink: invalid nodeColorMode '${String(v)}'.`); if (v !== this._nodeColorMode) { this._nodeColorMode = v; this._uniformDirty = true; this.emitVisualChange("visual"); } } get edgeColorMode() { return this._edgeColorMode; } set edgeColorMode(v) { assert(isColorMode(v), `NodeLink: invalid edgeColorMode '${String(v)}'.`); if (v !== this._edgeColorMode) { this._edgeColorMode = v; this._uniformDirty = true; this.emitVisualChange("visual"); } } get nodeScaleTransform() { return cloneScaleTransform(this._nodeScaleTransform); } setNodeScaleTransform(t) { this._nodeScaleTransform = normalizeNodeScaleTransform(t); this._uniformDirty = true; this._nodeScaleRevision++; this.emitVisualChange("scale"); } get edgeScaleTransform() { return cloneScaleTransform(this._edgeScaleTransform); } setEdgeScaleTransform(t) { this._edgeScaleTransform = normalizeEdgeScaleTransform(t); this._uniformDirty = true; this._edgeScaleRevision++; this.emitVisualChange("scale"); } applyNodeScaleStats(stats) { const n = cloneScaleTransform(this._nodeScaleTransform); if (Number.isFinite(stats.min)) n.domainMin = stats.min; if (Number.isFinite(stats.max)) n.domainMax = stats.max; if (stats.percentileMin !== null && stats.percentileMax !== null) { n.clampMin = stats.percentileMin; n.clampMax = stats.percentileMax; } this._nodeScaleTransform = normalizeNodeScaleTransform(n); this._uniformDirty = true; this._nodeScaleRevision++; this.emitVisualChange("scale"); } applyEdgeScaleStats(stats) { const n = cloneScaleTransform(this._edgeScaleTransform); if (Number.isFinite(stats.min)) n.domainMin = stats.min; if (Number.isFinite(stats.max)) n.domainMax = stats.max; if (stats.percentileMin !== null && stats.percentileMax !== null) { n.clampMin = stats.percentileMin; n.clampMax = stats.percentileMax; } this._edgeScaleTransform = normalizeEdgeScaleTransform(n); this._uniformDirty = true; this._edgeScaleRevision++; this.emitVisualChange("scale"); } onVisualChange(listener) { this._visualChangeListeners.add(listener); return () => this._visualChangeListeners.delete(listener); } getNodeScaleSourceDescriptor(revision = this._nodeScaleRevision) { if (!this.nodeScalarsBuffer || this._nodeCount <= 0) return null; return { buffer: this.nodeScalarsBuffer, count: this._nodeCount, componentCount: this._nodeScaleTransform.componentCount, componentIndex: this._nodeScaleTransform.componentIndex, valueMode: this._nodeScaleTransform.valueMode, stride: this._nodeScaleTransform.stride, offset: this._nodeScaleTransform.offset, revision }; } getEdgeScaleSourceDescriptor(revision = this._edgeScaleRevision) { if (!this.edgeScalarsBuffer || this._edgeCount <= 0) return null; return { buffer: this.edgeScalarsBuffer, count: this._edgeCount, componentCount: this._edgeScaleTransform.componentCount, componentIndex: this._edgeScaleTransform.componentIndex, valueMode: this._edgeScaleTransform.valueMode, stride: this._edgeScaleTransform.stride, offset: this._edgeScaleTransform.offset, revision }; } get nodeColormap() { return this._nodeColormap; } set nodeColormap(v) { this._nodeColormap = v; this._uniformDirty = true; this.bindGroupKey = null; this.emitVisualChange("colormap"); } get edgeColormap() { return this._edgeColormap; } set edgeColormap(v) { this._edgeColormap = v; this._uniformDirty = true; this.bindGroupKey = null; this.emitVisualChange("colormap"); } get nodeColormapStops() { return this._nodeColormapStops; } set nodeColormapStops(v) { this._nodeColormapStops = normalizeColorStops(v); this._uniformDirty = true; this.emitVisualChange("colormap"); } get edgeColormapStops() { return this._edgeColormapStops; } set edgeColormapStops(v) { this._edgeColormapStops = normalizeColorStops(v); this._uniformDirty = true; this.emitVisualChange("colormap"); } get nodeSolidColor() { return [this._nodeSolidColor[0], this._nodeSolidColor[1], this._nodeSolidColor[2], this._nodeSolidColor[3]]; } set nodeSolidColor(v) { this._nodeSolidColor = [v[0], v[1], v[2], v[3]]; this._uniformDirty = true; this.emitVisualChange("visual"); } get edgeSolidColor() { return [this._edgeSolidColor[0], this._edgeSolidColor[1], this._edgeSolidColor[2], this._edgeSolidColor[3]]; } set edgeSolidColor(v) { this._edgeSolidColor = [v[0], v[1], v[2], v[3]]; this._uniformDirty = true; this.emitVisualChange("visual"); } get nodeSize() { return this._nodeSize; } set nodeSize(v) { if (v !== this._nodeSize) { this._nodeSize = Math.max(0, v); this._uniformDirty = true; this.emitVisualChange("visual"); } } get edgeSize() { return this._edgeSize; } set edgeSize(v) { if (v !== this._edgeSize) { this._edgeSize = Math.max(0, v); this._uniformDirty = true; this.emitVisualChange("visual"); } } get opacity() { return this._opacity; } set opacity(v) { if (v !== this._opacity) { this._opacity = clamp01(v); this._uniformDirty = true; this.emitVisualChange("visual"); } } get lit() { return this._lit; } set lit(v) { const b = !!v; if (b !== this._lit) { this._lit = b; this._uniformDirty = true; this.emitVisualChange("visual"); } } get minPointSize() { return this._minPointSize; } set minPointSize(v) { if (v !== this._minPointSize) { this._minPointSize = Math.max(0, v); if (this._maxPointSize < this._minPointSize) { this._maxPointSize = this._minPointSize; } this._uniformDirty = true; this.emitVisualChange("visual"); } } get maxPointSize() { return this._maxPointSize; } set maxPointSize(v) { if (v !== this._maxPointSize) { this._maxPointSize = Math.max(this._minPointSize, v); this._uniformDirty = true; this.emitVisualChange("visual"); } } get pointSizeAttenuation() { return this._pointSizeAttenuation; } set pointSizeAttenuation(v) { if (v !== this._pointSizeAttenuation) { this._pointSizeAttenuation = Math.max(0, v); this._uniformDirty = true; this.emitVisualChange("visual"); } } setNodePositions(data, opts = {}) { const stride = opts.stride ?? 3; const count = this.validateNodeArrayLength(data.length, stride, "nodePositions"); this.clearWasmNodePositionsState(true); this.replaceNodePositionsBuffer(null, false); this._nodeCount = count; this._nodePositionsCPU = this.packVec4FromStride(data, stride); this._nodePositionsExternal = false; this._nodePositionsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.clearComputedBoundsIfNeeded(); this.bindGroupKey = null; } updateNodePositions(data, startNode = 0, stride = 3) { const patchCount = this.validateNodeArrayLength(data.length, stride, "nodePositions patch"); const start = startNode | 0; assert(start >= 0, "NodeLink: updateNodePositions startNode must be >= 0."); assert(start + patchCount <= this._nodeCount, "NodeLink: updateNodePositions range exceeds nodeCount."); const packed = this.packVec4FromStride(data, stride); if (this._nodePositionsCPU) this._nodePositionsCPU.set(packed, start * 4); if (this.nodePositionsBuffer) this.queueWrite("nodePositions", start * 16, packed); else this._nodePositionsDirty = true; this.clearComputedBoundsIfNeeded(); } setNodePositionsBuffer(buffer, nodeCount, opts = {}) { assert(!!buffer, "NodeLink: nodePositionsBuffer is required."); assert(Number.isInteger(nodeCount) && nodeCount >= 0, "NodeLink: nodeCount must be an integer >= 0."); this.clearWasmNodePositionsState(true); this.replaceNodePositionsBuffer(buffer, !!opts.ownBuffer); this._nodeCount = nodeCount | 0; this._nodePositionsCPU = null; this._nodePositionsExternal = true; this._nodePositionsDirty = false; this.bindGroupKey = null; } setNodeScalars(data, opts = {}) { assert(data.length === this._nodeCount, "NodeLink: nodeScalars length must equal nodeCount."); this.clearWasmNodeScalarsState(true); this.replaceNodeScalarsBuffer(null, false); this._nodeScalarsCPU = new Float32Array(data); this._nodeScalarsExternal = false; this._nodeScalarsDirty = true; this._nodeScaleRevision++; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateNodeScalars(data, startNode = 0) { const start = startNode | 0; assert(start >= 0, "NodeLink: updateNodeScalars startNode must be >= 0."); assert(start + data.length <= this._nodeCount, "NodeLink: updateNodeScalars range exceeds nodeCount."); if (this._nodeScalarsCPU) this._nodeScalarsCPU.set(data, start); if (this.nodeScalarsBuffer) this.queueWrite("nodeScalars", start * 4, data); else this._nodeScalarsDirty = true; this._nodeScaleRevision++; } setNodeScalarsBuffer(buffer, opts = {}) { this.clearWasmNodeScalarsState(true); this.replaceNodeScalarsBuffer(buffer, !!buffer && !!opts.ownBuffer); this._nodeScalarsCPU = null; this._nodeScalarsExternal = !!buffer; this._nodeScalarsDirty = false; this._nodeScaleRevision++; this.bindGroupKey = null; } setNodeColors(data, opts = {}) { assert(data.length % 4 === 0, "NodeLink: nodeColors length must be a multiple of 4."); assert(data.length / 4 === this._nodeCount, "NodeLink: nodeColors length must equal nodeCount*4."); this.clearWasmNodeColorsState(true); this.replaceNodeColorsBuffer(null, false); this._nodeColorsCPU = new Float32Array(data); this._nodeColorsExternal = false; this._nodeColorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateNodeColors(data, startNode = 0) { assert(data.length % 4 === 0, "NodeLink: updateNodeColors length must be a multiple of 4."); const start = startNode | 0; const patchCount = data.length / 4; assert(start >= 0, "NodeLink: updateNodeColors startNode must be >= 0."); assert(start + patchCount <= this._nodeCount, "NodeLink: updateNodeColors range exceeds nodeCount."); if (this._nodeColorsCPU) this._nodeColorsCPU.set(data, start * 4); if (this.nodeColorsBuffer) this.queueWrite("nodeColors", start * 16, data); else this._nodeColorsDirty = true; } setNodeColorsBuffer(buffer, opts = {}) { this.clearWasmNodeColorsState(true); this.replaceNodeColorsBuffer(buffer, !!buffer && !!opts.ownBuffer); this._nodeColorsCPU = null; this._nodeColorsExternal = !!buffer; this._nodeColorsDirty = false; this.bindGroupKey = null; } setNodeRadii(data, opts = {}) { const stride = opts.stride ?? 3; const count = this.validateNodeArrayLength(data.length, stride, "nodeRadii"); assert(count === this._nodeCount, "NodeLink: nodeRadii count must equal nodeCount."); this.clearWasmNodeRadiiState(true); this.replaceNodeRadiiBuffer(null, false); this._nodeRadiiCPU = this.packVec4FromStride(data, stride); this._nodeRadiiExternal = false; this._nodeRadiiDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateNodeRadii(data, startNode = 0, stride = 3) { const patchCount = this.validateNodeArrayLength(data.length, stride, "nodeRadii patch"); const start = startNode | 0; assert(start >= 0, "NodeLink: updateNodeRadii startNode must be >= 0."); assert(start + patchCount <= this._nodeCount, "NodeLink: updateNodeRadii range exceeds nodeCount."); const packed = this.packVec4FromStride(data, stride); if (this._nodeRadiiCPU) this._nodeRadiiCPU.set(packed, start * 4); if (this.nodeRadiiBuffer) this.queueWrite("nodeRadii", start * 16, packed); else this._nodeRadiiDirty = true; } setNodeRadiiBuffer(buffer, opts = {}) { this.clearWasmNodeRadiiState(true); this.replaceNodeRadiiBuffer(buffer, !!buffer && !!opts.ownBuffer); this._nodeRadiiCPU = null; this._nodeRadiiExternal = !!buffer; this._nodeRadiiDirty = false; this.bindGroupKey = null; } setEdges(data, opts = {}) { assert(data instanceof Uint16Array || data instanceof Uint32Array, "NodeLink: edges must be a Uint16Array or Uint32Array."); assert(data.length % 2 === 0, "NodeLink: edges length must be a multiple of 2."); const u32 = data instanceof Uint32Array ? data : new Uint32Array(data); for (let i = 0; i < u32.length; i++) assert(u32[i] < this._nodeCount, `NodeLink: edge index ${u32[i]} is out of range.`); this.clearWasmEdgesState(true); this.replaceEdgesBuffer(null, false); this._edgeCount = u32.length / 2 | 0; this._edgesCPU = new Uint32Array(u32); this._edgesExternal = false; this._edgesDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateEdges(data, startEdge = 0) { assert(data instanceof Uint16Array || data instanceof Uint32Array, "NodeLink: updateEdges data must be a Uint16Array or Uint32Array."); assert(data.length % 2 === 0, "NodeLink: updateEdges length must be a multiple of 2."); const u32 = data instanceof Uint32Array ? data : new Uint32Array(data); const start = startEdge | 0; const patchCount = u32.length / 2 | 0; assert(start >= 0, "NodeLink: updateEdges startEdge must be >= 0."); assert(start + patchCount <= this._edgeCount, "NodeLink: updateEdges range exceeds edgeCount."); for (let i = 0; i < u32.length; i++) assert(u32[i] < this._nodeCount, `NodeLink: edge index ${u32[i]} is out of range.`); if (this._edgesCPU) this._edgesCPU.set(u32, start * 2); if (this.edgesBuffer) this.queueWrite("edges", start * 8, u32); else this._edgesDirty = true; } setEdgesBuffer(buffer, edgeCount, opts = {}) { assert(!!buffer, "NodeLink: edgesBuffer is required."); assert(Number.isInteger(edgeCount) && edgeCount >= 0, "NodeLink: edgeCount must be an integer >= 0."); this.clearWasmEdgesState(true); this.replaceEdgesBuffer(buffer, !!opts.ownBuffer); this._edgeCount = edgeCount | 0; this._edgesCPU = null; this._edgesExternal = true; this._edgesDirty = false; this.bindGroupKey = null; } setEdgeScalars(data, opts = {}) { assert(data.length === this._edgeCount, "NodeLink: edgeScalars length must equal edgeCount."); this.clearWasmEdgeScalarsState(true); this.replaceEdgeScalarsBuffer(null, false); this._edgeScalarsCPU = new Float32Array(data); this._edgeScalarsExternal = false; this._edgeScalarsDirty = true; this._edgeScaleRevision++; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateEdgeScalars(data, startEdge = 0) { const start = startEdge | 0; assert(start >= 0, "NodeLink: updateEdgeScalars startEdge must be >= 0."); assert(start + data.length <= this._edgeCount, "NodeLink: updateEdgeScalars range exceeds edgeCount."); if (this._edgeScalarsCPU) this._edgeScalarsCPU.set(data, start); if (this.edgeScalarsBuffer) this.queueWrite("edgeScalars", start * 4, data); else this._edgeScalarsDirty = true; this._edgeScaleRevision++; } setEdgeScalarsBuffer(buffer, opts = {}) { this.clearWasmEdgeScalarsState(true); this.replaceEdgeScalarsBuffer(buffer, !!buffer && !!opts.ownBuffer); this._edgeScalarsCPU = null; this._edgeScalarsExternal = !!buffer; this._edgeScalarsDirty = false; this._edgeScaleRevision++; this.bindGroupKey = null; } setEdgeColors(data, opts = {}) { assert(data.length % 4 === 0, "NodeLink: edgeColors length must be a multiple of 4."); assert(data.length / 4 === this._edgeCount, "NodeLink: edgeColors length must equal edgeCount*4."); this.clearWasmEdgeColorsState(true); this.replaceEdgeColorsBuffer(null, false); this._edgeColorsCPU = new Float32Array(data); this._edgeColorsExternal = false; this._edgeColorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } updateEdgeColors(data, startEdge = 0) { assert(data.length % 4 === 0, "NodeLink: updateEdgeColors length must be a multiple of 4."); const start = startEdge | 0; const patchCount = data.length / 4; assert(start >= 0, "NodeLink: updateEdgeColors startEdge must be >= 0."); assert(start + patchCount <= this._edgeCount, "NodeLink: updateEdgeColors range exceeds edgeCount."); if (this._edgeColorsCPU) this._edgeColorsCPU.set(data, start * 4); if (this.edgeColorsBuffer) this.queueWrite("edgeColors", start * 16, data); else this._edgeColorsDirty = true; } setEdgeColorsBuffer(buffer, opts = {}) { this.clearWasmEdgeColorsState(true); this.replaceEdgeColorsBuffer(buffer, !!buffer && !!opts.ownBuffer); this._edgeColorsCPU = null; this._edgeColorsExternal = !!buffer; this._edgeColorsDirty = false; this.bindGroupKey = null; } setWasmNodePositions(source, options = {}) { if (!this.setWasmNodeChannelSource("nodePositions", source, options.capacity)) return; this.refreshWasmNodePositions(options); } setWasmNodeScalars(source, options = {}) { if (!this.setWasmNodeChannelSource("nodeScalars", source, options.capacity)) return; this.refreshWasmNodeScalars(options); } setWasmNodeColors(source, options = {}) { if (!this.setWasmNodeChannelSource("nodeColors", source, options.capacity)) return; this.refreshWasmNodeColors(options); } setWasmNodeRadii(source, options = {}) { if (!this.setWasmNodeChannelSource("nodeRadii", source, options.capacity)) return; this.refreshWasmNodeRadii(options); } setWasmEdges(source, options = {}) { if (!this.setWasmEdgeChannelSource("edges", source, options.capacity)) return; this.refreshWasmEdges(options); } setWasmEdgeScalars(source, options = {}) { if (!this.setWasmEdgeChannelSource("edgeScalars", source, options.capacity)) return; this.refreshWasmEdgeScalars(options); } setWasmEdgeColors(source, options = {}) { if (!this.setWasmEdgeChannelSource("edgeColors", source, options.capacity)) return; this.refreshWasmEdgeColors(options); } refreshWasmNodePositions(options = {}) { const source = this._wasmNodePositionsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodePositions"); const count = this.resolveWasmNodeCount("nodePositions", source, options.nodeCount); this.setNodeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._nodePositionsCPU = this.copyWasmF32Range(source, count * NODELINK_VEC4_FLOATS); else this._nodePositionsCPU = null; this.updateWasmNodeBounds(options, source, count); this._wasmNodePositionsDirty = true; this._nodePositionsDirty = true; } refreshWasmNodeScalars(options = {}) { const source = this._wasmNodeScalarsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeScalars"); const count = this.resolveWasmNodeCount("nodeScalars", source, options.nodeCount); this.setNodeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._nodeScalarsCPU = this.copyWasmF32Range(source, count); else this._nodeScalarsCPU = null; this._wasmNodeScalarsDirty = true; this._nodeScalarsDirty = true; this._nodeScaleRevision++; } refreshWasmNodeColors(options = {}) { const source = this._wasmNodeColorsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeColors"); const count = this.resolveWasmNodeCount("nodeColors", source, options.nodeCount); this.setNodeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._nodeColorsCPU = this.copyWasmF32Range(source, count * NODELINK_VEC4_FLOATS); else this._nodeColorsCPU = null; this._wasmNodeColorsDirty = true; this._nodeColorsDirty = true; } refreshWasmNodeRadii(options = {}) { const source = this._wasmNodeRadiiSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeRadii"); const count = this.resolveWasmNodeCount("nodeRadii", source, options.nodeCount); this.setNodeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._nodeRadiiCPU = this.copyWasmF32Range(source, count * NODELINK_VEC4_FLOATS); else this._nodeRadiiCPU = null; this._wasmNodeRadiiDirty = true; this._nodeRadiiDirty = true; } refreshWasmEdges(options = {}) { const source = this._wasmEdgesSource; if (!source) return; source.refresh(); assertWasmU32View(source, "NodeLink: wasmEdges"); const count = this.resolveWasmEdgeCount("edges", source, options.edgeCount); this.setEdgeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._edgesCPU = this.copyWasmU32Range(source, count * NODELINK_U32_EDGE_COMPONENTS); else this._edgesCPU = null; this._wasmEdgesDirty = true; this._edgesDirty = true; } refreshWasmEdgeScalars(options = {}) { const source = this._wasmEdgeScalarsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmEdgeScalars"); const count = this.resolveWasmEdgeCount("edgeScalars", source, options.edgeCount); this.setEdgeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._edgeScalarsCPU = this.copyWasmF32Range(source, count); else this._edgeScalarsCPU = null; this._wasmEdgeScalarsDirty = true; this._edgeScalarsDirty = true; this._edgeScaleRevision++; } refreshWasmEdgeColors(options = {}) { const source = this._wasmEdgeColorsSource; if (!source) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmEdgeColors"); const count = this.resolveWasmEdgeCount("edgeColors", source, options.edgeCount); this.setEdgeCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._edgeColorsCPU = this.copyWasmF32Range(source, count * NODELINK_VEC4_FLOATS); else this._edgeColorsCPU = null; this._wasmEdgeColorsDirty = true; this._edgeColorsDirty = true; } refreshFromWasm(options = {}) { if (this._wasmNodePositionsSource) this.refreshWasmNodePositions(options); if (this._wasmNodeScalarsSource) this.refreshWasmNodeScalars(options); if (this._wasmNodeColorsSource) this.refreshWasmNodeColors(options); if (this._wasmNodeRadiiSource) this.refreshWasmNodeRadii(options); if (this._wasmEdgesSource) this.refreshWasmEdges(options); if (this._wasmEdgeScalarsSource) this.refreshWasmEdgeScalars(options); if (this._wasmEdgeColorsSource) this.refreshWasmEdgeColors(options); } clearWasmSources() { this.clearAllWasmState(true); } dropCPUData() { this._nodePositionsCPU = null; this._nodeScalarsCPU = null; this._nodeColorsCPU = null; this._nodeRadiiCPU = null; this._edgesCPU = null; this._edgeScalarsCPU = null; this._edgeColorsCPU = null; } decodePickElement(elementIndex) { if (!Number.isInteger(elementIndex) || elementIndex < 0) return null; if (elementIndex < this._nodeCount) return { component: "node", componentIndex: elementIndex | 0 }; const ei = (elementIndex | 0) - this._nodeCount; if (ei >= 0 && ei < this._edgeCount) return { component: "edge", componentIndex: ei }; return null; } mapLinearNodeIndexToNd(index) { return linearIndexToNdIndex(this._ndShape, index); } getNodeRecord(index) { const p = this._nodePositionsCPU; if (!p || index < 0 || index >= this._nodeCount) return null; const o = index * 4; const scalar = this._nodeScalarsCPU ? this._nodeScalarsCPU[index] : null; const color = this._nodeColorsCPU ? [this._nodeColorsCPU[o + 0], this._nodeColorsCPU[o + 1], this._nodeColorsCPU[o + 2], this._nodeColorsCPU[o + 3]] : null; return { position: [p[o + 0], p[o + 1], p[o + 2]], scalar, color }; } getEdgeRecord(index) { const edges = this._edgesCPU; if (!edges || index < 0 || index >= this._edgeCount) return null; const ei = index * 2; const src = edges[ei + 0] | 0; const dst = edges[ei + 1] | 0; const scalar = this._edgeScalarsCPU ? this._edgeScalarsCPU[index] : null; const color = this._edgeColorsCPU ? [this._edgeColorsCPU[index * 4 + 0], this._edgeColorsCPU[index * 4 + 1], this._edgeColorsCPU[index * 4 + 2], this._edgeColorsCPU[index * 4 + 3]] : null; let srcPosition = null; let dstPosition = null; if (this._nodePositionsCPU && src < this._nodeCount && dst < this._nodeCount) { const so = src * 4; const doff = dst * 4; srcPosition = [this._nodePositionsCPU[so + 0], this._nodePositionsCPU[so + 1], this._nodePositionsCPU[so + 2]]; dstPosition = [this._nodePositionsCPU[doff + 0], this._nodePositionsCPU[doff + 1], this._nodePositionsCPU[doff + 2]]; } return { src, dst, scalar, color, srcPosition, dstPosition }; } computeBoundsFromCPUData() { if (!this._nodePositionsCPU || this._nodeCount <= 0) return; const p = this._nodePositionsCPU; let minX = p[0], minY = p[1], minZ = p[2]; let maxX = p[0], maxY = p[1], maxZ = p[2]; for (let i = 1; i < this._nodeCount; i++) { const o = i * 4; const x = p[o + 0], y = p[o + 1], z = p[o + 2]; if (x < minX) minX = x; if (y < minY) minY = y; if (z < minZ) minZ = z; if (x > maxX) maxX = x; if (y > maxY) maxY = y; if (z > maxZ) maxZ = z; } const cx = (minX + maxX) * 0.5; const cy = (minY + maxY) * 0.5; const cz = (minZ + maxZ) * 0.5; let radius = 0; for (let i = 0; i < this._nodeCount; i++) { const o = i * 4; const dx = p[o + 0] - cx, dy = p[o + 1] - cy, dz = p[o + 2] - cz; const d = Math.sqrt(dx * dx + dy * dy + dz * dz); if (d > radius) radius = d; } this.setBounds(boundsFromBox([minX, minY, minZ], [maxX, maxY, maxZ]), "computed"); this.boundsCenter = [cx, cy, cz]; this.boundsRadius = radius; } getLocalBounds() { if (this._boundsSource === "none" && this._nodePositionsCPU) this.computeBoundsFromCPUData(); if (this._boundsSource === "none") return emptyBounds(this._nodeCount > 0); return boundsFromSphere(this.boundsCenter, this.boundsRadius); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } uploadWasmNodePositions(device, queue) { const source = this._wasmNodePositionsSource; if (!source || !this._wasmNodePositionsDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodePositions"); const count = this._nodeCount; validateWasmRecordRange(source, count, NODELINK_VEC4_FLOATS, "NodeLink: wasmNodePositions", "nodeCount"); if (count <= 0) { this._wasmNodePositionsDirty = false; this._nodePositionsDirty = false; this.clearPendingWrites("nodePositions"); return; } const data = source.array(); const byteLength = count * NODELINK_VEC4_BYTES; this.ensureWasmNodePositionsBuffer(device, count); const write = () => { assert(!!this.nodePositionsBuffer, "NodeLink: wasmNodePositions upload requires a nodePositionsBuffer."); queue.writeBuffer(this.nodePositionsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceNodePositionsBuffer(null, false); this._nodePositionsWasmManaged = false; this._wasmNodePositionsCapacity = 0; this.ensureWasmNodePositionsBuffer(device, count); write(); } if (this._keepCPUData) this._nodePositionsCPU = new Float32Array(data.subarray(0, count * NODELINK_VEC4_FLOATS)); else this._nodePositionsCPU = null; this._wasmNodePositionsDirty = false; this._nodePositionsDirty = false; this.clearPendingWrites("nodePositions"); } uploadWasmNodeScalars(device, queue) { const source = this._wasmNodeScalarsSource; if (!source || !this._wasmNodeScalarsDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeScalars"); const count = this._nodeCount; validateWasmRecordRange(source, count, 1, "NodeLink: wasmNodeScalars", "nodeCount"); if (count <= 0) { this._wasmNodeScalarsDirty = false; this._nodeScalarsDirty = false; this.clearPendingWrites("nodeScalars"); return; } const data = source.array(); const byteLength = count * NODELINK_F32_BYTES; this.ensureWasmNodeScalarsBuffer(device, count); const write = () => { assert(!!this.nodeScalarsBuffer, "NodeLink: wasmNodeScalars upload requires a nodeScalarsBuffer."); queue.writeBuffer(this.nodeScalarsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceNodeScalarsBuffer(null, false); this._nodeScalarsWasmManaged = false; this._wasmNodeScalarsCapacity = 0; this.ensureWasmNodeScalarsBuffer(device, count); write(); } if (this._keepCPUData) this._nodeScalarsCPU = new Float32Array(data.subarray(0, count)); else this._nodeScalarsCPU = null; this._wasmNodeScalarsDirty = false; this._nodeScalarsDirty = false; this.clearPendingWrites("nodeScalars"); } uploadWasmNodeColors(device, queue) { const source = this._wasmNodeColorsSource; if (!source || !this._wasmNodeColorsDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeColors"); const count = this._nodeCount; validateWasmRecordRange(source, count, NODELINK_VEC4_FLOATS, "NodeLink: wasmNodeColors", "nodeCount"); if (count <= 0) { this._wasmNodeColorsDirty = false; this._nodeColorsDirty = false; this.clearPendingWrites("nodeColors"); return; } const data = source.array(); const byteLength = count * NODELINK_VEC4_BYTES; this.ensureWasmNodeColorsBuffer(device, count); const write = () => { assert(!!this.nodeColorsBuffer, "NodeLink: wasmNodeColors upload requires a nodeColorsBuffer."); queue.writeBuffer(this.nodeColorsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceNodeColorsBuffer(null, false); this._nodeColorsWasmManaged = false; this._wasmNodeColorsCapacity = 0; this.ensureWasmNodeColorsBuffer(device, count); write(); } if (this._keepCPUData) this._nodeColorsCPU = new Float32Array(data.subarray(0, count * NODELINK_VEC4_FLOATS)); else this._nodeColorsCPU = null; this._wasmNodeColorsDirty = false; this._nodeColorsDirty = false; this.clearPendingWrites("nodeColors"); } uploadWasmNodeRadii(device, queue) { const source = this._wasmNodeRadiiSource; if (!source || !this._wasmNodeRadiiDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmNodeRadii"); const count = this._nodeCount; validateWasmRecordRange(source, count, NODELINK_VEC4_FLOATS, "NodeLink: wasmNodeRadii", "nodeCount"); if (count <= 0) { this._wasmNodeRadiiDirty = false; this._nodeRadiiDirty = false; this.clearPendingWrites("nodeRadii"); return; } const data = source.array(); const byteLength = count * NODELINK_VEC4_BYTES; this.ensureWasmNodeRadiiBuffer(device, count); const write = () => { assert(!!this.nodeRadiiBuffer, "NodeLink: wasmNodeRadii upload requires a nodeRadiiBuffer."); queue.writeBuffer(this.nodeRadiiBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceNodeRadiiBuffer(null, false); this._nodeRadiiWasmManaged = false; this._wasmNodeRadiiCapacity = 0; this.ensureWasmNodeRadiiBuffer(device, count); write(); } if (this._keepCPUData) this._nodeRadiiCPU = new Float32Array(data.subarray(0, count * NODELINK_VEC4_FLOATS)); else this._nodeRadiiCPU = null; this._wasmNodeRadiiDirty = false; this._nodeRadiiDirty = false; this.clearPendingWrites("nodeRadii"); } uploadWasmEdges(device, queue) { const source = this._wasmEdgesSource; if (!source || !this._wasmEdgesDirty) return; source.refresh(); assertWasmU32View(source, "NodeLink: wasmEdges"); const count = this._edgeCount; validateWasmRecordRange(source, count, NODELINK_U32_EDGE_COMPONENTS, "NodeLink: wasmEdges", "edgeCount"); if (count <= 0) { this._wasmEdgesDirty = false; this._edgesDirty = false; this.clearPendingWrites("edges"); return; } const data = source.array(); const byteLength = count * NODELINK_EDGE_BYTES; this.ensureWasmEdgesBuffer(device, count); const write = () => { assert(!!this.edgesBuffer, "NodeLink: wasmEdges upload requires an edgesBuffer."); queue.writeBuffer(this.edgesBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceEdgesBuffer(null, false); this._edgesWasmManaged = false; this._wasmEdgesCapacity = 0; this.ensureWasmEdgesBuffer(device, count); write(); } if (this._keepCPUData) this._edgesCPU = new Uint32Array(data.subarray(0, count * NODELINK_U32_EDGE_COMPONENTS)); else this._edgesCPU = null; this._wasmEdgesDirty = false; this._edgesDirty = false; this.clearPendingWrites("edges"); } uploadWasmEdgeScalars(device, queue) { const source = this._wasmEdgeScalarsSource; if (!source || !this._wasmEdgeScalarsDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmEdgeScalars"); const count = this._edgeCount; validateWasmRecordRange(source, count, 1, "NodeLink: wasmEdgeScalars", "edgeCount"); if (count <= 0) { this._wasmEdgeScalarsDirty = false; this._edgeScalarsDirty = false; this.clearPendingWrites("edgeScalars"); return; } const data = source.array(); const byteLength = count * NODELINK_F32_BYTES; this.ensureWasmEdgeScalarsBuffer(device, count); const write = () => { assert(!!this.edgeScalarsBuffer, "NodeLink: wasmEdgeScalars upload requires an edgeScalarsBuffer."); queue.writeBuffer(this.edgeScalarsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceEdgeScalarsBuffer(null, false); this._edgeScalarsWasmManaged = false; this._wasmEdgeScalarsCapacity = 0; this.ensureWasmEdgeScalarsBuffer(device, count); write(); } if (this._keepCPUData) this._edgeScalarsCPU = new Float32Array(data.subarray(0, count)); else this._edgeScalarsCPU = null; this._wasmEdgeScalarsDirty = false; this._edgeScalarsDirty = false; this.clearPendingWrites("edgeScalars"); } uploadWasmEdgeColors(device, queue) { const source = this._wasmEdgeColorsSource; if (!source || !this._wasmEdgeColorsDirty) return; source.refresh(); assertWasmF32View(source, "NodeLink: wasmEdgeColors"); const count = this._edgeCount; validateWasmRecordRange(source, count, NODELINK_VEC4_FLOATS, "NodeLink: wasmEdgeColors", "edgeCount"); if (count <= 0) { this._wasmEdgeColorsDirty = false; this._edgeColorsDirty = false; this.clearPendingWrites("edgeColors"); return; } const data = source.array(); const byteLength = count * NODELINK_VEC4_BYTES; this.ensureWasmEdgeColorsBuffer(device, count); const write = () => { assert(!!this.edgeColorsBuffer, "NodeLink: wasmEdgeColors upload requires an edgeColorsBuffer."); queue.writeBuffer(this.edgeColorsBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceEdgeColorsBuffer(null, false); this._edgeColorsWasmManaged = false; this._wasmEdgeColorsCapacity = 0; this.ensureWasmEdgeColorsBuffer(device, count); write(); } if (this._keepCPUData) this._edgeColorsCPU = new Float32Array(data.subarray(0, count * NODELINK_VEC4_FLOATS)); else this._edgeColorsCPU = null; this._wasmEdgeColorsDirty = false; this._edgeColorsDirty = false; this.clearPendingWrites("edgeColors"); } uploadWasmSources(device, queue) { this.uploadWasmNodePositions(device, queue); this.uploadWasmNodeScalars(device, queue); this.uploadWasmNodeColors(device, queue); this.uploadWasmNodeRadii(device, queue); this.uploadWasmEdges(device, queue); this.uploadWasmEdgeScalars(device, queue); this.uploadWasmEdgeColors(device, queue); } upload(device, queue) { if (this.hasDirtyWasmSources()) this.uploadWasmSources(device, queue); const hadQueuedWrites = this._pendingWrites.length > 0; let nonWasmUploaded = false; const uploadF32 = (buf, owned, cpu, dirty) => { if (!dirty || !cpu) return { buffer: buf, owned }; nonWasmUploaded = true; if (!buf || !owned) return { buffer: createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST), owned: true }; try { queue.writeBuffer(buf, 0, cpu.buffer, cpu.byteOffset, cpu.byteLength); return { buffer: buf, owned: true }; } catch { return { buffer: createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST), owned: true }; } }; const uploadU32 = (buf, owned, cpu, dirty) => { if (!dirty || !cpu) return { buffer: buf, owned }; nonWasmUploaded = true; if (!buf || !owned) return { buffer: createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST), owned: true }; try { queue.writeBuffer(buf, 0, cpu.buffer, cpu.byteOffset, cpu.byteLength); return { buffer: buf, owned: true }; } catch { return { buffer: createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST), owned: true }; } }; if (!this._wasmNodePositionsSource && !this._nodePositionsExternal) { const result = uploadF32(this.nodePositionsBuffer, this._nodePositionsOwned, this._nodePositionsCPU, this._nodePositionsDirty); this.replaceNodePositionsBuffer(result.buffer, result.owned); } if (!this._wasmNodeScalarsSource && !this._nodeScalarsExternal) { const result = uploadF32(this.nodeScalarsBuffer, this._nodeScalarsOwned, this._nodeScalarsCPU, this._nodeScalarsDirty); this.replaceNodeScalarsBuffer(result.buffer, result.owned); } if (!this._wasmNodeColorsSource && !this._nodeColorsExternal) { const result = uploadF32(this.nodeColorsBuffer, this._nodeColorsOwned, this._nodeColorsCPU, this._nodeColorsDirty); this.replaceNodeColorsBuffer(result.buffer, result.owned); } if (!this._wasmNodeRadiiSource && !this._nodeRadiiExternal) { const result = uploadF32(this.nodeRadiiBuffer, this._nodeRadiiOwned, this._nodeRadiiCPU, this._nodeRadiiDirty); this.replaceNodeRadiiBuffer(result.buffer, result.owned); } if (!this._wasmEdgesSource && !this._edgesExternal) { const result = uploadU32(this.edgesBuffer, this._edgesOwned, this._edgesCPU, this._edgesDirty); this.replaceEdgesBuffer(result.buffer, result.owned); } if (!this._wasmEdgeScalarsSource && !this._edgeScalarsExternal) { const result = uploadF32(this.edgeScalarsBuffer, this._edgeScalarsOwned, this._edgeScalarsCPU, this._edgeScalarsDirty); this.replaceEdgeScalarsBuffer(result.buffer, result.owned); } if (!this._wasmEdgeColorsSource && !this._edgeColorsExternal) { const result = uploadF32(this.edgeColorsBuffer, this._edgeColorsOwned, this._edgeColorsCPU, this._edgeColorsDirty); this.replaceEdgeColorsBuffer(result.buffer, result.owned); } this.flushQueuedWrites(queue); if (!this._keepCPUData) this.dropCPUData(); this._nodePositionsDirty = false; this._nodeScalarsDirty = false; this._nodeColorsDirty = false; this._nodeRadiiDirty = false; this._edgesDirty = false; this._edgeScalarsDirty = false; this._edgeColorsDirty = false; if (nonWasmUploaded || hadQueuedWrites) this.bindGroupKey = null; } getUniformBufferSize() { return UNIFORM_BYTE_SIZE3; } getUniformData() { const out = new Float32Array(UNIFORM_FLOAT_COUNT3); out.fill(0); out[0] = Math.max(0, this._nodeSize); out[1] = Math.max(0, this._edgeSize); out[2] = clamp01(this._opacity); out[3] = this._lit ? 1 : 0; packScaleTransform(this._nodeScaleTransform, out, 4); out[24] = colorModeId3(this._nodeColorMode); out[25] = typeof this._nodeColormap === "string" && this._nodeColormap === "custom" ? Math.min(8, Math.max(2, this._nodeColormapStops.length)) : 0; out[26] = nodeGeometryModeId(this._nodeGeometryMode); out[27] = this.nodeRadiiBuffer ? 1 : 0; packScaleTransform(this._edgeScaleTransform, out, 28); out[48] = colorModeId3(this._edgeColorMode); out[49] = typeof this._edgeColormap === "string" && this._edgeColormap === "custom" ? Math.min(8, Math.max(2, this._edgeColormapStops.length)) : 0; out[50] = edgeGeometryModeId(this._edgeGeometryMode); out[52] = this._nodeSolidColor[0]; out[53] = this._nodeSolidColor[1]; out[54] = this._nodeSolidColor[2]; out[55] = this._nodeSolidColor[3]; out[56] = this._edgeSolidColor[0]; out[57] = this._edgeSolidColor[1]; out[58] = this._edgeSolidColor[2]; out[59] = this._edgeSolidColor[3]; out[60] = this._minPointSize; out[61] = this._maxPointSize; out[62] = this._pointSizeAttenuation; const nodeStops = this._nodeColormapStops; for (let i = 0; i < 8; i++) { const s = nodeStops[Math.min(i, Math.max(1, nodeStops.length - 1))]; const o = 64 + i * 4; out[o + 0] = s[0]; out[o + 1] = s[1]; out[o + 2] = s[2]; out[o + 3] = s[3]; } const edgeStops = this._edgeColormapStops; for (let i = 0; i < 8; i++) { const s = edgeStops[Math.min(i, Math.max(1, edgeStops.length - 1))]; const o = 96 + i * 4; out[o + 0] = s[0]; out[o + 1] = s[1]; out[o + 2] = s[2]; out[o + 3] = s[3]; } return out; } get dirtyUniforms() { return this._uniformDirty; } markUniformsClean() { this._uniformDirty = false; } getNodeColormapKey() { const c = this._nodeColormap; return c instanceof Colormap ? `cm:${c.id}` : `cm:${c}`; } getEdgeColormapKey() { const c = this._edgeColormap; return c instanceof Colormap ? `cm:${c.id}` : `cm:${c}`; } getNodeColormapForBinding() { const c = this._nodeColormap; if (c instanceof Colormap) return c; return c === "custom" ? Colormap.builtin("grayscale") : Colormap.builtin(c); } getEdgeColormapForBinding() { const c = this._edgeColormap; if (c instanceof Colormap) return c; return c === "custom" ? Colormap.builtin("grayscale") : Colormap.builtin(c); } destroyOwnedBuffer(buffer, owned) { if (!buffer || !owned) return; buffer.destroy(); } destroy() { this.destroyOwnedBuffer(this.nodePositionsBuffer, this._nodePositionsOwned); this.destroyOwnedBuffer(this.nodeScalarsBuffer, this._nodeScalarsOwned); this.destroyOwnedBuffer(this.nodeColorsBuffer, this._nodeColorsOwned); this.destroyOwnedBuffer(this.nodeRadiiBuffer, this._nodeRadiiOwned); this.destroyOwnedBuffer(this.edgesBuffer, this._edgesOwned); this.destroyOwnedBuffer(this.edgeScalarsBuffer, this._edgeScalarsOwned); this.destroyOwnedBuffer(this.edgeColorsBuffer, this._edgeColorsOwned); this.uniformBuffer?.destroy(); this.nodePositionsBuffer = null; this.nodeScalarsBuffer = null; this.nodeColorsBuffer = null; this.nodeRadiiBuffer = null; this.edgesBuffer = null; this.edgeScalarsBuffer = null; this.edgeColorsBuffer = null; this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this.dropCPUData(); this._pendingWrites.length = 0; this._visualChangeListeners.clear(); this._ndShape = null; this._nodeCount = 0; this._edgeCount = 0; this._nodePositionsExternal = false; this._nodeScalarsExternal = false; this._nodeColorsExternal = false; this._nodeRadiiExternal = false; this._edgesExternal = false; this._edgeScalarsExternal = false; this._edgeColorsExternal = false; this._nodePositionsOwned = false; this._nodeScalarsOwned = false; this._nodeColorsOwned = false; this._nodeRadiiOwned = false; this._edgesOwned = false; this._edgeScalarsOwned = false; this._edgeColorsOwned = false; this._wasmNodePositionsSource = null; this._wasmNodeScalarsSource = null; this._wasmNodeColorsSource = null; this._wasmNodeRadiiSource = null; this._wasmEdgesSource = null; this._wasmEdgeScalarsSource = null; this._wasmEdgeColorsSource = null; this._wasmNodePositionsDirty = false; this._wasmNodeScalarsDirty = false; this._wasmNodeColorsDirty = false; this._wasmNodeRadiiDirty = false; this._wasmEdgesDirty = false; this._wasmEdgeScalarsDirty = false; this._wasmEdgeColorsDirty = false; this._nodePositionsWasmManaged = false; this._nodeScalarsWasmManaged = false; this._nodeColorsWasmManaged = false; this._nodeRadiiWasmManaged = false; this._edgesWasmManaged = false; this._edgeScalarsWasmManaged = false; this._edgeColorsWasmManaged = false; this._wasmNodePositionsCapacity = 0; this._wasmNodeScalarsCapacity = 0; this._wasmNodeColorsCapacity = 0; this._wasmNodeRadiiCapacity = 0; this._wasmEdgesCapacity = 0; this._wasmEdgeScalarsCapacity = 0; this._wasmEdgeColorsCapacity = 0; this._wasmNodePositionsCapacityHint = 0; this._wasmNodeScalarsCapacityHint = 0; this._wasmNodeColorsCapacityHint = 0; this._wasmNodeRadiiCapacityHint = 0; this._wasmEdgesCapacityHint = 0; this._wasmEdgeScalarsCapacityHint = 0; this._wasmEdgeColorsCapacityHint = 0; this._ownExternalBuffers = false; this.transform.dispose(); } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) { try { listener(kind); } catch { } } } }; // typescript/world/splatfield.ts var UNIFORM_FLOAT_COUNT4 = 4; var UNIFORM_BYTE_SIZE4 = UNIFORM_FLOAT_COUNT4 * 4; var SPLAT_VEC4_FLOATS = 4; var SPLAT_F32_BYTES = 4; var SPLAT_VEC4_BYTES = SPLAT_VEC4_FLOATS * SPLAT_F32_BYTES; var srgbChannelToLinear = (value) => { const x = clamp01(value); if (x <= 0.04045) return x / 12.92; return Math.pow((x + 0.055) / 1.055, 2.4); }; var isColorSpace = (value) => value === "linear" || value === "srgb"; var isSHDegree = (value) => value === 0 || value === 1 || value === 2 || value === 3; var shCoeffCount = (degree) => { switch (degree) { case 0: return 1; case 1: return 4; case 2: return 9; case 3: return 16; } }; var shFloatCount = (degree) => shCoeffCount(degree) * 3; var validateCount = (length, stride, label) => { assert(length % stride === 0, `SplatField: ${label} length must be a multiple of ${stride}.`); return length / stride | 0; }; var splatWasmFieldName = (channel) => channel === "sphericalHarmonics" ? "wasmSphericalHarmonics" : `wasm${channel[0].toUpperCase()}${channel.slice(1)}`; var splatWasmComponents = (channel, shDegree) => channel === "sphericalHarmonics" ? shFloatCount(shDegree) : SPLAT_VEC4_FLOATS; var makeWhiteColorData = (count) => { const out = new Float32Array(count * 4); for (let i = 0; i < count; i++) { const base = i * 4; out[base + 0] = 1; out[base + 1] = 1; out[base + 2] = 1; out[base + 3] = 1; } return out; }; var resolveColorLayout = (colors, count) => { if (count !== null) { if (colors.length === count * 4) return { stride: 4, count }; if (colors.length === count * 3) return { stride: 3, count }; assert(false, `SplatField: colors length must equal splatCount * 3 or splatCount * 4.`); } if (colors.length === 0) return { stride: 4, count: 0 }; const isRGBA = colors.length % 4 === 0; const isRGB = colors.length % 3 === 0; assert(isRGBA || isRGB, "SplatField: colors length must be a multiple of 3 or 4."); assert(!(isRGBA && isRGB), "SplatField: colors length is ambiguous without splatCount or non-color attribute counts."); if (isRGBA) return { stride: 4, count: colors.length / 4 | 0 }; return { stride: 3, count: colors.length / 3 | 0 }; }; var validateExternalPackedBufferSize = (buffer, splatCount, label) => { const minByteSize = splatCount * 16; assert(buffer.size >= minByteSize, `SplatField: ${label} size must be >= splatCount * 16 bytes (${minByteSize}).`); }; var validateExternalSHBufferSize = (buffer, splatCount, degree) => { const minByteSize = splatCount * shFloatCount(degree) * 4; assert(buffer.size >= minByteSize, `SplatField: shBuffer size must be >= splatCount * ${shFloatCount(degree)} * 4 bytes (${minByteSize}).`); }; var hasCPUShInputs = (desc) => !!(desc.sh0 || desc.sh1 || desc.sh2 || desc.sh3); var hasWasmInputs = (desc) => !!(desc.wasmCenterOpacity || desc.wasmRotation || desc.wasmScale || desc.wasmColor || desc.wasmSphericalHarmonics); var resolveSHDegreeFromCPU = (desc) => { assert(!!desc.sh0, "SplatField: sh0 is required when using spherical harmonic coefficients."); if (desc.sh3) { assert(!!desc.sh1 && !!desc.sh2, "SplatField: sh3 requires sh0, sh1, and sh2."); return 3; } if (desc.sh2) { assert(!!desc.sh1, "SplatField: sh2 requires sh0 and sh1."); return 2; } if (desc.sh1) return 1; return 0; }; var validateCPUShInputs = (desc, countHint) => { const inferredDegree = resolveSHDegreeFromCPU(desc); if (desc.shDegree !== void 0) { assert(isSHDegree(desc.shDegree), "SplatField: shDegree must be 0, 1, 2, or 3."); assert(desc.shDegree === inferredDegree, "SplatField: shDegree must match the provided spherical harmonic coefficient arrays."); } const sh0 = desc.sh0; const count = countHint ?? validateCount(sh0.length, 3, "sh0"); assert(Number.isInteger(count) && count >= 0, "SplatField: splatCount must be an integer >= 0."); assert(sh0.length === count * 3, "SplatField: sh0 length must equal splatCount * 3."); if (inferredDegree >= 1) assert(desc.sh1 && desc.sh1.length === count * 9, "SplatField: sh1 length must equal splatCount * 9."); else assert(!desc.sh1, "SplatField: shDegree 0 must not provide sh1."); if (inferredDegree >= 2) assert(desc.sh2 && desc.sh2.length === count * 15, "SplatField: sh2 length must equal splatCount * 15."); else assert(!desc.sh2, "SplatField: shDegree 0 or 1 must not provide sh2."); if (inferredDegree >= 3) assert(desc.sh3 && desc.sh3.length === count * 21, "SplatField: sh3 length must equal splatCount * 21."); else assert(!desc.sh3, "SplatField: shDegree 0, 1, or 2 must not provide sh3."); const coeffFloats = shFloatCount(inferredDegree); const out = new Float32Array(count * coeffFloats); for (let i = 0; i < count; i++) { let dst = i * coeffFloats; out[dst++] = sh0[i * 3 + 0] ?? 0; out[dst++] = sh0[i * 3 + 1] ?? 0; out[dst++] = sh0[i * 3 + 2] ?? 0; if (inferredDegree >= 1) { const src = i * 9; out.set(desc.sh1.subarray(src, src + 9), dst); dst += 9; } if (inferredDegree >= 2) { const src = i * 15; out.set(desc.sh2.subarray(src, src + 15), dst); dst += 15; } if (inferredDegree >= 3) { const src = i * 21; out.set(desc.sh3.subarray(src, src + 21), dst); } } return { count, degree: inferredDegree, data: out }; }; var SplatField = class { transform = new Transform(); name = null; visible = true; boundsMin = [0, 0, 0]; boundsMax = [0, 0, 0]; boundsCenter = [0, 0, 0]; boundsRadius = 0; centerOpacityBuffer = null; rotationBuffer = null; scaleBuffer = null; colorBuffer = null; shBuffer = null; uniformBuffer = null; bindGroup = null; bindGroupKey = null; _splatCount = 0; _centerOpacityCPU = null; _rotationCPU = null; _scaleCPU = null; _colorCPU = null; _shCPU = null; _wasmCenterOpacitySource = null; _wasmRotationSource = null; _wasmScaleSource = null; _wasmColorSource = null; _wasmSphericalHarmonicsSource = null; _keepCPUData = false; _ndShape = null; _boundsSource = "none"; _dataDirty = true; _uniformDirty = true; _colorSpace = "linear"; _opacityScale = 1; _centerOpacityOwned = false; _rotationOwned = false; _scaleOwned = false; _colorOwned = false; _shOwned = false; _wasmCenterOpacityDirty = false; _wasmRotationDirty = false; _wasmScaleDirty = false; _wasmColorDirty = false; _wasmSphericalHarmonicsDirty = false; _centerOpacityWasmManaged = false; _rotationWasmManaged = false; _scaleWasmManaged = false; _colorWasmManaged = false; _sphericalHarmonicsWasmManaged = false; _wasmCenterOpacityCapacity = 0; _wasmRotationCapacity = 0; _wasmScaleCapacity = 0; _wasmColorCapacity = 0; _wasmSphericalHarmonicsCapacity = 0; _wasmCenterOpacityCapacityHint = 0; _wasmRotationCapacityHint = 0; _wasmScaleCapacityHint = 0; _wasmColorCapacityHint = 0; _wasmSphericalHarmonicsCapacityHint = 0; _externalColorBufferSrgb = false; _shDegree = 0; _usesSphericalHarmonics = false; _sortRevision = 0; _sortCacheable = true; constructor(desc = {}) { if (desc.name !== void 0) this.name = desc.name; if (desc.visible !== void 0) this.visible = !!desc.visible; if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; if (desc.ndShape !== void 0) this.ndShape = desc.ndShape; if (desc.colorSpace !== void 0) { assert(isColorSpace(desc.colorSpace), `SplatField: invalid colorSpace '${String(desc.colorSpace)}'.`); this._colorSpace = desc.colorSpace; } if (desc.shDegree !== void 0) assert(isSHDegree(desc.shDegree), "SplatField: shDegree must be 0, 1, 2, or 3."); if (desc.opacityScale !== void 0) this._opacityScale = Math.max(0, desc.opacityScale); this.applyExplicitBounds(desc); const hasExternalWasmInputs = hasWasmInputs(desc); const hasShInputs = hasCPUShInputs(desc) || !!desc.shBuffer || !!desc.wasmSphericalHarmonics; const hasDirectColorInputs = !!(desc.colors || desc.colorBuffer || desc.wasmColor); assert(!(hasShInputs && hasDirectColorInputs), "SplatField: direct colors and spherical harmonic coefficients cannot be mixed."); assert(!(!hasShInputs && desc.shDegree !== void 0), "SplatField: shDegree requires sh0, shBuffer, or wasmSphericalHarmonics."); const hasCPUInputs = !!(desc.positions || desc.rotations || desc.scales || desc.opacities || desc.colors || hasCPUShInputs(desc)); const hasExternalInputs = !!(desc.centerOpacityBuffer || desc.rotationBuffer || desc.scaleBuffer || desc.colorBuffer || desc.shBuffer); assert(!(hasExternalWasmInputs && (hasCPUInputs || hasExternalInputs)), "SplatField: CPU-array, external-buffer, and external-WebAssembly descriptors cannot be mixed."); assert(!(hasCPUInputs && hasExternalInputs), "SplatField: CPU-array and external-buffer descriptors cannot be mixed."); if (hasExternalWasmInputs) { if (desc.wasmSphericalHarmonics) assert(desc.shDegree !== void 0, "SplatField: shDegree is required when using wasmSphericalHarmonics."); const wasmCapacity = assertWasmCapacity(desc.wasmCapacity, "SplatField: wasmCapacity"); this.setWasmPackedData({ centerOpacity: desc.wasmCenterOpacity ?? null, rotation: desc.wasmRotation ?? null, scale: desc.wasmScale ?? null, color: desc.wasmColor ?? null, sphericalHarmonics: desc.wasmSphericalHarmonics ?? null }, { splatCount: desc.splatCount, capacity: wasmCapacity, keepCPUData: this._keepCPUData, shDegree: desc.shDegree }); } else if (hasExternalInputs) this.setExternalData(desc); else if (hasCPUInputs) this.setCPUData(desc); else if (desc.splatCount !== void 0) { assert(Number.isInteger(desc.splatCount) && desc.splatCount >= 0, "SplatField: splatCount must be an integer >= 0."); this._splatCount = desc.splatCount | 0; this._dataDirty = false; } } applyExplicitBounds(desc) { if (desc.boundsMin && desc.boundsMax) { const bounds = boundsFromBox(desc.boundsMin, desc.boundsMax); this.setBounds(bounds, "explicit"); if (desc.boundsCenter) this.boundsCenter = [desc.boundsCenter[0], desc.boundsCenter[1], desc.boundsCenter[2]]; if (desc.boundsRadius !== void 0) this.boundsRadius = Math.max(0, desc.boundsRadius); return; } if (desc.boundsCenter || desc.boundsRadius !== void 0) { const center = desc.boundsCenter ?? [0, 0, 0]; const radius = desc.boundsRadius ?? 0; this.setBounds(boundsFromSphere(center, radius), "explicit"); } } setBounds(bounds, source) { this.boundsMin = [bounds.boxMin[0], bounds.boxMin[1], bounds.boxMin[2]]; this.boundsMax = [bounds.boxMax[0], bounds.boxMax[1], bounds.boxMax[2]]; this.boundsCenter = [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]]; this.boundsRadius = bounds.sphereRadius; this._boundsSource = source; } clearComputedBoundsIfNeeded() { if (this._boundsSource !== "computed") return; this._boundsSource = "none"; this.boundsMin = [0, 0, 0]; this.boundsMax = [0, 0, 0]; this.boundsCenter = [0, 0, 0]; this.boundsRadius = 0; } replaceCenterOpacityBuffer(buffer, owned) { if (this.centerOpacityBuffer && this.centerOpacityBuffer !== buffer && this._centerOpacityOwned) this.centerOpacityBuffer.destroy(); this.centerOpacityBuffer = buffer; this._centerOpacityOwned = owned; } replaceRotationBuffer(buffer, owned) { if (this.rotationBuffer && this.rotationBuffer !== buffer && this._rotationOwned) this.rotationBuffer.destroy(); this.rotationBuffer = buffer; this._rotationOwned = owned; } replaceScaleBuffer(buffer, owned) { if (this.scaleBuffer && this.scaleBuffer !== buffer && this._scaleOwned) this.scaleBuffer.destroy(); this.scaleBuffer = buffer; this._scaleOwned = owned; } replaceColorBuffer(buffer, owned) { if (this.colorBuffer && this.colorBuffer !== buffer && this._colorOwned) this.colorBuffer.destroy(); this.colorBuffer = buffer; this._colorOwned = owned; } replaceSHBuffer(buffer, owned) { if (this.shBuffer && this.shBuffer !== buffer && this._shOwned) this.shBuffer.destroy(); this.shBuffer = buffer; this._shOwned = owned; } hasExternalWasmSources() { return !!(this._wasmCenterOpacitySource || this._wasmRotationSource || this._wasmScaleSource || this._wasmColorSource || this._wasmSphericalHarmonicsSource); } hasDirtyWasmSources() { return this._wasmCenterOpacityDirty || this._wasmRotationDirty || this._wasmScaleDirty || this._wasmColorDirty || this._wasmSphericalHarmonicsDirty; } clearNonWasmDataForWasm() { this.replaceCenterOpacityBuffer(null, false); this.replaceRotationBuffer(null, false); this.replaceScaleBuffer(null, false); this.replaceColorBuffer(null, false); this.replaceSHBuffer(null, false); this.dropCPUData(); this._externalColorBufferSrgb = false; this._usesSphericalHarmonics = false; this._shDegree = 0; this._uniformDirty = true; this._dataDirty = false; this.bindGroupKey = null; } enterWasmSourceFamily() { if (this.hasExternalWasmSources()) return; this.clearNonWasmDataForWasm(); } clearWasmCenterOpacityState(destroyManagedBuffer) { this._wasmCenterOpacitySource = null; this._wasmCenterOpacityDirty = false; this._wasmCenterOpacityCapacityHint = 0; if (destroyManagedBuffer && this._centerOpacityWasmManaged) { this.replaceCenterOpacityBuffer(null, false); this.bindGroupKey = null; } this._centerOpacityWasmManaged = false; this._wasmCenterOpacityCapacity = 0; } clearWasmRotationState(destroyManagedBuffer) { this._wasmRotationSource = null; this._wasmRotationDirty = false; this._wasmRotationCapacityHint = 0; if (destroyManagedBuffer && this._rotationWasmManaged) { this.replaceRotationBuffer(null, false); this.bindGroupKey = null; } this._rotationWasmManaged = false; this._wasmRotationCapacity = 0; } clearWasmScaleState(destroyManagedBuffer) { this._wasmScaleSource = null; this._wasmScaleDirty = false; this._wasmScaleCapacityHint = 0; if (destroyManagedBuffer && this._scaleWasmManaged) { this.replaceScaleBuffer(null, false); this.bindGroupKey = null; } this._scaleWasmManaged = false; this._wasmScaleCapacity = 0; } clearWasmColorState(destroyManagedBuffer) { const hadColorSource = !!this._wasmColorSource || this._colorWasmManaged; this._wasmColorSource = null; this._wasmColorDirty = false; this._wasmColorCapacityHint = 0; if (destroyManagedBuffer && this._colorWasmManaged) { this.replaceColorBuffer(null, false); this.bindGroupKey = null; } this._colorWasmManaged = false; this._wasmColorCapacity = 0; this._externalColorBufferSrgb = false; if (hadColorSource && this.hasExternalWasmSources()) this._dataDirty = true; } clearWasmSphericalHarmonicsState(destroyManagedBuffer) { const hadSHSource = !!this._wasmSphericalHarmonicsSource || this._sphericalHarmonicsWasmManaged; this._wasmSphericalHarmonicsSource = null; this._wasmSphericalHarmonicsDirty = false; this._wasmSphericalHarmonicsCapacityHint = 0; if (destroyManagedBuffer && this._sphericalHarmonicsWasmManaged) { this.replaceSHBuffer(null, false); this.bindGroupKey = null; } this._sphericalHarmonicsWasmManaged = false; this._wasmSphericalHarmonicsCapacity = 0; this._shCPU = null; this._usesSphericalHarmonics = false; this._shDegree = 0; this._uniformDirty = true; if (hadSHSource && this.hasExternalWasmSources() && !this._wasmColorSource) this._dataDirty = true; } clearAllWasmState(destroyManagedBuffers) { this.clearWasmCenterOpacityState(destroyManagedBuffers); this.clearWasmRotationState(destroyManagedBuffers); this.clearWasmScaleState(destroyManagedBuffers); this.clearWasmColorState(destroyManagedBuffers); this.clearWasmSphericalHarmonicsState(destroyManagedBuffers); } primaryWasmChannel() { if (this._wasmCenterOpacitySource) return "centerOpacity"; if (this._wasmRotationSource) return "rotation"; if (this._wasmScaleSource) return "scale"; if (this._wasmColorSource) return "color"; if (this._wasmSphericalHarmonicsSource) return "sphericalHarmonics"; return null; } resolveWasmSplatCount(channel, source, explicitSplatCount) { const field = splatWasmFieldName(channel); const primary = this.primaryWasmChannel(); const components = splatWasmComponents(channel, this._shDegree); if (explicitSplatCount !== void 0) { const count = assertWasmRecordCount(explicitSplatCount, "SplatField: splatCount"); assert(!primary || channel === primary || count === this._splatCount, `SplatField: refreshWasm${field.slice(4)} splatCount must match the current splatCount when another wasm source is active; call refreshFromWasm() to update splat count.`); validateWasmRecordRange(source, count, components, `SplatField: ${field}`, "splatCount"); return count; } if (channel === primary) return resolveWasmRecordCount(source, void 0, components, `SplatField: ${field}`, "SplatField: splatCount", "splatCount"); assert(this._splatCount > 0 || source.length === 0, `SplatField: splatCount is required when using ${field} without a primary wasm source.`); validateWasmRecordRange(source, this._splatCount, components, `SplatField: ${field}`, "splatCount"); return this._splatCount; } setSplatCountFromWasm(splatCount) { const count = assertWasmRecordCount(splatCount, "SplatField: splatCount"); const changed = count !== this._splatCount; this._splatCount = count; if (!changed) return; if (this._wasmCenterOpacitySource) this._wasmCenterOpacityDirty = true; if (this._wasmRotationSource) this._wasmRotationDirty = true; if (this._wasmScaleSource) this._wasmScaleDirty = true; if (this._wasmColorSource) this._wasmColorDirty = true; if (this._wasmSphericalHarmonicsSource) this._wasmSphericalHarmonicsDirty = true; if (!this._wasmColorSource && this.hasExternalWasmSources()) this._dataDirty = true; } setWasmChannelSource(channel, source, capacity) { if (source === null) { if (channel === "centerOpacity") this.clearWasmCenterOpacityState(true); else if (channel === "rotation") this.clearWasmRotationState(true); else if (channel === "scale") this.clearWasmScaleState(true); else if (channel === "color") this.clearWasmColorState(true); else this.clearWasmSphericalHarmonicsState(true); return false; } this.enterWasmSourceFamily(); const field = splatWasmFieldName(channel); const wasmSource = assertWasmF32View(source, `SplatField: ${field}`); const capacityHint = assertWasmCapacity(capacity, `SplatField: ${field} capacity`); if (channel === "centerOpacity") { this._wasmCenterOpacityCapacityHint = capacityHint; if (!this._centerOpacityWasmManaged) { this.replaceCenterOpacityBuffer(null, false); this._wasmCenterOpacityCapacity = 0; this.bindGroupKey = null; } this._wasmCenterOpacitySource = wasmSource; this._centerOpacityCPU = null; } else if (channel === "rotation") { this._wasmRotationCapacityHint = capacityHint; if (!this._rotationWasmManaged) { this.replaceRotationBuffer(null, false); this._wasmRotationCapacity = 0; this.bindGroupKey = null; } this._wasmRotationSource = wasmSource; this._rotationCPU = null; } else if (channel === "scale") { this._wasmScaleCapacityHint = capacityHint; if (!this._scaleWasmManaged) { this.replaceScaleBuffer(null, false); this._wasmScaleCapacity = 0; this.bindGroupKey = null; } this._wasmScaleSource = wasmSource; this._scaleCPU = null; } else if (channel === "color") { this.clearWasmSphericalHarmonicsState(true); this._wasmColorCapacityHint = capacityHint; if (!this._colorWasmManaged) { this.replaceColorBuffer(null, false); this._wasmColorCapacity = 0; this.bindGroupKey = null; } this._wasmColorSource = wasmSource; this._colorCPU = null; this._externalColorBufferSrgb = this._colorSpace === "srgb"; this._usesSphericalHarmonics = false; this._shDegree = 0; this._uniformDirty = true; } else { this.clearWasmColorState(true); this._wasmSphericalHarmonicsCapacityHint = capacityHint; if (!this._sphericalHarmonicsWasmManaged) { this.replaceSHBuffer(null, false); this._wasmSphericalHarmonicsCapacity = 0; this.bindGroupKey = null; } this._wasmSphericalHarmonicsSource = wasmSource; this._shCPU = null; this._externalColorBufferSrgb = false; this._usesSphericalHarmonics = true; this._uniformDirty = true; } return true; } copyWasmActiveRange(source, elementCount) { const view = source.array(); return new Float32Array(view.subarray(0, elementCount)); } ensureWasmCenterOpacityBuffer(device, splatCount) { const required = Math.max(splatCount, this._wasmCenterOpacityCapacityHint); if (required <= 0) return; if (this.centerOpacityBuffer && this._centerOpacityWasmManaged && this._wasmCenterOpacityCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmCenterOpacityCapacity); this.replaceCenterOpacityBuffer(device.createBuffer({ label: "SplatField.wasmCenterOpacity", size: capacity * SPLAT_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._centerOpacityWasmManaged = true; this._wasmCenterOpacityCapacity = capacity; this.bindGroupKey = null; } ensureWasmRotationBuffer(device, splatCount) { const required = Math.max(splatCount, this._wasmRotationCapacityHint); if (required <= 0) return; if (this.rotationBuffer && this._rotationWasmManaged && this._wasmRotationCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmRotationCapacity); this.replaceRotationBuffer(device.createBuffer({ label: "SplatField.wasmRotation", size: capacity * SPLAT_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._rotationWasmManaged = true; this._wasmRotationCapacity = capacity; this.bindGroupKey = null; } ensureWasmScaleBuffer(device, splatCount) { const required = Math.max(splatCount, this._wasmScaleCapacityHint); if (required <= 0) return; if (this.scaleBuffer && this._scaleWasmManaged && this._wasmScaleCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmScaleCapacity); this.replaceScaleBuffer(device.createBuffer({ label: "SplatField.wasmScale", size: capacity * SPLAT_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._scaleWasmManaged = true; this._wasmScaleCapacity = capacity; this.bindGroupKey = null; } ensureWasmColorBuffer(device, splatCount) { const required = Math.max(splatCount, this._wasmColorCapacityHint); if (required <= 0) return; if (this.colorBuffer && this._colorWasmManaged && this._wasmColorCapacity >= required) return; const capacity = growWasmCapacity(required, this._wasmColorCapacity); this.replaceColorBuffer(device.createBuffer({ label: "SplatField.wasmColor", size: capacity * SPLAT_VEC4_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._colorWasmManaged = true; this._wasmColorCapacity = capacity; this.bindGroupKey = null; } ensureWasmSphericalHarmonicsBuffer(device, splatCount) { const required = Math.max(splatCount, this._wasmSphericalHarmonicsCapacityHint); if (required <= 0) return; const bytesPerSplat = shFloatCount(this._shDegree) * SPLAT_F32_BYTES; const requiredBytes = required * bytesPerSplat; if (this.shBuffer && this._sphericalHarmonicsWasmManaged && this._wasmSphericalHarmonicsCapacity >= required && this.shBuffer.size >= requiredBytes) return; const capacity = growWasmCapacity(required, this._wasmSphericalHarmonicsCapacity); this.replaceSHBuffer(device.createBuffer({ label: "SplatField.wasmSphericalHarmonics", size: capacity * bytesPerSplat, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._sphericalHarmonicsWasmManaged = true; this._wasmSphericalHarmonicsCapacity = capacity; this.bindGroupKey = null; } computeBoundsFromPackedData(centerOpacity, scales, splatCount) { if (splatCount <= 0) return; let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let i = 0; i < splatCount; i++) { const base = i * 4; const x = centerOpacity[base + 0]; const y = centerOpacity[base + 1]; const z = centerOpacity[base + 2]; const radius = 3 * Math.max(Math.abs(scales[base + 0]), Math.abs(scales[base + 1]), Math.abs(scales[base + 2])); minX = Math.min(minX, x - radius); minY = Math.min(minY, y - radius); minZ = Math.min(minZ, z - radius); maxX = Math.max(maxX, x + radius); maxY = Math.max(maxY, y + radius); maxZ = Math.max(maxZ, z + radius); } this.setBounds(boundsFromBox([minX, minY, minZ], [maxX, maxY, maxZ]), "computed"); } computeBoundsFromWasmSources(splatCount) { const centerOpacitySource = this._wasmCenterOpacitySource; const scaleSource = this._wasmScaleSource; if (!centerOpacitySource || !scaleSource || splatCount <= 0) return false; centerOpacitySource.refresh(); scaleSource.refresh(); assertWasmF32View(centerOpacitySource, "SplatField: wasmCenterOpacity"); assertWasmF32View(scaleSource, "SplatField: wasmScale"); validateWasmRecordRange(centerOpacitySource, splatCount, SPLAT_VEC4_FLOATS, "SplatField: wasmCenterOpacity", "splatCount"); validateWasmRecordRange(scaleSource, splatCount, SPLAT_VEC4_FLOATS, "SplatField: wasmScale", "splatCount"); this.computeBoundsFromPackedData(centerOpacitySource.array(), scaleSource.array(), splatCount); return true; } updateWasmBounds(options) { if (options.recomputeBounds && this._boundsSource !== "explicit" && this.computeBoundsFromWasmSources(this._splatCount)) return; this.clearComputedBoundsIfNeeded(); } assertWasmCoreSourcesAvailable(method) { if (this._splatCount <= 0) return; assert(!!this._wasmCenterOpacitySource && !!this._wasmRotationSource && !!this._wasmScaleSource, `SplatField: wasmCenterOpacity, wasmRotation, and wasmScale are required for ${method}.`); } assertCanSetSingleWasmCoreChannel(method) { assert(this.hasExternalWasmSources(), `SplatField: ${method} cannot replace a non-wasm source family by itself; use setWasmPackedData() with wasmCenterOpacity, wasmRotation, and wasmScale for initial wasm source-family replacement.`); } setCPUData(desc) { this._sortCacheable = true; this._sortRevision++; this.clearAllWasmState(true); const positions = desc.positions ?? null; const rotations = desc.rotations ?? null; const scales = desc.scales ?? null; const opacities = desc.opacities ?? null; const colors = desc.colors ?? null; const usesSH = hasCPUShInputs(desc); const positionCount = positions ? validateCount(positions.length, 3, "positions") : null; const rotationCount = rotations ? validateCount(rotations.length, 4, "rotations") : null; const scaleCount = scales ? validateCount(scales.length, 3, "scales") : null; const opacityCount = opacities ? opacities.length : null; const inferredCount = desc.splatCount ?? positionCount ?? rotationCount ?? scaleCount ?? opacityCount ?? null; const colorLayout = colors ? resolveColorLayout(colors, inferredCount) : null; const colorStride = colorLayout?.stride ?? 4; const colorCount = colorLayout?.count ?? null; const sh = usesSH ? validateCPUShInputs(desc, inferredCount ?? colorCount ?? null) : null; const count = inferredCount ?? colorCount ?? sh?.count ?? 0; assert(Number.isInteger(count) && count >= 0, "SplatField: splatCount must be an integer >= 0."); if (positionCount !== null) assert(positionCount === count, "SplatField: positions length does not match splatCount."); if (rotationCount !== null) assert(rotationCount === count, "SplatField: rotations length does not match splatCount."); if (scaleCount !== null) assert(scaleCount === count, "SplatField: scales length does not match splatCount."); if (opacityCount !== null) assert(opacityCount === count, "SplatField: opacities length does not match splatCount."); if (colorCount !== null) assert(colorCount === count, "SplatField: colors length does not match splatCount."); if (sh) assert(sh.count === count, "SplatField: spherical harmonic coefficient lengths do not match splatCount."); this._splatCount = count | 0; this._centerOpacityCPU = new Float32Array(count * 4); this._rotationCPU = new Float32Array(count * 4); this._scaleCPU = new Float32Array(count * 4); this._colorCPU = makeWhiteColorData(count); this._shCPU = sh?.data ?? null; this._shDegree = sh?.degree ?? 0; this._usesSphericalHarmonics = !!sh; for (let i = 0; i < count; i++) { const centerBase = i * 4; const positionBase = i * 3; const colorBase = i * colorStride; this._centerOpacityCPU[centerBase + 0] = positions ? positions[positionBase + 0] : 0; this._centerOpacityCPU[centerBase + 1] = positions ? positions[positionBase + 1] : 0; this._centerOpacityCPU[centerBase + 2] = positions ? positions[positionBase + 2] : 0; this._centerOpacityCPU[centerBase + 3] = opacities ? opacities[i] : 1; this._rotationCPU[centerBase + 0] = rotations ? rotations[centerBase + 0] : 0; this._rotationCPU[centerBase + 1] = rotations ? rotations[centerBase + 1] : 0; this._rotationCPU[centerBase + 2] = rotations ? rotations[centerBase + 2] : 0; this._rotationCPU[centerBase + 3] = rotations ? rotations[centerBase + 3] : 1; this._scaleCPU[centerBase + 0] = scales ? scales[positionBase + 0] : 1; this._scaleCPU[centerBase + 1] = scales ? scales[positionBase + 1] : 1; this._scaleCPU[centerBase + 2] = scales ? scales[positionBase + 2] : 1; this._scaleCPU[centerBase + 3] = 0; if (colors) { let r = colors[colorBase + 0] ?? 1; let g = colors[colorBase + 1] ?? 1; let b = colors[colorBase + 2] ?? 1; const a = colorStride === 4 ? colors[colorBase + 3] ?? 1 : 1; if (this._colorSpace === "srgb") { r = srgbChannelToLinear(r); g = srgbChannelToLinear(g); b = srgbChannelToLinear(b); } this._colorCPU[centerBase + 0] = r; this._colorCPU[centerBase + 1] = g; this._colorCPU[centerBase + 2] = b; this._colorCPU[centerBase + 3] = a; } } this._externalColorBufferSrgb = false; this._centerOpacityOwned = true; this._rotationOwned = true; this._scaleOwned = true; this._colorOwned = true; this._shOwned = !!sh; this.replaceCenterOpacityBuffer(null, false); this.replaceRotationBuffer(null, false); this.replaceScaleBuffer(null, false); this.replaceColorBuffer(null, false); this.replaceSHBuffer(null, false); this._dataDirty = true; this._uniformDirty = true; this.bindGroupKey = null; this.clearComputedBoundsIfNeeded(); if (this._boundsSource === "none" && count > 0) this.computeBoundsFromCPUData(); } setExternalData(desc) { this._sortCacheable = false; this._sortRevision++; this.clearAllWasmState(true); assert(!!desc.centerOpacityBuffer && !!desc.rotationBuffer && !!desc.scaleBuffer, "SplatField: centerOpacityBuffer, rotationBuffer, and scaleBuffer are required when using external buffers."); assert(Number.isInteger(desc.splatCount) && (desc.splatCount ?? -1) >= 0, "SplatField: splatCount is required when using external buffers."); if (desc.shBuffer) assert(desc.shDegree !== void 0, "SplatField: shDegree is required when using shBuffer."); const ownBuffers = !!desc.ownBuffers; const splatCount = desc.splatCount | 0; const centerOpacityBuffer = resolveGPUBuffer(desc.centerOpacityBuffer); const rotationBuffer = resolveGPUBuffer(desc.rotationBuffer); const scaleBuffer = resolveGPUBuffer(desc.scaleBuffer); const colorBuffer = desc.colorBuffer ? resolveGPUBuffer(desc.colorBuffer) : null; const shBuffer = desc.shBuffer ? resolveGPUBuffer(desc.shBuffer) : null; validateExternalPackedBufferSize(centerOpacityBuffer, splatCount, "centerOpacityBuffer"); validateExternalPackedBufferSize(rotationBuffer, splatCount, "rotationBuffer"); validateExternalPackedBufferSize(scaleBuffer, splatCount, "scaleBuffer"); if (colorBuffer) validateExternalPackedBufferSize(colorBuffer, splatCount, "colorBuffer"); if (shBuffer) validateExternalSHBufferSize(shBuffer, splatCount, desc.shDegree); this._splatCount = splatCount; this.replaceCenterOpacityBuffer(centerOpacityBuffer, ownBuffers); this.replaceRotationBuffer(rotationBuffer, ownBuffers); this.replaceScaleBuffer(scaleBuffer, ownBuffers); this.replaceColorBuffer(colorBuffer, ownBuffers && !!colorBuffer); this.replaceSHBuffer(shBuffer, ownBuffers && !!shBuffer); this._centerOpacityCPU = null; this._rotationCPU = null; this._scaleCPU = null; this._colorCPU = colorBuffer ? null : makeWhiteColorData(splatCount); this._shCPU = null; this._externalColorBufferSrgb = !!colorBuffer && this._colorSpace === "srgb"; this._shDegree = desc.shDegree ?? 0; this._usesSphericalHarmonics = !!shBuffer; this._dataDirty = !colorBuffer && splatCount > 0; this._uniformDirty = true; this.bindGroupKey = null; this.clearComputedBoundsIfNeeded(); } get splatCount() { return this._splatCount; } get colorSpace() { return this._colorSpace; } get opacityScale() { return this._opacityScale; } get usesSphericalHarmonics() { return this._usesSphericalHarmonics; } get shDegree() { return this._shDegree; } set opacityScale(value) { const next = Math.max(0, value); if (next === this._opacityScale) return; this._opacityScale = next; this._uniformDirty = true; } get externalColorBufferSrgb() { return this._externalColorBufferSrgb; } get ndShape() { return this._ndShape ? this._ndShape.slice() : null; } set ndShape(shape) { this._ndShape = normalizePositiveIntShape(shape, "SplatField: ndShape"); } mapLinearIndexToNd(index) { return linearIndexToNdIndex(this._ndShape, index); } setWasmCenterOpacity(source, options = {}) { if (source !== null) this.assertCanSetSingleWasmCoreChannel("setWasmCenterOpacity()"); if (!this.setWasmChannelSource("centerOpacity", source, options.capacity)) return; this.refreshWasmCenterOpacity(options); } setWasmRotation(source, options = {}) { if (source !== null) this.assertCanSetSingleWasmCoreChannel("setWasmRotation()"); if (!this.setWasmChannelSource("rotation", source, options.capacity)) return; this.refreshWasmRotation(options); } setWasmScale(source, options = {}) { if (source !== null) this.assertCanSetSingleWasmCoreChannel("setWasmScale()"); if (!this.setWasmChannelSource("scale", source, options.capacity)) return; this.refreshWasmScale(options); } setWasmColor(source, options = {}) { if (!this.setWasmChannelSource("color", source, options.capacity)) { if (this.hasExternalWasmSources()) this._dataDirty = true; return; } this.refreshWasmColor(options); } setWasmSphericalHarmonics(source, options = {}) { if (source !== null) { assert(options.shDegree !== void 0 || this._usesSphericalHarmonics, "SplatField: shDegree is required when using wasmSphericalHarmonics."); if (options.shDegree !== void 0) { assert(isSHDegree(options.shDegree), "SplatField: shDegree must be 0, 1, 2, or 3."); this._shDegree = options.shDegree; } } if (!this.setWasmChannelSource("sphericalHarmonics", source, options.capacity)) return; this.refreshWasmSphericalHarmonics(options); } setWasmPackedData(sources, options = {}) { const hasColor = !!sources.color; const hasSH = !!sources.sphericalHarmonics; assert(!(hasColor && hasSH), "SplatField: direct colors and spherical harmonic coefficients cannot be mixed."); if (hasSH) { assert(options.shDegree !== void 0 || this._usesSphericalHarmonics, "SplatField: shDegree is required when using wasmSphericalHarmonics."); if (options.shDegree !== void 0) { assert(isSHDegree(options.shDegree), "SplatField: shDegree must be 0, 1, 2, or 3."); this._shDegree = options.shDegree; } } if (Object.prototype.hasOwnProperty.call(sources, "centerOpacity")) this.setWasmChannelSource("centerOpacity", sources.centerOpacity ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "rotation")) this.setWasmChannelSource("rotation", sources.rotation ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "scale")) this.setWasmChannelSource("scale", sources.scale ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "color")) this.setWasmChannelSource("color", sources.color ?? null, options.capacity); if (Object.prototype.hasOwnProperty.call(sources, "sphericalHarmonics")) this.setWasmChannelSource("sphericalHarmonics", sources.sphericalHarmonics ?? null, options.capacity); this.refreshFromWasm(options); this.assertWasmCoreSourcesAvailable("setWasmPackedData"); } refreshWasmCenterOpacity(options = {}) { const source = this._wasmCenterOpacitySource; if (!source) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmCenterOpacity"); const count = this.resolveWasmSplatCount("centerOpacity", source, options.splatCount); this.setSplatCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._centerOpacityCPU = this.copyWasmActiveRange(source, count * SPLAT_VEC4_FLOATS); else this._centerOpacityCPU = null; this.updateWasmBounds(options); this._wasmCenterOpacityDirty = true; this._sortCacheable = true; this._sortRevision++; this.assertWasmCoreSourcesAvailable("refreshWasmCenterOpacity"); } refreshWasmRotation(options = {}) { const source = this._wasmRotationSource; if (!source) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmRotation"); const count = this.resolveWasmSplatCount("rotation", source, options.splatCount); this.setSplatCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._rotationCPU = this.copyWasmActiveRange(source, count * SPLAT_VEC4_FLOATS); else this._rotationCPU = null; this.updateWasmBounds(options); this._wasmRotationDirty = true; this.assertWasmCoreSourcesAvailable("refreshWasmRotation"); } refreshWasmScale(options = {}) { const source = this._wasmScaleSource; if (!source) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmScale"); const count = this.resolveWasmSplatCount("scale", source, options.splatCount); this.setSplatCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._scaleCPU = this.copyWasmActiveRange(source, count * SPLAT_VEC4_FLOATS); else this._scaleCPU = null; if (!this._wasmColorSource && this._keepCPUData) this._colorCPU = makeWhiteColorData(count); this.updateWasmBounds(options); this._wasmScaleDirty = true; this.assertWasmCoreSourcesAvailable("refreshWasmScale"); } refreshWasmColor(options = {}) { const source = this._wasmColorSource; if (!source) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmColor"); const count = this.resolveWasmSplatCount("color", source, options.splatCount); this.setSplatCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) this._colorCPU = this.copyWasmActiveRange(source, count * SPLAT_VEC4_FLOATS); else this._colorCPU = null; this._externalColorBufferSrgb = this._colorSpace === "srgb"; this._usesSphericalHarmonics = false; this._shDegree = 0; this._uniformDirty = true; this._wasmColorDirty = true; this.assertWasmCoreSourcesAvailable("refreshWasmColor"); } refreshWasmSphericalHarmonics(options = {}) { const source = this._wasmSphericalHarmonicsSource; if (!source) return; if (options.shDegree !== void 0) { assert(isSHDegree(options.shDegree), "SplatField: shDegree must be 0, 1, 2, or 3."); this._shDegree = options.shDegree; } source.refresh(); assertWasmF32View(source, "SplatField: wasmSphericalHarmonics"); const count = this.resolveWasmSplatCount("sphericalHarmonics", source, options.splatCount); this.setSplatCountFromWasm(count); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; if (this._keepCPUData) { this._shCPU = this.copyWasmActiveRange(source, count * shFloatCount(this._shDegree)); this._colorCPU = makeWhiteColorData(count); } else { this._shCPU = null; this._colorCPU = null; } this._externalColorBufferSrgb = false; this._usesSphericalHarmonics = true; this._uniformDirty = true; this._dataDirty = true; this._wasmSphericalHarmonicsDirty = true; this.assertWasmCoreSourcesAvailable("refreshWasmSphericalHarmonics"); } refreshFromWasm(options = {}) { if (this._wasmCenterOpacitySource) this.refreshWasmCenterOpacity(options); if (this._wasmRotationSource) this.refreshWasmRotation(options); if (this._wasmScaleSource) this.refreshWasmScale(options); if (this._wasmColorSource) this.refreshWasmColor(options); if (this._wasmSphericalHarmonicsSource) this.refreshWasmSphericalHarmonics(options); } clearWasmSources() { this.clearAllWasmState(true); this._dataDirty = false; } getSplatRecord(index) { if (!Number.isInteger(index) || index < 0 || index >= this._splatCount) return null; const centerOpacity = this._centerOpacityCPU; const rotation = this._rotationCPU; const scale = this._scaleCPU; if (!centerOpacity || !rotation || !scale) return null; const base = index * 4; const packed = [centerOpacity[base + 0], centerOpacity[base + 1], centerOpacity[base + 2], centerOpacity[base + 3]]; const color = this._colorCPU ? [this._colorCPU[base + 0], this._colorCPU[base + 1], this._colorCPU[base + 2], this._colorCPU[base + 3]] : null; const sphericalHarmonics = this.getSphericalHarmonicsRecord(index); return { position: [packed[0], packed[1], packed[2]], rotation: [rotation[base + 0], rotation[base + 1], rotation[base + 2], rotation[base + 3]], scale: [scale[base + 0], scale[base + 1], scale[base + 2]], opacity: packed[3], color, sphericalHarmonicsDegree: sphericalHarmonics ? this._shDegree : null, sphericalHarmonics, packed }; } getSphericalHarmonicsRecord(index) { if (!this._usesSphericalHarmonics || !this._shCPU) return null; if (!Number.isInteger(index) || index < 0 || index >= this._splatCount) return null; const floats = shFloatCount(this._shDegree); const base = index * floats; return Array.from(this._shCPU.subarray(base, base + floats)); } dropCPUData() { this._centerOpacityCPU = null; this._rotationCPU = null; this._scaleCPU = null; this._colorCPU = null; this._shCPU = null; } computeBoundsFromCPUData() { const centers = this._centerOpacityCPU; const scales = this._scaleCPU; const count = this._splatCount; if (!centers || !scales || count <= 0) return; let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let i = 0; i < count; i++) { const base = i * 4; const x = centers[base + 0]; const y = centers[base + 1]; const z = centers[base + 2]; const radius = 3 * Math.max(Math.abs(scales[base + 0]), Math.abs(scales[base + 1]), Math.abs(scales[base + 2])); minX = Math.min(minX, x - radius); minY = Math.min(minY, y - radius); minZ = Math.min(minZ, z - radius); maxX = Math.max(maxX, x + radius); maxY = Math.max(maxY, y + radius); maxZ = Math.max(maxZ, z + radius); } this.setBounds(boundsFromBox([minX, minY, minZ], [maxX, maxY, maxZ]), "computed"); } getLocalBounds() { if (this._boundsSource === "none" && this._centerOpacityCPU && this._scaleCPU) this.computeBoundsFromCPUData(); if (this._boundsSource === "none") return emptyBounds(this._splatCount > 0); return boundsFromBoxAndSphere(this.boundsMin, this.boundsMax, this.boundsCenter, this.boundsRadius); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } uploadWasmCenterOpacity(device, queue) { const source = this._wasmCenterOpacitySource; if (!source || !this._wasmCenterOpacityDirty) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmCenterOpacity"); const count = this._splatCount; validateWasmRecordRange(source, count, SPLAT_VEC4_FLOATS, "SplatField: wasmCenterOpacity", "splatCount"); if (count <= 0) { this._wasmCenterOpacityDirty = false; return; } const data = source.array(); const byteLength = count * SPLAT_VEC4_BYTES; this.ensureWasmCenterOpacityBuffer(device, count); const write = () => { assert(!!this.centerOpacityBuffer, "SplatField: wasmCenterOpacity upload requires a centerOpacityBuffer."); queue.writeBuffer(this.centerOpacityBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceCenterOpacityBuffer(null, false); this._centerOpacityWasmManaged = false; this._wasmCenterOpacityCapacity = 0; this.ensureWasmCenterOpacityBuffer(device, count); write(); } if (this._keepCPUData) this._centerOpacityCPU = new Float32Array(data.subarray(0, count * SPLAT_VEC4_FLOATS)); else this._centerOpacityCPU = null; this._wasmCenterOpacityDirty = false; } uploadWasmRotation(device, queue) { const source = this._wasmRotationSource; if (!source || !this._wasmRotationDirty) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmRotation"); const count = this._splatCount; validateWasmRecordRange(source, count, SPLAT_VEC4_FLOATS, "SplatField: wasmRotation", "splatCount"); if (count <= 0) { this._wasmRotationDirty = false; return; } const data = source.array(); const byteLength = count * SPLAT_VEC4_BYTES; this.ensureWasmRotationBuffer(device, count); const write = () => { assert(!!this.rotationBuffer, "SplatField: wasmRotation upload requires a rotationBuffer."); queue.writeBuffer(this.rotationBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceRotationBuffer(null, false); this._rotationWasmManaged = false; this._wasmRotationCapacity = 0; this.ensureWasmRotationBuffer(device, count); write(); } if (this._keepCPUData) this._rotationCPU = new Float32Array(data.subarray(0, count * SPLAT_VEC4_FLOATS)); else this._rotationCPU = null; this._wasmRotationDirty = false; } uploadWasmScale(device, queue) { const source = this._wasmScaleSource; if (!source || !this._wasmScaleDirty) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmScale"); const count = this._splatCount; validateWasmRecordRange(source, count, SPLAT_VEC4_FLOATS, "SplatField: wasmScale", "splatCount"); if (count <= 0) { this._wasmScaleDirty = false; return; } const data = source.array(); const byteLength = count * SPLAT_VEC4_BYTES; this.ensureWasmScaleBuffer(device, count); const write = () => { assert(!!this.scaleBuffer, "SplatField: wasmScale upload requires a scaleBuffer."); queue.writeBuffer(this.scaleBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceScaleBuffer(null, false); this._scaleWasmManaged = false; this._wasmScaleCapacity = 0; this.ensureWasmScaleBuffer(device, count); write(); } if (this._keepCPUData) this._scaleCPU = new Float32Array(data.subarray(0, count * SPLAT_VEC4_FLOATS)); else this._scaleCPU = null; this._wasmScaleDirty = false; } uploadWasmColor(device, queue) { const source = this._wasmColorSource; if (!source || !this._wasmColorDirty) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmColor"); const count = this._splatCount; validateWasmRecordRange(source, count, SPLAT_VEC4_FLOATS, "SplatField: wasmColor", "splatCount"); if (count <= 0) { this._wasmColorDirty = false; return; } const data = source.array(); const byteLength = count * SPLAT_VEC4_BYTES; this.ensureWasmColorBuffer(device, count); const write = () => { assert(!!this.colorBuffer, "SplatField: wasmColor upload requires a colorBuffer."); queue.writeBuffer(this.colorBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceColorBuffer(null, false); this._colorWasmManaged = false; this._wasmColorCapacity = 0; this.ensureWasmColorBuffer(device, count); write(); } if (this._keepCPUData) this._colorCPU = new Float32Array(data.subarray(0, count * SPLAT_VEC4_FLOATS)); else this._colorCPU = null; this._wasmColorDirty = false; } uploadWasmSphericalHarmonics(device, queue) { const source = this._wasmSphericalHarmonicsSource; if (!source || !this._wasmSphericalHarmonicsDirty) return; source.refresh(); assertWasmF32View(source, "SplatField: wasmSphericalHarmonics"); const count = this._splatCount; const floatsPerSplat = shFloatCount(this._shDegree); validateWasmRecordRange(source, count, floatsPerSplat, "SplatField: wasmSphericalHarmonics", "splatCount"); if (count <= 0) { this._wasmSphericalHarmonicsDirty = false; return; } const data = source.array(); const byteLength = count * floatsPerSplat * SPLAT_F32_BYTES; this.ensureWasmSphericalHarmonicsBuffer(device, count); const write = () => { assert(!!this.shBuffer, "SplatField: wasmSphericalHarmonics upload requires an shBuffer."); queue.writeBuffer(this.shBuffer, 0, data.buffer, data.byteOffset, byteLength); }; try { write(); } catch { this.replaceSHBuffer(null, false); this._sphericalHarmonicsWasmManaged = false; this._wasmSphericalHarmonicsCapacity = 0; this.ensureWasmSphericalHarmonicsBuffer(device, count); write(); } if (this._keepCPUData) this._shCPU = new Float32Array(data.subarray(0, count * floatsPerSplat)); else this._shCPU = null; this._wasmSphericalHarmonicsDirty = false; } uploadWasmFallbackColor(device, queue) { if (this._wasmColorSource || !this.hasExternalWasmSources() || !this._dataDirty) return; const count = this._splatCount; if (count <= 0) return; const data = makeWhiteColorData(count); this.ensureWasmColorBuffer(device, count); const write = () => { assert(!!this.colorBuffer, "SplatField: wasm fallback color upload requires a colorBuffer."); queue.writeBuffer(this.colorBuffer, 0, data.buffer, data.byteOffset, data.byteLength); }; try { write(); } catch { this.replaceColorBuffer(null, false); this._colorWasmManaged = false; this._wasmColorCapacity = 0; this.ensureWasmColorBuffer(device, count); write(); } if (this._keepCPUData) this._colorCPU = data; else this._colorCPU = null; } uploadWasmSources(device, queue) { this.uploadWasmCenterOpacity(device, queue); this.uploadWasmRotation(device, queue); this.uploadWasmScale(device, queue); this.uploadWasmColor(device, queue); this.uploadWasmSphericalHarmonics(device, queue); this.uploadWasmFallbackColor(device, queue); } upload(device, queue) { if (this.hasExternalWasmSources()) { if (this.hasDirtyWasmSources() || this._dataDirty) this.uploadWasmSources(device, queue); this._dataDirty = this.hasDirtyWasmSources(); return; } if (!this._dataDirty) return; const uploadBuffer = (current, data, label) => { if (!data || data.byteLength === 0) return { buffer: current, created: false }; const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; if (!current) return { buffer: createBuffer(device, data, usage, label), created: true }; try { queue.writeBuffer(current, 0, data.buffer, data.byteOffset, data.byteLength); return { buffer: current, created: false }; } catch { current.destroy(); return { buffer: createBuffer(device, data, usage, label), created: true }; } }; const centerOpacity = uploadBuffer(this.centerOpacityBuffer, this._centerOpacityCPU, "SplatField.centerOpacity"); const rotation = uploadBuffer(this.rotationBuffer, this._rotationCPU, "SplatField.rotation"); const scale = uploadBuffer(this.scaleBuffer, this._scaleCPU, "SplatField.scale"); const color = uploadBuffer(this.colorBuffer, this._colorCPU, "SplatField.color"); const sh = uploadBuffer(this.shBuffer, this._shCPU, "SplatField.sh"); this.centerOpacityBuffer = centerOpacity.buffer; this.rotationBuffer = rotation.buffer; this.scaleBuffer = scale.buffer; this.colorBuffer = color.buffer; this.shBuffer = sh.buffer; if (centerOpacity.created && this.centerOpacityBuffer) this._centerOpacityOwned = true; if (rotation.created && this.rotationBuffer) this._rotationOwned = true; if (scale.created && this.scaleBuffer) this._scaleOwned = true; if (color.created && this.colorBuffer) this._colorOwned = true; if (sh.created && this.shBuffer) this._shOwned = true; if (!this._keepCPUData) this.dropCPUData(); this._dataDirty = false; this.bindGroupKey = null; } getUniformBufferSize() { return UNIFORM_BYTE_SIZE4; } getUniformData() { const out = new Float32Array(UNIFORM_FLOAT_COUNT4); out[0] = clamp01(this._opacityScale); out[1] = this._externalColorBufferSrgb || this._usesSphericalHarmonics && this._colorSpace === "srgb" ? 1 : 0; out[2] = this._usesSphericalHarmonics ? 1 : 0; out[3] = this._shDegree; return out; } get dirtyUniforms() { return this._uniformDirty; } get sortRevision() { return this._sortRevision; } get sortCacheable() { return this._sortCacheable; } markUniformsClean() { this._uniformDirty = false; } destroyOwnedBuffer(buffer, owned) { if (!buffer || !owned) return; buffer.destroy(); } destroy() { this.destroyOwnedBuffer(this.centerOpacityBuffer, this._centerOpacityOwned); this.destroyOwnedBuffer(this.rotationBuffer, this._rotationOwned); this.destroyOwnedBuffer(this.scaleBuffer, this._scaleOwned); this.destroyOwnedBuffer(this.colorBuffer, this._colorOwned); this.destroyOwnedBuffer(this.shBuffer, this._shOwned); this.uniformBuffer?.destroy(); this.centerOpacityBuffer = null; this.rotationBuffer = null; this.scaleBuffer = null; this.colorBuffer = null; this.shBuffer = null; this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this._centerOpacityCPU = null; this._rotationCPU = null; this._scaleCPU = null; this._colorCPU = null; this._shCPU = null; this._ndShape = null; this._splatCount = 0; this._wasmCenterOpacitySource = null; this._wasmRotationSource = null; this._wasmScaleSource = null; this._wasmColorSource = null; this._wasmSphericalHarmonicsSource = null; this._wasmCenterOpacityDirty = false; this._wasmRotationDirty = false; this._wasmScaleDirty = false; this._wasmColorDirty = false; this._wasmSphericalHarmonicsDirty = false; this._centerOpacityWasmManaged = false; this._rotationWasmManaged = false; this._scaleWasmManaged = false; this._colorWasmManaged = false; this._sphericalHarmonicsWasmManaged = false; this._wasmCenterOpacityCapacity = 0; this._wasmRotationCapacity = 0; this._wasmScaleCapacity = 0; this._wasmColorCapacity = 0; this._wasmSphericalHarmonicsCapacity = 0; this._wasmCenterOpacityCapacityHint = 0; this._wasmRotationCapacityHint = 0; this._wasmScaleCapacityHint = 0; this._wasmColorCapacityHint = 0; this._wasmSphericalHarmonicsCapacityHint = 0; this._externalColorBufferSrgb = false; this._usesSphericalHarmonics = false; this._shDegree = 0; this.transform.dispose(); } }; // typescript/world/scene.ts var Scene = class _Scene { _meshes = []; _pointClouds = []; _glyphFields = []; _nodeLinks = []; _splatFields = []; _latticeSpaces = []; _lights = []; _background; static MAX_LIGHTS = 8; constructor(descriptor = {}) { this._background = descriptor.background ?? [0, 0, 0]; } get background() { return this._background; } set background(value) { this._background = value; } get meshes() { return this._meshes; } get pointClouds() { return this._pointClouds; } get glyphFields() { return this._glyphFields; } get nodeLinks() { return this._nodeLinks; } get splatFields() { return this._splatFields; } get latticeSpaces() { return this._latticeSpaces; } add(obj) { if (obj instanceof Mesh) { if (obj.destroyed) throw new Error("Scene: cannot add a destroyed mesh."); if (!this._meshes.includes(obj)) { this._meshes.push(obj); registerMeshSceneOwner(obj, this); } } else if (obj instanceof PointCloud) { if (!this._pointClouds.includes(obj)) this._pointClouds.push(obj); } else if (obj instanceof GlyphField) { if (!this._glyphFields.includes(obj)) this._glyphFields.push(obj); } else if (obj instanceof NodeLink) { if (!this._nodeLinks.includes(obj)) this._nodeLinks.push(obj); } else if (obj instanceof SplatField) { if (!this._splatFields.includes(obj)) this._splatFields.push(obj); } else { if (!this._latticeSpaces.includes(obj)) this._latticeSpaces.push(obj); } return this; } remove(obj) { if (obj instanceof Mesh) { const idx = this._meshes.indexOf(obj); if (idx !== -1) this._meshes.splice(idx, 1); unregisterMeshSceneOwner(obj, this); } else if (obj instanceof PointCloud) { const idx = this._pointClouds.indexOf(obj); if (idx !== -1) this._pointClouds.splice(idx, 1); } else if (obj instanceof GlyphField) { const idx = this._glyphFields.indexOf(obj); if (idx !== -1) this._glyphFields.splice(idx, 1); } else if (obj instanceof NodeLink) { const idx = this._nodeLinks.indexOf(obj); if (idx !== -1) this._nodeLinks.splice(idx, 1); } else if (obj instanceof SplatField) { const idx = this._splatFields.indexOf(obj); if (idx !== -1) this._splatFields.splice(idx, 1); } else { const idx = this._latticeSpaces.indexOf(obj); if (idx !== -1) this._latticeSpaces.splice(idx, 1); } return this; } clear() { for (const mesh of this._meshes) unregisterMeshSceneOwner(mesh, this); this._meshes = []; this._pointClouds = []; this._glyphFields = []; this._nodeLinks = []; this._splatFields = []; this._latticeSpaces = []; return this; } clearPointClouds() { this._pointClouds = []; return this; } clearGlyphFields() { this._glyphFields = []; return this; } clearNodeLinks() { this._nodeLinks = []; return this; } clearSplatFields() { this._splatFields = []; return this; } clearLatticeSpaces() { this._latticeSpaces = []; return this; } get lights() { return this._lights; } addLight(light) { if (!this._lights.includes(light)) { if (this._lights.length >= _Scene.MAX_LIGHTS && light.type !== "ambient") console.warn(`Scene: Maximum of ${_Scene.MAX_LIGHTS} non-ambient lights supported.`); this._lights.push(light); } return this; } removeLight(light) { const idx = this._lights.indexOf(light); if (idx !== -1) this._lights.splice(idx, 1); return this; } clearLights() { this._lights = []; return this; } findByName(name) { return this._meshes.find((m) => m.name === name); } findAllByName(name) { return this._meshes.filter((m) => m.name === name); } findPointCloudByName(name) { return this._pointClouds.find((p) => p.name === name); } findAllPointCloudsByName(name) { return this._pointClouds.filter((p) => p.name === name); } findGlyphFieldByName(name) { return this._glyphFields.find((g) => g.name === name); } findAllGlyphFieldsByName(name) { return this._glyphFields.filter((g) => g.name === name); } findNodeLinkByName(name) { return this._nodeLinks.find((n) => n.name === name); } findAllNodeLinksByName(name) { return this._nodeLinks.filter((n) => n.name === name); } findSplatFieldByName(name) { return this._splatFields.find((s) => s.name === name); } findAllSplatFieldsByName(name) { return this._splatFields.filter((s) => s.name === name); } findLatticeSpaceByName(name) { return this._latticeSpaces.find((s) => s.name === name); } findAllLatticeSpacesByName(name) { return this._latticeSpaces.filter((s) => s.name === name); } get visibleMeshes() { return this._meshes.filter((m) => m.visible); } get visiblePointClouds() { return this._pointClouds.filter((p) => p.visible); } get visibleGlyphFields() { return this._glyphFields.filter((g) => g.visible); } get visibleNodeLinks() { return this._nodeLinks.filter((n) => n.visible); } get visibleSplatFields() { return this._splatFields.filter((s) => s.visible); } get visibleLatticeSpaces() { return this._latticeSpaces.filter((s) => s.visible); } get enabledLights() { return this._lights.filter((l) => l.enabled); } getAmbientColor() { const ambient = this._lights.find((l) => l.type === "ambient" && l.enabled); if (ambient) { return [ ambient.color[0] * ambient.intensity, ambient.color[1] * ambient.intensity, ambient.color[2] * ambient.intensity ]; } return [0, 0, 0]; } getLightingData() { const ambient = this.getAmbientColor(); const lights = this.enabledLights.filter((l) => l.type !== "ambient").slice(0, _Scene.MAX_LIGHTS); return { ambient, lights }; } getBounds(options = {}) { const visibleOnly = options.visibleOnly ?? true; let aggregated = emptyBounds(false); const addBounds = (bounds) => { if (bounds.empty) { if (bounds.partial) aggregated.partial = true; return; } aggregated = unionBounds(aggregated, bounds); }; const meshes = visibleOnly ? this.visibleMeshes : this._meshes; const clouds = visibleOnly ? this.visiblePointClouds : this._pointClouds; const glyphs = visibleOnly ? this.visibleGlyphFields : this._glyphFields; const links = visibleOnly ? this.visibleNodeLinks : this._nodeLinks; const splats = visibleOnly ? this.visibleSplatFields : this._splatFields; const spaces = visibleOnly ? this.visibleLatticeSpaces : this._latticeSpaces; for (const mesh of meshes) addBounds(mesh.getWorldBounds()); for (const pointCloud of clouds) addBounds(pointCloud.getWorldBounds()); for (const glyphField of glyphs) addBounds(glyphField.getWorldBounds()); for (const nodeLink of links) addBounds(nodeLink.getWorldBounds()); for (const splatField of splats) addBounds(splatField.getWorldBounds()); for (const latticeSpace of spaces) addBounds(latticeSpace.getWorldBounds()); return aggregated; } traverse(callback) { for (const mesh of this._meshes) callback(mesh); } traverseVisible(callback) { for (const mesh of this._meshes) if (mesh.visible) callback(mesh); } traversePointClouds(callback) { for (const pc of this._pointClouds) callback(pc); } traverseVisiblePointClouds(callback) { for (const pc of this._pointClouds) if (pc.visible) callback(pc); } traverseGlyphFields(callback) { for (const g of this._glyphFields) callback(g); } traverseVisibleGlyphFields(callback) { for (const g of this._glyphFields) if (g.visible) callback(g); } traverseNodeLinks(callback) { for (const n of this._nodeLinks) callback(n); } traverseVisibleNodeLinks(callback) { for (const n of this._nodeLinks) if (n.visible) callback(n); } traverseSplatFields(callback) { for (const s of this._splatFields) callback(s); } traverseVisibleSplatFields(callback) { for (const s of this._splatFields) if (s.visible) callback(s); } traverseLatticeSpaces(callback) { for (const s of this._latticeSpaces) callback(s); } traverseVisibleLatticeSpaces(callback) { for (const s of this._latticeSpaces) if (s.visible) callback(s); } destroy() { const meshes = [...this._meshes]; for (const mesh of meshes) this.remove(mesh); for (const mesh of meshes) mesh.destroy(); for (const pc of this._pointClouds) pc.destroy(); for (const g of this._glyphFields) g.destroy(); for (const n of this._nodeLinks) n.destroy(); for (const s of this._splatFields) s.destroy(); for (const s of this._latticeSpaces) s.destroy(); this._meshes = []; this._pointClouds = []; this._glyphFields = []; this._nodeLinks = []; this._splatFields = []; this._latticeSpaces = []; this._lights = []; } }; // typescript/world/camera.ts var Camera = class _Camera { transform; type; _projectionMatrix = null; _viewMatrix = null; _viewProjectionMatrix = null; _projectionDirty = true; static _quatScratch = [0, 0, 0, 1]; static _posScratch = [0, 0, 0]; _viewMatrixArray = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; constructor(type) { this.type = type; this.transform = new Transform(); } get destroyed() { return this.transform.disposed; } writeViewMatrixToArray(out, offset = 0) { const q = this.transform.getWorldRotation(_Camera._quatScratch); const x = q[0], y = q[1], z = q[2], w = q[3]; const pos = this.transform.getWorldPosition(_Camera._posScratch); const tx = pos[0], ty = pos[1], tz = pos[2]; const xx = x * x, yy = y * y, zz = z * z; const xy = x * y, xz = x * z, yz = y * z; const wx = w * x, wy = w * y, wz = w * z; const m0 = 1 - 2 * (yy + zz); const m1 = 2 * (xy + wz); const m2 = 2 * (xz - wy); const m4 = 2 * (xy - wz); const m5 = 1 - 2 * (xx + zz); const m6 = 2 * (yz + wx); const m8 = 2 * (xz + wy); const m9 = 2 * (yz - wx); const m10 = 1 - 2 * (xx + yy); out[offset + 0] = m0; out[offset + 1] = m4; out[offset + 2] = m8; out[offset + 3] = 0; out[offset + 4] = m1; out[offset + 5] = m5; out[offset + 6] = m9; out[offset + 7] = 0; out[offset + 8] = m2; out[offset + 9] = m6; out[offset + 10] = m10; out[offset + 11] = 0; out[offset + 12] = -(m0 * tx + m1 * ty + m2 * tz); out[offset + 13] = -(m4 * tx + m5 * ty + m6 * tz); out[offset + 14] = -(m8 * tx + m9 * ty + m10 * tz); out[offset + 15] = 1; return out; } writeViewMatrixTo(outPtr) { const f32 = TransformStore.global().f32(); this.writeViewMatrixToArray(f32, outPtr >>> 2); } get viewMatrix() { this.writeViewMatrixToArray(this._viewMatrixArray); this._viewMatrix = this._viewMatrixArray; return this._viewMatrix; } get viewProjectionMatrix() { const proj = this.getProjectionMatrix(); const view = this.viewMatrix; this._viewProjectionMatrix = mat4.mul(proj, view); return this._viewProjectionMatrix; } get position() { return this.transform.worldPosition; } setWorldPosition(x, y, z) { const parent = this.transform.parent; if (!parent) { this.transform.setPosition(x, y, z); return this; } const m = parent.worldMatrix; const a = m[0], b = m[4], c = m[8]; const d = m[1], e = m[5], f = m[9]; const g = m[2], h = m[6], i = m[10]; const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); if (!Number.isFinite(det) || det === 0) return this; const dx = x - m[12]; const dy = y - m[13]; const dz = z - m[14]; const invDet = 1 / det; const lx = ((e * i - f * h) * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * invDet; const ly = ((f * g - d * i) * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * invDet; const lz = ((d * h - e * g) * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * invDet; if (Number.isFinite(lx) && Number.isFinite(ly) && Number.isFinite(lz)) this.transform.setPosition(lx, ly, lz); return this; } get up() { const q = this.transform.getWorldRotation(_Camera._quatScratch); const x = q[0], y = q[1], z = q[2], w = q[3]; const m4 = 2 * (x * y - w * z); const m5 = 1 - 2 * (x * x + z * z); const m6 = 2 * (y * z + w * x); return [m4, m5, m6]; } lookAt(xOrTarget, y, z) { const target = typeof xOrTarget === "number" ? [xOrTarget, y, z] : xOrTarget; return this.lookAtWithUp(target, [0, 1, 0]); } lookAtWithUp(target, up) { const eye = this.transform.worldPosition; const forward = vec3.normalize(vec3.sub(target, eye)); let upVec = [up[0], up[1], up[2]]; if (Math.abs(vec3.dot(forward, upVec)) > 0.999) { if (Math.abs(forward[1]) < 0.9) upVec = [0, 1, 0]; else upVec = [1, 0, 0]; } const right = vec3.normalize(vec3.cross(forward, upVec)); const correctedUp = vec3.cross(right, forward); const lookMatrix = [ right[0], right[1], right[2], 0, correctedUp[0], correctedUp[1], correctedUp[2], 0, -forward[0], -forward[1], -forward[2], 0, 0, 0, 0, 1 ]; const quat2 = _Camera.matrixToQuaternion(lookMatrix); const parent = this.transform.parent; if (!parent) this.transform.setRotation(quat2[0], quat2[1], quat2[2], quat2[3]); else { const p = parent.getWorldRotation(_Camera._quatScratch); const px = -p[0], py = -p[1], pz = -p[2], pw = p[3]; const qx = quat2[0], qy = quat2[1], qz = quat2[2], qw = quat2[3]; this.transform.setRotation(pw * qx + px * qw + py * qz - pz * qy, pw * qy - px * qz + py * qw + pz * qx, pw * qz + px * qy - py * qx + pz * qw, pw * qw - px * qx - py * qy - pz * qz); } return this; } static matrixToQuaternion(m) { const trace = m[0] + m[5] + m[10]; let qw, qx, qy, qz; if (trace > 0) { const s = 0.5 / Math.sqrt(trace + 1); qw = 0.25 / s; qx = (m[6] - m[9]) * s; qy = (m[8] - m[2]) * s; qz = (m[1] - m[4]) * s; } else if (m[0] > m[5] && m[0] > m[10]) { const s = 2 * Math.sqrt(1 + m[0] - m[5] - m[10]); qw = (m[6] - m[9]) / s; qx = 0.25 * s; qy = (m[4] + m[1]) / s; qz = (m[8] + m[2]) / s; } else if (m[5] > m[10]) { const s = 2 * Math.sqrt(1 + m[5] - m[0] - m[10]); qw = (m[8] - m[2]) / s; qx = (m[4] + m[1]) / s; qy = 0.25 * s; qz = (m[9] + m[6]) / s; } else { const s = 2 * Math.sqrt(1 + m[10] - m[0] - m[5]); qw = (m[1] - m[4]) / s; qx = (m[8] + m[2]) / s; qy = (m[9] + m[6]) / s; qz = 0.25 * s; } return [qx, qy, qz, qw]; } markProjectionDirty() { this._projectionDirty = true; } destroy() { this.transform.dispose(); } }; var PerspectiveCamera = class extends Camera { _fov; _aspect; _autoAspect; _near; _far; constructor(descriptor = {}) { super("perspective"); this._fov = descriptor.fov ?? 60; this._aspect = descriptor.aspect ?? 16 / 9; this._autoAspect = descriptor.autoAspect ?? true; this._near = descriptor.near ?? 0.1; this._far = descriptor.far ?? 1e3; } get fov() { return this._fov; } set fov(value) { if (value === this._fov) return; this._fov = value; this.markProjectionDirty(); } get aspect() { return this._aspect; } set aspect(value) { if (value === this._aspect) return; this._aspect = value; this.markProjectionDirty(); } get autoAspect() { return this._autoAspect; } set autoAspect(value) { if (value === this._autoAspect) return; this._autoAspect = value; } get near() { return this._near; } set near(value) { if (value === this._near) return; this._near = value; this.markProjectionDirty(); } get far() { return this._far; } set far(value) { if (value === this._far) return; this._far = value; this.markProjectionDirty(); } updateAspect(width, height) { this._aspect = width / height; this.markProjectionDirty(); return this; } getProjectionMatrix() { if (this._projectionDirty || !this._projectionMatrix) { const fovRad = this._fov * Math.PI / 180; this._projectionMatrix = mat4.perspective(fovRad, this._aspect, this._near, this._far); this._projectionDirty = false; } return this._projectionMatrix; } }; var OrthographicCamera = class extends Camera { _left; _right; _top; _bottom; _near; _far; constructor(descriptor = {}) { super("orthographic"); this._left = descriptor.left ?? -10; this._right = descriptor.right ?? 10; this._top = descriptor.top ?? 10; this._bottom = descriptor.bottom ?? -10; this._near = descriptor.near ?? 0.1; this._far = descriptor.far ?? 1e3; } get left() { return this._left; } set left(value) { if (value === this._left) return; this._left = value; this.markProjectionDirty(); } get right() { return this._right; } set right(value) { if (value === this._right) return; this._right = value; this.markProjectionDirty(); } get top() { return this._top; } set top(value) { if (value === this._top) return; this._top = value; this.markProjectionDirty(); } get bottom() { return this._bottom; } set bottom(value) { if (value === this._bottom) return; this._bottom = value; this.markProjectionDirty(); } get near() { return this._near; } set near(value) { if (value === this._near) return; this._near = value; this.markProjectionDirty(); } get far() { return this._far; } set far(value) { if (value === this._far) return; this._far = value; this.markProjectionDirty(); } updateFromCanvas(width, height, zoom = 1) { const halfWidth = width / 2 / zoom; const halfHeight = height / 2 / zoom; this._left = -halfWidth; this._right = halfWidth; this._top = halfHeight; this._bottom = -halfHeight; this.markProjectionDirty(); return this; } getProjectionMatrix() { if (this._projectionDirty || !this._projectionMatrix) { this._projectionMatrix = this.computeOrthographicMatrix(); this._projectionDirty = false; } return this._projectionMatrix; } computeOrthographicMatrix() { const lr = 1 / (this._left - this._right); const bt = 1 / (this._bottom - this._top); const nf = 1 / (this._near - this._far); return [ -2 * lr, 0, 0, 0, 0, -2 * bt, 0, 0, 0, 0, nf, 0, (this._left + this._right) * lr, (this._top + this._bottom) * bt, this._near * nf, 1 ]; } }; // typescript/world/light.ts var normalizeDirection = (value) => { const len = Math.sqrt(value[0] ** 2 + value[1] ** 2 + value[2] ** 2); if (len <= 0) return [0, -1, 0]; return [value[0] / len, value[1] / len, value[2] / len]; }; var Light = class { type; _color = [1, 1, 1]; _intensity = 1; _enabled = true; constructor(type) { this.type = type; } get color() { return this._color; } set color(value) { this._color = value; } get intensity() { return this._intensity; } set intensity(value) { this._intensity = value; } get enabled() { return this._enabled; } set enabled(value) { this._enabled = value; } }; var lightTransforms = /* @__PURE__ */ new WeakMap(); var bindLightToTransform = (light, transform) => { lightTransforms.set(light, transform); }; var unbindLightTransform = (light) => { lightTransforms.delete(light); }; var getBoundTransform = (light) => { const transform = lightTransforms.get(light); if (!transform || transform.disposed) return null; return transform; }; var resolveBoundPosition = (light, fallback) => { const transform = getBoundTransform(light); if (!transform) return fallback; const position = transform.worldPosition; return [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0]; }; var resolveBoundDirection = (light, fallback) => { const transform = getBoundTransform(light); if (!transform) return fallback; const wm = transform.worldMatrix; return normalizeDirection([-(wm[8] ?? 0), -(wm[9] ?? 0), -(wm[10] ?? -1)]); }; var resolveLightPosition = (light) => light.position; var resolveLightDirection = (light) => light.direction; var AmbientLight = class extends Light { constructor(descriptor = {}) { super("ambient"); this._color = descriptor.color ?? [1, 1, 1]; this._intensity = descriptor.intensity ?? 0.1; } }; var DirectionalLight = class extends Light { _direction; constructor(descriptor = {}) { super("directional"); this._direction = descriptor.direction ?? [0, -1, 0]; this._color = descriptor.color ?? [1, 1, 1]; this._intensity = descriptor.intensity ?? 1; } get direction() { return resolveBoundDirection(this, this._direction); } set direction(value) { this._direction = normalizeDirection(value); } }; var PointLight = class extends Light { _position; _range; constructor(descriptor = {}) { super("point"); this._position = descriptor.position ?? [0, 0, 0]; this._color = descriptor.color ?? [1, 1, 1]; this._intensity = descriptor.intensity ?? 1; this._range = descriptor.range ?? 10; } get position() { return resolveBoundPosition(this, this._position); } set position(value) { this._position = value; } get range() { return this._range; } set range(value) { this._range = value; } }; var SpotLight = class extends Light { _position; _direction; _range; _innerCone; _outerCone; constructor(descriptor = {}) { super("spot"); this._position = descriptor.position ?? [0, 0, 0]; this._direction = normalizeDirection(descriptor.direction ?? [0, -1, 0]); this._color = descriptor.color ?? [1, 1, 1]; this._intensity = descriptor.intensity ?? 1; this._range = descriptor.range ?? 10; this._innerCone = descriptor.innerCone ?? Math.PI / 8; this._outerCone = descriptor.outerCone ?? Math.PI / 6; if (this._innerCone > this._outerCone) this._innerCone = this._outerCone; } get position() { return resolveBoundPosition(this, this._position); } set position(value) { this._position = value; } get direction() { return resolveBoundDirection(this, this._direction); } set direction(value) { this._direction = normalizeDirection(value); } get range() { return this._range; } set range(value) { this._range = value; } get innerCone() { return this._innerCone; } set innerCone(value) { this._innerCone = Math.max(0, Math.min(value, this._outerCone)); } get outerCone() { return this._outerCone; } set outerCone(value) { this._outerCone = Math.max(0, value); if (this._innerCone > this._outerCone) this._innerCone = this._outerCone; } }; // typescript/effects/shadows.ts var shadowStates = /* @__PURE__ */ new WeakMap(); var finiteNonNegative = (value, label) => { if (!Number.isFinite(value) || value < 0) throw new Error(`ShadowSystem: ${label} must be a finite non-negative number.`); return value; }; var finitePositive = (value, label) => { if (!Number.isFinite(value) || value <= 0) throw new Error(`ShadowSystem: ${label} must be a finite positive number.`); return value; }; var finiteF32 = (value, label) => { if (!Number.isFinite(value) || !Number.isFinite(Math.fround(value))) throw new Error(`ShadowSystem: ${label} must be representable as a finite 32-bit float.`); return value; }; var depthBiasInteger = (value) => { if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) throw new Error("ShadowSystem: depthBias must be a signed 32-bit integer."); return value; }; var positiveInteger = (value, label, maximum) => { if (!Number.isInteger(value) || value <= 0) throw new Error(`ShadowSystem: ${label} must be a positive integer.`); if (value > maximum) throw new Error(`ShadowSystem: ${label} ${value} exceeds the active device limit ${maximum}.`); return value; }; var validateFilter = (value) => { if (value !== "hard" && value !== "pcf") throw new Error(`ShadowSystem: unsupported filter '${String(value)}'.`); return value; }; var validateUpdateMode = (value) => { if (value !== "always" && value !== "manual") throw new Error(`ShadowSystem: unsupported update mode '${String(value)}'.`); return value; }; var resolveVolume = (volume) => { if (!volume) return null; if (!Array.isArray(volume.center) || volume.center.length !== 3 || volume.center.some((component) => !Number.isFinite(component))) throw new Error("ShadowSystem: volume.center must contain three finite numbers."); const width = finitePositive(volume.width, "volume.width"); return { center: [volume.center[0], volume.center[1], volume.center[2]], width, height: finitePositive(volume.height ?? width, "volume.height"), depth: finitePositive(volume.depth ?? width * 2, "volume.depth") }; }; var publicConfiguration = (state) => ({ bias: state.bias, normalBias: state.normalBias, distance: state.distance, updateMode: state.updateMode, volume: state.volume ? { center: [state.volume.center[0], state.volume.center[1], state.volume.center[2]], width: state.volume.width, height: state.volume.height, depth: state.volume.depth } : null }); var ShadowSystem = class { _mapSize = 1024; _maxViews = 4; _filter = "pcf"; _depthBias = 1; _depthBiasSlopeScale = 1.5; _depthBiasClamp = 25e-4; _revision = 0; _maxMapSize = 8192; _maxArrayLayers = 256; constructor(descriptor = {}) { shadowStates.set(this, /* @__PURE__ */ new Map()); if (descriptor.mapSize !== void 0) this._mapSize = positiveInteger(descriptor.mapSize, "mapSize", this._maxMapSize); if (descriptor.maxViews !== void 0) this._maxViews = positiveInteger(descriptor.maxViews, "maxViews", this._maxArrayLayers); if (descriptor.filter !== void 0) this._filter = validateFilter(descriptor.filter); if (descriptor.depthBias !== void 0) this._depthBias = depthBiasInteger(descriptor.depthBias); if (descriptor.depthBiasSlopeScale !== void 0) this._depthBiasSlopeScale = finiteF32(descriptor.depthBiasSlopeScale, "depthBiasSlopeScale"); if (descriptor.depthBiasClamp !== void 0) this._depthBiasClamp = finiteF32(descriptor.depthBiasClamp, "depthBiasClamp"); } get mapSize() { return this._mapSize; } set mapSize(value) { const next = positiveInteger(value, "mapSize", this._maxMapSize); if (next === this._mapSize) return; this._mapSize = next; this.markConfigurationChanged(); } get maxViews() { return this._maxViews; } set maxViews(value) { const next = positiveInteger(value, "maxViews", this._maxArrayLayers); if (next === this._maxViews) return; this._maxViews = next; this.markConfigurationChanged(); } get filter() { return this._filter; } set filter(value) { const next = validateFilter(value); if (next === this._filter) return; this._filter = next; this._revision++; } get depthBias() { return this._depthBias; } set depthBias(value) { const next = depthBiasInteger(value); if (next === this._depthBias) return; this._depthBias = next; this.markConfigurationChanged(); } get depthBiasSlopeScale() { return this._depthBiasSlopeScale; } set depthBiasSlopeScale(value) { const next = finiteF32(value, "depthBiasSlopeScale"); if (next === this._depthBiasSlopeScale) return; this._depthBiasSlopeScale = next; this.markConfigurationChanged(); } get depthBiasClamp() { return this._depthBiasClamp; } set depthBiasClamp(value) { const next = finiteF32(value, "depthBiasClamp"); if (next === this._depthBiasClamp) return; this._depthBiasClamp = next; this.markConfigurationChanged(); } get revision() { return this._revision; } enable(light, descriptor = {}) { if (!(light instanceof DirectionalLight)) throw new Error("ShadowSystem.enable: only DirectionalLight is supported."); const states = shadowStates.get(this); const previous = states.get(light); const updateMode = validateUpdateMode(descriptor.updateMode ?? previous?.updateMode ?? "always"); const volume = descriptor.volume === void 0 ? previous?.volume ?? null : resolveVolume(descriptor.volume); states.set(light, { bias: finiteNonNegative(descriptor.bias ?? previous?.bias ?? 5e-4, "bias"), normalBias: finiteNonNegative(descriptor.normalBias ?? previous?.normalBias ?? 0.02, "normalBias"), distance: finitePositive(descriptor.distance ?? previous?.distance ?? 100, "distance"), updateMode, volume: volume ? resolveVolume(volume) : null, dirty: true }); this._revision++; } disable(light) { const removed = shadowStates.get(this).delete(light); if (removed) this._revision++; return removed; } isEnabled(light) { return shadowStates.get(this).has(light); } get(light) { const state = shadowStates.get(this).get(light); return state ? publicConfiguration(state) : null; } needsUpdate(light) { return shadowStates.get(this).get(light)?.dirty ?? false; } requestUpdate(light) { const states = shadowStates.get(this); if (light) { const shadow = states.get(light); if (shadow) shadow.dirty = true; return; } for (const shadow of states.values()) shadow.dirty = true; } destroy() { const states = shadowStates.get(this); if (states.size === 0) return; states.clear(); this._revision++; } markConfigurationChanged() { this._revision++; for (const shadow of shadowStates.get(this).values()) shadow.dirty = true; } }; var getShadowRuntimeState = (system, light) => shadowStates.get(system).get(light) ?? null; var setShadowRuntimeClean = (system, light) => { const state = shadowStates.get(system).get(light); if (state) state.dirty = false; }; var setShadowDeviceLimits = (system, maxMapSize, maxArrayLayers) => { positiveInteger(system.mapSize, "mapSize", maxMapSize); positiveInteger(system.maxViews, "maxViews", maxArrayLayers); system["_maxMapSize"] = maxMapSize; system["_maxArrayLayers"] = maxArrayLayers; }; // typescript/effects/index.ts var RenderEffects = class { shadows; constructor(descriptor = {}) { this.shadows = new ShadowSystem(descriptor.shadows); } destroy() { this.shadows.destroy(); } }; // wgsl/world/pointcloud.wgsl var pointcloud_default = "struct PointData { position: vec3, scalar: f32, } struct PointCloudUniforms { size_params: vec4, scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, visual: vec4, colors: array, 8>, } struct VertexOutput { @builtin(position) position: vec4, @location(0) col: vec4, @location(1) point_coord: vec2, } struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var points: array; @group(1) @binding(1) var pc: PointCloudUniforms; @group(1) @binding(2) var colormap_sampler: sampler; @group(1) @binding(3) var colormap_tex: texture_1d; @group(1) @binding(4) var point_colors: array>; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } fn srgb_from_linear(linear: vec3) -> vec3 { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3(1.0 / 2.4)) - vec3(a); let use_hi = linear > vec3(0.0031308); return select(lo, hi, use_hi); } fn sample_custom_stops(t: f32, stop_count: u32) -> vec4 { let n = min(stop_count, 8u); let x = scale_clamp01(t) * f32(n - 1u); let i = u32(floor(x)); let f = x - f32(i); if (i >= n - 1u) { return pc.colors[n - 1u]; } return pc.colors[i] + f * (pc.colors[i + 1u] - pc.colors[i]); } fn colormap(t_in: f32) -> vec4 { let t = scale_clamp01(t_in); let stop_count = u32(pc.visual.z + 0.5); if (stop_count >= 2u) { return sample_custom_stops(t, stop_count); } return textureSampleLevel(colormap_tex, colormap_sampler, t, 0.0); } fn vec4_component(v: vec4, idx: u32) -> f32 { if (idx == 0u) { return v.x; } if (idx == 1u) { return v.y; } if (idx == 2u) { return v.z; } return v.w; } fn shifted_value_vector(v: vec4, offset_floats: f32) -> vec4 { let o = min(3u, u32(offset_floats + 0.5)); let i0 = min(3u, o + 0u); let i1 = min(3u, o + 1u); let i2 = min(3u, o + 2u); let i3 = min(3u, o + 3u); return vec4( vec4_component(v, i0), vec4_component(v, i1), vec4_component(v, i2), vec4_component(v, i3), ); } @vertex fn vs_main( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let p = points[instance_index]; let world_pos = model.model * vec4(p.position, 1.0); let clip = camera.view_proj * world_pos; let base_size = pc.size_params.x; let min_size = pc.size_params.y; let max_size = pc.size_params.z; let atten = pc.size_params.w; var size_px = base_size; if (atten > 0.0) { let dist = distance(camera.position, world_pos.xyz); size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); let uv = vec2( f32((vertex_index + 2u) / 3u % 2u), f32((vertex_index + 1u) / 3u % 2u), ); let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; let color_mode = u32(pc.visual.w + 0.5); var c: vec4; if (color_mode == 0u) { c = point_colors[instance_index]; } else { let raw_vec = shifted_value_vector(vec4(p.position, p.scalar), pc.scale_domain.z); let component_count = u32(pc.scale_source.x + 0.5); let component_index = u32(pc.scale_source.y + 0.5); let value_mode = u32(pc.scale_source.z + 0.5); let raw_value = scale_select_value(raw_vec, component_count, component_index, value_mode); let finite_raw = scale_is_finite(raw_value); var t = scale_apply_transform( raw_value, vec4(pc.scale_domain.x, pc.scale_domain.y, 0.0, pc.scale_domain.w), pc.scale_clamp, pc.scale_params, pc.scale_flags, ); c = colormap(t); if (!finite_raw) { c = vec4(0.0, 0.0, 0.0, 0.0); } } let alpha = scale_clamp01(c.a) * scale_clamp01(pc.visual.x); var out: VertexOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.point_coord = uv * 2.0 - vec2(1.0, 1.0); out.col = vec4(srgb_from_linear(max(c.rgb, vec3(0.0))), alpha); return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let uv = in.point_coord; let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } let falloff = (1.0 - r2); let alpha = falloff * falloff; return vec4(in.col.rgb, in.col.a * alpha); }"; // wgsl/world/glyphfield.wgsl var glyphfield_default = "struct GlyphFieldUniforms { scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, visual: vec4, solid_color: vec4, colors: array, 8>, } struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) @interpolate(flat) attrib: vec4, } struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct Light { position: vec4, color: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var positions: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; @group(1) @binding(3) var attributes: array>; @group(1) @binding(4) var glyph: GlyphFieldUniforms; @group(1) @binding(5) var colormap_sampler: sampler; @group(1) @binding(6) var colormap_tex: texture_1d; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } fn srgb_from_linear(linear: vec3) -> vec3 { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3(1.0 / 2.4)) - vec3(a); let use_hi = linear > vec3(0.0031308); return select(lo, hi, use_hi); } fn rotate_by_quat(v: vec3, q: vec4) -> vec3 { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } fn sample_custom_stops(t: f32) -> vec4 { let count = u32(glyph.visual.y + 0.5); if (count <= 1u) { return glyph.colors[0u]; } let n = min(count, 8u); let x = scale_clamp01(t) * f32(n - 1u); let i = u32(floor(x)); let f = x - f32(i); if (i >= n - 1u) { return glyph.colors[n - 1u]; } return glyph.colors[i] + f * (glyph.colors[i + 1u] - glyph.colors[i]); } fn colormap(t_in: f32) -> vec4 { let t = scale_clamp01(t_in); let stop_count = u32(glyph.visual.y + 0.5); if (stop_count >= 2u) { return sample_custom_stops(t); } return textureSample(colormap_tex, colormap_sampler, t); } fn apply_lighting(world_pos: vec3, n: vec3, base_color: vec3) -> vec3 { var lo = lighting.ambient.rgb * base_color; let light_count = min(lighting.light_count, 8u); for (var i = 0u; i < light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - world_pos; let distance = length(light_dir); l = select(vec3(0.0, 1.0, 0.0), light_dir / distance, distance > 1e-6); attenuation = 1.0 / max(distance * distance, 1e-6); let range = light.params.x; if (range > 0.0) { let f = scale_clamp01(1.0 - distance / range); attenuation *= f * f; } } let n_dot_l = max(dot(n, l), 0.0); let radiance = light.color.rgb * light.color.a * attenuation; lo += base_color * radiance * n_dot_l; } return lo; } fn vec4_component(v: vec4, idx: u32) -> f32 { if (idx == 0u) { return v.x; } if (idx == 1u) { return v.y; } if (idx == 2u) { return v.z; } return v.w; } fn shifted_value_vector(v: vec4, offset_floats: f32) -> vec4 { let o = min(3u, u32(offset_floats + 0.5)); let i0 = min(3u, o + 0u); let i1 = min(3u, o + 1u); let i2 = min(3u, o + 2u); let i3 = min(3u, o + 3u); return vec4( vec4_component(v, i0), vec4_component(v, i1), vec4_component(v, i2), vec4_component(v, i3), ); } @vertex fn vs_main(in: VertexInput, @builtin(instance_index) instance_index: u32) -> VertexOutput { let p4 = positions[instance_index]; let q = rotations[instance_index]; let s4 = scales[instance_index]; let a4 = attributes[instance_index]; let scl = s4.xyz; let local_pos = rotate_by_quat(in.position * scl, q) + p4.xyz; let world_pos4 = model.model * vec4(local_pos, 1.0); let world_pos = world_pos4.xyz; let inv_scale = 1.0 / max(abs(scl), vec3(1e-6)); let local_n = in.normal * inv_scale; let inst_n = rotate_by_quat(local_n, q); let world_n = normalize((model.normal * vec4(inst_n, 0.0)).xyz); var out: VertexOutput; out.position = camera.view_proj * vec4(world_pos, 1.0); out.world_pos = world_pos; out.normal = world_n; out.attrib = a4; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let color_mode = u32(round(glyph.visual.z)); let lit = glyph.visual.w > 0.5; var base_color: vec3; var alpha: f32 = 1.0; if (color_mode == 0u) { base_color = in.attrib.rgb; alpha = in.attrib.a; } else if (color_mode == 1u) { let shifted = shifted_value_vector(in.attrib, glyph.scale_domain.z); let component_count = u32(glyph.scale_source.x + 0.5); let component_index = u32(glyph.scale_source.y + 0.5); let value_mode = u32(glyph.scale_source.z + 0.5); let raw_value = scale_select_value(shifted, component_count, component_index, value_mode); if (!scale_is_finite(raw_value)) { discard; } let t = scale_apply_transform( raw_value, vec4(glyph.scale_domain.x, glyph.scale_domain.y, 0.0, glyph.scale_domain.w), glyph.scale_clamp, glyph.scale_params, glyph.scale_flags, ); let cmap = colormap(t); base_color = cmap.rgb; alpha = cmap.a; } else { base_color = glyph.solid_color.rgb; alpha = glyph.solid_color.a; } base_color = max(base_color, vec3(0.0)); alpha = scale_clamp01(alpha) * scale_clamp01(glyph.visual.x); var shaded = base_color; if (lit) { shaded = apply_lighting(in.world_pos, normalize(in.normal), base_color); } return vec4(srgb_from_linear(shaded), alpha); }"; // wgsl/world/nodelink.wgsl var nodelink_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct Light { position: vec4, color: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array, } struct NodeLinkUniforms { global: vec4, node_scale_source: vec4, node_scale_domain: vec4, node_scale_clamp: vec4, node_scale_params: vec4, node_scale_flags: vec4, node_visual: vec4, edge_scale_source: vec4, edge_scale_domain: vec4, edge_scale_clamp: vec4, edge_scale_params: vec4, edge_scale_flags: vec4, edge_visual: vec4, node_solid: vec4, edge_solid: vec4, point_params: vec4, node_stops: array, 8>, edge_stops: array, 8>, } struct NodeVertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) @interpolate(flat) node_index: u32, @location(3) point_coord: vec2, @location(4) @interpolate(flat) is_point: f32, } struct EdgeVertexOutput { @builtin(position) position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) @interpolate(flat) edge_index: u32, @location(3) @interpolate(flat) lit_enabled: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var node_positions: array>; @group(1) @binding(1) var node_scalars: array; @group(1) @binding(2) var node_colors: array>; @group(1) @binding(3) var node_radii: array>; @group(1) @binding(4) var edges: array>; @group(1) @binding(5) var edge_scalars: array; @group(1) @binding(6) var edge_colors: array>; @group(1) @binding(7) var nl: NodeLinkUniforms; @group(1) @binding(8) var node_colormap_sampler: sampler; @group(1) @binding(9) var node_colormap_tex: texture_1d; @group(1) @binding(10) var edge_colormap_sampler: sampler; @group(1) @binding(11) var edge_colormap_tex: texture_1d; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } fn srgb_from_linear(linear: vec3) -> vec3 { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3(1.0 / 2.4)) - vec3(a); let use_hi = linear > vec3(0.0031308); return select(lo, hi, use_hi); } fn sample_custom_stops(t_in: f32, stops: array, 8>, stop_count_in: u32) -> vec4 { let n = min(8u, max(2u, stop_count_in)); let x = scale_clamp01(t_in) * f32(n - 1u); let i = u32(floor(x)); let f = x - f32(i); if (i >= n - 1u) { return stops[n - 1u]; } return stops[i] + f * (stops[i + 1u] - stops[i]); } fn sample_node_colormap(t: f32) -> vec4 { let stop_count = u32(nl.node_visual.y + 0.5); if (stop_count >= 2u) { return sample_custom_stops(t, nl.node_stops, stop_count); } return textureSampleLevel(node_colormap_tex, node_colormap_sampler, scale_clamp01(t), 0.0); } fn sample_edge_colormap(t: f32) -> vec4 { let stop_count = u32(nl.edge_visual.y + 0.5); if (stop_count >= 2u) { return sample_custom_stops(t, nl.edge_stops, stop_count); } return textureSampleLevel(edge_colormap_tex, edge_colormap_sampler, scale_clamp01(t), 0.0); } fn node_color(index: u32) -> vec4 { let mode = u32(round(nl.node_visual.x)); if (mode == 0u) { return node_colors[index]; } if (mode == 1u) { let raw_value = node_scalars[index]; if (!scale_is_finite(raw_value)) { return vec4(0.0, 0.0, 0.0, 0.0); } let t = scale_apply_transform( raw_value, vec4(nl.node_scale_domain.x, nl.node_scale_domain.y, 0.0, nl.node_scale_domain.w), nl.node_scale_clamp, nl.node_scale_params, nl.node_scale_flags, ); return sample_node_colormap(t); } return nl.node_solid; } fn edge_color(index: u32) -> vec4 { let mode = u32(round(nl.edge_visual.x)); if (mode == 0u) { return edge_colors[index]; } if (mode == 1u) { let raw_value = edge_scalars[index]; if (!scale_is_finite(raw_value)) { return vec4(0.0, 0.0, 0.0, 0.0); } let t = scale_apply_transform( raw_value, vec4(nl.edge_scale_domain.x, nl.edge_scale_domain.y, 0.0, nl.edge_scale_domain.w), nl.edge_scale_clamp, nl.edge_scale_params, nl.edge_scale_flags, ); return sample_edge_colormap(t); } return nl.edge_solid; } fn apply_lighting(world_pos: vec3, n: vec3, base_color: vec3) -> vec3 { var lo = lighting.ambient.rgb * base_color; let light_count = min(lighting.light_count, 8u); for (var i = 0u; i < light_count; i++) { let light = lighting.lights[i]; var l: vec3; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { l = normalize(-light.position.xyz); } else { let light_dir = light.position.xyz - world_pos; let distance = length(light_dir); l = select(vec3(0.0, 1.0, 0.0), light_dir / distance, distance > 1e-6); attenuation = 1.0 / max(distance * distance, 1e-6); let range = light.params.x; if (range > 0.0) { let f = scale_clamp01(1.0 - distance / range); attenuation *= f * f; } } let n_dot_l = max(dot(n, l), 0.0); let radiance = light.color.rgb * light.color.a * attenuation; lo += base_color * radiance * n_dot_l; } return lo; } fn build_edge_frame(src: vec3, dst: vec3) -> mat3x3 { let y_axis = normalize(dst - src); var z = vec3(0.0, 0.0, 1.0); if (abs(dot(z, y_axis)) > 0.99) { z = vec3(1.0, 0.0, 0.0); } let x_axis = normalize(cross(z, y_axis)); let z_axis = normalize(cross(y_axis, x_axis)); return mat3x3(x_axis, y_axis, z_axis); } @vertex fn vs_node_points( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> NodeVertexOutput { let p = node_positions[instance_index].xyz; let world_pos4 = model.model * vec4(p, 1.0); let clip = camera.view_proj * world_pos4; let base_size = nl.global.x; let min_size = nl.point_params.x; let max_size = nl.point_params.y; let atten = nl.point_params.z; var size_px = base_size; if (atten > 0.0) { let dist = distance(camera.position, world_pos4.xyz); size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); let uv = vec2(f32((vertex_index + 2u) / 3u % 2u), f32((vertex_index + 1u) / 3u % 2u)); let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; var out: NodeVertexOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.world_pos = world_pos4.xyz; out.normal = vec3(0.0, 0.0, 1.0); out.node_index = instance_index; out.point_coord = uv * 2.0 - vec2(1.0, 1.0); out.is_point = 1.0; return out; } @vertex fn vs_node_solid( @location(0) position: vec3, @location(1) normal: vec3, @builtin(instance_index) instance_index: u32, ) -> NodeVertexOutput { let center = node_positions[instance_index].xyz; let mode = u32(round(nl.node_visual.z)); let use_radii = nl.node_visual.w > 0.5; var scale_vec = vec3(max(nl.global.x, 1e-6)); if (use_radii) { let rv = max(node_radii[instance_index].xyz, vec3(1e-6)); if (mode == 2u) { scale_vec = rv * max(nl.global.x, 1e-6); } else { scale_vec = vec3(rv.x * max(nl.global.x, 1e-6)); } } let obj_pos = center + (position * scale_vec); let world_pos4 = model.model * vec4(obj_pos, 1.0); let local_n = normalize(normal / scale_vec); let world_n = normalize((model.normal * vec4(local_n, 0.0)).xyz); var out: NodeVertexOutput; out.position = camera.view_proj * world_pos4; out.world_pos = world_pos4.xyz; out.normal = world_n; out.node_index = instance_index; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @vertex fn vs_edge_lines( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> EdgeVertexOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let obj_pos = select(src, dst, (vertex_index & 1u) == 1u); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: EdgeVertexOutput; out.position = camera.view_proj * world_pos4; out.world_pos = world_pos4.xyz; out.normal = vec3(0.0, 1.0, 0.0); out.edge_index = instance_index; out.lit_enabled = 0.0; return out; } @vertex fn vs_edge_cylinders( @location(0) position: vec3, @location(1) normal: vec3, @builtin(instance_index) instance_index: u32, ) -> EdgeVertexOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let seg = dst - src; let seg_len = max(length(seg), 1e-6); let basis = build_edge_frame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3(position.x * radius, position.y * seg_len, position.z * radius); let obj_pos = ((src + dst) * 0.5) + (basis * local); let world_pos4 = model.model * vec4(obj_pos, 1.0); let local_n = normalize(basis * vec3(normal.x, 0.0, normal.z)); let world_n = normalize((model.normal * vec4(local_n, 0.0)).xyz); var out: EdgeVertexOutput; out.position = camera.view_proj * world_pos4; out.world_pos = world_pos4.xyz; out.normal = world_n; out.edge_index = instance_index; out.lit_enabled = 1.0; return out; } @fragment fn fs_node(in: NodeVertexOutput) -> @location(0) vec4 { var c = node_color(in.node_index); if (in.is_point > 0.5) { let r2 = dot(in.point_coord, in.point_coord); if (r2 > 1.0) { discard; } let falloff = (1.0 - r2); c = vec4(c.rgb, c.a * (falloff * falloff)); } else if (nl.global.w > 0.5) { let lit_rgb = apply_lighting( in.world_pos, normalize(in.normal), max(c.rgb, vec3(0.0)), ); c = vec4(lit_rgb, c.a); } c = vec4(c.rgb, c.a * scale_clamp01(nl.global.z)); return vec4(srgb_from_linear(max(c.rgb, vec3(0.0))), c.a); } @fragment fn fs_edge(in: EdgeVertexOutput) -> @location(0) vec4 { var c = edge_color(in.edge_index); if (nl.global.w > 0.5 && in.lit_enabled > 0.5) { let lit_rgb = apply_lighting( in.world_pos, normalize(in.normal), max(c.rgb, vec3(0.0)), ); c = vec4(lit_rgb, c.a); } c = vec4(c.rgb, c.a * scale_clamp01(nl.global.z)); return vec4(srgb_from_linear(max(c.rgb, vec3(0.0))), c.a); }"; // wgsl/world/splatfield.wgsl var splatfield_default = "const SH_C0: f32 = 0.28209479177387814; const SH_C1: f32 = 0.4886025119029199; const SH_C2_0: f32 = 1.0925484305920792; const SH_C2_1: f32 = -1.0925484305920792; const SH_C2_2: f32 = 0.31539156525252005; const SH_C2_3: f32 = -1.0925484305920792; const SH_C2_4: f32 = 0.5462742152960396; const SH_C3_0: f32 = -0.5900435899266435; const SH_C3_1: f32 = 2.890611442640554; const SH_C3_2: f32 = -0.4570457994644658; const SH_C3_3: f32 = 0.3731763325901154; const SH_C3_4: f32 = -0.4570457994644658; const SH_C3_5: f32 = 1.445305721320277; const SH_C3_6: f32 = -0.5900435899266435; struct CameraUniforms { view_proj: mat4x4, position: vec3, viewport_height: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct SplatFieldUniforms { params: vec4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) local_coord: vec2, @location(1) color: vec3, @location(2) alpha_base: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var center_opacity: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; @group(1) @binding(3) var colors: array>; @group(1) @binding(4) var sorted_indices: array; @group(1) @binding(5) var splat_field: SplatFieldUniforms; @group(1) @binding(6) var sh_coefficients: array; fn linear_from_srgb(srgb: vec3) -> vec3 { let x = clamp(srgb, vec3(0.0), vec3(1.0)); let lo = x / vec3(12.92); let hi = pow((x + vec3(0.055)) / vec3(1.055), vec3(2.4)); let use_hi = x > vec3(0.04045); return select(lo, hi, use_hi); } fn rotate_by_quat(v: vec3, q: vec4) -> vec3 { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } fn safe_normalize(v: vec3) -> vec3 { let len_sq = dot(v, v); return select(vec3(0.0, 0.0, 1.0), v * inverseSqrt(max(len_sq, 1e-12)), len_sq > 1e-12); } fn sh_coeff_count_for_degree(degree: u32) -> u32 { if (degree == 0u) { return 1u; } if (degree == 1u) { return 4u; } if (degree == 2u) { return 9u; } return 16u; } fn sh_coeff_base(splat_index: u32, degree: u32) -> u32 { return splat_index * sh_coeff_count_for_degree(degree) * 3u; } fn read_sh_rgb(splat_index: u32, coeff_index: u32, degree: u32) -> vec3 { let base = sh_coeff_base(splat_index, degree) + coeff_index * 3u; return vec3( sh_coefficients[base + 0u], sh_coefficients[base + 1u], sh_coefficients[base + 2u], ); } fn evaluate_spherical_harmonics(splat_index: u32, dir: vec3, degree: u32) -> vec3 { let x = dir.x; let y = dir.y; let z = dir.z; let x2 = x * x; let y2 = y * y; let z2 = z * z; var result = SH_C0 * read_sh_rgb(splat_index, 0u, degree); if (degree >= 1u) { result += (-SH_C1 * y) * read_sh_rgb(splat_index, 1u, degree); result += (SH_C1 * z) * read_sh_rgb(splat_index, 2u, degree); result += (-SH_C1 * x) * read_sh_rgb(splat_index, 3u, degree); } if (degree >= 2u) { result += (SH_C2_0 * x * y) * read_sh_rgb(splat_index, 4u, degree); result += (SH_C2_1 * y * z) * read_sh_rgb(splat_index, 5u, degree); result += (SH_C2_2 * (2.0 * z2 - x2 - y2)) * read_sh_rgb(splat_index, 6u, degree); result += (SH_C2_3 * x * z) * read_sh_rgb(splat_index, 7u, degree); result += (SH_C2_4 * (x2 - y2)) * read_sh_rgb(splat_index, 8u, degree); } if (degree >= 3u) { result += (SH_C3_0 * y * (3.0 * x2 - y2)) * read_sh_rgb(splat_index, 9u, degree); result += (SH_C3_1 * x * y * z) * read_sh_rgb(splat_index, 10u, degree); result += (SH_C3_2 * y * (4.0 * z2 - x2 - y2)) * read_sh_rgb(splat_index, 11u, degree); result += (SH_C3_3 * z * (2.0 * z2 - 3.0 * x2 - 3.0 * y2)) * read_sh_rgb(splat_index, 12u, degree); result += (SH_C3_4 * x * (4.0 * z2 - x2 - y2)) * read_sh_rgb(splat_index, 13u, degree); result += (SH_C3_5 * z * (x2 - y2)) * read_sh_rgb(splat_index, 14u, degree); result += (SH_C3_6 * x * (x2 - 3.0 * y2)) * read_sh_rgb(splat_index, 15u, degree); } return result + vec3(0.5); } fn safe_clip_w(w: f32) -> f32 { return select(1e-6, w, abs(w) > 1e-6); } fn splat_center_renderable(clip: vec4) -> bool { let eps = 1e-6; return (clip.w > eps) && (clip.z >= -eps) && (clip.z <= clip.w + eps); } fn row4(m: mat4x4, r: u32) -> vec4 { return vec4(m[0][r], m[1][r], m[2][r], m[3][r]); } fn invalid_vertex() -> VertexOutput { var out: VertexOutput; out.position = vec4(2.0, 2.0, 2.0, 1.0); out.local_coord = vec2(0.0); out.color = vec3(0.0); out.alpha_base = 0.0; return out; } fn quad_corner(vertex_index: u32) -> vec2 { if (vertex_index == 0u) { return vec2(-1.0, -1.0); } if (vertex_index == 1u) { return vec2(1.0, -1.0); } if (vertex_index == 2u) { return vec2(-1.0, 1.0); } if (vertex_index == 3u) { return vec2(-1.0, 1.0); } if (vertex_index == 4u) { return vec2(1.0, -1.0); } return vec2(1.0, 1.0); } @vertex fn vs_main( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let splat_index = sorted_indices[instance_index]; let center_opacity_value = center_opacity[splat_index]; let rotation_value = rotations[splat_index]; let scale_value = max(abs(scales[splat_index].xyz), vec3(1e-6)); let color_value = colors[splat_index]; let world_center4 = model.model * vec4(center_opacity_value.xyz, 1.0); let clip_center = camera.view_proj * world_center4; if (!splat_center_renderable(clip_center)) { return invalid_vertex(); } let guarded_clip_w = safe_clip_w(clip_center.w); let local_axis_x = rotate_by_quat(vec3(scale_value.x, 0.0, 0.0), rotation_value); let local_axis_y = rotate_by_quat(vec3(0.0, scale_value.y, 0.0), rotation_value); let local_axis_z = rotate_by_quat(vec3(0.0, 0.0, scale_value.z), rotation_value); let world_axis_x = (model.model * vec4(local_axis_x, 0.0)).xyz; let world_axis_y = (model.model * vec4(local_axis_y, 0.0)).xyz; let world_axis_z = (model.model * vec4(local_axis_z, 0.0)).xyz; let view_proj_row0 = row4(camera.view_proj, 0u); let view_proj_row1 = row4(camera.view_proj, 1u); let view_proj_row3 = row4(camera.view_proj, 3u); let inv_clip_w_sq = 1.0 / (guarded_clip_w * guarded_clip_w); let jx = (view_proj_row0.xyz * guarded_clip_w - clip_center.x * view_proj_row3.xyz) * inv_clip_w_sq; let jy = (view_proj_row1.xyz * guarded_clip_w - clip_center.y * view_proj_row3.xyz) * inv_clip_w_sq; let a0 = vec2(dot(jx, world_axis_x), dot(jy, world_axis_x)); let a1 = vec2(dot(jx, world_axis_y), dot(jy, world_axis_y)); let a2 = vec2(dot(jx, world_axis_z), dot(jy, world_axis_z)); let cov_xx = a0.x * a0.x + a1.x * a1.x + a2.x * a2.x; let cov_xy = a0.x * a0.y + a1.x * a1.y + a2.x * a2.y; let cov_yy = a0.y * a0.y + a1.y * a1.y + a2.y * a2.y; let trace = cov_xx + cov_yy; let diff = cov_xx - cov_yy; let root = sqrt(max(0.0, diff * diff + 4.0 * cov_xy * cov_xy)); let lambda0 = max(1e-10, 0.5 * (trace + root)); let lambda1 = max(1e-10, 0.5 * (trace - root)); var axis0 = vec2(1.0, 0.0); if (abs(cov_xy) > 1e-8) { axis0 = normalize(vec2(cov_xy, lambda0 - cov_xx)); } else if (cov_yy > cov_xx) { axis0 = vec2(0.0, 1.0); } let axis1 = vec2(-axis0.y, axis0.x); let basis0 = axis0 * sqrt(lambda0) * 3.0; let basis1 = axis1 * sqrt(lambda1) * 3.0; let viewport_height = max(camera.viewport_height, 1.0); let radius_ndc = max(length(basis0), length(basis1)); let radius_px = radius_ndc * 0.5 * viewport_height; let max_radius_px = max(96.0, min(512.0, viewport_height * 0.45)); let fade_start_px = max_radius_px * 0.75; if (radius_px >= max_radius_px) { return invalid_vertex(); } let radius_fade = 1.0 - smoothstep(fade_start_px, max_radius_px, radius_px); let corner = quad_corner(vertex_index); let ndc_offset = (basis0 * corner.x) + (basis1 * corner.y); let clip_offset = ndc_offset * clip_center.w; var linear_color: vec3; if (splat_field.params.z > 0.5) { let world_dir = safe_normalize(world_center4.xyz - camera.position); let local_dir = safe_normalize((transpose(model.normal) * vec4(world_dir, 0.0)).xyz); let degree = u32(splat_field.params.w + 0.5); linear_color = max( evaluate_spherical_harmonics(splat_index, local_dir, degree), vec3(0.0), ); } else { linear_color = max(color_value.rgb, vec3(0.0)); } if (splat_field.params.y > 0.5) { linear_color = linear_from_srgb(linear_color); } let alpha_base = clamp(color_value.a, 0.0, 1.0) * clamp(center_opacity_value.w, 0.0, 1.0) * clamp(splat_field.params.x, 0.0, 1.0) * radius_fade; var out: VertexOutput; out.position = clip_center + vec4(clip_offset, 0.0, 0.0); out.local_coord = corner; out.color = linear_color; out.alpha_base = alpha_base; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let q = dot(in.local_coord, in.local_coord); if (q > 1.0) { discard; } let alpha = in.alpha_base * exp(-4.5 * q); if (alpha <= 1e-4) { discard; } return vec4(in.color * alpha, alpha); }"; // wgsl/world/splatfield-sort.wgsl var splatfield_sort_default = "struct SortTransform { mvp: mat4x4, } @group(0) @binding(0) var center_opacity: array>; @group(0) @binding(1) var sort_transform: SortTransform; @group(0) @binding(2) var keys_out: array; @group(0) @binding(3) var indices_out: array; fn safe_clip_w(w: f32) -> f32 { return select(1e-6, w, abs(w) > 1e-6); } fn splat_center_renderable(clip: vec4) -> bool { let eps = 1e-6; return (clip.w > eps) && (clip.z >= -eps) && (clip.z <= clip.w + eps); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(¢er_opacity); if (i >= n) { return; } let clip = sort_transform.mvp * vec4(center_opacity[i].xyz, 1.0); if (!splat_center_renderable(clip)) { keys_out[i] = 0xffffffffu; indices_out[i] = i; return; } let depth = clamp(clip.z / safe_clip_w(clip.w), 0.0, 1.0); let far_to_near = 1.0 - depth; keys_out[i] = u32(round(far_to_near * 4294967040.0)); indices_out[i] = i; }"; // wgsl/world/splatfield-radix-flags.wgsl var splatfield_radix_flags_default = "override BIT: u32 = 0u; const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_INVOCATION: u32 = 4u; const ELEMENTS_PER_WORKGROUP: u32 = WORKGROUP_SIZE * ELEMENTS_PER_INVOCATION; @group(0) @binding(0) var keys: array; @group(0) @binding(1) var prefix: array; @group(0) @binding(2) var block_sums: array; var temp: array; fn zero_bit(i: u32, n: u32) -> u32 { if (i >= n) { return 0u; } return select(0u, 1u, ((keys[i] >> BIT) & 1u) == 0u); } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&keys); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid * ELEMENTS_PER_INVOCATION; let i1 = i0 + 1u; let i2 = i0 + 2u; let i3 = i0 + 3u; let v0 = zero_bit(i0, n); let v1 = zero_bit(i1, n); let v2 = zero_bit(i2, n); let v3 = zero_bit(i3, n); temp[tid] = v0 + v1 + v2 + v3; var offset = 1u; var d = WORKGROUP_SIZE / 2u; loop { workgroupBarrier(); if (d == 0u) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; temp[bi] = temp[bi] + temp[ai]; } offset = offset * 2u; d = d / 2u; } if (tid == 0u) { block_sums[wid.x] = temp[WORKGROUP_SIZE - 1u]; temp[WORKGROUP_SIZE - 1u] = 0u; } d = 1u; loop { offset = offset / 2u; workgroupBarrier(); if (d >= WORKGROUP_SIZE) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; let t = temp[ai]; temp[ai] = temp[bi]; temp[bi] = temp[bi] + t; } d = d * 2u; } workgroupBarrier(); let thread_offset = temp[tid]; if (i0 < n) { prefix[i0] = thread_offset; } if (i1 < n) { prefix[i1] = thread_offset + v0; } if (i2 < n) { prefix[i2] = thread_offset + v0 + v1; } if (i3 < n) { prefix[i3] = thread_offset + v0 + v1 + v2; } }"; // wgsl/world/splatfield-radix-scatter-pairs.wgsl var splatfield_radix_scatter_pairs_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keys_in: array; @group(0) @binding(1) var values_in: array; @group(0) @binding(2) var prefix: array; @group(0) @binding(3) var keys_out: array; @group(0) @binding(4) var values_out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keys_in); if (i >= n) { return; } let k = keys_in[i]; let is_zero = ((k >> BIT) & 1u) == 0u; let zero_pos = prefix[i]; let last_key = keys_in[n - 1u]; let zero_count = prefix[n - 1u] + select(0u, 1u, ((last_key >> BIT) & 1u) == 0u); let one_pos = zero_count + (i - zero_pos); let dst = select(one_pos, zero_pos, is_zero); keys_out[dst] = k; values_out[dst] = values_in[i]; }"; // wgsl/world/latticespace.wgsl var latticespace_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, viewport_height: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct Light { position: vec4, color: vec4, params: vec4, } struct LightingUniforms { ambient: vec4, light_count: u32, _pad0: vec3, lights: array, } struct LatticeSpaceUniforms { dimensions: vec4, origin: vec4, spacing: vec4, cell_scale: vec4, range_min: vec4, range_max: vec4, data_config: vec4, visual: vec4, filters: vec4, solid_color: vec4, scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, colors: array, 8>, } struct VertexOutput { @builtin(position) position: vec4, @location(0) world_position: vec3, @location(1) normal: vec3, @location(2) local_position: vec3, @location(3) @interpolate(flat) cell_index: u32, @location(4) @interpolate(flat) cell: vec3, @location(5) @interpolate(flat) face: u32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(0) @binding(2) var lighting: LightingUniforms; @group(1) @binding(0) var cell_data: array; @group(1) @binding(1) var cell_mask: array; @group(1) @binding(2) var sorted_indices: array; @group(1) @binding(3) var lattice: LatticeSpaceUniforms; @group(1) @binding(4) var colormap_sampler: sampler; @group(1) @binding(5) var colormap_texture: texture_1d; fn finite_value(value: f32) -> bool { let bits = bitcast(value); return (bits & 0x7f800000u) != 0x7f800000u; } fn component(value: vec4, index: u32) -> f32 { if (index == 0u) { return value.x; } if (index == 1u) { return value.y; } if (index == 2u) { return value.z; } return value.w; } fn load_value(index: u32) -> vec4 { let count = u32(lattice.data_config.x + 0.5); let base = index * count; var out = vec4(0.0); if (count > 0u) { out.x = cell_data[base]; } if (count > 1u) { out.y = cell_data[base + 1u]; } if (count > 2u) { out.z = cell_data[base + 2u]; } if (count > 3u) { out.w = cell_data[base + 3u]; } return out; } fn select_scalar(value: vec4) -> f32 { let count = max(1u, min(4u, u32(lattice.scale_source.x + 0.5))); if (u32(lattice.scale_source.z + 0.5) == 1u) { if (count == 1u) { return abs(value.x); } if (count == 2u) { return length(value.xy); } if (count == 3u) { return length(value.xyz); } return length(value); } return component(value, min(3u, u32(lattice.scale_source.y + 0.5))); } fn cell_visible(index: u32, value: vec4) -> bool { if (lattice.data_config.w > 0.5 && cell_mask[index] == 0u) { return false; } let mode = u32(lattice.data_config.y + 0.5); if (mode == 0u) { let scalar = select_scalar(value); if (!finite_value(scalar)) { return false; } if (lattice.filters.x > 0.5 && (scalar < lattice.visual.z || scalar > lattice.visual.w)) { return false; } } else if ( mode == 1u && ( !finite_value(value.x) || !finite_value(value.y) || !finite_value(value.z) || !finite_value(value.w) ) ) { return false; } return true; } fn ordinal_to_cell(ordinal: u32) -> vec3 { let size = vec3(lattice.range_max.xyz - lattice.range_min.xyz); let x = ordinal % size.x; let y = (ordinal / size.x) % size.y; let z = ordinal / max(1u, size.x * size.y); return vec3(lattice.range_min.xyz) + vec3(x, y, z); } fn cell_to_linear(cell: vec3) -> u32 { let dims = vec3(lattice.dimensions.xyz); return cell.x + dims.x * (cell.y + dims.y * cell.z); } fn linear_to_cell(index: u32) -> vec3 { let dims = vec3(lattice.dimensions.xyz); return vec3(index % dims.x, (index / dims.x) % dims.y, index / (dims.x * dims.y)); } fn cube_vertex(vertex_index: u32) -> vec3 { let face = vertex_index / 6u; let tri = vertex_index % 6u; let uv = array, 6>( vec2(-1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(1.0, -1.0), )[tri] * 0.5; if (face == 0u) { return vec3(-0.5, uv.y, -uv.x); } if (face == 1u) { return vec3(0.5, uv.y, uv.x); } if (face == 2u) { return vec3(uv.x, -0.5, -uv.y); } if (face == 3u) { return vec3(uv.x, 0.5, uv.y); } if (face == 4u) { return vec3(uv.x, uv.y, -0.5); } return vec3(-uv.x, uv.y, 0.5); } fn face_normal(face: u32) -> vec3 { return array, 6>( vec3(-1, 0, 0), vec3(1, 0, 0), vec3(0, -1, 0), vec3(0, 1, 0), vec3(0, 0, -1), vec3(0, 0, 1), )[face]; } fn resolve_fragment_cell(in: VertexOutput) -> vec4 { if (u32(lattice.dimensions.w + 0.5) == 3u) { return vec4(in.cell, in.cell_index); } let relative = (in.local_position.xy - (lattice.origin.xy - 0.5 * lattice.spacing.xy)) / lattice.spacing.xy; let cell = vec2(floor(relative)); return vec4(cell, 0u, cell.x + u32(lattice.dimensions.x) * cell.y); } fn internal_face(cell: vec3, face: u32) -> bool { if (any(lattice.cell_scale.xyz < vec3(0.999999))) { return false; } let dims = vec3(lattice.dimensions.xyz); var neighbor = vec3(cell); if (face == 0u) { neighbor.x -= 1; } else if (face == 1u) { neighbor.x += 1; } else if (face == 2u) { neighbor.y -= 1; } else if (face == 3u) { neighbor.y += 1; } else if (face == 4u) { neighbor.z -= 1; } else { neighbor.z += 1; } if (any(neighbor < vec3(0)) || any(neighbor >= vec3(dims))) { return false; } if ( any(neighbor < vec3(lattice.range_min.xyz)) || any(neighbor >= vec3(lattice.range_max.xyz)) ) { return false; } let index = cell_to_linear(vec3(neighbor)); var value = vec4(0); if (u32(lattice.data_config.y + 0.5) != 2u) { value = load_value(index); } return cell_visible(index, value); } fn scale_mode(value: f32, mode: u32) -> f32 { if (mode == 0u) { return value; } if (mode == 1u) { return log(max(value, 1e-20)) / log(max(lattice.scale_params.y, 1.000001)); } let threshold = max(lattice.scale_params.z, 1e-20); return sign(value) * log(1.0 + abs(value) / threshold) / log(max(lattice.scale_params.y, 1.000001)); } fn scale_value(value: f32) -> f32 { var v = value; if (u32(lattice.scale_domain.w + 0.5) != 0u && lattice.scale_clamp.y > lattice.scale_clamp.x) { v = clamp(v, lattice.scale_clamp.x, lattice.scale_clamp.y); } var domain_min = lattice.scale_domain.x; var domain_max = lattice.scale_domain.y; if (domain_max <= domain_min && lattice.scale_clamp.y > lattice.scale_clamp.x) { domain_min = lattice.scale_clamp.x; domain_max = lattice.scale_clamp.y; } let mode = u32(lattice.scale_params.x + 0.5); let a = scale_mode(domain_min, mode); let b = scale_mode(domain_max, mode); let x = scale_mode(v, mode); var t = clamp((x - a) / max(1e-20, b - a), 0.0, 1.0); t = pow(t, max(lattice.scale_params.w, 1e-6)); return select(t, 1.0 - t, lattice.scale_flags.x > 0.5); } fn map_color(t: f32) -> vec4 { let count = u32(lattice.filters.y + 0.5); if (count < 2u) { return textureSample(colormap_texture, colormap_sampler, clamp(t, 0.0, 1.0)); } let x = clamp(t, 0.0, 1.0) * f32(count - 1u); let index = min(u32(floor(x)), count - 1u); let next = min(index + 1u, count - 1u); return mix(lattice.colors[index], lattice.colors[next], x - f32(index)); } fn linear_from_srgb(value: vec3) -> vec3 { return select( value / 12.92, pow((value + vec3(0.055)) / 1.055, vec3(2.4)), value > vec3(0.04045), ); } fn srgb_from_linear(value: vec3) -> vec3 { return select( 12.92 * value, 1.055 * pow(value, vec3(1.0 / 2.4)) - vec3(0.055), value > vec3(0.0031308), ); } fn apply_lighting(position: vec3, normal: vec3, color: vec3) -> vec3 { var result = lighting.ambient.rgb * color; for (var i = 0u; i < min(lighting.light_count, 8u); i++) { let light = lighting.lights[i]; var direction: vec3; var attenuation = 1.0; if (light.position.w == 0.0) { direction = normalize(-light.position.xyz); } else { let delta = light.position.xyz - position; let distance = length(delta); direction = select(vec3(0, 1, 0), delta / distance, distance > 1e-6); attenuation = 1.0 / max(distance * distance, 1e-6); } result += color * light.color.rgb * light.color.a * attenuation * max(dot(normal, direction), 0.0); } return result; } @vertex fn vs_2d(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { let uv = array, 6>( vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(0, 1), vec2(1, 0), vec2(1, 1), )[vertex_index]; let min_cell = lattice.range_min.xy; let max_cell = lattice.range_max.xy; let first_edge = lattice.origin.xy + min_cell * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let last_edge = lattice.origin.xy + max_cell * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let local = vec3(first_edge + uv * (last_edge - first_edge), lattice.origin.z); let world = model.model * vec4(local, 1.0); var out: VertexOutput; out.position = camera.view_proj * world; out.world_position = world.xyz; out.normal = normalize((model.normal * vec4(0, 0, 1, 0)).xyz); out.local_position = local; out.cell_index = 0u; out.cell = vec3(0u); out.face = 5u; return out; } @vertex fn vs_3d( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let use_sorted = lattice.filters.z > 0.5; let index = select( cell_to_linear(ordinal_to_cell(instance_index)), sorted_indices[instance_index], use_sorted, ); let cell = linear_to_cell(index); let face = vertex_index / 6u; let local = lattice.origin.xyz + vec3(cell) * lattice.spacing.xyz + cube_vertex(vertex_index) * lattice.spacing.xyz * lattice.cell_scale.xyz; let world = model.model * vec4(local, 1.0); var out: VertexOutput; out.position = camera.view_proj * world; out.world_position = world.xyz; out.normal = normalize((model.normal * vec4(face_normal(face), 0.0)).xyz); out.local_position = local; out.cell_index = index; out.cell = cell; out.face = face; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let resolved = resolve_fragment_cell(in); let cell = resolved.xyz; let index = resolved.w; if (u32(lattice.dimensions.w + 0.5) == 2u) { if ( any(cell.xy < vec2(lattice.range_min.xy)) || any(cell.xy >= vec2(lattice.range_max.xy)) ) { discard; } let center = lattice.origin.xy + vec2(cell.xy) * lattice.spacing.xy; let normalized = abs((in.local_position.xy - center) / lattice.spacing.xy); if (any(normalized > 0.5 * lattice.cell_scale.xy)) { discard; } } else if (internal_face(cell, in.face)) { discard; } let mode = u32(lattice.data_config.y + 0.5); var value = vec4(0); if (mode != 2u) { value = load_value(index); } if (!cell_visible(index, value)) { discard; } var color: vec4; if (mode == 0u) { color = map_color(scale_value(select_scalar(value))); } else if (mode == 1u) { color = value; if (lattice.data_config.z > 0.5) { color = vec4(linear_from_srgb(color.rgb), color.a); } } else { color = lattice.solid_color; } var rgb = max(color.rgb, vec3(0)); if (lattice.visual.y > 0.5) { rgb = apply_lighting(in.world_position, normalize(in.normal), rgb); } return vec4( srgb_from_linear(rgb), clamp(color.a, 0.0, 1.0) * clamp(lattice.visual.x, 0.0, 1.0), ); }"; // wgsl/world/latticespace-sort.wgsl var latticespace_sort_default = "struct SortTransform { mvp: mat4x4, } struct LatticeUniforms { dimensions: vec4, origin: vec4, spacing: vec4, cell_scale: vec4, range_min: vec4, range_max: vec4, data_config: vec4, visual: vec4, filters: vec4, solid_color: vec4, scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, colors: array, 8>, } @group(0) @binding(0) var lattice: LatticeUniforms; @group(0) @binding(1) var sort_transform: SortTransform; @group(0) @binding(2) var keys_out: array; @group(0) @binding(3) var indices_out: array; fn ordinal_to_cell(ordinal: u32) -> vec3 { let size = vec3(lattice.range_max.xyz - lattice.range_min.xyz); return vec3(lattice.range_min.xyz) + vec3( ordinal % size.x, (ordinal / size.x) % size.y, ordinal / max(1u, size.x * size.y), ); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let ordinal = gid.x; if (ordinal >= arrayLength(&keys_out)) { return; } let cell = ordinal_to_cell(ordinal); let dims = vec3(lattice.dimensions.xyz); let index = cell.x + dims.x * (cell.y + dims.y * cell.z); let center = lattice.origin.xyz + vec3(cell) * lattice.spacing.xyz; let clip = sort_transform.mvp * vec4(center, 1.0); if (clip.w <= 1e-6 || clip.z < -1e-6 || clip.z > clip.w + 1e-6) { keys_out[ordinal] = 0xffffffffu; indices_out[ordinal] = index; return; } let depth = clamp(clip.z / max(clip.w, 1e-6), 0.0, 1.0); keys_out[ordinal] = u32(round((1.0 - depth) * 4294967040.0)); indices_out[ordinal] = index; }"; // wgsl/world/latticespace-radix-flags.wgsl var latticespace_radix_flags_default = "override BIT: u32 = 0u; const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_INVOCATION: u32 = 4u; const ELEMENTS_PER_WORKGROUP: u32 = WORKGROUP_SIZE * ELEMENTS_PER_INVOCATION; @group(0) @binding(0) var keys: array; @group(0) @binding(1) var prefix: array; @group(0) @binding(2) var block_sums: array; var temp: array; fn zero_bit(i: u32, n: u32) -> u32 { if (i >= n) { return 0u; } return select(0u, 1u, ((keys[i] >> BIT) & 1u) == 0u); } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&keys); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid * ELEMENTS_PER_INVOCATION; let i1 = i0 + 1u; let i2 = i0 + 2u; let i3 = i0 + 3u; let v0 = zero_bit(i0, n); let v1 = zero_bit(i1, n); let v2 = zero_bit(i2, n); let v3 = zero_bit(i3, n); temp[tid] = v0 + v1 + v2 + v3; var offset = 1u; var d = WORKGROUP_SIZE / 2u; loop { workgroupBarrier(); if (d == 0u) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; temp[bi] = temp[bi] + temp[ai]; } offset = offset * 2u; d = d / 2u; } if (tid == 0u) { block_sums[wid.x] = temp[WORKGROUP_SIZE - 1u]; temp[WORKGROUP_SIZE - 1u] = 0u; } d = 1u; loop { offset = offset / 2u; workgroupBarrier(); if (d >= WORKGROUP_SIZE) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; let t = temp[ai]; temp[ai] = temp[bi]; temp[bi] = temp[bi] + t; } d = d * 2u; } workgroupBarrier(); let thread_offset = temp[tid]; if (i0 < n) { prefix[i0] = thread_offset; } if (i1 < n) { prefix[i1] = thread_offset + v0; } if (i2 < n) { prefix[i2] = thread_offset + v0 + v1; } if (i3 < n) { prefix[i3] = thread_offset + v0 + v1 + v2; } }"; // wgsl/world/latticespace-radix-scatter-pairs.wgsl var latticespace_radix_scatter_pairs_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keys_in: array; @group(0) @binding(1) var values_in: array; @group(0) @binding(2) var prefix: array; @group(0) @binding(3) var keys_out: array; @group(0) @binding(4) var values_out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keys_in); if (i >= n) { return; } let k = keys_in[i]; let is_zero = ((k >> BIT) & 1u) == 0u; let zero_pos = prefix[i]; let last_key = keys_in[n - 1u]; let zero_count = prefix[n - 1u] + select(0u, 1u, ((last_key >> BIT) & 1u) == 0u); let one_pos = zero_count + (i - zero_pos); let dst = select(one_pos, zero_pos, is_zero); keys_out[dst] = k; values_out[dst] = values_in[i]; }"; // wgsl/compute/scan-block-exclusive-u32.wgsl var scan_block_exclusive_u32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_INVOCATION: u32 = 4u; const ELEMENTS_PER_WORKGROUP: u32 = WORKGROUP_SIZE * ELEMENTS_PER_INVOCATION; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; @group(0) @binding(2) var block_sums: array; var temp: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid * ELEMENTS_PER_INVOCATION; let i1 = i0 + 1u; let i2 = i0 + 2u; let i3 = i0 + 3u; let v0 = select(0u, input[i0], i0 < n); let v1 = select(0u, input[i1], i1 < n); let v2 = select(0u, input[i2], i2 < n); let v3 = select(0u, input[i3], i3 < n); temp[tid] = v0 + v1 + v2 + v3; var offset = 1u; var d = WORKGROUP_SIZE / 2u; loop { workgroupBarrier(); if (d == 0u) { break; } if (tid < d) { let i1 = offset * ((tid * 2u) + 1u) - 1u; let i2 = offset * ((tid * 2u) + 2u) - 1u; temp[i2] = temp[i2] + temp[i1]; } offset = offset * 2u; d = d / 2u; } if (tid == 0u) { block_sums[wid.x] = temp[WORKGROUP_SIZE - 1u]; temp[WORKGROUP_SIZE - 1u] = 0u; } d = 1u; loop { offset = offset / 2u; workgroupBarrier(); if (d >= WORKGROUP_SIZE) { break; } if (tid < d) { let i1 = offset * ((tid * 2u) + 1u) - 1u; let i2 = offset * ((tid * 2u) + 2u) - 1u; let t = temp[i1]; temp[i1] = temp[i2]; temp[i2] = temp[i2] + t; } d = d * 2u; } workgroupBarrier(); let thread_offset = temp[tid]; if (i0 < n) { output[i0] = thread_offset; } if (i1 < n) { output[i1] = thread_offset + v0; } if (i2 < n) { output[i2] = thread_offset + v0 + v1; } if (i3 < n) { output[i3] = thread_offset + v0 + v1 + v2; } }"; // wgsl/compute/scan-add-block-offsets-u32.wgsl var scan_add_block_offsets_u32_default = "const ELEMENTS_PER_WORKGROUP: u32 = 1024u; @group(0) @binding(0) var data: array; @group(0) @binding(1) var block_offsets: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&data); if (i >= n) { return; } let block = i / ELEMENTS_PER_WORKGROUP; let off = block_offsets[block]; data[i] = data[i] + off; }"; // typescript/core/resources.ts var refreshWasmStagingViews = (ctx) => { const buf = wasm.memory().buffer; const needRefresh = buf !== ctx._wasmBuffer || !ctx.cameraUniformStagingView || ctx.cameraUniformStagingView.byteOffset !== ctx.cameraUniformStagingPtr || !ctx.lightingUniformStagingView || ctx.lightingUniformStagingView.byteOffset !== ctx.lightingUniformStagingPtr || !ctx.modelUniformStagingView || ctx.modelUniformStagingView.byteOffset !== ctx.modelUniformStagingPtr; if (!needRefresh) return; ctx._wasmBuffer = buf; ctx.cameraUniformStagingView = wasm.f32view(ctx.cameraUniformStagingPtr, 20); ctx.lightingUniformStagingView = wasm.f32view(ctx.lightingUniformStagingPtr, 8 + Scene.MAX_LIGHTS * 16); ctx.lightingCountView = wasm.u32view(ctx.lightingUniformStagingPtr + 16, 1); ctx.modelUniformStagingView = wasm.f32view(ctx.modelUniformStagingPtr, 32); }; var getObjectId = (ctx, obj) => { let id = ctx.objectIds.get(obj); if (id !== void 0) return id; id = ctx.nextObjectId++; ctx.objectIds.set(obj, id); ctx.objectsById.set(id, obj); return id; }; var createGlobalBindGroupLayout = (ctx) => { ctx.globalBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 80 } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 128 } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: (8 + Scene.MAX_LIGHTS * 16) * 4 } } ] }); }; var createSkinBindGroupLayout = (ctx) => { ctx.skinBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }] }); }; var createUniformBuffers = (ctx) => { ctx.cameraUniformBuffer = ctx.device.createBuffer({ size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.lightingUniformBuffer = ctx.device.createBuffer({ size: (8 + Scene.MAX_LIGHTS * 16) * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.pickUniformBuffers = []; ctx.pickBindGroups = []; const pickLayout = ctx.getPickBindGroupLayout(); ctx.modelUniformStride = Math.max(128, ctx.device.limits.minUniformBufferOffsetAlignment || 256); ensureModelUniformCapacity(ctx, ctx.INITIAL_UNIFORM_CAPACITY); for (let i = 0; i < ctx.INITIAL_UNIFORM_CAPACITY; i++) { const pickBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.pickUniformBuffers.push(pickBuffer); ctx.pickBindGroups.push(ctx.device.createBindGroup({ layout: pickLayout, entries: [{ binding: 0, resource: { buffer: pickBuffer } }] })); } ctx.cameraUniformStagingPtr = 0; ctx.lightingUniformStagingPtr = 0; ctx.modelUniformStagingPtr = 0; ctx._wasmBuffer = null; }; var ensurePickUniformPool = (ctx, requiredCount) => { const current = ctx.pickUniformBuffers.length; if (requiredCount <= current) return; let newSize = Math.max(1, current); while (newSize < requiredCount) newSize *= 2; ctx.pickUniformBuffers.length = newSize; ctx.pickBindGroups.length = newSize; const pickLayout = ctx.getPickBindGroupLayout(); for (let i = current; i < newSize; i++) { const pickBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.pickUniformBuffers[i] = pickBuffer; ctx.pickBindGroups[i] = ctx.device.createBindGroup({ layout: pickLayout, entries: [{ binding: 0, resource: { buffer: pickBuffer } }] }); } }; var ensureModelUniformCapacity = (ctx, requiredCount) => { if (requiredCount <= ctx.modelUniformBufferCapacity && ctx.modelUniformBuffer && ctx.modelUniformBindGroup) return; let capacity = Math.max(1, ctx.modelUniformBufferCapacity || ctx.INITIAL_UNIFORM_CAPACITY); while (capacity < requiredCount) capacity *= 2; const byteLength = capacity * ctx.modelUniformStride; if (byteLength > ctx.device.limits.maxBufferSize) throw new Error(`Renderer model uniform capacity exceeds maxBufferSize (${byteLength} > ${ctx.device.limits.maxBufferSize}).`); ctx.modelUniformBuffer?.destroy(); ctx.modelUniformBuffer = ctx.device.createBuffer({ label: "WasmGPU packed model uniforms", size: byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.modelUniformBufferCapacity = capacity; ctx.modelUniformBindGroup = ctx.device.createBindGroup({ layout: ctx.globalBindGroupLayout, entries: [ { binding: 0, resource: { buffer: ctx.cameraUniformBuffer } }, { binding: 1, resource: { buffer: ctx.modelUniformBuffer, size: 128 } }, { binding: 2, resource: { buffer: ctx.lightingUniformBuffer } } ] }); }; var prepareModelUniforms = (ctx, modelPtrs) => { ctx.modelUniformSlots.clear(); for (const ptr of modelPtrs) if (!ctx.modelUniformSlots.has(ptr)) ctx.modelUniformSlots.set(ptr, ctx.modelUniformSlots.size); const count = ctx.modelUniformSlots.size; if (count <= 0) return; ensureModelUniformCapacity(ctx, count); const ptrsPtr = frameArena.alloc(count * 4, 4); const ptrs = wasm.u32view(ptrsPtr, count); for (const [ptr, slot] of ctx.modelUniformSlots) ptrs[slot] = ptr >>> 0; const packedPtr = frameArena.allocF32(count * 32); transformf.packModelNormalMat4FromPtrs(packedPtr, ptrsPtr, count); const uploadPtr = frameArena.alloc(count * ctx.modelUniformStride, ctx.modelUniformStride); const src = wasm.u8view(packedPtr, count * 128); const dst = wasm.u8view(uploadPtr, count * ctx.modelUniformStride); for (let slot = 0; slot < count; slot++) dst.set(src.subarray(slot * 128, slot * 128 + 128), slot * ctx.modelUniformStride); ctx.queue.writeBuffer(ctx.modelUniformBuffer, 0, driver.bytes(), uploadPtr, count * ctx.modelUniformStride); }; var getModelUniformSlot = (ctx, modelPtr) => { const slot = ctx.modelUniformSlots.get(modelPtr); if (slot === void 0) throw new Error("Renderer model transform was not prepared for this pass."); return slot; }; var bindModelUniform = (ctx, pass, modelPtr) => { const slot = getModelUniformSlot(ctx, modelPtr); pass.setBindGroup(0, ctx.modelUniformBindGroup, [slot * ctx.modelUniformStride]); return slot; }; var createFallbackTextures = (ctx) => { ctx.fallbackSampler = ctx.device.createSampler({ addressModeU: "repeat", addressModeV: "repeat", magFilter: "linear", minFilter: "linear", mipmapFilter: "linear" }); const create1x1 = (rgba, wantSrgbView) => { const tex = ctx.device.createTexture({ size: { width: 1, height: 1 }, format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, viewFormats: ["rgba8unorm-srgb"] }); const data = new Uint8Array(256); data[0] = rgba[0]; data[1] = rgba[1]; data[2] = rgba[2]; data[3] = rgba[3]; ctx.queue.writeTexture( { texture: tex }, data, { bytesPerRow: 256, rowsPerImage: 1 }, { width: 1, height: 1 } ); const linear = tex.createView({ format: "rgba8unorm" }); const srgb = wantSrgbView ? tex.createView({ format: "rgba8unorm-srgb" }) : linear; return { tex, linear, srgb }; }; const white = create1x1([255, 255, 255, 255], true); ctx.fallbackWhiteTexture = white.tex; ctx.fallbackWhiteViewLinear = white.linear; ctx.fallbackWhiteViewSrgb = white.srgb; const normal = create1x1([128, 128, 255, 255], false); ctx.fallbackNormalTexture = normal.tex; ctx.fallbackNormalViewLinear = normal.linear; const mr = create1x1([0, 255, 255, 255], false); ctx.fallbackMRTex = mr.tex; ctx.fallbackMRViewLinear = mr.linear; const occ = create1x1([255, 0, 0, 255], false); ctx.fallbackOcclusionTex = occ.tex; ctx.fallbackOcclusionViewLinear = occ.linear; const anisotropy = create1x1([255, 128, 255, 255], false); ctx.fallbackAnisotropyTexture = anisotropy.tex; ctx.fallbackAnisotropyViewLinear = anisotropy.linear; }; var writeCameraUniforms = (ctx, camera) => { refreshWasmStagingViews(ctx); const proj = camera.getProjectionMatrix(); ctx.modelUniformStagingView.set(proj, 0); const viewPtr = ctx.modelUniformStagingPtr + 16 * 4; camera.writeViewMatrixTo(viewPtr); mat4f.mul(ctx.cameraUniformStagingPtr, ctx.modelUniformStagingPtr, viewPtr); const store = TransformStore.global(); const storeF32 = store.f32(); const base = (store.worldPtr >>> 2) + camera.transform.index * 16; ctx.cameraUniformStagingView[16] = storeF32[base + 12]; ctx.cameraUniformStagingView[17] = storeF32[base + 13]; ctx.cameraUniformStagingView[18] = storeF32[base + 14]; ctx.cameraUniformStagingView[19] = ctx.height; ctx.queue.writeBuffer(ctx.cameraUniformBuffer, 0, ctx.cameraUniformStagingView); }; var writeLightingUniforms = (ctx, scene) => { const { ambient, lights } = scene.getLightingData(); refreshWasmStagingViews(ctx); const data = ctx.lightingUniformStagingView; data.fill(0); data[0] = ambient[0]; data[1] = ambient[1]; data[2] = ambient[2]; data[3] = 1; ctx.lightingCountView[0] = lights.length; let offset = 8; for (let i = 0; i < lights.length && i < Scene.MAX_LIGHTS; i++) { const light = lights[i]; if (light instanceof DirectionalLight) { const direction = resolveLightDirection(light); data[offset + 0] = direction[0]; data[offset + 1] = direction[1]; data[offset + 2] = direction[2]; data[offset + 3] = 0; } else if (light instanceof PointLight) { const position = resolveLightPosition(light); data[offset + 0] = position[0]; data[offset + 1] = position[1]; data[offset + 2] = position[2]; data[offset + 3] = 1; data[offset + 12] = light.range; } else if (light instanceof SpotLight) { const position = resolveLightPosition(light); const direction = resolveLightDirection(light); data[offset + 0] = position[0]; data[offset + 1] = position[1]; data[offset + 2] = position[2]; data[offset + 3] = 2; data[offset + 8] = direction[0]; data[offset + 9] = direction[1]; data[offset + 10] = direction[2]; data[offset + 12] = light.range; data[offset + 13] = Math.cos(light.innerCone); data[offset + 14] = Math.cos(light.outerCone); } data[offset + 4] = light.color[0]; data[offset + 5] = light.color[1]; data[offset + 6] = light.color[2]; data[offset + 7] = light.intensity; if (light instanceof DirectionalLight) { const direction = resolveLightDirection(light); data[offset + 8] = direction[0]; data[offset + 9] = direction[1]; data[offset + 10] = direction[2]; } offset += 16; } ctx.queue.writeBuffer(ctx.lightingUniformBuffer, 0, data); }; var ensureInstanceBuffer = (ctx, byteLength) => { if (ctx.instanceBuffer && ctx.instanceBufferCapacityBytes >= byteLength) return; ctx.instanceBuffer?.destroy(); let cap = ctx.instanceBufferCapacityBytes || ctx.INSTANCE_STRIDE_BYTES * 256; while (cap < byteLength) cap *= 2; ctx.instanceBufferCapacityBytes = cap; ctx.instanceBuffer = ctx.device.createBuffer({ size: cap, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); ctx.instanceBufferGeneration++; }; // typescript/core/materials.ts var getOrCreatePipeline = (ctx, material, instanced = false, skinned = false, skinned8 = false, mirrored = false, forceNoDepthWrite = false, receiveShadow = false) => { if (instanced && skinned) throw new Error("Renderer: instanced + skinned pipelines are not supported (attribute layout conflict)."); if (skinned8 && !skinned) skinned = true; const shadows = receiveShadow && material instanceof StandardMaterial && ctx.shadowRenderer.activeViewCount > 0; const key = getPipelineCacheKey(ctx, material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite, shadows); let pipeline = ctx.pipelineCache.get(key); if (pipeline) return pipeline; const shaderCode = material.getShaderCode({ instanced, skinned, skinned8, shadows, shadowGroup: skinned ? 3 : 2 }); let shaderModule = ctx.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: shaderCode }); ctx.shaderCache.set(shaderCode, shaderModule); } const materialBindGroupLayout = material.createBindGroupLayout(ctx.device); const bindGroupLayouts = [ctx.globalBindGroupLayout, materialBindGroupLayout]; if (skinned) bindGroupLayouts.push(ctx.skinBindGroupLayout); if (shadows) bindGroupLayouts.push(ctx.shadowRenderer.bindGroupLayout); const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts }); let buffers; const standardMaterial = material instanceof StandardMaterial; if (instanced && standardMaterial) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 12, offset: 0, format: "float32x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: ctx.INSTANCE_STRIDE_BYTES, stepMode: "instance", attributes: [ { shaderLocation: 3, offset: 0, format: "float32x4" }, { shaderLocation: 4, offset: 16, format: "float32x4" }, { shaderLocation: 5, offset: 32, format: "float32x4" }, { shaderLocation: 6, offset: 48, format: "float32x4" }, { shaderLocation: 7, offset: 64, format: "float32x4" }, { shaderLocation: 8, offset: 80, format: "float32x4" }, { shaderLocation: 9, offset: 96, format: "float32x4" }, { shaderLocation: 10, offset: 112, format: "float32x4" } ] } ]; } else if (instanced) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: ctx.INSTANCE_STRIDE_BYTES, stepMode: "instance", attributes: [ { shaderLocation: 3, offset: 0, format: "float32x4" }, { shaderLocation: 4, offset: 16, format: "float32x4" }, { shaderLocation: 5, offset: 32, format: "float32x4" }, { shaderLocation: 6, offset: 48, format: "float32x4" }, { shaderLocation: 7, offset: 64, format: "float32x4" }, { shaderLocation: 8, offset: 80, format: "float32x4" }, { shaderLocation: 9, offset: 96, format: "float32x4" }, { shaderLocation: 10, offset: 112, format: "float32x4" } ] } ]; } else if (skinned8 && standardMaterial) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 12, offset: 0, format: "float32x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: 48, attributes: [ { shaderLocation: 3, offset: 0, format: "uint16x4" }, { shaderLocation: 4, offset: 8, format: "float32x4" }, { shaderLocation: 5, offset: 24, format: "uint16x4" }, { shaderLocation: 6, offset: 32, format: "float32x4" } ] } ]; } else if (skinned8) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 4, offset: 0, format: "float32x4" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 5, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 6, offset: 0, format: "float32x4" }] } ]; } else if (skinned && standardMaterial) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 12, offset: 0, format: "float32x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: 24, attributes: [ { shaderLocation: 3, offset: 0, format: "uint16x4" }, { shaderLocation: 4, offset: 8, format: "float32x4" } ] } ]; } else if (skinned) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 4, offset: 0, format: "float32x4" }] } ]; } else if (standardMaterial) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 12, offset: 0, format: "float32x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] } ]; } else { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 2, offset: 0, format: "float32x2" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 11, offset: 0, format: "float32x2" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 13, offset: 0, format: "float32x4" }] } ]; } pipeline = ctx.device.createRenderPipeline({ layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [ { format: ctx.format, blend: getBlendState(ctx, material.blendMode) } ] }, primitive: { topology: "triangle-list", cullMode: getCullMode(ctx, material.cullMode), frontFace: mirrored ? "cw" : "ccw" }, depthStencil: { format: "depth24plus", depthWriteEnabled: forceNoDepthWrite ? false : material.depthWrite, depthCompare: material.depthTest ? "less" : "always" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getPipelineCacheKey = (ctx, material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite = false, receiveShadow = false) => { const ctorId = getObjectId(ctx, material.constructor); const isBuiltin = material.constructor === UnlitMaterial || material.constructor === StandardMaterial || material.constructor === DataMaterial; const depthWriteKey = forceNoDepthWrite ? "no-depth-write" : material.depthWrite ? "depth-write" : "no-depth-write"; if (material instanceof StandardMaterial) { const plan = material.getLayoutPlan(); return `${ctorId}_${material.blendMode}_${material.cullMode}_${depthWriteKey}_${material.depthTest}_${plan.featureKey}_${mirrored ? "cw" : "ccw"}_${instanced ? "inst" : "mesh"}_${skinned8 ? "skin8" : skinned ? "skin4" : "noskin"}_${receiveShadow ? "shadows" : "no-shadows"}`; } const matKey = isBuiltin ? `${ctorId}` : `${ctorId}_${getObjectId(ctx, material)}`; return `${matKey}_${material.blendMode}_${material.cullMode}_${depthWriteKey}_${material.depthTest}_${mirrored ? "cw" : "ccw"}_${instanced ? "inst" : "mesh"}_${skinned8 ? "skin8" : skinned ? "skin4" : "noskin"}`; }; var isMirroredWorldMatrix = (_ctx, storeF32, base) => { const a00 = storeF32[base + 0]; const a01 = storeF32[base + 4]; const a02 = storeF32[base + 8]; const a10 = storeF32[base + 1]; const a11 = storeF32[base + 5]; const a12 = storeF32[base + 9]; const a20 = storeF32[base + 2]; const a21 = storeF32[base + 6]; const a22 = storeF32[base + 10]; const det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20); return det < 0; }; var getBlendState = (_ctx, mode) => { switch (mode) { case "opaque" /* Opaque */: return void 0; case "transparent" /* Transparent */: return { color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } }; case "additive" /* Additive */: return { color: { srcFactor: "src-alpha", dstFactor: "one", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one", operation: "add" } }; } }; var getCullMode = (_ctx, mode) => { switch (mode) { case "none" /* None */: return "none"; case "back" /* Back */: return "back"; case "front" /* Front */: return "front"; } }; var getPremultipliedAlphaBlendState = (_ctx) => { return { color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } }; }; var bindSizedBuffer = (_ctx, buffer, size, offset = 0) => { return { buffer, offset, size }; }; var getOrCreateShaderModule = (ctx, code) => { let module = ctx.shaderCache.get(code); if (!module) { module = ctx.device.createShaderModule({ code }); ctx.shaderCache.set(code, module); } return module; }; var getMaterialBindGroupKey = (ctx, material) => { if (material instanceof UnlitMaterial) { const bc = material.baseColorTexture; return `unlit:${bc?.id ?? 0}:${bc?.revision ?? 0}`; } if (material instanceof StandardMaterial) { const plan = material.getLayoutPlan(); const parts = ["standard", plan.featureKey]; for (const b of plan.bindings) { if (b.slot === "transmissionSource") parts.push(`src:${ctx.transmissionSourceRevision}`); else { const tex = getMaterialTextureForSlot(material, b.slot); parts.push(`${b.slot}:${tex?.id ?? 0}:${tex?.revision ?? 0}`); } } return parts.join(":"); } if (material instanceof DataMaterial) { const bufId = material.dataBuffer ? getObjectId(ctx, material.dataBuffer) : 0; return `data:${bufId}:${material.getColormapKey()}`; } return "custom"; }; var ensureMaterialBindGroup = (ctx, material) => { if (material instanceof CustomMaterial) { const key2 = getMaterialBindGroupKey(ctx, material); if (material.bindGroup && material.bindGroupKey === key2) return; material.bindGroup = ctx.device.createBindGroup({ layout: material.createBindGroupLayout(ctx.device), entries: material.getBindGroupEntries() }); material.bindGroupKey = key2; if (material.dirty) material.markClean(); return; } if (!material.uniformBuffer) { material.uniformBuffer = ctx.device.createBuffer({ size: material.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); } if (material.dirty) { const data = material.getUniformData(); ctx.queue.writeBuffer(material.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); material.markClean(); } if (material instanceof DataMaterial) { material.upload(ctx.device, ctx.queue); } const key = getMaterialBindGroupKey(ctx, material); if (material.bindGroup && material.bindGroupKey === key) return; const layout = material.createBindGroupLayout(ctx.device); if (material instanceof UnlitMaterial) { const tex = material.baseColorTexture; const sampler = tex ? tex.getSampler(ctx.device, ctx.fallbackSampler) : ctx.fallbackSampler; const view = tex ? tex.getView(ctx.device, ctx.queue, "srgb", ctx.fallbackWhiteViewSrgb) : ctx.fallbackWhiteViewSrgb; material.bindGroup = ctx.device.createBindGroup({ layout, entries: [ { binding: 0, resource: { buffer: material.uniformBuffer } }, { binding: 1, resource: sampler }, { binding: 2, resource: view } ] }); material.bindGroupKey = key; return; } if (material instanceof StandardMaterial) { const plan = material.getLayoutPlan(); const entries = [ { binding: 0, resource: { buffer: material.uniformBuffer } } ]; for (const b of plan.bindings) { if (b.slot === "transmissionSource") { entries.push({ binding: b.samplerBinding, resource: ctx.fallbackSampler }, { binding: b.textureBinding, resource: ctx.transmissionSourceView ?? ctx.fallbackWhiteViewLinear }); continue; } const tex = getMaterialTextureForSlot(material, b.slot); let fallbackView = ctx.fallbackWhiteViewLinear; if (b.colorSpace === "srgb") fallbackView = ctx.fallbackWhiteViewSrgb; else if (b.slot === "metallicRoughness") fallbackView = ctx.fallbackMRViewLinear; else if (b.slot === "normal" || b.slot === "clearcoatNormal") fallbackView = ctx.fallbackNormalViewLinear; else if (b.slot === "occlusion") fallbackView = ctx.fallbackOcclusionViewLinear; else if (b.slot === "anisotropy") fallbackView = ctx.fallbackAnisotropyViewLinear; const sampler = tex ? tex.getSampler(ctx.device, ctx.fallbackSampler) : ctx.fallbackSampler; const view = tex ? tex.getView(ctx.device, ctx.queue, b.colorSpace, fallbackView) : fallbackView; entries.push({ binding: b.samplerBinding, resource: sampler }, { binding: b.textureBinding, resource: view }); } material.bindGroup = ctx.device.createBindGroup({ layout, entries }); material.bindGroupKey = key; return; } if (material instanceof DataMaterial) { if (!ctx.dataMaterialDummyDataBuffer) { ctx.dataMaterialDummyDataBuffer = ctx.device.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); ctx.queue.writeBuffer(ctx.dataMaterialDummyDataBuffer, 0, new Uint8Array(4)); } const dataBuffer = material.dataBuffer ?? ctx.dataMaterialDummyDataBuffer; const cmap = material.getColormapForBinding().getGPUResources(ctx.device, ctx.queue); material.bindGroup = ctx.device.createBindGroup({ layout, entries: [ { binding: 0, resource: { buffer: material.uniformBuffer } }, { binding: 1, resource: { buffer: dataBuffer } }, { binding: 2, resource: cmap.sampler }, { binding: 3, resource: cmap.view } ] }); material.bindGroupKey = key; return; } material.bindGroup = ctx.device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer: material.uniformBuffer } }] }); material.bindGroupKey = key; }; var materialSupportsInstancing = (_ctx, material) => material instanceof UnlitMaterial || material instanceof StandardMaterial; var materialSupportsSkinning = (_ctx, material) => material instanceof UnlitMaterial || material instanceof StandardMaterial; // typescript/core/objects.ts var warmMeshDrawList = (ctx, items) => { let lastMaterial = null; let lastGeometry = null; let lastVertexSourceId = -1; for (let i = 0; i < items.length; ) { const first = items[i]; const material = first.material; const geometry = first.geometry; const vertexSourceId = first.vertexSourceId; let j = i + 1; while (j < items.length) { const it = items[j]; if (it.pipeline !== first.pipeline) break; if (it.material !== material) break; if (it.vertexSourceId !== vertexSourceId) break; j++; } const runCount = j - i; const vertexSourceChanged = geometry !== lastGeometry || vertexSourceId !== lastVertexSourceId; if (vertexSourceChanged) { geometry.upload(ctx.device); getMeshVertexBuffers(first.mesh, ctx.device, ctx.queue); lastGeometry = geometry; lastVertexSourceId = vertexSourceId; } else if (hasMeshMorphRuntime(first.mesh)) getMeshVertexBuffers(first.mesh, ctx.device, ctx.queue); if (material !== lastMaterial) { ensureMaterialBindGroup(ctx, material); lastMaterial = material; } const canInstance = runCount > 1 && !first.skinned && !hasMeshMorphRuntime(first.mesh) && materialSupportsInstancing(ctx, material) && items === ctx.opaqueDrawList; if (canInstance) { getOrCreatePipeline(ctx, material, true, false, false, first.mirrored, false, first.receiveShadow); warmInstancedRunResources(ctx, items, i, runCount); } else if (first.skinned) { for (let k = i; k < j; k++) { const skin = items[k].mesh.skin; if (skin) warmSkinResources(ctx, skin); } } i = j; } }; var warmSkinResources = (ctx, skin) => { if (!skin) return; if (ctx.framePreparedSkins.has(skin)) return; skin.ensureGpuResources(ctx.device, ctx.skinBindGroupLayout); const jointCount = skin.jointCount | 0; const jointMatPtr = frameArena.allocF32(jointCount * 16); animf.computeJointMatricesTo(jointMatPtr, skin.skin.jointIndicesPtr, jointCount, skin.skin.invBindPtr, TransformStore.global().worldPtr, skin.meshWorldMatrixPtr); const bytes = driver.bytes(); ctx.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); ctx.framePreparedSkins.add(skin); ctx.frameSkinPreparationCount++; }; var warmInstancedRunResources = (ctx, items, start, count) => { const ptrsPtr = frameArena.alloc(count * 4, 4); const u32 = TransformStore.global().u32(); const ptrsBase = ptrsPtr >>> 2; for (let i = 0; i < count; i++) u32[ptrsBase + i] = items[start + i].mesh.transform.worldMatrixPtr >>> 0; const outPtr = frameArena.allocF32(count * 32); transformf.packModelNormalMat4FromPtrs(outPtr, ptrsPtr, count); const outBytes = count * ctx.INSTANCE_STRIDE_BYTES; const dstOffset = ctx.instanceBufferOffset; const dstEnd = dstOffset + outBytes; ensureInstanceBuffer(ctx, dstEnd); const bytes = driver.bytes(); ctx.queue.writeBuffer(ctx.instanceBuffer, dstOffset, bytes, outPtr, outBytes); ctx.instanceBufferOffset = dstEnd; }; var warmPointCloudDrawList = (ctx, items) => { for (const item of items) { const cloud = item.cloud; if (!cloud.visible) continue; if (cloud.pointCount <= 0) continue; ensurePointCloudBindGroup(ctx, cloud); } }; var warmSplatFieldDrawList = (ctx, items) => { for (const item of items) { const field = item.field; if (!field.visible) continue; if (field.splatCount <= 0) continue; ensureSplatFieldBindGroup(ctx, field); } }; var warmGlyphFieldDrawList = (ctx, items) => { for (const item of items) { const field = item.field; if (!field.visible) continue; if (field.instanceCount <= 0) continue; item.geometry.upload(ctx.device); ensureGlyphFieldBindGroup(ctx, field); } }; var warmNodeLinkDrawList = (ctx, items) => { for (const item of items) { ensureNodeLinkBindGroup(ctx, item.link); if (item.geometry) item.geometry.upload(ctx.device); } }; var warmLatticeSpaceDrawList = (ctx, items) => { for (const item of items) { if (!item.space.visible || item.space.drawCellCount <= 0) continue; ensureLatticeSpaceBindGroup(ctx, item.space); } }; var executeDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastMaterial = null; let lastGeometry = null; let lastVertexSourceId = -1; for (let i = 0; i < items.length; ) { const first = items[i]; const pipeline = first.pipeline; const material = first.material; const geometry = first.geometry; const vertexSourceId = first.vertexSourceId; let j = i + 1; while (j < items.length) { const it = items[j]; if (it.pipeline !== pipeline) break; if (it.material !== material) break; if (it.vertexSourceId !== vertexSourceId) break; j++; } const runCount = j - i; const vertexSourceChanged = pipeline !== lastPipeline || geometry !== lastGeometry || vertexSourceId !== lastVertexSourceId; if (vertexSourceChanged) geometry.upload(ctx.device); if (material !== lastMaterial) ensureMaterialBindGroup(ctx, material); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (material !== lastMaterial) { pass.setBindGroup(1, material.bindGroup); lastMaterial = material; } if (vertexSourceChanged) { const buffers = getMeshVertexBuffers(first.mesh, ctx.device, ctx.queue); pass.setVertexBuffer(0, buffers.positionBuffer); pass.setVertexBuffer(1, buffers.normalBuffer); pass.setVertexBuffer(2, geometry.uvBuffer); pass.setVertexBuffer(3, geometry.uv1Buffer); const standardMaterial = material instanceof StandardMaterial; if (standardMaterial) { pass.setVertexBuffer(4, geometry.tangentBuffer); pass.setVertexBuffer(5, buffers.colorBuffer); } else pass.setVertexBuffer(4, buffers.colorBuffer); if (first.skinned) { if (standardMaterial) pass.setVertexBuffer(6, geometry.skinInfluenceBuffer); else { pass.setVertexBuffer(5, geometry.jointsBuffer); pass.setVertexBuffer(6, geometry.weightsBuffer); if (first.skinned8) { pass.setVertexBuffer(7, geometry.joints1Buffer); pass.setVertexBuffer(8, geometry.weights1Buffer); } } } if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; lastVertexSourceId = vertexSourceId; } else if (hasMeshMorphRuntime(first.mesh)) getMeshVertexBuffers(first.mesh, ctx.device, ctx.queue); const canInstance = runCount > 1 && !first.skinned && !hasMeshMorphRuntime(first.mesh) && materialSupportsInstancing(ctx, material) && items === ctx.opaqueDrawList; if (canInstance) { const instancedPipeline = getOrCreatePipeline(ctx, material, true, false, false, first.mirrored, false, first.receiveShadow); if (instancedPipeline !== lastPipeline) { pass.setPipeline(instancedPipeline); lastPipeline = instancedPipeline; } drawInstancedRun(ctx, pass, geometry, material, items, i, runCount); } else { for (let k = i; k < j; k++) { const mesh = items[k].mesh; const skin = first.skinned ? mesh.skin : null; if (skin) { warmSkinResources(ctx, skin); pass.setBindGroup(2, skin.bindGroup); } if (first.receiveShadow) ctx.shadowRenderer.bindReceiver(pass, first.skinned); bindModelUniform(ctx, pass, mesh.transform.worldMatrixPtr); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount); else pass.draw(geometry.vertexCount); } } i = j; } if (items === ctx.opaqueDrawList && ctx.instanceRunCache.length > ctx.instanceRunCacheIndex) ctx.instanceRunCache.length = ctx.instanceRunCacheIndex; }; var executePointCloudDrawList = (ctx, pass, items) => { if (items.length === 0) return; let lastPipeline = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const cloud = item.cloud; if (!cloud.visible) continue; if (cloud.pointCount <= 0) continue; ensurePointCloudBindGroup(ctx, cloud); if (!cloud.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; } bindModelUniform(ctx, pass, cloud.transform.worldMatrixPtr); pass.setBindGroup(1, cloud.bindGroup); pass.draw(6, cloud.pointCount); } }; var executeSplatFieldDrawList = (ctx, pass, items) => { if (items.length === 0) return; let lastPipeline = null; let lastField = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const field = item.field; if (!field.visible) continue; if (field.splatCount <= 0) continue; ensureSplatFieldBindGroup(ctx, field); if (!field.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; lastField = null; } if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); pass.draw(6, field.splatCount); } }; var executeLatticeSpaceDrawList = (ctx, pass, items) => { if (items.length === 0) return; let lastPipeline = null; for (const item of items) { const space = item.space; if (!space.visible || space.drawCellCount <= 0) continue; ensureLatticeSpaceBindGroup(ctx, space); if (!space.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; } bindModelUniform(ctx, pass, space.transform.worldMatrixPtr); pass.setBindGroup(1, space.bindGroup); if (space.dimensionCount === 2) pass.draw(6); else pass.draw(36, space.drawCellCount); } }; var executeGlyphFieldDrawList = (ctx, pass, list) => { if (list.length === 0) return; let lastPipeline = null; let lastGeometry = null; let lastField = null; for (let i = 0; i < list.length; i++) { const item = list[i]; const field = item.field; const geometry = item.geometry; if (!field.visible) continue; if (field.instanceCount <= 0) continue; ensureGlyphFieldBindGroup(ctx, field); if (!field.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; lastGeometry = null; lastField = null; } if (geometry !== lastGeometry) { geometry.upload(ctx.device); pass.setVertexBuffer(0, geometry.positionBuffer); pass.setVertexBuffer(1, geometry.normalBuffer); if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; } if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, field.instanceCount); else pass.draw(geometry.vertexCount, field.instanceCount); } }; var executeNodeLinkDrawList = (ctx, pass, list) => { if (list.length === 0) return; let lastPipeline = null; let lastGeometry = null; let lastLink = null; for (let i = 0; i < list.length; i++) { const item = list[i]; const link = item.link; ensureNodeLinkBindGroup(ctx, link); if (!link.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; lastGeometry = null; lastLink = null; } if (item.geometry && item.geometry !== lastGeometry) { item.geometry.upload(ctx.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); pass.setVertexBuffer(1, item.geometry.normalBuffer); if (item.geometry.isIndexed) pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); lastGeometry = item.geometry; } if (link !== lastLink) { pass.setBindGroup(1, link.bindGroup); lastLink = link; } bindModelUniform(ctx, pass, link.transform.worldMatrixPtr); if (item.passKind === "node-points") { pass.draw(6, link.nodeCount); } else if (item.passKind === "edge-lines") { pass.draw(2, link.edgeCount); } else if (item.passKind === "node-solid") { if (!item.geometry) continue; if (item.geometry.isIndexed) pass.drawIndexed(item.geometry.indexCount, link.nodeCount); else pass.draw(item.geometry.vertexCount, link.nodeCount); } else { if (!item.geometry) continue; if (item.geometry.isIndexed) pass.drawIndexed(item.geometry.indexCount, link.edgeCount); else pass.draw(item.geometry.vertexCount, link.edgeCount); } } }; var drawInstancedRun = (ctx, pass, geometry, material, items, start, count) => { const outBytes = count * ctx.INSTANCE_STRIDE_BYTES; const dstOffset = ctx.instanceBufferOffset; const dstEnd = dstOffset + outBytes; ensureInstanceBuffer(ctx, dstEnd); const cacheIndex = ctx.instanceRunCacheIndex++; let cache = ctx.instanceRunCache[cacheIndex]; const store = TransformStore.global(); const world = store.f32(); let reusable = !!cache && cache.generation === ctx.instanceBufferGeneration && cache.offset === dstOffset && cache.count === count; if (reusable) { for (let i = 0; i < count && reusable; i++) { const mesh = items[start + i].mesh; if (cache.meshes[i] !== mesh) { reusable = false; break; } const base = mesh.transform.worldMatrixPtr >>> 2; const snapshotBase = i * 16; for (let j = 0; j < 16; j++) if (!Object.is(cache.matrices[snapshotBase + j], world[base + j])) { reusable = false; break; } } } if (!reusable) { const ptrsPtr = frameArena.alloc(count * 4, 4); const u32 = store.u32(); const ptrsBase = ptrsPtr >>> 2; if (!cache || cache.count !== count) { cache = { generation: ctx.instanceBufferGeneration, offset: dstOffset, count, meshes: new Array(count), matrices: new Float32Array(count * 16) }; ctx.instanceRunCache[cacheIndex] = cache; } cache.generation = ctx.instanceBufferGeneration; cache.offset = dstOffset; cache.count = count; for (let i = 0; i < count; i++) { const mesh = items[start + i].mesh; const ptr = mesh.transform.worldMatrixPtr; u32[ptrsBase + i] = ptr >>> 0; cache.meshes[i] = mesh; cache.matrices.set(world.subarray(ptr >>> 2, (ptr >>> 2) + 16), i * 16); } const outPtr = frameArena.allocF32(count * 32); transformf.packModelNormalMat4FromPtrs(outPtr, ptrsPtr, count); ctx.queue.writeBuffer(ctx.instanceBuffer, dstOffset, driver.bytes(), outPtr, outBytes); ctx.instanceRunUploadCount++; } pass.setBindGroup(0, ctx.modelUniformBindGroup, [0]); if (items[start].receiveShadow) ctx.shadowRenderer.bindReceiver(pass, false); if (material instanceof StandardMaterial) { pass.setVertexBuffer(4, geometry.tangentBuffer); pass.setVertexBuffer(5, geometry.colorBuffer); pass.setVertexBuffer(6, ctx.instanceBuffer, dstOffset, outBytes); } else { pass.setVertexBuffer(4, geometry.colorBuffer); pass.setVertexBuffer(5, ctx.instanceBuffer, dstOffset, outBytes); } if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, count); else pass.draw(geometry.vertexCount, count); ctx.instanceBufferOffset = dstEnd; }; var getOrCreateSplatFieldSortState = (ctx, field) => { let state = ctx.splatFieldSortStates.get(field); if (!state) { state = { sortedIndexBuffer: null, sortedIndexCapacity: 0, transformBuffer: null, lastMvp: new Float32Array(16), lastRevision: -1, lastCount: -1, valid: false, sortCount: 0, radixBindGroupKey: null, radixBindGroups: [] }; ctx.splatFieldSortStates.set(field, state); } if (!state.transformBuffer) state.transformBuffer = ctx.device.createBuffer({ size: 16 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); if (field.splatCount > state.sortedIndexCapacity) { state.sortedIndexBuffer?.destroy(); let cap = Math.max(1, state.sortedIndexCapacity || 256); while (cap < field.splatCount) cap *= 2; state.sortedIndexCapacity = cap; state.sortedIndexBuffer = ctx.device.createBuffer({ size: cap * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC }); state.valid = false; state.radixBindGroupKey = null; field.bindGroupKey = null; } return state; }; var destroySplatFieldSortState = (_ctx, field, state) => { state.sortedIndexBuffer?.destroy(); state.transformBuffer?.destroy(); field.bindGroup = null; field.bindGroupKey = null; }; var ensureSplatSortCapacity = (ctx, count) => { if (count <= ctx.splatSortCapacity) return; let cap = Math.max(1, ctx.splatSortCapacity || 256); while (cap < count) cap *= 2; const keyUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; const indexUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; ctx.splatSortKeyA?.destroy(); ctx.splatSortKeyB?.destroy(); ctx.splatSortIndexA?.destroy(); ctx.splatSortIndexB?.destroy(); ctx.splatSortPrefix?.destroy(); ctx.splatSortCapacity = cap; ctx.splatSortKeyA = ctx.device.createBuffer({ size: cap * 4, usage: keyUsage }); ctx.splatSortKeyB = ctx.device.createBuffer({ size: cap * 4, usage: keyUsage }); ctx.splatSortIndexA = ctx.device.createBuffer({ size: cap * 4, usage: indexUsage }); ctx.splatSortIndexB = ctx.device.createBuffer({ size: cap * 4, usage: indexUsage }); ctx.splatSortPrefix = ctx.device.createBuffer({ size: cap * 4, usage: GPUBufferUsage.STORAGE }); }; var ensureSplatSortScanLevel = (ctx, level, count) => { while (ctx.splatSortScanLevels.length <= level) ctx.splatSortScanLevels.push({ blockSums: null, blockSumsCapacity: 0, blockOffsets: null, blockOffsetsCapacity: 0 }); const scanLevel = ctx.splatSortScanLevels[level]; if (count > scanLevel.blockSumsCapacity) { scanLevel.blockSums?.destroy(); let cap = Math.max(1, scanLevel.blockSumsCapacity || 1); while (cap < count) cap *= 2; scanLevel.blockSumsCapacity = cap; scanLevel.blockSums = ctx.device.createBuffer({ size: cap * 4, usage: GPUBufferUsage.STORAGE }); } if (count > scanLevel.blockOffsetsCapacity) { scanLevel.blockOffsets?.destroy(); let cap = Math.max(1, scanLevel.blockOffsetsCapacity || 1); while (cap < count) cap *= 2; scanLevel.blockOffsetsCapacity = cap; scanLevel.blockOffsets = ctx.device.createBuffer({ size: cap * 4, usage: GPUBufferUsage.STORAGE }); } return scanLevel; }; var ensureSplatSortFrameCapacity = (ctx, count, level = 0) => { if (count <= 0) return; if (level === 0) ensureSplatSortCapacity(ctx, count); const numBlocks = ceilDiv(count, 512); ensureSplatSortScanLevel(ctx, level, numBlocks); if (numBlocks > 1) ensureSplatSortFrameCapacity(ctx, numBlocks, level + 1); }; var getSplatFieldBindGroupLayout = (ctx) => { if (ctx.splatFieldBindGroupLayout) return ctx.splatFieldBindGroupLayout; ctx.splatFieldBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 5, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 16 } }, { binding: 6, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } } ] }); return ctx.splatFieldBindGroupLayout; }; var getOrCreateSplatFieldPipeline = (ctx) => { const key = `splatfield:${ctx.format}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; const shaderModule = getOrCreateShaderModule(ctx, splatfield_default); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getSplatFieldBindGroupLayout(ctx)] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: ctx.format, blend: getPremultipliedAlphaBlendState(ctx) }] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getSplatFieldBindGroupKey = (ctx, field, state) => { const centerOpacity = field.centerOpacityBuffer; const rotation = field.rotationBuffer; const scale = field.scaleBuffer; const color = field.colorBuffer; const uniform = field.uniformBuffer; const sorted = state.sortedIndexBuffer; const sh = field.shBuffer; return `splatfield:${centerOpacity ? getObjectId(ctx, centerOpacity) : 0}:${rotation ? getObjectId(ctx, rotation) : 0}:${scale ? getObjectId(ctx, scale) : 0}:${color ? getObjectId(ctx, color) : 0}:${sorted ? getObjectId(ctx, sorted) : 0}:${uniform ? getObjectId(ctx, uniform) : 0}:${sh ? getObjectId(ctx, sh) : 0}`; }; var ensureSplatFieldBindGroup = (ctx, field) => { field.upload(ctx.device, ctx.queue); if (!field.centerOpacityBuffer || !field.rotationBuffer || !field.scaleBuffer || !field.colorBuffer) return; if (field.splatCount <= 0) return; const state = getOrCreateSplatFieldSortState(ctx, field); if (!state.sortedIndexBuffer) return; if (!field.uniformBuffer) { field.uniformBuffer = ctx.device.createBuffer({ size: field.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); field.bindGroupKey = null; } if (field.dirtyUniforms) { const data = field.getUniformData(); ctx.queue.writeBuffer(field.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); field.markUniformsClean(); } if (!ctx.splatFieldDummySHBuffer) ctx.splatFieldDummySHBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const key = getSplatFieldBindGroupKey(ctx, field, state); if (field.bindGroup && field.bindGroupKey === key) return; field.bindGroup = ctx.device.createBindGroup({ layout: getSplatFieldBindGroupLayout(ctx), entries: [ { binding: 0, resource: { buffer: field.centerOpacityBuffer } }, { binding: 1, resource: { buffer: field.rotationBuffer } }, { binding: 2, resource: { buffer: field.scaleBuffer } }, { binding: 3, resource: { buffer: field.colorBuffer } }, { binding: 4, resource: { buffer: state.sortedIndexBuffer } }, { binding: 5, resource: { buffer: field.uniformBuffer } }, { binding: 6, resource: { buffer: field.shBuffer ?? ctx.splatFieldDummySHBuffer } } ] }); field.bindGroupKey = key; }; var getSplatSortKeygenBindGroupLayout = (ctx) => { if (ctx.splatSortKeygenBindGroupLayout) return ctx.splatSortKeygenBindGroupLayout; ctx.splatSortKeygenBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform", minBindingSize: 64 } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); return ctx.splatSortKeygenBindGroupLayout; }; var getSplatSortFlagsBindGroupLayout = (ctx) => { if (ctx.splatSortFlagsBindGroupLayout) return ctx.splatSortFlagsBindGroupLayout; ctx.splatSortFlagsBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); return ctx.splatSortFlagsBindGroupLayout; }; var getSplatSortScanBlockBindGroupLayout = (ctx) => { if (ctx.splatSortScanBlockBindGroupLayout) return ctx.splatSortScanBlockBindGroupLayout; ctx.splatSortScanBlockBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); return ctx.splatSortScanBlockBindGroupLayout; }; var getSplatSortScanAddBindGroupLayout = (ctx) => { if (ctx.splatSortScanAddBindGroupLayout) return ctx.splatSortScanAddBindGroupLayout; ctx.splatSortScanAddBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } } ] }); return ctx.splatSortScanAddBindGroupLayout; }; var getSplatSortScatterBindGroupLayout = (ctx) => { if (ctx.splatSortScatterBindGroupLayout) return ctx.splatSortScatterBindGroupLayout; ctx.splatSortScatterBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); return ctx.splatSortScatterBindGroupLayout; }; var getOrCreateSplatSortKeygenPipeline = (ctx) => { const key = "splat:sort:keygen"; const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getSplatSortKeygenBindGroupLayout(ctx)] }), compute: { module: getOrCreateShaderModule(ctx, splatfield_sort_default), entryPoint: "main" } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateSplatSortFlagsPipeline = (ctx, bit) => { const key = `splat:sort:flags:${bit | 0}`; const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getSplatSortFlagsBindGroupLayout(ctx)] }), compute: { module: getOrCreateShaderModule(ctx, splatfield_radix_flags_default), entryPoint: "main", constants: { BIT: bit | 0 } } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateSplatSortScanBlockPipeline = (ctx) => { const key = "splat:sort:scan:blockExclusiveU32"; const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getSplatSortScanBlockBindGroupLayout(ctx)] }), compute: { module: getOrCreateShaderModule(ctx, scan_block_exclusive_u32_default), entryPoint: "main" } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateSplatSortScanAddPipeline = (ctx) => { const key = "splat:sort:scan:addOffsetsU32"; const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getSplatSortScanAddBindGroupLayout(ctx)] }), compute: { module: getOrCreateShaderModule(ctx, scan_add_block_offsets_u32_default), entryPoint: "main" } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateSplatSortScatterPipeline = (ctx, bit) => { const key = `splat:sort:scatter:${bit | 0}`; const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getSplatSortScatterBindGroupLayout(ctx)] }), compute: { module: getOrCreateShaderModule(ctx, splatfield_radix_scatter_pairs_default), entryPoint: "main", constants: { BIT: bit | 0 } } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var encodeSplatSortScanExclusive = (ctx, pass, input, count, out, level = 0) => { if (count <= 0) return; const numBlocks = ceilDiv(count, 512); const scanLevel = ensureSplatSortScanLevel(ctx, level, numBlocks); const scanBlocksBg = ctx.device.createBindGroup({ layout: getSplatSortScanBlockBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, input, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, out, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, scanLevel.blockSums, numBlocks * 4) } ] }); pass.setPipeline(getOrCreateSplatSortScanBlockPipeline(ctx)); pass.setBindGroup(0, scanBlocksBg); pass.dispatchWorkgroups(numBlocks, 1, 1); if (numBlocks <= 1) return; encodeSplatSortScanExclusive(ctx, pass, scanLevel.blockSums, numBlocks, scanLevel.blockOffsets, level + 1); const addOffsetsBg = ctx.device.createBindGroup({ layout: getSplatSortScanAddBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, out, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, scanLevel.blockOffsets, numBlocks * 4) } ] }); pass.setPipeline(getOrCreateSplatSortScanAddPipeline(ctx)); pass.setBindGroup(0, addOffsetsBg); pass.dispatchWorkgroups(ceilDiv(count, 256), 1, 1); }; var encodeSplatFieldSort = (ctx, pass, field, state) => { if (!field.centerOpacityBuffer) return null; if (!state.transformBuffer || !state.sortedIndexBuffer) return null; const count = field.splatCount | 0; if (count <= 0) return null; ensureSplatSortCapacity(ctx, count); const mvpPtr = frameArena.allocF32(16); mat4f.mul(mvpPtr, ctx.cameraUniformStagingPtr, field.transform.worldMatrixPtr); ctx.queue.writeBuffer(state.transformBuffer, 0, driver.bytes(), mvpPtr, 16 * 4); const blocks = ceilDiv(count, 1024); const scan = ensureSplatSortScanLevel(ctx, 0, blocks); const bindGroupKey = [count, field.centerOpacityBuffer, state.transformBuffer, ctx.splatSortKeyA, ctx.splatSortKeyB, ctx.splatSortIndexA, ctx.splatSortIndexB, ctx.splatSortPrefix, scan.blockSums, scan.blockOffsets].map((value) => typeof value === "number" ? value : getObjectId(ctx, value)).join(":"); if (state.radixBindGroupKey !== bindGroupKey) { const flags = (keys) => ctx.device.createBindGroup({ layout: getSplatSortFlagsBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, keys, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, ctx.splatSortPrefix, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, scan.blockSums, blocks * 4) } ] }); const scatter = (keysIn, valuesIn, keysOut, valuesOut) => ctx.device.createBindGroup({ layout: getSplatSortScatterBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, keysIn, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, valuesIn, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, ctx.splatSortPrefix, count * 4) }, { binding: 3, resource: bindSizedBuffer(ctx, keysOut, count * 4) }, { binding: 4, resource: bindSizedBuffer(ctx, valuesOut, count * 4) } ] }); state.radixBindGroups = [ ctx.device.createBindGroup({ layout: getSplatSortKeygenBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, field.centerOpacityBuffer, count * 16) }, { binding: 1, resource: { buffer: state.transformBuffer } }, { binding: 2, resource: bindSizedBuffer(ctx, ctx.splatSortKeyA, count * 4) }, { binding: 3, resource: bindSizedBuffer(ctx, ctx.splatSortIndexA, count * 4) } ] }), flags(ctx.splatSortKeyA), flags(ctx.splatSortKeyB), scatter(ctx.splatSortKeyA, ctx.splatSortIndexA, ctx.splatSortKeyB, ctx.splatSortIndexB), scatter(ctx.splatSortKeyB, ctx.splatSortIndexB, ctx.splatSortKeyA, ctx.splatSortIndexA), blocks > 1 ? ctx.device.createBindGroup({ layout: getSplatSortScanAddBindGroupLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, ctx.splatSortPrefix, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, scan.blockOffsets, blocks * 4) } ] }) : null ]; state.radixBindGroupKey = bindGroupKey; } pass.setPipeline(getOrCreateSplatSortKeygenPipeline(ctx)); pass.setBindGroup(0, state.radixBindGroups[0]); pass.dispatchWorkgroups(ceilDiv(count, 256), 1, 1); let keyIn = ctx.splatSortKeyA; let keyOut = ctx.splatSortKeyB; let valueIn = ctx.splatSortIndexA; let valueOut = ctx.splatSortIndexB; for (let bit = 0; bit < 32; bit++) { pass.setPipeline(getOrCreateSplatSortFlagsPipeline(ctx, bit)); pass.setBindGroup(0, state.radixBindGroups[1 + (bit & 1)]); pass.dispatchWorkgroups(blocks, 1, 1); if (blocks > 1) { encodeSplatSortScanExclusive(ctx, pass, scan.blockSums, blocks, scan.blockOffsets, 1); pass.setPipeline(getOrCreateSplatSortScanAddPipeline(ctx)); pass.setBindGroup(0, state.radixBindGroups[5]); pass.dispatchWorkgroups(ceilDiv(count, 256), 1, 1); } pass.setPipeline(getOrCreateSplatSortScatterPipeline(ctx, bit)); pass.setBindGroup(0, state.radixBindGroups[3 + (bit & 1)]); pass.dispatchWorkgroups(ceilDiv(count, 256), 1, 1); const nextKeyIn = keyOut; keyOut = keyOut === ctx.splatSortKeyA ? ctx.splatSortKeyB : ctx.splatSortKeyA; keyIn = nextKeyIn; const nextValueIn = valueOut; valueOut = valueOut === ctx.splatSortIndexA ? ctx.splatSortIndexB : ctx.splatSortIndexA; valueIn = nextValueIn; } return valueIn; }; var encodeSplatFieldSorts = (ctx, encoder) => { if (ctx.transparentSplatFieldDrawList.length === 0) return; let maxCount = 0; for (const item of ctx.transparentSplatFieldDrawList) { const field = item.field; if (!field.visible) continue; if (field.splatCount <= 0) continue; if (field.splatCount > maxCount) maxCount = field.splatCount; } ensureSplatSortFrameCapacity(ctx, maxCount); for (const item of ctx.transparentSplatFieldDrawList) { const field = item.field; field.upload(ctx.device, ctx.queue); if (!field.centerOpacityBuffer || !field.rotationBuffer || !field.scaleBuffer) continue; if (field.splatCount <= 0) continue; const state = getOrCreateSplatFieldSortState(ctx, field); const signaturePtr = frameArena.allocF32(16); mat4f.mul(signaturePtr, ctx.cameraUniformStagingPtr, field.transform.worldMatrixPtr); const signature = wasm.f32view(signaturePtr, 16); let unchanged = field.sortCacheable && state.valid && state.lastRevision === field.sortRevision && state.lastCount === field.splatCount; for (let i = 0; i < 16 && unchanged; i++) if (!Object.is(state.lastMvp[i], signature[i])) unchanged = false; if (unchanged) continue; const computePass = encoder.beginComputePass(); const finalIndices = encodeSplatFieldSort(ctx, computePass, field, state); computePass.end(); if (finalIndices && state.sortedIndexBuffer) { encoder.copyBufferToBuffer(finalIndices, 0, state.sortedIndexBuffer, 0, field.splatCount * 4); state.lastMvp.set(signature); state.lastRevision = field.sortRevision; state.lastCount = field.splatCount; state.valid = field.sortCacheable; state.sortCount++; } } }; var getOrCreateLatticeSpaceSortState = (ctx, space) => { let state = ctx.latticeSpaceSortStates.get(space); if (!state) { state = { sortedIndexBuffer: null, sortedIndexCapacity: 0, identityKey: null, transformBuffer: null, lastMvp: new Float32Array(16), lastRevision: -1, lastCount: -1, valid: false, sortCount: 0, radixBindGroupKey: null, radixBindGroups: [] }; ctx.latticeSpaceSortStates.set(space, state); } if (!state.transformBuffer) state.transformBuffer = ctx.device.createBuffer({ label: "LatticeSpace.sortTransform", size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); if (space.drawCellCount > state.sortedIndexCapacity) { state.sortedIndexBuffer?.destroy(); let capacity = Math.max(256, state.sortedIndexCapacity || 256); while (capacity < space.drawCellCount) capacity *= 2; state.sortedIndexCapacity = capacity; state.sortedIndexBuffer = ctx.device.createBuffer({ label: "LatticeSpace.sortedIndices", size: capacity * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC }); state.valid = false; state.radixBindGroupKey = null; state.identityKey = null; } const range = space.indexRange; const identityKey = `${space.dimensionCount}:${range.min.join(",")}:${range.max.join(",")}`; const needsIdentity = space.dimensionCount !== 3 || space.blendMode === "opaque" /* Opaque */; if (needsIdentity && state.sortedIndexBuffer && state.identityKey !== identityKey) { const sizeX = range.max[0] - range.min[0]; const sizeY = range.max[1] - range.min[1]; const indices = new Uint32Array(space.drawCellCount); for (let ordinal = 0; ordinal < indices.length; ordinal++) { const x = range.min[0] + ordinal % sizeX; const y = range.min[1] + Math.floor(ordinal / sizeX) % sizeY; const z = space.dimensionCount === 3 ? (range.min[2] ?? 0) + Math.floor(ordinal / (sizeX * sizeY)) : 0; indices[ordinal] = space.mapCellIndexToLinear(space.dimensionCount === 3 ? [x, y, z] : [x, y]); } ctx.queue.writeBuffer(state.sortedIndexBuffer, 0, indices); state.identityKey = identityKey; space.bindGroupKey = null; } return state; }; var destroyLatticeSpaceSortState = (_ctx, space, state) => { state.sortedIndexBuffer?.destroy(); state.transformBuffer?.destroy(); space.bindGroup = null; space.bindGroupKey = null; }; var ensureLatticeSortCapacity = (ctx, count) => { if (count <= ctx.latticeSortCapacity) return; let capacity = Math.max(256, ctx.latticeSortCapacity || 256); while (capacity < count) capacity *= 2; for (const buffer of [ctx.latticeSortKeyA, ctx.latticeSortKeyB, ctx.latticeSortIndexA, ctx.latticeSortIndexB, ctx.latticeSortPrefix]) buffer?.destroy(); const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; ctx.latticeSortCapacity = capacity; ctx.latticeSortKeyA = ctx.device.createBuffer({ size: capacity * 4, usage }); ctx.latticeSortKeyB = ctx.device.createBuffer({ size: capacity * 4, usage }); ctx.latticeSortIndexA = ctx.device.createBuffer({ size: capacity * 4, usage }); ctx.latticeSortIndexB = ctx.device.createBuffer({ size: capacity * 4, usage }); ctx.latticeSortPrefix = ctx.device.createBuffer({ size: capacity * 4, usage: GPUBufferUsage.STORAGE }); }; var ensureLatticeSortScanLevel = (ctx, level, count) => { while (ctx.latticeSortScanLevels.length <= level) ctx.latticeSortScanLevels.push({ blockSums: null, blockSumsCapacity: 0, blockOffsets: null, blockOffsetsCapacity: 0 }); const scan = ctx.latticeSortScanLevels[level]; if (count > scan.blockSumsCapacity) { scan.blockSums?.destroy(); let capacity = Math.max(1, scan.blockSumsCapacity); while (capacity < count) capacity *= 2; scan.blockSumsCapacity = capacity; scan.blockSums = ctx.device.createBuffer({ size: capacity * 4, usage: GPUBufferUsage.STORAGE }); } if (count > scan.blockOffsetsCapacity) { scan.blockOffsets?.destroy(); let capacity = Math.max(1, scan.blockOffsetsCapacity); while (capacity < count) capacity *= 2; scan.blockOffsetsCapacity = capacity; scan.blockOffsets = ctx.device.createBuffer({ size: capacity * 4, usage: GPUBufferUsage.STORAGE }); } return scan; }; var ensureLatticeSortFrameCapacity = (ctx, count, level = 0) => { if (count <= 0) return; if (level === 0) ensureLatticeSortCapacity(ctx, count); const blocks = ceilDiv(count, 512); ensureLatticeSortScanLevel(ctx, level, blocks); if (blocks > 1) ensureLatticeSortFrameCapacity(ctx, blocks, level + 1); }; var getLatticeSpaceBindGroupLayout = (ctx) => { if (ctx.latticeSpaceBindGroupLayout) return ctx.latticeSpaceBindGroupLayout; ctx.latticeSpaceBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, buffer: { type: "uniform", minBindingSize: 368 } }, { binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } } ] }); return ctx.latticeSpaceBindGroupLayout; }; var getOrCreateLatticeSpacePipeline = (ctx, space) => { const key = ["latticespace", `rank=${space.dimensionCount}`, `blend=${space.blendMode}`, `cull=${space.cullMode}`, `depthTest=${space.depthTest ? 1 : 0}`, `depthWrite=${space.depthWrite ? 1 : 0}`, `fmt=${ctx.format}`].join("|"); const cached = ctx.pipelineCache.get(key); if (cached) return cached; const module = getOrCreateShaderModule(ctx, latticespace_default); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getLatticeSpaceBindGroupLayout(ctx)] }), vertex: { module, entryPoint: space.dimensionCount === 2 ? "vs_2d" : "vs_3d", buffers: [] }, fragment: { module, entryPoint: "fs_main", targets: [{ format: ctx.format, blend: getBlendState(ctx, space.blendMode) }] }, primitive: { topology: "triangle-list", cullMode: space.dimensionCount === 2 ? "none" : getCullMode(ctx, space.cullMode) }, depthStencil: { format: "depth24plus", depthWriteEnabled: space.depthWrite, depthCompare: space.depthTest ? "less" : "always" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var ensureLatticeSpaceBindGroup = (ctx, space) => { space.upload(ctx.device, ctx.queue); if (space.colorMode !== "solid" && !space.dataBuffer) return; const state = getOrCreateLatticeSpaceSortState(ctx, space); if (!state.sortedIndexBuffer) return; if (!space.uniformBuffer) { space.uniformBuffer = ctx.device.createBuffer({ label: "LatticeSpace.uniforms", size: space.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); space.bindGroupKey = null; } if (space.dirtyUniforms) { const data = space.getUniformData(); ctx.queue.writeBuffer(space.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); space.markUniformsClean(); } if (!ctx.latticeSpaceDummyF32Buffer) ctx.latticeSpaceDummyF32Buffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); if (!ctx.latticeSpaceDummyU32Buffer) { ctx.latticeSpaceDummyU32Buffer = ctx.device.createBuffer({ size: Math.max(16, space.cellCount * 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const active = new Uint32Array(space.cellCount); active.fill(1); ctx.queue.writeBuffer(ctx.latticeSpaceDummyU32Buffer, 0, active); } const dataBuffer = space.dataBuffer ?? ctx.latticeSpaceDummyF32Buffer; const maskBuffer = space.maskBuffer ?? ctx.latticeSpaceDummyU32Buffer; const colormap = space.getColormapForBinding(); const colormapGPU = colormap.getGPUResources(ctx.device, ctx.queue); const key = `latticespace:${getObjectId(ctx, dataBuffer)}:${getObjectId(ctx, maskBuffer)}:${getObjectId(ctx, state.sortedIndexBuffer)}:${getObjectId(ctx, space.uniformBuffer)}:${space.getColormapKey()}`; if (space.bindGroup && space.bindGroupKey === key) return; space.bindGroup = ctx.device.createBindGroup({ layout: getLatticeSpaceBindGroupLayout(ctx), entries: [ { binding: 0, resource: { buffer: dataBuffer } }, { binding: 1, resource: { buffer: maskBuffer } }, { binding: 2, resource: { buffer: state.sortedIndexBuffer } }, { binding: 3, resource: { buffer: space.uniformBuffer } }, { binding: 4, resource: colormapGPU.sampler }, { binding: 5, resource: colormapGPU.view } ] }); space.bindGroupKey = key; }; var getLatticeSortKeygenLayout = (ctx) => { return ctx.latticeSortKeygenBindGroupLayout ??= ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform", minBindingSize: 368 } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform", minBindingSize: 64 } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); }; var getLatticeSortFlagsLayout = (ctx) => { return ctx.latticeSortFlagsBindGroupLayout ??= ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); }; var getLatticeSortScanBlockLayout = (ctx) => { return ctx.latticeSortScanBlockBindGroupLayout ??= ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); }; var getLatticeSortScanAddLayout = (ctx) => { return ctx.latticeSortScanAddBindGroupLayout ??= ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } } ] }); }; var getLatticeSortScatterLayout = (ctx) => { return ctx.latticeSortScatterBindGroupLayout ??= ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }, { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } } ] }); }; var latticePipeline = (ctx, key, shader, layout, constants) => { const cached = ctx.computePipelineCache.get(key); if (cached) return cached; const pipeline = ctx.device.createComputePipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [layout] }), compute: { module: getOrCreateShaderModule(ctx, shader), entryPoint: "main", ...constants ? { constants } : {} } }); ctx.computePipelineCache.set(key, pipeline); return pipeline; }; var encodeLatticeSortScanExclusive = (ctx, pass, input, count, output, level = 0) => { if (count <= 0) return; const blocks = ceilDiv(count, 512); const scan = ensureLatticeSortScanLevel(ctx, level, blocks); pass.setPipeline(latticePipeline(ctx, "lattice:sort:scan:block", scan_block_exclusive_u32_default, getLatticeSortScanBlockLayout(ctx))); pass.setBindGroup(0, ctx.device.createBindGroup({ layout: getLatticeSortScanBlockLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, input, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, output, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, scan.blockSums, blocks * 4) } ] })); pass.dispatchWorkgroups(blocks); if (blocks <= 1) return; encodeLatticeSortScanExclusive(ctx, pass, scan.blockSums, blocks, scan.blockOffsets, level + 1); pass.setPipeline(latticePipeline(ctx, "lattice:sort:scan:add", scan_add_block_offsets_u32_default, getLatticeSortScanAddLayout(ctx))); pass.setBindGroup(0, ctx.device.createBindGroup({ layout: getLatticeSortScanAddLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, output, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, scan.blockOffsets, blocks * 4) } ] })); pass.dispatchWorkgroups(ceilDiv(count, 256)); }; var encodeLatticeSpaceSort = (ctx, pass, space, state) => { const count = space.drawCellCount; if (space.dimensionCount !== 3 || count <= 0 || !space.uniformBuffer || !state.transformBuffer || !state.sortedIndexBuffer) return null; ensureLatticeSortCapacity(ctx, count); const mvpPtr = frameArena.allocF32(16); mat4f.mul(mvpPtr, ctx.cameraUniformStagingPtr, space.transform.worldMatrixPtr); ctx.queue.writeBuffer(state.transformBuffer, 0, driver.bytes(), mvpPtr, 64); const blocks = ceilDiv(count, 1024); const scan = ensureLatticeSortScanLevel(ctx, 0, blocks); const bindGroupKey = [count, space.uniformBuffer, state.transformBuffer, ctx.latticeSortKeyA, ctx.latticeSortKeyB, ctx.latticeSortIndexA, ctx.latticeSortIndexB, ctx.latticeSortPrefix, scan.blockSums, scan.blockOffsets].map((value) => typeof value === "number" ? value : getObjectId(ctx, value)).join(":"); if (state.radixBindGroupKey !== bindGroupKey) { const flags = (keys) => ctx.device.createBindGroup({ layout: getLatticeSortFlagsLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, keys, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, ctx.latticeSortPrefix, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, scan.blockSums, blocks * 4) } ] }); const scatter = (keysIn, valuesIn, keysOut, valuesOut) => ctx.device.createBindGroup({ layout: getLatticeSortScatterLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, keysIn, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, valuesIn, count * 4) }, { binding: 2, resource: bindSizedBuffer(ctx, ctx.latticeSortPrefix, count * 4) }, { binding: 3, resource: bindSizedBuffer(ctx, keysOut, count * 4) }, { binding: 4, resource: bindSizedBuffer(ctx, valuesOut, count * 4) } ] }); state.radixBindGroups = [ ctx.device.createBindGroup({ layout: getLatticeSortKeygenLayout(ctx), entries: [ { binding: 0, resource: { buffer: space.uniformBuffer } }, { binding: 1, resource: { buffer: state.transformBuffer } }, { binding: 2, resource: bindSizedBuffer(ctx, ctx.latticeSortKeyA, count * 4) }, { binding: 3, resource: bindSizedBuffer(ctx, ctx.latticeSortIndexA, count * 4) } ] }), flags(ctx.latticeSortKeyA), flags(ctx.latticeSortKeyB), scatter(ctx.latticeSortKeyA, ctx.latticeSortIndexA, ctx.latticeSortKeyB, ctx.latticeSortIndexB), scatter(ctx.latticeSortKeyB, ctx.latticeSortIndexB, ctx.latticeSortKeyA, ctx.latticeSortIndexA), blocks > 1 ? ctx.device.createBindGroup({ layout: getLatticeSortScanAddLayout(ctx), entries: [ { binding: 0, resource: bindSizedBuffer(ctx, ctx.latticeSortPrefix, count * 4) }, { binding: 1, resource: bindSizedBuffer(ctx, scan.blockOffsets, blocks * 4) } ] }) : null ]; state.radixBindGroupKey = bindGroupKey; } pass.setPipeline(latticePipeline(ctx, "lattice:sort:keygen", latticespace_sort_default, getLatticeSortKeygenLayout(ctx))); pass.setBindGroup(0, state.radixBindGroups[0]); pass.dispatchWorkgroups(ceilDiv(count, 256)); let keyIn = ctx.latticeSortKeyA; let keyOut = ctx.latticeSortKeyB; let valueIn = ctx.latticeSortIndexA; let valueOut = ctx.latticeSortIndexB; for (let bit = 0; bit < 32; bit++) { pass.setPipeline(latticePipeline(ctx, `lattice:sort:flags:${bit}`, latticespace_radix_flags_default, getLatticeSortFlagsLayout(ctx), { BIT: bit })); pass.setBindGroup(0, state.radixBindGroups[1 + (bit & 1)]); pass.dispatchWorkgroups(blocks); if (blocks > 1) { encodeLatticeSortScanExclusive(ctx, pass, scan.blockSums, blocks, scan.blockOffsets, 1); pass.setPipeline(latticePipeline(ctx, "lattice:sort:scan:add", scan_add_block_offsets_u32_default, getLatticeSortScanAddLayout(ctx))); pass.setBindGroup(0, state.radixBindGroups[5]); pass.dispatchWorkgroups(ceilDiv(count, 256)); } pass.setPipeline(latticePipeline(ctx, `lattice:sort:scatter:${bit}`, latticespace_radix_scatter_pairs_default, getLatticeSortScatterLayout(ctx), { BIT: bit })); pass.setBindGroup(0, state.radixBindGroups[3 + (bit & 1)]); pass.dispatchWorkgroups(ceilDiv(count, 256)); [keyIn, keyOut] = [keyOut, keyIn]; [valueIn, valueOut] = [valueOut, valueIn]; } return valueIn; }; var encodeLatticeSpaceSorts = (ctx, encoder) => { let maximum = 0; for (const item of ctx.transparentLatticeSpaceDrawList) if (item.space.dimensionCount === 3) maximum = Math.max(maximum, item.space.drawCellCount); if (maximum <= 0) return; ensureLatticeSortFrameCapacity(ctx, maximum); for (const item of ctx.transparentLatticeSpaceDrawList) { const space = item.space; if (space.dimensionCount !== 3 || space.drawCellCount <= 0) continue; ensureLatticeSpaceBindGroup(ctx, space); const state = getOrCreateLatticeSpaceSortState(ctx, space); const signaturePtr = frameArena.allocF32(16); mat4f.mul(signaturePtr, ctx.cameraUniformStagingPtr, space.transform.worldMatrixPtr); const signature = wasm.f32view(signaturePtr, 16); let unchanged = state.valid && state.lastRevision === space.sortRevision && state.lastCount === space.drawCellCount; for (let i = 0; i < 16 && unchanged; i++) if (!Object.is(state.lastMvp[i], signature[i])) unchanged = false; if (unchanged) continue; const pass = encoder.beginComputePass(); const result = encodeLatticeSpaceSort(ctx, pass, space, state); pass.end(); if (result && state.sortedIndexBuffer) { encoder.copyBufferToBuffer(result, 0, state.sortedIndexBuffer, 0, space.drawCellCount * 4); state.lastMvp.set(signature); state.lastRevision = space.sortRevision; state.lastCount = space.drawCellCount; state.valid = true; state.sortCount++; state.identityKey = null; } } }; var getPointCloudBindGroupLayout = (ctx) => { if (ctx.pointCloudBindGroupLayout) return ctx.pointCloudBindGroupLayout; ctx.pointCloudBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 240 } }, { binding: 2, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } }, { binding: 4, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } } ] }); return ctx.pointCloudBindGroupLayout; }; var getPointCloudPipelineCacheKey = (ctx, cloud) => { return ["pointcloud", `blend=${cloud.blendMode}`, `depthTest=${cloud.depthTest ? 1 : 0}`, `depthWrite=${cloud.depthWrite ? 1 : 0}`, `fmt=${ctx.format}`].join("|"); }; var getOrCreatePointCloudPipeline = (ctx, cloud) => { const key = getPointCloudPipelineCacheKey(ctx, cloud); const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(pointcloud_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: pointcloud_default }); ctx.shaderCache.set(pointcloud_default, shaderModule); } const bindGroupLayout = getPointCloudBindGroupLayout(ctx); const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, bindGroupLayout] }); const blend = getBlendState(ctx, cloud.blendMode); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [ { format: ctx.format, blend } ] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: cloud.depthTest ? { format: "depth24plus", depthWriteEnabled: cloud.depthWrite, depthCompare: "less" } : void 0 }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getPointCloudBindGroupKey = (ctx, cloud) => { const points = cloud.pointsBuffer; const colors = cloud.colorsBuffer; const uniforms = cloud.uniformBuffer; return `pointcloud:${points ? getObjectId(ctx, points) : 0}:${colors ? getObjectId(ctx, colors) : 0}:${uniforms ? getObjectId(ctx, uniforms) : 0}:${cloud.getColormapKey()}`; }; var ensurePointCloudBindGroup = (ctx, cloud) => { cloud.upload(ctx.device, ctx.queue); if (!cloud.pointsBuffer) return; if (cloud.pointCount <= 0) return; if (!cloud.uniformBuffer) { cloud.uniformBuffer = ctx.device.createBuffer({ size: cloud.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); cloud.bindGroupKey = null; } if (cloud.dirtyUniforms) { const data = cloud.getUniformData(); ctx.queue.writeBuffer(cloud.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); cloud.markUniformsClean(); } if (!ctx.pointCloudDummyColorsBuffer) { ctx.pointCloudDummyColorsBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const white = new Float32Array([1, 1, 1, 1]); ctx.queue.writeBuffer(ctx.pointCloudDummyColorsBuffer, 0, white.buffer, white.byteOffset, white.byteLength); } const key = getPointCloudBindGroupKey(ctx, cloud); if (cloud.bindGroup && cloud.bindGroupKey === key) return; const layout = getPointCloudBindGroupLayout(ctx); const cmapGPU = cloud.getColormapForBinding().getGPUResources(ctx.device, ctx.queue); cloud.bindGroup = ctx.device.createBindGroup({ layout, entries: [ { binding: 0, resource: { buffer: cloud.pointsBuffer } }, { binding: 1, resource: { buffer: cloud.uniformBuffer } }, { binding: 2, resource: cmapGPU.sampler }, { binding: 3, resource: cmapGPU.view }, { binding: 4, resource: { buffer: cloud.colorsBuffer ?? ctx.pointCloudDummyColorsBuffer } } ] }); cloud.bindGroupKey = key; }; var getGlyphFieldBindGroupLayout = (ctx) => { if (ctx.glyphFieldBindGroupLayout) return ctx.glyphFieldBindGroupLayout; ctx.glyphFieldBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 4, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 240 } }, { binding: 5, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } } ] }); return ctx.glyphFieldBindGroupLayout; }; var getOrCreateGlyphFieldPipeline = (ctx, field) => { const key = `glyphfield:${ctx.format}:${field.blendMode}:${field.depthWrite ? 1 : 0}:${field.depthTest ? 1 : 0}:${field.cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; const shaderCode = glyphfield_default; let shaderModule = ctx.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: shaderCode }); ctx.shaderCache.set(shaderCode, shaderModule); } const layout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getGlyphFieldBindGroupLayout(ctx)] }); const pipeline = ctx.device.createRenderPipeline({ layout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] } ] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: ctx.format, blend: getBlendState(ctx, field.blendMode) }] }, primitive: { topology: "triangle-list", cullMode: getCullMode(ctx, field.cullMode) }, depthStencil: field.depthTest || field.depthWrite ? { format: "depth24plus", depthWriteEnabled: field.depthWrite, depthCompare: field.depthTest ? "less" : "always" } : void 0 }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getGlyphFieldBindGroupKey = (ctx, field) => { const p = field.positionsBuffer; const r = field.rotationsBuffer; const s = field.scalesBuffer; const a = field.attributesBuffer; const u = field.uniformBuffer; return `glyphfield:${p ? getObjectId(ctx, p) : 0}:${r ? getObjectId(ctx, r) : 0}:${s ? getObjectId(ctx, s) : 0}:${a ? getObjectId(ctx, a) : 0}:${u ? getObjectId(ctx, u) : 0}:${field.getColormapKey()}`; }; var ensureGlyphFieldBindGroup = (ctx, field) => { field.upload(ctx.device, ctx.queue); if (!field.positionsBuffer) return; if (!field.rotationsBuffer) return; if (!field.scalesBuffer) return; if (field.instanceCount <= 0) return; if (!field.uniformBuffer) { field.uniformBuffer = ctx.device.createBuffer({ size: field.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); field.bindGroupKey = null; } if (field.dirtyUniforms) { const data = field.getUniformData(); ctx.queue.writeBuffer(field.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); field.markUniformsClean(); } if (!field.attributesBuffer) { if (!ctx.glyphFieldDummyAttributesBuffer) { ctx.glyphFieldDummyAttributesBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); } field.attributesBuffer = ctx.glyphFieldDummyAttributesBuffer; field.bindGroupKey = null; } const key = getGlyphFieldBindGroupKey(ctx, field); if (field.bindGroup && field.bindGroupKey === key) return; const layout = getGlyphFieldBindGroupLayout(ctx); const cmapGPU = field.getColormapForBinding().getGPUResources(ctx.device, ctx.queue); field.bindGroup = ctx.device.createBindGroup({ layout, entries: [ { binding: 0, resource: { buffer: field.positionsBuffer } }, { binding: 1, resource: { buffer: field.rotationsBuffer } }, { binding: 2, resource: { buffer: field.scalesBuffer } }, { binding: 3, resource: { buffer: field.attributesBuffer } }, { binding: 4, resource: { buffer: field.uniformBuffer } }, { binding: 5, resource: cmapGPU.sampler }, { binding: 6, resource: cmapGPU.view } ] }); field.bindGroupKey = key; }; var getNodeLinkBindGroupLayout = (ctx) => { if (ctx.nodeLinkBindGroupLayout) return ctx.nodeLinkBindGroupLayout; ctx.nodeLinkBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { binding: 5, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 6, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 7, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 512 } }, { binding: 8, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } }, { binding: 10, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 11, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "1d" } } ] }); return ctx.nodeLinkBindGroupLayout; }; var getNodeLinkPipelineCacheKey = (ctx, link, passKind) => { const cull = passKind === "node-solid" || passKind === "edge-cylinders" ? link.cullMode : "none"; return ["nodelink", passKind, `blend=${link.blendMode}`, `depthTest=${link.depthTest ? 1 : 0}`, `depthWrite=${link.depthWrite ? 1 : 0}`, `cull=${cull}`, `fmt=${ctx.format}`].join("|"); }; var getOrCreateNodeLinkPipeline = (ctx, link, passKind) => { const key = getNodeLinkPipelineCacheKey(ctx, link, passKind); const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(nodelink_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: nodelink_default }); ctx.shaderCache.set(nodelink_default, shaderModule); } const layout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getNodeLinkBindGroupLayout(ctx)] }); let vertexEntry = "vs_node_points"; let fragmentEntry = "fs_node"; let buffers = []; let topology = "triangle-list"; let cullMode = "none"; if (passKind === "node-solid") { vertexEntry = "vs_node_solid"; fragmentEntry = "fs_node"; buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] } ]; topology = "triangle-list"; cullMode = getCullMode(ctx, link.cullMode); } else if (passKind === "edge-lines") { vertexEntry = "vs_edge_lines"; fragmentEntry = "fs_edge"; buffers = []; topology = "line-list"; cullMode = "none"; } else if (passKind === "edge-cylinders") { vertexEntry = "vs_edge_cylinders"; fragmentEntry = "fs_edge"; buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: "float32x3" }] } ]; topology = "triangle-list"; cullMode = getCullMode(ctx, link.cullMode); } const pipeline = ctx.device.createRenderPipeline({ label: key, layout, vertex: { module: shaderModule, entryPoint: vertexEntry, buffers }, fragment: { module: shaderModule, entryPoint: fragmentEntry, targets: [{ format: ctx.format, blend: getBlendState(ctx, link.blendMode) }] }, primitive: { topology, cullMode }, depthStencil: link.depthTest || link.depthWrite ? { format: "depth24plus", depthWriteEnabled: link.depthWrite, depthCompare: link.depthTest ? "less" : "always" } : void 0 }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getNodeLinkBindGroupKey = (ctx, link) => { const np = link.nodePositionsBuffer; const ns = link.nodeScalarsBuffer; const nc = link.nodeColorsBuffer; const nr = link.nodeRadiiBuffer; const ep = link.edgesBuffer; const es = link.edgeScalarsBuffer; const ec = link.edgeColorsBuffer; const u = link.uniformBuffer; return `nodelink:${np ? getObjectId(ctx, np) : 0}:${ns ? getObjectId(ctx, ns) : 0}:${nc ? getObjectId(ctx, nc) : 0}:${nr ? getObjectId(ctx, nr) : 0}:${ep ? getObjectId(ctx, ep) : 0}:${es ? getObjectId(ctx, es) : 0}:${ec ? getObjectId(ctx, ec) : 0}:${u ? getObjectId(ctx, u) : 0}:${link.getNodeColormapKey()}:${link.getEdgeColormapKey()}`; }; var ensureNodeLinkBindGroup = (ctx, link) => { link.upload(ctx.device, ctx.queue); if (!link.nodePositionsBuffer) return; if (!link.uniformBuffer) { link.uniformBuffer = ctx.device.createBuffer({ size: link.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); link.bindGroupKey = null; } if (link.dirtyUniforms) { const data = link.getUniformData(); ctx.queue.writeBuffer(link.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); link.markUniformsClean(); } if (!ctx.nodeLinkDummyF32Buffer) ctx.nodeLinkDummyF32Buffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); if (!ctx.nodeLinkDummyU32Buffer) ctx.nodeLinkDummyU32Buffer = ctx.device.createBuffer({ size: 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const key = getNodeLinkBindGroupKey(ctx, link); if (link.bindGroup && link.bindGroupKey === key) return; const nodeCmap = link.getNodeColormapForBinding().getGPUResources(ctx.device, ctx.queue); const edgeCmap = link.getEdgeColormapForBinding().getGPUResources(ctx.device, ctx.queue); link.bindGroup = ctx.device.createBindGroup({ layout: getNodeLinkBindGroupLayout(ctx), entries: [ { binding: 0, resource: { buffer: link.nodePositionsBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 1, resource: { buffer: link.nodeScalarsBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 2, resource: { buffer: link.nodeColorsBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 3, resource: { buffer: link.nodeRadiiBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 4, resource: { buffer: link.edgesBuffer ?? ctx.nodeLinkDummyU32Buffer } }, { binding: 5, resource: { buffer: link.edgeScalarsBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 6, resource: { buffer: link.edgeColorsBuffer ?? ctx.nodeLinkDummyF32Buffer } }, { binding: 7, resource: { buffer: link.uniformBuffer } }, { binding: 8, resource: nodeCmap.sampler }, { binding: 9, resource: nodeCmap.view }, { binding: 10, resource: edgeCmap.sampler }, { binding: 11, resource: edgeCmap.view } ] }); link.bindGroupKey = key; }; // wgsl/effects/shadow-caster.wgsl var shadow_caster_default = "struct ShadowView { view_projection: mat4x4, } struct ShadowModel { model: mat4x4, } struct VertexInput { @location(0) position: vec3, } @group(0) @binding(0) var shadow_view: ShadowView; @group(1) @binding(0) var shadow_model: ShadowModel; @vertex fn vs_main(in: VertexInput) -> @builtin(position) vec4 { return shadow_view.view_projection * shadow_model.model * vec4(in.position, 1.0); }"; // wgsl/effects/shadow-caster-instanced.wgsl var shadow_caster_instanced_default = "struct ShadowView { view_projection: mat4x4, } struct VertexInput { @location(0) position: vec3, @location(1) model0: vec4, @location(2) model1: vec4, @location(3) model2: vec4, @location(4) model3: vec4, } @group(0) @binding(0) var shadow_view: ShadowView; @vertex fn vs_main(in: VertexInput) -> @builtin(position) vec4 { let model = mat4x4(in.model0, in.model1, in.model2, in.model3); return shadow_view.view_projection * model * vec4(in.position, 1.0); }"; // wgsl/effects/shadow-caster-skinned.wgsl var shadow_caster_skinned_default = "struct ShadowView { view_projection: mat4x4, } struct ShadowModel { model: mat4x4, } struct SkinBuffer { joints: array>, } struct VertexInput { @location(0) position: vec3, @location(1) joints: vec4, @location(2) weights: vec4, } @group(0) @binding(0) var shadow_view: ShadowView; @group(1) @binding(0) var shadow_model: ShadowModel; @group(2) @binding(0) var skin: SkinBuffer; @vertex fn vs_main(in: VertexInput) -> @builtin(position) vec4 { let skin_matrix = skin.joints[in.joints.x] * in.weights.x + skin.joints[in.joints.y] * in.weights.y + skin.joints[in.joints.z] * in.weights.z + skin.joints[in.joints.w] * in.weights.w; return shadow_view.view_projection * shadow_model.model * skin_matrix * vec4(in.position, 1.0); }"; // wgsl/effects/shadow-caster-skinned8.wgsl var shadow_caster_skinned8_default = "struct ShadowView { view_projection: mat4x4, } struct ShadowModel { model: mat4x4, } struct SkinBuffer { joints: array>, } struct VertexInput { @location(0) position: vec3, @location(1) joints: vec4, @location(2) weights: vec4, @location(3) joints1: vec4, @location(4) weights1: vec4, } @group(0) @binding(0) var shadow_view: ShadowView; @group(1) @binding(0) var shadow_model: ShadowModel; @group(2) @binding(0) var skin: SkinBuffer; @vertex fn vs_main(in: VertexInput) -> @builtin(position) vec4 { let skin_matrix = skin.joints[in.joints.x] * in.weights.x + skin.joints[in.joints.y] * in.weights.y + skin.joints[in.joints.z] * in.weights.z + skin.joints[in.joints.w] * in.weights.w + skin.joints[in.joints1.x] * in.weights1.x + skin.joints[in.joints1.y] * in.weights1.y + skin.joints[in.joints1.z] * in.weights1.z + skin.joints[in.joints1.w] * in.weights1.w; return shadow_view.view_projection * shadow_model.model * skin_matrix * vec4(in.position, 1.0); }"; // typescript/core/shadows.ts var SHADOW_METADATA_FLOATS = Scene.MAX_LIGHTS * 20; var DYNAMIC_UNIFORM_STRIDE_FLOATS = 64; var EMPTY_F32 = new Float32Array(0); var writeOrthographic = (out, left, right, bottom, top, near, far) => { const lr = 1 / (left - right), bt = 1 / (bottom - top), nf = 1 / (near - far); out[0] = -2 * lr; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = -2 * bt; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = nf; out[11] = 0; out[12] = (left + right) * lr; out[13] = (top + bottom) * bt; out[14] = near * nf; out[15] = 1; }; var multiplyMatrices = (out, a, b) => { for (let column = 0; column < 4; column++) { const c = column * 4; for (let row = 0; row < 4; row++) out[c + row] = a[row] * b[c] + a[4 + row] * b[c + 1] + a[8 + row] * b[c + 2] + a[12 + row] * b[c + 3]; } }; var multiplyMatrixRanges = (out, a, aOffset, b, bOffset) => { for (let column = 0; column < 4; column++) { const c = column * 4, bc = bOffset + c, b0 = b[bc], b1 = b[bc + 1], b2 = b[bc + 2], b3 = b[bc + 3]; out[c] = a[aOffset] * b0 + a[aOffset + 4] * b1 + a[aOffset + 8] * b2 + a[aOffset + 12] * b3; out[c + 1] = a[aOffset + 1] * b0 + a[aOffset + 5] * b1 + a[aOffset + 9] * b2 + a[aOffset + 13] * b3; out[c + 2] = a[aOffset + 2] * b0 + a[aOffset + 6] * b1 + a[aOffset + 10] * b2 + a[aOffset + 14] * b3; out[c + 3] = a[aOffset + 3] * b0 + a[aOffset + 7] * b1 + a[aOffset + 11] * b2 + a[aOffset + 15] * b3; } }; var projectedSphereExtent = (matrix, offset, axisX, axisY, axisZ, radius) => { const localX = axisX * matrix[offset] + axisY * matrix[offset + 1] + axisZ * matrix[offset + 2], localY = axisX * matrix[offset + 4] + axisY * matrix[offset + 5] + axisZ * matrix[offset + 6], localZ = axisX * matrix[offset + 8] + axisY * matrix[offset + 9] + axisZ * matrix[offset + 10]; return radius * Math.hypot(localX, localY, localZ); }; var RendererShadows = class { ctx; texture = null; arrayView = null; layerViews = []; sampler = null; metadataBuffer = null; viewBuffer = null; modelBuffer = null; instanceBuffer = null; receiverLayout = null; receiverBindGroup = null; viewLayout = null; viewBindGroup = null; modelLayout = null; modelBindGroup = null; pipelineStatic = null; pipelineInstanced = null; pipelineSkinned = null; pipelineSkinned8 = null; pipelineDepthBias = Number.NaN; pipelineDepthBiasSlopeScale = Number.NaN; pipelineDepthBiasClamp = Number.NaN; resourceRevision = -1; metadataRevision = -1; resourceMapSize = 0; resourceMaxViews = 0; resourceFilter = null; modelCapacity = 0; instanceCapacityBytes = 0; modelScratch = EMPTY_F32; metadataScratch = new Float32Array(SHADOW_METADATA_FLOATS); viewScratch = EMPTY_F32; activeViews = []; activeViewsUsed = 0; casters = []; castersUsed = 0; matrixCache = /* @__PURE__ */ new WeakMap(); layerCache = /* @__PURE__ */ new WeakMap(); layerOwners = []; frustumCorners = new Float32Array(24); viewMatrixScratch = new Float32Array(16); projectionMatrixScratch = new Float32Array(16); skinWorldMatrixScratch = new Float32Array(16); _casterPreparationSerial = 0; _instancedCasterRunCount = 0; constructor(ctx) { this.ctx = ctx; } get activeViewCount() { return this.activeViewsUsed; } get casterCount() { return this.castersUsed; } get casterPreparationSerial() { return this._casterPreparationSerial; } get instancedCasterRunCount() { return this._instancedCasterRunCount; } get hasResources() { return this.texture !== null; } hasCaster(mesh) { for (let i = 0; i < this.castersUsed; i++) if (this.casters[i].mesh === mesh) return true; return false; } getViewProjection(light) { const matrix = this.matrixCache.get(light); return matrix ? new Float32Array(matrix) : null; } get bindGroupLayout() { if (!this.receiverLayout) throw new Error("Renderer shadows: receiver resources are not active."); return this.receiverLayout; } prepare(scene, camera) { const system = this.ctx.effects.shadows; const { lights } = scene.getLightingData(); this.activeViewsUsed = 0; for (let lightIndex = 0; lightIndex < lights.length && this.activeViewsUsed < system.maxViews; lightIndex++) { const light = lights[lightIndex]; if (!(light instanceof DirectionalLight)) continue; const shadow = getShadowRuntimeState(system, light); if (!shadow) continue; const layer = this.activeViewsUsed; const layerChanged = this.layerCache.get(light) !== layer; const layerOwnerChanged = this.layerOwners[layer] !== light; let matrix = this.matrixCache.get(light); const update = shadow.updateMode === "always" || shadow.dirty || layerChanged || layerOwnerChanged || !matrix; if (!matrix) { matrix = new Float32Array(16); this.matrixCache.set(light, matrix); } if (update) this.fitDirectionalView(matrix, scene, camera, light, shadow, system.mapSize); this.layerCache.set(light, layer); const view = this.acquireActiveView(this.activeViewsUsed++); view.light = light; view.shadow = shadow; view.lightIndex = lightIndex; view.layer = layer; view.matrix = matrix; view.update = update; } if (this.activeViewsUsed === 0) { this.destroyResources(); this.castersUsed = 0; return; } const resourcesCreated = this.ensureResources(); let anyUpdate = resourcesCreated; for (let i = 0; i < this.activeViewsUsed; i++) { const view = this.activeViews[i]; if (resourcesCreated) view.update = true; anyUpdate ||= view.update; } if (anyUpdate || this.metadataRevision !== system.revision) this.prepareMetadata(); if (anyUpdate) this.prepareCasters(scene); else this.castersUsed = 0; } encode(encoder) { if (!this.texture || !this.viewBindGroup || !this.modelBindGroup) return; for (let viewIndex = 0; viewIndex < this.activeViewsUsed; viewIndex++) { const view = this.activeViews[viewIndex]; if (!view.update) continue; const pass = encoder.beginRenderPass({ colorAttachments: [], depthStencilAttachment: { view: this.layerViews[view.layer], depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" } }); pass.setBindGroup(0, this.viewBindGroup, [view.layer * 256]); let lastPipeline = null; for (let i = 0; i < this.castersUsed; i++) { const caster = this.casters[i]; if (caster.instanceCount === 0) continue; const mesh = caster.mesh; const geometry = mesh.geometry; const instanced = caster.instanceCount > 1; const pipeline = this.getCasterPipeline(caster.skinned, caster.skinned8, instanced); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } pass.setVertexBuffer(0, caster.positionBuffer); if (instanced) pass.setVertexBuffer(1, this.instanceBuffer, caster.instanceOffset, caster.instanceCount * this.ctx.INSTANCE_STRIDE_BYTES); else if (caster.skinned) { pass.setVertexBuffer(1, geometry.skinInfluenceBuffer); pass.setBindGroup(2, mesh.skin.bindGroup); } if (!instanced) pass.setBindGroup(1, this.modelBindGroup, [i * 256]); if (geometry.isIndexed) { pass.setIndexBuffer(geometry.indexBuffer, "uint32"); pass.drawIndexed(geometry.indexCount, caster.instanceCount); } else pass.draw(geometry.vertexCount, caster.instanceCount); } pass.end(); if (view.light) { this.layerOwners[view.layer] = view.light; setShadowRuntimeClean(this.ctx.effects.shadows, view.light); } } } warmup() { if (this.activeViewsUsed === 0) return; this.getCasterPipeline(false, false, false); for (let i = 0; i < this.castersUsed; i++) { const caster = this.casters[i]; if (caster.instanceCount === 0) continue; this.getCasterPipeline(caster.skinned, caster.skinned8, caster.instanceCount > 1); } } bindReceiver(pass, skinned) { if (this.receiverBindGroup) pass.setBindGroup(skinned ? 3 : 2, this.receiverBindGroup); } destroy() { this.destroyResources(); this.activeViewsUsed = 0; this.castersUsed = 0; this.matrixCache = /* @__PURE__ */ new WeakMap(); this.layerCache = /* @__PURE__ */ new WeakMap(); } acquireActiveView(index) { let view = this.activeViews[index]; if (!view) { view = { light: null, shadow: null, lightIndex: 0, layer: 0, matrix: new Float32Array(16), update: false }; this.activeViews[index] = view; } return view; } acquireCaster(index) { let caster = this.casters[index]; if (!caster) { caster = { mesh: null, positionBuffer: null, skinned: false, skinned8: false, instanceCount: 1, instanceOffset: 0 }; this.casters[index] = caster; } return caster; } writeCameraFrustumCorners(camera, distance) { const world = camera.transform.worldMatrix; const px = world[12], py = world[13], pz = world[14]; const rx = world[0], ry = world[1], rz = world[2]; const ux = world[4], uy = world[5], uz = world[6]; const fx = -world[8], fy = -world[9], fz = -world[10]; let near = 0.1, far = distance; let nearLeft, nearRight, nearBottom, nearTop; let farLeft, farRight, farBottom, farTop; if (camera instanceof PerspectiveCamera) { near = camera.near; far = Math.max(near + 1e-4, Math.min(Number.isFinite(camera.far) ? camera.far : distance, distance)); const tangent = Math.tan(camera.fov * Math.PI / 360); nearTop = tangent * near; nearBottom = -nearTop; nearRight = nearTop * camera.aspect; nearLeft = -nearRight; farTop = tangent * far; farBottom = -farTop; farRight = farTop * camera.aspect; farLeft = -farRight; } else if (camera instanceof OrthographicCamera) { near = camera.near; far = Math.max(near + 1e-4, Math.min(camera.far, near + distance)); nearLeft = farLeft = camera.left; nearRight = farRight = camera.right; nearBottom = farBottom = camera.bottom; nearTop = farTop = camera.top; } else { nearLeft = nearBottom = farLeft = farBottom = -distance * 0.5; nearRight = nearTop = farRight = farTop = distance * 0.5; } const corners = this.frustumCorners; let offset = 0; for (let plane = 0; plane < 2; plane++) { const z = plane === 0 ? near : far; const left = plane === 0 ? nearLeft : farLeft; const right = plane === 0 ? nearRight : farRight; const bottom = plane === 0 ? nearBottom : farBottom; const top = plane === 0 ? nearTop : farTop; for (let yIndex = 0; yIndex < 2; yIndex++) { const y = yIndex === 0 ? bottom : top; for (let xIndex = 0; xIndex < 2; xIndex++) { const x = xIndex === 0 ? left : right; corners[offset++] = px + rx * x + ux * y + fx * z; corners[offset++] = py + ry * x + uy * y + fy * z; corners[offset++] = pz + rz * x + uz * y + fz * z; } } } } fitDirectionalView(out, scene, camera, light, shadow, mapSize) { const lightDirection = resolveLightDirection(light); const directionLength = Math.hypot(lightDirection[0], lightDirection[1], lightDirection[2]) || 1; const dx = lightDirection[0] / directionLength, dy = lightDirection[1] / directionLength, dz = lightDirection[2] / directionLength; const candidateUpX = Math.abs(dy) > 0.98 ? 1 : 0, candidateUpY = Math.abs(dy) > 0.98 ? 0 : 1; let rx = dy * 0 - dz * candidateUpY, ry = dz * candidateUpX - dx * 0, rz = dx * candidateUpY - dy * candidateUpX; const rightLength = Math.hypot(rx, ry, rz) || 1; rx /= rightLength; ry /= rightLength; rz /= rightLength; const ux = ry * dz - rz * dy, uy = rz * dx - rx * dz, uz = rx * dy - ry * dx; let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; if (shadow.volume) { const center = shadow.volume.center; const centerX2 = center[0] * rx + center[1] * ry + center[2] * rz; const centerY2 = center[0] * ux + center[1] * uy + center[2] * uz; const centerZ2 = center[0] * dx + center[1] * dy + center[2] * dz; minX = centerX2 - shadow.volume.width * 0.5; maxX = centerX2 + shadow.volume.width * 0.5; minY = centerY2 - shadow.volume.height * 0.5; maxY = centerY2 + shadow.volume.height * 0.5; minZ = centerZ2 - shadow.volume.depth * 0.5; maxZ = centerZ2 + shadow.volume.depth * 0.5; } else { this.writeCameraFrustumCorners(camera, shadow.distance); for (let i = 0; i < 24; i += 3) { const x = this.frustumCorners[i], y = this.frustumCorners[i + 1], z = this.frustumCorners[i + 2]; const lightX = x * rx + y * ry + z * rz; const lightY = x * ux + y * uy + z * uz; const lightZ = x * dx + y * dy + z * dz; if (lightX < minX) minX = lightX; if (lightX > maxX) maxX = lightX; if (lightY < minY) minY = lightY; if (lightY > maxY) maxY = lightY; if (lightZ < minZ) minZ = lightZ; if (lightZ > maxZ) maxZ = lightZ; } const storeF32 = TransformStore.global().f32(); for (const mesh of scene.meshes) { if (mesh.destroyed || !mesh.visible || !mesh.castShadow) continue; const bounds = getMeshLocalBoundsSource(mesh); const matrixBase = mesh.transform.worldMatrixPtr >>> 2; const localCenter = bounds.boundsCenter; const worldX = storeF32[matrixBase] * localCenter[0] + storeF32[matrixBase + 4] * localCenter[1] + storeF32[matrixBase + 8] * localCenter[2] + storeF32[matrixBase + 12], worldY = storeF32[matrixBase + 1] * localCenter[0] + storeF32[matrixBase + 5] * localCenter[1] + storeF32[matrixBase + 9] * localCenter[2] + storeF32[matrixBase + 13], worldZ = storeF32[matrixBase + 2] * localCenter[0] + storeF32[matrixBase + 6] * localCenter[1] + storeF32[matrixBase + 10] * localCenter[2] + storeF32[matrixBase + 14]; const localRadius = bounds.boundsRadius; const lightX = worldX * rx + worldY * ry + worldZ * rz, lightY = worldX * ux + worldY * uy + worldZ * uz, lightZ = worldX * dx + worldY * dy + worldZ * dz; const extentX = projectedSphereExtent(storeF32, matrixBase, rx, ry, rz, localRadius), extentY = projectedSphereExtent(storeF32, matrixBase, ux, uy, uz, localRadius), extentZ = projectedSphereExtent(storeF32, matrixBase, dx, dy, dz, localRadius); let casterMinX = lightX - extentX, casterMaxX = lightX + extentX, casterMinY = lightY - extentY, casterMaxY = lightY + extentY, casterMinZ = lightZ - extentZ, casterMaxZ = lightZ + extentZ; const skinInstance = mesh.skin; const usesSkinning = skinInstance !== null && mesh.geometry.hasSkinAttributes && (mesh.material instanceof StandardMaterial || mesh.material instanceof UnlitMaterial); if (usesSkinning) { const skin = skinInstance.skin; const inverseBind = wasm.f32view(skin.invBindPtr, skin.jointCount * 16); for (let jointIndex = 0; jointIndex < skin.jointCount; jointIndex++) { const joint = skin.joints[jointIndex]; if (joint.disposed) continue; multiplyMatrixRanges(this.skinWorldMatrixScratch, storeF32, joint.worldMatrixPtr >>> 2, inverseBind, jointIndex * 16); const matrix = this.skinWorldMatrixScratch; const skinnedWorldX = matrix[0] * localCenter[0] + matrix[4] * localCenter[1] + matrix[8] * localCenter[2] + matrix[12], skinnedWorldY = matrix[1] * localCenter[0] + matrix[5] * localCenter[1] + matrix[9] * localCenter[2] + matrix[13], skinnedWorldZ = matrix[2] * localCenter[0] + matrix[6] * localCenter[1] + matrix[10] * localCenter[2] + matrix[14]; const skinnedLightX = skinnedWorldX * rx + skinnedWorldY * ry + skinnedWorldZ * rz, skinnedLightY = skinnedWorldX * ux + skinnedWorldY * uy + skinnedWorldZ * uz, skinnedLightZ = skinnedWorldX * dx + skinnedWorldY * dy + skinnedWorldZ * dz; const skinnedExtentX = projectedSphereExtent(matrix, 0, rx, ry, rz, localRadius), skinnedExtentY = projectedSphereExtent(matrix, 0, ux, uy, uz, localRadius), skinnedExtentZ = projectedSphereExtent(matrix, 0, dx, dy, dz, localRadius); casterMinX = Math.min(casterMinX, skinnedLightX - skinnedExtentX); casterMaxX = Math.max(casterMaxX, skinnedLightX + skinnedExtentX); casterMinY = Math.min(casterMinY, skinnedLightY - skinnedExtentY); casterMaxY = Math.max(casterMaxY, skinnedLightY + skinnedExtentY); casterMinZ = Math.min(casterMinZ, skinnedLightZ - skinnedExtentZ); casterMaxZ = Math.max(casterMaxZ, skinnedLightZ + skinnedExtentZ); } } if (casterMaxX < minX || casterMinX > maxX || casterMaxY < minY || casterMinY > maxY) continue; if (casterMinZ < minZ) minZ = casterMinZ; if (casterMaxZ > maxZ) maxZ = casterMaxZ; } } const rawWidth = Math.max(0.01, maxX - minX), rawHeight = Math.max(0.01, maxY - minY); const stabilizationScale = mapSize > 2 ? mapSize / (mapSize - 2) : 2; const width = rawWidth * stabilizationScale, height = rawHeight * stabilizationScale; const depthPadding = Math.max(0.1, (maxZ - minZ) * 0.02), depth = Math.max(0.01, maxZ - minZ + depthPadding * 2); let centerX = (minX + maxX) * 0.5, centerY = (minY + maxY) * 0.5; const centerZ = (minZ + maxZ) * 0.5; centerX = Math.round(centerX / (width / mapSize)) * (width / mapSize); centerY = Math.round(centerY / (height / mapSize)) * (height / mapSize); const centerWorldX = rx * centerX + ux * centerY + dx * centerZ, centerWorldY = ry * centerX + uy * centerY + dy * centerZ, centerWorldZ = rz * centerX + uz * centerY + dz * centerZ; const eyeX = centerWorldX - dx * depth * 0.5, eyeY = centerWorldY - dy * depth * 0.5, eyeZ = centerWorldZ - dz * depth * 0.5; const view = this.viewMatrixScratch; view[0] = rx; view[1] = ux; view[2] = -dx; view[3] = 0; view[4] = ry; view[5] = uy; view[6] = -dy; view[7] = 0; view[8] = rz; view[9] = uz; view[10] = -dz; view[11] = 0; view[12] = -(rx * eyeX + ry * eyeY + rz * eyeZ); view[13] = -(ux * eyeX + uy * eyeY + uz * eyeZ); view[14] = dx * eyeX + dy * eyeY + dz * eyeZ; view[15] = 1; writeOrthographic(this.projectionMatrixScratch, -width * 0.5, width * 0.5, -height * 0.5, height * 0.5, 0, depth); multiplyMatrices(out, this.projectionMatrixScratch, view); } ensureResources() { const system = this.ctx.effects.shadows; const incompatible = this.resourceRevision !== system.revision && (this.resourceMapSize !== system.mapSize || this.resourceMaxViews !== system.maxViews); if (this.texture && !incompatible) { this.resourceRevision = system.revision; if (this.resourceFilter !== system.filter) this.createReceiverSamplerAndBindGroup(); return false; } this.destroyResources(); const device = this.ctx.device; this.resourceMapSize = system.mapSize; this.resourceMaxViews = system.maxViews; this.resourceRevision = system.revision; this.texture = device.createTexture({ label: "WasmGPU shadow map array", size: { width: system.mapSize, height: system.mapSize, depthOrArrayLayers: system.maxViews }, format: "depth32float", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); this.arrayView = this.texture.createView({ dimension: "2d-array", arrayLayerCount: system.maxViews }); this.layerViews = Array.from({ length: system.maxViews }, (_, layer) => this.texture.createView({ dimension: "2d", baseArrayLayer: layer, arrayLayerCount: 1 })); this.metadataBuffer = device.createBuffer({ size: SHADOW_METADATA_FLOATS * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.viewBuffer = device.createBuffer({ size: system.maxViews * 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.viewScratch = new Float32Array(system.maxViews * DYNAMIC_UNIFORM_STRIDE_FLOATS); this.receiverLayout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth", viewDimension: "2d-array" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: SHADOW_METADATA_FLOATS * 4 } } ] }); this.createReceiverSamplerAndBindGroup(); this.viewLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 64 } }] }); this.viewBindGroup = device.createBindGroup({ layout: this.viewLayout, entries: [{ binding: 0, resource: { buffer: this.viewBuffer, size: 64 } }] }); this.modelLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 64 } }] }); this.ensureModelCapacity(1); return true; } createReceiverSamplerAndBindGroup() { const filter = this.ctx.effects.shadows.filter; this.resourceFilter = filter; this.sampler = this.ctx.device.createSampler({ compare: "less-equal", minFilter: filter === "pcf" ? "linear" : "nearest", magFilter: filter === "pcf" ? "linear" : "nearest", addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge" }); this.receiverBindGroup = this.ctx.device.createBindGroup({ layout: this.receiverLayout, entries: [{ binding: 0, resource: this.arrayView }, { binding: 1, resource: this.sampler }, { binding: 2, resource: { buffer: this.metadataBuffer } }] }); } ensureModelCapacity(count) { if (this.modelBuffer && count <= this.modelCapacity) return; let capacity = Math.max(64, this.modelCapacity); while (capacity < count) capacity *= 2; this.modelBuffer?.destroy(); this.modelCapacity = capacity; this.modelScratch = new Float32Array(capacity * DYNAMIC_UNIFORM_STRIDE_FLOATS); this.modelBuffer = this.ctx.device.createBuffer({ size: capacity * 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.modelBindGroup = this.ctx.device.createBindGroup({ layout: this.modelLayout, entries: [{ binding: 0, resource: { buffer: this.modelBuffer, size: 64 } }] }); } prepareMetadata() { const system = this.ctx.effects.shadows; const filter = system.filter === "pcf" ? 1 : 0; this.metadataScratch.fill(0); for (let i = 0; i < Scene.MAX_LIGHTS; i++) this.metadataScratch[i * 20 + 16] = -1; this.viewScratch.fill(0); for (let i = 0; i < this.activeViewsUsed; i++) { const view = this.activeViews[i]; this.viewScratch.set(view.matrix, view.layer * DYNAMIC_UNIFORM_STRIDE_FLOATS); const offset = view.lightIndex * 20; this.metadataScratch.set(view.matrix, offset); this.metadataScratch[offset + 16] = view.layer; this.metadataScratch[offset + 17] = view.shadow.bias; this.metadataScratch[offset + 18] = view.shadow.normalBias; this.metadataScratch[offset + 19] = filter; } this.ctx.queue.writeBuffer(this.viewBuffer, 0, this.viewScratch); this.ctx.queue.writeBuffer(this.metadataBuffer, 0, this.metadataScratch); this.metadataRevision = system.revision; } prepareCasters(scene) { this.castersUsed = 0; this._instancedCasterRunCount = 0; this._casterPreparationSerial++; for (const mesh of scene.meshes) { if (mesh.destroyed || !mesh.visible || !mesh.castShadow) continue; const geometry = mesh.geometry; geometry.upload(this.ctx.device); const buffers = getMeshVertexBuffers(mesh, this.ctx.device, this.ctx.queue); const supportedSkin = mesh.material instanceof StandardMaterial || mesh.material instanceof UnlitMaterial; const skinned = supportedSkin && mesh.skin !== null && geometry.hasSkinAttributes; if (skinned) warmSkinResources(this.ctx, mesh.skin); const caster = this.acquireCaster(this.castersUsed++); caster.mesh = mesh; caster.positionBuffer = buffers.positionBuffer; caster.skinned = skinned; caster.skinned8 = skinned && geometry.hasSkin8Attributes; caster.instanceCount = 1; caster.instanceOffset = 0; } this.ensureModelCapacity(Math.max(1, this.castersUsed)); this.modelScratch.fill(0, 0, this.castersUsed * DYNAMIC_UNIFORM_STRIDE_FLOATS); const store = TransformStore.global(); const storeF32 = store.f32(); for (let i = 0; i < this.castersUsed; i++) { const source = this.casters[i].mesh.transform.worldMatrixPtr >>> 2; const destination = i * DYNAMIC_UNIFORM_STRIDE_FLOATS; for (let component = 0; component < 16; component++) this.modelScratch[destination + component] = storeF32[source + component]; } if (this.castersUsed > 0) this.ctx.queue.writeBuffer(this.modelBuffer, 0, this.modelScratch.buffer, 0, this.castersUsed * 256); let totalInstanceBytes = 0; for (let i = 0; i < this.castersUsed; ) { const first = this.casters[i]; if (first.skinned || hasMeshMorphRuntime(first.mesh)) { i++; continue; } const geometry = first.mesh.geometry; let end = i + 1; while (end < this.castersUsed) { const next = this.casters[end]; if (next.skinned || hasMeshMorphRuntime(next.mesh) || next.mesh.geometry !== geometry) break; end++; } const count = end - i; if (count > 1) { first.instanceCount = count; first.instanceOffset = totalInstanceBytes; for (let j = i + 1; j < end; j++) this.casters[j].instanceCount = 0; totalInstanceBytes += count * this.ctx.INSTANCE_STRIDE_BYTES; this._instancedCasterRunCount++; } i = end; } if (totalInstanceBytes === 0) return; this.ensureInstanceCapacity(totalInstanceBytes); for (let i = 0; i < this.castersUsed; i++) { const caster = this.casters[i]; if (caster.instanceCount <= 1) continue; const count = caster.instanceCount; const ptrsPtr = frameArena.alloc(count * 4, 4); const ptrs = store.u32(); const ptrBase = ptrsPtr >>> 2; for (let j = 0; j < count; j++) ptrs[ptrBase + j] = this.casters[i + j].mesh.transform.worldMatrixPtr >>> 0; const outPtr = frameArena.allocF32(count * 32); transformf.packModelNormalMat4FromPtrs(outPtr, ptrsPtr, count); const byteLength = count * this.ctx.INSTANCE_STRIDE_BYTES; this.ctx.queue.writeBuffer(this.instanceBuffer, caster.instanceOffset, driver.bytes(), outPtr, byteLength); } } ensureInstanceCapacity(byteLength) { if (this.instanceBuffer && this.instanceCapacityBytes >= byteLength) return; this.instanceBuffer?.destroy(); let capacity = this.instanceCapacityBytes || this.ctx.INSTANCE_STRIDE_BYTES * 256; while (capacity < byteLength) capacity *= 2; this.instanceCapacityBytes = capacity; this.instanceBuffer = this.ctx.device.createBuffer({ label: "WasmGPU shadow instances", size: capacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); } getCasterPipeline(skinned, skinned8, instanced) { const system = this.ctx.effects.shadows; if (this.pipelineDepthBias !== system.depthBias || this.pipelineDepthBiasSlopeScale !== system.depthBiasSlopeScale || this.pipelineDepthBiasClamp !== system.depthBiasClamp) { this.pipelineStatic = null; this.pipelineInstanced = null; this.pipelineSkinned = null; this.pipelineSkinned8 = null; this.pipelineDepthBias = system.depthBias; this.pipelineDepthBiasSlopeScale = system.depthBiasSlopeScale; this.pipelineDepthBiasClamp = system.depthBiasClamp; } if (instanced && this.pipelineInstanced) return this.pipelineInstanced; if (skinned8 && this.pipelineSkinned8) return this.pipelineSkinned8; if (skinned && !skinned8 && this.pipelineSkinned) return this.pipelineSkinned; if (!instanced && !skinned && this.pipelineStatic) return this.pipelineStatic; const code = instanced ? shadow_caster_instanced_default : skinned8 ? shadow_caster_skinned8_default : skinned ? shadow_caster_skinned_default : shadow_caster_default; const module = this.ctx.device.createShaderModule({ code }); const layouts = [this.viewLayout]; if (!instanced) layouts.push(this.modelLayout); if (skinned) layouts.push(this.ctx.skinBindGroupLayout); const buffers = [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }]; if (instanced) buffers.push({ arrayStride: this.ctx.INSTANCE_STRIDE_BYTES, stepMode: "instance", attributes: [ { shaderLocation: 1, offset: 0, format: "float32x4" }, { shaderLocation: 2, offset: 16, format: "float32x4" }, { shaderLocation: 3, offset: 32, format: "float32x4" }, { shaderLocation: 4, offset: 48, format: "float32x4" } ] }); else if (skinned) buffers.push({ arrayStride: skinned8 ? 48 : 24, attributes: skinned8 ? [ { shaderLocation: 1, offset: 0, format: "uint16x4" }, { shaderLocation: 2, offset: 8, format: "float32x4" }, { shaderLocation: 3, offset: 24, format: "uint16x4" }, { shaderLocation: 4, offset: 32, format: "float32x4" } ] : [ { shaderLocation: 1, offset: 0, format: "uint16x4" }, { shaderLocation: 2, offset: 8, format: "float32x4" } ] }); const pipeline = this.ctx.device.createRenderPipeline({ label: `WasmGPU shadow caster ${instanced ? "instanced" : skinned8 ? "skin8" : skinned ? "skin4" : "static"}`, layout: this.ctx.device.createPipelineLayout({ bindGroupLayouts: layouts }), vertex: { module, entryPoint: "vs_main", buffers }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: "depth32float", depthWriteEnabled: true, depthCompare: "less", depthBias: system.depthBias, depthBiasSlopeScale: system.depthBiasSlopeScale, depthBiasClamp: system.depthBiasClamp } }); if (instanced) this.pipelineInstanced = pipeline; else if (skinned8) this.pipelineSkinned8 = pipeline; else if (skinned) this.pipelineSkinned = pipeline; else this.pipelineStatic = pipeline; return pipeline; } destroyResources() { this.texture?.destroy(); this.metadataBuffer?.destroy(); this.viewBuffer?.destroy(); this.modelBuffer?.destroy(); this.instanceBuffer?.destroy(); this.texture = null; this.arrayView = null; this.layerViews.length = 0; this.layerOwners.length = 0; this.sampler = null; this.metadataBuffer = null; this.viewBuffer = null; this.modelBuffer = null; this.instanceBuffer = null; this.receiverLayout = null; this.receiverBindGroup = null; this.viewLayout = null; this.viewBindGroup = null; this.modelLayout = null; this.modelBindGroup = null; this.pipelineStatic = null; this.pipelineInstanced = null; this.pipelineSkinned = null; this.pipelineSkinned8 = null; this.pipelineDepthBias = Number.NaN; this.pipelineDepthBiasSlopeScale = Number.NaN; this.pipelineDepthBiasClamp = Number.NaN; this.modelCapacity = 0; this.instanceCapacityBytes = 0; this.modelScratch = EMPTY_F32; this.viewScratch = EMPTY_F32; this.resourceMapSize = 0; this.resourceMaxViews = 0; this.resourceFilter = null; this.metadataRevision = -1; } }; // typescript/core/timing.ts var createGpuTimingResources = (ctx) => { if (!ctx.gpuTimingSupported) return; if (ctx.gpuQuerySet) return; try { ctx.gpuQuerySet = ctx.device.createQuerySet({ type: "timestamp", count: 2 }); ctx.gpuResolveBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC }); ctx.gpuResultBuffer = ctx.device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); } catch (e) { ctx.gpuQuerySet = null; ctx.gpuResolveBuffer?.destroy(); ctx.gpuResolveBuffer = null; ctx.gpuResultBuffer?.destroy(); ctx.gpuResultBuffer = null; ctx.gpuTimingSupported = false; ctx.gpuTimingEnabled = false; console.warn("Renderer: failed to initialize GPU timing resources:", e); } }; var tryReadGpuTiming = (ctx) => { if (!ctx.gpuResultPending) return; const buf = ctx.gpuResultBuffer; if (!buf) return; if (buf.mapState !== "unmapped") return; ctx.gpuResultPending = false; buf.mapAsync(GPUMapMode.READ).then(() => { try { const mapped = buf.getMappedRange(); const times = new BigUint64Array(mapped); const begin = times[0]; const end = times[1]; const delta = end - begin; const ns = delta > 0n ? Number(delta) : 0; ctx._gpuTimeNs = Number.isFinite(ns) ? ns : 0; } catch { } finally { try { buf.unmap(); } catch { } } }).catch(() => { try { buf.unmap(); } catch { } }); }; // wgsl/core/smaa.wgsl var smaa_default = "struct Params { rt_metrics: vec4, threshold: f32, _pad0: f32, _pad1: f32, _pad2: f32, } struct VertexOutput { @builtin(position) pos: vec4, @location(0) uv: vec2, } @group(0) @binding(0) var params: Params; @group(0) @binding(1) var samp_linear: sampler; @group(0) @binding(2) var samp_point: sampler; @group(0) @binding(3) var scene_tex: texture_2d; @group(0) @binding(4) var edges_tex: texture_2d; @group(0) @binding(5) var blend_tex: texture_2d; fn luma(rgb: vec3) -> f32 { return dot(rgb, vec3(0.2126, 0.7152, 0.0722)); } fn edge_v(uv: vec2) -> bool { return textureSampleLevel(edges_tex, samp_point, uv, 0.0).r > 0.5; } fn edge_h(uv: vec2) -> bool { return textureSampleLevel(edges_tex, samp_point, uv, 0.0).g > 0.5; } @vertex fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VertexOutput { var positions = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0), ); var uvs = array, 3>( vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0, -1.0), ); var out: VertexOutput; out.pos = vec4(positions[vi], 0.0, 1.0); out.uv = uvs[vi]; return out; } @fragment fn fs_smaa_edges(in: VertexOutput) -> @location(0) vec4 { let t = params.rt_metrics.xy; let c = textureSampleLevel(scene_tex, samp_point, in.uv, 0.0).rgb; let l = luma(c); let l_left = luma( textureSampleLevel(scene_tex, samp_point, in.uv + vec2(-t.x, 0.0), 0.0).rgb, ); let l_top = luma( textureSampleLevel(scene_tex, samp_point, in.uv + vec2(0.0, -t.y), 0.0).rgb, ); let d_left = abs(l - l_left); let d_top = abs(l - l_top); let e_v = select(0.0, 1.0, d_left >= params.threshold); let e_h = select(0.0, 1.0, d_top >= params.threshold); return vec4(e_v, e_h, 0.0, 0.0); } @fragment fn fs_smaa_weights(in: VertexOutput) -> @location(0) vec4 { let t = params.rt_metrics.xy; let e = textureSampleLevel(edges_tex, samp_point, in.uv, 0.0); var w_left: f32 = 0.0; var w_top: f32 = 0.0; if (e.r > 0.5) { var up: i32 = 0; var down: i32 = 0; for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edge_v(in.uv + vec2(0.0, -t.y * f32(s)))) { break; } up = up + 1; } for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edge_v(in.uv + vec2(0.0, t.y * f32(s)))) { break; } down = down + 1; } let len = f32(up + down + 1); w_left = clamp(len / 17.0, 0.0, 1.0) * 0.5; } if (e.g > 0.5) { var left: i32 = 0; var right: i32 = 0; for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edge_h(in.uv + vec2(-t.x * f32(s), 0.0))) { break; } left = left + 1; } for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edge_h(in.uv + vec2(t.x * f32(s), 0.0))) { break; } right = right + 1; } let len = f32(left + right + 1); w_top = clamp(len / 17.0, 0.0, 1.0) * 0.5; } return vec4(w_left, w_top, 0.0, 0.0); } @fragment fn fs_smaa_neighborhood(in: VertexOutput) -> @location(0) vec4 { let t = params.rt_metrics.xy; let c = textureSampleLevel(scene_tex, samp_linear, in.uv, 0.0); let w = textureSampleLevel(blend_tex, samp_point, in.uv, 0.0); let w_l = w.r; let w_t = w.g; let w_r = textureSampleLevel(blend_tex, samp_point, in.uv + vec2(t.x, 0.0), 0.0).r; let w_b = textureSampleLevel(blend_tex, samp_point, in.uv + vec2(0.0, t.y), 0.0).g; var best_w: f32 = 0.0; var dir: i32 = -1; if (w_l > best_w) { best_w = w_l; dir = 0; } if (w_r > best_w) { best_w = w_r; dir = 1; } if (w_t > best_w) { best_w = w_t; dir = 2; } if (w_b > best_w) { best_w = w_b; dir = 3; } if (best_w <= 0.0) { return c; } var n: vec4 = c; if (dir == 0) { n = textureSampleLevel(scene_tex, samp_linear, in.uv + vec2(-t.x, 0.0), 0.0); } else if (dir == 1) { n = textureSampleLevel(scene_tex, samp_linear, in.uv + vec2(t.x, 0.0), 0.0); } else if (dir == 2) { n = textureSampleLevel(scene_tex, samp_linear, in.uv + vec2(0.0, -t.y), 0.0); } else { n = textureSampleLevel(scene_tex, samp_linear, in.uv + vec2(0.0, t.y), 0.0); } return mix(c, n, best_w); }"; // typescript/core/postprocessing.ts var createSmaaResources = (ctx) => { if (ctx.smaaParamsBuffer) return; ctx.smaaParamsBuffer = ctx.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ctx.smaaSamplerPoint = ctx.device.createSampler({ addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge", magFilter: "nearest", minFilter: "nearest" }); ctx.smaaSamplerLinear = ctx.device.createSampler({ addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge", magFilter: "linear", minFilter: "linear" }); const shaderCode = smaa_default; ctx.smaaShaderModule = ctx.device.createShaderModule({ code: shaderCode }); ctx.smaaEdgeBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } } ] }); ctx.smaaWeightBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } } ] }); ctx.smaaNeighborhoodBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } }, { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } } ] }); const edgeLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.smaaEdgeBindGroupLayout] }); const weightLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.smaaWeightBindGroupLayout] }); const neighLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.smaaNeighborhoodBindGroupLayout] }); ctx.smaaEdgePipeline = ctx.device.createRenderPipeline({ layout: edgeLayout, vertex: { module: ctx.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: ctx.smaaShaderModule, entryPoint: "fs_smaa_edges", targets: [{ format: "rgba8unorm" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); ctx.smaaWeightPipeline = ctx.device.createRenderPipeline({ layout: weightLayout, vertex: { module: ctx.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: ctx.smaaShaderModule, entryPoint: "fs_smaa_weights", targets: [{ format: "rgba8unorm" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); ctx.smaaNeighborhoodPipeline = ctx.device.createRenderPipeline({ layout: neighLayout, vertex: { module: ctx.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: ctx.smaaShaderModule, entryPoint: "fs_smaa_neighborhood", targets: [{ format: ctx.format }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); }; var resizeSmaaTargets = (ctx) => { if (!ctx.smaaEnabled) return; if (!ctx.smaaParamsBuffer) createSmaaResources(ctx); ctx.smaaSceneColorTexture?.destroy(); ctx.smaaEdgesTexture?.destroy(); ctx.smaaBlendTexture?.destroy(); const w = ctx.width | 0; const h = ctx.height | 0; if (w <= 0 || h <= 0) return; ctx.smaaSceneColorTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: ctx.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); ctx.smaaSceneColorView = ctx.smaaSceneColorTexture.createView(); const intermediateFormat = "rgba8unorm"; ctx.smaaEdgesTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: intermediateFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); ctx.smaaEdgesView = ctx.smaaEdgesTexture.createView(); ctx.smaaBlendTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: intermediateFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); ctx.smaaBlendView = ctx.smaaBlendTexture.createView(); const params = new Float32Array(8); params[0] = 1 / w; params[1] = 1 / h; params[2] = w; params[3] = h; params[4] = 0.1; ctx.queue.writeBuffer(ctx.smaaParamsBuffer, 0, params); ctx.smaaEdgeBindGroup = ctx.device.createBindGroup({ layout: ctx.smaaEdgeBindGroupLayout, entries: [ { binding: 0, resource: { buffer: ctx.smaaParamsBuffer } }, { binding: 2, resource: ctx.smaaSamplerPoint }, { binding: 3, resource: ctx.smaaSceneColorView } ] }); ctx.smaaWeightBindGroup = ctx.device.createBindGroup({ layout: ctx.smaaWeightBindGroupLayout, entries: [ { binding: 0, resource: { buffer: ctx.smaaParamsBuffer } }, { binding: 2, resource: ctx.smaaSamplerPoint }, { binding: 4, resource: ctx.smaaEdgesView } ] }); ctx.smaaNeighborhoodBindGroup = ctx.device.createBindGroup({ layout: ctx.smaaNeighborhoodBindGroupLayout, entries: [ { binding: 0, resource: { buffer: ctx.smaaParamsBuffer } }, { binding: 1, resource: ctx.smaaSamplerLinear }, { binding: 2, resource: ctx.smaaSamplerPoint }, { binding: 3, resource: ctx.smaaSceneColorView }, { binding: 5, resource: ctx.smaaBlendView } ] }); }; var executeSmaa = (ctx, encoder, outputView) => { if (!ctx.smaaEdgePipeline || !ctx.smaaWeightPipeline || !ctx.smaaNeighborhoodPipeline) return; if (!ctx.smaaEdgeBindGroup || !ctx.smaaWeightBindGroup || !ctx.smaaNeighborhoodBindGroup) return; if (!ctx.smaaEdgesView || !ctx.smaaBlendView) return; const edgePass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.smaaEdgesView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); edgePass.setPipeline(ctx.smaaEdgePipeline); edgePass.setBindGroup(0, ctx.smaaEdgeBindGroup); edgePass.draw(3); edgePass.end(); const weightPass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.smaaBlendView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); weightPass.setPipeline(ctx.smaaWeightPipeline); weightPass.setBindGroup(0, ctx.smaaWeightBindGroup); weightPass.draw(3); weightPass.end(); const neighborhoodPass = encoder.beginRenderPass({ colorAttachments: [ { view: outputView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" } ] }); neighborhoodPass.setPipeline(ctx.smaaNeighborhoodPipeline); neighborhoodPass.setBindGroup(0, ctx.smaaNeighborhoodBindGroup); neighborhoodPass.draw(3); neighborhoodPass.end(); }; // typescript/core/transmission.ts var isOpticallyTransmissiveMaterial = (material) => { if (!(material instanceof StandardMaterial)) return false; const transmission = material.extensions.transmission; return (transmission?.factor ?? 0) > 0; }; var hasOpticalTransmissionDrawItems = (ctx) => { for (const item of ctx.transparentDrawList) if (isOpticallyTransmissiveMaterial(item.material)) return true; return false; }; var ensureTransmissionTargets = (ctx, needSceneTarget) => { const haveSource = ctx.transmissionSourceTexture !== null && ctx.transmissionSourceView !== null; const haveSceneTarget = !needSceneTarget || ctx.transmissionSceneColorTexture !== null && ctx.transmissionSceneColorView !== null; if (haveSource && haveSceneTarget) return; resizeTransmissionTargets(ctx, needSceneTarget); }; var resizeTransmissionTargets = (ctx, needSceneTarget) => { ctx.transmissionSceneColorTexture?.destroy(); ctx.transmissionSourceTexture?.destroy(); ctx.transmissionSceneColorTexture = null; ctx.transmissionSceneColorView = null; ctx.transmissionSourceTexture = null; ctx.transmissionSourceView = null; const w = ctx.width | 0; const h = ctx.height | 0; if (w <= 0 || h <= 0) { ctx.transmissionSourceRevision++; return; } if (needSceneTarget) { ctx.transmissionSceneColorTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: ctx.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); ctx.transmissionSceneColorView = ctx.transmissionSceneColorTexture.createView(); } ctx.transmissionSourceTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: ctx.format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST }); ctx.transmissionSourceView = ctx.transmissionSourceTexture.createView(); ctx.transmissionSourceRevision++; }; // typescript/core/drawlists.ts var transparentTypeOrder = (item) => { if ("mesh" in item) return 0; if ("field" in item && "geometry" in item) return 1; if ("cloud" in item) return 2; if ("link" in item) return 3; if ("space" in item) return 4; return 5; }; var compareTransparentDrawItems = (a, b) => { const depth = b.sortKey - a.sortKey; if (depth !== 0) return depth; const pipeline = a.pipelineId - b.pipelineId; if (pipeline !== 0) return pipeline; if ("mesh" in a && "mesh" in b) return a.materialId - b.materialId || a.geometryId - b.geometryId || a.vertexSourceId - b.vertexSourceId || (a.skinned ? 1 : 0) - (b.skinned ? 1 : 0) || (a.skinned8 ? 1 : 0) - (b.skinned8 ? 1 : 0); if ("cloud" in a && "cloud" in b) return a.cloudId - b.cloudId; if ("field" in a && "geometry" in a && "field" in b && "geometry" in b) return a.geometryId - b.geometryId || a.fieldId - b.fieldId; if ("link" in a && "link" in b) return a.geometryId - b.geometryId || a.linkId - b.linkId; if ("field" in a && !("geometry" in a) && "field" in b && !("geometry" in b)) return a.fieldId - b.fieldId; if ("space" in a && "space" in b) return a.spaceId - b.spaceId; return transparentTypeOrder(a) - transparentTypeOrder(b); }; var acquireDrawItem = (ctx) => { const i = ctx.drawItemPoolUsed++; let item = ctx.drawItemPool[i]; if (!item) { item = { mesh: null, geometry: null, material: null, pipeline: null, pipelineId: 0, materialId: 0, geometryId: 0, vertexSourceId: 0, skinned: false, skinned8: false, mirrored: false, receiveShadow: false, sortKey: 0 }; ctx.drawItemPool[i] = item; } return item; }; var acquirePointCloudDrawItem = (ctx) => { const i = ctx.pointCloudDrawItemPoolUsed++; let item = ctx.pointCloudDrawItemPool[i]; if (!item) { item = { cloud: null, pipeline: null, pipelineId: 0, cloudId: 0, sortKey: 0 }; ctx.pointCloudDrawItemPool[i] = item; } return item; }; var acquireSplatFieldDrawItem = (ctx) => { const i = ctx.splatFieldDrawItemPoolUsed++; let item = ctx.splatFieldDrawItemPool[i]; if (!item) { item = { field: null, pipeline: null, pipelineId: 0, fieldId: 0, sortKey: 0 }; ctx.splatFieldDrawItemPool[i] = item; } return item; }; var acquireGlyphFieldDrawItem = (ctx) => { const i = ctx.glyphFieldDrawItemPoolUsed++; let item = ctx.glyphFieldDrawItemPool[i]; if (!item) { item = { field: null, geometry: null, pipeline: null, pipelineId: 0, geometryId: 0, fieldId: 0, sortKey: 0 }; ctx.glyphFieldDrawItemPool[i] = item; } return item; }; var acquireNodeLinkDrawItem = (ctx) => { const i = ctx.nodeLinkDrawItemPoolUsed++; let item = ctx.nodeLinkDrawItemPool[i]; if (!item) { item = { link: null, pipeline: null, pipelineId: 0, linkId: 0, passKind: "node-points", geometry: null, geometryId: 0, sortKey: 0 }; ctx.nodeLinkDrawItemPool[i] = item; } return item; }; var acquireLatticeSpaceDrawItem = (ctx) => { const i = ctx.latticeSpaceDrawItemPoolUsed++; let item = ctx.latticeSpaceDrawItemPool[i]; if (!item) { item = { space: null, pipeline: null, pipelineId: 0, spaceId: 0, sortKey: 0 }; ctx.latticeSpaceDrawItemPool[i] = item; } return item; }; var ensureCullingCapacity = (ctx, count) => { if (count <= ctx.cullCapacity) return; let cap = Math.max(1, ctx.cullCapacity); while (cap < count) cap *= 2; const centersPtr = wasm.allocF32(cap * 3); if (!centersPtr) throw new Error(`Renderer culling center allocation failed (${cap * 3} f32 elements).`); const radiiPtr = wasm.allocF32(cap); if (!radiiPtr) { wasm.freeF32(centersPtr, cap * 3); throw new Error(`Renderer culling radius allocation failed (${cap} f32 elements).`); } const oldCentersPtr = ctx.cullCentersPtr; const oldRadiiPtr = ctx.cullRadiiPtr; const oldCap = ctx.cullCapacity; ctx.cullCentersPtr = centersPtr; ctx.cullRadiiPtr = radiiPtr; ctx.cullCapacity = cap; if (oldCentersPtr) wasm.freeF32(oldCentersPtr, oldCap * 3); if (oldRadiiPtr) wasm.freeF32(oldRadiiPtr, oldCap); }; var destroyCullingScratch = (ctx) => { const centersPtr = ctx.cullCentersPtr; const radiiPtr = ctx.cullRadiiPtr; const cap = ctx.cullCapacity; if (centersPtr) wasm.freeF32(centersPtr, cap * 3); if (radiiPtr) wasm.freeF32(radiiPtr, cap); ctx.cullCentersPtr = 0; ctx.cullRadiiPtr = 0; ctx.cullCapacity = 0; }; var recordFrustumCounts = (ctx, tested, visible) => { ctx.frameFrustumTested += tested; ctx.frameFrustumVisible += visible; }; var buildDrawLists = (ctx, scene, camera) => { ctx.drawItemPoolUsed = 0; ctx.opaqueDrawList.length = 0; ctx.transparentDrawList.length = 0; const candidates = ctx.cullMeshScratch; candidates.length = 0; for (const mesh of scene.meshes) { if (mesh.destroyed) continue; if (!mesh.visible) continue; candidates.push(mesh); } const count = candidates.length; if (count === 0) return; let visibleIndicesBase = 0; let visibleCount = count; const store = TransformStore.global(); const storeF32 = store.f32(); const storeU32 = store.u32(); const camWb = camera.transform.worldMatrixPtr >>> 2; const camX = storeF32[camWb + 12]; const camY = storeF32[camWb + 13]; const camZ = storeF32[camWb + 14]; if (ctx.frustumCullingEnabled) { ensureCullingCapacity(ctx, count); const worldPtrsPtr = frameArena.alloc(count * 4, 4); const localCentersPtr = frameArena.allocF32(count * 3); const localRadiiPtr = frameArena.allocF32(count); const worldPtrs = storeU32.subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + count); const localCenters = storeF32.subarray(localCentersPtr >>> 2, (localCentersPtr >>> 2) + count * 3); const localRadii = storeF32.subarray(localRadiiPtr >>> 2, (localRadiiPtr >>> 2) + count); for (let i = 0; i < count; i++) { const mesh = candidates[i]; const bounds = getMeshLocalBoundsSource(mesh); const lc = bounds.boundsCenter; const centerBase = i * 3; worldPtrs[i] = mesh.transform.worldMatrixPtr >>> 0; localCenters[centerBase + 0] = lc[0]; localCenters[centerBase + 1] = lc[1]; localCenters[centerBase + 2] = lc[2]; localRadii[i] = bounds.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, count); const frustumPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(frustumPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(count * 4, 4); visibleCount = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, count, frustumPtr); visibleIndicesBase = outPtr >>> 2; } recordFrustumCounts(ctx, count, visibleCount); const pushMesh = (mesh) => { const geometry = mesh.geometry; const material = mesh.material; const skinned = mesh.skin !== null && geometry.hasSkinAttributes && materialSupportsSkinning(ctx, material); const skinned8 = skinned && geometry.hasSkin8Attributes; const wb = mesh.transform.worldMatrixPtr >>> 2; const mirrored = isMirroredWorldMatrix(ctx, storeF32, wb); const opticalTransmission = isOpticallyTransmissiveMaterial(material); const forceNoDepthWrite = opticalTransmission && material.blendMode !== "opaque" /* Opaque */; const receiveShadow = mesh.receiveShadow && material instanceof StandardMaterial && ctx.shadowRenderer.activeViewCount > 0; const pipeline = getOrCreatePipeline(ctx, material, false, skinned, skinned8, mirrored, forceNoDepthWrite, receiveShadow); const item = acquireDrawItem(ctx); item.mesh = mesh; item.geometry = geometry; item.material = material; item.pipeline = pipeline; item.pipelineId = getObjectId(ctx, pipeline); item.materialId = getObjectId(ctx, material); item.geometryId = getObjectId(ctx, geometry); item.vertexSourceId = getObjectId(ctx, getMeshVertexSource(mesh)); item.skinned = skinned; item.skinned8 = skinned8; item.mirrored = mirrored; item.receiveShadow = receiveShadow; item.sortKey = 0; if (material.blendMode === "opaque" /* Opaque */ && !opticalTransmission) ctx.opaqueDrawList.push(item); else { const dx = storeF32[wb + 12] - camX; const dy = storeF32[wb + 13] - camY; const dz = storeF32[wb + 14] - camZ; item.sortKey = dx * dx + dy * dy + dz * dz; ctx.transparentDrawList.push(item); } }; if (!ctx.frustumCullingEnabled) for (let i = 0; i < count; i++) pushMesh(candidates[i]); else { const visBase = visibleIndicesBase; for (let k = 0; k < visibleCount; k++) pushMesh(candidates[storeU32[visBase + k]]); } ctx.opaqueDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.materialId - b.materialId || a.vertexSourceId - b.vertexSourceId); ctx.transparentDrawList.sort(compareTransparentDrawItems); }; var buildPointCloudDrawLists = (ctx, scene) => { ctx.pointCloudDrawItemPoolUsed = 0; ctx.opaquePointCloudDrawList.length = 0; ctx.transparentPointCloudDrawList.length = 0; ctx.transparentMergedDrawList.length = 0; ctx.cullPointCloudScratch.length = 0; for (const pc of scene.pointClouds) { if (!pc.visible) continue; if (pc.pointCount <= 0) continue; ctx.cullPointCloudScratch.push(pc); } if (ctx.cullPointCloudScratch.length === 0) return; const ts = TransformStore.global(); const storeF32 = ts.f32(); const storeU32 = ts.u32(); const m = ctx.cameraUniformStagingView; const camX = m[16]; const camY = m[17]; const camZ = m[18]; const visible = []; if (ctx.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const pc of ctx.cullPointCloudScratch) { if (pc.boundsRadius > 0) bounded.push(pc); else unbounded.push(pc); } if (bounded.length > 0) { ensureCullingCapacity(ctx, bounded.length); const bcount = bounded.length; const worldPtrsPtr = frameArena.alloc(bcount * 4, 4); const localCentersPtr = frameArena.allocF32(bcount * 3); const localRadiiPtr = frameArena.allocF32(bcount); const worldPtrs = storeU32.subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + bcount); const localCenters = storeF32.subarray(localCentersPtr >>> 2, (localCentersPtr >>> 2) + bcount * 3); const localRadii = storeF32.subarray(localRadiiPtr >>> 2, (localRadiiPtr >>> 2) + bcount); for (let i = 0; i < bounded.length; i++) { const pc = bounded[i]; const cx = pc.boundsCenter[0]; const cy = pc.boundsCenter[1]; const cz = pc.boundsCenter[2]; const base = i * 3; worldPtrs[i] = pc.transform.worldMatrixPtr >>> 0; localCenters[base + 0] = cx; localCenters[base + 1] = cy; localCenters[base + 2] = cz; localRadii[i] = pc.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const frustumPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(frustumPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, bounded.length, frustumPtr); const outBase = outPtr >>> 2; for (let i = 0; i < numVisible; i++) visible.push(bounded[storeU32[outBase + i]]); } for (const pc of unbounded) visible.push(pc); } else for (const pc of ctx.cullPointCloudScratch) visible.push(pc); recordFrustumCounts(ctx, ctx.cullPointCloudScratch.length, visible.length); for (const pc of visible) { const pipeline = getOrCreatePointCloudPipeline(ctx, pc); const pipelineId = getObjectId(ctx, pipeline); const cloudId = getObjectId(ctx, pc); const item = acquirePointCloudDrawItem(ctx); item.cloud = pc; item.pipeline = pipeline; item.pipelineId = pipelineId; item.cloudId = cloudId; if (pc.blendMode === "opaque" /* Opaque */) { item.sortKey = 0; ctx.opaquePointCloudDrawList.push(item); } else { const worldBase = pc.transform.worldMatrixPtr >>> 2; const cx = pc.boundsCenter[0]; const cy = pc.boundsCenter[1]; const cz = pc.boundsCenter[2]; const cwx = storeF32[worldBase + 0] * cx + storeF32[worldBase + 4] * cy + storeF32[worldBase + 8] * cz + storeF32[worldBase + 12]; const cwy = storeF32[worldBase + 1] * cx + storeF32[worldBase + 5] * cy + storeF32[worldBase + 9] * cz + storeF32[worldBase + 13]; const cwz = storeF32[worldBase + 2] * cx + storeF32[worldBase + 6] * cy + storeF32[worldBase + 10] * cz + storeF32[worldBase + 14]; const dx = cwx - camX; const dy = cwy - camY; const dz = cwz - camZ; item.sortKey = dx * dx + dy * dy + dz * dz; ctx.transparentPointCloudDrawList.push(item); } } ctx.opaquePointCloudDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.cloudId - b.cloudId); ctx.transparentPointCloudDrawList.sort(compareTransparentDrawItems); }; var buildSplatFieldDrawLists = (ctx, scene, camera) => { const sceneFields = new Set(scene.splatFields); for (const [field, state] of ctx.splatFieldSortStates) { if (sceneFields.has(field)) continue; destroySplatFieldSortState(ctx, field, state); ctx.splatFieldSortStates.delete(field); } ctx.splatFieldDrawItemPoolUsed = 0; ctx.transparentSplatFieldDrawList.length = 0; ctx.cullSplatFieldScratch.length = 0; for (const field of scene.splatFields) { if (!field.visible) continue; if (field.splatCount <= 0) continue; ctx.cullSplatFieldScratch.push(field); } if (ctx.cullSplatFieldScratch.length === 0) return; const ts = TransformStore.global(); const f32 = ts.f32(); const camX = camera.position[0]; const camY = camera.position[1]; const camZ = camera.position[2]; const visible = []; if (ctx.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const field of ctx.cullSplatFieldScratch) { if (field.boundsRadius > 0) bounded.push(field); else unbounded.push(field); } if (bounded.length > 0) { ensureCullingCapacity(ctx, bounded.length); const bcount = bounded.length; const worldPtrsPtr = frameArena.alloc(bcount * 4, 4); const localCentersPtr = frameArena.allocF32(bcount * 3); const localRadiiPtr = frameArena.allocF32(bcount); const worldPtrs = ts.u32().subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + bcount); const localCenters = ts.f32().subarray(localCentersPtr >>> 2, (localCentersPtr >>> 2) + bcount * 3); const localRadii = ts.f32().subarray(localRadiiPtr >>> 2, (localRadiiPtr >>> 2) + bcount); for (let i = 0; i < bounded.length; i++) { const field = bounded[i]; const base = i * 3; worldPtrs[i] = field.transform.worldMatrixPtr >>> 0; localCenters[base + 0] = field.boundsCenter[0]; localCenters[base + 1] = field.boundsCenter[1]; localCenters[base + 2] = field.boundsCenter[2]; localRadii[i] = field.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, bounded.length, planesPtr); const u32 = ts.u32(); const outBase = outPtr >>> 2; for (let i = 0; i < numVisible; i++) visible.push(bounded[u32[outBase + i]]); } for (const field of unbounded) visible.push(field); } else for (const field of ctx.cullSplatFieldScratch) visible.push(field); recordFrustumCounts(ctx, ctx.cullSplatFieldScratch.length, visible.length); for (const field of visible) { const pipeline = getOrCreateSplatFieldPipeline(ctx); const item = acquireSplatFieldDrawItem(ctx); item.field = field; item.pipeline = pipeline; item.pipelineId = getObjectId(ctx, pipeline); item.fieldId = getObjectId(ctx, field); const worldBase = field.transform.worldMatrixPtr >>> 2; const cx = field.boundsCenter[0]; const cy = field.boundsCenter[1]; const cz = field.boundsCenter[2]; const cwx = f32[worldBase + 0] * cx + f32[worldBase + 4] * cy + f32[worldBase + 8] * cz + f32[worldBase + 12]; const cwy = f32[worldBase + 1] * cx + f32[worldBase + 5] * cy + f32[worldBase + 9] * cz + f32[worldBase + 13]; const cwz = f32[worldBase + 2] * cx + f32[worldBase + 6] * cy + f32[worldBase + 10] * cz + f32[worldBase + 14]; const dx = cwx - camX; const dy = cwy - camY; const dz = cwz - camZ; item.sortKey = dx * dx + dy * dy + dz * dz; ctx.transparentSplatFieldDrawList.push(item); } ctx.transparentSplatFieldDrawList.sort(compareTransparentDrawItems); }; var buildGlyphFieldDrawLists = (ctx, scene, camera) => { ctx.glyphFieldDrawItemPoolUsed = 0; ctx.opaqueGlyphFieldDrawList.length = 0; ctx.transparentGlyphFieldDrawList.length = 0; ctx.cullGlyphFieldScratch.length = 0; for (const gf of scene.glyphFields) { if (!gf.visible) continue; if (gf.instanceCount <= 0) continue; ctx.cullGlyphFieldScratch.push(gf); } if (ctx.cullGlyphFieldScratch.length === 0) return; const store = TransformStore.global(); const f32 = store.f32(); const camX = camera.position[0]; const camY = camera.position[1]; const camZ = camera.position[2]; const visible = []; if (ctx.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const gf of ctx.cullGlyphFieldScratch) { if (gf.boundsRadius > 0) bounded.push(gf); else unbounded.push(gf); } if (bounded.length > 0) { ensureCullingCapacity(ctx, bounded.length); const bcount = bounded.length; const worldPtrsPtr = frameArena.alloc(bcount * 4, 4); const localCentersPtr = frameArena.allocF32(bcount * 3); const localRadiiPtr = frameArena.allocF32(bcount); const worldPtrs = store.u32().subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + bcount); const localCenters = store.f32().subarray(localCentersPtr >>> 2, (localCentersPtr >>> 2) + bcount * 3); const localRadii = store.f32().subarray(localRadiiPtr >>> 2, (localRadiiPtr >>> 2) + bcount); for (let i = 0; i < bounded.length; i++) { const field = bounded[i]; const cx = field.boundsCenter[0]; const cy = field.boundsCenter[1]; const cz = field.boundsCenter[2]; const base = i * 3; worldPtrs[i] = field.transform.worldMatrixPtr >>> 0; localCenters[base + 0] = cx; localCenters[base + 1] = cy; localCenters[base + 2] = cz; localRadii[i] = field.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, bounded.length, planesPtr); const u32 = store.u32(); const outBase = outPtr >>> 2; for (let i = 0; i < numVisible; i++) visible.push(bounded[u32[outBase + i]]); } for (const gf of unbounded) visible.push(gf); } else for (const gf of ctx.cullGlyphFieldScratch) visible.push(gf); recordFrustumCounts(ctx, ctx.cullGlyphFieldScratch.length, visible.length); for (const gf of visible) { const geometry = gf.geometry; const pipeline = getOrCreateGlyphFieldPipeline(ctx, gf); const item = acquireGlyphFieldDrawItem(ctx); item.field = gf; item.geometry = geometry; item.pipeline = pipeline; item.pipelineId = getObjectId(ctx, pipeline); item.geometryId = getObjectId(ctx, geometry); item.fieldId = getObjectId(ctx, gf); if (gf.blendMode === "opaque" /* Opaque */) { item.sortKey = 0; ctx.opaqueGlyphFieldDrawList.push(item); } else { const base = gf.transform.worldMatrixPtr >>> 2; const dx = f32[base + 12] - camX; const dy = f32[base + 13] - camY; const dz = f32[base + 14] - camZ; item.sortKey = dx * dx + dy * dy + dz * dz; ctx.transparentGlyphFieldDrawList.push(item); } } if (ctx.opaqueGlyphFieldDrawList.length > 0) { ctx.opaqueGlyphFieldDrawList.sort((a, b) => { const d0 = a.pipelineId - b.pipelineId; if (d0 !== 0) return d0; const d1 = a.geometryId - b.geometryId; if (d1 !== 0) return d1; return a.fieldId - b.fieldId; }); } if (ctx.transparentGlyphFieldDrawList.length > 0) ctx.transparentGlyphFieldDrawList.sort(compareTransparentDrawItems); }; var getNodeLinkNodeGeometry = (ctx, mode) => { if (mode === "cubes") { if (!ctx.nodeLinkCubeGeometry) ctx.nodeLinkCubeGeometry = Geometry.box(1, 1, 1); return ctx.nodeLinkCubeGeometry; } if (!ctx.nodeLinkSphereGeometry) ctx.nodeLinkSphereGeometry = Geometry.sphere(0.5, 16, 12); return ctx.nodeLinkSphereGeometry; }; var getNodeLinkLinkGeometry = (ctx) => { if (!ctx.nodeLinkCylinderGeometry) ctx.nodeLinkCylinderGeometry = Geometry.cylinder(1, 1, 1, 14, 1, false); return ctx.nodeLinkCylinderGeometry; }; var buildNodeLinkDrawLists = (ctx, scene, camera) => { ctx.nodeLinkDrawItemPoolUsed = 0; ctx.opaqueNodeLinkDrawList.length = 0; ctx.transparentNodeLinkDrawList.length = 0; ctx.cullNodeLinkScratch.length = 0; for (const link of scene.nodeLinks) { if (!link.visible) continue; if (link.nodeCount <= 0 && link.edgeCount <= 0) continue; ctx.cullNodeLinkScratch.push(link); } if (ctx.cullNodeLinkScratch.length === 0) return; const ts = TransformStore.global(); const f32 = ts.f32(); const camX = camera.position[0]; const camY = camera.position[1]; const camZ = camera.position[2]; const visible = []; if (ctx.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const link of ctx.cullNodeLinkScratch) { if (link.boundsRadius > 0) bounded.push(link); else unbounded.push(link); } if (bounded.length > 0) { ensureCullingCapacity(ctx, bounded.length); const bcount = bounded.length; const worldPtrsPtr = frameArena.alloc(bcount * 4, 4); const localCentersPtr = frameArena.allocF32(bcount * 3); const localRadiiPtr = frameArena.allocF32(bcount); const worldPtrs = ts.u32().subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + bcount); const localCenters = ts.f32().subarray(localCentersPtr >>> 2, (localCentersPtr >>> 2) + bcount * 3); const localRadii = ts.f32().subarray(localRadiiPtr >>> 2, (localRadiiPtr >>> 2) + bcount); for (let i = 0; i < bounded.length; i++) { const link = bounded[i]; worldPtrs[i] = link.transform.worldMatrixPtr >>> 0; localCenters[i * 3 + 0] = link.boundsCenter[0]; localCenters[i * 3 + 1] = link.boundsCenter[1]; localCenters[i * 3 + 2] = link.boundsCenter[2]; localRadii[i] = link.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, bounded.length, planesPtr); const u32 = ts.u32(); const outBase = outPtr >>> 2; for (let i = 0; i < numVisible; i++) visible.push(bounded[u32[outBase + i]]); } for (const link of unbounded) visible.push(link); } else for (const link of ctx.cullNodeLinkScratch) visible.push(link); recordFrustumCounts(ctx, ctx.cullNodeLinkScratch.length, visible.length); const pushItem = (link, passKind, geometry) => { const pipeline = getOrCreateNodeLinkPipeline(ctx, link, passKind); const item = acquireNodeLinkDrawItem(ctx); item.link = link; item.pipeline = pipeline; item.pipelineId = getObjectId(ctx, pipeline); item.linkId = getObjectId(ctx, link); item.passKind = passKind; item.geometry = geometry; item.geometryId = geometry ? getObjectId(ctx, geometry) : 0; const worldBase = link.transform.worldMatrixPtr >>> 2; const cx = link.boundsCenter[0]; const cy = link.boundsCenter[1]; const cz = link.boundsCenter[2]; const cwx = f32[worldBase + 0] * cx + f32[worldBase + 4] * cy + f32[worldBase + 8] * cz + f32[worldBase + 12]; const cwy = f32[worldBase + 1] * cx + f32[worldBase + 5] * cy + f32[worldBase + 9] * cz + f32[worldBase + 13]; const cwz = f32[worldBase + 2] * cx + f32[worldBase + 6] * cy + f32[worldBase + 10] * cz + f32[worldBase + 14]; const dx = cwx - camX; const dy = cwy - camY; const dz = cwz - camZ; item.sortKey = dx * dx + dy * dy + dz * dz; if (link.blendMode === "opaque" /* Opaque */) ctx.opaqueNodeLinkDrawList.push(item); else ctx.transparentNodeLinkDrawList.push(item); }; for (const link of visible) { if (link.nodeCount > 0) { if (link.nodeGeometryMode === "points") pushItem(link, "node-points", null); else pushItem(link, "node-solid", getNodeLinkNodeGeometry(ctx, link.nodeGeometryMode)); } if (link.edgeCount > 0) { if (link.edgeGeometryMode === "lines") pushItem(link, "edge-lines", null); else pushItem(link, "edge-cylinders", getNodeLinkLinkGeometry(ctx)); } } ctx.opaqueNodeLinkDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.geometryId - b.geometryId || a.linkId - b.linkId); ctx.transparentNodeLinkDrawList.sort(compareTransparentDrawItems); }; var buildLatticeSpaceDrawLists = (ctx, scene, camera) => { const sceneSpaces = new Set(scene.latticeSpaces); for (const [space, state] of ctx.latticeSpaceSortStates) { if (sceneSpaces.has(space)) continue; destroyLatticeSpaceSortState(ctx, space, state); ctx.latticeSpaceSortStates.delete(space); } ctx.latticeSpaceDrawItemPoolUsed = 0; ctx.opaqueLatticeSpaceDrawList.length = 0; ctx.transparentLatticeSpaceDrawList.length = 0; ctx.cullLatticeSpaceScratch.length = 0; for (const space of scene.latticeSpaces) if (space.visible && space.drawCellCount > 0 && (space.hasData || space.colorMode === "solid")) ctx.cullLatticeSpaceScratch.push(space); const visible = []; if (ctx.frustumCullingEnabled && ctx.cullLatticeSpaceScratch.length > 0) { const count = ctx.cullLatticeSpaceScratch.length; ensureCullingCapacity(ctx, count); const store = TransformStore.global(); const worldPtrsPtr = frameArena.alloc(count * 4, 4); const centersPtr = frameArena.allocF32(count * 3); const radiiPtr = frameArena.allocF32(count); const worldPtrs = store.u32().subarray(worldPtrsPtr >>> 2, (worldPtrsPtr >>> 2) + count); const centers = store.f32().subarray(centersPtr >>> 2, (centersPtr >>> 2) + count * 3); const radii = store.f32().subarray(radiiPtr >>> 2, (radiiPtr >>> 2) + count); for (let i = 0; i < count; i++) { const space = ctx.cullLatticeSpaceScratch[i]; const bounds = space.getLocalBounds(); worldPtrs[i] = space.transform.worldMatrixPtr >>> 0; centers[i * 3] = bounds.sphereCenter[0]; centers[i * 3 + 1] = bounds.sphereCenter[1]; centers[i * 3 + 2] = bounds.sphereCenter[2]; radii[i] = bounds.sphereRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, centersPtr, radiiPtr, count); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, ctx.cameraUniformStagingPtr); const outPtr = frameArena.alloc(count * 4, 4); const visibleCount = cullf.spheresFrustum(outPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, count, planesPtr); const out = store.u32(); for (let i = 0; i < visibleCount; i++) visible.push(ctx.cullLatticeSpaceScratch[out[(outPtr >>> 2) + i]]); } else visible.push(...ctx.cullLatticeSpaceScratch); recordFrustumCounts(ctx, ctx.cullLatticeSpaceScratch.length, visible.length); for (const space of visible) { const pipeline = getOrCreateLatticeSpacePipeline(ctx, space); const item = acquireLatticeSpaceDrawItem(ctx); item.space = space; item.pipeline = pipeline; item.pipelineId = getObjectId(ctx, pipeline); item.spaceId = getObjectId(ctx, space); const bounds = space.getWorldBounds(); const dx = bounds.sphereCenter[0] - camera.position[0]; const dy = bounds.sphereCenter[1] - camera.position[1]; const dz = bounds.sphereCenter[2] - camera.position[2]; item.sortKey = dx * dx + dy * dy + dz * dz; if (space.blendMode === "opaque" /* Opaque */) ctx.opaqueLatticeSpaceDrawList.push(item); else ctx.transparentLatticeSpaceDrawList.push(item); } ctx.opaqueLatticeSpaceDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.spaceId - b.spaceId); ctx.transparentLatticeSpaceDrawList.sort(compareTransparentDrawItems); }; var executeTransparentMergedDrawList = (ctx, pass) => { const families = [ctx.transparentDrawList, ctx.transparentGlyphFieldDrawList, ctx.transparentPointCloudDrawList, ctx.transparentNodeLinkDrawList, ctx.transparentSplatFieldDrawList, ctx.transparentLatticeSpaceDrawList]; let populated = 0; let single = null; let total = 0; for (const family of families) if (family.length > 0) { populated++; single = family; total += family.length; } if (total === 0) return; let executionList; if (populated === 1) { ctx.transparentMergedDrawList.length = 0; executionList = single; } else { const merged = ctx.transparentMergedDrawList; merged.length = 0; const indices = [0, 0, 0, 0, 0, 0]; while (merged.length < total) { let selectedFamily = -1; let selected = null; for (let familyIndex = 0; familyIndex < families.length; familyIndex++) { const candidate = families[familyIndex][indices[familyIndex]]; if (!candidate) continue; if (!selected || compareTransparentDrawItems(candidate, selected) < 0) { selected = candidate; selectedFamily = familyIndex; } } merged.push(selected); indices[selectedFamily]++; } executionList = merged; } const bytes = driver.bytes(); let lastPipeline = null; let lastMaterial = null; let lastGeometry = null; let lastVertexSourceId = -1; let lastSkinned = false; let lastSkinned8 = false; let lastCloud = null; let lastSplatField = null; let lastGlyph = null; let lastNodeLink = null; let lastLatticeSpace = null; for (let i = 0; i < executionList.length; i++) { const item = executionList[i]; if ("space" in item) { const drawItem = item; const space = drawItem.space; if (!space.visible || space.drawCellCount <= 0) continue; ensureLatticeSpaceBindGroup(ctx, space); if (!space.bindGroup) continue; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastLatticeSpace = null; lastMaterial = null; lastGeometry = null; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (space !== lastLatticeSpace) { pass.setBindGroup(1, space.bindGroup); lastLatticeSpace = space; } bindModelUniform(ctx, pass, space.transform.worldMatrixPtr); if (space.dimensionCount === 2) pass.draw(6); else pass.draw(36, space.drawCellCount); continue; } if ("mesh" in item) { const drawItem = item; const mesh = drawItem.mesh; const geometry = drawItem.geometry; const material = drawItem.material; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastMaterial = null; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (geometry !== lastGeometry) geometry.upload(ctx.device); if (material !== lastMaterial) ensureMaterialBindGroup(ctx, material); if (material !== lastMaterial) { pass.setBindGroup(1, material.bindGroup); lastMaterial = material; } const vertexSourceChanged = geometry !== lastGeometry || drawItem.vertexSourceId !== lastVertexSourceId || drawItem.skinned !== lastSkinned || drawItem.skinned8 !== lastSkinned8; if (vertexSourceChanged) { const buffers = getMeshVertexBuffers(mesh, ctx.device, ctx.queue); pass.setVertexBuffer(0, buffers.positionBuffer); pass.setVertexBuffer(1, buffers.normalBuffer); pass.setVertexBuffer(2, geometry.uvBuffer); pass.setVertexBuffer(3, geometry.uv1Buffer); const standardMaterial = material instanceof StandardMaterial; if (standardMaterial) { pass.setVertexBuffer(4, geometry.tangentBuffer); pass.setVertexBuffer(5, buffers.colorBuffer); } else pass.setVertexBuffer(4, buffers.colorBuffer); if (drawItem.skinned) { if (standardMaterial) pass.setVertexBuffer(6, geometry.skinInfluenceBuffer); else { pass.setVertexBuffer(5, geometry.jointsBuffer); pass.setVertexBuffer(6, geometry.weightsBuffer); if (drawItem.skinned8) { pass.setVertexBuffer(7, geometry.joints1Buffer); pass.setVertexBuffer(8, geometry.weights1Buffer); } } } if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; lastVertexSourceId = drawItem.vertexSourceId; lastSkinned = drawItem.skinned; lastSkinned8 = drawItem.skinned8; } else if (hasMeshMorphRuntime(mesh)) getMeshVertexBuffers(mesh, ctx.device, ctx.queue); if (drawItem.skinned) { const skin = mesh.skin; if (skin) { skin.ensureGpuResources(ctx.device, ctx.skinBindGroupLayout); const jointCount = skin.jointCount | 0; const jointMatPtr = frameArena.allocF32(jointCount * 16); animf.computeJointMatricesTo(jointMatPtr, skin.skin.jointIndicesPtr, jointCount, skin.skin.invBindPtr, TransformStore.global().worldPtr, skin.meshWorldMatrixPtr); ctx.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); pass.setBindGroup(2, skin.bindGroup); } } bindModelUniform(ctx, pass, mesh.transform.worldMatrixPtr); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount); else pass.draw(geometry.vertexCount); continue; } if ("field" in item && "geometry" in item) { const drawItem = item; const field = drawItem.field; const geometry = drawItem.geometry; if (!field.visible) continue; if (field.instanceCount <= 0) continue; ensureGlyphFieldBindGroup(ctx, field); if (!field.bindGroup) continue; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastMaterial = null; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (geometry !== lastGeometry) { geometry.upload(ctx.device); pass.setVertexBuffer(0, geometry.positionBuffer); pass.setVertexBuffer(1, geometry.normalBuffer); if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; } if (field !== lastGlyph) { pass.setBindGroup(1, field.bindGroup); lastGlyph = field; lastCloud = null; lastSplatField = null; lastMaterial = null; lastNodeLink = null; } bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, field.instanceCount); else pass.draw(geometry.vertexCount, field.instanceCount); continue; } if ("cloud" in item) { const drawItem = item; const cloud = drawItem.cloud; if (!cloud.visible) continue; if (cloud.pointCount <= 0) continue; ensurePointCloudBindGroup(ctx, cloud); if (!cloud.bindGroup) continue; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastMaterial = null; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; lastSplatField = null; lastGlyph = null; lastMaterial = null; lastNodeLink = null; } bindModelUniform(ctx, pass, cloud.transform.worldMatrixPtr); pass.draw(6, cloud.pointCount); continue; } if ("field" in item && !("geometry" in item)) { const drawItem = item; const field = drawItem.field; if (!field.visible) continue; if (field.splatCount <= 0) continue; ensureSplatFieldBindGroup(ctx, field); if (!field.bindGroup) continue; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastMaterial = null; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (field !== lastSplatField) { pass.setBindGroup(1, field.bindGroup); lastSplatField = field; lastCloud = null; lastGlyph = null; lastMaterial = null; lastNodeLink = null; } bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); pass.draw(6, field.splatCount); continue; } if ("link" in item) { const drawItem = item; const link = drawItem.link; ensureNodeLinkBindGroup(ctx, link); if (!link.bindGroup) continue; if (drawItem.pipeline !== lastPipeline) { pass.setPipeline(drawItem.pipeline); lastPipeline = drawItem.pipeline; lastMaterial = null; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; lastCloud = null; lastSplatField = null; lastGlyph = null; lastNodeLink = null; } if (drawItem.geometry && drawItem.geometry !== lastGeometry) { drawItem.geometry.upload(ctx.device); pass.setVertexBuffer(0, drawItem.geometry.positionBuffer); pass.setVertexBuffer(1, drawItem.geometry.normalBuffer); if (drawItem.geometry.isIndexed) pass.setIndexBuffer(drawItem.geometry.indexBuffer, "uint32"); lastGeometry = drawItem.geometry; } if (link !== lastNodeLink) { pass.setBindGroup(1, link.bindGroup); lastNodeLink = link; lastCloud = null; lastSplatField = null; lastGlyph = null; lastMaterial = null; } bindModelUniform(ctx, pass, link.transform.worldMatrixPtr); if (drawItem.passKind === "node-points") { pass.draw(6, link.nodeCount); } else if (drawItem.passKind === "edge-lines") { pass.draw(2, link.edgeCount); } else if (drawItem.passKind === "node-solid") { if (!drawItem.geometry) continue; if (drawItem.geometry.isIndexed) pass.drawIndexed(drawItem.geometry.indexCount, link.nodeCount); else pass.draw(drawItem.geometry.vertexCount, link.nodeCount); } else { if (!drawItem.geometry) continue; if (drawItem.geometry.isIndexed) pass.drawIndexed(drawItem.geometry.indexCount, link.edgeCount); else pass.draw(drawItem.geometry.vertexCount, link.edgeCount); } continue; } } }; // typescript/world/latticespace.ts var BASE_UNIFORM_FLOAT_COUNT = 10 * 4; var UNIFORM_FLOAT_COUNT5 = BASE_UNIFORM_FLOAT_COUNT + SCALE_UNIFORM_FLOAT_COUNT + 8 * 4; var UNIFORM_BYTE_SIZE5 = UNIFORM_FLOAT_COUNT5 * 4; var DEFAULT_STOPS = [[0.267, 487e-5, 0.32942, 1], [0.99325, 0.90616, 0.14394, 1]]; var colorModeId4 = (mode) => mode === "scalar" ? 0 : mode === "rgba" ? 1 : 2; var colorSpaceId = (space) => space === "srgb" ? 1 : 0; var blendModeId = (mode) => mode === "opaque" /* Opaque */ ? 1 : mode === "transparent" /* Transparent */ ? 2 : 3; var cullModeId = (mode) => mode === "back" /* Back */ ? 1 : mode === "front" /* Front */ ? 2 : 3; var revisionScratch = new ArrayBuffer(4); var revisionF32 = new Float32Array(revisionScratch); var revisionU32 = new Uint32Array(revisionScratch); var mixRevision = (hash, value) => Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; var mixRevisionF32 = (hash, value) => { revisionF32[0] = Number.isFinite(value) ? value : 0; return mixRevision(hash, revisionU32[0]); }; var normalizeDimensions = (dimensions) => { assert(dimensions.length === 2 || dimensions.length === 3, "LatticeSpace: dimensions must contain [x,y] or [x,y,z]."); const out = dimensions.map((value) => { assert(Number.isSafeInteger(value) && value > 0, "LatticeSpace: dimensions must contain positive safe integers."); return value; }); const count = out.reduce((product, value) => product * value, 1); assert(Number.isSafeInteger(count) && count <= 4294967295, "LatticeSpace: cellCount must fit in an unsigned 32-bit index."); return out; }; var normalizeVec3 = (value, fallback, label, positive = false) => { const out = value ? [value[0], value[1], value[2]] : [fallback[0], fallback[1], fallback[2]]; for (const component of out) { assert(Number.isFinite(component), `LatticeSpace: ${label} must contain finite values.`); if (positive) assert(component > 0, `LatticeSpace: ${label} must contain positive values.`); } return out; }; var normalizeCellScale = (value) => { const source = typeof value === "number" ? [value, value, value] : value; const out = normalizeVec3(source, [1, 1, 1], "cellScale", true); for (const component of out) assert(component <= 1, "LatticeSpace: cellScale components must be <= 1."); return out; }; var LatticeSpace = class { transform = new Transform(); dimensions; dimensionCount; cellCount; componentCount; name = null; visible = true; blendMode = "opaque" /* Opaque */; cullMode = "back" /* Back */; depthWrite = true; depthTest = true; dataBuffer = null; maskBuffer = null; uniformBuffer = null; bindGroup = null; bindGroupKey = null; _origin; _spacing; _cellScale; _indexRange; _valueRange = null; _opacity = 1; _lit = false; _colorMode = "scalar"; _colorSpace = "linear"; _solidColor = [1, 1, 1, 1]; _colormap = "viridis"; _colormapStops = DEFAULT_STOPS.map((stop) => [stop[0], stop[1], stop[2], stop[3]]); _scaleTransform; _dataCPU = null; _maskCPU = null; _wasmDataSource = null; _wasmMaskSource = null; _keepCPUData = false; _dataDirty = false; _maskDirty = false; _uniformDirty = true; _dataOwned = false; _maskOwned = false; _dataWasmManaged = false; _maskWasmManaged = false; _wasmDataCapacity = 0; _wasmMaskCapacity = 0; _wasmCapacityHint = 0; _scaleRevision = 0; _dataRevision = 0; _maskRevision = 0; _visualChangeListeners = /* @__PURE__ */ new Set(); constructor(desc) { assert(!!desc, "LatticeSpace: descriptor is required."); this.dimensions = normalizeDimensions(desc.dimensions); this.dimensionCount = this.dimensions.length; this.cellCount = this.dimensions.reduce((product, value) => product * value, 1); const componentCount = desc.componentCount ?? 1; assert(Number.isInteger(componentCount) && componentCount >= 1 && componentCount <= 4, "LatticeSpace: componentCount must be 1, 2, 3, or 4."); this.componentCount = componentCount; this._origin = normalizeVec3(desc.origin, [0, 0, 0], "origin"); this._spacing = normalizeVec3(desc.spacing, [1, 1, 1], "spacing", true); this._cellScale = normalizeCellScale(desc.cellScale); this._indexRange = this.normalizeIndexRange(desc.indexRange); this._scaleTransform = this.normalizeLatticeScaleTransform(desc.scaleTransform ?? {}); this._wasmCapacityHint = assertWasmCapacity(desc.wasmCapacity, "LatticeSpace: wasmCapacity"); if (desc.name !== void 0) this.name = desc.name; if (desc.visible !== void 0) this.visible = !!desc.visible; if (desc.blendMode !== void 0) this.blendMode = desc.blendMode; if (desc.cullMode !== void 0) this.cullMode = desc.cullMode; if (desc.depthWrite !== void 0) this.depthWrite = !!desc.depthWrite; if (desc.depthTest !== void 0) this.depthTest = !!desc.depthTest; if (desc.opacity !== void 0) this.opacity = desc.opacity; if (desc.lit !== void 0) this.lit = desc.lit; if (desc.colorMode !== void 0) this.colorMode = desc.colorMode; if (desc.colorSpace !== void 0) this.colorSpace = desc.colorSpace; if (desc.solidColor !== void 0) this.solidColor = desc.solidColor; if (desc.colormap !== void 0) this._colormap = desc.colormap; if (desc.colormapStops !== void 0) this._colormapStops = normalizeColorStops(desc.colormapStops); if (desc.valueRange !== void 0) this.valueRange = desc.valueRange; this._keepCPUData = !!desc.keepCPUData; const dataSources = Number(!!desc.data) + Number(!!desc.wasmData) + Number(!!desc.dataBuffer); const maskSources = Number(!!desc.mask) + Number(!!desc.wasmMask) + Number(!!desc.maskBuffer); assert(dataSources <= 1, "LatticeSpace: data, wasmData, and dataBuffer are mutually exclusive."); assert(maskSources <= 1, "LatticeSpace: mask, wasmMask, and maskBuffer are mutually exclusive."); if (this._colorMode === "rgba") assert(this.componentCount === 4, "LatticeSpace: rgba colorMode requires componentCount 4."); if (desc.data) this.setData(desc.data, { keepCPUData: this._keepCPUData }); else if (desc.wasmData) this.setWasmData(desc.wasmData, { capacity: this._wasmCapacityHint, keepCPUData: this._keepCPUData }); else if (desc.dataBuffer) this.setDataBuffer(resolveGPUBuffer(desc.dataBuffer), { ownBuffer: !!desc.ownBuffers }); if (desc.mask) this.setMask(desc.mask, { keepCPUData: this._keepCPUData }); else if (desc.wasmMask) this.setWasmMask(desc.wasmMask, { capacity: this._wasmCapacityHint, keepCPUData: this._keepCPUData }); else if (desc.maskBuffer) this.setMaskBuffer(resolveGPUBuffer(desc.maskBuffer), { ownBuffer: !!desc.ownBuffers }); } normalizeLatticeScaleTransform(transform) { const normalized = normalizeScaleTransform({ componentCount: this.componentCount, componentIndex: 0, ...transform, stride: this.componentCount, offset: 0 }); assert(normalized.componentCount <= this.componentCount, "LatticeSpace: scaleTransform componentCount cannot exceed the lattice componentCount."); assert(normalized.componentIndex < this.componentCount, "LatticeSpace: scaleTransform componentIndex must address a lattice component."); return normalized; } normalizeIndex(index, label, allowEnd) { assert(index.length === this.dimensionCount, `LatticeSpace: ${label} rank must match dimensions.`); const out = index.map((value, axis) => { assert(Number.isInteger(value), `LatticeSpace: ${label} must contain integers.`); const maximum = this.dimensions[axis]; assert(value >= 0 && (allowEnd ? value <= maximum : value < maximum), `LatticeSpace: ${label} is outside dimensions.`); return value; }); return out; } normalizeIndexRange(range) { if (!range) return { min: new Array(this.dimensionCount).fill(0), max: [...this.dimensions] }; const min = this.normalizeIndex(range.min, "indexRange.min", false); const max = this.normalizeIndex(range.max, "indexRange.max", true); for (let axis = 0; axis < this.dimensionCount; axis++) assert(max[axis] > min[axis], "LatticeSpace: indexRange.max must be greater than indexRange.min on every axis."); return { min, max }; } replaceDataBuffer(buffer, owned) { if (this.dataBuffer && this.dataBuffer !== buffer && this._dataOwned) this.dataBuffer.destroy(); this.dataBuffer = buffer; this._dataOwned = !!buffer && owned; this.bindGroupKey = null; } replaceMaskBuffer(buffer, owned) { if (this.maskBuffer && this.maskBuffer !== buffer && this._maskOwned) this.maskBuffer.destroy(); this.maskBuffer = buffer; this._maskOwned = !!buffer && owned; this.bindGroupKey = null; } validateDataLength(length) { assert(length === this.cellCount * this.componentCount, "LatticeSpace: data length must equal cellCount * componentCount."); } get origin() { return [...this._origin]; } set origin(value) { this._origin = normalizeVec3(value, [0, 0, 0], "origin"); this._uniformDirty = true; } get spacing() { return [...this._spacing]; } set spacing(value) { this._spacing = normalizeVec3(value, [1, 1, 1], "spacing", true); this._uniformDirty = true; } get cellScale() { return [...this._cellScale]; } set cellScale(value) { this._cellScale = normalizeCellScale(value); this._uniformDirty = true; } get indexRange() { return { min: [...this._indexRange.min], max: [...this._indexRange.max] }; } set indexRange(value) { this._indexRange = this.normalizeIndexRange(value); this._uniformDirty = true; } get valueRange() { return this._valueRange ? [...this._valueRange] : null; } set valueRange(value) { if (value) assert(Number.isFinite(value[0]) && Number.isFinite(value[1]) && value[1] >= value[0], "LatticeSpace: valueRange must be a finite ascending pair."); this._valueRange = value ? [value[0], value[1]] : null; this._uniformDirty = true; } get opacity() { return this._opacity; } set opacity(value) { assert(Number.isFinite(value), "LatticeSpace: opacity must be finite."); this._opacity = value; this._uniformDirty = true; } get lit() { return this._lit; } set lit(value) { this._lit = !!value; this._uniformDirty = true; } get colorMode() { return this._colorMode; } set colorMode(value) { assert(value === "scalar" || value === "rgba" || value === "solid", "LatticeSpace: invalid colorMode."); if (value === "rgba") assert(this.componentCount === 4, "LatticeSpace: rgba colorMode requires componentCount 4."); if (this._colorMode === value) return; this._colorMode = value; this._uniformDirty = true; this.emitVisualChange("visual"); } get colorSpace() { return this._colorSpace; } set colorSpace(value) { assert(value === "linear" || value === "srgb", "LatticeSpace: invalid colorSpace."); this._colorSpace = value; this._uniformDirty = true; } get solidColor() { return [...this._solidColor]; } set solidColor(value) { this._solidColor = [value[0], value[1], value[2], value[3]]; this._uniformDirty = true; } get colormap() { return this._colormap; } set colormap(value) { this._colormap = value; this.bindGroupKey = null; this.emitVisualChange("colormap"); } get colormapStops() { return this._colormapStops; } set colormapStops(value) { this._colormapStops = normalizeColorStops(value); this._uniformDirty = true; this.emitVisualChange("colormap"); } get scaleTransform() { return cloneScaleTransform(this._scaleTransform); } get hasData() { return !!this.dataBuffer || !!this._dataCPU || !!this._wasmDataSource; } get hasMask() { return !!this.maskBuffer || !!this._maskCPU || !!this._wasmMaskSource; } get drawCellCount() { return this._indexRange.max.reduce((product, value, axis) => product * (value - this._indexRange.min[axis]), 1); } get occluderRevision() { let hash = 2166136261 >>> 0; hash = mixRevision(hash, this._dataRevision); hash = mixRevision(hash, this._maskRevision); hash = mixRevision(hash, blendModeId(this.blendMode)); hash = mixRevision(hash, cullModeId(this.cullMode)); hash = mixRevision(hash, this.depthWrite ? 1 : 0); hash = mixRevision(hash, this.depthTest ? 1 : 0); hash = mixRevision(hash, colorModeId4(this._colorMode)); hash = mixRevision(hash, this._valueRange ? 1 : 0); if (this._valueRange) for (const value of this._valueRange) hash = mixRevisionF32(hash, value); hash = mixRevision(hash, this._scaleTransform.valueMode === "magnitude" ? 1 : 0); hash = mixRevision(hash, this._scaleTransform.componentCount); hash = mixRevision(hash, this._scaleTransform.componentIndex); for (const value of [...this.dimensions, ...this._indexRange.min, ...this._indexRange.max]) hash = mixRevision(hash, value); for (const value of [...this._origin, ...this._spacing, ...this._cellScale]) hash = mixRevisionF32(hash, value); return hash >>> 0; } get sortRevision() { return this.occluderRevision; } setScaleTransform(transform) { this._scaleTransform = this.normalizeLatticeScaleTransform(transform); this._uniformDirty = true; this.emitVisualChange("scale"); } applyScaleStats(stats) { const next = cloneScaleTransform(this._scaleTransform); if (Number.isFinite(stats.min)) next.domainMin = stats.min; if (Number.isFinite(stats.max)) next.domainMax = stats.max; if (stats.percentileMin !== null && stats.percentileMax !== null) { next.clampMin = stats.percentileMin; next.clampMax = stats.percentileMax; } this.setScaleTransform(next); } onVisualChange(listener) { this._visualChangeListeners.add(listener); return () => this._visualChangeListeners.delete(listener); } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) try { listener(kind); } catch { } } getScaleSourceDescriptor(revision = this._scaleRevision) { if (!this.dataBuffer || this.cellCount <= 0 || this._colorMode === "solid") return null; return { buffer: this.dataBuffer, count: this.cellCount, componentCount: this._scaleTransform.componentCount, componentIndex: this._scaleTransform.componentIndex, valueMode: this._scaleTransform.valueMode, stride: this._scaleTransform.stride, offset: this._scaleTransform.offset, revision }; } getColormapKey() { return this._colormap instanceof Colormap ? `cm:${this._colormap.id}` : `cm:${this._colormap}`; } getColormapForBinding() { if (this._colormap instanceof Colormap) return this._colormap; return this._colormap === "custom" ? Colormap.builtin("grayscale") : Colormap.builtin(this._colormap); } mapLinearIndexToCell(index) { if (!Number.isInteger(index) || index < 0 || index >= this.cellCount) return null; const width = this.dimensions[0]; const height = this.dimensions[1]; const x = index % width; const y = Math.floor(index / width) % height; return this.dimensionCount === 2 ? [x, y] : [x, y, Math.floor(index / (width * height))]; } mapCellIndexToLinear(index) { const cell = this.normalizeIndex(index, "cell index", false); return cell[0] + this.dimensions[0] * (cell[1] + (this.dimensionCount === 3 ? this.dimensions[1] * (cell[2] ?? 0) : 0)); } getCellRecord(index) { const cell = this.mapLinearIndexToCell(index); if (!cell) return null; const values = []; if (this._dataCPU) for (let component = 0; component < this.componentCount; component++) values.push(this._dataCPU[index * this.componentCount + component]); const scalar = values.length ? this._scaleTransform.valueMode === "magnitude" ? Math.hypot(...values.slice(0, this._scaleTransform.componentCount)) : values[Math.min(this.componentCount - 1, this._scaleTransform.componentIndex)] : null; const color = this._colorMode === "rgba" && values.length === 4 ? [values[0], values[1], values[2], values[3]] : null; return { index: cell, center: [this._origin[0] + cell[0] * this._spacing[0], this._origin[1] + cell[1] * this._spacing[1], this._origin[2] + (cell[2] ?? 0) * this._spacing[2]], values, scalar, color, active: this._maskCPU ? this._maskCPU[index] !== 0 : true }; } setData(data, options = {}) { this.validateDataLength(data.length); this._wasmDataSource = null; this._dataWasmManaged = false; this._wasmDataCapacity = 0; this._dataCPU = new Float32Array(data); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; this._dataDirty = true; this._dataRevision++; this._scaleRevision++; this.bindGroupKey = null; } updateData(data, startCell = 0) { assert(Number.isInteger(startCell) && startCell >= 0, "LatticeSpace: startCell must be a non-negative integer."); assert(data.length % this.componentCount === 0 && startCell + data.length / this.componentCount <= this.cellCount, "LatticeSpace: updateData range exceeds cell data."); assert(!!this._dataCPU, "LatticeSpace: updateData requires retained CPU data; use setData for replacement."); this._dataCPU.set(data, startCell * this.componentCount); this._dataDirty = true; this._dataRevision++; this._scaleRevision++; } setDataBuffer(buffer, options = {}) { assert(buffer.size >= this.cellCount * this.componentCount * 4, "LatticeSpace: dataBuffer is too small."); this._wasmDataSource = null; this._dataCPU = null; this._dataDirty = false; this._dataWasmManaged = false; this._wasmDataCapacity = 0; this.replaceDataBuffer(buffer, !!options.ownBuffer); this._dataRevision++; this._scaleRevision++; } markDataDirty() { assert(!!this.dataBuffer, "LatticeSpace: markDataDirty requires a dataBuffer."); this._dataRevision++; this._scaleRevision++; } setMask(mask, options = {}) { assert(mask.length === this.cellCount, "LatticeSpace: mask length must equal cellCount."); this._wasmMaskSource = null; this._maskWasmManaged = false; this._wasmMaskCapacity = 0; this._maskCPU = new Uint32Array(mask); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; this._maskDirty = true; this._maskRevision++; this.bindGroupKey = null; } updateMask(mask, startCell = 0) { assert(Number.isInteger(startCell) && startCell >= 0 && startCell + mask.length <= this.cellCount, "LatticeSpace: updateMask range exceeds cell data."); assert(!!this._maskCPU, "LatticeSpace: updateMask requires retained CPU mask data; use setMask for replacement."); this._maskCPU.set(mask, startCell); this._maskDirty = true; this._maskRevision++; } setMaskBuffer(buffer, options = {}) { if (buffer) assert(buffer.size >= this.cellCount * 4, "LatticeSpace: maskBuffer is too small."); this._wasmMaskSource = null; this._maskCPU = null; this._maskDirty = false; this._maskWasmManaged = false; this._wasmMaskCapacity = 0; this.replaceMaskBuffer(buffer, !!buffer && !!options.ownBuffer); this._maskRevision++; } markMaskDirty() { assert(!!this.maskBuffer, "LatticeSpace: markMaskDirty requires a maskBuffer."); this._maskRevision++; } setWasmData(source, options = {}) { if (!source) { this._wasmDataSource = null; return; } this._wasmDataSource = assertWasmF32View(source, "LatticeSpace: wasmData"); this._wasmCapacityHint = assertWasmCapacity(options.capacity, "LatticeSpace: wasmData capacity"); this._dataCPU = null; this.refreshWasmData(options); } setWasmMask(source, options = {}) { if (!source) { this._wasmMaskSource = null; return; } this._wasmMaskSource = assertWasmU32View(source, "LatticeSpace: wasmMask"); this._wasmCapacityHint = assertWasmCapacity(options.capacity, "LatticeSpace: wasmMask capacity"); this._maskCPU = null; this.refreshWasmMask(options); } refreshWasmData(options = {}) { const source = this._wasmDataSource; if (!source) return; source.refresh(); assertWasmF32View(source, "LatticeSpace: wasmData"); this.validateDataLength(source.length); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; this._dataCPU = this._keepCPUData ? new Float32Array(source.array()) : null; this._dataDirty = true; this._dataRevision++; this._scaleRevision++; } refreshWasmMask(options = {}) { const source = this._wasmMaskSource; if (!source) return; source.refresh(); assertWasmU32View(source, "LatticeSpace: wasmMask"); assert(source.length === this.cellCount, "LatticeSpace: wasmMask length must equal cellCount."); this._keepCPUData = options.keepCPUData ?? this._keepCPUData; this._maskCPU = this._keepCPUData ? new Uint32Array(source.array()) : null; this._maskDirty = true; this._maskRevision++; } refreshFromWasm(options = {}) { this.refreshWasmData(options); this.refreshWasmMask(options); } clearWasmSources() { this._wasmDataSource = null; this._wasmMaskSource = null; } dropCPUData() { this._dataCPU = null; this._maskCPU = null; } getLocalBounds() { const min = this._indexRange.min; const max = this._indexRange.max; const half = [this._spacing[0] * this._cellScale[0] * 0.5, this._spacing[1] * this._cellScale[1] * 0.5, this.dimensionCount === 3 ? this._spacing[2] * this._cellScale[2] * 0.5 : 0]; return boundsFromBox( [ this._origin[0] + min[0] * this._spacing[0] - half[0], this._origin[1] + min[1] * this._spacing[1] - half[1], this._origin[2] + (min[2] ?? 0) * this._spacing[2] - half[2] ], [ this._origin[0] + (max[0] - 1) * this._spacing[0] + half[0], this._origin[1] + (max[1] - 1) * this._spacing[1] + half[1], this._origin[2] + ((max[2] ?? 1) - 1) * this._spacing[2] + half[2] ] ); } getWorldBounds() { return transformBounds(this.getLocalBounds(), this.transform.worldMatrix); } getBounds() { return this.getWorldBounds(); } ensureManagedDataBuffer(device) { const capacity = growWasmCapacity(Math.max(this.cellCount, this._wasmCapacityHint), this._wasmDataCapacity); if (this.dataBuffer && this._dataWasmManaged && this._wasmDataCapacity >= capacity) return; this.replaceDataBuffer(device.createBuffer({ label: "LatticeSpace.data", size: capacity * this.componentCount * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._dataWasmManaged = true; this._wasmDataCapacity = capacity; } ensureManagedMaskBuffer(device) { const capacity = growWasmCapacity(Math.max(this.cellCount, this._wasmCapacityHint), this._wasmMaskCapacity); if (this.maskBuffer && this._maskWasmManaged && this._wasmMaskCapacity >= capacity) return; this.replaceMaskBuffer(device.createBuffer({ label: "LatticeSpace.mask", size: capacity * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }), true); this._maskWasmManaged = true; this._wasmMaskCapacity = capacity; } upload(device, queue) { if (this._dataDirty) { const source = this._wasmDataSource ? (this._wasmDataSource.refresh(), this._wasmDataSource.array()) : this._dataCPU; if (source) { this.validateDataLength(source.length); this.ensureManagedDataBuffer(device); queue.writeBuffer(this.dataBuffer, 0, source.buffer, source.byteOffset, source.byteLength); if (!this._keepCPUData) this._dataCPU = null; } this._dataDirty = false; } if (this._maskDirty) { const source = this._wasmMaskSource ? (this._wasmMaskSource.refresh(), this._wasmMaskSource.array()) : this._maskCPU; if (source) { assert(source.length === this.cellCount, "LatticeSpace: mask length must equal cellCount."); this.ensureManagedMaskBuffer(device); queue.writeBuffer(this.maskBuffer, 0, source.buffer, source.byteOffset, source.byteLength); if (!this._keepCPUData) this._maskCPU = null; } this._maskDirty = false; } } getUniformBufferSize() { return UNIFORM_BYTE_SIZE5; } getUniformData() { const out = new Float32Array(UNIFORM_FLOAT_COUNT5); const dims = this.dimensions; const min = this._indexRange.min; const max = this._indexRange.max; out.set([dims[0], dims[1], dims[2] ?? 1, this.dimensionCount], 0); out.set([...this._origin, 0], 4); out.set([...this._spacing, 0], 8); out.set([...this._cellScale, 0], 12); out.set([min[0], min[1], min[2] ?? 0, 0], 16); out.set([max[0], max[1], max[2] ?? 1, 0], 20); out.set([this.componentCount, colorModeId4(this._colorMode), colorSpaceId(this._colorSpace), this.hasMask ? 1 : 0], 24); out.set([clamp01(this._opacity), this._lit ? 1 : 0, this._valueRange ? this._valueRange[0] : 0, this._valueRange ? this._valueRange[1] : 0], 28); out.set([this._valueRange ? 1 : 0, this._colormap === "custom" ? Math.min(8, Math.max(2, this._colormapStops.length)) : 0, this.dimensionCount === 3 ? 1 : 0, 0], 32); out.set(this._solidColor, 36); packScaleTransform(this._scaleTransform, out, BASE_UNIFORM_FLOAT_COUNT); const stopsOffset = BASE_UNIFORM_FLOAT_COUNT + SCALE_UNIFORM_FLOAT_COUNT; const count = Math.min(8, Math.max(2, this._colormapStops.length)); for (let i = 0; i < 8; i++) out.set(this._colormapStops[Math.min(i, count - 1)], stopsOffset + i * 4); return out; } get dirtyUniforms() { return this._uniformDirty; } markUniformsClean() { this._uniformDirty = false; } destroy() { if (this.dataBuffer && this._dataOwned) this.dataBuffer.destroy(); if (this.maskBuffer && this._maskOwned) this.maskBuffer.destroy(); this.uniformBuffer?.destroy(); this.dataBuffer = null; this.maskBuffer = null; this.uniformBuffer = null; this.bindGroup = null; this.bindGroupKey = null; this._dataCPU = null; this._maskCPU = null; this._wasmDataSource = null; this._wasmMaskSource = null; this._visualChangeListeners.clear(); this.transform.dispose(); } }; // wgsl/core/picking-mesh.wgsl var picking_mesh_default = "enable primitive_index; struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct VertexOutput { @builtin(position) position: vec4, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var pick: PickUniforms; @vertex fn vs_main(@location(0) position: vec3) -> VertexOutput { var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(position, 1.0); return out; } @fragment fn fs_main( @builtin(position) frag_coord: vec4, @builtin(primitive_index) primitive_index: u32, ) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + primitive_index); out.depth = frag_coord.z; return out; }"; // wgsl/core/picking-mesh-skinned.wgsl var picking_mesh_skinned_default = "enable primitive_index; struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct SkinBuffer { joints: array>, } struct VertexOutput { @builtin(position) position: vec4, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var pick: PickUniforms; @group(2) @binding(0) var skin: SkinBuffer; @vertex fn vs_main( @location(0) position: vec3, @location(3) joints: vec4, @location(4) weights: vec4, ) -> VertexOutput { var out: VertexOutput; let m = skin.joints[joints.x] * weights.x + skin.joints[joints.y] * weights.y + skin.joints[joints.z] * weights.z + skin.joints[joints.w] * weights.w; let local_pos = m * vec4(position, 1.0); out.position = camera.view_proj * model.model * local_pos; return out; } @fragment fn fs_main( @builtin(position) frag_coord: vec4, @builtin(primitive_index) primitive_index: u32, ) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + primitive_index); out.depth = frag_coord.z; return out; }"; // wgsl/core/picking-mesh-skinned8.wgsl var picking_mesh_skinned8_default = "enable primitive_index; struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct SkinBuffer { joints: array>, } struct VertexOutput { @builtin(position) position: vec4, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var pick: PickUniforms; @group(2) @binding(0) var skin: SkinBuffer; @vertex fn vs_main( @location(0) position: vec3, @location(3) joints0: vec4, @location(4) weights0: vec4, @location(5) joints1: vec4, @location(6) weights1: vec4, ) -> VertexOutput { var out: VertexOutput; let m = skin.joints[joints0.x] * weights0.x + skin.joints[joints0.y] * weights0.y + skin.joints[joints0.z] * weights0.z + skin.joints[joints0.w] * weights0.w + skin.joints[joints1.x] * weights1.x + skin.joints[joints1.y] * weights1.y + skin.joints[joints1.z] * weights1.z + skin.joints[joints1.w] * weights1.w; let local_pos = m * vec4(position, 1.0); out.position = camera.view_proj * model.model * local_pos; return out; } @fragment fn fs_main( @builtin(position) frag_coord: vec4, @builtin(primitive_index) primitive_index: u32, ) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + primitive_index); out.depth = frag_coord.z; return out; }"; // wgsl/world/picking-pointcloud.wgsl var picking_pointcloud_default = "struct PointData { position: vec3, scalar: f32, } struct PointCloudUniforms { size_params: vec4, scalar_params: vec4, options: vec4, colors: array, 8>, } struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct VertexOutput { @builtin(position) position: vec4, @location(0) point_coord: vec2, @location(1) @interpolate(flat) point_index: u32, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var points: array; @group(1) @binding(1) var pc: PointCloudUniforms; @group(2) @binding(0) var pick: PickUniforms; @vertex fn vs_main( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let p = points[instance_index]; let world_pos = model.model * vec4(p.position, 1.0); let clip = camera.view_proj * world_pos; let dist = distance(camera.position, world_pos.xyz); let base_size = pc.size_params.x; let min_size = pc.size_params.y; let max_size = pc.size_params.z; let atten = pc.size_params.w; var size_px = base_size; if (atten > 0.0) { size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); var uv = vec2(0.0); if (vertex_index == 0u) { uv = vec2(0.0, 0.0); } else if (vertex_index == 1u) { uv = vec2(1.0, 0.0); } else if (vertex_index == 2u) { uv = vec2(0.0, 1.0); } else if (vertex_index == 3u) { uv = vec2(1.0, 0.0); } else if (vertex_index == 4u) { uv = vec2(1.0, 1.0); } else if (vertex_index == 5u) { uv = vec2(0.0, 1.0); } let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; var out: VertexOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.point_coord = uv; out.point_index = instance_index; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { let uv = in.point_coord * 2.0 - vec2(1.0, 1.0); let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + in.point_index); out.depth = in.position.z; return out; }"; // wgsl/world/picking-glyphfield.wgsl var picking_glyphfield_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct VertexInput { @location(0) position: vec3, } struct VertexOutput { @builtin(position) position: vec4, @location(0) @interpolate(flat) instance_index: u32, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var positions: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; @group(2) @binding(0) var pick: PickUniforms; fn rotate_by_quat(v: vec3, q: vec4) -> vec3 { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } @vertex fn vs_main(in: VertexInput, @builtin(instance_index) instance_index: u32) -> VertexOutput { let p4 = positions[instance_index]; let q = rotations[instance_index]; let s4 = scales[instance_index]; let local_pos = rotate_by_quat(in.position * s4.xyz, q) + p4.xyz; let world_pos = model.model * vec4(local_pos, 1.0); var out: VertexOutput; out.position = camera.view_proj * world_pos; out.instance_index = instance_index; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + in.instance_index); out.depth = in.position.z; return out; }"; // wgsl/world/picking-nodelink.wgsl var picking_nodelink_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct NodeLinkUniforms { global: vec4, node_scale_source: vec4, node_scale_domain: vec4, node_scale_clamp: vec4, node_scale_params: vec4, node_scale_flags: vec4, node_visual: vec4, edge_scale_source: vec4, edge_scale_domain: vec4, edge_scale_clamp: vec4, edge_scale_params: vec4, edge_scale_flags: vec4, edge_visual: vec4, node_solid: vec4, edge_solid: vec4, point_params: vec4, node_stops: array, 8>, edge_stops: array, 8>, } struct PickOutput { @builtin(position) position: vec4, @location(0) @interpolate(flat) index: u32, @location(1) point_coord: vec2, @location(2) @interpolate(flat) is_point: f32, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var node_positions: array>; @group(1) @binding(3) var node_radii: array>; @group(1) @binding(4) var edges: array>; @group(1) @binding(7) var nl: NodeLinkUniforms; @group(2) @binding(0) var pick: PickUniforms; fn build_edge_frame(src: vec3, dst: vec3) -> mat3x3 { let y_axis = normalize(dst - src); var fallback_axis = vec3(0.0, 0.0, 1.0); if (abs(dot(fallback_axis, y_axis)) > 0.99) { fallback_axis = vec3(1.0, 0.0, 0.0); } let x_axis = normalize(cross(fallback_axis, y_axis)); let z_axis = normalize(cross(y_axis, x_axis)); return mat3x3(x_axis, y_axis, z_axis); } @vertex fn vs_pick_node_points( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> PickOutput { let p = node_positions[instance_index].xyz; let world_pos4 = model.model * vec4(p, 1.0); let clip = camera.view_proj * world_pos4; let base_size = nl.global.x; let min_size = nl.point_params.x; let max_size = nl.point_params.y; let atten = nl.point_params.z; var size_px = base_size; if (atten > 0.0) { let dist = distance(camera.position, world_pos4.xyz); size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); let uv = vec2(f32((vertex_index + 2u) / 3u % 2u), f32((vertex_index + 1u) / 3u % 2u)); let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; var out: PickOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.index = instance_index; out.point_coord = uv * 2.0 - vec2(1.0, 1.0); out.is_point = 1.0; return out; } @vertex fn vs_pick_node_solid( @location(0) position: vec3, @builtin(instance_index) instance_index: u32, ) -> PickOutput { let center = node_positions[instance_index].xyz; let mode = u32(round(nl.node_visual.z)); let use_radii = nl.node_visual.w > 0.5; var scale_vec = vec3(max(nl.global.x, 1e-6)); if (use_radii) { let rv = max(node_radii[instance_index].xyz, vec3(1e-6)); if (mode == 2u) { scale_vec = rv * max(nl.global.x, 1e-6); } else { scale_vec = vec3(rv.x * max(nl.global.x, 1e-6)); } } let obj_pos = center + (position * scale_vec); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: PickOutput; out.position = camera.view_proj * world_pos4; out.index = instance_index; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @vertex fn vs_pick_edge_lines( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> PickOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let obj_pos = select(src, dst, (vertex_index & 1u) == 1u); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: PickOutput; out.position = camera.view_proj * world_pos4; out.index = instance_index; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @vertex fn vs_pick_edge_cylinders( @location(0) position: vec3, @builtin(instance_index) instance_index: u32, ) -> PickOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let seg = dst - src; let seg_len = max(length(seg), 1e-6); let basis = build_edge_frame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3(position.x * radius, position.y * seg_len, position.z * radius); let obj_pos = ((src + dst) * 0.5) + (basis * local); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: PickOutput; out.position = camera.view_proj * world_pos4; out.index = instance_index; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @fragment fn fs_pick(in: PickOutput) -> FragmentOutput { if (in.is_point > 0.5) { let r2 = dot(in.point_coord, in.point_coord); if (r2 > 1.0) { discard; } } var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + in.index); out.depth = in.position.z; return out; }"; // wgsl/world/picking-splatfield.wgsl var picking_splatfield_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, viewport_height: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct SplatFieldUniforms { params: vec4, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct VertexOutput { @builtin(position) position: vec4, @location(0) local_coord: vec2, @location(1) @interpolate(flat) splat_index: u32, @location(2) alpha_base: f32, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var center_opacity: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; @group(1) @binding(3) var colors: array>; @group(1) @binding(5) var splat_field: SplatFieldUniforms; @group(1) @binding(6) var sh_coefficients: array; @group(2) @binding(0) var pick: PickUniforms; fn rotate_by_quat(v: vec3, q: vec4) -> vec3 { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } fn safe_clip_w(w: f32) -> f32 { return select(1e-6, w, abs(w) > 1e-6); } fn splat_center_renderable(clip: vec4) -> bool { let eps = 1e-6; return (clip.w > eps) && (clip.z >= -eps) && (clip.z <= clip.w + eps); } fn row4(m: mat4x4, r: u32) -> vec4 { return vec4(m[0][r], m[1][r], m[2][r], m[3][r]); } fn invalid_vertex() -> VertexOutput { var out: VertexOutput; out.position = vec4(2.0, 2.0, 2.0, 1.0); out.local_coord = vec2(0.0); out.splat_index = 0u; out.alpha_base = 0.0; return out; } fn quad_corner(vertex_index: u32) -> vec2 { if (vertex_index == 0u) { return vec2(-1.0, -1.0); } if (vertex_index == 1u) { return vec2(1.0, -1.0); } if (vertex_index == 2u) { return vec2(-1.0, 1.0); } if (vertex_index == 3u) { return vec2(-1.0, 1.0); } if (vertex_index == 4u) { return vec2(1.0, -1.0); } return vec2(1.0, 1.0); } @vertex fn vs_main( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let splat_index = instance_index; let center_opacity_value = center_opacity[splat_index]; let rotation_value = rotations[splat_index]; let scale_value = max(abs(scales[splat_index].xyz), vec3(1e-6)); let color_value = colors[splat_index]; let world_center4 = model.model * vec4(center_opacity_value.xyz, 1.0); let clip_center = camera.view_proj * world_center4; if (!splat_center_renderable(clip_center)) { return invalid_vertex(); } let guarded_clip_w = safe_clip_w(clip_center.w); let local_axis_x = rotate_by_quat(vec3(scale_value.x, 0.0, 0.0), rotation_value); let local_axis_y = rotate_by_quat(vec3(0.0, scale_value.y, 0.0), rotation_value); let local_axis_z = rotate_by_quat(vec3(0.0, 0.0, scale_value.z), rotation_value); let world_axis_x = (model.model * vec4(local_axis_x, 0.0)).xyz; let world_axis_y = (model.model * vec4(local_axis_y, 0.0)).xyz; let world_axis_z = (model.model * vec4(local_axis_z, 0.0)).xyz; let view_proj_row0 = row4(camera.view_proj, 0u); let view_proj_row1 = row4(camera.view_proj, 1u); let view_proj_row3 = row4(camera.view_proj, 3u); let inv_clip_w_sq = 1.0 / (guarded_clip_w * guarded_clip_w); let jx = (view_proj_row0.xyz * guarded_clip_w - clip_center.x * view_proj_row3.xyz) * inv_clip_w_sq; let jy = (view_proj_row1.xyz * guarded_clip_w - clip_center.y * view_proj_row3.xyz) * inv_clip_w_sq; let a0 = vec2(dot(jx, world_axis_x), dot(jy, world_axis_x)); let a1 = vec2(dot(jx, world_axis_y), dot(jy, world_axis_y)); let a2 = vec2(dot(jx, world_axis_z), dot(jy, world_axis_z)); let cov_xx = a0.x * a0.x + a1.x * a1.x + a2.x * a2.x; let cov_xy = a0.x * a0.y + a1.x * a1.y + a2.x * a2.y; let cov_yy = a0.y * a0.y + a1.y * a1.y + a2.y * a2.y; let trace = cov_xx + cov_yy; let diff = cov_xx - cov_yy; let root = sqrt(max(0.0, diff * diff + 4.0 * cov_xy * cov_xy)); let lambda0 = max(1e-10, 0.5 * (trace + root)); let lambda1 = max(1e-10, 0.5 * (trace - root)); var axis0 = vec2(1.0, 0.0); if (abs(cov_xy) > 1e-8) { axis0 = normalize(vec2(cov_xy, lambda0 - cov_xx)); } else if (cov_yy > cov_xx) { axis0 = vec2(0.0, 1.0); } let axis1 = vec2(-axis0.y, axis0.x); let basis0 = axis0 * sqrt(lambda0) * 3.0; let basis1 = axis1 * sqrt(lambda1) * 3.0; let viewport_height = max(camera.viewport_height, 1.0); let radius_ndc = max(length(basis0), length(basis1)); let radius_px = radius_ndc * 0.5 * viewport_height; let max_radius_px = max(96.0, min(512.0, viewport_height * 0.45)); let fade_start_px = max_radius_px * 0.75; if (radius_px >= max_radius_px) { return invalid_vertex(); } let radius_fade = 1.0 - smoothstep(fade_start_px, max_radius_px, radius_px); let corner = quad_corner(vertex_index); let ndc_offset = (basis0 * corner.x) + (basis1 * corner.y); let clip_offset = ndc_offset * clip_center.w; let alpha_base = clamp(color_value.a, 0.0, 1.0) * clamp(center_opacity_value.w, 0.0, 1.0) * clamp(splat_field.params.x, 0.0, 1.0) * radius_fade; var out: VertexOutput; out.position = clip_center + vec4(clip_offset, 0.0, 0.0); out.local_coord = corner; out.splat_index = splat_index; out.alpha_base = alpha_base; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { let q = dot(in.local_coord, in.local_coord); if (q > 1.0) { discard; } let alpha = in.alpha_base * exp(-4.5 * q); if (alpha <= 1e-4) { discard; } var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + in.splat_index); out.depth = in.position.z; return out; }"; // wgsl/world/picking-latticespace.wgsl var picking_latticespace_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, viewport_height: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct LatticeUniforms { dimensions: vec4, origin: vec4, spacing: vec4, cell_scale: vec4, range_min: vec4, range_max: vec4, data_config: vec4, visual: vec4, filters: vec4, solid_color: vec4, scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, colors: array, 8>, } struct PickUniforms { object_id: u32, element_base: u32, _pad0: u32, _pad1: u32, } struct VertexOutput { @builtin(position) position: vec4, @location(0) local_position: vec3, @location(1) @interpolate(flat) cell: vec3, @location(2) @interpolate(flat) cell_index: u32, @location(3) @interpolate(flat) face: u32, } struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var cell_data: array; @group(1) @binding(1) var cell_mask: array; @group(1) @binding(2) var sorted_indices: array; @group(1) @binding(3) var lattice: LatticeUniforms; @group(2) @binding(0) var pick: PickUniforms; fn finite_value(value: f32) -> bool { return (bitcast(value) & 0x7f800000u) != 0x7f800000u; } fn component(value: vec4, index: u32) -> f32 { if (index == 0u) { return value.x; } if (index == 1u) { return value.y; } if (index == 2u) { return value.z; } return value.w; } fn load_value(index: u32) -> vec4 { let count = u32(lattice.data_config.x + 0.5); let base = index * count; var out = vec4(0); if (count > 0u) { out.x = cell_data[base]; } if (count > 1u) { out.y = cell_data[base + 1u]; } if (count > 2u) { out.z = cell_data[base + 2u]; } if (count > 3u) { out.w = cell_data[base + 3u]; } return out; } fn select_scalar(value: vec4) -> f32 { let count = max(1u, min(4u, u32(lattice.scale_source.x + 0.5))); if (u32(lattice.scale_source.z + 0.5) == 1u) { if (count == 1u) { return abs(value.x); } if (count == 2u) { return length(value.xy); } if (count == 3u) { return length(value.xyz); } return length(value); } return component(value, min(3u, u32(lattice.scale_source.y + 0.5))); } fn cell_visible(index: u32) -> bool { if (lattice.data_config.w > 0.5 && cell_mask[index] == 0u) { return false; } let mode = u32(lattice.data_config.y + 0.5); if (mode == 2u) { return true; } let value = load_value(index); if (mode == 0u) { let scalar = select_scalar(value); if (!finite_value(scalar)) { return false; } if (lattice.filters.x > 0.5 && (scalar < lattice.visual.z || scalar > lattice.visual.w)) { return false; } } else if ( mode == 1u && ( !finite_value(value.x) || !finite_value(value.y) || !finite_value(value.z) || !finite_value(value.w) ) ) { return false; } return true; } fn cell_to_linear(cell: vec3) -> u32 { let dims = vec3(lattice.dimensions.xyz); return cell.x + dims.x * (cell.y + dims.y * cell.z); } fn ordinal_to_cell(ordinal: u32) -> vec3 { let size = vec3(lattice.range_max.xyz - lattice.range_min.xyz); return vec3(lattice.range_min.xyz) + vec3( ordinal % size.x, (ordinal / size.x) % size.y, ordinal / max(1u, size.x * size.y), ); } fn cube_vertex(vertex_index: u32) -> vec3 { let face = vertex_index / 6u; let tri = vertex_index % 6u; let uv = array, 6>( vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(-1, 1), vec2(1, 1), vec2(1, -1), )[tri] * 0.5; if (face == 0u) { return vec3(-0.5, uv.y, -uv.x); } if (face == 1u) { return vec3(0.5, uv.y, uv.x); } if (face == 2u) { return vec3(uv.x, -0.5, -uv.y); } if (face == 3u) { return vec3(uv.x, 0.5, uv.y); } if (face == 4u) { return vec3(uv.x, uv.y, -0.5); } return vec3(-uv.x, uv.y, 0.5); } fn internal_face(cell: vec3, face: u32) -> bool { if (any(lattice.cell_scale.xyz < vec3(0.999999))) { return false; } var neighbor = vec3(cell); if (face == 0u) { neighbor.x -= 1; } else if (face == 1u) { neighbor.x += 1; } else if (face == 2u) { neighbor.y -= 1; } else if (face == 3u) { neighbor.y += 1; } else if (face == 4u) { neighbor.z -= 1; } else { neighbor.z += 1; } if ( any(neighbor < vec3(lattice.range_min.xyz)) || any(neighbor >= vec3(lattice.range_max.xyz)) ) { return false; } return cell_visible(cell_to_linear(vec3(neighbor))); } @vertex fn vs_2d(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { let uv = array, 6>( vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(0, 1), vec2(1, 0), vec2(1, 1), )[vertex_index]; let first_edge = lattice.origin.xy + lattice.range_min.xy * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let last_edge = lattice.origin.xy + lattice.range_max.xy * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let local = vec3(mix(first_edge, last_edge, uv), lattice.origin.z); var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(local, 1); out.local_position = local; out.cell = vec3(0); out.cell_index = 0u; out.face = 5u; return out; } @vertex fn vs_3d( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let cell = ordinal_to_cell(instance_index); let index = cell_to_linear(cell); let local = lattice.origin.xyz + vec3(cell) * lattice.spacing.xyz + cube_vertex(vertex_index) * lattice.spacing.xyz * lattice.cell_scale.xyz; var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(local, 1); out.local_position = local; out.cell = cell; out.cell_index = index; out.face = vertex_index / 6u; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { var cell = in.cell; var index = in.cell_index; if (u32(lattice.dimensions.w + 0.5) == 2u) { let relative = (in.local_position.xy - (lattice.origin.xy - 0.5 * lattice.spacing.xy)) / lattice.spacing.xy; cell = vec3(vec2(floor(relative)), 0u); index = cell_to_linear(cell); if ( any(cell.xy < vec2(lattice.range_min.xy)) || any(cell.xy >= vec2(lattice.range_max.xy)) ) { discard; } let center = lattice.origin.xy + vec2(cell.xy) * lattice.spacing.xy; if ( any( abs((in.local_position.xy - center) / lattice.spacing.xy) > 0.5 * lattice.cell_scale.xy, ) ) { discard; } } else if (internal_face(cell, in.face)) { discard; } if (!cell_visible(index)) { discard; } var out: FragmentOutput; out.id = vec2(pick.object_id, pick.element_base + index); out.depth = in.position.z; return out; }"; // typescript/core/picking.ts var alignTo256 = (x) => x + 255 & ~255; var getPickMaxHits = (opts) => { const v = opts.maxHits; if (!Number.isFinite(v)) return 1e4; return Math.max(1, Math.floor(v)); }; var toFramebufferPixel = (clientCoord, clientSize, framebufferSize) => { const size = Math.max(1, framebufferSize | 0), t = clientCoord / Math.max(1, clientSize) * size, p = Math.floor(t); if (p < 0) return 0; if (p >= size) return size - 1; return p; }; var toClientBounds = (minX, minY, maxX, maxY, clientW, clientH) => { const x = clamp(minX, 0, clientW), y = clamp(minY, 0, clientH); const right = clamp(maxX, 0, clientW), bottom = clamp(maxY, 0, clientH); return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) }; }; var resolveSinglePixel = (ctx, x, y, clientW, clientH) => { if (!Number.isFinite(x) || !Number.isFinite(y)) return null; if (x < 0 || y < 0 || x >= clientW || y >= clientH) return null; const px = toFramebufferPixel(x, clientW, ctx.width); const py = toFramebufferPixel(y, clientH, ctx.height); return { px, py }; }; var resolveRectPickQuery = (ctx, x0, y0, x1, y1, maxHits, clientW, clientH) => { const minX = Math.min(x0, x1); const minY = Math.min(y0, y1); const maxX = Math.max(x0, x1); const maxY = Math.max(y0, y1); const bounds = toClientBounds(minX, minY, maxX, maxY, clientW, clientH); const sameX = Math.abs(x0 - x1) <= 1e-6; const sameY = Math.abs(y0 - y1) <= 1e-6; if (sameX && sameY) { const p = resolveSinglePixel(ctx, x0, y0, clientW, clientH); if (!p) return { mode: "rect", bounds, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; return { mode: "rect", bounds, x: p.px, y: p.py, width: 1, height: 1, maxHits, lasso: null }; } if (bounds.width <= 0 || bounds.height <= 0) return { mode: "rect", bounds, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; const maxClientX = Math.max(bounds.x, bounds.x + bounds.width - 1e-6); const maxClientY = Math.max(bounds.y, bounds.y + bounds.height - 1e-6); const px0 = toFramebufferPixel(bounds.x, clientW, ctx.width); const py0 = toFramebufferPixel(bounds.y, clientH, ctx.height); const px1 = toFramebufferPixel(maxClientX, clientW, ctx.width); const py1 = toFramebufferPixel(maxClientY, clientH, ctx.height); return { mode: "rect", bounds, x: Math.min(px0, px1), y: Math.min(py0, py1), width: Math.abs(px1 - px0) + 1, height: Math.abs(py1 - py0) + 1, maxHits, lasso: null }; }; var resolveLassoPickQuery = (ctx, points, maxHits, clientW, clientH) => { if (!Array.isArray(points) || points.length < 3) { return { mode: "lasso", bounds: { x: 0, y: 0, width: 0, height: 0 }, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < points.length; i++) { const p = points[i]; if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue; if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; } if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) { return { mode: "lasso", bounds: { x: 0, y: 0, width: 0, height: 0 }, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; } const bounds = toClientBounds(minX, minY, maxX, maxY, clientW, clientH); if (bounds.width <= 0 || bounds.height <= 0) { return { mode: "lasso", bounds, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; } const lasso = []; let minFx = Infinity; let minFy = Infinity; let maxFx = -Infinity; let maxFy = -Infinity; for (let i = 0; i < points.length; i++) { const p = points[i]; if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue; const fx = p.x / Math.max(1, clientW) * Math.max(1, ctx.width); const fy = p.y / Math.max(1, clientH) * Math.max(1, ctx.height); lasso.push({ x: fx, y: fy }); if (fx < minFx) minFx = fx; if (fy < minFy) minFy = fy; if (fx > maxFx) maxFx = fx; if (fy > maxFy) maxFy = fy; } if (lasso.length < 3 || !Number.isFinite(minFx) || !Number.isFinite(minFy) || !Number.isFinite(maxFx) || !Number.isFinite(maxFy)) { return { mode: "lasso", bounds, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; } const px0 = clamp(Math.floor(minFx), 0, Math.max(0, ctx.width - 1)), py0 = clamp(Math.floor(minFy), 0, Math.max(0, ctx.height - 1)); const px1 = clamp(Math.floor(maxFx), 0, Math.max(0, ctx.width - 1)), py1 = clamp(Math.floor(maxFy), 0, Math.max(0, ctx.height - 1)); if (px1 < px0 || py1 < py0) return { mode: "lasso", bounds, x: 0, y: 0, width: 0, height: 0, maxHits, lasso: null }; return { mode: "lasso", bounds, x: px0, y: py0, width: px1 - px0 + 1, height: py1 - py0 + 1, maxHits, lasso }; }; var preparePickFrame = (ctx, scene, camera) => { ctx.prepareSceneFrameBase(scene, camera, false, true); if (!ctx.pickIdView || !ctx.pickDepthView || !ctx.pickDepthPayloadView) resizePickTargets(ctx); }; var resolveRendererPickHit = (ctx, camera, sample) => { const obj = ctx.objectsById.get(sample.objectId); if (!obj) return null; const worldPosition = ctx.unprojectDepth(camera, sample.px, sample.py, sample.depth); if (obj instanceof Mesh) return { kind: "mesh", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; if (obj instanceof PointCloud) return { kind: "pointcloud", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; if (obj instanceof GlyphField) return { kind: "glyphfield", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; if (obj instanceof NodeLink) return { kind: "nodelink", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; if (obj instanceof SplatField) return { kind: "splatfield", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; if (obj instanceof LatticeSpace) return { kind: "latticespace", object: obj, objectId: sample.objectId, elementIndex: sample.elementIndex, worldPosition }; return null; }; var pointInPolygon = (x, y, polygon) => { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x, yi = polygon[i].y; const xj = polygon[j].x, yj = polygon[j].y; const intersects2 = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi || 1e-12) + xi; if (intersects2) inside = !inside; } return inside; }; var executePickRegion = async (ctx, scene, camera, query) => { if (query.width <= 0 || query.height <= 0) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; preparePickFrame(ctx, scene, camera); if (!ctx.pickIdView || !ctx.pickDepthView || !ctx.pickDepthPayloadView) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; const readback = ensurePickReadbackBuffers(ctx, query.width, query.height); if (!ctx.pickIdTexture || !ctx.pickDepthPayloadTexture || !ctx.pickIdReadbackBuffer || !ctx.pickDepthReadbackBuffer) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; if (ctx.pickIdReadbackBuffer.mapState !== "unmapped") try { ctx.pickIdReadbackBuffer.unmap(); } catch { } if (ctx.pickDepthReadbackBuffer.mapState !== "unmapped") try { ctx.pickDepthReadbackBuffer.unmap(); } catch { } const encoder = ctx.device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.pickIdView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }, { view: ctx.pickDepthPayloadView, clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: ctx.pickDepthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" } }); pass.setScissorRect(query.x, query.y, query.width, query.height); executeMeshPickDrawList(ctx, pass, ctx.opaqueDrawList); executeMeshPickDrawList(ctx, pass, ctx.transparentDrawList); executeGlyphPickDrawList(ctx, pass, ctx.opaqueGlyphFieldDrawList); executeGlyphPickDrawList(ctx, pass, ctx.transparentGlyphFieldDrawList); executePointCloudPickDrawList(ctx, pass, ctx.opaquePointCloudDrawList); executePointCloudPickDrawList(ctx, pass, ctx.transparentPointCloudDrawList); executeSplatFieldPickDrawList(ctx, pass, ctx.transparentSplatFieldDrawList); executeNodeLinkPickDrawList(ctx, pass, ctx.opaqueNodeLinkDrawList); executeNodeLinkPickDrawList(ctx, pass, ctx.transparentNodeLinkDrawList); executeLatticeSpacePickDrawList(ctx, pass, ctx.opaqueLatticeSpaceDrawList); executeLatticeSpacePickDrawList(ctx, pass, ctx.transparentLatticeSpaceDrawList); pass.end(); encoder.copyTextureToBuffer( { texture: ctx.pickIdTexture, origin: { x: query.x, y: query.y, z: 0 } }, { buffer: ctx.pickIdReadbackBuffer, bytesPerRow: readback.idBytesPerRow, rowsPerImage: query.height }, { width: query.width, height: query.height, depthOrArrayLayers: 1 } ); encoder.copyTextureToBuffer( { texture: ctx.pickDepthPayloadTexture, origin: { x: query.x, y: query.y, z: 0 } }, { buffer: ctx.pickDepthReadbackBuffer, bytesPerRow: readback.depthBytesPerRow, rowsPerImage: query.height }, { width: query.width, height: query.height, depthOrArrayLayers: 1 } ); ctx.queue.submit([encoder.finish()]); await Promise.all([ctx.pickIdReadbackBuffer.mapAsync(GPUMapMode.READ, 0, readback.idSizeBytes), ctx.pickDepthReadbackBuffer.mapAsync(GPUMapMode.READ, 0, readback.depthSizeBytes)]); let truncated = false; let sampledPixels = 0; const samples = /* @__PURE__ */ new Map(); try { const idWords = new Uint32Array(ctx.pickIdReadbackBuffer.getMappedRange(0, readback.idSizeBytes)); const depthWords = new Float32Array(ctx.pickDepthReadbackBuffer.getMappedRange(0, readback.depthSizeBytes)); const lasso = query.mode === "lasso" ? query.lasso : null; rows: for (let y = 0; y < query.height; y++) { const idRowBase = y * readback.idBytesPerRow >>> 2; const depthRowBase = y * readback.depthBytesPerRow >>> 2; const py = query.y + y; for (let x = 0; x < query.width; x++) { const px = query.x + x; if (lasso && !pointInPolygon(px + 0.5, py + 0.5, lasso)) continue; sampledPixels++; const idIndex = idRowBase + x * 2; const objectId = idWords[idIndex] >>> 0; if (objectId === 0) continue; const elementIndex = idWords[idIndex + 1] >>> 0; const depth = depthWords[depthRowBase + x]; if (!Number.isFinite(depth) || depth >= 1) continue; const key = `${objectId}:${elementIndex}`; const existing = samples.get(key); if (!existing) { if (samples.size >= query.maxHits) { truncated = true; break rows; } samples.set(key, { objectId, elementIndex, depth, px, py }); } else if (depth < existing.depth) { existing.depth = depth; existing.px = px; existing.py = py; } } } } finally { try { ctx.pickIdReadbackBuffer.unmap(); } catch { } try { ctx.pickDepthReadbackBuffer.unmap(); } catch { } } const hits = []; for (const sample of samples.values()) { const hit = resolveRendererPickHit(ctx, camera, sample); if (hit) hits.push(hit); } return { mode: query.mode, hits, truncated, bounds: query.bounds, sampledPixels }; }; var runPick = async (ctx, scene, camera, x, y) => { frameArena.reset(); ctx.resize(); const clientW = Math.max(1, ctx.canvas.clientWidth || ctx.width); const clientH = Math.max(1, ctx.canvas.clientHeight || ctx.height); const pixel = resolveSinglePixel(ctx, x, y, clientW, clientH); if (!pixel) return null; const query = { mode: "rect", bounds: { x, y, width: 0, height: 0 }, x: pixel.px, y: pixel.py, width: 1, height: 1, maxHits: 1, lasso: null }; const result = await executePickRegion(ctx, scene, camera, query); return result.hits.length > 0 ? result.hits[0] : null; }; var runPickRect = async (ctx, scene, camera, x0, y0, x1, y1, opts) => { frameArena.reset(); ctx.resize(); const clientW = Math.max(1, ctx.canvas.clientWidth || ctx.width); const clientH = Math.max(1, ctx.canvas.clientHeight || ctx.height); const query = resolveRectPickQuery(ctx, x0, y0, x1, y1, getPickMaxHits(opts), clientW, clientH); return executePickRegion(ctx, scene, camera, query); }; var runPickLasso = async (ctx, scene, camera, points, opts) => { frameArena.reset(); ctx.resize(); const clientW = Math.max(1, ctx.canvas.clientWidth || ctx.width); const clientH = Math.max(1, ctx.canvas.clientHeight || ctx.height); const query = resolveLassoPickQuery(ctx, points, getPickMaxHits(opts), clientW, clientH); return executePickRegion(ctx, scene, camera, query); }; var getPickBindGroupLayout = (ctx) => { if (ctx.pickBindGroupLayout) return ctx.pickBindGroupLayout; ctx.pickBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 16 } }] }); return ctx.pickBindGroupLayout; }; var resizePickTargets = (ctx) => { const w = ctx.width | 0; const h = ctx.height | 0; if (w <= 0 || h <= 0) return; ctx.pickIdTexture?.destroy(); ctx.pickDepthTexture?.destroy(); ctx.pickDepthPayloadTexture?.destroy(); ctx.pickIdTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: "rg32uint", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC }); ctx.pickIdView = ctx.pickIdTexture.createView(); ctx.pickDepthTexture = createDepthTexture(ctx.device, w, h); ctx.pickDepthView = ctx.pickDepthTexture.createView(); ctx.pickDepthPayloadTexture = ctx.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: "r32float", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC }); ctx.pickDepthPayloadView = ctx.pickDepthPayloadTexture.createView(); ensurePickReadbackBuffers(ctx, 1, 1); }; var ensurePickReadbackBuffers = (ctx, copyWidth, copyHeight) => { const width = Math.max(1, copyWidth | 0); const height = Math.max(1, copyHeight | 0); const idBytesPerRow = alignTo256(width * 8); const depthBytesPerRow = alignTo256(width * 4); const idSizeBytes = idBytesPerRow * height; const depthSizeBytes = depthBytesPerRow * height; if (!ctx.pickIdReadbackBuffer || ctx.pickIdReadbackCapacityBytes < idSizeBytes) { ctx.pickIdReadbackBuffer?.destroy(); ctx.pickIdReadbackBuffer = ctx.device.createBuffer({ size: idSizeBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); ctx.pickIdReadbackCapacityBytes = idSizeBytes; } if (!ctx.pickDepthReadbackBuffer || ctx.pickDepthReadbackCapacityBytes < depthSizeBytes) { ctx.pickDepthReadbackBuffer?.destroy(); ctx.pickDepthReadbackBuffer = ctx.device.createBuffer({ size: depthSizeBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); ctx.pickDepthReadbackCapacityBytes = depthSizeBytes; } return { idBytesPerRow, depthBytesPerRow, idSizeBytes, depthSizeBytes }; }; var writePickUniform = (ctx, slot, objectId, elementBase = 0) => { if (slot >= ctx.pickUniformBuffers.length) ensurePickUniformPool(ctx, slot + 1); const data = new Uint32Array([objectId >>> 0, elementBase >>> 0, 0, 0]); ctx.queue.writeBuffer(ctx.pickUniformBuffers[slot], 0, data.buffer, data.byteOffset, data.byteLength); }; var executeMeshPickDrawList = (ctx, pass, items) => { const bytes = driver.bytes(); let lastPipeline = null; let lastGeometry = null; let lastVertexSourceId = -1; let lastSkinned = false; let lastSkinned8 = false; for (let i = 0; i < items.length; i++) { const item = items[i]; const mesh = item.mesh; const geometry = item.geometry; if (!mesh.visible) continue; const pipeline = getOrCreatePickMeshPipeline(ctx, item.material, item.skinned, item.skinned8, item.mirrored); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; } const vertexSourceChanged = geometry !== lastGeometry || item.vertexSourceId !== lastVertexSourceId || item.skinned !== lastSkinned || item.skinned8 !== lastSkinned8; if (vertexSourceChanged) { geometry.upload(ctx.device); const buffers = getMeshVertexBuffers(mesh, ctx.device, ctx.queue); pass.setVertexBuffer(0, buffers.positionBuffer); if (item.skinned) { pass.setVertexBuffer(3, geometry.jointsBuffer); pass.setVertexBuffer(4, geometry.weightsBuffer); if (item.skinned8) { pass.setVertexBuffer(5, geometry.joints1Buffer); pass.setVertexBuffer(6, geometry.weights1Buffer); } } if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; lastVertexSourceId = item.vertexSourceId; lastSkinned = item.skinned; lastSkinned8 = item.skinned8; } else if (hasMeshMorphRuntime(mesh)) getMeshVertexBuffers(mesh, ctx.device, ctx.queue); const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); writePickUniform(ctx, slot, getObjectId(ctx, mesh), 0); bindModelUniform(ctx, pass, mesh.transform.worldMatrixPtr); pass.setBindGroup(1, ctx.pickBindGroups[slot]); if (item.skinned) { const skin = mesh.skin; if (skin) { skin.ensureGpuResources(ctx.device, ctx.skinBindGroupLayout); const jointCount = skin.jointCount | 0; const jointMatPtr = frameArena.allocF32(jointCount * 16); animf.computeJointMatricesTo( jointMatPtr, skin.skin.jointIndicesPtr, jointCount, skin.skin.invBindPtr, TransformStore.global().worldPtr, skin.meshWorldMatrixPtr ); ctx.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); pass.setBindGroup(2, skin.bindGroup); } } if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount); else pass.draw(geometry.vertexCount); } }; var executePointCloudPickDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastCloud = null; for (let i = 0; i < items.length; i++) { const cloud = items[i].cloud; if (!cloud.visible || cloud.pointCount <= 0) continue; ensurePointCloudBindGroup(ctx, cloud); if (!cloud.bindGroup) continue; const pipeline = getOrCreatePickPointCloudPipeline(ctx); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastCloud = null; } const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); writePickUniform(ctx, slot, getObjectId(ctx, cloud), 0); bindModelUniform(ctx, pass, cloud.transform.worldMatrixPtr); if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; } pass.setBindGroup(2, ctx.pickBindGroups[slot]); pass.draw(6, cloud.pointCount); } }; var executeGlyphPickDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastGeometry = null; let lastField = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const field = item.field; const geometry = item.geometry; if (!field.visible || field.instanceCount <= 0) continue; ensureGlyphFieldBindGroup(ctx, field); if (!field.bindGroup) continue; const pipeline = getOrCreatePickGlyphFieldPipeline(ctx, field); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastField = null; } if (geometry !== lastGeometry) { geometry.upload(ctx.device); pass.setVertexBuffer(0, geometry.positionBuffer); if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; } const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); writePickUniform(ctx, slot, getObjectId(ctx, field), 0); bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } pass.setBindGroup(2, ctx.pickBindGroups[slot]); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, field.instanceCount); else pass.draw(geometry.vertexCount, field.instanceCount); } }; var executeNodeLinkPickDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastGeometry = null; let lastLink = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const link = item.link; ensureNodeLinkBindGroup(ctx, link); if (!link.bindGroup) continue; const pipeline = getOrCreatePickNodeLinkPipeline(ctx, item.passKind, link); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastLink = null; } if (item.geometry && item.geometry !== lastGeometry) { item.geometry.upload(ctx.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); if (item.geometry.isIndexed) pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); lastGeometry = item.geometry; } const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); const elementBase = item.passKind === "edge-lines" || item.passKind === "edge-cylinders" ? link.nodeCount : 0; writePickUniform(ctx, slot, getObjectId(ctx, link), elementBase); bindModelUniform(ctx, pass, link.transform.worldMatrixPtr); if (link !== lastLink) { pass.setBindGroup(1, link.bindGroup); lastLink = link; } pass.setBindGroup(2, ctx.pickBindGroups[slot]); if (item.passKind === "node-points") { pass.draw(6, link.nodeCount); } else if (item.passKind === "edge-lines") { pass.draw(2, link.edgeCount); } else if (item.passKind === "node-solid") { if (!item.geometry) continue; if (item.geometry.isIndexed) pass.drawIndexed(item.geometry.indexCount, link.nodeCount); else pass.draw(item.geometry.vertexCount, link.nodeCount); } else { if (!item.geometry) continue; if (item.geometry.isIndexed) pass.drawIndexed(item.geometry.indexCount, link.edgeCount); else pass.draw(item.geometry.vertexCount, link.edgeCount); } } }; var executeSplatFieldPickDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastField = null; for (let i = 0; i < items.length; i++) { const field = items[i].field; if (!field.visible || field.splatCount <= 0) continue; ensureSplatFieldBindGroup(ctx, field); if (!field.bindGroup) continue; const pipeline = getOrCreatePickSplatFieldPipeline(ctx); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastField = null; } const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); writePickUniform(ctx, slot, getObjectId(ctx, field), 0); bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } pass.setBindGroup(2, ctx.pickBindGroups[slot]); pass.draw(6, field.splatCount); } }; var executeLatticeSpacePickDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastSpace = null; for (const item of items) { const space = item.space; if (!space.visible || space.drawCellCount <= 0) continue; ensureLatticeSpaceBindGroup(ctx, space); if (!space.bindGroup) continue; const pipeline = getOrCreatePickLatticeSpacePipeline(ctx, space); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastSpace = null; } const slot = ctx.pickUniformIndex++; ensurePickUniformPool(ctx, slot + 1); writePickUniform(ctx, slot, getObjectId(ctx, space), 0); bindModelUniform(ctx, pass, space.transform.worldMatrixPtr); if (space !== lastSpace) { pass.setBindGroup(1, space.bindGroup); lastSpace = space; } pass.setBindGroup(2, ctx.pickBindGroups[slot]); if (space.dimensionCount === 2) pass.draw(6); else pass.draw(36, space.drawCellCount); } }; var getOrCreatePickMeshPipeline = (ctx, material, skinned, skinned8, mirrored = false) => { if (skinned8 && !skinned) skinned = true; const cullMode = getCullMode(ctx, material.cullMode); const key = `pick:mesh:${cullMode}:${mirrored ? "cw" : "ccw"}:${skinned8 ? "skin8" : skinned ? "skin4" : "noskin"}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; const shaderCode = skinned8 ? picking_mesh_skinned8_default : skinned ? picking_mesh_skinned_default : picking_mesh_default; let shaderModule = ctx.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: shaderCode }); ctx.shaderCache.set(shaderCode, shaderModule); } const bindGroupLayouts = [ctx.globalBindGroupLayout, getPickBindGroupLayout(ctx)]; if (skinned) bindGroupLayouts.push(ctx.skinBindGroupLayout); const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts }); let buffers; if (skinned8) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 4, offset: 0, format: "float32x4" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 5, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 6, offset: 0, format: "float32x4" }] } ]; } else if (skinned) { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }, { arrayStride: 8, attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }] }, { arrayStride: 16, attributes: [{ shaderLocation: 4, offset: 0, format: "float32x4" }] } ]; } else { buffers = [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] } ]; } const pipeline = ctx.device.createRenderPipeline({ layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode, frontFace: mirrored ? "cw" : "ccw" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreatePickPointCloudPipeline = (ctx) => { const key = "pick:pointcloud"; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(picking_pointcloud_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: picking_pointcloud_default }); ctx.shaderCache.set(picking_pointcloud_default, shaderModule); } const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getPointCloudBindGroupLayout(ctx), getPickBindGroupLayout(ctx)] }); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreatePickGlyphFieldPipeline = (ctx, field) => { const cullMode = getCullMode(ctx, field.cullMode); const key = `pick:glyphfield:${cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(picking_glyphfield_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: picking_glyphfield_default }); ctx.shaderCache.set(picking_glyphfield_default, shaderModule); } const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getGlyphFieldBindGroupLayout(ctx), getPickBindGroupLayout(ctx)] }); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] } ] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreatePickNodeLinkPipeline = (ctx, passKind, link) => { const cullMode = passKind === "node-solid" || passKind === "edge-cylinders" ? getCullMode(ctx, link.cullMode) : "none"; const key = `pick:nodelink:${passKind}:${cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(picking_nodelink_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: picking_nodelink_default }); ctx.shaderCache.set(picking_nodelink_default, shaderModule); } const layout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getNodeLinkBindGroupLayout(ctx), getPickBindGroupLayout(ctx)] }); let entryPoint = "vs_pick_node_points"; let buffers = []; let topology = "triangle-list"; if (passKind === "node-solid") { entryPoint = "vs_pick_node_solid"; buffers = [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }]; topology = "triangle-list"; } else if (passKind === "edge-lines") { entryPoint = "vs_pick_edge_lines"; buffers = []; topology = "line-list"; } else if (passKind === "edge-cylinders") { entryPoint = "vs_pick_edge_cylinders"; buffers = [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }]; topology = "triangle-list"; } const pipeline = ctx.device.createRenderPipeline({ label: key, layout, vertex: { module: shaderModule, entryPoint, buffers }, fragment: { module: shaderModule, entryPoint: "fs_pick", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology, cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreatePickSplatFieldPipeline = (ctx) => { const key = "pick:splatfield"; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(picking_splatfield_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: picking_splatfield_default }); ctx.shaderCache.set(picking_splatfield_default, shaderModule); } const pipelineLayout = ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getSplatFieldBindGroupLayout(ctx), getPickBindGroupLayout(ctx)] }); const pipeline = ctx.device.createRenderPipeline({ label: key, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreatePickLatticeSpacePipeline = (ctx, space) => { const cullMode = space.dimensionCount === 2 ? "none" : getCullMode(ctx, space.cullMode); const key = `pick:latticespace:${space.dimensionCount}:${cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let module = ctx.shaderCache.get(picking_latticespace_default); if (!module) { module = ctx.device.createShaderModule({ code: picking_latticespace_default }); ctx.shaderCache.set(picking_latticespace_default, module); } const pipeline = ctx.device.createRenderPipeline({ label: key, layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getLatticeSpaceBindGroupLayout(ctx), getPickBindGroupLayout(ctx)] }), vertex: { module, entryPoint: space.dimensionCount === 2 ? "vs_2d" : "vs_3d", buffers: [] }, fragment: { module, entryPoint: "fs_main", targets: [{ format: "rg32uint" }, { format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; // wgsl/core/occlusion-mesh.wgsl var occlusion_mesh_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct VertexOutput { @builtin(position) position: vec4, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @vertex fn vs_main(@location(0) position: vec3) -> VertexOutput { var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(position, 1.0); return out; } @fragment fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) f32 { return frag_coord.z; }"; // wgsl/core/occlusion-reduce.wgsl var occlusion_reduce_default = "struct VertexOutput { @builtin(position) pos: vec4, } @group(0) @binding(0) var src_tex: texture_2d; @vertex fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { var positions = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0), ); var out: VertexOutput; out.pos = vec4(positions[idx], 0.0, 1.0); return out; } @fragment fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) f32 { let src_size = textureDimensions(src_tex); let dst_coord = vec2(i32(frag_coord.x), i32(frag_coord.y)); let base = dst_coord * 2; let x1 = min(base.x + 1, i32(src_size.x) - 1); let y1 = min(base.y + 1, i32(src_size.y) - 1); let d00 = textureLoad(src_tex, base, 0).x; let d10 = textureLoad(src_tex, vec2(x1, base.y), 0).x; let d01 = textureLoad(src_tex, vec2(base.x, y1), 0).x; let d11 = textureLoad(src_tex, vec2(x1, y1), 0).x; return max(max(d00, d10), max(d01, d11)); }"; // wgsl/world/occlusion-pointcloud.wgsl var occlusion_pointcloud_default = "struct PointData { position: vec3, scalar: f32, } struct PointCloudUniforms { size_params: vec4, scalar_params: vec4, options: vec4, colors: array, 8>, } struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct VertexOutput { @builtin(position) position: vec4, @location(0) point_coord: vec2, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var points: array; @group(1) @binding(1) var pc: PointCloudUniforms; @vertex fn vs_main( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let p = points[instance_index]; let world_pos = model.model * vec4(p.position, 1.0); let clip = camera.view_proj * world_pos; let dist = distance(camera.position, world_pos.xyz); let base_size = pc.size_params.x; let min_size = pc.size_params.y; let max_size = pc.size_params.z; let atten = pc.size_params.w; var size_px = base_size; if (atten > 0.0) { size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); var uv = vec2(0.0); if (vertex_index == 0u) { uv = vec2(0.0, 0.0); } else if (vertex_index == 1u) { uv = vec2(1.0, 0.0); } else if (vertex_index == 2u) { uv = vec2(0.0, 1.0); } else if (vertex_index == 3u) { uv = vec2(1.0, 0.0); } else if (vertex_index == 4u) { uv = vec2(1.0, 1.0); } else if (vertex_index == 5u) { uv = vec2(0.0, 1.0); } let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; var out: VertexOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.point_coord = uv; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) f32 { let uv = in.point_coord * 2.0 - vec2(1.0, 1.0); let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } return in.position.z; }"; // wgsl/world/occlusion-glyphfield.wgsl var occlusion_glyphfield_default = "struct GlyphFieldUniforms { scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, visual: vec4, solid_color: vec4, colors: array, 8>, } struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct VertexInput { @location(0) position: vec3, } struct VertexOutput { @builtin(position) position: vec4, @location(0) @interpolate(flat) attrib: vec4, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var positions: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; @group(1) @binding(3) var attributes: array>; @group(1) @binding(4) var glyph: GlyphFieldUniforms; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn vec4_component(v: vec4, idx: u32) -> f32 { if (idx == 0u) { return v.x; } if (idx == 1u) { return v.y; } if (idx == 2u) { return v.z; } return v.w; } fn shifted_value_vector(v: vec4, offset_floats: f32) -> vec4 { let o = min(3u, u32(offset_floats + 0.5)); let i0 = min(3u, o + 0u); let i1 = min(3u, o + 1u); let i2 = min(3u, o + 2u); let i3 = min(3u, o + 3u); return vec4( vec4_component(v, i0), vec4_component(v, i1), vec4_component(v, i2), vec4_component(v, i3), ); } fn rotate_by_quat(v: vec3, q: vec4) -> vec3 { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } @vertex fn vs_main(in: VertexInput, @builtin(instance_index) instance_index: u32) -> VertexOutput { let p4 = positions[instance_index]; let q = rotations[instance_index]; let s4 = scales[instance_index]; let local_pos = rotate_by_quat(in.position * s4.xyz, q) + p4.xyz; let world_pos = model.model * vec4(local_pos, 1.0); var out: VertexOutput; out.position = camera.view_proj * world_pos; out.attrib = attributes[instance_index]; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) f32 { let color_mode = u32(round(glyph.visual.z)); if (color_mode == 1u) { let shifted = shifted_value_vector(in.attrib, glyph.scale_domain.z); let component_count = u32(glyph.scale_source.x + 0.5); let component_index = u32(glyph.scale_source.y + 0.5); let value_mode = u32(glyph.scale_source.z + 0.5); let raw_value = scale_select_value(shifted, component_count, component_index, value_mode); if (!scale_is_finite(raw_value)) { discard; } } return in.position.z; }"; // wgsl/world/occlusion-nodelink.wgsl var occlusion_nodelink_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, _pad0: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct NodeLinkUniforms { global: vec4, node_scale_source: vec4, node_scale_domain: vec4, node_scale_clamp: vec4, node_scale_params: vec4, node_scale_flags: vec4, node_visual: vec4, edge_scale_source: vec4, edge_scale_domain: vec4, edge_scale_clamp: vec4, edge_scale_params: vec4, edge_scale_flags: vec4, edge_visual: vec4, node_solid: vec4, edge_solid: vec4, point_params: vec4, node_stops: array, 8>, edge_stops: array, 8>, } struct OcclusionOutput { @builtin(position) position: vec4, @location(0) point_coord: vec2, @location(1) @interpolate(flat) is_point: f32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var node_positions: array>; @group(1) @binding(3) var node_radii: array>; @group(1) @binding(4) var edges: array>; @group(1) @binding(7) var nl: NodeLinkUniforms; fn build_edge_frame(src: vec3, dst: vec3) -> mat3x3 { let y_axis = normalize(dst - src); var fallback_axis = vec3(0.0, 0.0, 1.0); if (abs(dot(fallback_axis, y_axis)) > 0.99) { fallback_axis = vec3(1.0, 0.0, 0.0); } let x_axis = normalize(cross(fallback_axis, y_axis)); let z_axis = normalize(cross(y_axis, x_axis)); return mat3x3(x_axis, y_axis, z_axis); } @vertex fn vs_node_points( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> OcclusionOutput { let p = node_positions[instance_index].xyz; let world_pos4 = model.model * vec4(p, 1.0); let clip = camera.view_proj * world_pos4; let base_size = nl.global.x; let min_size = nl.point_params.x; let max_size = nl.point_params.y; let atten = nl.point_params.z; var size_px = base_size; if (atten > 0.0) { let dist = distance(camera.position, world_pos4.xyz); size_px = base_size * (atten / max(dist, 1e-6)); } size_px = clamp(size_px, min_size, max_size); let uv = vec2(f32((vertex_index + 2u) / 3u % 2u), f32((vertex_index + 1u) / 3u % 2u)); let row0 = vec3(camera.view_proj[0][0], camera.view_proj[1][0], camera.view_proj[2][0]); let row1 = vec3(camera.view_proj[0][1], camera.view_proj[1][1], camera.view_proj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndc_size = (size_px * 2.0) / max(camera._pad0, 1.0); let offset_x = (uv.x - 0.5) * ndc_size / aspect * clip.w; let offset_y = -(uv.y - 0.5) * ndc_size * clip.w; var out: OcclusionOutput; out.position = clip + vec4(offset_x, offset_y, 0.0, 0.0); out.point_coord = uv * 2.0 - vec2(1.0, 1.0); out.is_point = 1.0; return out; } @vertex fn vs_node_solid( @location(0) position: vec3, @builtin(instance_index) instance_index: u32, ) -> OcclusionOutput { let center = node_positions[instance_index].xyz; let mode = u32(round(nl.node_visual.z)); let use_radii = nl.node_visual.w > 0.5; var scale_vec = vec3(max(nl.global.x, 1e-6)); if (use_radii) { let rv = max(node_radii[instance_index].xyz, vec3(1e-6)); if (mode == 2u) { scale_vec = rv * max(nl.global.x, 1e-6); } else { scale_vec = vec3(rv.x * max(nl.global.x, 1e-6)); } } let obj_pos = center + (position * scale_vec); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: OcclusionOutput; out.position = camera.view_proj * world_pos4; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @vertex fn vs_edge_lines( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> OcclusionOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let obj_pos = select(src, dst, (vertex_index & 1u) == 1u); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: OcclusionOutput; out.position = camera.view_proj * world_pos4; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @vertex fn vs_edge_cylinders( @location(0) position: vec3, @builtin(instance_index) instance_index: u32, ) -> OcclusionOutput { let edge = edges[instance_index]; let src = node_positions[edge.x].xyz; let dst = node_positions[edge.y].xyz; let seg = dst - src; let seg_len = max(length(seg), 1e-6); let basis = build_edge_frame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3(position.x * radius, position.y * seg_len, position.z * radius); let obj_pos = ((src + dst) * 0.5) + (basis * local); let world_pos4 = model.model * vec4(obj_pos, 1.0); var out: OcclusionOutput; out.position = camera.view_proj * world_pos4; out.point_coord = vec2(0.0, 0.0); out.is_point = 0.0; return out; } @fragment fn fs_main(in: OcclusionOutput) -> @location(0) f32 { if (in.is_point > 0.5) { let r2 = dot(in.point_coord, in.point_coord); if (r2 > 1.0) { discard; } } return in.position.z; }"; // wgsl/world/occlusion-latticespace.wgsl var occlusion_latticespace_default = "struct CameraUniforms { view_proj: mat4x4, position: vec3, viewport_height: f32, } struct ModelUniforms { model: mat4x4, normal: mat4x4, } struct LatticeUniforms { dimensions: vec4, origin: vec4, spacing: vec4, cell_scale: vec4, range_min: vec4, range_max: vec4, data_config: vec4, visual: vec4, filters: vec4, solid_color: vec4, scale_source: vec4, scale_domain: vec4, scale_clamp: vec4, scale_params: vec4, scale_flags: vec4, colors: array, 8>, } struct VertexOutput { @builtin(position) position: vec4, @location(0) local_position: vec3, @location(1) @interpolate(flat) cell: vec3, @location(2) @interpolate(flat) cell_index: u32, @location(3) @interpolate(flat) face: u32, } @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var cell_data: array; @group(1) @binding(1) var cell_mask: array; @group(1) @binding(2) var sorted_indices: array; @group(1) @binding(3) var lattice: LatticeUniforms; fn finite_value(value: f32) -> bool { return (bitcast(value) & 0x7f800000u) != 0x7f800000u; } fn component(value: vec4, index: u32) -> f32 { if (index == 0u) { return value.x; } if (index == 1u) { return value.y; } if (index == 2u) { return value.z; } return value.w; } fn cell_to_linear(cell: vec3) -> u32 { let dims = vec3(lattice.dimensions.xyz); return cell.x + dims.x * (cell.y + dims.y * cell.z); } fn select_scalar(value: vec4) -> f32 { let count = max(1u, min(4u, u32(lattice.scale_source.x + 0.5))); if (u32(lattice.scale_source.z + 0.5) == 1u) { if (count == 1u) { return abs(value.x); } if (count == 2u) { return length(value.xy); } if (count == 3u) { return length(value.xyz); } return length(value); } return component(value, min(3u, u32(lattice.scale_source.y + 0.5))); } fn cell_visible(index: u32) -> bool { if (lattice.data_config.w > 0.5 && cell_mask[index] == 0u) { return false; } let mode = u32(lattice.data_config.y + 0.5); if (mode == 2u) { return true; } let count = u32(lattice.data_config.x + 0.5); let base = index * count; var value = vec4(0); if (count > 0u) { value.x = cell_data[base]; } if (count > 1u) { value.y = cell_data[base + 1u]; } if (count > 2u) { value.z = cell_data[base + 2u]; } if (count > 3u) { value.w = cell_data[base + 3u]; } if (mode == 1u) { return finite_value(value.x) && finite_value(value.y) && finite_value(value.z) && finite_value(value.w); } let scalar = select_scalar(value); if (!finite_value(scalar)) { return false; } return lattice.filters.x < 0.5 || (scalar >= lattice.visual.z && scalar <= lattice.visual.w); } fn ordinal_to_cell(ordinal: u32) -> vec3 { let size = vec3(lattice.range_max.xyz - lattice.range_min.xyz); return vec3(lattice.range_min.xyz) + vec3( ordinal % size.x, (ordinal / size.x) % size.y, ordinal / max(1u, size.x * size.y), ); } fn cube_vertex(vertex_index: u32) -> vec3 { let face = vertex_index / 6u; let uv = array, 6>( vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(-1, 1), vec2(1, 1), vec2(1, -1), )[vertex_index % 6u] * 0.5; if (face == 0u) { return vec3(-0.5, uv.y, -uv.x); } if (face == 1u) { return vec3(0.5, uv.y, uv.x); } if (face == 2u) { return vec3(uv.x, -0.5, -uv.y); } if (face == 3u) { return vec3(uv.x, 0.5, uv.y); } if (face == 4u) { return vec3(uv.x, uv.y, -0.5); } return vec3(-uv.x, uv.y, 0.5); } fn internal_face(cell: vec3, face: u32) -> bool { if (any(lattice.cell_scale.xyz < vec3(0.999999))) { return false; } let dims = vec3(lattice.dimensions.xyz); var neighbor = vec3(cell); if (face == 0u) { neighbor.x -= 1; } else if (face == 1u) { neighbor.x += 1; } else if (face == 2u) { neighbor.y -= 1; } else if (face == 3u) { neighbor.y += 1; } else if (face == 4u) { neighbor.z -= 1; } else { neighbor.z += 1; } if (any(neighbor < vec3(0)) || any(neighbor >= vec3(dims))) { return false; } if ( any(neighbor < vec3(lattice.range_min.xyz)) || any(neighbor >= vec3(lattice.range_max.xyz)) ) { return false; } return cell_visible(cell_to_linear(vec3(neighbor))); } @vertex fn vs_2d(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { let uv = array, 6>( vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(0, 1), vec2(1, 0), vec2(1, 1), )[vertex_index]; let first = lattice.origin.xy + lattice.range_min.xy * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let last = lattice.origin.xy + lattice.range_max.xy * lattice.spacing.xy - 0.5 * lattice.spacing.xy; let local = vec3(mix(first, last, uv), lattice.origin.z); var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(local, 1); out.local_position = local; out.cell = vec3(0); out.cell_index = 0u; out.face = 5u; return out; } @vertex fn vs_3d( @builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32, ) -> VertexOutput { let cell = ordinal_to_cell(instance_index); let local = lattice.origin.xyz + vec3(cell) * lattice.spacing.xyz + cube_vertex(vertex_index) * lattice.spacing.xyz * lattice.cell_scale.xyz; var out: VertexOutput; out.position = camera.view_proj * model.model * vec4(local, 1); out.local_position = local; out.cell = cell; out.cell_index = cell_to_linear(cell); out.face = vertex_index / 6u; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) f32 { var cell = in.cell; var index = in.cell_index; if (u32(lattice.dimensions.w + 0.5) == 2u) { let relative = (in.local_position.xy - (lattice.origin.xy - 0.5 * lattice.spacing.xy)) / lattice.spacing.xy; cell = vec3(vec2(floor(relative)), 0u); if ( any(cell.xy < vec2(lattice.range_min.xy)) || any(cell.xy >= vec2(lattice.range_max.xy)) ) { discard; } index = cell_to_linear(cell); let center = lattice.origin.xy + vec2(cell.xy) * lattice.spacing.xy; if ( any( abs((in.local_position.xy - center) / lattice.spacing.xy) > 0.5 * lattice.cell_scale.xy, ) ) { discard; } } else if (internal_face(cell, in.face)) { discard; } if (!cell_visible(index)) { discard; } return in.position.z; }"; // typescript/core/occlusion.ts var occlusionHashScratch = new ArrayBuffer(4); var occlusionHashF32 = new Float32Array(occlusionHashScratch); var occlusionHashU32 = new Uint32Array(occlusionHashScratch); var mixOcclusionHash = (hash, value) => Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; var blendModeHash = (mode) => mode === "opaque" /* Opaque */ ? 1 : mode === "transparent" /* Transparent */ ? 2 : 3; var cullModeHash = (mode) => mode === "back" /* Back */ ? 1 : mode === "front" /* Front */ ? 2 : 3; var mixOcclusionHashF32 = (hash, value) => { occlusionHashF32[0] = Number.isFinite(value) ? value : 0; return mixOcclusionHash(hash, occlusionHashU32[0] >>> 0); }; var hashWorldMatrix = (ptr) => { const m = wasm.f32view(ptr, 16); let hash = 2166136261 >>> 0; for (let i = 0; i < 16; i++) hash = mixOcclusionHashF32(hash, m[i]); return hash >>> 0; }; var createOcclusionHierarchyLayout = (_ctx, width, height) => { const widths = [], heights = []; let w = Math.max(1, width | 0), h = Math.max(1, height | 0); while (true) { widths.push(w); heights.push(h); if (w === 1 && h === 1) break; w = Math.max(1, Math.floor(w / 2)); h = Math.max(1, Math.floor(h / 2)); } const mipCount = widths.length; const offsets = new Uint32Array(mipCount); const copyOffsets = new Uint32Array(mipCount); const rowBytes = new Uint32Array(mipCount); let texelOffset = 0; let byteOffset = 0; for (let i = 0; i < mipCount; i++) { offsets[i] = texelOffset >>> 0; copyOffsets[i] = byteOffset >>> 0; rowBytes[i] = alignTo(widths[i] * 4, 256) >>> 0; texelOffset += widths[i] * heights[i]; byteOffset += rowBytes[i] * heights[i]; } return { widths: Uint32Array.from(widths), heights: Uint32Array.from(heights), offsets, copyOffsets, rowBytes, mipCount, texelCount: texelOffset >>> 0, totalBytes: byteOffset >>> 0 }; }; var destroyOcclusionTextures = (ctx) => { ctx.occlusionHierarchyTexture?.destroy(); ctx.occlusionDepthTexture?.destroy(); ctx.occlusionHierarchyTexture = null; ctx.occlusionHierarchyMipViews = []; ctx.occlusionDepthTexture = null; ctx.occlusionDepthView = null; ctx.occlusionHierarchyLayout = null; ctx.occlusionWidth = 0; ctx.occlusionHeight = 0; ctx.occlusionReduceBindGroups.clear(); }; var invalidateOcclusionResources = (ctx) => { destroyOcclusionTextures(ctx); ctx.occlusionResourceGeneration++; ctx.latestOcclusionHierarchy = null; ctx.latestOcclusionHierarchySerial = 0; ctx.pendingOcclusionFrameState = null; if (ctx.occlusionHierarchyWasmPtr) wasm.freeF32(ctx.occlusionHierarchyWasmPtr, ctx.occlusionHierarchyWasmLength); ctx.occlusionHierarchyWasmPtr = 0; ctx.occlusionHierarchyWasmLength = 0; ctx.occlusionHierarchyWasmSerial = 0; for (const slot of ctx.occlusionReadbackSlots) { slot.metadata = null; slot.data = null; if (slot.state === "ready") slot.state = "idle"; } }; var ensureOcclusionResources = (ctx) => { if (!ctx.occlusionCullingEnabled) return; let targetW = ctx.width; let targetH = ctx.height; if (targetW >= targetH) { const scale = Math.min(1, ctx.OCCLUSION_MAX_LONG_EDGE / Math.max(1, targetW)); targetW = Math.max(1, Math.floor(targetW * scale)); targetH = Math.max(1, Math.floor(targetH * scale)); } else { const scale = Math.min(1, ctx.OCCLUSION_MAX_LONG_EDGE / Math.max(1, targetH)); targetW = Math.max(1, Math.floor(targetW * scale)); targetH = Math.max(1, Math.floor(targetH * scale)); } const layout = createOcclusionHierarchyLayout(ctx, targetW, targetH); if (ctx.occlusionHierarchyTexture && ctx.occlusionDepthTexture && ctx.occlusionWidth === targetW && ctx.occlusionHeight === targetH && ctx.occlusionHierarchyLayout?.mipCount === layout.mipCount) return; destroyOcclusionTextures(ctx); ctx.occlusionWidth = targetW; ctx.occlusionHeight = targetH; ctx.occlusionHierarchyLayout = layout; ctx.occlusionHierarchyTexture = ctx.device.createTexture({ size: { width: targetW, height: targetH, depthOrArrayLayers: 1 }, format: "r32float", mipLevelCount: layout.mipCount, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); ctx.occlusionHierarchyMipViews = []; for (let i = 0; i < layout.mipCount; i++) { ctx.occlusionHierarchyMipViews.push(ctx.occlusionHierarchyTexture.createView({ dimension: "2d", baseMipLevel: i, mipLevelCount: 1 })); } ctx.occlusionDepthTexture = createDepthTexture(ctx.device, targetW, targetH); ctx.occlusionDepthView = ctx.occlusionDepthTexture.createView(); while (ctx.occlusionReadbackSlots.length < ctx.OCCLUSION_READBACK_RING_SIZE) { ctx.occlusionReadbackSlots.push({ buffer: null, capacityBytes: 0, pending: null, state: "idle", metadata: null, data: null, serial: 0 }); } }; var ensureOcclusionReadbackBuffer = (ctx, slot, bytes) => { if (slot.buffer && slot.capacityBytes >= bytes) return; slot.buffer?.destroy(); slot.buffer = ctx.device.createBuffer({ size: Math.max(256, alignTo(bytes, 256)), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); slot.capacityBytes = Math.max(256, alignTo(bytes, 256)); }; var getIdleOcclusionReadbackSlot = (ctx) => { for (const slot of ctx.occlusionReadbackSlots) if (!slot.pending && slot.state !== "mapping") return slot; return null; }; var buildOcclusionFrameState = (ctx) => { let signature = 2166136261 >>> 0; const candidates = ctx.occlusionCandidateScratch; candidates.length = 0; const meshOccluders = []; const pointCloudOccluders = []; const glyphOccluders = []; const nodeLinkOccluders = []; const latticeSpaceOccluders = []; const candidateSeen = ctx.occlusionVisibleObjectIds; candidateSeen.clear(); for (const item of ctx.opaqueDrawList) { if (isSafeMeshOccluder(ctx, item)) { meshOccluders.push(item); signature = mixOcclusionHash(signature, 1); signature = mixOcclusionHash(signature, getObjectId(ctx, item.mesh)); signature = mixOcclusionHash(signature, getMeshOccluderToken(ctx, item)); } if (!candidateSeen.has(getObjectId(ctx, item.mesh)) && tryPushMeshOcclusionCandidate(ctx, item, candidates)) candidateSeen.add(getObjectId(ctx, item.mesh)); } for (const item of ctx.opaquePointCloudDrawList) { if (isSafePointCloudOccluder(item)) { pointCloudOccluders.push(item); signature = mixOcclusionHash(signature, 2); signature = mixOcclusionHash(signature, getObjectId(ctx, item.cloud)); signature = mixOcclusionHash(signature, item.cloud.occluderRevision >>> 0); signature = mixOcclusionHash(signature, hashWorldMatrix(item.cloud.transform.worldMatrixPtr)); } if (!candidateSeen.has(getObjectId(ctx, item.cloud)) && tryPushPointCloudOcclusionCandidate(ctx, item.cloud, candidates)) candidateSeen.add(getObjectId(ctx, item.cloud)); } for (const item of ctx.opaqueGlyphFieldDrawList) { if (isSafeGlyphOccluder(item)) { glyphOccluders.push(item); signature = mixOcclusionHash(signature, 3); signature = mixOcclusionHash(signature, getObjectId(ctx, item.field)); signature = mixOcclusionHash(signature, item.field.occluderRevision >>> 0); signature = mixOcclusionHash(signature, hashWorldMatrix(item.field.transform.worldMatrixPtr)); signature = mixOcclusionHash(signature, getObjectId(ctx, item.geometry)); } if (!candidateSeen.has(getObjectId(ctx, item.field)) && tryPushGlyphOcclusionCandidate(ctx, item.field, candidates)) candidateSeen.add(getObjectId(ctx, item.field)); } for (const item of ctx.opaqueNodeLinkDrawList) { if (isSafeNodeLinkOccluder(item)) { nodeLinkOccluders.push(item); signature = mixOcclusionHash(signature, 4); signature = mixOcclusionHash(signature, getObjectId(ctx, item.link)); signature = mixOcclusionHash(signature, item.link.occluderRevision >>> 0); signature = mixOcclusionHash(signature, hashWorldMatrix(item.link.transform.worldMatrixPtr)); signature = mixOcclusionHash(signature, item.passKind === "node-points" ? 1 : item.passKind === "node-solid" ? 2 : item.passKind === "edge-lines" ? 3 : 4); } if (!candidateSeen.has(getObjectId(ctx, item.link)) && tryPushNodeLinkOcclusionCandidate(ctx, item.link, candidates)) candidateSeen.add(getObjectId(ctx, item.link)); } for (const item of ctx.opaqueLatticeSpaceDrawList) { if (isSafeLatticeSpaceOccluder(item)) { latticeSpaceOccluders.push(item); signature = mixOcclusionHash(signature, 5); signature = mixOcclusionHash(signature, getObjectId(ctx, item.space)); signature = mixOcclusionHash(signature, item.space.occluderRevision); signature = mixOcclusionHash(signature, hashWorldMatrix(item.space.transform.worldMatrixPtr)); } if (!candidateSeen.has(getObjectId(ctx, item.space)) && tryPushLatticeSpaceOcclusionCandidate(ctx, item.space, candidates)) candidateSeen.add(getObjectId(ctx, item.space)); } candidateSeen.clear(); return { signature: signature >>> 0, candidates, meshOccluders, pointCloudOccluders, glyphOccluders, nodeLinkOccluders, latticeSpaceOccluders }; }; var tryPushMeshOcclusionCandidate = (ctx, item, out) => { const mesh = item.mesh; if (item.skinned) return false; const bounds = getMeshLocalBoundsSource(mesh); if (!(bounds.boundsRadius > 0) || !Number.isFinite(bounds.boundsRadius)) return false; const center = bounds.boundsCenter; if (!Number.isFinite(center[0]) || !Number.isFinite(center[1]) || !Number.isFinite(center[2])) return false; out.push({ kind: "mesh", object: mesh, objectId: getObjectId(ctx, mesh), worldMatrixPtr: mesh.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: bounds.boundsRadius }); return true; }; var tryPushPointCloudOcclusionCandidate = (ctx, cloud, out) => { if (!(cloud.boundsRadius > 0) || !Number.isFinite(cloud.boundsRadius)) return false; const center = cloud.boundsCenter; if (!Number.isFinite(center[0]) || !Number.isFinite(center[1]) || !Number.isFinite(center[2])) return false; out.push({ kind: "pointcloud", object: cloud, objectId: getObjectId(ctx, cloud), worldMatrixPtr: cloud.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: cloud.boundsRadius }); return true; }; var tryPushGlyphOcclusionCandidate = (ctx, field, out) => { if (!(field.boundsRadius > 0) || !Number.isFinite(field.boundsRadius)) return false; const center = field.boundsCenter; if (!Number.isFinite(center[0]) || !Number.isFinite(center[1]) || !Number.isFinite(center[2])) return false; out.push({ kind: "glyphfield", object: field, objectId: getObjectId(ctx, field), worldMatrixPtr: field.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: field.boundsRadius }); return true; }; var tryPushNodeLinkOcclusionCandidate = (ctx, link, out) => { if (!(link.boundsRadius > 0) || !Number.isFinite(link.boundsRadius)) return false; const center = link.boundsCenter; if (!Number.isFinite(center[0]) || !Number.isFinite(center[1]) || !Number.isFinite(center[2])) return false; out.push({ kind: "nodelink", object: link, objectId: getObjectId(ctx, link), worldMatrixPtr: link.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: link.boundsRadius }); return true; }; var tryPushLatticeSpaceOcclusionCandidate = (ctx, space, out) => { const bounds = space.getLocalBounds(); if (!(bounds.sphereRadius > 0) || !Number.isFinite(bounds.sphereRadius)) return false; out.push({ kind: "latticespace", object: space, objectId: getObjectId(ctx, space), worldMatrixPtr: space.transform.worldMatrixPtr, boundsCenter: [bounds.sphereCenter[0], bounds.sphereCenter[1], bounds.sphereCenter[2]], boundsRadius: bounds.sphereRadius }); return true; }; var viewProjectionMatches = (ctx, currentPtr, previous) => { const current = wasm.f32view(currentPtr, 16); if (previous.length !== 16) return false; for (let i = 0; i < 16; i++) if (Math.abs(current[i] - previous[i]) > ctx.OCCLUSION_VIEW_PROJ_EPSILON) return false; return true; }; var occlusionMetadataMatches = (ctx, camera, signature, meta) => { if (meta.resourceGeneration !== ctx.occlusionResourceGeneration) return false; if (meta.viewportWidth !== ctx.width || meta.viewportHeight !== ctx.height) return false; if (meta.hierarchyWidth !== ctx.occlusionWidth || meta.hierarchyHeight !== ctx.occlusionHeight) return false; if (meta.cameraType !== camera.type || meta.occluderSignature !== signature) return false; return viewProjectionMatches(ctx, ctx.cameraUniformStagingPtr, meta.viewProjection); }; var getValidOcclusionHierarchy = (ctx, camera, signature) => { const latest = ctx.latestOcclusionHierarchy; if (!latest || !ctx.occlusionHierarchyLayout) return null; const meta = latest.metadata; if (!occlusionMetadataMatches(ctx, camera, signature, meta)) return null; return latest; }; var applyOcclusionFiltering = (ctx, _camera, candidates, hierarchy) => { if (candidates.length === 0) return; ensureCullingCapacity(ctx, candidates.length); const worldPtrsPtr = frameArena.alloc(candidates.length * 4, 4); const localCentersPtr = frameArena.allocF32(candidates.length * 3); const localRadiiPtr = frameArena.allocF32(candidates.length); const worldPtrs = wasm.u32view(worldPtrsPtr, candidates.length); const localCenters = wasm.f32view(localCentersPtr, candidates.length * 3); const localRadii = wasm.f32view(localRadiiPtr, candidates.length); for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i]; worldPtrs[i] = candidate.worldMatrixPtr >>> 0; localCenters[i * 3 + 0] = candidate.boundsCenter[0]; localCenters[i * 3 + 1] = candidate.boundsCenter[1]; localCenters[i * 3 + 2] = candidate.boundsCenter[2]; localRadii[i] = candidate.boundsRadius; } cullf.prepareWorldSpheresFromPtrs(ctx.cullCentersPtr, ctx.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, candidates.length); const layout = hierarchy.metadata.layout; const depthPtr = ctx.occlusionHierarchyWasmSerial === ctx.latestOcclusionHierarchySerial && ctx.occlusionHierarchyWasmLength === hierarchy.data.length ? ctx.occlusionHierarchyWasmPtr : frameArena.allocF32(hierarchy.data.length); if (depthPtr !== ctx.occlusionHierarchyWasmPtr) wasm.f32view(depthPtr, hierarchy.data.length).set(hierarchy.data); const mipOffsetsPtr = frameArena.alloc(layout.offsets.byteLength, 4); const mipWidthsPtr = frameArena.alloc(layout.widths.byteLength, 4); const mipHeightsPtr = frameArena.alloc(layout.heights.byteLength, 4); wasm.u32view(mipOffsetsPtr, layout.offsets.length).set(layout.offsets); wasm.u32view(mipWidthsPtr, layout.widths.length).set(layout.widths); wasm.u32view(mipHeightsPtr, layout.heights.length).set(layout.heights); const outPtr = frameArena.alloc(candidates.length * 4, 4); const statsPtr = frameArena.alloc(12, 4); const visibleCount = cullf.spheresOcclusion(outPtr, statsPtr, ctx.cullCentersPtr, ctx.cullRadiiPtr, candidates.length, ctx.cameraUniformStagingPtr, ctx.width, ctx.height, mipOffsetsPtr, mipWidthsPtr, mipHeightsPtr, layout.mipCount, depthPtr, hierarchy.data.length, ctx.OCCLUSION_NEAR_EPSILON, ctx.OCCLUSION_MAX_SCREEN_COVERAGE, ctx.OCCLUSION_DEPTH_BIAS); if (ctx.occlusionCullingStatsEnabled) { const stats = wasm.u32view(statsPtr, 3); ctx.cullingStats.occlusion.tested = stats[0] >>> 0; ctx.cullingStats.occlusion.visible = stats[1] >>> 0; ctx.cullingStats.occlusion.occluded = stats[2] >>> 0; } const visibleSet = ctx.occlusionVisibleObjectIds; const candidateSet = ctx.occlusionCandidateObjectIds; visibleSet.clear(); candidateSet.clear(); for (let i = 0; i < candidates.length; i++) candidateSet.add(candidates[i].objectId); const out = wasm.u32view(outPtr, visibleCount); for (let i = 0; i < visibleCount; i++) visibleSet.add(candidates[out[i]].objectId); filterOpaqueDrawListInPlace(ctx.opaqueDrawList, (item) => { const id = getObjectId(ctx, item.mesh); return !candidateSet.has(id) || visibleSet.has(id); }); filterOpaqueDrawListInPlace(ctx.opaquePointCloudDrawList, (item) => { const id = getObjectId(ctx, item.cloud); return !candidateSet.has(id) || visibleSet.has(id); }); filterOpaqueDrawListInPlace(ctx.opaqueGlyphFieldDrawList, (item) => { const id = getObjectId(ctx, item.field); return !candidateSet.has(id) || visibleSet.has(id); }); filterOpaqueDrawListInPlace(ctx.opaqueNodeLinkDrawList, (item) => { const id = getObjectId(ctx, item.link); return !candidateSet.has(id) || visibleSet.has(id); }); filterOpaqueDrawListInPlace(ctx.opaqueLatticeSpaceDrawList, (item) => { const id = getObjectId(ctx, item.space); return !candidateSet.has(id) || visibleSet.has(id); }); visibleSet.clear(); candidateSet.clear(); }; var filterOpaqueDrawListInPlace = (items, keep) => { let write = 0; for (let i = 0; i < items.length; i++) { const item = items[i]; if (!keep(item)) continue; items[write++] = item; } items.length = write; }; var isCoverageStableMeshMaterial = (material) => { if (material instanceof CustomMaterial) return false; if (material instanceof DataMaterial) return false; if (material instanceof UnlitMaterial) return material.alphaCutoff <= 0; if (material instanceof StandardMaterial) return material.alphaCutoff <= 0 && !material.usesTransmissionLayout(); return false; }; var isSafeMeshOccluder = (ctx, item) => { const material = item.material; if (material.blendMode !== "opaque" /* Opaque */) return false; if (!material.depthWrite || !material.depthTest) return false; if (isOpticallyTransmissiveMaterial(material)) return false; if (!isCoverageStableMeshMaterial(material)) return false; if (item.skinned) return false; return true; }; var getMeshOccluderToken = (ctx, item) => { const mesh = item.mesh; const material = item.material; let hash = 2166136261 >>> 0; hash = mixOcclusionHash(hash, getObjectId(ctx, item.geometry)); hash = mixOcclusionHash(hash, getObjectId(ctx, getMeshVertexSource(mesh))); hash = mixOcclusionHash(hash, getObjectId(ctx, material)); hash = mixOcclusionHash(hash, blendModeHash(material.blendMode)); hash = mixOcclusionHash(hash, material.depthWrite ? 1 : 0); hash = mixOcclusionHash(hash, material.depthTest ? 1 : 0); hash = mixOcclusionHash(hash, cullModeHash(material.cullMode)); hash = mixOcclusionHash(hash, item.mirrored ? 1 : 0); hash = mixOcclusionHash(hash, item.skinned ? 1 : 0); hash = mixOcclusionHash(hash, hasMeshMorphRuntime(mesh) ? 1 : 0); hash = mixOcclusionHash(hash, getMeshMorphRevision(mesh)); hash = mixOcclusionHash(hash, mesh.geometry.morphBaseRevision); hash = mixOcclusionHash(hash, hashWorldMatrix(mesh.transform.worldMatrixPtr)); if (material instanceof UnlitMaterial) hash = mixOcclusionHashF32(hash, material.alphaCutoff); if (material instanceof StandardMaterial) { hash = mixOcclusionHashF32(hash, material.alphaCutoff); hash = mixOcclusionHash(hash, material.getFeatureMask() >>> 0); hash = mixOcclusionHash(hash, material.usesTransmissionLayout() ? 1 : 0); } return hash >>> 0; }; var isSafePointCloudOccluder = (item) => item.cloud.blendMode === "opaque" /* Opaque */ && item.cloud.depthWrite && item.cloud.depthTest; var isSafeGlyphOccluder = (item) => item.field.blendMode === "opaque" /* Opaque */ && item.field.depthWrite && item.field.depthTest; var isSafeNodeLinkOccluder = (item) => item.link.blendMode === "opaque" /* Opaque */ && item.link.depthWrite && item.link.depthTest; var isSafeLatticeSpaceOccluder = (item) => item.space.blendMode === "opaque" /* Opaque */ && item.space.depthWrite && item.space.depthTest; var captureOcclusionHierarchy = (ctx, camera) => { const frameState = ctx.pendingOcclusionFrameState; if (!frameState) return; const safeOccluderCount = frameState.meshOccluders.length + frameState.pointCloudOccluders.length + frameState.glyphOccluders.length + frameState.nodeLinkOccluders.length + frameState.latticeSpaceOccluders.length; if (safeOccluderCount <= 0) return; ensureOcclusionResources(ctx); if (!ctx.occlusionHierarchyTexture || !ctx.occlusionDepthView || !ctx.occlusionHierarchyLayout) return; if (getValidOcclusionHierarchy(ctx, camera, frameState.signature)) return; for (const pending of ctx.occlusionReadbackSlots) if (pending.state === "mapping" && pending.metadata && occlusionMetadataMatches(ctx, camera, frameState.signature, pending.metadata)) return; const slot = getIdleOcclusionReadbackSlot(ctx); if (!slot) return; ensureOcclusionReadbackBuffer(ctx, slot, ctx.occlusionHierarchyLayout.totalBytes); if (!slot.buffer) return; const encoder = ctx.device.createCommandEncoder(); const capturePass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.occlusionHierarchyMipViews[0], clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: ctx.occlusionDepthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" } }); executeOcclusionMeshDrawList(ctx, capturePass, frameState.meshOccluders); executeOcclusionGlyphFieldDrawList(ctx, capturePass, frameState.glyphOccluders); executeOcclusionPointCloudDrawList(ctx, capturePass, frameState.pointCloudOccluders); executeOcclusionNodeLinkDrawList(ctx, capturePass, frameState.nodeLinkOccluders); executeOcclusionLatticeSpaceDrawList(ctx, capturePass, frameState.latticeSpaceOccluders); capturePass.end(); for (let mip = 1; mip < ctx.occlusionHierarchyLayout.mipCount; mip++) { const pass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.occlusionHierarchyMipViews[mip], clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); pass.setPipeline(getOrCreateOcclusionReducePipeline(ctx)); pass.setBindGroup(0, getOrCreateOcclusionReduceBindGroup(ctx, mip - 1)); pass.draw(3); pass.end(); } for (let mip = 0; mip < ctx.occlusionHierarchyLayout.mipCount; mip++) { encoder.copyTextureToBuffer( { texture: ctx.occlusionHierarchyTexture, mipLevel: mip }, { buffer: slot.buffer, offset: ctx.occlusionHierarchyLayout.copyOffsets[mip], bytesPerRow: ctx.occlusionHierarchyLayout.rowBytes[mip], rowsPerImage: ctx.occlusionHierarchyLayout.heights[mip] }, { width: ctx.occlusionHierarchyLayout.widths[mip], height: ctx.occlusionHierarchyLayout.heights[mip], depthOrArrayLayers: 1 } ); } const metadata = { resourceGeneration: ctx.occlusionResourceGeneration, viewportWidth: ctx.width, viewportHeight: ctx.height, hierarchyWidth: ctx.occlusionWidth, hierarchyHeight: ctx.occlusionHeight, cameraType: camera.type, occluderSignature: frameState.signature, viewProjection: new Float32Array(wasm.f32view(ctx.cameraUniformStagingPtr, 16)), layout: ctx.occlusionHierarchyLayout }; slot.metadata = metadata; slot.data = null; slot.serial = ++ctx.occlusionCaptureSerial; slot.state = "mapping"; ctx.queue.submit([encoder.finish()]); slot.pending = slot.buffer.mapAsync(GPUMapMode.READ).then(() => { if (!slot.buffer || !slot.metadata) return; const mapped = slot.buffer.getMappedRange(); const data = new Float32Array(slot.metadata.layout.texelCount); for (let mip = 0; mip < slot.metadata.layout.mipCount; mip++) { const width = slot.metadata.layout.widths[mip]; const height = slot.metadata.layout.heights[mip]; const texelOffset = slot.metadata.layout.offsets[mip]; const rowBytes = slot.metadata.layout.rowBytes[mip]; const copyOffset = slot.metadata.layout.copyOffsets[mip]; for (let row = 0; row < height; row++) { const src = new Float32Array(mapped, copyOffset + row * rowBytes, width); data.set(src, texelOffset + row * width); } } slot.data = data; slot.state = "ready"; if (slot.metadata && slot.metadata.resourceGeneration === ctx.occlusionResourceGeneration && slot.serial >= ctx.latestOcclusionHierarchySerial) { ctx.latestOcclusionHierarchySerial = slot.serial; ctx.latestOcclusionHierarchy = { metadata: slot.metadata, data }; if (data.length > ctx.occlusionHierarchyWasmLength) { if (ctx.occlusionHierarchyWasmPtr) wasm.freeF32(ctx.occlusionHierarchyWasmPtr, ctx.occlusionHierarchyWasmLength); ctx.occlusionHierarchyWasmPtr = wasm.allocF32(data.length); if (!ctx.occlusionHierarchyWasmPtr) throw new Error(`Renderer occlusion hierarchy allocation failed (${data.length} f32 elements).`); ctx.occlusionHierarchyWasmLength = data.length; } wasm.f32view(ctx.occlusionHierarchyWasmPtr, data.length).set(data); ctx.occlusionHierarchyWasmSerial = slot.serial; } }).catch(() => { slot.data = null; slot.metadata = null; slot.state = "idle"; }).finally(() => { try { slot.buffer?.unmap(); } catch { } if (slot.state !== "ready") slot.state = "idle"; slot.pending = null; }); }; var executeOcclusionMeshDrawList = (ctx, pass, items) => { let lastPipeline = null; let lastGeometry = null; let lastVertexSourceId = -1; for (let i = 0; i < items.length; i++) { const item = items[i]; const geometry = item.geometry; if (geometry !== lastGeometry || item.vertexSourceId !== lastVertexSourceId) { geometry.upload(ctx.device); getMeshVertexBuffers(item.mesh, ctx.device, ctx.queue); lastGeometry = geometry; lastVertexSourceId = item.vertexSourceId; } const pipeline = getOrCreateOcclusionMeshPipeline(ctx, item); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } bindModelUniform(ctx, pass, item.mesh.transform.worldMatrixPtr); const geometryBuffers = getMeshVertexBuffers(item.mesh, ctx.device, ctx.queue); pass.setVertexBuffer(0, geometryBuffers.positionBuffer); if (geometry.isIndexed && geometry.indexBuffer) { pass.setIndexBuffer(geometry.indexBuffer, "uint32"); pass.drawIndexed(geometry.indexCount); } else pass.draw(geometry.vertexCount); } }; var executeOcclusionPointCloudDrawList = (ctx, pass, items) => { let lastCloud = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const cloud = item.cloud; ensurePointCloudBindGroup(ctx, cloud); if (!cloud.bindGroup) continue; bindModelUniform(ctx, pass, cloud.transform.worldMatrixPtr); pass.setPipeline(getOrCreateOcclusionPointCloudPipeline(ctx)); if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; } pass.draw(6, cloud.pointCount); } }; var executeOcclusionGlyphFieldDrawList = (ctx, pass, list) => { let lastGeometry = null; let lastField = null; let lastPipeline = null; for (let i = 0; i < list.length; i++) { const item = list[i]; const field = item.field; ensureGlyphFieldBindGroup(ctx, field); if (!field.bindGroup) continue; const pipeline = getOrCreateOcclusionGlyphFieldPipeline(ctx, field); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (item.geometry !== lastGeometry) { item.geometry.upload(ctx.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); lastGeometry = item.geometry; } bindModelUniform(ctx, pass, field.transform.worldMatrixPtr); if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } if (item.geometry.isIndexed && item.geometry.indexBuffer) { pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); pass.drawIndexed(item.geometry.indexCount, field.instanceCount); } else pass.draw(item.geometry.vertexCount, field.instanceCount); } }; var executeOcclusionNodeLinkDrawList = (ctx, pass, list) => { let lastLink = null; let lastGeometry = null; let lastPipeline = null; for (let i = 0; i < list.length; i++) { const item = list[i]; const link = item.link; ensureNodeLinkBindGroup(ctx, link); if (!link.bindGroup) continue; const pipeline = getOrCreateOcclusionNodeLinkPipeline(ctx, link, item.passKind); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (item.geometry && item.geometry !== lastGeometry) { item.geometry.upload(ctx.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); lastGeometry = item.geometry; } bindModelUniform(ctx, pass, link.transform.worldMatrixPtr); if (link !== lastLink) { pass.setBindGroup(1, link.bindGroup); lastLink = link; } if (item.passKind === "node-points") pass.draw(6, link.nodeCount); else if (item.passKind === "edge-lines") pass.draw(2, link.edgeCount); else if (item.passKind === "node-solid") { if (!item.geometry) continue; if (item.geometry.isIndexed && item.geometry.indexBuffer) { pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); pass.drawIndexed(item.geometry.indexCount, link.nodeCount); } else pass.draw(item.geometry.vertexCount, link.nodeCount); } else { if (!item.geometry) continue; if (item.geometry.isIndexed && item.geometry.indexBuffer) { pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); pass.drawIndexed(item.geometry.indexCount, link.edgeCount); } else pass.draw(item.geometry.vertexCount, link.edgeCount); } } }; var executeOcclusionLatticeSpaceDrawList = (ctx, pass, list) => { let lastSpace = null; let lastPipeline = null; for (const item of list) { const space = item.space; ensureLatticeSpaceBindGroup(ctx, space); if (!space.bindGroup) continue; const pipeline = getOrCreateOcclusionLatticeSpacePipeline(ctx, space); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } bindModelUniform(ctx, pass, space.transform.worldMatrixPtr); if (space !== lastSpace) { pass.setBindGroup(1, space.bindGroup); lastSpace = space; } if (space.dimensionCount === 2) pass.draw(6); else pass.draw(36, space.drawCellCount); } }; var getOcclusionReduceBindGroupLayout = (ctx) => { if (ctx.occlusionReduceBindGroupLayout) return ctx.occlusionReduceBindGroupLayout; ctx.occlusionReduceBindGroupLayout = ctx.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float", viewDimension: "2d" } }] }); return ctx.occlusionReduceBindGroupLayout; }; var getOrCreateOcclusionReducePipeline = (ctx) => { if (ctx.occlusionReducePipeline) return ctx.occlusionReducePipeline; let shaderModule = ctx.shaderCache.get(occlusion_reduce_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: occlusion_reduce_default }); ctx.shaderCache.set(occlusion_reduce_default, shaderModule); } ctx.occlusionReducePipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [getOcclusionReduceBindGroupLayout(ctx)] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); return ctx.occlusionReducePipeline; }; var getOrCreateOcclusionReduceBindGroup = (ctx, srcMip) => { if (!ctx.occlusionHierarchyTexture) throw new Error("Renderer: occlusion hierarchy texture is not initialized."); const key = `occlusion-reduce:${getObjectId(ctx, ctx.occlusionHierarchyTexture)}:${srcMip}`; const cached = ctx.occlusionReduceBindGroups.get(key); if (cached) return cached; const bindGroup = ctx.device.createBindGroup({ layout: getOcclusionReduceBindGroupLayout(ctx), entries: [{ binding: 0, resource: ctx.occlusionHierarchyMipViews[srcMip] }] }); ctx.occlusionReduceBindGroups.set(key, bindGroup); return bindGroup; }; var getOrCreateOcclusionMeshPipeline = (ctx, item) => { const cullMode = getCullMode(ctx, item.material.cullMode); const key = `occlusion:mesh:${cullMode}:${item.mirrored ? "cw" : "ccw"}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(occlusion_mesh_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: occlusion_mesh_default }); ctx.shaderCache.set(occlusion_mesh_default, shaderModule); } const pipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode, frontFace: item.mirrored ? "cw" : "ccw" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateOcclusionPointCloudPipeline = (ctx) => { const key = "occlusion:pointcloud"; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(occlusion_pointcloud_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: occlusion_pointcloud_default }); ctx.shaderCache.set(occlusion_pointcloud_default, shaderModule); } const pipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getPointCloudBindGroupLayout(ctx)] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateOcclusionGlyphFieldPipeline = (ctx, field) => { const cullMode = getCullMode(ctx, field.cullMode); const key = `occlusion:glyphfield:${cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(occlusion_glyphfield_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: occlusion_glyphfield_default }); ctx.shaderCache.set(occlusion_glyphfield_default, shaderModule); } const pipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getGlyphFieldBindGroupLayout(ctx)] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateOcclusionNodeLinkPipeline = (ctx, link, passKind) => { const key = `occlusion:nodelink:${passKind}:${link.cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let shaderModule = ctx.shaderCache.get(occlusion_nodelink_default); if (!shaderModule) { shaderModule = ctx.device.createShaderModule({ code: occlusion_nodelink_default }); ctx.shaderCache.set(occlusion_nodelink_default, shaderModule); } let vertexEntry = "vs_node_points"; let buffers = []; let topology = "triangle-list"; let cullMode = "none"; if (passKind === "node-solid") { vertexEntry = "vs_node_solid"; buffers = [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }]; cullMode = getCullMode(ctx, link.cullMode); } else if (passKind === "edge-lines") { vertexEntry = "vs_edge_lines"; topology = "line-list"; } else if (passKind === "edge-cylinders") { vertexEntry = "vs_edge_cylinders"; buffers = [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }] }]; cullMode = getCullMode(ctx, link.cullMode); } const pipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getNodeLinkBindGroupLayout(ctx)] }), vertex: { module: shaderModule, entryPoint: vertexEntry, buffers }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology, cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; var getOrCreateOcclusionLatticeSpacePipeline = (ctx, space) => { const cullMode = space.dimensionCount === 2 ? "none" : getCullMode(ctx, space.cullMode); const key = `occlusion:latticespace:${space.dimensionCount}:${cullMode}`; const cached = ctx.pipelineCache.get(key); if (cached) return cached; let module = ctx.shaderCache.get(occlusion_latticespace_default); if (!module) { module = ctx.device.createShaderModule({ code: occlusion_latticespace_default }); ctx.shaderCache.set(occlusion_latticespace_default, module); } const pipeline = ctx.device.createRenderPipeline({ layout: ctx.device.createPipelineLayout({ bindGroupLayouts: [ctx.globalBindGroupLayout, getLatticeSpaceBindGroupLayout(ctx)] }), vertex: { module, entryPoint: space.dimensionCount === 2 ? "vs_2d" : "vs_3d", buffers: [] }, fragment: { module, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" } }); ctx.pipelineCache.set(key, pipeline); return pipeline; }; // typescript/core/renderer.ts var Renderer = class _Renderer { canvas; effects; shadowRenderer; context; device; queue; format; depthTexture; depthView; width = 0; height = 0; smaaEnabled = false; smaaSceneColorTexture = null; smaaSceneColorView = null; smaaEdgesTexture = null; smaaEdgesView = null; smaaBlendTexture = null; smaaBlendView = null; smaaParamsBuffer = null; smaaSamplerPoint = null; smaaSamplerLinear = null; smaaShaderModule = null; smaaEdgePipeline = null; smaaWeightPipeline = null; smaaNeighborhoodPipeline = null; smaaEdgeBindGroupLayout = null; smaaWeightBindGroupLayout = null; smaaNeighborhoodBindGroupLayout = null; smaaEdgeBindGroup = null; smaaWeightBindGroup = null; smaaNeighborhoodBindGroup = null; transmissionSceneColorTexture = null; transmissionSceneColorView = null; transmissionSourceTexture = null; transmissionSourceView = null; transmissionSourceRevision = 0; globalBindGroupLayout; skinBindGroupLayout; cameraUniformBuffer; modelUniformBuffer = null; modelUniformBufferCapacity = 0; modelUniformStride = 256; modelUniformBindGroup = null; modelUniformSlots = /* @__PURE__ */ new Map(); modelUniformPtrScratch = []; pickUniformIndex = 0; INITIAL_UNIFORM_CAPACITY = 64; lightingUniformBuffer; instanceBuffer = null; instanceBufferCapacityBytes = 0; instanceBufferOffset = 0; instanceBufferGeneration = 0; instanceRunCacheIndex = 0; instanceRunUploadCount = 0; instanceRunCache = []; INSTANCE_STRIDE_BYTES = 128; framePreparedSkins = /* @__PURE__ */ new Set(); frameSkinPreparationCount = 0; pipelineCache = /* @__PURE__ */ new Map(); shaderCache = /* @__PURE__ */ new Map(); drawItemPool = []; drawItemPoolUsed = 0; opaqueDrawList = []; transparentDrawList = []; pointCloudBindGroupLayout = null; pointCloudDummyColorsBuffer = null; pointCloudDrawItemPool = []; pointCloudDrawItemPoolUsed = 0; opaquePointCloudDrawList = []; transparentPointCloudDrawList = []; splatFieldBindGroupLayout = null; splatFieldDummySHBuffer = null; splatFieldDrawItemPool = []; splatFieldDrawItemPoolUsed = 0; transparentSplatFieldDrawList = []; cullSplatFieldScratch = []; splatFieldSortStates = /* @__PURE__ */ new Map(); splatSortCapacity = 0; splatSortKeyA = null; splatSortKeyB = null; splatSortIndexA = null; splatSortIndexB = null; splatSortPrefix = null; splatSortScanLevels = []; computePipelineCache = /* @__PURE__ */ new Map(); splatSortKeygenBindGroupLayout = null; splatSortFlagsBindGroupLayout = null; splatSortScanBlockBindGroupLayout = null; splatSortScanAddBindGroupLayout = null; splatSortScatterBindGroupLayout = null; glyphFieldBindGroupLayout = null; glyphFieldDummyAttributesBuffer = null; glyphFieldDrawItemPool = []; glyphFieldDrawItemPoolUsed = 0; opaqueGlyphFieldDrawList = []; transparentGlyphFieldDrawList = []; nodeLinkBindGroupLayout = null; nodeLinkDummyF32Buffer = null; nodeLinkDummyU32Buffer = null; nodeLinkDrawItemPool = []; nodeLinkDrawItemPoolUsed = 0; opaqueNodeLinkDrawList = []; transparentNodeLinkDrawList = []; cullNodeLinkScratch = []; nodeLinkSphereGeometry = null; nodeLinkCubeGeometry = null; nodeLinkCylinderGeometry = null; latticeSpaceBindGroupLayout = null; latticeSpaceDummyF32Buffer = null; latticeSpaceDummyU32Buffer = null; latticeSpaceDrawItemPool = []; latticeSpaceDrawItemPoolUsed = 0; opaqueLatticeSpaceDrawList = []; transparentLatticeSpaceDrawList = []; cullLatticeSpaceScratch = []; latticeSpaceSortStates = /* @__PURE__ */ new Map(); latticeSortCapacity = 0; latticeSortKeyA = null; latticeSortKeyB = null; latticeSortIndexA = null; latticeSortIndexB = null; latticeSortPrefix = null; latticeSortScanLevels = []; latticeSortKeygenBindGroupLayout = null; latticeSortFlagsBindGroupLayout = null; latticeSortScanBlockBindGroupLayout = null; latticeSortScanAddBindGroupLayout = null; latticeSortScatterBindGroupLayout = null; cullGlyphFieldScratch = []; transparentMergedDrawList = []; cullPointCloudScratch = []; objectIds = /* @__PURE__ */ new WeakMap(); objectsById = /* @__PURE__ */ new Map(); nextObjectId = 1; cameraUniformStagingPtr; lightingUniformStagingPtr; modelUniformStagingPtr; cameraUniformStagingView; lightingUniformStagingView; lightingCountView; modelUniformStagingView; _wasmBuffer = null; frustumCullingEnabled = true; frustumCullingStatsEnabled = false; occlusionCullingEnabled = false; occlusionCullingStatsEnabled = false; cullingStats = { frustum: { tested: 0, visible: 0 }, occlusion: { tested: 0, visible: 0, occluded: 0 } }; frameFrustumTested = 0; frameFrustumVisible = 0; cullCentersPtr = 0; cullRadiiPtr = 0; cullCapacity = 0; cullMeshScratch = []; occlusionVisibleObjectIds = /* @__PURE__ */ new Set(); occlusionCandidateObjectIds = /* @__PURE__ */ new Set(); occlusionCandidateScratch = []; pendingOcclusionFrameState = null; latestOcclusionHierarchy = null; latestOcclusionHierarchySerial = 0; occlusionResourceGeneration = 0; occlusionHierarchyWasmPtr = 0; occlusionHierarchyWasmLength = 0; occlusionHierarchyWasmSerial = 0; occlusionHierarchyTexture = null; occlusionHierarchyMipViews = []; occlusionDepthTexture = null; occlusionDepthView = null; occlusionHierarchyLayout = null; occlusionWidth = 0; occlusionHeight = 0; occlusionReadbackSlots = []; occlusionCaptureSerial = 0; occlusionReduceBindGroupLayout = null; occlusionReducePipeline = null; occlusionReduceBindGroups = /* @__PURE__ */ new Map(); OCCLUSION_READBACK_RING_SIZE = 3; OCCLUSION_MAX_LONG_EDGE = 256; OCCLUSION_NEAR_EPSILON = 1e-5; OCCLUSION_MAX_SCREEN_COVERAGE = 0.2; OCCLUSION_DEPTH_BIAS = 2e-4; OCCLUSION_VIEW_PROJ_EPSILON = 1e-6; fallbackSampler; fallbackWhiteTexture; fallbackWhiteViewLinear; fallbackWhiteViewSrgb; fallbackNormalTexture; fallbackNormalViewLinear; fallbackMRTex; fallbackMRViewLinear; fallbackOcclusionTex; fallbackOcclusionViewLinear; fallbackAnisotropyTexture; fallbackAnisotropyViewLinear; gpuTimingSupported = false; gpuTimingEnabled = false; gpuQuerySet = null; gpuResolveBuffer = null; gpuResultBuffer = null; gpuResultPending = false; _gpuTimeNs = null; dataMaterialDummyDataBuffer = null; pickBindGroupLayout = null; pickUniformBuffers = []; pickBindGroups = []; pickIdTexture = null; pickIdView = null; pickDepthTexture = null; pickDepthView = null; pickDepthPayloadTexture = null; pickDepthPayloadView = null; pickIdReadbackBuffer = null; pickDepthReadbackBuffer = null; pickIdReadbackCapacityBytes = 0; pickDepthReadbackCapacityBytes = 0; pickTail = Promise.resolve(); destroyed = false; constructor(canvas) { this.canvas = canvas; this.effects = new RenderEffects(); this.shadowRenderer = new RendererShadows(this); } static async create(canvas, descriptor = {}) { const renderer = new _Renderer(canvas); await renderer.init(descriptor); return renderer; } async init(descriptor) { if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser."); const adapter = await navigator.gpu.requestAdapter({ powerPreference: descriptor.powerPreference ?? "high-performance" }); if (!adapter) throw new Error("Failed to get GPU adapter."); const requiredFeatures = []; if (adapter.features.has("timestamp-query")) requiredFeatures.push("timestamp-query"); if (adapter.features.has("primitive-index")) requiredFeatures.push("primitive-index"); const deviceDesc = {}; if (requiredFeatures.length > 0) deviceDesc.requiredFeatures = requiredFeatures; const requiredLimits = {}; if (descriptor.maxBufferSize !== void 0) requiredLimits.maxBufferSize = descriptor.maxBufferSize; if (descriptor.maxStorageBufferBindingSize !== void 0) requiredLimits.maxStorageBufferBindingSize = descriptor.maxStorageBufferBindingSize; if (descriptor.maxUniformBufferBindingSize !== void 0) requiredLimits.maxUniformBufferBindingSize = descriptor.maxUniformBufferBindingSize; if (Object.keys(requiredLimits).length > 0) deviceDesc.requiredLimits = requiredLimits; this.device = await adapter.requestDevice(deviceDesc); setShadowDeviceLimits(this.effects.shadows, this.device.limits.maxTextureDimension2D, this.device.limits.maxTextureArrayLayers); this.gpuTimingSupported = this.device.features.has("timestamp-query"); this.queue = this.device.queue; this.context = this.canvas.getContext("webgpu"); if (!this.context) throw new Error("Failed to get WebGPU canvas context."); if (descriptor.canvasFormat) this.format = descriptor.canvasFormat; else if (typeof navigator.gpu.getPreferredCanvasFormat === "function") this.format = navigator.gpu.getPreferredCanvasFormat(); else this.format = "rgba8unorm"; this.smaaEnabled = descriptor.antialias ?? false; if (this.smaaEnabled) this.createSmaaResources(); this.createGlobalBindGroupLayout(); this.createSkinBindGroupLayout(); this.createUniformBuffers(); this.createFallbackTextures(); this.resize(); this.frustumCullingEnabled = descriptor.frustumCulling ?? true; this.frustumCullingStatsEnabled = descriptor.frustumCullingStats ?? false; this.occlusionCullingEnabled = descriptor.occlusionCulling ?? false; this.occlusionCullingStatsEnabled = descriptor.occlusionCullingStats ?? false; if (this.occlusionCullingEnabled) this.ensureOcclusionResources(); } get gpu() { return { device: this.device, queue: this.queue, format: this.format }; } get gpuTimeNs() { return this._gpuTimeNs; } get isGpuTimingSupported() { return this.gpuTimingSupported; } enableGpuTiming(enabled) { const want = !!enabled; if (want && this.gpuTimingSupported && !this.gpuQuerySet) this.createGpuTimingResources(); this.gpuTimingEnabled = want && this.gpuTimingSupported; } createGpuTimingResources() { createGpuTimingResources(this); } tryReadGpuTiming() { tryReadGpuTiming(this); } resize() { const dpr = Math.max(1, window.devicePixelRatio || 1); const w = Math.max(1, Math.floor(this.canvas.clientWidth * dpr)); const h = Math.max(1, Math.floor(this.canvas.clientHeight * dpr)); if (w === this.width && h === this.height) return; this.width = w; this.height = h; this.canvas.width = w; this.canvas.height = h; this.context.configure({ device: this.device, format: this.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, alphaMode: "opaque" }); if (this.depthTexture) this.depthTexture.destroy(); this.depthTexture = createDepthTexture(this.device, this.width, this.height); this.depthView = this.depthTexture.createView(); this.transmissionSceneColorTexture?.destroy(); this.transmissionSourceTexture?.destroy(); this.transmissionSceneColorTexture = null; this.transmissionSceneColorView = null; this.transmissionSourceTexture = null; this.transmissionSourceView = null; this.transmissionSourceRevision++; if (this.smaaEnabled) this.resizeSmaaTargets(); resizePickTargets(this); this.invalidateOcclusionResources(); } get aspectRatio() { return this.width / this.height; } refreshWasmStagingViews() { refreshWasmStagingViews(this); } getObjectId(obj) { return getObjectId(this, obj); } acquireDrawItem() { return acquireDrawItem(this); } acquirePointCloudDrawItem() { return acquirePointCloudDrawItem(this); } acquireSplatFieldDrawItem() { return acquireSplatFieldDrawItem(this); } acquireGlyphFieldDrawItem() { return acquireGlyphFieldDrawItem(this); } acquireNodeLinkDrawItem() { return acquireNodeLinkDrawItem(this); } ensureCullingCapacity(count) { ensureCullingCapacity(this, count); } prepareSceneFrameBase(scene, camera, prepareShadows = false, prepareAllModels = false) { this.pickUniformIndex = 0; this.instanceBufferOffset = 0; this.instanceRunCacheIndex = 0; this.framePreparedSkins.clear(); this.frameSkinPreparationCount = 0; this.frameFrustumTested = 0; this.frameFrustumVisible = 0; this.pendingOcclusionFrameState = null; this.cameraUniformStagingPtr = frameArena.allocF32(20); this.lightingUniformStagingPtr = frameArena.allocF32(8 + Scene.MAX_LIGHTS * 16); this.modelUniformStagingPtr = frameArena.allocF32(32); if (camera instanceof PerspectiveCamera && camera.autoAspect) camera.aspect = this.aspectRatio; Transform.updateAll(); this.writeCameraUniforms(camera); if (prepareShadows) this.shadowRenderer.prepare(scene, camera); this.writeLightingUniforms(scene); this.buildDrawLists(scene, camera); this.buildPointCloudDrawLists(scene, camera); this.buildSplatFieldDrawLists(scene, camera); this.buildGlyphFieldDrawLists(scene, camera); this.buildNodeLinkDrawLists(scene, camera); this.buildLatticeSpaceDrawLists(scene, camera); this.prepareFrameModelUniforms(prepareAllModels); } applyRenderCullingAndStats(camera) { this.cullingStats.frustum.tested = this.frustumCullingStatsEnabled ? this.frameFrustumTested : 0; this.cullingStats.frustum.visible = this.frustumCullingStatsEnabled ? this.frameFrustumVisible : 0; this.cullingStats.occlusion.tested = 0; this.cullingStats.occlusion.visible = 0; this.cullingStats.occlusion.occluded = 0; if (!this.occlusionCullingEnabled) return; this.pendingOcclusionFrameState = this.buildOcclusionFrameState(); if (!this.pendingOcclusionFrameState) return; const hierarchy = this.getValidOcclusionHierarchy(camera, this.pendingOcclusionFrameState.signature); if (!hierarchy) return; this.applyOcclusionFiltering(camera, this.pendingOcclusionFrameState.candidates, hierarchy); } render(scene, camera) { this.resize(); const swapTexture = this.context.getCurrentTexture(); const swapView = swapTexture.createView(); this.prepareSceneFrameBase(scene, camera, true); this.applyRenderCullingAndStats(camera); const encoder = this.device.createCommandEncoder(); this.shadowRenderer.encode(encoder); this.encodeSplatFieldSorts(encoder); this.encodeLatticeSpaceSorts(encoder); const timestampWrites = this.gpuTimingEnabled && this.gpuQuerySet ? { querySet: this.gpuQuerySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 } : void 0; const timestampBeginWrites = this.gpuTimingEnabled && this.gpuQuerySet ? { querySet: this.gpuQuerySet, beginningOfPassWriteIndex: 0 } : void 0; const timestampEndWrites = this.gpuTimingEnabled && this.gpuQuerySet ? { querySet: this.gpuQuerySet, endOfPassWriteIndex: 1 } : void 0; const hasTransmission = this.hasOpticalTransmissionDrawItems(); if (this.smaaEnabled) { if (!this.smaaSceneColorView || !this.smaaEdgesView || !this.smaaBlendView) this.resizeSmaaTargets(); if (hasTransmission) this.ensureTransmissionTargets(false); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: this.smaaSceneColorView, clearValue: { r: scene.background[0], g: scene.background[1], b: scene.background[2], a: 1 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: this.depthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" }, ...hasTransmission && timestampBeginWrites ? { timestampWrites: timestampBeginWrites } : timestampWrites ? { timestampWrites } : {} }); this.executeDrawList(pass, this.opaqueDrawList); this.executeGlyphFieldDrawList(pass, this.opaqueGlyphFieldDrawList); this.executePointCloudDrawList(pass, this.opaquePointCloudDrawList); this.executeNodeLinkDrawList(pass, this.opaqueNodeLinkDrawList); this.executeLatticeSpaceDrawList(pass, this.opaqueLatticeSpaceDrawList); if (!hasTransmission) this.executeTransparentMergedDrawList(pass); pass.end(); if (hasTransmission) { encoder.copyTextureToTexture( { texture: this.smaaSceneColorTexture }, { texture: this.transmissionSourceTexture }, { width: this.width, height: this.height, depthOrArrayLayers: 1 } ); const transparentPass = encoder.beginRenderPass({ colorAttachments: [ { view: this.smaaSceneColorView, loadOp: "load", storeOp: "store" } ], depthStencilAttachment: { view: this.depthView, depthLoadOp: "load", depthStoreOp: "store" }, ...timestampEndWrites ? { timestampWrites: timestampEndWrites } : {} }); this.executeTransparentMergedDrawList(transparentPass); transparentPass.end(); } if (timestampWrites && this.gpuResolveBuffer && this.gpuResultBuffer) { encoder.resolveQuerySet(this.gpuQuerySet, 0, 2, this.gpuResolveBuffer, 0); if (this.gpuResultBuffer.mapState === "unmapped") { encoder.copyBufferToBuffer(this.gpuResolveBuffer, 0, this.gpuResultBuffer, 0, 16); this.gpuResultPending = true; } } this.executeSmaa(encoder, swapView); } else { if (hasTransmission) this.ensureTransmissionTargets(true); const sceneColorView = hasTransmission ? this.transmissionSceneColorView : swapView; const pass = encoder.beginRenderPass({ colorAttachments: [ { view: sceneColorView, clearValue: { r: scene.background[0], g: scene.background[1], b: scene.background[2], a: 1 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: this.depthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" }, ...hasTransmission && timestampBeginWrites ? { timestampWrites: timestampBeginWrites } : timestampWrites ? { timestampWrites } : {} }); this.executeDrawList(pass, this.opaqueDrawList); this.executeGlyphFieldDrawList(pass, this.opaqueGlyphFieldDrawList); this.executePointCloudDrawList(pass, this.opaquePointCloudDrawList); this.executeNodeLinkDrawList(pass, this.opaqueNodeLinkDrawList); this.executeLatticeSpaceDrawList(pass, this.opaqueLatticeSpaceDrawList); if (!hasTransmission) this.executeTransparentMergedDrawList(pass); pass.end(); if (hasTransmission) { encoder.copyTextureToTexture( { texture: this.transmissionSceneColorTexture }, { texture: this.transmissionSourceTexture }, { width: this.width, height: this.height, depthOrArrayLayers: 1 } ); const transparentPass = encoder.beginRenderPass({ colorAttachments: [ { view: this.transmissionSceneColorView, loadOp: "load", storeOp: "store" } ], depthStencilAttachment: { view: this.depthView, depthLoadOp: "load", depthStoreOp: "store" }, ...timestampEndWrites ? { timestampWrites: timestampEndWrites } : {} }); this.executeTransparentMergedDrawList(transparentPass); transparentPass.end(); encoder.copyTextureToTexture( { texture: this.transmissionSceneColorTexture }, { texture: swapTexture }, { width: this.width, height: this.height, depthOrArrayLayers: 1 } ); } if (timestampWrites && this.gpuResolveBuffer && this.gpuResultBuffer) { encoder.resolveQuerySet(this.gpuQuerySet, 0, 2, this.gpuResolveBuffer, 0); if (this.gpuResultBuffer.mapState === "unmapped") { encoder.copyBufferToBuffer(this.gpuResolveBuffer, 0, this.gpuResultBuffer, 0, 16); this.gpuResultPending = true; } } } this.queue.submit([encoder.finish()]); this.tryReadGpuTiming(); if (this.occlusionCullingEnabled) this.captureOcclusionHierarchy(camera); } warmup(scene, camera) { this.resize(); this.prepareSceneFrameBase(scene, camera, true); if (this.occlusionCullingEnabled) this.ensureOcclusionResources(); const hasTransmission = this.hasOpticalTransmissionDrawItems(); if (hasTransmission) this.ensureTransmissionTargets(!this.smaaEnabled); this.shadowRenderer.warmup(); this.warmMeshDrawList(this.opaqueDrawList); this.warmMeshDrawList(this.transparentDrawList); this.warmPointCloudDrawList(this.opaquePointCloudDrawList); this.warmPointCloudDrawList(this.transparentPointCloudDrawList); this.warmSplatFieldDrawList(this.transparentSplatFieldDrawList); this.warmGlyphFieldDrawList(this.opaqueGlyphFieldDrawList); this.warmGlyphFieldDrawList(this.transparentGlyphFieldDrawList); this.warmNodeLinkDrawList(this.opaqueNodeLinkDrawList); this.warmNodeLinkDrawList(this.transparentNodeLinkDrawList); this.warmLatticeSpaceDrawList(this.opaqueLatticeSpaceDrawList); this.warmLatticeSpaceDrawList(this.transparentLatticeSpaceDrawList); } schedulePick(run) { const task = this.pickTail.then(run, run); this.pickTail = task.then(() => void 0, () => void 0); return task; } pick(scene, camera, x, y, _opts = {}) { return this.schedulePick(() => runPick(this, scene, camera, x, y)); } pickRect(scene, camera, x0, y0, x1, y1, opts = {}) { return this.schedulePick(() => runPickRect(this, scene, camera, x0, y0, x1, y1, opts)); } pickLasso(scene, camera, points, opts = {}) { return this.schedulePick(() => runPickLasso(this, scene, camera, points, opts)); } destroy() { if (this.destroyed) return; destroyCullingScratch(this); this.destroyed = true; this.shadowRenderer.destroy(); this.effects.destroy(); this.destroyOcclusionTextures(); this.depthTexture?.destroy(); this.smaaSceneColorTexture?.destroy(); this.smaaEdgesTexture?.destroy(); this.smaaBlendTexture?.destroy(); this.transmissionSceneColorTexture?.destroy(); this.transmissionSourceTexture?.destroy(); this.smaaSceneColorTexture = null; this.smaaSceneColorView = null; this.smaaEdgesTexture = null; this.smaaEdgesView = null; this.smaaBlendTexture = null; this.smaaBlendView = null; this.transmissionSceneColorTexture = null; this.transmissionSceneColorView = null; this.transmissionSourceTexture = null; this.transmissionSourceView = null; this.transmissionSourceRevision++; this.smaaParamsBuffer?.destroy(); this.smaaParamsBuffer = null; this.smaaEdgeBindGroup = null; this.smaaWeightBindGroup = null; this.smaaNeighborhoodBindGroup = null; this.smaaEdgePipeline = null; this.smaaWeightPipeline = null; this.smaaNeighborhoodPipeline = null; this.smaaShaderModule = null; this.smaaEdgeBindGroupLayout = null; this.smaaWeightBindGroupLayout = null; this.smaaNeighborhoodBindGroupLayout = null; this.smaaSamplerPoint = null; this.smaaSamplerLinear = null; this.fallbackWhiteTexture?.destroy(); this.fallbackNormalTexture?.destroy(); this.fallbackMRTex?.destroy(); this.fallbackOcclusionTex?.destroy(); this.fallbackAnisotropyTexture?.destroy(); this.cameraUniformBuffer?.destroy(); this.modelUniformBuffer?.destroy(); this.modelUniformBuffer = null; this.modelUniformBufferCapacity = 0; this.modelUniformBindGroup = null; this.modelUniformSlots.clear(); for (const buffer of this.pickUniformBuffers) buffer.destroy(); this.pickUniformBuffers = []; this.pickBindGroups = []; this.pickBindGroupLayout = null; this.pickIdTexture?.destroy(); this.pickDepthTexture?.destroy(); this.pickDepthPayloadTexture?.destroy(); this.pickIdTexture = null; this.pickIdView = null; this.pickDepthTexture = null; this.pickDepthView = null; this.pickDepthPayloadTexture = null; this.pickDepthPayloadView = null; this.pickIdReadbackBuffer?.destroy(); this.pickDepthReadbackBuffer?.destroy(); this.pickIdReadbackBuffer = null; this.pickDepthReadbackBuffer = null; this.pickIdReadbackCapacityBytes = 0; this.pickDepthReadbackCapacityBytes = 0; this.pickTail = Promise.resolve(); for (const slot of this.occlusionReadbackSlots) { slot.buffer?.destroy(); slot.buffer = null; slot.capacityBytes = 0; slot.pending = null; slot.metadata = null; slot.data = null; slot.state = "idle"; } this.occlusionReadbackSlots = []; this.occlusionReduceBindGroups.clear(); this.occlusionReduceBindGroupLayout = null; this.occlusionReducePipeline = null; this.latestOcclusionHierarchy = null; this.latestOcclusionHierarchySerial = 0; if (this.occlusionHierarchyWasmPtr) wasm.freeF32(this.occlusionHierarchyWasmPtr, this.occlusionHierarchyWasmLength); this.occlusionHierarchyWasmPtr = 0; this.occlusionHierarchyWasmLength = 0; this.occlusionHierarchyWasmSerial = 0; this.pendingOcclusionFrameState = null; this.lightingUniformBuffer?.destroy(); this.instanceBuffer?.destroy(); this.instanceBuffer = null; this.instanceBufferCapacityBytes = 0; this.instanceRunCache.length = 0; this.pipelineCache.clear(); this.computePipelineCache.clear(); this.shaderCache.clear(); this.pointCloudBindGroupLayout = null; this.pointCloudDummyColorsBuffer?.destroy(); this.pointCloudDummyColorsBuffer = null; this.splatFieldBindGroupLayout = null; this.splatFieldDummySHBuffer?.destroy(); this.splatFieldDummySHBuffer = null; for (const [field, state] of this.splatFieldSortStates) this.destroySplatFieldSortState(field, state); this.splatFieldSortStates.clear(); this.splatSortKeyA?.destroy(); this.splatSortKeyB?.destroy(); this.splatSortIndexA?.destroy(); this.splatSortIndexB?.destroy(); this.splatSortPrefix?.destroy(); this.splatSortKeyA = null; this.splatSortKeyB = null; this.splatSortIndexA = null; this.splatSortIndexB = null; this.splatSortPrefix = null; this.splatSortCapacity = 0; for (const level of this.splatSortScanLevels) { level.blockSums?.destroy(); level.blockOffsets?.destroy(); } this.splatSortScanLevels = []; this.splatSortKeygenBindGroupLayout = null; this.splatSortFlagsBindGroupLayout = null; this.splatSortScanBlockBindGroupLayout = null; this.splatSortScanAddBindGroupLayout = null; this.splatSortScatterBindGroupLayout = null; this.glyphFieldBindGroupLayout = null; this.nodeLinkBindGroupLayout = null; this.glyphFieldDummyAttributesBuffer?.destroy(); this.glyphFieldDummyAttributesBuffer = null; this.nodeLinkDummyF32Buffer?.destroy(); this.nodeLinkDummyU32Buffer?.destroy(); this.nodeLinkDummyF32Buffer = null; this.nodeLinkDummyU32Buffer = null; this.latticeSpaceDummyF32Buffer?.destroy(); this.latticeSpaceDummyU32Buffer?.destroy(); this.latticeSpaceDummyF32Buffer = null; this.latticeSpaceDummyU32Buffer = null; this.latticeSpaceBindGroupLayout = null; for (const [space, state] of this.latticeSpaceSortStates) destroyLatticeSpaceSortState(this, space, state); this.latticeSpaceSortStates.clear(); for (const buffer of [this.latticeSortKeyA, this.latticeSortKeyB, this.latticeSortIndexA, this.latticeSortIndexB, this.latticeSortPrefix]) buffer?.destroy(); this.latticeSortKeyA = null; this.latticeSortKeyB = null; this.latticeSortIndexA = null; this.latticeSortIndexB = null; this.latticeSortPrefix = null; this.latticeSortCapacity = 0; for (const level of this.latticeSortScanLevels) { level.blockSums?.destroy(); level.blockOffsets?.destroy(); } this.latticeSortScanLevels = []; this.latticeSortKeygenBindGroupLayout = null; this.latticeSortFlagsBindGroupLayout = null; this.latticeSortScanBlockBindGroupLayout = null; this.latticeSortScanAddBindGroupLayout = null; this.latticeSortScatterBindGroupLayout = null; this.nodeLinkSphereGeometry = null; this.nodeLinkCubeGeometry = null; this.nodeLinkCylinderGeometry = null; this.gpuQuerySet?.destroy(); this.gpuQuerySet = null; this.gpuResolveBuffer?.destroy(); this.gpuResolveBuffer = null; this.gpuResultBuffer?.destroy(); this.gpuResultBuffer = null; this.gpuResultPending = false; this._gpuTimeNs = null; this.dataMaterialDummyDataBuffer?.destroy(); this.dataMaterialDummyDataBuffer = null; this.objectsById.clear(); this.objectIds = /* @__PURE__ */ new WeakMap(); this.nextObjectId = 1; try { this.context?.unconfigure?.(); } catch { } try { this.device?.destroy?.(); } catch { } } createGlobalBindGroupLayout() { createGlobalBindGroupLayout(this); } createSkinBindGroupLayout() { createSkinBindGroupLayout(this); } createUniformBuffers() { createUniformBuffers(this); } getPickBindGroupLayout() { return getPickBindGroupLayout(this); } prepareFrameModelUniforms(prepareAllModels = false) { const ptrs = this.modelUniformPtrScratch; ptrs.length = 0; for (let i = 0; i < this.opaqueDrawList.length; ) { const first = this.opaqueDrawList[i]; let end = i + 1; while (end < this.opaqueDrawList.length) { const item = this.opaqueDrawList[end]; if (item.pipeline !== first.pipeline || item.material !== first.material || item.vertexSourceId !== first.vertexSourceId) break; end++; } const instanced = !prepareAllModels && !this.occlusionCullingEnabled && end - i > 1 && !first.skinned && !hasMeshMorphRuntime(first.mesh) && this.materialSupportsInstancing(first.material); if (!instanced) for (let j = i; j < end; j++) ptrs.push(this.opaqueDrawList[j].mesh.transform.worldMatrixPtr); i = end; } for (const item of this.transparentDrawList) ptrs.push(item.mesh.transform.worldMatrixPtr); for (const item of this.opaquePointCloudDrawList) ptrs.push(item.cloud.transform.worldMatrixPtr); for (const item of this.transparentPointCloudDrawList) ptrs.push(item.cloud.transform.worldMatrixPtr); for (const item of this.transparentSplatFieldDrawList) ptrs.push(item.field.transform.worldMatrixPtr); for (const item of this.opaqueGlyphFieldDrawList) ptrs.push(item.field.transform.worldMatrixPtr); for (const item of this.transparentGlyphFieldDrawList) ptrs.push(item.field.transform.worldMatrixPtr); for (const item of this.opaqueNodeLinkDrawList) ptrs.push(item.link.transform.worldMatrixPtr); for (const item of this.transparentNodeLinkDrawList) ptrs.push(item.link.transform.worldMatrixPtr); for (const item of this.opaqueLatticeSpaceDrawList) ptrs.push(item.space.transform.worldMatrixPtr); for (const item of this.transparentLatticeSpaceDrawList) ptrs.push(item.space.transform.worldMatrixPtr); prepareModelUniforms(this, ptrs); } createFallbackTextures() { createFallbackTextures(this); } createSmaaResources() { createSmaaResources(this); } resizeSmaaTargets() { resizeSmaaTargets(this); } ensureTransmissionTargets(needSceneTarget) { ensureTransmissionTargets(this, needSceneTarget); } resizeTransmissionTargets(needSceneTarget) { resizeTransmissionTargets(this, needSceneTarget); } unprojectDepth(camera, px, py, depth) { const x = (px + 0.5) / Math.max(1, this.width) * 2 - 1; const y = 1 - (py + 0.5) / Math.max(1, this.height) * 2; const z = depth; const inv = mat4.invert(camera.viewProjectionMatrix); const wx = inv[0] * x + inv[4] * y + inv[8] * z + inv[12]; const wy = inv[1] * x + inv[5] * y + inv[9] * z + inv[13]; const wz = inv[2] * x + inv[6] * y + inv[10] * z + inv[14]; const ww = inv[3] * x + inv[7] * y + inv[11] * z + inv[15]; if (!Number.isFinite(ww) || Math.abs(ww) <= 1e-8) return [0, 0, 0]; return [wx / ww, wy / ww, wz / ww]; } executeSmaa(encoder, outputView) { executeSmaa(this, encoder, outputView); } writeCameraUniforms(camera) { writeCameraUniforms(this, camera); } writeLightingUniforms(scene) { writeLightingUniforms(this, scene); } recordFrustumCounts(tested, visible) { recordFrustumCounts(this, tested, visible); } destroyOcclusionTextures() { destroyOcclusionTextures(this); } invalidateOcclusionResources() { invalidateOcclusionResources(this); } ensureOcclusionResources() { ensureOcclusionResources(this); } buildOcclusionFrameState() { return buildOcclusionFrameState(this); } getValidOcclusionHierarchy(camera, signature) { return getValidOcclusionHierarchy(this, camera, signature); } applyOcclusionFiltering(camera, candidates, hierarchy) { applyOcclusionFiltering(this, camera, candidates, hierarchy); } buildDrawLists(scene, camera) { buildDrawLists(this, scene, camera); } isOpticallyTransmissiveMaterial(material) { return isOpticallyTransmissiveMaterial(material); } hasOpticalTransmissionDrawItems() { return hasOpticalTransmissionDrawItems(this); } buildPointCloudDrawLists(scene, camera) { buildPointCloudDrawLists(this, scene); } buildSplatFieldDrawLists(scene, camera) { buildSplatFieldDrawLists(this, scene, camera); } buildGlyphFieldDrawLists(scene, camera) { buildGlyphFieldDrawLists(this, scene, camera); } getNodeLinkNodeGeometry(mode) { return getNodeLinkNodeGeometry(this, mode); } getNodeLinkLinkGeometry() { return getNodeLinkLinkGeometry(this); } buildNodeLinkDrawLists(scene, camera) { buildNodeLinkDrawLists(this, scene, camera); } buildLatticeSpaceDrawLists(scene, camera) { buildLatticeSpaceDrawLists(this, scene, camera); } captureOcclusionHierarchy(camera) { captureOcclusionHierarchy(this, camera); } warmMeshDrawList(items) { warmMeshDrawList(this, items); } warmSkinResources(skin) { warmSkinResources(this, skin); } warmInstancedRunResources(items, start, count) { warmInstancedRunResources(this, items, start, count); } warmPointCloudDrawList(items) { warmPointCloudDrawList(this, items); } warmSplatFieldDrawList(items) { warmSplatFieldDrawList(this, items); } warmGlyphFieldDrawList(items) { warmGlyphFieldDrawList(this, items); } warmNodeLinkDrawList(items) { warmNodeLinkDrawList(this, items); } warmLatticeSpaceDrawList(items) { warmLatticeSpaceDrawList(this, items); } executeDrawList(pass, items) { executeDrawList(this, pass, items); } executePointCloudDrawList(pass, items) { executePointCloudDrawList(this, pass, items); } executeSplatFieldDrawList(pass, items) { executeSplatFieldDrawList(this, pass, items); } executeGlyphFieldDrawList(pass, list) { executeGlyphFieldDrawList(this, pass, list); } executeNodeLinkDrawList(pass, list) { executeNodeLinkDrawList(this, pass, list); } executeLatticeSpaceDrawList(pass, list) { executeLatticeSpaceDrawList(this, pass, list); } executeTransparentMergedDrawList(pass) { executeTransparentMergedDrawList(this, pass); } drawInstancedRun(pass, geometry, material, items, start, count) { drawInstancedRun(this, pass, geometry, material, items, start, count); } getOrCreatePipeline(material, instanced = false, skinned = false, skinned8 = false, mirrored = false, forceNoDepthWrite = false, receiveShadow = false) { return getOrCreatePipeline(this, material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite, receiveShadow); } getPipelineCacheKey(material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite = false, receiveShadow = false) { return getPipelineCacheKey(this, material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite, receiveShadow); } isMirroredWorldMatrix(storeF32, base) { return isMirroredWorldMatrix(this, storeF32, base); } getBlendState(mode) { return getBlendState(this, mode); } getCullMode(mode) { return getCullMode(this, mode); } getPremultipliedAlphaBlendState() { return getPremultipliedAlphaBlendState(this); } bindSizedBuffer(buffer, size, offset = 0) { return bindSizedBuffer(this, buffer, size, offset); } getOrCreateShaderModule(code) { return getOrCreateShaderModule(this, code); } getMaterialBindGroupKey(material) { return getMaterialBindGroupKey(this, material); } ensureMaterialBindGroup(material) { ensureMaterialBindGroup(this, material); } materialSupportsInstancing(material) { return materialSupportsInstancing(this, material); } materialSupportsSkinning(material) { return materialSupportsSkinning(this, material); } ensureInstanceBuffer(byteLength) { ensureInstanceBuffer(this, byteLength); } getOrCreateSplatFieldSortState(field) { return getOrCreateSplatFieldSortState(this, field); } destroySplatFieldSortState(field, state) { destroySplatFieldSortState(this, field, state); } ensureSplatSortCapacity(count) { ensureSplatSortCapacity(this, count); } ensureSplatSortScanLevel(level, count) { return ensureSplatSortScanLevel(this, level, count); } ensureSplatSortFrameCapacity(count, level = 0) { ensureSplatSortFrameCapacity(this, count, level); } getSplatFieldBindGroupLayout() { return getSplatFieldBindGroupLayout(this); } getOrCreateSplatFieldPipeline() { return getOrCreateSplatFieldPipeline(this); } getSplatFieldBindGroupKey(field, state) { return getSplatFieldBindGroupKey(this, field, state); } ensureSplatFieldBindGroup(field) { ensureSplatFieldBindGroup(this, field); } getSplatSortKeygenBindGroupLayout() { return getSplatSortKeygenBindGroupLayout(this); } getSplatSortFlagsBindGroupLayout() { return getSplatSortFlagsBindGroupLayout(this); } getSplatSortScanBlockBindGroupLayout() { return getSplatSortScanBlockBindGroupLayout(this); } getSplatSortScanAddBindGroupLayout() { return getSplatSortScanAddBindGroupLayout(this); } getSplatSortScatterBindGroupLayout() { return getSplatSortScatterBindGroupLayout(this); } getOrCreateSplatSortKeygenPipeline() { return getOrCreateSplatSortKeygenPipeline(this); } getOrCreateSplatSortFlagsPipeline(bit) { return getOrCreateSplatSortFlagsPipeline(this, bit); } getOrCreateSplatSortScanBlockPipeline() { return getOrCreateSplatSortScanBlockPipeline(this); } getOrCreateSplatSortScanAddPipeline() { return getOrCreateSplatSortScanAddPipeline(this); } getOrCreateSplatSortScatterPipeline(bit) { return getOrCreateSplatSortScatterPipeline(this, bit); } encodeSplatSortScanExclusive(pass, input, count, out, level = 0) { encodeSplatSortScanExclusive(this, pass, input, count, out, level); } encodeSplatFieldSort(pass, field, state) { return encodeSplatFieldSort(this, pass, field, state); } encodeSplatFieldSorts(encoder) { encodeSplatFieldSorts(this, encoder); } encodeLatticeSpaceSorts(encoder) { encodeLatticeSpaceSorts(this, encoder); } getPointCloudBindGroupLayout() { return getPointCloudBindGroupLayout(this); } getPointCloudPipelineCacheKey(cloud) { return getPointCloudPipelineCacheKey(this, cloud); } getOrCreatePointCloudPipeline(cloud) { return getOrCreatePointCloudPipeline(this, cloud); } getPointCloudBindGroupKey(cloud) { return getPointCloudBindGroupKey(this, cloud); } ensurePointCloudBindGroup(cloud) { ensurePointCloudBindGroup(this, cloud); } getGlyphFieldBindGroupLayout() { return getGlyphFieldBindGroupLayout(this); } getOrCreateGlyphFieldPipeline(field) { return getOrCreateGlyphFieldPipeline(this, field); } getGlyphFieldBindGroupKey(field) { return getGlyphFieldBindGroupKey(this, field); } ensureGlyphFieldBindGroup(field) { ensureGlyphFieldBindGroup(this, field); } getNodeLinkBindGroupLayout() { return getNodeLinkBindGroupLayout(this); } getNodeLinkPipelineCacheKey(link, passKind) { return getNodeLinkPipelineCacheKey(this, link, passKind); } getOrCreateNodeLinkPipeline(link, passKind) { return getOrCreateNodeLinkPipeline(this, link, passKind); } getNodeLinkBindGroupKey(link) { return getNodeLinkBindGroupKey(this, link); } ensureNodeLinkBindGroup(link) { ensureNodeLinkBindGroup(this, link); } }; // typescript/core/stats.ts var RollingAverage = class { constructor(capacity) { this.capacity = capacity; this.values = new Float64Array(Math.max(1, capacity | 0)); } capacity; values; cursor = 0; count = 0; total = 0; addSample(v) { if (!Number.isFinite(v) || v < 0) return; const i = this.cursor; this.total -= this.values[i]; this.values[i] = v; this.total += v; this.cursor = (i + 1) % this.values.length; if (this.count < this.values.length) this.count++; } get() { return this.count > 0 ? this.total / this.count : 0; } }; var formatNumber = (x, decimals) => { if (!Number.isFinite(x)) return "n/a"; const d = Math.max(0, decimals | 0); return x.toFixed(d); }; var formatBytes = (bytes, decimals) => { if (!Number.isFinite(bytes)) return "n/a"; const abs = Math.abs(bytes); if (abs < 1024) return `${formatNumber(bytes, 0)} B`; if (abs < 1024 * 1024) return `${formatNumber(bytes / 1024, decimals)} KiB`; return `${formatNumber(bytes / (1024 * 1024), decimals)} MiB`; }; var PerformanceStats = class { element; textEl; graphCanvas; graphCtx; sources; fpsAvg; frameMsAvg; cpuMsAvg; gpuMsAvg; history; historyCursor = 0; targetFps; updateIntervalMs; decimals; lastTextUpdateMs = 0; lastDtSeconds = 0; show; label; constructor(sources = {}, desc = {}) { if (typeof document === "undefined") throw new Error("PerformanceStats requires a DOM environment (document is undefined)."); this.sources = sources; this.targetFps = Math.max(1, desc.targetFps ?? 60); this.updateIntervalMs = Math.max(0, desc.updateIntervalMs ?? 250); this.decimals = Math.max(0, desc.decimals ?? 1); const historyLength = Math.max(4, desc.historyLength ?? 60) | 0; this.history = new Float32Array(historyLength); const avgWindow = Math.max(1, Math.min(240, historyLength)); this.fpsAvg = new RollingAverage(avgWindow); this.frameMsAvg = new RollingAverage(avgWindow); this.cpuMsAvg = new RollingAverage(avgWindow); this.gpuMsAvg = new RollingAverage(avgWindow); this.show = { showFps: desc.showFps ?? true, showFrameTime: desc.showFrameTime ?? true, showCpuTime: desc.showCpuTime ?? true, showGpuTime: desc.showGpuTime ?? true, showMemory: desc.showMemory ?? true, showCulling: desc.showCulling ?? false, graph: desc.graph ?? true }; this.label = desc.label ?? null; const canvas = desc.canvas ?? null; const parent = desc.parent ?? canvas?.parentElement ?? document.body; const position = desc.position ?? "top-left"; const paddingPx = Math.max(0, desc.paddingPx ?? 8); const zIndex = (desc.zIndex ?? 9999) | 0; const pointerEvents = desc.pointerEvents ?? "none"; const el = document.createElement("div"); this.element = el; el.style.position = parent === document.body || parent === document.documentElement ? "fixed" : "absolute"; el.style.zIndex = String(zIndex); el.style.pointerEvents = pointerEvents; el.style.padding = `${paddingPx}px`; el.style.background = "rgba(0, 0, 0, 0.75)"; el.style.color = "#ffffff"; el.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"; el.style.fontSize = "12px"; el.style.lineHeight = "1.2"; el.style.whiteSpace = "pre"; el.style.userSelect = "none"; if (el.style.position === "absolute") { const cs = getComputedStyle(parent); if (cs.position === "static") parent.style.position = "relative"; } if (position.includes("top")) el.style.top = "0"; if (position.includes("bottom")) el.style.bottom = "0"; if (position.includes("left")) el.style.left = "0"; if (position.includes("right")) el.style.right = "0"; const textEl = document.createElement("pre"); this.textEl = textEl; textEl.style.margin = "0"; textEl.style.padding = "0"; textEl.style.whiteSpace = "pre"; el.appendChild(textEl); if (this.show.graph) { const gw = Math.max(32, desc.graphWidthPx ?? 120) | 0; const gh = Math.max(16, desc.graphHeightPx ?? 40) | 0; const gc = document.createElement("canvas"); gc.style.display = "block"; gc.style.marginTop = "6px"; gc.style.width = `${gw}px`; gc.style.height = `${gh}px`; const dpr = Math.max(1, globalThis.devicePixelRatio || 1); gc.width = Math.max(1, Math.floor(gw * dpr)); gc.height = Math.max(1, Math.floor(gh * dpr)); const ctx = gc.getContext("2d"); if (ctx) ctx.scale(dpr, dpr); this.graphCanvas = gc; this.graphCtx = ctx; el.appendChild(gc); } else { this.graphCanvas = null; this.graphCtx = null; } parent.appendChild(el); this.refreshText(); } update(dtSeconds, cpuFrameMs = 0) { this.lastDtSeconds = dtSeconds; const frameMs = dtSeconds * 1e3; const fps = dtSeconds > 0 ? 1 / dtSeconds : 0; this.fpsAvg.addSample(fps); this.frameMsAvg.addSample(frameMs); this.cpuMsAvg.addSample(cpuFrameMs); const gpuNs = this.sources.getGpuTimeNs?.() ?? null; if (gpuNs !== null && Number.isFinite(gpuNs)) this.gpuMsAvg.addSample(gpuNs / 1e6); this.history[this.historyCursor] = fps; this.historyCursor = (this.historyCursor + 1) % this.history.length; this.drawGraph(); const time = nowMs(); if (this.updateIntervalMs === 0 || time - this.lastTextUpdateMs >= this.updateIntervalMs) { this.lastTextUpdateMs = time; this.refreshText(); } } destroy() { this.element.remove(); } drawGraph() { if (!this.graphCanvas || !this.graphCtx) return; const ctx = this.graphCtx; const w = parseFloat(this.graphCanvas.style.width) || 120; const h = parseFloat(this.graphCanvas.style.height) || 40; ctx.clearRect(0, 0, w, h); const n = this.history.length; const barW = w / n; const scale = h / this.targetFps; for (let i = 0; i < n; i++) { const idx = (this.historyCursor + i) % n; const fps = this.history[idx]; const barH = clamp(fps * scale, 0, h); const x = i * barW; const y = h - barH; ctx.fillStyle = "rgba(255, 255, 255, 0.85)"; ctx.fillRect(x, y, Math.max(1, barW - 0.5), barH); } ctx.strokeStyle = "rgba(255, 255, 255, 0.35)"; ctx.strokeRect(0.5, 0.5, w - 1, h - 1); } refreshText() { const d = this.decimals; const lines = []; if (this.label) lines.push(this.label); const fpsAvg = this.fpsAvg.get(); const frameMsAvg = this.frameMsAvg.get(); const hz = frameMsAvg > 0 ? 1e3 / frameMsAvg : 0; const cpuMsAvg = this.cpuMsAvg.get(); if (this.show.showFps) lines.push(`FPS: ${formatNumber(fpsAvg, 1)}`); if (this.show.showFrameTime) lines.push(`Frame: ${formatNumber(frameMsAvg, d)} ms (\u2248${formatNumber(hz, 0)} Hz)`); if (this.show.showCpuTime) { const denom = Math.max(1e-4, this.lastDtSeconds) * 1e3; const load = clamp(cpuMsAvg / denom * 100, 0, 1e3); lines.push(`CPU: ${formatNumber(cpuMsAvg, d)} ms (${formatNumber(load, 0)}%)`); } if (this.show.showGpuTime) { const gpuNs = this.sources.getGpuTimeNs?.() ?? null; if (gpuNs === null || !Number.isFinite(gpuNs)) { lines.push("GPU: n/a"); } else { const gpuAvg = this.gpuMsAvg.get(); const denom = Math.max(1e-4, this.lastDtSeconds) * 1e3; const load = clamp(gpuAvg / denom * 100, 0, 1e3); lines.push(`GPU: ${formatNumber(gpuAvg, d)} ms (${formatNumber(load, 0)}%)`); } } if (this.show.showMemory) { try { const used = frameArena.usedBytes(); const cap = frameArena.capBytes(); lines.push(`Frame arena: ${formatBytes(used, d)} / ${formatBytes(cap, d)}`); } catch { } try { const memBytes = wasm.memory().buffer.byteLength; lines.push(`WASM memory: ${formatBytes(memBytes, d)}`); } catch { } const pm = typeof performance !== "undefined" ? performance.memory : null; if (pm && typeof pm.usedJSHeapSize === "number") { const used = pm.usedJSHeapSize; const total = typeof pm.totalJSHeapSize === "number" ? pm.totalJSHeapSize : NaN; if (Number.isFinite(total)) lines.push(`JS heap: ${formatBytes(used, d)} / ${formatBytes(total, d)}`); else lines.push(`JS heap: ${formatBytes(used, d)}`); } } if (this.show.showCulling) { const stats = this.sources.getCullingStats?.() ?? null; if (stats) { lines.push(`Frustum: visible ${stats.frustum.visible} / tested ${stats.frustum.tested}`); lines.push(`Occlusion: visible ${stats.occlusion.visible} / tested ${stats.occlusion.tested} / occluded ${stats.occlusion.occluded}`); } } this.textEl.textContent = lines.join("\n"); } }; // typescript/compute/pipeline.ts var ComputePipeline = class { device; shaderCode; entryPoint; constants; pipeline; bindGroupLayouts; label; constructor(device, desc) { this.device = device; this.shaderCode = desc.code; this.entryPoint = desc.entryPoint ?? "main"; this.constants = desc.constants; this.label = desc.label ?? null; const module = device.createShaderModule({ code: desc.code }); if (desc.bindGroups && desc.bindGroups.length > 0) { const layouts = desc.bindGroups.map((group, index) => { const normalized = normalizeBindGroupLayout(group, `ComputePipeline bind group ${index}`); return device.createBindGroupLayout({ label: normalized.label, entries: normalized.entries }); }); const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: layouts }); this.pipeline = device.createComputePipeline({ label: desc.label, layout: pipelineLayout, compute: { module, entryPoint: this.entryPoint, constants: this.constants } }); this.bindGroupLayouts = layouts; } else { this.pipeline = device.createComputePipeline({ label: desc.label, layout: "auto", compute: { module, entryPoint: this.entryPoint, constants: this.constants } }); this.bindGroupLayouts = []; } } getBindGroupLayout(groupIndex) { if (this.bindGroupLayouts.length > 0) { const layout = this.bindGroupLayouts[groupIndex]; assert(!!layout, `Bind group layout ${groupIndex} not found (pipeline has ${this.bindGroupLayouts.length} explicit groups)`); return layout; } return this.pipeline.getBindGroupLayout(groupIndex); } createBindGroup(groupIndex, resources, label) { const layout = this.getBindGroupLayout(groupIndex); const entries = normalizeBindGroupResources(resources, `ComputePipeline bind group ${groupIndex}`); return this.device.createBindGroup({ label, layout, entries }); } }; // typescript/compute/workgroups.ts var makeWorkgroupSize = (x, y = 1, z = 1) => { assert(isPositiveInt(x), `workgroupSize.x must be a positive integer (got ${x})`); assert(isPositiveInt(y), `workgroupSize.y must be a positive integer (got ${y})`); assert(isPositiveInt(z), `workgroupSize.z must be a positive integer (got ${z})`); return [x, y, z]; }; var makeWorkgroupCounts = (x, y = 1, z = 1) => { assert(isNonNegativeInt(x), `workgroups.x must be an integer >= 0 (got ${x})`); assert(isNonNegativeInt(y), `workgroups.y must be an integer >= 0 (got ${y})`); assert(isNonNegativeInt(z), `workgroups.z must be an integer >= 0 (got ${z})`); return [x, y, z]; }; var workgroups1D = (invocations, workgroupSizeX) => { assert(Number.isFinite(invocations), `invocations must be finite (got ${invocations})`); assert(invocations >= 0, `invocations must be >= 0 (got ${invocations})`); assert(isPositiveInt(workgroupSizeX), `workgroupSizeX must be a positive integer (got ${workgroupSizeX})`); if (invocations === 0) return [0, 1, 1]; const x = ceilDiv(invocations, workgroupSizeX); return [x, 1, 1]; }; var workgroups2D = (width, height, workgroupSizeX, workgroupSizeY) => { assert(Number.isFinite(width) && Number.isFinite(height), "width/height must be finite"); assert(width >= 0 && height >= 0, `width/height must be >= 0 (got ${width}x${height})`); assert(isPositiveInt(workgroupSizeX), `workgroupSizeX must be a positive integer (got ${workgroupSizeX})`); assert(isPositiveInt(workgroupSizeY), `workgroupSizeY must be a positive integer (got ${workgroupSizeY})`); if (width === 0 || height === 0) return [0, 1, 1]; const x = ceilDiv(width, workgroupSizeX); const y = ceilDiv(height, workgroupSizeY); return [x, y, 1]; }; var workgroups3D = (width, height, depth, workgroupSizeX, workgroupSizeY, workgroupSizeZ) => { assert(Number.isFinite(width) && Number.isFinite(height) && Number.isFinite(depth), "width/height/depth must be finite"); assert(width >= 0 && height >= 0 && depth >= 0, `width/height/depth must be >= 0 (got ${width}x${height}x${depth})`); assert(isPositiveInt(workgroupSizeX), `workgroupSizeX must be a positive integer (got ${workgroupSizeX})`); assert(isPositiveInt(workgroupSizeY), `workgroupSizeY must be a positive integer (got ${workgroupSizeY})`); assert(isPositiveInt(workgroupSizeZ), `workgroupSizeZ must be a positive integer (got ${workgroupSizeZ})`); if (width === 0 || height === 0 || depth === 0) return [0, 1, 1]; const x = ceilDiv(width, workgroupSizeX); const y = ceilDiv(height, workgroupSizeY); const z = ceilDiv(depth, workgroupSizeZ); return [x, y, z]; }; // typescript/compute/dispatch.ts var normalizeWorkgroups = (w) => { if (Array.isArray(w)) { const x2 = w[0] ?? 0; const y2 = w[1] ?? 1; const z2 = w[2] ?? 1; assert(isNonNegativeInt(x2), `workgroups.x must be an integer >= 0 (got ${x2})`); assert(isNonNegativeInt(y2), `workgroups.y must be an integer >= 0 (got ${y2})`); assert(isNonNegativeInt(z2), `workgroups.z must be an integer >= 0 (got ${z2})`); return { x: x2, y: y2, z: z2 }; } const x = w.x; const y = w.y ?? 1; const z = w.z ?? 1; assert(isNonNegativeInt(x), `workgroups.x must be an integer >= 0 (got ${x})`); assert(isNonNegativeInt(y), `workgroups.y must be an integer >= 0 (got ${y})`); assert(isNonNegativeInt(z), `workgroups.z must be an integer >= 0 (got ${z})`); return { x, y, z }; }; var validateWorkgroupsForDevice = (device, workgroups) => validateWorkgroups(workgroups, device.limits.maxComputeWorkgroupsPerDimension); var validateWorkgroups = (w, maxWorkgroupsPerDimension) => { const obj = w, x = Array.isArray(w) ? w[0] ?? 0 : obj.x, y = Array.isArray(w) ? w[1] ?? 1 : obj.y ?? 1, z = Array.isArray(w) ? w[2] ?? 1 : obj.z ?? 1; assert(isNonNegativeInt(x), `workgroups.x must be an integer >= 0 (got ${x})`); assert(isNonNegativeInt(y), `workgroups.y must be an integer >= 0 (got ${y})`); assert(isNonNegativeInt(z), `workgroups.z must be an integer >= 0 (got ${z})`); if (maxWorkgroupsPerDimension !== void 0) assert(x <= maxWorkgroupsPerDimension && y <= maxWorkgroupsPerDimension && z <= maxWorkgroupsPerDimension, `dispatchWorkgroups exceeds device.limits.maxComputeWorkgroupsPerDimension (${maxWorkgroupsPerDimension})`); }; var resolvePipeline = (p) => p instanceof ComputePipeline ? p.pipeline : p; var encodeDispatch = (encoder, cmd) => { const pass = encoder.beginComputePass({ label: cmd.label }); const pipeline = resolvePipeline(cmd.pipeline); pass.setPipeline(pipeline); if (cmd.bindGroups) { for (let i = 0; i < cmd.bindGroups.length; i++) { const bg = cmd.bindGroups[i]; if (bg) pass.setBindGroup(i, bg); } } const w = cmd.workgroups, obj = w, x = Array.isArray(w) ? w[0] ?? 0 : obj.x, y = Array.isArray(w) ? w[1] ?? 1 : obj.y ?? 1, z = Array.isArray(w) ? w[2] ?? 1 : obj.z ?? 1; assert(isNonNegativeInt(x), `workgroups.x must be an integer >= 0 (got ${x})`); assert(isNonNegativeInt(y), `workgroups.y must be an integer >= 0 (got ${y})`); assert(isNonNegativeInt(z), `workgroups.z must be an integer >= 0 (got ${z})`); if (x > 0 && y > 0 && z > 0) pass.dispatchWorkgroups(x, y, z); pass.end(); }; var encodeDispatchBatchWithLimit = (encoder, commands, label, maxWorkgroupsPerDimension) => { for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) validateWorkgroups(commands[commandIndex].workgroups, maxWorkgroupsPerDimension); const pass = encoder.beginComputePass({ label }); let lastPipelineSource = null; let lastPipeline = null; let lastBindGroupsSource = null; const lastBindGroups = []; let lastWorkgroupsSource = null; let lastX = 0, lastY = 1, lastZ = 1; for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) { const cmd = commands[commandIndex]; const pipeline = cmd.pipeline === lastPipelineSource && lastPipeline !== null ? lastPipeline : resolvePipeline(cmd.pipeline); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastBindGroupsSource = null; lastBindGroups.length = 0; } lastPipelineSource = cmd.pipeline; if (cmd.bindGroups && cmd.bindGroups !== lastBindGroupsSource) { for (let i = 0; i < cmd.bindGroups.length; i++) { const bg = cmd.bindGroups[i]; if (bg && bg !== lastBindGroups[i]) { pass.setBindGroup(i, bg); lastBindGroups[i] = bg; } } lastBindGroupsSource = cmd.bindGroups; } const w = cmd.workgroups; if (w !== lastWorkgroupsSource) { const obj = w; lastX = Array.isArray(w) ? w[0] ?? 0 : obj.x; lastY = Array.isArray(w) ? w[1] ?? 1 : obj.y ?? 1; lastZ = Array.isArray(w) ? w[2] ?? 1 : obj.z ?? 1; lastWorkgroupsSource = w; } const x = lastX; const y = lastY; const z = lastZ; if (x === 0 || y === 0 || z === 0) continue; if (cmd.label) pass.pushDebugGroup(cmd.label); pass.dispatchWorkgroups(x, y, z); if (cmd.label) pass.popDebugGroup(); } pass.end(); }; // typescript/compute/scratch.ts var ScratchBufferPool = class { device; usage; labelPrefix; buffersBySize = /* @__PURE__ */ new Map(); cursorBySize = /* @__PURE__ */ new Map(); constructor(device, opts) { this.device = device; this.usage = opts.usage; this.labelPrefix = opts.labelPrefix ?? "scratch"; } acquire(byteLength, label) { assert(Number.isInteger(byteLength) && byteLength >= 0, `ScratchBufferPool.acquire: byteLength must be an integer >= 0 (got ${byteLength})`); const size = Math.max(4, alignTo(byteLength, 4)); let list = this.buffersBySize.get(size); if (!list) { list = []; this.buffersBySize.set(size, list); } const cursor = this.cursorBySize.get(size) ?? 0; let buf; if (cursor < list.length) { buf = list[cursor]; } else { const baseLabel = label ? `${this.labelPrefix}:${label}` : `${this.labelPrefix}:${size}`; const indexedLabel = cursor === 0 ? baseLabel : `${baseLabel}:${cursor}`; buf = this.device.createBuffer({ label: indexedLabel, size, usage: this.usage }); list.push(buf); } this.cursorBySize.set(size, cursor + 1); return buf; } reset() { for (const size of this.cursorBySize.keys()) this.cursorBySize.set(size, 0); } destroy() { for (const list of this.buffersBySize.values()) for (const buf of list) buf.destroy(); this.buffersBySize.clear(); this.cursorBySize.clear(); } }; // wgsl/compute/reduce-max-f32.wgsl var reduce_max_f32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = -0x1.fffffep+127f; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = max(acc, input[i1]); } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = max(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/reduce-max-u32.wgsl var reduce_max_u32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = 0u; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = max(acc, input[i1]); } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = max(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/reduce-min-f32.wgsl var reduce_min_f32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = 0x1.fffffep+127f; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = min(acc, input[i1]); } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = min(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/reduce-min-u32.wgsl var reduce_min_u32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = 0xFFFFFFFFu; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = min(acc, input[i1]); } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = min(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/reduce-sum-f32.wgsl var reduce_sum_f32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = 0.0; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = acc + input[i1]; } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = share[tid] + share[tid + stride]; } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/reduce-sum-u32.wgsl var reduce_sum_u32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var acc = 0u; if (i0 < n) { acc = input[i0]; } if (i1 < n) { acc = acc + input[i1]; } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = share[tid] + share[tid + stride]; } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/argreduce-argmax-initial.wgsl var argreduce_argmax_initial_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; struct Pair { value: f32, index: u32, } @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; fn is_nan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalid_pair() -> Pair { return Pair(-0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let a_nan = is_nan(a.value); let b_nan = is_nan(b.value); if (a_nan && b_nan) { if (a.index <= b.index) { return a; } return b; } if (a_nan) { return b; } if (b_nan) { return a; } if (a.value > b.value) { return a; } if (b.value > a.value) { return b; } if (a.index <= b.index) { return a; } return b; } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var a = invalid_pair(); var b = invalid_pair(); if (i0 < n) { a = Pair(input[i0], i0); } if (i1 < n) { b = Pair(input[i1], i1); } share[tid] = better(a, b); workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = better(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/argreduce-argmax-pairs.wgsl var argreduce_argmax_pairs_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; struct Pair { value: f32, index: u32, } @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; fn is_nan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalid_pair() -> Pair { return Pair(-0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let a_nan = is_nan(a.value); let b_nan = is_nan(b.value); if (a_nan && b_nan) { if (a.index <= b.index) { return a; } return b; } if (a_nan) { return b; } if (b_nan) { return a; } if (a.value > b.value) { return a; } if (b.value > a.value) { return b; } if (a.index <= b.index) { return a; } return b; } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var a = invalid_pair(); var b = invalid_pair(); if (i0 < n) { a = input[i0]; } if (i1 < n) { b = input[i1]; } share[tid] = better(a, b); workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = better(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/argreduce-argmin-initial.wgsl var argreduce_argmin_initial_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; struct Pair { value: f32, index: u32, } @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; fn is_nan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalid_pair() -> Pair { return Pair(0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let a_nan = is_nan(a.value); let b_nan = is_nan(b.value); if (a_nan && b_nan) { if (a.index <= b.index) { return a; } return b; } if (a_nan) { return b; } if (b_nan) { return a; } if (a.value < b.value) { return a; } if (b.value < a.value) { return b; } if (a.index <= b.index) { return a; } return b; } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var a = invalid_pair(); var b = invalid_pair(); if (i0 < n) { a = Pair(input[i0], i0); } if (i1 < n) { b = Pair(input[i1], i1); } share[tid] = better(a, b); workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = better(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/argreduce-argmin-pairs.wgsl var argreduce_argmin_pairs_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; struct Pair { value: f32, index: u32, } @group(0) @binding(0) var input: array; @group(0) @binding(1) var output: array; var share: array; fn is_nan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalid_pair() -> Pair { return Pair(0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let a_nan = is_nan(a.value); let b_nan = is_nan(b.value); if (a_nan && b_nan) { if (a.index <= b.index) { return a; } return b; } if (a_nan) { return b; } if (b_nan) { return a; } if (a.value < b.value) { return a; } if (b.value < a.value) { return b; } if (a.index <= b.index) { return a; } return b; } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&input); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; var a = invalid_pair(); var b = invalid_pair(); if (i0 < n) { a = input[i0]; } if (i1 < n) { b = input[i1]; } share[tid] = better(a, b); workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = better(share[tid], share[tid + stride]); } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/histogram-clear-atomic-u32.wgsl var histogram_clear_atomic_u32_default = "@group(0) @binding(0) var bins: array>; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; if (i < arrayLength(&bins)) { atomicStore(&bins[i], 0u); } }"; // wgsl/compute/histogram-u32.wgsl var histogram_u32_default = "@group(0) @binding(0) var keys: array; @group(0) @binding(1) var bins: array>; var local_bins: array, 256>; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keys); if (i >= n) { return; } let k = keys[i]; let b = arrayLength(&bins); if (k < b) { _ = atomicAdd(&bins[k], 1u); } } @compute @workgroup_size(256) fn main_local_256( @builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3, ) { let tid = lid.x; atomicStore(&local_bins[tid], 0u); workgroupBarrier(); let n = arrayLength(&keys); let b = arrayLength(&bins); let base = wid.x * 1024u + tid; for (var j = 0u; j < 4u; j++) { let i = base + j * 256u; if (i < n) { let key = keys[i]; if (key < b) { _ = atomicAdd(&local_bins[key], 1u); } } } workgroupBarrier(); if (tid < b) { _ = atomicAdd(&bins[tid], atomicLoad(&local_bins[tid])); } }"; // wgsl/compute/compact-f32.wgsl var compact_f32_default = "@group(0) @binding(0) var input: array; @group(0) @binding(1) var flags: array; @group(0) @binding(2) var prefix: array; @group(0) @binding(3) var output: array; @group(0) @binding(4) var output_count: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&flags); if (i >= n) { return; } if (flags[i] != 0u) { let dst = prefix[i]; output[dst] = input[i]; } if (i + 1u == n) { output_count[0] = prefix[i] + flags[i]; } }"; // wgsl/compute/compact-u32.wgsl var compact_u32_default = "@group(0) @binding(0) var input: array; @group(0) @binding(1) var flags: array; @group(0) @binding(2) var prefix: array; @group(0) @binding(3) var output: array; @group(0) @binding(4) var output_count: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&flags); if (i >= n) { return; } if (flags[i] != 0u) { let dst = prefix[i]; output[dst] = input[i]; } if (i + 1u == n) { output_count[0] = prefix[i] + flags[i]; } }"; // wgsl/compute/sort-radix-flags-u32.wgsl var sort_radix_flags_u32_default = "override BIT: u32 = 0u; const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_INVOCATION: u32 = 4u; const ELEMENTS_PER_WORKGROUP: u32 = WORKGROUP_SIZE * ELEMENTS_PER_INVOCATION; @group(0) @binding(0) var keys: array; @group(0) @binding(1) var prefix: array; @group(0) @binding(2) var block_sums: array; var temp: array; fn zero_bit(i: u32, n: u32) -> u32 { if (i >= n) { return 0u; } return select(0u, 1u, ((keys[i] >> BIT) & 1u) == 0u); } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let n = arrayLength(&keys); let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid * ELEMENTS_PER_INVOCATION; let i1 = i0 + 1u; let i2 = i0 + 2u; let i3 = i0 + 3u; let v0 = zero_bit(i0, n); let v1 = zero_bit(i1, n); let v2 = zero_bit(i2, n); let v3 = zero_bit(i3, n); temp[tid] = v0 + v1 + v2 + v3; var offset = 1u; var d = WORKGROUP_SIZE / 2u; loop { workgroupBarrier(); if (d == 0u) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; temp[bi] = temp[bi] + temp[ai]; } offset = offset * 2u; d = d / 2u; } if (tid == 0u) { block_sums[wid.x] = temp[WORKGROUP_SIZE - 1u]; temp[WORKGROUP_SIZE - 1u] = 0u; } d = 1u; loop { offset = offset / 2u; workgroupBarrier(); if (d >= WORKGROUP_SIZE) { break; } if (tid < d) { let ai = offset * ((tid * 2u) + 1u) - 1u; let bi = offset * ((tid * 2u) + 2u) - 1u; let t = temp[ai]; temp[ai] = temp[bi]; temp[bi] = temp[bi] + t; } d = d * 2u; } workgroupBarrier(); let thread_offset = temp[tid]; if (i0 < n) { prefix[i0] = thread_offset; } if (i1 < n) { prefix[i1] = thread_offset + v0; } if (i2 < n) { prefix[i2] = thread_offset + v0 + v1; } if (i3 < n) { prefix[i3] = thread_offset + v0 + v1 + v2; } }"; // wgsl/compute/sort-radix-scatter-u32.wgsl var sort_radix_scatter_u32_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keys_in: array; @group(0) @binding(1) var prefix: array; @group(0) @binding(2) var keys_out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keys_in); if (i >= n) { return; } let k = keys_in[i]; let is_zero = ((k >> BIT) & 1u) == 0u; let zero_pos = prefix[i]; let last_key = keys_in[n - 1u]; let z = prefix[n - 1u] + select(0u, 1u, ((last_key >> BIT) & 1u) == 0u); let one_pos = z + (i - zero_pos); let dst = select(one_pos, zero_pos, is_zero); keys_out[dst] = k; }"; // wgsl/compute/sort-radix-scatter-pairs-u32.wgsl var sort_radix_scatter_pairs_u32_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keys_in: array; @group(0) @binding(1) var values_in: array; @group(0) @binding(2) var prefix: array; @group(0) @binding(3) var keys_out: array; @group(0) @binding(4) var values_out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keys_in); if (i >= n) { return; } let k = keys_in[i]; let is_zero = ((k >> BIT) & 1u) == 0u; let zero_pos = prefix[i]; let last_key = keys_in[n - 1u]; let z = prefix[n - 1u] + select(0u, 1u, ((last_key >> BIT) & 1u) == 0u); let one_pos = z + (i - zero_pos); let dst = select(one_pos, zero_pos, is_zero); keys_out[dst] = k; values_out[dst] = values_in[i]; }"; // wgsl/compute/copy-f32.wgsl var copy_f32_default = "@group(0) @binding(0) var src: array; @group(0) @binding(1) var dst: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&dst); if (i < n) { dst[i] = src[i]; } }"; // wgsl/compute/copy-u32.wgsl var copy_u32_default = "@group(0) @binding(0) var src: array; @group(0) @binding(1) var dst: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&dst); if (i < n) { dst[i] = src[i]; } }"; // wgsl/compute/scale-extract-f32.wgsl var scale_extract_f32_default = "struct Params { count: u32, component_count: u32, component_index: u32, value_mode: u32, stride: u32, offset: u32, _pad0: u32, _pad1: u32, } @group(0) @binding(0) var src: array; @group(0) @binding(1) var out_values: array; @group(0) @binding(2) var out_flags: array; @group(0) @binding(3) var params: Params; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; if (i >= params.count) { return; } let cc = max(1u, min(4u, params.component_count)); let ci = min(3u, params.component_index); let base = i * max(1u, params.stride) + params.offset; var x: f32 = src[base + 0u]; var y: f32 = 0.0; var z: f32 = 0.0; var w: f32 = 0.0; if (cc > 1u) { y = src[base + 1u]; } if (cc > 2u) { z = src[base + 2u]; } if (cc > 3u) { w = src[base + 3u]; } let raw = scale_select_value(vec4(x, y, z, w), cc, ci, params.value_mode); if (scale_is_finite(raw)) { out_values[i] = raw; out_flags[i] = 1u; } else { out_values[i] = 0.0; out_flags[i] = 0u; } }"; // wgsl/compute/scale-histogram-f32.wgsl var scale_histogram_f32_default = "struct Params { count: u32, bin_count: u32, min_value: f32, max_value: f32, } @group(0) @binding(0) var values: array; @group(0) @binding(1) var bins: array>; @group(0) @binding(2) var params: Params; var local_bins: array, 256>; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; if (i >= params.count) { return; } if (params.bin_count == 0u) { return; } let min_value = params.min_value; let max_value = params.max_value; if (!(max_value > min_value)) { return; } let v = values[i]; if (!scale_is_finite(v)) { return; } let t = clamp((v - min_value) / (max_value - min_value), 0.0, 0.99999994); let b = min(params.bin_count - 1u, u32(t * f32(params.bin_count))); atomicAdd(&bins[b], 1u); } @compute @workgroup_size(256) fn main_local_256( @builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3, ) { let tid = lid.x; atomicStore(&local_bins[tid], 0u); workgroupBarrier(); if (params.bin_count > 0u && params.max_value > params.min_value) { let base = wid.x * 1024u + tid; for (var j = 0u; j < 4u; j++) { let i = base + j * 256u; if (i < params.count) { let v = values[i]; if (scale_is_finite(v)) { let t = clamp( (v - params.min_value) / (params.max_value - params.min_value), 0.0, 0.99999994 ); let b = min(params.bin_count - 1u, u32(t * f32(params.bin_count))); _ = atomicAdd(&local_bins[b], 1u); } } } } workgroupBarrier(); if (tid < params.bin_count) { _ = atomicAdd(&bins[tid], atomicLoad(&local_bins[tid])); } }"; // wgsl/compute/scale-remap-f32.wgsl var scale_remap_f32_default = "struct Params { count: u32, _pad0: u32, _pad1: u32, _pad2: u32, domain: vec4, clamp_config: vec4, scale_params: vec4, scale_flags: vec4, } @group(0) @binding(0) var values: array; @group(0) @binding(1) var out_values: array; @group(0) @binding(2) var params: Params; fn scale_is_nan(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn scale_is_inf(v: f32) -> bool { let u = bitcast(v); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) == 0u; } fn scale_is_finite(v: f32) -> bool { return !scale_is_nan(v) && !scale_is_inf(v); } fn scale_clamp01(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn scale_log_base(x: f32, base: f32) -> f32 { let b = max(base, 1.000001); return log(x) / log(b); } fn scale_apply_mode(x: f32, mode_id: u32, linthresh: f32, base: f32) -> f32 { if (mode_id == 0u) { return x; } if (mode_id == 1u) { return scale_log_base(max(x, 1e-20), base); } let lt = max(linthresh, 1e-20); let s = select(-1.0, 1.0, x >= 0.0); let y = scale_log_base(1.0 + abs(x) / lt, base); return s * y; } fn scale_select_value( v: vec4, component_count_in: u32, component_index_in: u32, value_mode: u32, ) -> f32 { let component_count = max(1u, min(4u, component_count_in)); let component_index = min(3u, component_index_in); if (value_mode == 1u) { if (component_count == 1u) { return abs(v.x); } if (component_count == 2u) { return length(v.xy); } if (component_count == 3u) { return length(v.xyz); } return length(v); } if (component_index == 0u) { return v.x; } if (component_index == 1u) { return v.y; } if (component_index == 2u) { return v.z; } return v.w; } fn scale_apply_transform( raw_value: f32, domain: vec4, clamp_config: vec4, params: vec4, flags: vec4, ) -> f32 { if (!scale_is_finite(raw_value)) { return 0.0; } var v = raw_value; let clamp_mode = u32(domain.w + 0.5); let clamp_min = clamp_config.x; let clamp_max = clamp_config.y; if (clamp_mode != 0u && clamp_max > clamp_min) { v = clamp(v, clamp_min, clamp_max); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clamp_max > clamp_min) { d0 = clamp_min; d1 = clamp_max; } let mode_id = u32(params.x + 0.5); let base = params.y; let linthresh = params.z; let gamma = max(params.w, 1e-6); let a = scale_apply_mode(d0, mode_id, linthresh, base); let b = scale_apply_mode(d1, mode_id, linthresh, base); let x = scale_apply_mode(v, mode_id, linthresh, base); let denom = max(1e-20, b - a); var t = scale_clamp01((x - a) / denom); t = pow(t, gamma); if (flags.x > 0.5) { t = 1.0 - t; } return scale_clamp01(t); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; if (i >= params.count) { return; } let v = values[i]; if (!scale_is_finite(v)) { out_values[i] = 0.0; return; } out_values[i] = scale_apply_transform( v, params.domain, params.clamp_config, params.scale_params, params.scale_flags, ); }"; // wgsl/compute/add-f32.wgsl var add_f32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] + b[id.x]; } }"; // wgsl/compute/add-u32.wgsl var add_u32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] + b[id.x]; } }"; // wgsl/compute/add-c64.wgsl var add_c64_default = "@group(0) @binding(0) var a: array>; @group(0) @binding(1) var b: array>; @group(0) @binding(2) var out: array>; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] + b[id.x]; } }"; // wgsl/compute/sub-f32.wgsl var sub_f32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] - b[id.x]; } }"; // wgsl/compute/sub-u32.wgsl var sub_u32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] - b[id.x]; } }"; // wgsl/compute/sub-c64.wgsl var sub_c64_default = "@group(0) @binding(0) var a: array>; @group(0) @binding(1) var b: array>; @group(0) @binding(2) var out: array>; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] - b[id.x]; } }"; // wgsl/compute/mul-f32.wgsl var mul_f32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] * b[id.x]; } }"; // wgsl/compute/mul-u32.wgsl var mul_u32_default = "@group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var out: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = a[id.x] * b[id.x]; } }"; // wgsl/compute/mul-c64.wgsl var mul_c64_default = "@group(0) @binding(0) var a: array>; @group(0) @binding(1) var b: array>; @group(0) @binding(2) var out: array>; fn cmul(x: vec2, y: vec2) -> vec2 { return vec2(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = cmul(a[id.x], b[id.x]); } }"; // wgsl/compute/scl-f32.wgsl var scl_f32_default = "@group(0) @binding(0) var input: array; @group(0) @binding(1) var out: array; @group(0) @binding(2) var params: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = bitcast(params[0]) * input[id.x]; } }"; // wgsl/compute/scl-u32.wgsl var scl_u32_default = "@group(0) @binding(0) var input: array; @group(0) @binding(1) var out: array; @group(0) @binding(2) var params: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = params[0] * input[id.x]; } }"; // wgsl/compute/scl-c64.wgsl var scl_c64_default = "@group(0) @binding(0) var input: array>; @group(0) @binding(1) var out: array>; @group(0) @binding(2) var params: array; fn cmul(x: vec2, y: vec2) -> vec2 { return vec2(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = cmul(vec2(bitcast(params[0]), bitcast(params[1])), input[id.x]); } }"; // wgsl/compute/dot-f32.wgsl var dot_f32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; let n = arrayLength(&a); var acc = 0.0; if (i0 < n) { acc = a[i0] * b[i0]; } if (i1 < n) { acc = acc + a[i1] * b[i1]; } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = share[tid] + share[tid + stride]; } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/dot-u32.wgsl var dot_u32_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var output: array; var share: array; @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let tid = lid.x; let base = wid.x * ELEMENTS_PER_WORKGROUP; let i0 = base + tid; let i1 = i0 + WORKGROUP_SIZE; let n = arrayLength(&a); var acc = 0u; if (i0 < n) { acc = a[i0] * b[i0]; } if (i1 < n) { acc = acc + a[i1] * b[i1]; } share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = share[tid] + share[tid + stride]; } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid.x] = share[0]; } }"; // wgsl/compute/dot-c64.wgsl var dot_c64_default = "const WORKGROUP_SIZE: u32 = 256u; const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var a: array>; @group(0) @binding(1) var b: array>; @group(0) @binding(2) var output: array>; var share: array, 256>; fn cmul(x: vec2, y: vec2) -> vec2 { return vec2(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x); } fn reduce_and_write(tid: u32, wid: u32, acc: vec2) { share[tid] = acc; workgroupBarrier(); var stride = WORKGROUP_SIZE / 2u; loop { if (stride == 0u) { break; } if (tid < stride) { share[tid] = share[tid] + share[tid + stride]; } workgroupBarrier(); stride = stride / 2u; } if (tid == 0u) { output[wid] = share[0]; } } @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let i0 = wid.x * ELEMENTS_PER_WORKGROUP + lid.x; let i1 = i0 + WORKGROUP_SIZE; let n = arrayLength(&a); var acc = vec2(0.0); if (i0 < n) { acc = cmul(a[i0], b[i0]); } if (i1 < n) { acc = acc + cmul(a[i1], b[i1]); } reduce_and_write(lid.x, wid.x, acc); } @compute @workgroup_size(256) fn reduce(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { let i0 = wid.x * ELEMENTS_PER_WORKGROUP + lid.x; let i1 = i0 + WORKGROUP_SIZE; let n = arrayLength(&a); var acc = vec2(0.0); if (i0 < n) { acc = a[i0]; } if (i1 < n) { acc = acc + a[i1]; } reduce_and_write(lid.x, wid.x, acc); }"; // wgsl/compute/axpy-f32.wgsl var axpy_f32_default = "@group(0) @binding(0) var x: array; @group(0) @binding(1) var y: array; @group(0) @binding(2) var out: array; @group(0) @binding(3) var params: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = bitcast(params[0]) * x[id.x] + y[id.x]; } }"; // wgsl/compute/axpy-u32.wgsl var axpy_u32_default = "@group(0) @binding(0) var x: array; @group(0) @binding(1) var y: array; @group(0) @binding(2) var out: array; @group(0) @binding(3) var params: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { out[id.x] = params[0] * x[id.x] + y[id.x]; } }"; // wgsl/compute/axpy-c64.wgsl var axpy_c64_default = "@group(0) @binding(0) var x: array>; @group(0) @binding(1) var y: array>; @group(0) @binding(2) var out: array>; @group(0) @binding(3) var params: array; fn cmul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) id: vec3) { if (id.x < arrayLength(&out)) { let alpha = vec2(bitcast(params[0]), bitcast(params[1])); out[id.x] = cmul(alpha, x[id.x]) + y[id.x]; } }"; // wgsl/compute/gemm-f32.wgsl var gemm_f32_default = "const TILE: u32 = 16u; @group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var c: array; @group(0) @binding(3) var params: array; var tile_a: array; var tile_b: array; @compute @workgroup_size(16, 16, 1) fn main(@builtin(workgroup_id) wg: vec3, @builtin(local_invocation_id) lid: vec3) { let m = params[0]; let n = params[1]; let k = params[2]; let row = wg.y * TILE + lid.y; let col = wg.x * TILE + lid.x; let local = lid.y * TILE + lid.x; var acc = 0.0; for (var base = 0u; base < k; base = base + TILE) { let ak = base + lid.x; let bk = base + lid.y; tile_a[local] = select(0.0, a[row * k + ak], row < m && ak < k); tile_b[local] = select(0.0, b[bk * n + col], bk < k && col < n); workgroupBarrier(); for (var t = 0u; t < TILE; t = t + 1u) { acc = acc + tile_a[lid.y * TILE + t] * tile_b[t * TILE + lid.x]; } workgroupBarrier(); } if (row < m && col < n) { let alpha = bitcast(params[3]); let beta = bitcast(params[4]); let old = select(0.0, c[row * n + col], beta != 0.0); c[row * n + col] = alpha * acc + beta * old; } }"; // wgsl/compute/gemm-u32.wgsl var gemm_u32_default = "const TILE: u32 = 16u; @group(0) @binding(0) var a: array; @group(0) @binding(1) var b: array; @group(0) @binding(2) var c: array; @group(0) @binding(3) var params: array; var tile_a: array; var tile_b: array; @compute @workgroup_size(16, 16, 1) fn main(@builtin(workgroup_id) wg: vec3, @builtin(local_invocation_id) lid: vec3) { let m = params[0]; let n = params[1]; let k = params[2]; let row = wg.y * TILE + lid.y; let col = wg.x * TILE + lid.x; let local = lid.y * TILE + lid.x; var acc = 0u; for (var base = 0u; base < k; base = base + TILE) { let ak = base + lid.x; let bk = base + lid.y; tile_a[local] = select(0u, a[row * k + ak], row < m && ak < k); tile_b[local] = select(0u, b[bk * n + col], bk < k && col < n); workgroupBarrier(); for (var t = 0u; t < TILE; t = t + 1u) { acc = acc + tile_a[lid.y * TILE + t] * tile_b[t * TILE + lid.x]; } workgroupBarrier(); } if (row < m && col < n) { let beta = params[4]; let old = select(0u, c[row * n + col], beta != 0u); c[row * n + col] = params[3] * acc + beta * old; } }"; // wgsl/compute/gemm-c64.wgsl var gemm_c64_default = "const TILE: u32 = 16u; @group(0) @binding(0) var a: array>; @group(0) @binding(1) var b: array>; @group(0) @binding(2) var c: array>; @group(0) @binding(3) var params: array; var tile_a: array, 256>; var tile_b: array, 256>; fn cmul(x: vec2, y: vec2) -> vec2 { return vec2(x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x); } @compute @workgroup_size(16, 16, 1) fn main(@builtin(workgroup_id) wg: vec3, @builtin(local_invocation_id) lid: vec3) { let m = params[0]; let n = params[1]; let k = params[2]; let row = wg.y * TILE + lid.y; let col = wg.x * TILE + lid.x; let local = lid.y * TILE + lid.x; var acc = vec2(0.0); for (var base = 0u; base < k; base = base + TILE) { let ak = base + lid.x; let bk = base + lid.y; tile_a[local] = select(vec2(0.0), a[row * k + ak], row < m && ak < k); tile_b[local] = select(vec2(0.0), b[bk * n + col], bk < k && col < n); workgroupBarrier(); for (var t = 0u; t < TILE; t = t + 1u) { acc = acc + cmul(tile_a[lid.y * TILE + t], tile_b[t * TILE + lid.x]); } workgroupBarrier(); } if (row < m && col < n) { let alpha = vec2(bitcast(params[3]), bitcast(params[4])); let beta = vec2(bitcast(params[5]), bitcast(params[6])); var old = vec2(0.0); if (beta.x != 0.0 || beta.y != 0.0) { old = c[row * n + col]; } c[row * n + col] = cmul(alpha, acc) + cmul(beta, old); } }"; // wgsl/compute/lu-factor-f32.wgsl var lu_factor_f32_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var matrices: array; @group(0) @binding(2) var ipiv: array; var wg_abs: array; var wg_row: array; var pivot_row: u32; @compute @workgroup_size(128, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let stride = params.elems_per_matrix; let base = b * stride; let base_ipiv = b * n; for (var kk = 0u; kk < n; kk = kk + 1u) { var pv = -1.0; var pr = kk; var ii = kk + lid; while (ii < n) { let aik = matrices[base + ii * n + kk]; let av = abs(aik); if (av > pv || (av == pv && ii < pr)) { pv = av; pr = ii; } ii = ii + 128u; } wg_abs[lid] = pv; wg_row[lid] = pr; workgroupBarrier(); var s = 64u; while (s > 0u) { if (lid < s) { let i1 = lid + s; if (i1 < 128u) { let av0 = wg_abs[lid]; let av1 = wg_abs[i1]; let r0 = wg_row[lid]; let r1 = wg_row[i1]; if (av1 > av0 || (av1 == av0 && r1 < r0)) { wg_abs[lid] = av1; wg_row[lid] = r1; } } } workgroupBarrier(); s = s >> 1u; } if (lid == 0u) { pivot_row = wg_row[0]; ipiv[base_ipiv + kk] = pivot_row; } workgroupBarrier(); let piv = pivot_row; var jj = lid; while (jj < n) { let ia = base + kk * n + jj; let ib = base + piv * n + jj; let va = matrices[ia]; let vb = matrices[ib]; matrices[ia] = vb; matrices[ib] = va; jj = jj + 128u; } workgroupBarrier(); let p = matrices[base + kk * n + kk]; let col_len = n - kk - 1u; var t = lid; while (t < col_len) { let i = kk + 1u + t; let ik = base + i * n + kk; matrices[ik] = matrices[ik] / p; t = t + 128u; } workgroupBarrier(); let dim = n - kk - 1u; let total = dim * dim; t = lid; while (t < total) { let ii = t / dim; let jj2 = t % dim; let i = kk + 1u + ii; let j = kk + 1u + jj2; let lik = matrices[base + i * n + kk]; let ukj = matrices[base + kk * n + j]; let ij = base + i * n + j; matrices[ij] = matrices[ij] - lik * ukj; t = t + 128u; } workgroupBarrier(); } }"; // wgsl/compute/lu-factor-lead-f32.wgsl var lu_factor_lead_f32_default = "const WG_SIZE: u32 = 128u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array; @group(0) @binding(2) var ipiv: array; var wg_abs: array; var wg_row: array; var pivot_row: u32; @compute @workgroup_size(WG_SIZE, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let base = b * params.elems_per_matrix; let base_ipiv = b * n; let kk = params.kk; let pw = params.pw; for (var j: u32 = 0u; j < pw; j = j + 1u) { let col = kk + j; if (col >= n) { break; } var pv: f32 = -1.0; var pr: u32 = col; var ii = col + lid; while (ii < n) { let aik = matrices[base + ii * n + col]; let av = abs(aik); if (av > pv || (av == pv && ii < pr)) { pv = av; pr = ii; } ii = ii + WG_SIZE; } wg_abs[lid] = pv; wg_row[lid] = pr; workgroupBarrier(); var s: u32 = WG_SIZE >> 1u; while (s > 0u) { if (lid < s) { let i1 = lid + s; let av0 = wg_abs[lid]; let av1 = wg_abs[i1]; let r0 = wg_row[lid]; let r1 = wg_row[i1]; if (av1 > av0 || (av1 == av0 && r1 < r0)) { wg_abs[lid] = av1; wg_row[lid] = r1; } } workgroupBarrier(); s = s >> 1u; } if (lid == 0u) { pivot_row = wg_row[0]; ipiv[base_ipiv + col] = pivot_row; } workgroupBarrier(); let piv = pivot_row; var jj: u32 = lid; while (jj < n) { let ia = base + col * n + jj; let ib = base + piv * n + jj; let va = matrices[ia]; let vb = matrices[ib]; matrices[ia] = vb; matrices[ib] = va; jj = jj + WG_SIZE; } workgroupBarrier(); let pivval = matrices[base + col * n + col]; var t: u32 = col + 1u + lid; while (t < n) { let idx = base + t * n + col; matrices[idx] = matrices[idx] / pivval; t = t + WG_SIZE; } workgroupBarrier(); let endc = min(n, kk + pw); let inner_cols = endc - (col + 1u); if (inner_cols > 0u) { let inner_rows = n - (col + 1u); let total = inner_rows * inner_cols; var u: u32 = lid; while (u < total) { let row_t = u / inner_cols; let col_t = u % inner_cols; let i = col + 1u + row_t; let c = col + 1u + col_t; let lik = matrices[base + i * n + col]; let ucj = matrices[base + col * n + c]; let idx = base + i * n + c; matrices[idx] = matrices[idx] - lik * ucj; u = u + WG_SIZE; } workgroupBarrier(); } } }"; // wgsl/compute/lu-factor-upper-f32.wgsl var lu_factor_upper_f32_default = "const MAX_PANEL_B: u32 = 16u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array; @compute @workgroup_size(256, 1, 1) fn main(@builtin(global_invocation_id) gid: vec3) { let n = params.n; let kk = params.kk; let pw = params.pw; let trail_n = n - (kk + pw); if (trail_n == 0u || pw == 0u) { return; } let idx = gid.x; let b = idx / trail_n; if (b >= params.batch_count) { return; } let j = idx - b * trail_n; let col = (kk + pw) + j; let base = b * params.elems_per_matrix; var u_col: array; for (var i: u32 = 0u; i < pw; i = i + 1u) { let row = kk + i; var sum_v = matrices[base + row * n + col]; for (var r: u32 = 0u; r < i; r = r + 1u) { let l_val = matrices[base + row * n + (kk + r)]; sum_v = sum_v - l_val * u_col[r]; } u_col[i] = sum_v; matrices[base + row * n + col] = sum_v; } }"; // wgsl/compute/lu-factor-trailing-f32.wgsl var lu_factor_trailing_f32_default = "const TILE_M: u32 = 16u; const TILE_N: u32 = 8u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array; var l_tile: array; var u_tile: array; @compute @workgroup_size(TILE_M, TILE_N, 1) fn main( @builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_id) lid: vec3, @builtin(local_invocation_index) lid_idx: u32, ) { let b = wg_id.z; if (b >= params.batch_count) { return; } let n = params.n; let kk = params.kk; let pw = params.pw; let base = b * params.elems_per_matrix; let m_dim = n - (kk + pw); let n_dim = n - (kk + pw); if (m_dim == 0u || n_dim == 0u || pw == 0u) { return; } let global_i = wg_id.y * TILE_M + lid.x; let global_j = wg_id.x * TILE_N + lid.y; let valid = (global_i < m_dim) && (global_j < n_dim); var acc = 0.0; for (var k: u32 = 0u; k < pw; k = k + 1u) { if (lid_idx < TILE_M) { let i_g = wg_id.y * TILE_M + lid_idx; if (i_g < m_dim) { let row = (kk + pw) + i_g; l_tile[lid_idx] = matrices[base + row * n + (kk + k)]; } else { l_tile[lid_idx] = 0.0; } } else if (lid_idx < TILE_M + TILE_N) { let j_local = lid_idx - TILE_M; let j_g = wg_id.x * TILE_N + j_local; if (j_g < n_dim) { let col = (kk + pw) + j_g; u_tile[j_local] = matrices[base + (kk + k) * n + col]; } else { u_tile[j_local] = 0.0; } } workgroupBarrier(); acc = acc + l_tile[lid.x] * u_tile[lid.y]; workgroupBarrier(); } if (valid) { let row = (kk + pw) + global_i; let col = (kk + pw) + global_j; let idx = base + row * n + col; matrices[idx] = matrices[idx] - acc; } }"; // wgsl/compute/lu-factor-c64.wgsl var lu_factor_c64_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var matrices: array>; @group(0) @binding(2) var ipiv: array; var wg_abs: array; var wg_row: array; var pivot_row: u32; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } fn cx_div(a: vec2, b: vec2) -> vec2 { let d = b.x * b.x + b.y * b.y; return vec2((a.x * b.x + a.y * b.y) / d, (a.y * b.x - a.x * b.y) / d); } fn cx_magsq(z: vec2) -> f32 { return z.x * z.x + z.y * z.y; } @compute @workgroup_size(128, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let stride = params.elems_per_matrix; let base = b * stride; let base_ipiv = b * n; for (var kk = 0u; kk < n; kk = kk + 1u) { var pv = -1.0; var pr = kk; var ii = kk + lid; while (ii < n) { let aik = matrices[base + ii * n + kk]; let ms = cx_magsq(aik); if (ms > pv || (ms == pv && ii < pr)) { pv = ms; pr = ii; } ii = ii + 128u; } wg_abs[lid] = pv; wg_row[lid] = pr; workgroupBarrier(); var s = 64u; while (s > 0u) { if (lid < s) { let i1 = lid + s; if (i1 < 128u) { let av0 = wg_abs[lid]; let av1 = wg_abs[i1]; let r0 = wg_row[lid]; let r1 = wg_row[i1]; if (av1 > av0 || (av1 == av0 && r1 < r0)) { wg_abs[lid] = av1; wg_row[lid] = r1; } } } workgroupBarrier(); s = s >> 1u; } if (lid == 0u) { pivot_row = wg_row[0]; ipiv[base_ipiv + kk] = pivot_row; } workgroupBarrier(); let piv = pivot_row; var jj = lid; while (jj < n) { let ia = base + kk * n + jj; let ib = base + piv * n + jj; let va = matrices[ia]; let vb = matrices[ib]; matrices[ia] = vb; matrices[ib] = va; jj = jj + 128u; } workgroupBarrier(); let p = matrices[base + kk * n + kk]; let col_len = n - kk - 1u; var t = lid; while (t < col_len) { let i = kk + 1u + t; let ik = base + i * n + kk; matrices[ik] = cx_div(matrices[ik], p); t = t + 128u; } workgroupBarrier(); let dim = n - kk - 1u; let total = dim * dim; t = lid; while (t < total) { let ii2 = t / dim; let jj2 = t % dim; let i = kk + 1u + ii2; let j = kk + 1u + jj2; let lik = matrices[base + i * n + kk]; let ukj = matrices[base + kk * n + j]; let ij = base + i * n + j; matrices[ij] = matrices[ij] - cx_mul(lik, ukj); t = t + 128u; } workgroupBarrier(); } }"; // wgsl/compute/lu-factor-lead-c64.wgsl var lu_factor_lead_c64_default = "const WG_SIZE: u32 = 128u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array>; @group(0) @binding(2) var ipiv: array; var wg_abs: array; var wg_row: array; var pivot_row: u32; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } fn cx_div(a: vec2, b: vec2) -> vec2 { let d = b.x * b.x + b.y * b.y; return vec2((a.x * b.x + a.y * b.y) / d, (a.y * b.x - a.x * b.y) / d); } fn cx_magsq(z: vec2) -> f32 { return z.x * z.x + z.y * z.y; } @compute @workgroup_size(WG_SIZE, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let base = b * params.elems_per_matrix; let base_ipiv = b * n; let kk = params.kk; let pw = params.pw; for (var j: u32 = 0u; j < pw; j = j + 1u) { let col = kk + j; if (col >= n) { break; } var pv: f32 = -1.0; var pr: u32 = col; var ii = col + lid; while (ii < n) { let aik = matrices[base + ii * n + col]; let ms = cx_magsq(aik); if (ms > pv || (ms == pv && ii < pr)) { pv = ms; pr = ii; } ii = ii + WG_SIZE; } wg_abs[lid] = pv; wg_row[lid] = pr; workgroupBarrier(); var s: u32 = WG_SIZE >> 1u; while (s > 0u) { if (lid < s) { let i1 = lid + s; let av0 = wg_abs[lid]; let av1 = wg_abs[i1]; let r0 = wg_row[lid]; let r1 = wg_row[i1]; if (av1 > av0 || (av1 == av0 && r1 < r0)) { wg_abs[lid] = av1; wg_row[lid] = r1; } } workgroupBarrier(); s = s >> 1u; } if (lid == 0u) { pivot_row = wg_row[0]; ipiv[base_ipiv + col] = pivot_row; } workgroupBarrier(); let piv = pivot_row; var jj: u32 = lid; while (jj < n) { let ia = base + col * n + jj; let ib = base + piv * n + jj; let va = matrices[ia]; let vb = matrices[ib]; matrices[ia] = vb; matrices[ib] = va; jj = jj + WG_SIZE; } workgroupBarrier(); let pivval = matrices[base + col * n + col]; var t: u32 = col + 1u + lid; while (t < n) { let idx = base + t * n + col; matrices[idx] = cx_div(matrices[idx], pivval); t = t + WG_SIZE; } workgroupBarrier(); let endc = min(n, kk + pw); let inner_cols = endc - (col + 1u); if (inner_cols > 0u) { let inner_rows = n - (col + 1u); let total = inner_rows * inner_cols; var u: u32 = lid; while (u < total) { let row_t = u / inner_cols; let col_t = u % inner_cols; let i = col + 1u + row_t; let c = col + 1u + col_t; let lik = matrices[base + i * n + col]; let ucj = matrices[base + col * n + c]; let idx = base + i * n + c; matrices[idx] = matrices[idx] - cx_mul(lik, ucj); u = u + WG_SIZE; } workgroupBarrier(); } } }"; // wgsl/compute/lu-factor-upper-c64.wgsl var lu_factor_upper_c64_default = "const MAX_PANEL_B: u32 = 16u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array>; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } @compute @workgroup_size(256, 1, 1) fn main(@builtin(global_invocation_id) gid: vec3) { let n = params.n; let kk = params.kk; let pw = params.pw; let trail_n = n - (kk + pw); if (trail_n == 0u || pw == 0u) { return; } let idx = gid.x; let b = idx / trail_n; if (b >= params.batch_count) { return; } let j = idx - b * trail_n; let col = (kk + pw) + j; let base = b * params.elems_per_matrix; var u_col: array, MAX_PANEL_B>; for (var i: u32 = 0u; i < pw; i = i + 1u) { let row = kk + i; var sum_v = matrices[base + row * n + col]; for (var r: u32 = 0u; r < i; r = r + 1u) { let l_val = matrices[base + row * n + (kk + r)]; sum_v = sum_v - cx_mul(l_val, u_col[r]); } u_col[i] = sum_v; matrices[base + row * n + col] = sum_v; } }"; // wgsl/compute/lu-factor-trailing-c64.wgsl var lu_factor_trailing_c64_default = "const TILE_M: u32 = 16u; const TILE_N: u32 = 8u; struct LuBlockedParams { batch_count: u32, n: u32, elems_per_matrix: u32, kk: u32, pw: u32, _pad0: u32, _pad1: u32, _pad2: u32, } @group(0) @binding(0) var params: LuBlockedParams; @group(0) @binding(1) var matrices: array>; var l_tile: array, TILE_M>; var u_tile: array, TILE_N>; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } @compute @workgroup_size(TILE_M, TILE_N, 1) fn main( @builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_id) lid: vec3, @builtin(local_invocation_index) lid_idx: u32, ) { let b = wg_id.z; if (b >= params.batch_count) { return; } let n = params.n; let kk = params.kk; let pw = params.pw; let base = b * params.elems_per_matrix; let m_dim = n - (kk + pw); let n_dim = n - (kk + pw); if (m_dim == 0u || n_dim == 0u || pw == 0u) { return; } let global_i = wg_id.y * TILE_M + lid.x; let global_j = wg_id.x * TILE_N + lid.y; let valid = (global_i < m_dim) && (global_j < n_dim); var acc = vec2(0.0, 0.0); for (var k: u32 = 0u; k < pw; k = k + 1u) { if (lid_idx < TILE_M) { let i_g = wg_id.y * TILE_M + lid_idx; if (i_g < m_dim) { let row = (kk + pw) + i_g; l_tile[lid_idx] = matrices[base + row * n + (kk + k)]; } else { l_tile[lid_idx] = vec2(0.0, 0.0); } } else if (lid_idx < TILE_M + TILE_N) { let j_local = lid_idx - TILE_M; let j_g = wg_id.x * TILE_N + j_local; if (j_g < n_dim) { let col = (kk + pw) + j_g; u_tile[j_local] = matrices[base + (kk + k) * n + col]; } else { u_tile[j_local] = vec2(0.0, 0.0); } } workgroupBarrier(); acc = acc + cx_mul(l_tile[lid.x], u_tile[lid.y]); workgroupBarrier(); } if (valid) { let row = (kk + pw) + global_i; let col = (kk + pw) + global_j; let idx = base + row * n + col; matrices[idx] = matrices[idx] - acc; } }"; // wgsl/compute/lu-solve-large-f32.wgsl var lu_solve_large_f32_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var lu: array; @group(0) @binding(2) var rhs: array; @group(0) @binding(3) var x: array; @group(0) @binding(4) var ipiv: array; @compute @workgroup_size(1, 1, 1) fn main(@builtin(global_invocation_id) gid: vec3) { let b = gid.x; if (b >= params.batch_count) { return; } let n = params.n; let stride = params.elems_per_matrix; let base = b * stride; let base_rhs = b * n; let base_ipiv = b * n; for (var i = 0u; i < n; i = i + 1u) { x[base_rhs + i] = rhs[base_rhs + i]; } for (var kk = 0u; kk < n - 1u; kk = kk + 1u) { let p = ipiv[base_ipiv + kk]; if (p != kk) { let t = x[base_rhs + kk]; x[base_rhs + kk] = x[base_rhs + p]; x[base_rhs + p] = t; } } for (var i = 0u; i < n; i = i + 1u) { var sum = x[base_rhs + i]; for (var j = 0u; j < i; j = j + 1u) { sum = sum - lu[base + i * n + j] * x[base_rhs + j]; } x[base_rhs + i] = sum; } for (var ii = n; ii > 0u; ii = ii - 1u) { let i = ii - 1u; var sum = x[base_rhs + i]; for (var j = i + 1u; j < n; j = j + 1u) { sum = sum - lu[base + i * n + j] * x[base_rhs + j]; } x[base_rhs + i] = sum / lu[base + i * n + i]; } }"; // wgsl/compute/lu-solve-shared-f32.wgsl var lu_solve_shared_f32_default = "const WG_SIZE: u32 = 64u; const BS: u32 = 32u; const MAX_N: u32 = 512u; const STRIDE_ITERS: u32 = 8u; struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var lu: array; @group(0) @binding(2) var rhs: array; @group(0) @binding(3) var x: array; @group(0) @binding(4) var ipiv: array; var wg_x: array; var wg_partial: array; @compute @workgroup_size(WG_SIZE, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let base = b * params.elems_per_matrix; let base_rhs = b * n; let base_ipiv = b * n; { for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let t = lid + s * WG_SIZE; if (t < n) { wg_x[t] = rhs[base_rhs + t]; } } } workgroupBarrier(); if (lid == 0u) { for (var k: u32 = 0u; k < n; k = k + 1u) { let p = ipiv[base_ipiv + k]; if (p != k) { let tmp = wg_x[k]; wg_x[k] = wg_x[p]; wg_x[p] = tmp; } } } workgroupBarrier(); let n_blocks = (n + BS - 1u) / BS; for (var bi: u32 = 0u; bi < n_blocks; bi = bi + 1u) { let bs0 = bi * BS; let bs1 = min(bs0 + BS, n); for (var ii: u32 = bs0; ii < bs1; ii = ii + 1u) { var partial = 0.0; for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let j = bs0 + lid + s * WG_SIZE; if (j < ii) { partial = partial + lu[base + ii * n + j] * wg_x[j]; } } wg_partial[lid] = partial; workgroupBarrier(); if (lid == 0u) { var sum = wg_partial[0]; for (var t: u32 = 1u; t < WG_SIZE; t = t + 1u) { sum = sum + wg_partial[t]; } wg_x[ii] = wg_x[ii] - sum; } workgroupBarrier(); } var k = bs1 + lid; while (k < n) { var update = 0.0; for (var j: u32 = bs0; j < bs1; j = j + 1u) { update = update + lu[base + k * n + j] * wg_x[j]; } wg_x[k] = wg_x[k] - update; k = k + WG_SIZE; } workgroupBarrier(); } for (var b_idx: u32 = 0u; b_idx < n_blocks; b_idx = b_idx + 1u) { let bi = n_blocks - 1u - b_idx; let bs0 = bi * BS; let bs1 = min(bs0 + BS, n); let bsz = bs1 - bs0; for (var ii_off: u32 = 0u; ii_off < bsz; ii_off = ii_off + 1u) { let ii = bs1 - 1u - ii_off; var partial = 0.0; for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let j = ii + 1u + lid + s * WG_SIZE; if (j < bs1) { partial = partial + lu[base + ii * n + j] * wg_x[j]; } } wg_partial[lid] = partial; workgroupBarrier(); if (lid == 0u) { var sum = wg_partial[0]; for (var t: u32 = 1u; t < WG_SIZE; t = t + 1u) { sum = sum + wg_partial[t]; } let new_val = wg_x[ii] - sum; wg_x[ii] = new_val / lu[base + ii * n + ii]; } workgroupBarrier(); } var k = lid; while (k < bs0) { var update = 0.0; for (var j: u32 = bs0; j < bs1; j = j + 1u) { update = update + lu[base + k * n + j] * wg_x[j]; } wg_x[k] = wg_x[k] - update; k = k + WG_SIZE; } workgroupBarrier(); } { for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let w = lid + s * WG_SIZE; if (w < n) { x[base_rhs + w] = wg_x[w]; } } } }"; // wgsl/compute/lu-solve-large-c64.wgsl var lu_solve_large_c64_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var lu: array>; @group(0) @binding(2) var rhs: array>; @group(0) @binding(3) var x: array>; @group(0) @binding(4) var ipiv: array; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } fn cx_div(a: vec2, b: vec2) -> vec2 { let d = b.x * b.x + b.y * b.y; return vec2((a.x * b.x + a.y * b.y) / d, (a.y * b.x - a.x * b.y) / d); } @compute @workgroup_size(1, 1, 1) fn main(@builtin(global_invocation_id) gid: vec3) { let b = gid.x; if (b >= params.batch_count) { return; } let n = params.n; let stride = params.elems_per_matrix; let base = b * stride; let base_rhs = b * n; let base_ipiv = b * n; for (var i = 0u; i < n; i = i + 1u) { x[base_rhs + i] = rhs[base_rhs + i]; } for (var kk = 0u; kk < n - 1u; kk = kk + 1u) { let p = ipiv[base_ipiv + kk]; if (p != kk) { let t = x[base_rhs + kk]; x[base_rhs + kk] = x[base_rhs + p]; x[base_rhs + p] = t; } } for (var i = 0u; i < n; i = i + 1u) { var sum = x[base_rhs + i]; for (var j = 0u; j < i; j = j + 1u) { sum = sum - cx_mul(lu[base + i * n + j], x[base_rhs + j]); } x[base_rhs + i] = sum; } for (var ii = n; ii > 0u; ii = ii - 1u) { let i = ii - 1u; var sum = x[base_rhs + i]; for (var j = i + 1u; j < n; j = j + 1u) { sum = sum - cx_mul(lu[base + i * n + j], x[base_rhs + j]); } x[base_rhs + i] = cx_div(sum, lu[base + i * n + i]); } }"; // wgsl/compute/lu-solve-shared-c64.wgsl var lu_solve_shared_c64_default = "const WG_SIZE: u32 = 64u; const BS: u32 = 32u; const MAX_N: u32 = 512u; const STRIDE_ITERS: u32 = 8u; struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: u32, } @group(0) @binding(0) var params: LuBatchedParams; @group(0) @binding(1) var lu: array>; @group(0) @binding(2) var rhs: array>; @group(0) @binding(3) var x: array>; @group(0) @binding(4) var ipiv: array; var wg_x: array, MAX_N>; var wg_partial: array, WG_SIZE>; fn cx_mul(a: vec2, b: vec2) -> vec2 { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } fn cx_div(a: vec2, b: vec2) -> vec2 { let d = b.x * b.x + b.y * b.y; return vec2((a.x * b.x + a.y * b.y) / d, (a.y * b.x - a.x * b.y) / d); } @compute @workgroup_size(WG_SIZE, 1, 1) fn main(@builtin(workgroup_id) wg_id: vec3, @builtin(local_invocation_index) lid: u32) { let b = wg_id.x; if (b >= params.batch_count) { return; } let n = params.n; let base = b * params.elems_per_matrix; let base_rhs = b * n; let base_ipiv = b * n; { for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let t = lid + s * WG_SIZE; if (t < n) { wg_x[t] = rhs[base_rhs + t]; } } } workgroupBarrier(); if (lid == 0u) { for (var k: u32 = 0u; k < n; k = k + 1u) { let p = ipiv[base_ipiv + k]; if (p != k) { let tmp = wg_x[k]; wg_x[k] = wg_x[p]; wg_x[p] = tmp; } } } workgroupBarrier(); let n_blocks = (n + BS - 1u) / BS; for (var bi: u32 = 0u; bi < n_blocks; bi = bi + 1u) { let bs0 = bi * BS; let bs1 = min(bs0 + BS, n); for (var ii: u32 = bs0; ii < bs1; ii = ii + 1u) { var partial = vec2(0.0, 0.0); for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let j = bs0 + lid + s * WG_SIZE; if (j < ii) { partial = partial + cx_mul(lu[base + ii * n + j], wg_x[j]); } } wg_partial[lid] = partial; workgroupBarrier(); if (lid == 0u) { var sum = wg_partial[0]; for (var t: u32 = 1u; t < WG_SIZE; t = t + 1u) { sum = sum + wg_partial[t]; } wg_x[ii] = wg_x[ii] - sum; } workgroupBarrier(); } var k = bs1 + lid; while (k < n) { var update = vec2(0.0, 0.0); for (var j: u32 = bs0; j < bs1; j = j + 1u) { update = update + cx_mul(lu[base + k * n + j], wg_x[j]); } wg_x[k] = wg_x[k] - update; k = k + WG_SIZE; } workgroupBarrier(); } for (var b_idx: u32 = 0u; b_idx < n_blocks; b_idx = b_idx + 1u) { let bi = n_blocks - 1u - b_idx; let bs0 = bi * BS; let bs1 = min(bs0 + BS, n); let bsz = bs1 - bs0; for (var ii_off: u32 = 0u; ii_off < bsz; ii_off = ii_off + 1u) { let ii = bs1 - 1u - ii_off; var partial = vec2(0.0, 0.0); for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let j = ii + 1u + lid + s * WG_SIZE; if (j < bs1) { partial = partial + cx_mul(lu[base + ii * n + j], wg_x[j]); } } wg_partial[lid] = partial; workgroupBarrier(); if (lid == 0u) { var sum = wg_partial[0]; for (var t: u32 = 1u; t < WG_SIZE; t = t + 1u) { sum = sum + wg_partial[t]; } let new_val = wg_x[ii] - sum; wg_x[ii] = cx_div(new_val, lu[base + ii * n + ii]); } workgroupBarrier(); } var k = lid; while (k < bs0) { var update = vec2(0.0, 0.0); for (var j: u32 = bs0; j < bs1; j = j + 1u) { update = update + cx_mul(lu[base + k * n + j], wg_x[j]); } wg_x[k] = wg_x[k] - update; k = k + WG_SIZE; } workgroupBarrier(); } { for (var s: u32 = 0u; s < STRIDE_ITERS; s = s + 1u) { let w = lid + s * WG_SIZE; if (w < n) { x[base_rhs + w] = wg_x[w]; } } } }"; // typescript/compute/kernels.ts var bytesPerElement = (type) => 4; var identityU32 = (op) => { if (op === "sum") return 0; if (op === "min") return 4294967295; return 0; }; var identityF32Bits = (op) => { if (op === "sum") return 0; if (op === "min") return 2139095040; return 4286578688; }; var identityArgPairBits = (op) => { if (op === "argmin") return { valueBits: 2139095040, index: 4294967295 }; return { valueBits: 4286578688, index: 4294967295 }; }; var assertByteLengthMultipleOf = (byteLength, unit, label) => assert(byteLength % unit === 0, `${label}: byteLength (${byteLength}) must be divisible by ${unit}`); var ComputeKernels = class { device; queue; scratch; pipelines; luBatchedParamsBuffer; luBlockedParamsBuffer; scaleExtractParamsBuffer; scaleHistogramParamsBuffer; scaleRemapParamsBuffer; scaleExtractParamsData; scaleHistogramParamsData; scaleHistogramParamsView; scaleRemapParamsData; scaleRemapParamsView; scaleRemapParamsF32; scaleTransformPacked; constructor(device, queue) { this.device = device; this.queue = queue; this.scratch = new ScratchBufferPool(device, { usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, labelPrefix: "kernels:scratch" }); this.pipelines = /* @__PURE__ */ new Map(); this.luBatchedParamsBuffer = null; this.luBlockedParamsBuffer = null; this.scaleExtractParamsBuffer = null; this.scaleHistogramParamsBuffer = null; this.scaleRemapParamsBuffer = null; this.scaleExtractParamsData = new Uint32Array(8); this.scaleHistogramParamsData = new ArrayBuffer(16); this.scaleHistogramParamsView = new DataView(this.scaleHistogramParamsData); this.scaleRemapParamsData = new ArrayBuffer(80); this.scaleRemapParamsView = new DataView(this.scaleRemapParamsData); this.scaleRemapParamsF32 = new Float32Array(this.scaleRemapParamsData); this.scaleTransformPacked = new Float32Array(20); } destroy() { this.scratch.destroy(); this.pipelines.clear(); this.luBatchedParamsBuffer?.destroy(); this.luBatchedParamsBuffer = null; this.luBlockedParamsBuffer?.destroy(); this.luBlockedParamsBuffer = null; this.scaleExtractParamsBuffer?.destroy(); this.scaleExtractParamsBuffer = null; this.scaleHistogramParamsBuffer?.destroy(); this.scaleHistogramParamsBuffer = null; this.scaleRemapParamsBuffer?.destroy(); this.scaleRemapParamsBuffer = null; } getLuBatchedParamsBuffer() { if (!this.luBatchedParamsBuffer) this.luBatchedParamsBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "kernels:luBatchedParams" }); return this.luBatchedParamsBuffer; } getLuBlockedParamsBuffer() { if (!this.luBlockedParamsBuffer) this.luBlockedParamsBuffer = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "kernels:luBlockedParams" }); return this.luBlockedParamsBuffer; } getScaleExtractParamsBuffer() { if (!this.scaleExtractParamsBuffer) this.scaleExtractParamsBuffer = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "scale:extract:params" }); return this.scaleExtractParamsBuffer; } getScaleHistogramParamsBuffer() { if (!this.scaleHistogramParamsBuffer) this.scaleHistogramParamsBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "histogramF32:params" }); return this.scaleHistogramParamsBuffer; } getScaleRemapParamsBuffer() { if (!this.scaleRemapParamsBuffer) this.scaleRemapParamsBuffer = this.device.createBuffer({ size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "scale:remap:params" }); return this.scaleRemapParamsBuffer; } getPipeline(key, create) { let p = this.pipelines.get(key); if (!p) { p = create(); this.pipelines.set(key, p); } return p; } bindSized(res, sizeBytes) { assert(Number.isInteger(sizeBytes) && sizeBytes >= 0, `bindSized: sizeBytes must be an integer >= 0 (got ${sizeBytes})`); const aligned = alignTo(sizeBytes, 4); return { buffer: res, size: Math.max(4, aligned) }; } resolveCount(buf, elemBytes, count) { assertByteLengthMultipleOf(buf.byteLength, elemBytes, "resolveCount"); const total = buf.byteLength / elemBytes; if (count === void 0) return total; assert(Number.isInteger(count) && count >= 0, `count must be an integer >= 0 (got ${count})`); assert(count <= total, `count (${count}) exceeds buffer element capacity (${total})`); return count; } execute(commands, opts) { if (commands.length === 0) return; const encoder = opts?.encoder ?? this.device.createCommandEncoder(); encodeDispatchBatchWithLimit(encoder, commands, opts?.label, opts?.validateLimits ? this.device.limits.maxComputeWorkgroupsPerDimension : void 0); if (!opts?.encoder) { this.queue.submit([encoder.finish()]); this.scratch.reset(); } } executeHistogramCommands(commands, bins, binCount, nativeClear, opts) { if (!nativeClear) { this.execute(commands, opts); return; } const encoder = opts?.encoder ?? this.device.createCommandEncoder(); encoder.clearBuffer(bins.buffer, 0, binCount * 4); encodeDispatchBatchWithLimit(encoder, commands, opts?.label, opts?.validateLimits ? this.device.limits.maxComputeWorkgroupsPerDimension : void 0); if (!opts?.encoder) { this.queue.submit([encoder.finish()]); this.scratch.reset(); } } writeScalarU32(dst, value) { const buf = resolveGPUBuffer(dst); const tmp = new Uint32Array([value >>> 0]); this.queue.writeBuffer(buf, 0, tmp); } writeScalarF32(dst, value) { const buf = resolveGPUBuffer(dst); const tmp = new Float32Array([value]); this.queue.writeBuffer(buf, 0, tmp); } writeScalarF32Bits(dst, bits) { const buf = resolveGPUBuffer(dst); const tmp = new Uint32Array([bits >>> 0]); this.queue.writeBuffer(buf, 0, tmp); } writeArgPairBits(dst, valueBits, index) { const buf = resolveGPUBuffer(dst); const tmp = new Uint32Array([valueBits >>> 0, index >>> 0]); this.queue.writeBuffer(buf, 0, tmp); } getReducePipeline(type, op) { const key = `kernels:reduce:${type}:${op}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: type === "f32" ? op === "sum" ? reduce_sum_f32_default : op === "max" ? reduce_max_f32_default : reduce_min_f32_default : op === "sum" ? reduce_sum_u32_default : op === "max" ? reduce_max_u32_default : reduce_min_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getArgReduceInitialPipeline(op) { const key = `kernels:argreduce:init:${op}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: op === "argmax" ? argreduce_argmax_initial_default : argreduce_argmin_initial_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getArgReducePairsPipeline(op) { const key = `kernels:argreduce:pairs:${op}`; return this.getPipeline(key, () => { const code = op === "argmax" ? argreduce_argmax_pairs_default : argreduce_argmin_pairs_default; return new ComputePipeline(this.device, { label: key, code, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } encodeReduceScalar(commands, type, op, input, inputCount, out, labelPrefix) { assert(Number.isInteger(inputCount) && inputCount > 0, "encodeReduceScalar expects inputCount > 0"); const elemBytes = bytesPerElement(type); let inRes = input; let n = inputCount; let pass = 0; while (true) { const outCount = ceilDiv(n, 512); const isFinal = outCount <= 1; const outRes = isFinal ? out : this.scratch.acquire(outCount * elemBytes, `${labelPrefix}:reduce:${pass}`); const pipeline = this.getReducePipeline(type, op); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(inRes, n * elemBytes), 1: this.bindSized(outRes, outCount * elemBytes) }, `${labelPrefix}:reduce:${pass}:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(outCount, 1, 1), label: `${labelPrefix}:reduce:${pass}` }); if (isFinal) break; inRes = outRes; n = outCount; pass++; } } encodeArgReduceF32Scalar(commands, op, input, inputCount, out, labelPrefix) { assert(Number.isInteger(inputCount) && inputCount > 0, "encodeArgReduceF32Scalar expects inputCount > 0"); let inRes = input; let n = inputCount; let inStrideBytes = 4; let pass = 0; while (true) { const outCount = ceilDiv(n, 512); const isFinal = outCount <= 1; const outRes = isFinal ? out : this.scratch.acquire(outCount * 8, `${labelPrefix}:argreduce:${pass}`); const pipeline = pass === 0 ? this.getArgReduceInitialPipeline(op) : this.getArgReducePairsPipeline(op); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(inRes, n * inStrideBytes), 1: this.bindSized(outRes, outCount * 8) }, `${labelPrefix}:argreduce:${pass}:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(outCount, 1, 1), label: `${labelPrefix}:argreduce:${pass}` }); if (isFinal) break; inRes = outRes; n = outCount; inStrideBytes = 8; pass++; } } getScanBlockExclusiveU32Pipeline() { const key = "kernels:scan:blockExclusiveU32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scan_block_exclusive_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getScanAddBlockOffsetsU32Pipeline() { const key = "kernels:scan:addBlockOffsetsU32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scan_add_block_offsets_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: false }), storageBufferLayout({ binding: 1, readOnly: true }) ] } ] }); }); } encodeScanExclusiveU32Into(commands, input, count, out, labelPrefix) { assert(Number.isInteger(count) && count >= 0, `encodeScanExclusiveU32Into: count must be an integer >= 0 (got ${count})`); if (count === 0) return; const numBlocks = ceilDiv(count, 1024); const blockSums = this.scratch.acquire(numBlocks * 4, `${labelPrefix}:blockSums`); { const pipeline = this.getScanBlockExclusiveU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 4), 1: this.bindSized(out, count * 4), 2: this.bindSized(blockSums, numBlocks * 4) }, `${labelPrefix}:scanBlocks:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(numBlocks, 1, 1), label: `${labelPrefix}:scanBlocks` }); } if (numBlocks <= 1) return; const blockOffsets = this.scratch.acquire(numBlocks * 4, `${labelPrefix}:blockOffsets`); this.encodeScanExclusiveU32Into(commands, blockSums, numBlocks, blockOffsets, `${labelPrefix}:scanBlockSums`); { const pipeline = this.getScanAddBlockOffsetsU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(out, count * 4), 1: this.bindSized(blockOffsets, numBlocks * 4) }, `${labelPrefix}:addOffsets:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `${labelPrefix}:addOffsets` }); } } encodeRadixZeroScanInto(commands, keys, count, prefix, bit, labelPrefix) { const numBlocks = ceilDiv(count, 1024); const blockSums = this.scratch.acquire(numBlocks * 4, `${labelPrefix}:blockSums`); const pipeline = this.getRadixFlagsPipeline(bit); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(keys, count * 4), 1: this.bindSized(prefix, count * 4), 2: this.bindSized(blockSums, numBlocks * 4) }, `${labelPrefix}:scanBlocks:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(numBlocks, 1, 1), label: `${labelPrefix}:scanBlocks` }); if (numBlocks <= 1) return; const blockOffsets = this.scratch.acquire(numBlocks * 4, `${labelPrefix}:blockOffsets`); this.encodeScanExclusiveU32Into(commands, blockSums, numBlocks, blockOffsets, `${labelPrefix}:scanBlockSums`); const addPipeline = this.getScanAddBlockOffsetsU32Pipeline(); const addBg = addPipeline.createBindGroup(0, { 0: this.bindSized(prefix, count * 4), 1: this.bindSized(blockOffsets, numBlocks * 4) }, `${labelPrefix}:addOffsets:bg`); commands.push({ pipeline: addPipeline, bindGroups: [addBg], workgroups: workgroups1D(count, 256), label: `${labelPrefix}:addOffsets` }); } getHistogramClearPipeline() { const key = "kernels:histogram:clearAtomicU32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: histogram_clear_atomic_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: false }) ] } ] }); }); } getHistogramPipeline(local256 = false) { const key = local256 ? "kernels:histogram:u32:local256" : "kernels:histogram:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: histogram_u32_default, entryPoint: local256 ? "main_local_256" : "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getCompactPipeline(type) { const key = `kernels:compact:${type}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: type === "u32" ? compact_u32_default : compact_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: false }) ] } ] }); }); } getRadixFlagsPipeline(bit) { const b = bit | 0; const key = `kernels:radix:flags:bit${b}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sort_radix_flags_u32_default, entryPoint: "main", constants: { BIT: b }, bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getRadixScatterPipeline(bit) { const b = bit | 0; const key = `kernels:radix:scatter:bit${b}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sort_radix_scatter_u32_default, entryPoint: "main", constants: { BIT: b }, bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getRadixScatterPairsPipeline(bit) { const b = bit | 0; const key = `kernels:radix:scatterPairs:bit${b}`; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sort_radix_scatter_pairs_u32_default, entryPoint: "main", constants: { BIT: b }, bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: false }) ] } ] }); }); } getCopyF32Pipeline() { const key = "kernels:copy:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: copy_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getCopyU32Pipeline() { const key = "kernels:copy:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: copy_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getScaleExtractF32Pipeline() { const key = "kernels:scale:extractF32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scale_extract_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }), uniformBufferLayout({ binding: 3 }) ] } ] }); }); } getScaleHistogramF32Pipeline(local256 = false) { const key = local256 ? "kernels:scale:histogramF32:local256" : "kernels:scale:histogramF32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scale_histogram_f32_default, entryPoint: local256 ? "main_local_256" : "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), uniformBufferLayout({ binding: 2 }) ] } ] }); }); } getScaleRemapF32Pipeline() { const key = "kernels:scale:remapF32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scale_remap_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), uniformBufferLayout({ binding: 2 }) ] } ] }); }); } getAddF32Pipeline() { const key = "kernels:add:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: add_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getAddU32Pipeline() { const key = "kernels:add:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: add_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getAddC64Pipeline() { const key = "kernels:add:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: add_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getSubF32Pipeline() { const key = "kernels:sub:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sub_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getSubU32Pipeline() { const key = "kernels:sub:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sub_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getSubC64Pipeline() { const key = "kernels:sub:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: sub_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getMulF32Pipeline() { const key = "kernels:mul:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: mul_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getMulU32Pipeline() { const key = "kernels:mul:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: mul_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getMulC64Pipeline() { const key = "kernels:mul:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: mul_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getSclF32Pipeline() { const key = "kernels:scl:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scl_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: true }) ] } ] }); }); } getSclU32Pipeline() { const key = "kernels:scl:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scl_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: true }) ] } ] }); }); } getSclC64Pipeline() { const key = "kernels:scl:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scl_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: true }) ] } ] }); }); } getDotF32Pipeline() { const key = "kernels:dot:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: dot_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getDotU32Pipeline() { const key = "kernels:dot:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: dot_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getDotC64Pipeline() { const key = "kernels:dot:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: dot_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getDotC64ReducePipeline() { const key = "kernels:dot:c64:reduce"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: dot_c64_default, entryPoint: "reduce", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getAxpyF32Pipeline() { const key = "kernels:axpy:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: axpy_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getAxpyU32Pipeline() { const key = "kernels:axpy:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: axpy_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getAxpyC64Pipeline() { const key = "kernels:axpy:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: axpy_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getGemmF32Pipeline() { const key = "kernels:gemm:f32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: gemm_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getGemmU32Pipeline() { const key = "kernels:gemm:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: gemm_u32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getGemmC64Pipeline() { const key = "kernels:gemm:c64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: gemm_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ storageBufferLayout({ binding: 0, readOnly: true }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: false }), storageBufferLayout({ binding: 3, readOnly: true }) ] } ] }); }); } getLuFactorRealPipeline() { const key = "kernels:lu:factorReal"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuFactorRealLeadPipeline() { const key = "kernels:lu:factorRealLead"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_lead_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuFactorRealUpperPipeline() { const key = "kernels:lu:factorRealUpper"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_upper_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuFactorRealTrailingPipeline() { const key = "kernels:lu:factorRealTrailing"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_trailing_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuSolveRealPipeline() { const key = "kernels:lu:solveReal"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_large_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: true }) ] } ] }); }); } getLuSolveRealSharedPipeline() { const key = "kernels:lu:solveRealShared"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_shared_f32_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: true }) ] } ] }); }); } getLuFactorC64LeadPipeline() { const key = "kernels:lu:factorC64Lead"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_lead_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuFactorC64UpperPipeline() { const key = "kernels:lu:factorC64Upper"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_upper_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuFactorC64TrailingPipeline() { const key = "kernels:lu:factorC64Trailing"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_trailing_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuFactorC64SmallPipeline() { const key = "kernels:lu:factorC64Small"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuSolveC64LargePipeline() { const key = "kernels:lu:solveC64Large"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_large_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: true }) ] } ] }); }); } getLuSolveC64Pipeline() { const key = "kernels:lu:solveC64"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_shared_c64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: true }), storageBufferLayout({ binding: 2, readOnly: true }), storageBufferLayout({ binding: 3, readOnly: false }), storageBufferLayout({ binding: 4, readOnly: true }) ] } ] }); }); } encodeCopyF32(commands, src, count, dst, labelPrefix) { assert(Number.isInteger(count) && count >= 0, `encodeCopyF32: count must be an integer >= 0 (got ${count})`); if (count === 0) return; const pipeline = this.getCopyF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(src, count * 4), 1: this.bindSized(dst, count * 4) }, `${labelPrefix}:copy:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `${labelPrefix}:copy` }); } encodeCopyU32(commands, src, count, dst, labelPrefix) { assert(Number.isInteger(count) && count >= 0, `encodeCopyU32: count must be an integer >= 0 (got ${count})`); if (count === 0) return; const pipeline = this.getCopyU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(src, count * 4), 1: this.bindSized(dst, count * 4) }, `${labelPrefix}:copy:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `${labelPrefix}:copy` }); } resolveVectorPairCount(a, b, elemBytes, count, label) { assertByteLengthMultipleOf(a.byteLength, elemBytes, `${label}: a`); assertByteLengthMultipleOf(b.byteLength, elemBytes, `${label}: b`); const aCount = a.byteLength / elemBytes; const bCount = b.byteLength / elemBytes; if (count === void 0) { assert(aCount === bCount, `${label}: input logical lengths must match when count is omitted (${aCount} != ${bCount})`); return aCount; } assert(Number.isInteger(count) && count >= 0, `${label}: count must be an integer >= 0 (got ${count})`); assert(count <= aCount, `${label}: a has insufficient capacity for count ${count}`); assert(count <= bCount, `${label}: b has insufficient capacity for count ${count}`); return count; } resolveVectorOutput(a, b, elemBytes, count, out, label) { const result = out ?? new StorageBuffer(this.device, this.queue, { label: `${label}:out`, byteLength: count * elemBytes, copySrc: true }); assertByteLengthMultipleOf(result.byteLength, elemBytes, `${label}: out`); assert(result.byteLength >= count * elemBytes, `${label}: out has insufficient capacity for count ${count}`); assert(result !== a && result !== b, `${label}: out must be distinct from all inputs`); return result; } validateGemmDimensions(m, n, k, label) { assert(Number.isInteger(m) && m >= 0, `${label}: m must be an integer >= 0 (got ${m})`); assert(Number.isInteger(n) && n >= 0, `${label}: n must be an integer >= 0 (got ${n})`); assert(Number.isInteger(k) && k >= 0, `${label}: k must be an integer >= 0 (got ${k})`); assert(m <= 4294967295 && n <= 4294967295 && k <= 4294967295, `${label}: dimensions must fit in u32`); const aCount = m * k; const bCount = k * n; const cCount = m * n; assert(Number.isSafeInteger(aCount) && Number.isSafeInteger(bCount) && Number.isSafeInteger(cCount), `${label}: matrix element-count overflow`); assert(aCount <= 4294967295 && bCount <= 4294967295 && cCount <= 4294967295, `${label}: matrix element count must fit in u32`); return { aCount, bCount, cCount }; } addF32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "addF32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "addF32"); if (count === 0) return out; const pipeline = this.getAddF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "addF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "addF32" }], opts); return out; } addU32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "addU32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "addU32"); if (count === 0) return out; const pipeline = this.getAddU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "addU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "addU32" }], opts); return out; } addC64(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 8, opts.count, "addC64"); const out = this.resolveVectorOutput(a, b, 8, count, opts.out, "addC64"); if (count === 0) return out; const pipeline = this.getAddC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 8), 1: this.bindSized(b, count * 8), 2: this.bindSized(out, count * 8) }, "addC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "addC64" }], opts); return out; } subF32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "subF32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "subF32"); if (count === 0) return out; const pipeline = this.getSubF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "subF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "subF32" }], opts); return out; } subU32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "subU32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "subU32"); if (count === 0) return out; const pipeline = this.getSubU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "subU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "subU32" }], opts); return out; } subC64(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 8, opts.count, "subC64"); const out = this.resolveVectorOutput(a, b, 8, count, opts.out, "subC64"); if (count === 0) return out; const pipeline = this.getSubC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 8), 1: this.bindSized(b, count * 8), 2: this.bindSized(out, count * 8) }, "subC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "subC64" }], opts); return out; } mulF32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "mulF32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "mulF32"); if (count === 0) return out; const pipeline = this.getMulF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "mulF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "mulF32" }], opts); return out; } mulU32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "mulU32"); const out = this.resolveVectorOutput(a, b, 4, count, opts.out, "mulU32"); if (count === 0) return out; const pipeline = this.getMulU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(out, count * 4) }, "mulU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "mulU32" }], opts); return out; } mulC64(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 8, opts.count, "mulC64"); const out = this.resolveVectorOutput(a, b, 8, count, opts.out, "mulC64"); if (count === 0) return out; const pipeline = this.getMulC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 8), 1: this.bindSized(b, count * 8), 2: this.bindSized(out, count * 8) }, "mulC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "mulC64" }], opts); return out; } sclF32(input, scalar, opts = {}) { const count = this.resolveCount(input, 4, opts.count); const out = this.resolveVectorOutput(input, null, 4, count, opts.out, "sclF32"); if (count === 0) return out; const params = this.scratch.acquire(4, "sclF32:params"); this.queue.writeBuffer(params, 0, new Float32Array([scalar])); const pipeline = this.getSclF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 4), 1: this.bindSized(out, count * 4), 2: { buffer: params, size: 4 } }, "sclF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "sclF32" }], opts); return out; } sclU32(input, scalar, opts = {}) { assert(Number.isInteger(scalar) && scalar >= 0 && scalar <= 4294967295, `sclU32: scalar must be a u32 integer (got ${scalar})`); const count = this.resolveCount(input, 4, opts.count); const out = this.resolveVectorOutput(input, null, 4, count, opts.out, "sclU32"); if (count === 0) return out; const params = this.scratch.acquire(4, "sclU32:params"); this.queue.writeBuffer(params, 0, new Uint32Array([scalar])); const pipeline = this.getSclU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 4), 1: this.bindSized(out, count * 4), 2: { buffer: params, size: 4 } }, "sclU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "sclU32" }], opts); return out; } sclC64(input, scalar, opts = {}) { assert(Array.isArray(scalar) && scalar.length === 2 && typeof scalar[0] === "number" && typeof scalar[1] === "number", `sclC64: scalar must be a [real, imaginary] tuple`); const count = this.resolveCount(input, 8, opts.count); const out = this.resolveVectorOutput(input, null, 8, count, opts.out, "sclC64"); if (count === 0) return out; const params = this.scratch.acquire(8, "sclC64:params"); this.queue.writeBuffer(params, 0, new Float32Array(scalar)); const pipeline = this.getSclC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 8), 1: this.bindSized(out, count * 8), 2: { buffer: params, size: 8 } }, "sclC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "sclC64" }], opts); return out; } axpyF32(x, y, alpha, opts = {}) { const count = this.resolveVectorPairCount(x, y, 4, opts.count, "axpyF32"); const out = this.resolveVectorOutput(x, y, 4, count, opts.out, "axpyF32"); if (count === 0) return out; const params = this.scratch.acquire(4, "axpyF32:params"); this.queue.writeBuffer(params, 0, new Float32Array([alpha])); const pipeline = this.getAxpyF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(x, count * 4), 1: this.bindSized(y, count * 4), 2: this.bindSized(out, count * 4), 3: { buffer: params, size: 4 } }, "axpyF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "axpyF32" }], opts); return out; } axpyU32(x, y, alpha, opts = {}) { assert(Number.isInteger(alpha) && alpha >= 0 && alpha <= 4294967295, `axpyU32: alpha must be a u32 integer (got ${alpha})`); const count = this.resolveVectorPairCount(x, y, 4, opts.count, "axpyU32"); const out = this.resolveVectorOutput(x, y, 4, count, opts.out, "axpyU32"); if (count === 0) return out; const params = this.scratch.acquire(4, "axpyU32:params"); this.queue.writeBuffer(params, 0, new Uint32Array([alpha])); const pipeline = this.getAxpyU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(x, count * 4), 1: this.bindSized(y, count * 4), 2: this.bindSized(out, count * 4), 3: { buffer: params, size: 4 } }, "axpyU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "axpyU32" }], opts); return out; } axpyC64(x, y, alpha, opts = {}) { assert(Array.isArray(alpha) && alpha.length === 2 && typeof alpha[0] === "number" && typeof alpha[1] === "number", `axpyC64: alpha must be a [real, imaginary] tuple`); const count = this.resolveVectorPairCount(x, y, 8, opts.count, "axpyC64"); const out = this.resolveVectorOutput(x, y, 8, count, opts.out, "axpyC64"); if (count === 0) return out; const params = this.scratch.acquire(8, "axpyC64:params"); this.queue.writeBuffer(params, 0, new Float32Array(alpha)); const pipeline = this.getAxpyC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(x, count * 8), 1: this.bindSized(y, count * 8), 2: this.bindSized(out, count * 8), 3: { buffer: params, size: 8 } }, "axpyC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "axpyC64" }], opts); return out; } dotF32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "dotF32"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "dotF32:out", byteLength: 4, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 4, "dotF32: out"); assert(out.byteLength >= 4, "dotF32: out must be at least 4 bytes"); assert(out !== a && out !== b, "dotF32: out must be distinct from all inputs"); if (count === 0) { this.writeScalarF32(out, 0); return out; } const commands = []; const partialCount = ceilDiv(count, 512); const partial = partialCount === 1 ? out : this.scratch.acquire(partialCount * 4, "dotF32:partial"); const pipeline = this.getDotF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(partial, partialCount * 4) }, "dotF32:bg"); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(partialCount, 1, 1), label: "dotF32" }); if (partialCount > 1) this.encodeReduceScalar(commands, "f32", "sum", partial, partialCount, out, "dotF32"); this.execute(commands, opts); return out; } dotU32(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 4, opts.count, "dotU32"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "dotU32:out", byteLength: 4, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 4, "dotU32: out"); assert(out.byteLength >= 4, "dotU32: out must be at least 4 bytes"); assert(out !== a && out !== b, "dotU32: out must be distinct from all inputs"); if (count === 0) { this.writeScalarU32(out, 0); return out; } const commands = []; const partialCount = ceilDiv(count, 512); const partial = partialCount === 1 ? out : this.scratch.acquire(partialCount * 4, "dotU32:partial"); const pipeline = this.getDotU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, count * 4), 1: this.bindSized(b, count * 4), 2: this.bindSized(partial, partialCount * 4) }, "dotU32:bg"); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(partialCount, 1, 1), label: "dotU32" }); if (partialCount > 1) this.encodeReduceScalar(commands, "u32", "sum", partial, partialCount, out, "dotU32"); this.execute(commands, opts); return out; } dotC64(a, b, opts = {}) { const count = this.resolveVectorPairCount(a, b, 8, opts.count, "dotC64"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "dotC64:out", byteLength: 8, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 8, "dotC64: out"); assert(out.byteLength >= 8, "dotC64: out must be at least 8 bytes"); assert(out !== a && out !== b, "dotC64: out must be distinct from all inputs"); if (count === 0) { this.queue.writeBuffer(out.buffer, 0, new Float32Array([0, 0])); return out; } const commands = []; let n = count; let input = a; let pass = 0; while (true) { const partialCount = ceilDiv(n, 512); const partial = partialCount === 1 ? out : this.scratch.acquire(partialCount * 8, `dotC64:partial:${pass}`); const pipeline = pass === 0 ? this.getDotC64Pipeline() : this.getDotC64ReducePipeline(); const second = pass === 0 ? b : input; const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, n * 8), 1: this.bindSized(second, n * 8), 2: this.bindSized(partial, partialCount * 8) }, `dotC64:${pass}:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(partialCount, 1, 1), label: `dotC64:${pass}` }); if (partialCount === 1) break; input = partial; n = partialCount; pass++; } this.execute(commands, opts); return out; } gemmF32(a, b, m, n, k, opts = {}) { const sizes = this.validateGemmDimensions(m, n, k, "gemmF32"); assertByteLengthMultipleOf(a.byteLength, 4, "gemmF32: a"); assertByteLengthMultipleOf(b.byteLength, 4, "gemmF32: b"); assert(a.byteLength >= sizes.aCount * 4, "gemmF32: a has insufficient capacity"); assert(b.byteLength >= sizes.bCount * 4, "gemmF32: b has insufficient capacity"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "gemmF32:out", byteLength: sizes.cCount * 4, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 4, "gemmF32: out"); assert(out.byteLength >= sizes.cCount * 4, "gemmF32: out has insufficient capacity"); assert(out !== a && out !== b, "gemmF32: out must be distinct from a and b"); if (m === 0 || n === 0) return out; const raw = new ArrayBuffer(32); const view = new DataView(raw); view.setUint32(0, m, true); view.setUint32(4, n, true); view.setUint32(8, k, true); view.setFloat32(12, opts.alpha ?? 1, true); view.setFloat32(16, opts.beta ?? 0, true); const params = this.scratch.acquire(32, "gemmF32:params"); this.queue.writeBuffer(params, 0, raw); const pipeline = this.getGemmF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, sizes.aCount * 4), 1: this.bindSized(b, sizes.bCount * 4), 2: this.bindSized(out, sizes.cCount * 4), 3: { buffer: params, size: 32 } }, "gemmF32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(ceilDiv(n, 16), ceilDiv(m, 16), 1), label: "gemmF32" }], opts); return out; } gemmU32(a, b, m, n, k, opts = {}) { const alpha = opts.alpha ?? 1; const beta = opts.beta ?? 0; assert(Number.isInteger(alpha) && alpha >= 0 && alpha <= 4294967295, `gemmU32: alpha must be a u32 integer (got ${alpha})`); assert(Number.isInteger(beta) && beta >= 0 && beta <= 4294967295, `gemmU32: beta must be a u32 integer (got ${beta})`); const sizes = this.validateGemmDimensions(m, n, k, "gemmU32"); assertByteLengthMultipleOf(a.byteLength, 4, "gemmU32: a"); assertByteLengthMultipleOf(b.byteLength, 4, "gemmU32: b"); assert(a.byteLength >= sizes.aCount * 4, "gemmU32: a has insufficient capacity"); assert(b.byteLength >= sizes.bCount * 4, "gemmU32: b has insufficient capacity"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "gemmU32:out", byteLength: sizes.cCount * 4, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 4, "gemmU32: out"); assert(out.byteLength >= sizes.cCount * 4, "gemmU32: out has insufficient capacity"); assert(out !== a && out !== b, "gemmU32: out must be distinct from a and b"); if (m === 0 || n === 0) return out; const data = new Uint32Array(8); data[0] = m; data[1] = n; data[2] = k; data[3] = alpha; data[4] = beta; const params = this.scratch.acquire(32, "gemmU32:params"); this.queue.writeBuffer(params, 0, data); const pipeline = this.getGemmU32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(a, sizes.aCount * 4), 1: this.bindSized(b, sizes.bCount * 4), 2: this.bindSized(out, sizes.cCount * 4), 3: { buffer: params, size: 32 } }, "gemmU32:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(ceilDiv(n, 16), ceilDiv(m, 16), 1), label: "gemmU32" }], opts); return out; } gemmC64(a, b, m, n, k, opts = {}) { const alpha = opts.alpha ?? [1, 0]; const beta = opts.beta ?? [0, 0]; assert(Array.isArray(alpha) && alpha.length === 2 && typeof alpha[0] === "number" && typeof alpha[1] === "number", `gemmC64: alpha must be a [real, imaginary] tuple`); assert(Array.isArray(beta) && beta.length === 2 && typeof beta[0] === "number" && typeof beta[1] === "number", `gemmC64: beta must be a [real, imaginary] tuple`); const sizes = this.validateGemmDimensions(m, n, k, "gemmC64"); assertByteLengthMultipleOf(a.byteLength, 8, "gemmC64: a"); assertByteLengthMultipleOf(b.byteLength, 8, "gemmC64: b"); assert(a.byteLength >= sizes.aCount * 8, "gemmC64: a has insufficient capacity"); assert(b.byteLength >= sizes.bCount * 8, "gemmC64: b has insufficient capacity"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "gemmC64:out", byteLength: sizes.cCount * 8, copySrc: true }); assertByteLengthMultipleOf(out.byteLength, 8, "gemmC64: out"); assert(out.byteLength >= sizes.cCount * 8, "gemmC64: out has insufficient capacity"); assert(out !== a && out !== b, "gemmC64: out must be distinct from a and b"); if (m === 0 || n === 0) return out; const raw = new ArrayBuffer(32); const view = new DataView(raw); view.setUint32(0, m, true); view.setUint32(4, n, true); view.setUint32(8, k, true); view.setFloat32(12, alpha[0], true); view.setFloat32(16, alpha[1], true); view.setFloat32(20, beta[0], true); view.setFloat32(24, beta[1], true); const params = this.scratch.acquire(32, "gemmC64:params"); this.queue.writeBuffer(params, 0, raw); const aBinding = sizes.aCount === 0 ? this.scratch.acquire(8, "gemmC64:emptyA") : a; const bBinding = sizes.bCount === 0 ? this.scratch.acquire(8, "gemmC64:emptyB") : b; const pipeline = this.getGemmC64Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(aBinding, Math.max(8, sizes.aCount * 8)), 1: this.bindSized(bBinding, Math.max(8, sizes.bCount * 8)), 2: this.bindSized(out, sizes.cCount * 8), 3: { buffer: params, size: 32 } }, "gemmC64:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: makeWorkgroupCounts(ceilDiv(n, 16), ceilDiv(m, 16), 1), label: "gemmC64" }], opts); return out; } copyU32(src, opts = {}) { let count = opts.count; if (count === void 0) { if (src instanceof StorageBuffer) count = this.resolveCount(src, 4, void 0); else assert(false, "copyU32: opts.count is required when src is not a StorageBuffer"); } assert(Number.isInteger(count) && count >= 0, `copyU32: count must be an integer >= 0 (got ${count})`); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "copyU32:out", byteLength: count * 4, copySrc: true }); assert(out.byteLength >= count * 4, "copyU32: out buffer is too small for requested count"); const commands = []; this.encodeCopyU32(commands, src, count, out, "copyU32"); this.execute(commands, opts); return out; } reduceU32(input, op, opts = {}) { const count = this.resolveCount(input, 4, opts.count); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: `reduceU32:${op}`, byteLength: 4, copySrc: true }); assert(out.byteLength >= 4, "reduceU32: out buffer must be at least 4 bytes"); if (count === 0) { this.writeScalarU32(out, identityU32(op)); return out; } const commands = []; this.encodeReduceScalar(commands, "u32", op, input, count, out, `reduceU32:${op}`); this.execute(commands, opts); return out; } sumU32(input, opts = {}) { return this.reduceU32(input, "sum", opts); } minU32(input, opts = {}) { return this.reduceU32(input, "min", opts); } maxU32(input, opts = {}) { return this.reduceU32(input, "max", opts); } copyF32(src, opts = {}) { let count = opts.count; if (count === void 0) { if (src instanceof StorageBuffer) count = this.resolveCount(src, 4, void 0); else assert(false, "copyF32: opts.count is required when src is not a StorageBuffer"); } assert(Number.isInteger(count) && count >= 0, `copyF32: count must be an integer >= 0 (got ${count})`); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "copyF32:out", byteLength: count * 4, copySrc: true }); assert(out.byteLength >= count * 4, "copyF32: out buffer is too small for requested count"); const commands = []; this.encodeCopyF32(commands, src, count, out, "copyF32"); this.execute(commands, opts); return out; } extractScaleValuesF32(src, opts) { assert(!opts.encoder, "extractScaleValuesF32 does not support opts.encoder"); const count = opts.count; assert(Number.isInteger(count) && count >= 0, `extractScaleValuesF32: count must be an integer >= 0 (got ${count})`); const componentCount = Math.max(1, Math.min(4, Math.floor(opts.componentCount ?? 1))); const componentIndex = Math.max(0, Math.min(3, Math.floor(opts.componentIndex ?? 0))); const stride = Math.max(componentCount, Math.floor(opts.stride ?? componentCount)); const offset = Math.max(0, Math.floor(opts.offset ?? 0)); const valueMode = opts.valueMode ?? "component"; assert(valueMode === "component" || valueMode === "magnitude", `extractScaleValuesF32: invalid valueMode ${String(valueMode)}`); const requiredSourceFloats = count > 0 ? offset + (count - 1) * stride + componentCount : 0; const srcByteLength = src instanceof StorageBuffer ? src.byteLength : resolveGPUBuffer(src).size; assert(requiredSourceFloats * 4 <= srcByteLength, `extractScaleValuesF32: source range exceeds source buffer capacity (required ${requiredSourceFloats} f32, capacity ${Math.floor(srcByteLength / 4)} f32)`); const values = opts.values ?? new StorageBuffer(this.device, this.queue, { label: "scale:extract:values", byteLength: count * 4, copySrc: true }); const flags = opts.flags ?? new StorageBuffer(this.device, this.queue, { label: "scale:extract:flags", byteLength: count * 4, copySrc: true }); assert(values.byteLength >= count * 4, "extractScaleValuesF32: values buffer too small for count"); assert(flags.byteLength >= count * 4, "extractScaleValuesF32: flags buffer too small for count"); if (count === 0) return { values, flags }; const params = this.getScaleExtractParamsBuffer(); const paramsData = this.scaleExtractParamsData; paramsData[0] = count >>> 0; paramsData[1] = componentCount >>> 0; paramsData[2] = componentIndex >>> 0; paramsData[3] = scaleValueModeToId(valueMode) >>> 0; paramsData[4] = stride >>> 0; paramsData[5] = offset >>> 0; paramsData[6] = 0; paramsData[7] = 0; this.queue.writeBuffer(params, 0, paramsData); const commands = []; const pipeline = this.getScaleExtractF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(src, requiredSourceFloats * 4), 1: this.bindSized(values, count * 4), 2: this.bindSized(flags, count * 4), 3: { buffer: params, size: 32 } }, "scale:extract:bg"); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "scale:extract" }); this.execute(commands, opts); return { values, flags }; } histogramF32(values, binCount, opts) { assert(!opts.encoder, "histogramF32 does not support opts.encoder"); assert(Number.isInteger(binCount) && binCount >= 0, `histogramF32: binCount must be an integer >= 0 (got ${binCount})`); const count = this.resolveCount(values, 4, opts.count); const bins = opts.bins ?? new StorageBuffer(this.device, this.queue, { label: "histogramF32:bins", byteLength: binCount * 4, copySrc: true }); assert(bins.byteLength >= binCount * 4, "histogramF32: bins buffer is too small for binCount"); const commands = []; const shouldClear = binCount > 0 && (opts.clear ?? true); const nativeClear = shouldClear && (bins.usage & GPUBufferUsage.COPY_DST) !== 0; if (shouldClear && !nativeClear) { const pipelineClear = this.getHistogramClearPipeline(); const bgClear = pipelineClear.createBindGroup(0, { 0: this.bindSized(bins, binCount * 4) }, "histogramF32:clear:bg"); commands.push({ pipeline: pipelineClear, bindGroups: [bgClear], workgroups: workgroups1D(binCount, 256), label: "histogramF32:clear" }); } if (count > 0 && binCount > 0 && Number.isFinite(opts.minValue) && Number.isFinite(opts.maxValue) && opts.maxValue > opts.minValue) { const params = this.getScaleHistogramParamsBuffer(); const raw = this.scaleHistogramParamsData; const dv = this.scaleHistogramParamsView; dv.setUint32(0, count >>> 0, true); dv.setUint32(4, binCount >>> 0, true); dv.setFloat32(8, opts.minValue, true); dv.setFloat32(12, opts.maxValue, true); this.queue.writeBuffer(params, 0, raw); const local256 = binCount <= 256; const pipelineHist = this.getScaleHistogramF32Pipeline(local256); const bgHist = pipelineHist.createBindGroup(0, { 0: this.bindSized(values, count * 4), 1: this.bindSized(bins, binCount * 4), 2: { buffer: params, size: 16 } }, "histogramF32:hist:bg"); commands.push({ pipeline: pipelineHist, bindGroups: [bgHist], workgroups: workgroups1D(count, local256 ? 1024 : 256), label: "histogramF32:accum" }); } this.executeHistogramCommands(commands, bins, binCount, nativeClear, opts); return bins; } remapScaleF32(input, opts) { assert(!opts.encoder, "remapScaleF32 does not support opts.encoder"); const count = this.resolveCount(input, 4, opts.count); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "scale:remap:out", byteLength: count * 4, copySrc: true }); assert(out.byteLength >= count * 4, "remapScaleF32: out buffer is too small for requested count"); if (count === 0) return out; const transform = normalizeScaleTransform(opts.transform); const packed = this.scaleTransformPacked; packScaleTransform(transform, packed, 0); const params = this.getScaleRemapParamsBuffer(); const raw = this.scaleRemapParamsData; const dv = this.scaleRemapParamsView; dv.setUint32(0, count >>> 0, true); const f32 = this.scaleRemapParamsF32; f32[4] = packed[4]; f32[5] = packed[5]; f32[6] = 0; f32[7] = scaleClampModeToId(transform.clampMode); f32[8] = packed[8]; f32[9] = packed[9]; f32[10] = packed[10]; f32[11] = packed[11]; f32[12] = scaleModeToId(transform.mode); f32[13] = packed[13]; f32[14] = packed[14]; f32[15] = packed[15]; f32[16] = packed[16]; f32[17] = 0; f32[18] = 0; f32[19] = 0; this.queue.writeBuffer(params, 0, raw); const pipeline = this.getScaleRemapF32Pipeline(); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 4), 1: this.bindSized(out, count * 4), 2: { buffer: params, size: 80 } }, "scale:remap:bg"); this.execute([ { pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: "scale:remap" } ], opts); return out; } reduceF32(input, op, opts = {}) { const count = this.resolveCount(input, 4, opts.count); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: `reduceF32:${op}`, byteLength: 4, copySrc: true }); assert(out.byteLength >= 4, "reduceF32: out buffer must be at least 4 bytes"); if (count === 0) { this.writeScalarF32Bits(out, identityF32Bits(op)); return out; } const commands = []; this.encodeReduceScalar(commands, "f32", op, input, count, out, `reduceF32:${op}`); this.execute(commands, opts); return out; } sumF32(input, opts = {}) { return this.reduceF32(input, "sum", opts); } minF32(input, opts = {}) { return this.reduceF32(input, "min", opts); } maxF32(input, opts = {}) { return this.reduceF32(input, "max", opts); } argminF32(input, opts = {}) { return this.argReduceF32(input, "argmin", opts); } argmaxF32(input, opts = {}) { return this.argReduceF32(input, "argmax", opts); } argReduceF32(input, op, opts = {}) { const count = this.resolveCount(input, 4, opts.count); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: `argReduceF32:${op}`, byteLength: 8, copySrc: true }); assert(out.byteLength >= 8, "argReduceF32: out buffer must be at least 8 bytes"); if (count === 0) { const id = identityArgPairBits(op); this.writeArgPairBits(out, id.valueBits, id.index); return out; } const commands = []; this.encodeArgReduceF32Scalar(commands, op, input, count, out, `argReduceF32:${op}`); this.execute(commands, opts); return out; } scanExclusiveU32(input, opts = {}) { const count = this.resolveCount(input, 4, opts.count); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: "scanExclusiveU32", byteLength: count * 4, copySrc: true }); assert(out.byteLength >= count * 4, "scanExclusiveU32: out buffer is too small for requested count"); if (count === 0) return out; const commands = []; this.encodeScanExclusiveU32Into(commands, input, count, out, "scanExclusiveU32"); this.execute(commands, opts); return out; } histogramU32(keys, binCount, opts = {}) { assert(Number.isInteger(binCount) && binCount >= 0, `binCount must be an integer >= 0 (got ${binCount})`); const count = this.resolveCount(keys, 4, opts.count); const bins = opts.bins ?? new StorageBuffer(this.device, this.queue, { label: "histogramU32:bins", byteLength: binCount * 4, copySrc: true }); assert(bins.byteLength >= binCount * 4, "histogramU32: bins buffer is too small for binCount"); const commands = []; const shouldClear = binCount > 0 && (opts.clear ?? true); const nativeClear = shouldClear && (bins.usage & GPUBufferUsage.COPY_DST) !== 0; if (shouldClear && !nativeClear) { const pipelineClear = this.getHistogramClearPipeline(); const bgClear = pipelineClear.createBindGroup(0, { 0: this.bindSized(bins, binCount * 4) }, "histogramU32:clear:bg"); commands.push({ pipeline: pipelineClear, bindGroups: [bgClear], workgroups: workgroups1D(binCount, 256), label: "histogramU32:clear" }); } if (count > 0 && binCount > 0) { const local256 = binCount <= 256; const pipelineHist = this.getHistogramPipeline(local256); const bgHist = pipelineHist.createBindGroup(0, { 0: this.bindSized(keys, count * 4), 1: this.bindSized(bins, binCount * 4) }, "histogramU32:hist:bg"); commands.push({ pipeline: pipelineHist, bindGroups: [bgHist], workgroups: workgroups1D(count, local256 ? 1024 : 256), label: "histogramU32:accum" }); } this.executeHistogramCommands(commands, bins, binCount, nativeClear, opts); return bins; } compactU32(input, flags, opts = {}) { return this.compactTyped(input, flags, "u32", opts); } compactF32(input, flags, opts = {}) { return this.compactTyped(input, flags, "f32", opts); } compactTyped(input, flags, type, opts) { const count = this.resolveCount(flags, 4, opts.count); const inputCount = this.resolveCount(input, 4, opts.count); assert(inputCount === count, "compact: input and flags counts must match"); const out = opts.out ?? new StorageBuffer(this.device, this.queue, { label: `compact:${type}:out`, byteLength: count * 4, copySrc: true }); assert(out.byteLength >= count * 4, "compact: out buffer is too small for requested count"); const countOut = new StorageBuffer(this.device, this.queue, { label: `compact:${type}:count`, byteLength: 4, copySrc: true }); if (count === 0) { this.writeScalarU32(countOut, 0); return { output: out, count: countOut }; } const prefix = this.scratch.acquire(count * 4, `compact:${type}:prefix`); const commands = []; this.encodeScanExclusiveU32Into(commands, flags, count, prefix, `compact:${type}:scan`); { const pipeline = this.getCompactPipeline(type); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(input, count * 4), 1: this.bindSized(flags, count * 4), 2: this.bindSized(prefix, count * 4), 3: this.bindSized(out, count * 4), 4: this.bindSized(countOut, 4) }, `compact:${type}:compact:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `compact:${type}:scatter` }); } this.execute(commands, opts); return { output: out, count: countOut }; } radixSortKeysU32(keys, opts = {}) { const count = this.resolveCount(keys, 4, opts.count); const inPlace = opts.inPlace ?? false; const out = inPlace ? keys : opts.out ?? new StorageBuffer(this.device, this.queue, { label: "radixSortKeysU32:out", byteLength: count * 4, copySrc: true }); if (!inPlace) assert(out.byteLength >= count * 4, "radixSortKeysU32: out buffer is too small for requested count"); if (count <= 1) { if (!inPlace && count === 1) { const commands2 = []; this.encodeCopyU32(commands2, keys, 1, out, "radixSortKeysU32"); this.execute(commands2, opts); } return out; } const prefix = this.scratch.acquire(count * 4, "radix:prefix"); const scratchKeys = this.scratch.acquire(count * 4, "radix:keysScratch"); const bufA = scratchKeys; const bufB = out; let inBuf = keys; let outBuf = bufA; const commands = []; for (let bit = 0; bit < 32; bit++) { this.encodeRadixZeroScanInto(commands, inBuf, count, prefix, bit, `radix:bit${bit}:scan`); { const pipeline = this.getRadixScatterPipeline(bit); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(inBuf, count * 4), 1: this.bindSized(prefix, count * 4), 2: this.bindSized(outBuf, count * 4) }, `radix:bit${bit}:scatter:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `radix:bit${bit}:scatter` }); } inBuf = outBuf; outBuf = outBuf === bufA ? bufB : bufA; } if (inPlace) { if (inBuf !== keys) this.encodeCopyU32(commands, inBuf, count, keys, "radixSortKeysU32:finalize"); else if (inBuf !== out) this.encodeCopyU32(commands, inBuf, count, out, "radixSortKeysU32:finalize"); } this.execute(commands, opts); return out; } radixSortPairsU32(keys, values, opts = {}) { assert(keys !== values, "radixSortPairsU32: keys and values must be distinct StorageBuffer instances"); const count = this.resolveCount(keys, 4, opts.count); this.resolveCount(values, 4, count); const inPlace = opts.inPlace ?? false; assert(!(inPlace && (opts.outKeys !== void 0 || opts.outValues !== void 0)), "radixSortPairsU32: outKeys/outValues are not supported when inPlace is true"); const outKeys = inPlace ? keys : opts.outKeys ?? new StorageBuffer(this.device, this.queue, { label: "radixSortPairsU32:outKeys", byteLength: count * 4, copySrc: true }); const outValues = inPlace ? values : opts.outValues ?? new StorageBuffer(this.device, this.queue, { label: "radixSortPairsU32:outValues", byteLength: count * 4, copySrc: true }); if (!inPlace) { assertByteLengthMultipleOf(outKeys.byteLength, 4, "radixSortPairsU32: outKeys"); assertByteLengthMultipleOf(outValues.byteLength, 4, "radixSortPairsU32: outValues"); assert(outKeys.byteLength >= count * 4, "radixSortPairsU32: outKeys buffer is too small for requested count"); assert(outValues.byteLength >= count * 4, "radixSortPairsU32: outValues buffer is too small for requested count"); assert(outKeys !== outValues, "radixSortPairsU32: outKeys and outValues must be distinct StorageBuffer instances"); assert(outKeys !== keys && outKeys !== values && outValues !== keys && outValues !== values, "radixSortPairsU32: output buffers must be distinct from input buffers unless inPlace is true"); } if (count <= 1) { if (!inPlace && count === 1) { const commands2 = []; this.encodeCopyU32(commands2, keys, 1, outKeys, "radixSortPairsU32:keys"); this.encodeCopyU32(commands2, values, 1, outValues, "radixSortPairsU32:values"); this.execute(commands2, opts); } return { keys: outKeys, values: outValues }; } const prefix = this.scratch.acquire(count * 4, "radix:prefix"); const scratchKeys = this.scratch.acquire(count * 4, "radix:keysScratch"); const scratchValues = this.scratch.acquire(count * 4, "radix:valuesScratch"); const keyBufA = scratchKeys; const keyBufB = outKeys; const valueBufA = scratchValues; const valueBufB = outValues; let inKeys = keys; let outKeysBuf = keyBufA; let inValues = values; let outValuesBuf = valueBufA; const commands = []; for (let bit = 0; bit < 32; bit++) { this.encodeRadixZeroScanInto(commands, inKeys, count, prefix, bit, `radixPairs:bit${bit}:scan`); { const pipeline = this.getRadixScatterPairsPipeline(bit); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(inKeys, count * 4), 1: this.bindSized(inValues, count * 4), 2: this.bindSized(prefix, count * 4), 3: this.bindSized(outKeysBuf, count * 4), 4: this.bindSized(outValuesBuf, count * 4) }, `radixPairs:bit${bit}:scatter:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `radixPairs:bit${bit}:scatter` }); } inKeys = outKeysBuf; outKeysBuf = outKeysBuf === keyBufA ? keyBufB : keyBufA; inValues = outValuesBuf; outValuesBuf = outValuesBuf === valueBufA ? valueBufB : valueBufA; } if (inPlace) { if (inKeys !== keys) this.encodeCopyU32(commands, inKeys, count, keys, "radixSortPairsU32:finalizeKeys"); if (inValues !== values) this.encodeCopyU32(commands, inValues, count, values, "radixSortPairsU32:finalizeValues"); } else { if (inKeys !== outKeys) this.encodeCopyU32(commands, inKeys, count, outKeys, "radixSortPairsU32:finalizeKeys"); if (inValues !== outValues) this.encodeCopyU32(commands, inValues, count, outValues, "radixSortPairsU32:finalizeValues"); } this.execute(commands, opts); return { keys: outKeys, values: outValues }; } luFactorF32Batched(matrices, ipiv, batchCount, n, opts = {}) { assert(!opts.encoder, "luFactorF32Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luFactorF32Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luFactorF32Batched: n must be an integer >= 0 (got ${n})`); assert(matrices !== ipiv, "luFactorF32Batched: matrices and ipiv must be distinct StorageBuffer instances"); if (batchCount === 0 || n === 0) return; const elemsPerMatrix = n * n; assert(Number.isFinite(elemsPerMatrix) && elemsPerMatrix <= 4294967295, "luFactorF32Batched: n*n overflow"); const needBytes = batchCount * elemsPerMatrix * 4; const ipivBytes = batchCount * n * 4; assert(matrices.byteLength >= needBytes, `luFactorF32Batched: matrices buffer too small (need ${needBytes} bytes, have ${matrices.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luFactorF32Batched: ipiv buffer too small (need ${ipivBytes} bytes, have ${ipiv.byteLength})`); if (n < 160) { const params2 = this.getLuBatchedParamsBuffer(); this.queue.writeBuffer(params2, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, 0])); const pipeline = this.getLuFactorRealPipeline(); const bg = pipeline.createBindGroup(0, { 0: { buffer: params2, size: 16 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorF32Batched:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luFactorF32Batched" }], opts); return; } const params = this.getLuBlockedParamsBuffer(); const leadPipe = this.getLuFactorRealLeadPipeline(); const upperPipe = this.getLuFactorRealUpperPipeline(); const trailingPipe = this.getLuFactorRealTrailingPipeline(); const bgLead = leadPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorF32:lead:bg"); const bgUpper = upperPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorF32:upper:bg"); const bgTrailing = trailingPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorF32:trailing:bg"); const PANEL_B = 16; for (let kk = 0; kk < n; kk += PANEL_B) { const pw = Math.min(PANEL_B, n - kk); const trail = n - (kk + pw); this.queue.writeBuffer(params, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, kk >>> 0, pw >>> 0, 0, 0, 0])); const cmds = [{ pipeline: leadPipe, bindGroups: [bgLead], workgroups: { x: batchCount, y: 1, z: 1 }, label: `luFactorF32:lead:kk${kk}` }]; if (trail > 0) { const totalTrsm = batchCount * trail; cmds.push({ pipeline: upperPipe, bindGroups: [bgUpper], workgroups: workgroups1D(totalTrsm, 256), label: `luFactorF32:upper:kk${kk}` }); const mDim = trail; const nDim = trail; const tileM = 16; const tileN = 8; const mTiles = Math.ceil(mDim / tileM); const nTiles = Math.ceil(nDim / tileN); cmds.push({ pipeline: trailingPipe, bindGroups: [bgTrailing], workgroups: { x: nTiles, y: mTiles, z: batchCount }, label: `luFactorF32:trailing:kk${kk}` }); } this.execute(cmds, opts); } } luSolveF32Batched(lu, ipiv, rhs, outX, batchCount, n, opts = {}) { assert(!opts.encoder, "luSolveF32Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luSolveF32Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luSolveF32Batched: n must be an integer >= 0 (got ${n})`); assert(lu !== rhs && lu !== outX && lu !== ipiv && rhs !== outX && rhs !== ipiv && outX !== ipiv, "luSolveF32Batched: lu, ipiv, rhs, and outX must be distinct StorageBuffer instances"); if (batchCount === 0 || n === 0) return; const elemsPerMatrix = n * n; assert(Number.isFinite(elemsPerMatrix) && elemsPerMatrix <= 4294967295, "luSolveF32Batched: n*n overflow"); const luBytes = batchCount * elemsPerMatrix * 4; const rhsBytes = batchCount * n * 4; const ipivBytes = batchCount * n * 4; assert(lu.byteLength >= luBytes, `luSolveF32Batched: lu buffer too small (need ${luBytes} bytes, have ${lu.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luSolveF32Batched: ipiv buffer too small (need ${ipivBytes} bytes, have ${ipiv.byteLength})`); assert(rhs.byteLength >= rhsBytes, `luSolveF32Batched: rhs buffer too small (need ${rhsBytes} bytes, have ${rhs.byteLength})`); assert(outX.byteLength >= rhsBytes, `luSolveF32Batched: outX buffer too small (need ${rhsBytes} bytes, have ${outX.byteLength})`); const params = this.getLuBatchedParamsBuffer(); this.queue.writeBuffer(params, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, 0])); const pipeline = n <= 512 ? this.getLuSolveRealSharedPipeline() : this.getLuSolveRealPipeline(); const bg = pipeline.createBindGroup(0, { 0: { buffer: params, size: 16 }, 1: this.bindSized(lu, luBytes), 2: this.bindSized(rhs, rhsBytes), 3: this.bindSized(outX, rhsBytes), 4: this.bindSized(ipiv, ipivBytes) }, "luSolveF32Batched:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luSolveF32Batched" }], opts); } luFactorC64Batched(matrices, ipiv, batchCount, n, opts = {}) { assert(!opts.encoder, "luFactorC64Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luFactorC64Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luFactorC64Batched: n must be an integer >= 0 (got ${n})`); assert(matrices !== ipiv, "luFactorC64Batched: matrices and ipiv must be distinct StorageBuffer instances"); if (batchCount === 0 || n === 0) return; const elemsPerMatrix = n * n; assert(Number.isFinite(elemsPerMatrix) && elemsPerMatrix <= 4294967295, "luFactorC64Batched: n*n overflow"); const needBytes = batchCount * elemsPerMatrix * 8; const ipivBytes = batchCount * n * 4; assert(matrices.byteLength >= needBytes, `luFactorC64Batched: matrices buffer too small (need ${needBytes} bytes, have ${matrices.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luFactorC64Batched: ipiv buffer too small (need ${ipivBytes} bytes, have ${ipiv.byteLength})`); if (n < 160) { const params2 = this.getLuBatchedParamsBuffer(); this.queue.writeBuffer(params2, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, 0])); const pipeline = this.getLuFactorC64SmallPipeline(); const bg = pipeline.createBindGroup(0, { 0: { buffer: params2, size: 16 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorC64Batched:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luFactorC64Batched" }], opts); return; } const params = this.getLuBlockedParamsBuffer(); const leadPipe = this.getLuFactorC64LeadPipeline(); const upperPipe = this.getLuFactorC64UpperPipeline(); const trailingPipe = this.getLuFactorC64TrailingPipeline(); const bgLead = leadPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorC64:lead:bg"); const bgUpper = upperPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorC64:upper:bg"); const bgTrailing = trailingPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorC64:trailing:bg"); const PANEL_B = 16; for (let kk = 0; kk < n; kk += PANEL_B) { const pw = Math.min(PANEL_B, n - kk); const trail = n - (kk + pw); this.queue.writeBuffer(params, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, kk >>> 0, pw >>> 0, 0, 0, 0])); const cmds = [{ pipeline: leadPipe, bindGroups: [bgLead], workgroups: { x: batchCount, y: 1, z: 1 }, label: `luFactorC64:lead:kk${kk}` }]; if (trail > 0) { const totalTrsm = batchCount * trail; cmds.push({ pipeline: upperPipe, bindGroups: [bgUpper], workgroups: workgroups1D(totalTrsm, 256), label: `luFactorC64:upper:kk${kk}` }); const mDim = trail; const nDim = trail; const tileM = 16; const tileN = 8; const mTiles = Math.ceil(mDim / tileM); const nTiles = Math.ceil(nDim / tileN); cmds.push({ pipeline: trailingPipe, bindGroups: [bgTrailing], workgroups: { x: nTiles, y: mTiles, z: batchCount }, label: `luFactorC64:trailing:kk${kk}` }); } this.execute(cmds, opts); } } luSolveC64Batched(lu, ipiv, rhs, outX, batchCount, n, opts = {}) { assert(!opts.encoder, "luSolveC64Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luSolveC64Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luSolveC64Batched: n must be an integer >= 0 (got ${n})`); assert(lu !== rhs && lu !== outX && lu !== ipiv && rhs !== outX && rhs !== ipiv && outX !== ipiv, "luSolveC64Batched: lu, ipiv, rhs, and outX must be distinct StorageBuffer instances"); if (batchCount === 0 || n === 0) return; const elemsPerMatrix = n * n; assert(Number.isFinite(elemsPerMatrix) && elemsPerMatrix <= 4294967295, "luSolveC64Batched: n*n overflow"); const luBytes = batchCount * elemsPerMatrix * 8; const rhsBytes = batchCount * n * 8; const ipivBytes = batchCount * n * 4; assert(lu.byteLength >= luBytes, `luSolveC64Batched: lu buffer too small (need ${luBytes} bytes, have ${lu.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luSolveC64Batched: ipiv buffer too small (need ${ipivBytes} bytes, have ${ipiv.byteLength})`); assert(rhs.byteLength >= rhsBytes, `luSolveC64Batched: rhs buffer too small (need ${rhsBytes} bytes, have ${rhs.byteLength})`); assert(outX.byteLength >= rhsBytes, `luSolveC64Batched: outX buffer too small (need ${rhsBytes} bytes, have ${outX.byteLength})`); const params = this.getLuBatchedParamsBuffer(); this.queue.writeBuffer(params, 0, new Uint32Array([batchCount >>> 0, n >>> 0, elemsPerMatrix >>> 0, 0])); const pipeline = n <= 512 ? this.getLuSolveC64Pipeline() : this.getLuSolveC64LargePipeline(); const bg = pipeline.createBindGroup(0, { 0: { buffer: params, size: 16 }, 1: this.bindSized(lu, luBytes), 2: this.bindSized(rhs, rhsBytes), 3: this.bindSized(outX, rhsBytes), 4: this.bindSized(ipiv, ipivBytes) }, "luSolveC64Batched:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luSolveC64Batched" }], opts); } }; // wgsl/compute/blit-rgba8.wgsl var blit_rgba8_default = "struct Params { p0: vec4, p1: vec4, } struct VertexOutput { @builtin(position) position: vec4, } @group(0) @binding(0) var params: Params; @group(0) @binding(1) var pixels: array; fn unpack_rgba8(x: u32) -> vec4 { let r = f32(x & 255u) / 255.0; let g = f32((x >> 8u) & 255u) / 255.0; let b = f32((x >> 16u) & 255u) / 255.0; let a = f32((x >> 24u) & 255u) / 255.0; return vec4(r, g, b, a); } @vertex fn vs_main(@builtin(vertex_index) vid: u32) -> VertexOutput { var pos = array, 3>( vec2(-1.0, -1.0), vec2( 3.0, -1.0), vec2(-1.0, 3.0), ); var out: VertexOutput; out.position = vec4(pos[vid], 0.0, 1.0); return out; } @fragment fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { let display_w = max(1.0, params.p0.x); let display_h = max(1.0, params.p0.y); let out_w = max(1.0, params.p0.z); let out_h = max(1.0, params.p0.w); let flip_y = params.p1.x > 0.5; let x_out = clamp(i32(floor(pos.x * out_w / display_w)), 0, i32(out_w) - 1); var y_out = clamp(i32(floor(pos.y * out_h / display_h)), 0, i32(out_h) - 1); if (flip_y) { y_out = i32(out_h) - 1 - y_out; } let idx = u32(y_out) * u32(out_w) + u32(x_out); return unpack_rgba8(pixels[idx]); }"; // typescript/compute/blit.ts var getDefaultCanvasFormat = () => { const nav = typeof navigator !== "undefined" ? navigator : null; const gpu = nav && nav.gpu ? nav.gpu : null; if (gpu && typeof gpu.getPreferredCanvasFormat === "function") return gpu.getPreferredCanvasFormat(); throw new Error("blitRGBA8BufferToCanvas: opts.format must be provided when navigator.gpu is unavailable."); }; var RGBA8BufferCanvasBlitter = class { device; queue; paramsStride; paramsCapacity; paramsBuffer; paramsF32; paramsIndex = 0; pipelineByFormat = /* @__PURE__ */ new Map(); canvasState = /* @__PURE__ */ new WeakMap(); constructor(device, queue, opts = {}) { this.device = device; this.queue = queue; const alignment = Math.max(256, device.limits.minUniformBufferOffsetAlignment); this.paramsStride = alignTo(32, alignment); this.paramsCapacity = Math.max(1, opts.uniformCapacity ?? 256); this.paramsBuffer = device.createBuffer({ label: "WasmGPU:compute:blitRGBA8:params", size: this.paramsStride * this.paramsCapacity, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.paramsF32 = new Float32Array(8); } destroy() { this.paramsBuffer.destroy(); this.pipelineByFormat.clear(); } encode(encoder, canvas, src, outWidth, outHeight, opts = {}) { assert(isNonNegativeInt(outWidth) && isNonNegativeInt(outHeight), `outWidth/outHeight must be integers >= 0 (got ${outWidth}x${outHeight})`); if (outWidth === 0 || outHeight === 0) return; const state = this.getOrCreateCanvasState(canvas); const format = opts.format ?? state.format ?? getDefaultCanvasFormat(); const alphaMode = opts.alphaMode ?? state.alphaMode ?? "opaque"; const didResize = opts.autoResize ?? true ? this.autoResizeCanvas(canvas, state, opts.dpr) : this.syncCanvasSizeWithoutResize(canvas, state); const needsConfigure = !state.configured || didResize || state.format !== format || state.alphaMode !== alphaMode; if (needsConfigure) { state.context.configure({ device: this.device, format, alphaMode }); state.configured = true; state.format = format; state.alphaMode = alphaMode; } const displayW = Math.max(1, canvas.width); const displayH = Math.max(1, canvas.height); this.paramsF32[0] = displayW; this.paramsF32[1] = displayH; this.paramsF32[2] = outWidth; this.paramsF32[3] = outHeight; this.paramsF32[4] = opts.flipY ? 1 : 0; this.paramsF32[5] = 0; this.paramsF32[6] = 0; this.paramsF32[7] = 0; const uniformOffset = this.allocParamsChunk(); this.queue.writeBuffer(this.paramsBuffer, uniformOffset, this.paramsF32.buffer, this.paramsF32.byteOffset, this.paramsF32.byteLength); const pipelineState = this.getPipeline(format); const srcBuffer = resolveGPUBuffer(src); let bindGroup = pipelineState.bindGroups.get(srcBuffer); if (!bindGroup) { bindGroup = this.device.createBindGroup({ label: opts.label ? `${opts.label}:bindGroup` : void 0, layout: pipelineState.bindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.paramsBuffer, offset: 0, size: 32 } }, { binding: 1, resource: { buffer: srcBuffer } } ] }); pipelineState.bindGroups.set(srcBuffer, bindGroup); } const view = state.context.getCurrentTexture().createView(); const loadOp = opts.loadOp ?? "load"; const storeOp = opts.storeOp ?? "store"; const clearValue = opts.clearColor ?? { r: 0, g: 0, b: 0, a: 1 }; const pass = encoder.beginRenderPass({ label: opts.label, colorAttachments: [ { view, clearValue, loadOp, storeOp } ] }); pass.setPipeline(pipelineState.pipeline); pass.setBindGroup(0, bindGroup, [uniformOffset]); pass.draw(3, 1, 0, 0); pass.end(); } allocParamsChunk() { const idx = this.paramsIndex++; if (this.paramsIndex >= this.paramsCapacity) this.paramsIndex = 0; return idx % this.paramsCapacity * this.paramsStride; } getOrCreateCanvasState(canvas) { const cached = this.canvasState.get(canvas); if (cached) return cached; const context = canvas.getContext("webgpu"); assert(!!context, "blitRGBA8BufferToCanvas: failed to acquire a WebGPU canvas context"); const state = { context, width: Math.max(1, canvas.width), height: Math.max(1, canvas.height), format: null, alphaMode: "opaque", configured: false }; this.canvasState.set(canvas, state); return state; } autoResizeCanvas(canvas, state, dprOverride) { const dpr = dprOverride ?? Math.max(1, typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1); const w = Math.max(1, Math.floor(canvas.clientWidth * dpr)); const h = Math.max(1, Math.floor(canvas.clientHeight * dpr)); if (w === state.width && h === state.height) return false; state.width = w; state.height = h; canvas.width = w; canvas.height = h; return true; } syncCanvasSizeWithoutResize(canvas, state) { const w = Math.max(1, canvas.width); const h = Math.max(1, canvas.height); if (w === state.width && h === state.height) return false; state.width = w; state.height = h; return true; } getPipeline(format) { const cached = this.pipelineByFormat.get(format); if (cached) return cached; const module = this.device.createShaderModule({ label: "WasmGPU:compute:blitRGBA8:shader", code: blit_rgba8_default }); const bindGroupLayout = this.device.createBindGroupLayout({ label: "WasmGPU:compute:blitRGBA8:bgl", entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 32 } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } } ] }); const pipeline = this.device.createRenderPipeline({ label: "WasmGPU:compute:blitRGBA8:pipeline", layout: this.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), vertex: { module, entryPoint: "vs_main" }, fragment: { module, entryPoint: "fs_main", targets: [{ format }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); const state = { pipeline, bindGroupLayout, bindGroups: /* @__PURE__ */ new WeakMap() }; this.pipelineByFormat.set(format, state); return state; } }; // typescript/compute/readback.ts var resolveLogicalByteLength = (src) => { if (src instanceof StorageBuffer) return src.byteLength; return Number(src.size); }; var resolveSourceUsage = (src) => { if (src instanceof StorageBuffer) return src.usage; const usage = src.usage; return typeof usage === "number" ? usage : null; }; var ReadbackRing = class { device; queue; labelPrefix; slots = []; cursor = 0; destroyed = false; constructor(device, queue, desc = {}) { this.device = device; this.queue = queue; const slotCount = Math.max(1, desc.slots ?? 3); this.labelPrefix = desc.labelPrefix ?? "WasmGPU:readback"; for (let i = 0; i < slotCount; i++) this.slots.push(this.createSlot(4, i)); } createSlot(capacityBytes, index) { const size = Math.max(4, alignTo(capacityBytes, 4)); const buffer = this.device.createBuffer({ label: `${this.labelPrefix}:slot${index}`, size, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); return { buffer, capacityBytes: size, tail: Promise.resolve() }; } ensureNotDestroyed() { assert(!this.destroyed, "ReadbackRing is destroyed"); } get isDestroyed() { return this.destroyed; } assertSourceCanReadback(src) { if (src instanceof StorageBuffer) { assert(src.canReadback, "ReadbackRing requires the source StorageBuffer to be created with copySrc: true"); return; } const usage = resolveSourceUsage(src); if (usage !== null) assert((usage & GPUBufferUsage.COPY_SRC) !== 0, "ReadbackRing requires the source GPUBuffer to have GPUBufferUsage.COPY_SRC"); } validateReadRange(src, srcOffsetBytes, sizeBytes) { this.ensureNotDestroyed(); assert(this.slots.length > 0, "ReadbackRing has no slots"); assert(isNonNegativeInt(srcOffsetBytes), `srcOffsetBytes must be an integer >= 0 (got ${srcOffsetBytes})`); assert((srcOffsetBytes & 3) === 0, `srcOffsetBytes must be 4-byte aligned for readback (got ${srcOffsetBytes})`); this.assertSourceCanReadback(src); const logicalByteLength = resolveLogicalByteLength(src); const remaining = logicalByteLength - srcOffsetBytes; assert(remaining >= 0, `srcOffsetBytes (${srcOffsetBytes}) exceeds source byteLength (${logicalByteLength})`); const size = sizeBytes ?? remaining; assert(isNonNegativeInt(size), `sizeBytes must be an integer >= 0 (got ${size})`); assert(size <= remaining, `sizeBytes (${size}) exceeds remaining bytes (${remaining})`); return size; } async copyMappedBytes(src, srcOffsetBytes, size, opts, destination) { if (size === 0) return; const alignedSize = alignTo(size, 4); const physicalByteLength = Number(resolveGPUBuffer(src).size); assert(srcOffsetBytes + alignedSize <= physicalByteLength, `Aligned copy range (offset ${srcOffsetBytes}, size ${alignedSize}) exceeds physical source bytes (${physicalByteLength})`); const slotIndex = this.cursor; this.cursor = (this.cursor + 1) % this.slots.length; const slot = this.slots[slotIndex]; const run = async () => { this.ensureNotDestroyed(); if (slot.buffer.mapState === "mapped" || slot.buffer.mapState === "pending") { try { slot.buffer.unmap(); } catch { } assert(slot.buffer.mapState !== "mapped" && slot.buffer.mapState !== "pending", "ReadbackRing internal error: staging buffer is still mapped"); } if (alignedSize > slot.capacityBytes) { try { slot.buffer.destroy(); } catch { } const newSlot = this.createSlot(alignedSize, slotIndex); slot.buffer = newSlot.buffer; slot.capacityBytes = newSlot.capacityBytes; } const srcBuf = resolveGPUBuffer(src); const encoder = this.device.createCommandEncoder({ label: opts.label ? `${opts.label}:copyToStaging` : `${this.labelPrefix}:copyToStaging` }); encoder.copyBufferToBuffer(srcBuf, srcOffsetBytes, slot.buffer, 0, alignedSize); this.queue.submit([encoder.finish()]); try { await slot.buffer.mapAsync(GPUMapMode.READ, 0, alignedSize); const mapped = slot.buffer.getMappedRange(0, alignedSize); const out = destination(); assert(out.byteLength >= size, `readback destination is too small (${out.byteLength} bytes for ${size} bytes)`); out.set(new Uint8Array(mapped, 0, size), 0); slot.buffer.unmap(); } catch (e) { try { slot.buffer.unmap(); } catch { } throw e; } }; const job = slot.tail.then(run, run); slot.tail = job.then(() => { }, () => { }); return job; } async read(src, srcOffsetBytes = 0, sizeBytes, opts = {}) { const size = this.validateReadRange(src, srcOffsetBytes, sizeBytes); const out = new Uint8Array(size); await this.copyMappedBytes(src, srcOffsetBytes, size, opts, () => out); return out.buffer; } async readIntoWasmMemory(mem, dstPtrBytes, src, srcOffsetBytes = 0, sizeBytes, opts = {}) { assert(isNonNegativeInt(dstPtrBytes), `dstPtrBytes must be an integer >= 0 (got ${dstPtrBytes})`); const size = this.validateReadRange(src, srcOffsetBytes, sizeBytes); await this.copyMappedBytes(src, srcOffsetBytes, size, opts, () => { const buffer = mem.buffer; assert(dstPtrBytes + size <= buffer.byteLength, `readback destination exceeds WebAssembly memory (${dstPtrBytes} + ${size} > ${buffer.byteLength})`); return new Uint8Array(buffer, dstPtrBytes, size); }); } async readAs(ctor, src, srcOffsetBytes = 0, sizeBytes, opts = {}) { const bytes = await this.read(src, srcOffsetBytes, sizeBytes, opts); const bpe = ctor.BYTES_PER_ELEMENT; assert(bytes.byteLength % bpe === 0, `readAs: byteLength (${bytes.byteLength}) is not divisible by BYTES_PER_ELEMENT (${bpe})`); const len = bytes.byteLength / bpe; return new ctor(bytes, 0, len); } readU32(src, elemOffset = 0, elemCount, opts = {}) { assert(isNonNegativeInt(elemOffset), `elemOffset must be an integer >= 0 (got ${elemOffset})`); const byteOffset = elemOffset * 4; const byteLength = elemCount === void 0 ? void 0 : elemCount * 4; return this.readAs(Uint32Array, src, byteOffset, byteLength, opts); } readF32(src, elemOffset = 0, elemCount, opts = {}) { assert(isNonNegativeInt(elemOffset), `elemOffset must be an integer >= 0 (got ${elemOffset})`); const byteOffset = elemOffset * 4; const byteLength = elemCount === void 0 ? void 0 : elemCount * 4; return this.readAs(Float32Array, src, byteOffset, byteLength, opts); } async readScalarU32(src, srcOffsetBytes = 0, opts = {}) { const out = await this.readAs(Uint32Array, src, srcOffsetBytes, 4, opts); return out[0] >>> 0; } async readScalarF32(src, srcOffsetBytes = 0, opts = {}) { const out = await this.readAs(Float32Array, src, srcOffsetBytes, 4, opts); return out[0]; } destroy() { if (this.destroyed) return; this.destroyed = true; for (const slot of this.slots) { try { if (slot.buffer.mapState === "mapped" || slot.buffer.mapState === "pending") slot.buffer.unmap(); } catch { } try { slot.buffer.destroy(); } catch { } slot.tail = Promise.resolve(); } this.slots.length = 0; } }; // typescript/compute/index.ts var Compute = class { device; queue; kernels; readback; ndarray = Ndarray; CPUndarray = CPUndarray; GPUndarray = GPUndarray; _rgba8Blitter = null; constructor(device, queue, desc = {}) { this.device = device; this.queue = queue; this.kernels = new ComputeKernels(device, queue); this.readback = new ReadbackRing(device, queue, desc.readback); } createStorageBuffer(desc) { return new StorageBuffer(this.device, this.queue, desc); } createUniformBuffer(desc) { return new UniformBuffer(this.device, this.queue, desc); } createPipeline(desc) { return new ComputePipeline(this.device, desc); } createReadbackRing(desc = {}) { return new ReadbackRing(this.device, this.queue, desc); } encodeDispatch(encoder, cmd, validateLimits = false) { if (validateLimits) validateWorkgroupsForDevice(this.device, cmd.workgroups); encodeDispatch(encoder, cmd); } encodeDispatchBatch(encoder, commands, label, validateLimits = false) { encodeDispatchBatchWithLimit(encoder, commands, label, validateLimits ? this.device.limits.maxComputeWorkgroupsPerDimension : void 0); } dispatch(cmd, opts = {}) { const encoder = this.device.createCommandEncoder(); this.encodeDispatch(encoder, cmd, opts.validateLimits ?? false); const commandBuffer = encoder.finish(); if (opts.submit !== false) this.queue.submit([commandBuffer]); return commandBuffer; } dispatchBatch(commands, label, opts = {}) { const encoder = this.device.createCommandEncoder(); this.encodeDispatchBatch(encoder, commands, label, opts.validateLimits ?? false); const commandBuffer = encoder.finish(); if (opts.submit !== false) this.queue.submit([commandBuffer]); return commandBuffer; } dispatch1D(pipeline, bindGroups, invocations, workgroupSizeX, label, opts = {}) { const workgroups = workgroups1D(invocations, workgroupSizeX); return this.dispatch({ pipeline, bindGroups, workgroups, label }, opts); } dispatch2D(pipeline, bindGroups, width, height, workgroupSizeX, workgroupSizeY, label, opts = {}) { const workgroups = workgroups2D(width, height, workgroupSizeX, workgroupSizeY); return this.dispatch({ pipeline, bindGroups, workgroups, label }, opts); } dispatch3D(pipeline, bindGroups, width, height, depth, workgroupSizeX, workgroupSizeY, workgroupSizeZ, label, opts = {}) { const workgroups = workgroups3D(width, height, depth, workgroupSizeX, workgroupSizeY, workgroupSizeZ); return this.dispatch({ pipeline, bindGroups, workgroups, label }, opts); } blitRGBA8BufferToCanvas(encoder, canvas, src, outWidth, outHeight, opts = {}) { if (!this._rgba8Blitter) this._rgba8Blitter = new RGBA8BufferCanvasBlitter(this.device, this.queue); this._rgba8Blitter.encode(encoder, canvas, src, outWidth, outHeight, opts); } workgroups1D(invocations, workgroupSizeX) { return workgroups1D(invocations, workgroupSizeX); } workgroups2D(width, height, workgroupSizeX, workgroupSizeY) { return workgroups2D(width, height, workgroupSizeX, workgroupSizeY); } workgroups3D(width, height, depth, workgroupSizeX, workgroupSizeY, workgroupSizeZ) { return workgroups3D(width, height, depth, workgroupSizeX, workgroupSizeY, workgroupSizeZ); } destroy() { this._rgba8Blitter?.destroy(); this._rgba8Blitter = null; this.readback.destroy(); this.kernels.destroy(); } }; // typescript/gltf/uri.ts var URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/; var RELATIVE_URI_ORIGIN = "https://wasmgpu-relative.invalid"; var MIME_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; var PARAMETER_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+=(?:[!#$%&'*+\-.^_`|~0-9A-Za-z]+|"[^"\r\n]*")$/; var isAbsoluteUri = (value) => URI_SCHEME_PATTERN.test(value); var dataUriError = (uri, message) => { const preview = uri.length > 64 ? `${uri.slice(0, 64)}...` : uri; return new Error(`Invalid data URI (${preview}): ${message}`); }; var hexValue = (value) => { if (value >= 48 && value <= 57) return value - 48; if (value >= 65 && value <= 70) return value - 65 + 10; if (value >= 97 && value <= 102) return value - 97 + 10; return -1; }; var decodeMetadataToken = (token, uri) => { const out = []; for (let i = 0; i < token.length; i++) { const code = token.charCodeAt(i); if (code !== 37) { if (code > 127) throw dataUriError(uri, "metadata must use ASCII characters"); out.push(code); continue; } if (i + 2 >= token.length) throw dataUriError(uri, "malformed percent escape in metadata"); const high = hexValue(token.charCodeAt(++i)); const low = hexValue(token.charCodeAt(++i)); if (high < 0 || low < 0) throw dataUriError(uri, "malformed percent escape in metadata"); out.push(high << 4 | low); } return String.fromCharCode(...out); }; var decodePayloadBytes = (payload, uri, allowUtf8) => { const bytes = []; for (let i = 0; i < payload.length; ) { const code = payload.charCodeAt(i); if (code === 37) { if (i + 2 >= payload.length) throw dataUriError(uri, "malformed percent escape in payload"); const high = hexValue(payload.charCodeAt(i + 1)); const low = hexValue(payload.charCodeAt(i + 2)); if (high < 0 || low < 0) throw dataUriError(uri, "malformed percent escape in payload"); bytes.push(high << 4 | low); i += 3; continue; } const codePoint = payload.codePointAt(i); if (codePoint === void 0) break; if (codePoint <= 127) bytes.push(codePoint); else { if (!allowUtf8) throw dataUriError(uri, "base64 payload contains an unescaped non-ASCII character"); if (codePoint >= 55296 && codePoint <= 57343) throw dataUriError(uri, "payload contains an unpaired surrogate"); const encoded = new TextEncoder().encode(String.fromCodePoint(codePoint)); for (const byte of encoded) bytes.push(byte); } i += codePoint > 65535 ? 2 : 1; } return new Uint8Array(bytes); }; var isDataUri = (uri) => uri.slice(0, 5).toLowerCase() === "data:"; var decodeDataUri = (uri) => { if (!isDataUri(uri)) throw dataUriError(uri, "expected a data: scheme"); const comma = uri.indexOf(",", 5); if (comma < 0) throw dataUriError(uri, "missing comma separator"); const metadata = uri.slice(5, comma); const payload = uri.slice(comma + 1); const parts = metadata.split(";"); const first = decodeMetadataToken((parts.shift() ?? "").trim(), uri); let isBase64 = false; const parameters = []; for (const rawPart of parts) { const part = decodeMetadataToken(rawPart.trim(), uri); if (part.length === 0) continue; if (part.toLowerCase() === "base64") { if (isBase64) throw dataUriError(uri, "duplicate base64 flag"); isBase64 = true; continue; } parameters.push(part); } for (const parameter of parameters) if (!PARAMETER_PATTERN.test(parameter)) throw dataUriError(uri, `invalid media type parameter '${parameter}'`); let mimeType = null; if (first.length > 0) { const slash = first.indexOf("/"); if (slash <= 0 || slash === first.length - 1 || first.indexOf("/", slash + 1) >= 0) throw dataUriError(uri, `invalid media type '${first}'`); const type = first.slice(0, slash); const subtype = first.slice(slash + 1); if (!MIME_TOKEN_PATTERN.test(type) || !MIME_TOKEN_PATTERN.test(subtype)) throw dataUriError(uri, `invalid media type '${first}'`); mimeType = [first, ...parameters].join(";"); } if (isBase64) { const encoded = decodePayloadBytes(payload, uri, false); let binary; try { const chunks = []; for (let offset = 0; offset < encoded.length; offset += 32768) chunks.push(String.fromCharCode(...encoded.subarray(offset, offset + 32768))); binary = atob(chunks.join("")); } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw dataUriError(uri, `invalid base64 payload (${detail})`); } const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) & 255; return { mimeType, data: bytes.buffer }; } const decoded = decodePayloadBytes(payload, uri, true); return { mimeType, data: decoded.buffer }; }; var pathAndSuffix = (url) => { const query = url.search(/[?#]/); return query < 0 ? { path: url, suffix: "" } : { path: url.slice(0, query), suffix: url.slice(query) }; }; var dirnameUrl = (url) => { if (!url) return ""; if (isAbsoluteUri(url) || url.startsWith("//")) { try { const protocolRelative = url.startsWith("//"); const parsed = new URL(url, RELATIVE_URI_ORIGIN); const slash2 = parsed.pathname.lastIndexOf("/"); parsed.pathname = slash2 < 0 ? "/" : parsed.pathname.slice(0, slash2 + 1); parsed.search = ""; parsed.hash = ""; if (protocolRelative) return `//${parsed.host}${parsed.pathname}`; return parsed.href; } catch { } } const { path } = pathAndSuffix(url); const slash = path.lastIndexOf("/"); return slash < 0 ? "" : path.slice(0, slash + 1); }; var normalizeDirectoryUrl = (url) => { if (!url) return ""; const { path, suffix } = pathAndSuffix(url); return path.endsWith("/") ? url : `${path}/${suffix}`; }; var resolveUri = (baseUri, uri) => { if (isAbsoluteUri(uri)) return uri; if (!baseUri) return uri; if (isAbsoluteUri(baseUri) || baseUri.startsWith("//")) { const protocolRelative = baseUri.startsWith("//"); const absoluteBase = protocolRelative ? `https:${baseUri}` : baseUri; try { const resolved2 = new URL(uri, absoluteBase).href; return protocolRelative ? resolved2.slice("https:".length) : resolved2; } catch { return uri; } } if (uri.startsWith("//")) return new URL(uri, RELATIVE_URI_ORIGIN).href.slice("https:".length); const sentinelBase = new URL(baseUri.startsWith("/") ? baseUri : `/${baseUri}`, RELATIVE_URI_ORIGIN); const resolved = new URL(uri, sentinelBase); if (resolved.origin !== RELATIVE_URI_ORIGIN) return resolved.href; const path = resolved.href.slice(RELATIVE_URI_ORIGIN.length); return uri.startsWith("/") || baseUri.startsWith("/") ? path : path.slice(1); }; // typescript/gltf/compatibility.ts var GLTF_SUPPORTED_VERSION = Object.freeze({ major: 2, minor: 0, text: "2.0" }); var PARSED_SUPPORTED_VERSION = { major: BigInt(GLTF_SUPPORTED_VERSION.major), minor: BigInt(GLTF_SUPPORTED_VERSION.minor), text: GLTF_SUPPORTED_VERSION.text }; var VERSION_PATTERN = /^(\d+)\.(\d+)$/; var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value); var compareVersions = (left, right) => { if (left.major !== right.major) return left.major < right.major ? -1 : 1; if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1; return 0; }; var versionText = (value) => typeof value === "string" ? value : String(value); var parseVersion = (value, field) => { if (typeof value !== "string") throw new Error(`Invalid glTF ${field} '${versionText(value)}'; expected . (supported glTF ${GLTF_SUPPORTED_VERSION.text}).`); const match = VERSION_PATTERN.exec(value); if (!match) throw new Error(`Invalid glTF ${field} '${value}'; expected . (supported glTF ${GLTF_SUPPORTED_VERSION.text}).`); return { major: BigInt(match[1]), minor: BigInt(match[2]), text: value }; }; function validateGltfCompatibility(value) { if (!isRecord(value) || !isRecord(value.asset)) throw new Error(`Invalid glTF asset: missing asset object (supported glTF ${GLTF_SUPPORTED_VERSION.text}).`); const version = parseVersion(value.asset.version, "asset.version"); if (version.major !== PARSED_SUPPORTED_VERSION.major) throw new Error(`Unsupported glTF asset version ${version.text}; this implementation supports glTF ${GLTF_SUPPORTED_VERSION.text} and compatible 2.x assets.`); if (value.asset.minVersion !== void 0) { const minVersion = parseVersion(value.asset.minVersion, "asset.minVersion"); if (compareVersions(minVersion, version) > 0) throw new Error(`Invalid glTF asset: asset.minVersion ${minVersion.text} exceeds asset.version ${version.text} (supported glTF ${GLTF_SUPPORTED_VERSION.text}).`); if (compareVersions(minVersion, PARSED_SUPPORTED_VERSION) > 0) throw new Error(`Unsupported glTF asset minimum version ${minVersion.text}; this implementation supports glTF ${GLTF_SUPPORTED_VERSION.text}.`); } } var decodeGltfJson = (bytes, context) => { let text; try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Invalid glTF ${context}: UTF-8 decoding failed (${detail}).`); } let parsed; try { parsed = JSON.parse(text); } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Invalid glTF ${context}: JSON parsing failed (${detail}).`); } validateGltfCompatibility(parsed); return parsed; }; // typescript/gltf/glb.ts var GLB_MAGIC = 1179937895; var GLB_VERSION_2 = 2; var CHUNK_JSON = 1313821514; var CHUNK_BIN = 5130562; var parseGLB = (glb) => { const dv = new DataView(glb); if (dv.byteLength < 12) throw new Error("Invalid GLB: too small"); const magic = dv.getUint32(0, true); const version = dv.getUint32(4, true); const length = dv.getUint32(8, true); if (magic !== GLB_MAGIC) throw new Error("Invalid GLB: bad magic"); if (version !== GLB_VERSION_2) throw new Error(`Unsupported GLB version: ${version}`); if (length > dv.byteLength) throw new Error("Invalid GLB: length exceeds buffer"); let offset = 12; let jsonChunk = null; let binChunk = null; while (offset + 8 <= length) { const chunkLength = dv.getUint32(offset + 0, true); const chunkType = dv.getUint32(offset + 4, true); offset += 8; if (offset + chunkLength > length) throw new Error("Invalid GLB: chunk exceeds buffer length"); const chunk = glb.slice(offset, offset + chunkLength); if (chunkType === CHUNK_JSON) jsonChunk = chunk; else if (chunkType === CHUNK_BIN && !binChunk) binChunk = chunk; offset += chunkLength; } if (!jsonChunk) throw new Error("Invalid GLB: missing JSON chunk"); const json = decodeGltfJson(jsonChunk, "GLB JSON chunk"); return { json, binChunk }; }; // typescript/gltf/accessors.ts var COMPONENT_INFO = { 5120: { bytes: 1, ctor: Int8Array, signed: true, bits: 8 }, 5121: { bytes: 1, ctor: Uint8Array, signed: false, bits: 8 }, 5122: { bytes: 2, ctor: Int16Array, signed: true, bits: 16 }, 5123: { bytes: 2, ctor: Uint16Array, signed: false, bits: 16 }, 5124: { bytes: 4, ctor: Int32Array, signed: true, bits: 32 }, 5125: { bytes: 4, ctor: Uint32Array, signed: false, bits: 32 }, 5126: { bytes: 4, ctor: Float32Array, signed: true, bits: 32 } }; var gltfNumComponents = (type) => { switch (type) { case "SCALAR": return 1; case "VEC2": return 2; case "VEC3": return 3; case "VEC4": return 4; case "MAT2": return 4; case "MAT3": return 9; case "MAT4": return 16; default: throw new Error(`Unsupported accessor type: ${String(type)}`); } }; var getAccessor = (json, index) => { const a = json.accessors?.[index]; if (!a) throw new Error(`Invalid accessor index: ${index}`); return a; }; var getBufferView = (json, index) => { const bv = json.bufferViews?.[index]; if (!bv) throw new Error(`Invalid bufferView index: ${index}`); return bv; }; var roundUp4 = (value) => Math.ceil(value / 4) * 4; var requireSafeNonNegativeInteger = (value, context) => { if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${context} must be a non-negative safe integer, got ${String(value)}.`); return value; }; var checkedMultiply = (left, right, context) => { const result = left * right; if (!Number.isSafeInteger(result)) throw new Error(`${context} is too large.`); return result; }; var checkedAdd2 = (left, right, context) => { const result = left + right; if (!Number.isSafeInteger(result)) throw new Error(`${context} is too large.`); return result; }; var getAccessorLayout = (type, componentByteSize, byteStride) => { if (!Number.isSafeInteger(componentByteSize) || componentByteSize <= 0) throw new Error(`Invalid accessor component byte size: ${componentByteSize}`); const logicalComponentCount = gltfNumComponents(type); const isMatrix = type === "MAT2" || type === "MAT3" || type === "MAT4"; const rows = isMatrix ? Math.sqrt(logicalComponentCount) : logicalComponentCount; const columns = isMatrix ? rows : 1; const logicalByteSize = checkedMultiply(logicalComponentCount, componentByteSize, "Accessor logical byte size"); const logicalColumnByteSize = checkedMultiply(rows, componentByteSize, "Accessor logical column byte size"); const columnStride = isMatrix ? componentByteSize < 4 ? roundUp4(logicalColumnByteSize) : logicalColumnByteSize : logicalColumnByteSize; const physicalElementStride = checkedMultiply(columnStride, columns, "Accessor physical element stride"); const elementStride = byteStride === void 0 ? physicalElementStride : requireSafeNonNegativeInteger(byteStride, "Accessor byteStride"); if (!Number.isSafeInteger(elementStride) || elementStride < physicalElementStride) { throw new Error(`Invalid accessor byteStride (${elementStride}) < physical element stride (${physicalElementStride})`); } const finalElementLogicalEnd = checkedAdd2(checkedMultiply(columns - 1, columnStride, "Accessor final column offset"), logicalColumnByteSize, "Accessor final element logical end"); return { rows, columns, logicalComponentCount, componentByteSize, logicalByteSize, columnStride, physicalElementStride, elementStride, finalElementLogicalEnd, compact: columnStride === logicalColumnByteSize && elementStride === logicalByteSize }; }; var accessorSourceByteLength = (layout, count) => { if (count <= 0) return 0; return checkedAdd2(checkedMultiply(count - 1, layout.elementStride, "Accessor source byte length"), layout.finalElementLogicalEnd, "Accessor source byte length"); }; var validateAccessorSource = (buffer, bufferLength, bufferView, accessorOffset, sourceByteLength, context) => { const viewOffset = bufferView.byteOffset ?? 0; const viewLength = bufferView.byteLength; if (!Number.isSafeInteger(viewOffset) || viewOffset < 0 || !Number.isSafeInteger(viewLength) || viewLength < 0 || !Number.isSafeInteger(accessorOffset) || accessorOffset < 0) { throw new Error(`${context}: invalid bufferView/accessor byte offset or length.`); } requireSafeNonNegativeInteger(sourceByteLength, `${context} source byte length`); const relativeEnd = checkedAdd2(accessorOffset, sourceByteLength, `${context} bufferView range`); if (relativeEnd > viewLength) throw new Error(`${context}: accessor data exceeds bufferView.byteLength (${relativeEnd} > ${viewLength}).`); const start = checkedAdd2(viewOffset, accessorOffset, `${context} buffer offset`); const bufferEnd = checkedAdd2(start, sourceByteLength, `${context} buffer range`); if (bufferEnd > bufferLength) throw new Error(`${context}: accessor data exceeds its buffer.`); return start; }; var copyBytesToWasm = (buffer, byteOffset, byteLength) => { const ptr = wasm.allocBytes(byteLength); if (!ptr && byteLength !== 0) throw new Error("WebAssembly allocation failed while decoding an accessor."); try { const src = new Uint8Array(buffer, byteOffset, byteLength); wasm.u8view(ptr, byteLength).set(src); return ptr; } catch (error) { if (ptr) wasm.freeBytes(ptr, byteLength); throw error; } }; var getBufferWithDeclaredLength = (json, buffers, index, context) => { const definition = json.buffers?.[index]; if (!definition) throw new Error(`${context}: missing buffer[${index}] definition.`); const data = buffers[index]; if (!data) throw new Error(`${context}: missing buffer[${index}] data.`); const length = requireSafeNonNegativeInteger(definition.byteLength, `${context} buffer[${index}].byteLength`); if (data.byteLength < length) throw new Error(`${context}: buffer[${index}] contains ${data.byteLength} bytes, but ${length} were declared.`); return { definition, data, length }; }; var copyBytesFromWasm = (ptr, byteLength) => { if (!ptr && byteLength !== 0) throw new Error("WebAssembly accessor output allocation failed."); const out = new Uint8Array(byteLength); out.set(wasm.u8view(ptr, byteLength)); return out; }; var readAccessor = (doc, accessorIndex) => { const json = doc.json; const accessor = getAccessor(json, accessorIndex); const componentType = accessor.componentType; const info = COMPONENT_INFO[componentType]; if (!info) throw new Error(`Unsupported accessor componentType: ${componentType}`); const count = requireSafeNonNegativeInteger(accessor.count, `accessor[${accessorIndex}].count`); const type = accessor.type; const numComps = gltfNumComponents(type); const normalized = accessor.normalized === true; const context = `accessor[${accessorIndex}]`; let base; const outputComponentCount = checkedMultiply(count, numComps, `${context} component count`); const outputByteLength = checkedMultiply(outputComponentCount, info.bytes, `${context} output byte length`); if (accessor.bufferView === void 0) { if (accessor.byteOffset !== void 0) throw new Error(`${context}: byteOffset requires bufferView.`); base = new info.ctor(new ArrayBuffer(outputByteLength), 0, outputComponentCount); } else { const bv = getBufferView(json, accessor.bufferView); if (bv.extensions?.["EXT_meshopt_compression"] && json.extensionsRequired?.includes("EXT_meshopt_compression")) throw new Error("Required EXT_meshopt_compression must be handled by glTF import preflight."); const bufferInfo = getBufferWithDeclaredLength(json, doc.buffers, bv.buffer, context); const buffer = bufferInfo.data; requireSafeNonNegativeInteger(bv.byteOffset ?? 0, `${context} bufferView.byteOffset`); const accOffset = requireSafeNonNegativeInteger(accessor.byteOffset ?? 0, `${context}.byteOffset`); const layout = getAccessorLayout(type, info.bytes, bv.byteStride); const sourceByteLength = accessorSourceByteLength(layout, count); const start = validateAccessorSource(buffer, bufferInfo.length, bv, accOffset, sourceByteLength, context); const isAligned = start % info.bytes === 0; if (count === 0) base = new info.ctor(new ArrayBuffer(0), 0, 0); else if (layout.compact && isAligned) base = new info.ctor(buffer, start, outputComponentCount); else { const compactByteLength = checkedMultiply(count, layout.logicalByteSize, `${context} compact byte length`); const srcPtr = copyBytesToWasm(buffer, start, sourceByteLength); let outPtr = 0; try { outPtr = wasm.allocBytes(compactByteLength); if (!outPtr && compactByteLength !== 0) throw new Error(`${context}: WebAssembly allocation failed.`); accessorf.compact(outPtr, srcPtr, count, layout.rows, layout.columns, info.bytes, layout.elementStride); const outBytes = copyBytesFromWasm(outPtr, compactByteLength); const outBuffer = new ArrayBuffer(compactByteLength); new Uint8Array(outBuffer).set(outBytes); base = new info.ctor(outBuffer, 0, outputComponentCount); } finally { if (outPtr) wasm.freeBytes(outPtr, compactByteLength); if (srcPtr) wasm.freeBytes(srcPtr, sourceByteLength); } } } if (accessor.sparse) { const out = base.slice(); applySparse(doc, accessor, out, componentType, numComps); base = out; } return { accessor, componentType, type, count, numComponents: numComps, normalized, array: base }; }; var applySparse = (doc, accessor, out, componentType, numComps) => { const sparse = accessor.sparse; const count = requireSafeNonNegativeInteger(accessor.count, "Sparse accessor count"); const scount = requireSafeNonNegativeInteger(sparse.count, "Sparse accessor count"); if (scount > count) throw new Error(`Sparse accessor count ${scount} exceeds accessor count ${count}.`); if (scount <= 0) return; const idxBv = getBufferView(doc.json, sparse.indices.bufferView); if (idxBv.extensions?.["EXT_meshopt_compression"] && doc.json.extensionsRequired?.includes("EXT_meshopt_compression")) throw new Error("Required EXT_meshopt_compression sparse indices must be handled by glTF import preflight."); const idxBufferInfo = getBufferWithDeclaredLength(doc.json, doc.buffers, idxBv.buffer, "sparse indices"); const idxBuf = idxBufferInfo.data; const idxComponent = sparse.indices.componentType; if (idxComponent !== 5121 && idxComponent !== 5123 && idxComponent !== 5125) throw new Error(`Unsupported sparse indices componentType: ${idxComponent}`); const idxInfo = COMPONENT_INFO[idxComponent]; if (!idxInfo) throw new Error(`Unsupported sparse indices componentType: ${idxComponent}`); const idxStride = idxInfo.bytes; if (idxBv.byteStride !== void 0 && idxBv.byteStride !== idxStride) throw new Error("Sparse indices must be tightly packed."); const idxByteLength = checkedMultiply(scount, idxStride, "Sparse index byte length"); const idxStart = validateAccessorSource(idxBuf, idxBufferInfo.length, idxBv, sparse.indices.byteOffset ?? 0, idxByteLength, "sparse indices"); const idxView = new DataView(idxBuf, idxStart, idxByteLength); let previousIndex = -1; for (let i = 0; i < scount; i++) { const index = idxComponent === 5121 ? idxView.getUint8(i) : idxComponent === 5123 ? idxView.getUint16(i * 2, true) : idxView.getUint32(i * 4, true); if (index >= count) throw new Error(`Sparse index ${index} at ${i} is out of range for accessor count ${count}.`); if (index <= previousIndex) throw new Error(`Sparse indices must be strictly increasing (index ${index} at ${i}).`); previousIndex = index; } const valBv = getBufferView(doc.json, sparse.values.bufferView); if (valBv.extensions?.["EXT_meshopt_compression"] && doc.json.extensionsRequired?.includes("EXT_meshopt_compression")) throw new Error("Required EXT_meshopt_compression sparse values must be handled by glTF import preflight."); const valueBufferInfo = getBufferWithDeclaredLength(doc.json, doc.buffers, valBv.buffer, "sparse values"); const valBuf = valueBufferInfo.data; const valOffset = sparse.values.byteOffset ?? 0; const compInfo = COMPONENT_INFO[componentType]; if (!compInfo) throw new Error(`Unsupported sparse values componentType: ${componentType}`); const componentCount = out.length; const componentBytes = compInfo.bytes; const outByteLength = checkedMultiply(componentCount, componentBytes, "Sparse accessor output byte length"); const valuesByteLength = checkedMultiply(checkedMultiply(scount, numComps, "Sparse values component count"), componentBytes, "Sparse values byte length"); const outPtr = wasm.allocBytes(outByteLength); if (!outPtr && outByteLength !== 0) throw new Error("sparse accessor: WebAssembly allocation failed."); let idxPtr = 0; let valuesPtr = 0; let valuesSrcPtr = 0; let valuesSourceLength = 0; try { wasm.u8view(outPtr, outByteLength).set(new Uint8Array(out.buffer, out.byteOffset, outByteLength)); idxPtr = copyBytesToWasm(idxBuf, idxStart, idxByteLength); if (!idxPtr && idxByteLength !== 0) throw new Error("sparse indices: WebAssembly allocation failed."); const valueLayout = getAccessorLayout(accessor.type, componentBytes, valBv.byteStride); valuesSourceLength = accessorSourceByteLength(valueLayout, scount); const valuesStart = validateAccessorSource(valBuf, valueBufferInfo.length, valBv, valOffset, valuesSourceLength, "sparse values"); valuesPtr = wasm.allocBytes(valuesByteLength); if (!valuesPtr && valuesByteLength !== 0) throw new Error("sparse values: WebAssembly allocation failed."); valuesSrcPtr = copyBytesToWasm(valBuf, valuesStart, valuesSourceLength); if (!valuesSrcPtr && valuesSourceLength !== 0) throw new Error("sparse values source: WebAssembly allocation failed."); if (valueLayout.compact) wasm.u8view(valuesPtr, valuesByteLength).set(new Uint8Array(valBuf, valuesStart, valuesByteLength)); else accessorf.compact(valuesPtr, valuesSrcPtr, scount, valueLayout.rows, valueLayout.columns, componentBytes, valueLayout.elementStride); accessorf.applySparse(outPtr, componentCount, componentType, numComps, idxPtr, idxComponent, valuesPtr, scount); const outBytes = wasm.u8view(outPtr, outByteLength); new Uint8Array(out.buffer, out.byteOffset, outByteLength).set(outBytes); } finally { if (valuesSrcPtr) wasm.freeBytes(valuesSrcPtr, valuesSourceLength); if (valuesPtr) wasm.freeBytes(valuesPtr, valuesByteLength); if (idxPtr) wasm.freeBytes(idxPtr, idxByteLength); if (outPtr) wasm.freeBytes(outPtr, outByteLength); } }; var readAccessorAsFloat32 = (doc, accessorIndex) => { const view = readAccessor(doc, accessorIndex); const info = COMPONENT_INFO[view.componentType]; if (!info) throw new Error(`Unsupported componentType: ${view.componentType}`); if (view.componentType === 5126 && !view.normalized) return view.array; if (view.array.length === 0) return new Float32Array(0); const srcByteLength = checkedMultiply(view.array.length, info.bytes, "Accessor conversion source byte length"); const sourceArray = view.array.buffer; const sourceOffset = view.array.byteOffset; const srcPtr = copyBytesToWasm(sourceArray, sourceOffset, srcByteLength); const outPtr = wasm.allocF32(view.array.length); if (!outPtr) { wasm.freeBytes(srcPtr, srcByteLength); throw new Error("WebAssembly allocation failed while converting an accessor to Float32Array."); } try { accessorf.convertToF32(outPtr, srcPtr, view.array.length, view.componentType, view.normalized); const out = new Float32Array(view.array.length); out.set(wasm.f32view(outPtr, view.array.length)); return out; } finally { wasm.freeF32(outPtr, view.array.length); wasm.freeBytes(srcPtr, srcByteLength); } }; var readAccessorAsUint16 = (doc, accessorIndex) => { const view = readAccessor(doc, accessorIndex); const ct = view.componentType; const info = COMPONENT_INFO[ct]; if (!info) throw new Error(`Unsupported componentType: ${ct}`); if (ct === 5123 && !view.normalized) return view.array; if (view.array.length === 0) return new Uint16Array(0); const srcByteLength = checkedMultiply(view.array.length, info.bytes, "Accessor conversion source byte length"); const sourceArray = view.array.buffer; const sourceOffset = view.array.byteOffset; const srcPtr = copyBytesToWasm(sourceArray, sourceOffset, srcByteLength); const outByteLength = checkedMultiply(view.array.length, 2, "Accessor Uint16 output byte length"); const outPtr = wasm.allocBytes(outByteLength); if (!outPtr) { wasm.freeBytes(srcPtr, srcByteLength); throw new Error("WebAssembly allocation failed while converting an accessor to Uint16Array."); } try { accessorf.convertToU16(outPtr, srcPtr, view.array.length, ct); const out = new Uint16Array(view.array.length); new Uint8Array(out.buffer).set(wasm.u8view(outPtr, outByteLength)); return out; } finally { wasm.freeBytes(outPtr, outByteLength); wasm.freeBytes(srcPtr, srcByteLength); } }; var readIndicesAsUint32 = (doc, accessorIndex) => { const view = readAccessor(doc, accessorIndex); const ct = view.componentType; const info = COMPONENT_INFO[ct]; if (!info) throw new Error(`Unsupported componentType: ${ct}`); if (ct === 5125 && !view.normalized) return view.array; if (view.array.length === 0) return new Uint32Array(0); const srcByteLength = checkedMultiply(view.array.length, info.bytes, "Index conversion source byte length"); const sourceArray = view.array.buffer; const sourceOffset = view.array.byteOffset; const srcPtr = copyBytesToWasm(sourceArray, sourceOffset, srcByteLength); const outByteLength = checkedMultiply(view.array.length, 4, "Index Uint32 output byte length"); const outPtr = wasm.allocBytes(outByteLength); if (!outPtr) { wasm.freeBytes(srcPtr, srcByteLength); throw new Error("WebAssembly allocation failed while converting indices to Uint32Array."); } try { accessorf.convertToU32(outPtr, srcPtr, view.array.length, ct); const out = new Uint32Array(view.array.length); new Uint8Array(out.buffer).set(wasm.u8view(outPtr, outByteLength)); return out; } finally { wasm.freeBytes(outPtr, outByteLength); wasm.freeBytes(srcPtr, srcByteLength); } }; // typescript/gltf/loader.ts var warn = (opts, msg) => opts?.onWarning?.(msg); var getFetch = (opts) => { const f = opts?.fetch ?? globalThis.fetch; if (!f) throw new Error("loadGltf(): fetch() is not available. Pass LoadGltfOptions.fetch or provide an ArrayBuffer source."); return f; }; var fetchArrayBuffer = async (url, opts) => { const f = getFetch(opts); const res = await f(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); const bytes = await res.arrayBuffer(); return { bytes, responseUrl: typeof res.url === "string" && res.url.length > 0 ? res.url : url }; }; var isGLB = (bytes) => bytes.byteLength >= 4 && new DataView(bytes).getUint32(0, true) === 1179937895; var requireBufferLength = (value, context) => { if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${context} must be a non-negative safe integer, got ${String(value)}.`); return value; }; var restrictBufferToDeclaredLength = (bytes, length, context) => { if (bytes.byteLength < length) throw new Error(`${context} contains ${bytes.byteLength} bytes, but ${length} were declared.`); return bytes.slice(0, length); }; var resolveBuffers = async (json, resourceBaseUrl, opts, glbBinChunk) => { const buffers = json.buffers ?? []; const out = new Array(buffers.length); for (let i = 0; i < buffers.length; i++) { const b = buffers[i]; const length = requireBufferLength(b.byteLength, `buffers[${i}].byteLength`); if (b.uri === void 0) { if (glbBinChunk === null || glbBinChunk === void 0) throw new Error(`buffers[${i}] has no uri but no GLB BIN chunk was provided`); out[i] = restrictBufferToDeclaredLength(glbBinChunk, length, `buffers[${i}] GLB BIN chunk`); continue; } if (isDataUri(b.uri)) { out[i] = restrictBufferToDeclaredLength(decodeDataUri(b.uri).data, length, `buffers[${i}] data URI`); continue; } const url = resolveUri(resourceBaseUrl, b.uri); out[i] = restrictBufferToDeclaredLength((await fetchArrayBuffer(url, opts)).bytes, length, `buffers[${i}] resource`); } return out; }; var resolveImages = async (json, buffers, resourceBaseUrl, opts) => { const images = json.images ?? []; const out = new Array(images.length); for (let i = 0; i < images.length; i++) { const img = images[i]; if (img.uri !== void 0) { if (isDataUri(img.uri)) { out[i] = decodeDataUri(img.uri).data; } else { const url = resolveUri(resourceBaseUrl, img.uri); out[i] = (await fetchArrayBuffer(url, opts)).bytes; } continue; } if (img.bufferView !== void 0) { const bv = json.bufferViews?.[img.bufferView]; if (!bv) throw new Error(`Invalid images[${i}].bufferView: ${img.bufferView}`); const buffer = buffers[bv.buffer]; if (!buffer) throw new Error(`Missing buffer[${bv.buffer}] for images[${i}]`); const start = requireBufferLength(bv.byteOffset ?? 0, `images[${i}].bufferView.byteOffset`); const length = requireBufferLength(bv.byteLength, `images[${i}].bufferView.byteLength`); const end = start + length; if (!Number.isSafeInteger(end) || end > buffer.byteLength) throw new Error(`images[${i}].bufferView exceeds its buffer.`); out[i] = buffer.slice(start, end); continue; } warn(opts, `images[${i}] has neither uri nor bufferView; skipping`); out[i] = new ArrayBuffer(0); } return out; }; var finalizeDocument = async (json, resourceBaseUrl, opts, glbBinChunk) => { validateGltfCompatibility(json); const buffers = await resolveBuffers(json, resourceBaseUrl, opts, glbBinChunk); const doc = { json, buffers, resourceBaseUrl }; if (opts?.loadImages) doc.images = await resolveImages(json, buffers, resourceBaseUrl, opts); return doc; }; var parseRootBytes = (bytes, context) => { if (isGLB(bytes)) return parseGLB(bytes); return { json: decodeGltfJson(bytes, `${context} JSON`), binChunk: null }; }; var loadGltf = async (source, opts) => { if (typeof source === "string") { const fetched = await fetchArrayBuffer(source, opts); const resourceBaseUrl2 = opts?.resourceBaseUrl !== void 0 ? normalizeDirectoryUrl(opts.resourceBaseUrl) : fetched.responseUrl; const { json: json2, binChunk: binChunk2 } = parseRootBytes(fetched.bytes, `source '${source}'`); return finalizeDocument(json2, resourceBaseUrl2, opts, binChunk2); } const resourceBaseUrl = opts?.resourceBaseUrl !== void 0 ? normalizeDirectoryUrl(opts.resourceBaseUrl) : ""; const { json, binChunk } = parseRootBytes(source, "in-memory source"); return finalizeDocument(json, resourceBaseUrl, opts, binChunk); }; // wgsl/graphics/mipmap.wgsl var mipmap_default = "struct VertexOutput { @builtin(position) pos: vec4, @location(0) uv: vec2, } @group(0) @binding(0) var samp: sampler; @group(0) @binding(1) var tex: texture_2d; @vertex fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { var positions = array, 3>( vec2(-1.0, -1.0), vec2( 3.0, -1.0), vec2(-1.0, 3.0), ); var uvs = array, 3>( vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0, -1.0), ); var o: VertexOutput; o.pos = vec4(positions[idx], 0.0, 1.0); o.uv = uvs[idx]; return o; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { return textureSample(tex, samp, in.uv); }"; // typescript/graphics/texture.ts var __texture2d_id = 1; var hasCreateImageBitmap = () => typeof globalThis.createImageBitmap === "function"; var urlBlobPromises = /* @__PURE__ */ new WeakMap(); var getUrlBlob = (source) => { const cached = urlBlobPromises.get(source); if (cached) return cached; const pending = (async () => { const response = await fetch(source.url); if (!response.ok) throw new Error(`Failed to fetch ${source.url}: ${response.status} ${response.statusText}`); return response.blob(); })(); urlBlobPromises.set(source, pending); return pending; }; var describeTextureSource = (source) => { if (source.kind === "url") return `URL '${source.url}'`; if (source.kind === "bytes") return `${source.mimeType ?? "unknown-type"} byte source`; return "ImageBitmap source"; }; var mipLevelCountForSize = (w, h) => { const m = Math.max(1, w | 0, h | 0); return (Math.floor(Math.log2(m)) | 0) + 1; }; var mipmapCache = /* @__PURE__ */ new WeakMap(); var getMipmapCache = (device) => { const cached = mipmapCache.get(device); if (cached) return cached; const module = device.createShaderModule({ code: mipmap_default }); const bindGroupLayout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } } ] }); const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }); const createPipeline = (format) => device.createRenderPipeline({ layout: pipelineLayout, vertex: { module, entryPoint: "vs_main" }, fragment: { module, entryPoint: "fs_main", targets: [{ format }] }, primitive: { topology: "triangle-list" } }); const sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" }); const created = { pipelineLinear: createPipeline("rgba8unorm"), pipelineSrgb: createPipeline("rgba8unorm-srgb"), sampler, bindGroupLayout }; mipmapCache.set(device, created); return created; }; var Texture2D = class _Texture2D { id = __texture2d_id++; _source; _mipmaps; _imageDecode; _mipmapColorSpace = null; samplerDesc; _gpuTexture = null; _viewLinear = null; _viewSrgb = null; _sampler = null; _uploadPromise = null; _uploadStarted = false; _uploadGeneration = 0; _uploadError = null; _revision = 0; _width = 0; _height = 0; constructor(desc) { this._source = desc.source; this._mipmaps = desc.mipmaps ?? true; this._imageDecode = desc.imageDecode; this.samplerDesc = { addressModeU: desc.sampler?.addressModeU ?? "repeat", addressModeV: desc.sampler?.addressModeV ?? "repeat", addressModeW: desc.sampler?.addressModeW ?? "repeat", magFilter: desc.sampler?.magFilter ?? "linear", minFilter: desc.sampler?.minFilter ?? "linear", mipmapFilter: desc.sampler?.mipmapFilter ?? "linear", lodMinClamp: desc.sampler?.lodMinClamp ?? 0, lodMaxClamp: desc.sampler?.lodMaxClamp ?? 32 }; } get revision() { return this._revision; } get width() { return this._width; } get height() { return this._height; } get uploaded() { return !!this._gpuTexture; } get uploadError() { return this._uploadError; } static createFrom(desc) { return new _Texture2D(desc); } getSampler(device, fallback) { if (this._sampler) return this._sampler; try { this._sampler = device.createSampler(this.samplerDesc); return this._sampler; } catch (e) { if (fallback) return fallback; throw e; } } getView(device, queue, colorSpace, fallbackView) { if (this._uploadError) throw this._uploadError; if (this._gpuTexture) { if (colorSpace === "srgb") return this._viewSrgb ?? fallbackView; return this._viewLinear ?? fallbackView; } this.ensureUploaded(device, queue, colorSpace); if (this._uploadError) throw this._uploadError; return fallbackView; } destroy() { this._uploadGeneration++; this._uploadError = null; this._gpuTexture?.destroy(); this._gpuTexture = null; this._viewLinear = null; this._viewSrgb = null; this._sampler = null; this._uploadStarted = false; this._uploadPromise = null; this._mipmapColorSpace = null; this._revision++; } ensureUploaded(device, queue, colorSpace = "linear") { if (this._uploadError) throw this._uploadError; if (this._uploadStarted) return; const uploadGeneration = ++this._uploadGeneration; this._uploadStarted = true; this._mipmapColorSpace = colorSpace; this._uploadPromise = (async () => { let bitmap = null; let texture = null; try { bitmap = await this.decodeBitmap(); if (uploadGeneration !== this._uploadGeneration) return; const w = bitmap.width | 0; const h = bitmap.height | 0; const mipLevelCount = this._mipmaps ? mipLevelCountForSize(w, h) : 1; texture = device.createTexture({ size: { width: w, height: h }, format: "rgba8unorm", mipLevelCount, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, viewFormats: ["rgba8unorm-srgb"] }); queue.copyExternalImageToTexture({ source: bitmap }, { texture }, { width: w, height: h }); if (this._mipmaps && mipLevelCount > 1) this.generateMipmaps(device, texture, mipLevelCount, this._mipmapColorSpace ?? "linear"); if (uploadGeneration !== this._uploadGeneration) { try { texture.destroy(); } catch { } return; } const viewLinear = texture.createView({ format: "rgba8unorm" }); const viewSrgb = texture.createView({ format: "rgba8unorm-srgb" }); this._viewLinear = viewLinear; this._viewSrgb = viewSrgb; this._width = w; this._height = h; this._gpuTexture = texture; this._revision++; } catch (e) { if (uploadGeneration === this._uploadGeneration) { const cause = e instanceof Error ? e : new Error(String(e)); const contextual = new Error(`Texture2D ${this.id}: failed to upload ${describeTextureSource(this._source)}: ${cause.message}`); contextual.cause = cause; this._uploadError = contextual; this._uploadStarted = false; this._uploadPromise = null; this._mipmapColorSpace = null; } try { texture?.destroy(); } catch { } throw this._uploadError ?? e; } finally { if (bitmap && this._source.kind !== "bitmap") try { bitmap.close?.(); } catch { } } })(); this._uploadPromise.catch(() => { }); } async decodeBitmap() { const src = this._source; if (src.kind === "bitmap") return src.bitmap; if (!hasCreateImageBitmap()) throw new Error("createImageBitmap() is not available in this environment."); const colorSpaceConversion = this._imageDecode?.colorSpaceConversion ?? (this._mipmapColorSpace === "srgb" ? "default" : "none"); const fallbackWithoutOptions = this._imageDecode?.fallbackWithoutOptions ?? true; const options = { premultiplyAlpha: "none", imageOrientation: "none", colorSpaceConversion }; if (src.kind === "url") { const blob2 = await getUrlBlob(src); try { return await createImageBitmap(blob2, options); } catch (e) { if (fallbackWithoutOptions) return await createImageBitmap(blob2); throw e; } } const blob = new Blob([src.bytes], { type: src.mimeType ?? "application/octet-stream" }); try { return await createImageBitmap(blob, options); } catch (e) { if (fallbackWithoutOptions) return await createImageBitmap(blob); throw e; } } generateMipmaps(device, texture, mipLevels, colorSpace) { const cache = getMipmapCache(device); const pipeline = colorSpace === "srgb" ? cache.pipelineSrgb : cache.pipelineLinear; const viewFormat = colorSpace === "srgb" ? "rgba8unorm-srgb" : "rgba8unorm"; const encoder = device.createCommandEncoder(); for (let level = 1; level < mipLevels; level++) { const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1, format: viewFormat }); const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1, format: viewFormat }); const bindGroup = device.createBindGroup({ layout: cache.bindGroupLayout, entries: [{ binding: 0, resource: cache.sampler }, { binding: 1, resource: srcView }] }); const pass = encoder.beginRenderPass({ colorAttachments: [{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }] }); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.draw(3); pass.end(); } device.queue.submit([encoder.finish()]); } }; // typescript/graphics/animation.ts var findKeyframe = (times, time) => { const n = times.length | 0; if (n <= 1) return { i0: 0, i1: 0, alpha: 0, dt: 0 }; if (time <= times[0]) return { i0: 0, i1: 0, alpha: 0, dt: times[1] - times[0] }; if (time >= times[n - 1]) return { i0: n - 1, i1: n - 1, alpha: 0, dt: times[n - 1] - times[n - 2] }; let lo = 0; let hi = n - 1; while (lo + 1 < hi) { const mid = lo + hi >> 1; if (times[mid] <= time) lo = mid; else hi = mid; } const i0 = lo; const i1 = lo + 1; const dt = times[i1] - times[i0]; if (dt === 0) return { i0, i1: i0, alpha: 0, dt: 0 }; return { i0, i1, alpha: clamp01((time - times[i0]) / dt), dt }; }; var hermite = (t) => { const t2 = t * t; const t3 = t2 * t; return [ 2 * t3 - 3 * t2 + 1, t3 - 2 * t2 + t, -2 * t3 + 3 * t2, t3 - t2 ]; }; var sampleValueSampler = (sampler, time, out) => { out.fill(0); const valueSize = sampler.valueSize | 0; if (valueSize <= 0) return; const { i0, i1, alpha, dt } = findKeyframe(sampler.input, time); switch (sampler.interpolation) { case "STEP": { const base = i0 * valueSize; for (let i = 0; i < valueSize; i++) out[i] = sampler.output[base + i] ?? 0; return; } case "CUBICSPLINE": { const [h00, h10, h01, h11] = hermite(alpha); const stride = valueSize * 3; const base0 = i0 * stride; const base1 = i1 * stride; const v0 = base0 + valueSize; const out0 = base0 + valueSize * 2; const in1 = base1; const v1 = base1 + valueSize; for (let i = 0; i < valueSize; i++) { const p0 = sampler.output[v0 + i] ?? 0; const m0 = (sampler.output[out0 + i] ?? 0) * dt; const p1 = sampler.output[v1 + i] ?? 0; const m1 = (sampler.output[in1 + i] ?? 0) * dt; out[i] = h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1; } return; } case "LINEAR": default: { const base0 = i0 * valueSize; const base1 = i1 * valueSize; for (let i = 0; i < valueSize; i++) { const v0 = sampler.output[base0 + i] ?? 0; const v1 = sampler.output[base1 + i] ?? 0; out[i] = v0 + (v1 - v0) * alpha; } return; } } }; var AnimationClip = class { name; samplerCount; channelCount; _samplersPtr; _channelsPtr; startTime; endTime; _ownedF32Allocs; _ownedU32Allocs; _weightSamplers; _weightChannels; _pointerSamplers; _pointerChannels; _disposed = false; constructor(desc) { this.name = desc.name; this.samplerCount = desc.samplerCount | 0; this.channelCount = desc.channelCount | 0; this._samplersPtr = desc.samplersPtr; this._channelsPtr = desc.channelsPtr; this.startTime = desc.startTime; this.endTime = desc.endTime; this._ownedF32Allocs = desc.ownedF32Allocs ?? null; this._ownedU32Allocs = desc.ownedU32Allocs ?? null; this._weightSamplers = desc.weightSamplers ?? null; this._weightChannels = desc.weightChannels ?? null; this._pointerSamplers = desc.pointerSamplers ?? null; this._pointerChannels = desc.pointerChannels ?? null; } get duration() { return Math.max(0, this.endTime - this.startTime); } get disposed() { return this._disposed; } get samplersPtr() { this.assertAlive(); return this._samplersPtr; } get channelsPtr() { this.assertAlive(); return this._channelsPtr; } assertAlive() { if (this._disposed) throw new Error(`AnimationClip '${this.name}' is disposed (use-after-dispose).`); } sample(timeSeconds) { this.assertAlive(); if (this.channelCount > 0) { const store = TransformStore.global(); const soa = { posPtr: store.posPtr, rotPtr: store.rotPtr, sclPtr: store.sclPtr }; animf.sampleClipTRS(soa.posPtr, soa.rotPtr, soa.sclPtr, store.count | 0, this.samplersPtr, this.samplerCount, this.channelsPtr, this.channelCount, timeSeconds); store.markDirty(); } if (this._weightSamplers && this._weightChannels) { for (const channel of this._weightChannels) { const sampler = this._weightSamplers[channel.sampler]; if (!sampler || channel.meshes.length === 0) continue; sampleValueSampler(sampler, timeSeconds, channel.scratch); for (const mesh of channel.meshes) setMeshMorphWeights(mesh, channel.scratch); } } if (this._pointerSamplers && this._pointerChannels) { for (const channel of this._pointerChannels) { const sampler = this._pointerSamplers[channel.sampler]; if (!sampler) continue; sampleValueSampler(sampler, timeSeconds, channel.scratch); channel.setValue(channel.scratch); } } } dispose() { if (this._disposed) return; if (this._ownedF32Allocs) { for (const a of this._ownedF32Allocs) if (a.ptr) wasm.freeF32(a.ptr, a.len | 0); } if (this._ownedU32Allocs) { for (const a of this._ownedU32Allocs) if (a.ptr) wasm.freeU32(a.ptr, a.len | 0); } this._disposed = true; this._ownedF32Allocs = null; this._ownedU32Allocs = null; this._weightSamplers = null; this._weightChannels = null; this._pointerSamplers = null; this._pointerChannels = null; } }; var AnimationPlayer = class { clip; time = 0; speed = 1; loop = true; playing = true; constructor(clip, opts = {}) { this.clip = clip; if (opts.speed !== void 0) this.speed = opts.speed; if (opts.loop !== void 0) this.loop = opts.loop; if (opts.playing !== void 0) this.playing = opts.playing; this.time = clip.startTime; } update(dtSeconds) { if (!this.playing) return; const dur = this.clip.duration; if (dur <= 0) { this.clip.sample(this.clip.startTime); return; } this.time += dtSeconds * this.speed; if (this.loop) { const start = this.clip.startTime; const end = this.clip.endTime; while (this.time < start) this.time += dur; while (this.time >= end) this.time -= dur; } else { this.time = Math.max(this.clip.startTime, Math.min(this.time, this.clip.endTime)); } this.clip.sample(this.time); } }; var Skin = class { name; joints; jointCount; _jointIndicesPtr; _invBindPtr; _disposed = false; constructor(name, joints, inverseBindMatrices) { this.name = name; this.joints = joints; this.jointCount = joints.length | 0; let jointIndicesPtr = 0; let invBindPtr = 0; try { jointIndicesPtr = wasm.allocU32(this.jointCount); if (!jointIndicesPtr && this.jointCount !== 0) throw new Error(`Skin '${name}': joint index allocation failed (${this.jointCount} elements).`); const u32 = wasm.u32view(jointIndicesPtr, this.jointCount); for (let i = 0; i < this.jointCount; i++) u32[i] = joints[i].index >>> 0; invBindPtr = wasm.allocF32(this.jointCount * 16); if (!invBindPtr && this.jointCount !== 0) throw new Error(`Skin '${name}': inverse bind allocation failed (${this.jointCount * 16} elements).`); const f32 = wasm.f32view(invBindPtr, this.jointCount * 16); if (inverseBindMatrices && inverseBindMatrices.length === this.jointCount * 16) { f32.set(inverseBindMatrices); } else { for (let j = 0; j < this.jointCount; j++) { const o = j * 16; f32[o + 0] = 1; f32[o + 1] = 0; f32[o + 2] = 0; f32[o + 3] = 0; f32[o + 4] = 0; f32[o + 5] = 1; f32[o + 6] = 0; f32[o + 7] = 0; f32[o + 8] = 0; f32[o + 9] = 0; f32[o + 10] = 1; f32[o + 11] = 0; f32[o + 12] = 0; f32[o + 13] = 0; f32[o + 14] = 0; f32[o + 15] = 1; } } } catch (error) { if (invBindPtr) wasm.freeF32(invBindPtr, this.jointCount * 16); if (jointIndicesPtr) wasm.freeU32(jointIndicesPtr, this.jointCount); throw error; } this._jointIndicesPtr = jointIndicesPtr; this._invBindPtr = invBindPtr; } get disposed() { return this._disposed; } get jointIndicesPtr() { this.assertAlive(); return this._jointIndicesPtr; } get invBindPtr() { this.assertAlive(); return this._invBindPtr; } assertAlive() { if (this._disposed) throw new Error(`Skin '${this.name}' is disposed (use-after-dispose).`); } createInstance(meshTransform) { this.assertAlive(); return new SkinInstance(this, meshTransform); } dispose() { if (this._disposed) return; if (this._jointIndicesPtr) wasm.freeU32(this._jointIndicesPtr, this.jointCount); if (this._invBindPtr) wasm.freeF32(this._invBindPtr, this.jointCount * 16); this._disposed = true; } }; var SkinInstance = class { skin; meshTransform; _disposed = false; boneBuffer = null; bindGroup = null; constructor(skin, meshTransform) { if (skin.disposed) throw new Error(`Skin '${skin.name}' is disposed (use-after-dispose).`); this.skin = skin; this.meshTransform = meshTransform; } get disposed() { return this._disposed; } get meshWorldMatrixPtr() { this.assertAlive(); return this.meshTransform.worldMatrixPtr; } assertAlive() { if (this._disposed) throw new Error(`SkinInstance '${this.skin.name}' is disposed (use-after-dispose).`); if (this.skin.disposed) throw new Error(`Skin '${this.skin.name}' is disposed (use-after-dispose).`); } get jointCount() { this.assertAlive(); return this.skin.jointCount; } ensureGpuResources(device, layout) { this.assertAlive(); if (this.boneBuffer && this.bindGroup) return; const byteSize = this.skin.jointCount * 16 * 4; this.boneBuffer = device.createBuffer({ size: byteSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); this.bindGroup = device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer: this.boneBuffer } }] }); } dispose() { if (this._disposed) return; this.boneBuffer?.destroy(); this.boneBuffer = null; this.bindGroup = null; this._disposed = true; } }; // typescript/gltf/import.ts var getNodeVisibility = (source) => { const ext = source?.extensions?.["KHR_node_visibility"]; return typeof ext?.visible === "boolean" ? ext.visible : true; }; var GltfImportedNode = class { index; name; transform; parentIndex; children; meshes; splatFields; camera; light; _visible; _effectiveVisible; _parentNode; _childNodes; constructor(index, transform, source) { this.index = index; this.name = source?.name; this.transform = transform; this.parentIndex = null; this.children = [...source?.children ?? []]; this.meshes = []; this.splatFields = []; this.camera = null; this.light = null; this._visible = getNodeVisibility(source); this._effectiveVisible = this._visible; this._parentNode = null; this._childNodes = []; } get visible() { return this._visible; } set visible(value) { this._visible = !!value; this.updateEffectiveVisibility(); } get effectiveVisible() { return this._effectiveVisible; } setParentNode(parent) { this._parentNode = parent; if (!parent._childNodes.includes(this)) parent._childNodes.push(this); this.updateEffectiveVisibility(); } applyVisibility() { for (const mesh of this.meshes) mesh.visible = this._effectiveVisible; for (const splatField of this.splatFields) splatField.visible = this._effectiveVisible; if (this.light) this.light.enabled = this._effectiveVisible; } updateEffectiveVisibility() { const next = this._visible && (this._parentNode?.effectiveVisible ?? true); this._effectiveVisible = next; this.applyVisibility(); for (const child of this._childNodes) child.updateEffectiveVisibility(); } }; var warn2 = (opts, msg) => { opts?.onWarning?.(msg); }; var ImportTransaction = class _ImportTransaction { _entries = []; _settled = false; own(value, label, cleanup) { if (this._settled) throw new Error("glTF import transaction is already settled."); const entry = { label, cleanup: () => cleanup(value), active: true }; this._entries.push(entry); return { value, transfer() { entry.active = false; return value; }, dispose() { if (!entry.active) return; entry.active = false; entry.cleanup(); } }; } defer(label, cleanup) { this.own(void 0, label, cleanup); } rollback(error) { const cleanupErrors = this.settleAndCleanup(); if (cleanupErrors.length > 0 && error && (typeof error === "object" || typeof error === "function")) try { Object.defineProperty(error, "cleanupErrors", { value: cleanupErrors, configurable: true }); } catch { } throw error; } commit() { if (this._settled) throw new Error("glTF import transaction is already settled."); this._settled = true; let entries = this._entries.filter((entry) => entry.active); this._entries = []; return () => { if (!entries) return; const current = entries; entries = null; const cleanupErrors = _ImportTransaction.cleanupEntries(current); if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors.map((item) => item.error), `glTF import cleanup failed for ${cleanupErrors.map((item) => item.label).join(", ")}.`); }; } settleAndCleanup() { if (this._settled) return []; this._settled = true; const entries = this._entries; this._entries = []; return _ImportTransaction.cleanupEntries(entries); } static cleanupEntries(entries) { const errors = []; for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; if (!entry.active) continue; entry.active = false; try { entry.cleanup(); } catch (error) { errors.push({ label: entry.label, error }); } } entries.length = 0; return errors; } }; var getTextureInfoTexCoord = (info) => { const transform = info?.extensions?.KHR_texture_transform; const texCoord = transform && typeof transform.texCoord === "number" ? transform.texCoord : info?.texCoord; return (texCoord ?? 0) | 0; }; var validateMaterialTextureCoordinates = (mat, attrs, opts, context) => { if (!mat) return; const validateInfo = (info, usage) => { if (!info) return; const texCoord = getTextureInfoTexCoord(info); if (texCoord < 0 || texCoord > 1) { warn2(opts, `${context}: texture usage '${usage}' references TEXCOORD_${texCoord}, but WasmGPU supports TEXCOORD_0 and TEXCOORD_1; using TEXCOORD_0.`); return; } if (attrs[`TEXCOORD_${texCoord}`] === void 0) warn2(opts, `${context}: texture usage '${usage}' references missing TEXCOORD_${texCoord}; sampling will use zero coordinates.`); }; validateInfo(mat.pbrMetallicRoughness?.baseColorTexture, "baseColor"); validateInfo(mat.pbrMetallicRoughness?.metallicRoughnessTexture, "metallicRoughness"); validateInfo(mat.normalTexture, "normal"); validateInfo(mat.occlusionTexture, "occlusion"); validateInfo(mat.emissiveTexture, "emissive"); const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness; validateInfo(specGloss?.diffuseTexture, "diffuse"); validateInfo(specGloss?.specularGlossinessTexture, "specularGlossiness"); const clearcoat = mat.extensions?.KHR_materials_clearcoat; validateInfo(clearcoat?.clearcoatTexture, "clearcoat"); validateInfo(clearcoat?.clearcoatRoughnessTexture, "clearcoatRoughness"); validateInfo(clearcoat?.clearcoatNormalTexture, "clearcoatNormal"); const specular = mat.extensions?.KHR_materials_specular; validateInfo(specular?.specularTexture, "specular"); validateInfo(specular?.specularColorTexture, "specularColor"); const sheen = mat.extensions?.KHR_materials_sheen; validateInfo(sheen?.sheenColorTexture, "sheenColor"); validateInfo(sheen?.sheenRoughnessTexture, "sheenRoughness"); const iridescence = mat.extensions?.KHR_materials_iridescence; validateInfo(iridescence?.iridescenceTexture, "iridescence"); validateInfo(iridescence?.iridescenceThicknessTexture, "iridescenceThickness"); const anisotropy = mat.extensions?.KHR_materials_anisotropy; validateInfo(anisotropy?.anisotropyTexture, "anisotropy"); const transmission = mat.extensions?.KHR_materials_transmission; validateInfo(transmission?.transmissionTexture, "transmission"); const volume = mat.extensions?.KHR_materials_volume; validateInfo(volume?.thicknessTexture, "volumeThickness"); const diffuseTransmission = mat.extensions?.KHR_materials_diffuse_transmission; validateInfo(diffuseTransmission?.diffuseTransmissionTexture, "diffuseTransmission"); validateInfo(diffuseTransmission?.diffuseTransmissionColorTexture, "diffuseTransmissionColor"); }; var GL_NEAREST = 9728; var GL_LINEAR = 9729; var GL_NEAREST_MIPMAP_NEAREST = 9984; var GL_LINEAR_MIPMAP_NEAREST = 9985; var GL_NEAREST_MIPMAP_LINEAR = 9986; var GL_LINEAR_MIPMAP_LINEAR = 9987; var GL_CLAMP_TO_EDGE = 33071; var GL_MIRRORED_REPEAT = 33648; var GL_REPEAT = 10497; var GL_POINTS = 0; var KHR_GAUSSIAN_SPLATTING = "KHR_gaussian_splatting"; var gltfWrapToAddressMode = (wrap) => { switch (wrap) { case GL_CLAMP_TO_EDGE: return "clamp-to-edge"; case GL_MIRRORED_REPEAT: return "mirror-repeat"; case GL_REPEAT: default: return "repeat"; } }; var gltfMagToFilterMode = (mag) => { switch (mag) { case GL_NEAREST: return "nearest"; case GL_LINEAR: default: return "linear"; } }; var gltfMinToFilterModes = (min) => { switch (min) { case GL_NEAREST: return { minFilter: "nearest", mipmapFilter: "nearest", useMipmaps: false }; case GL_LINEAR: return { minFilter: "linear", mipmapFilter: "nearest", useMipmaps: false }; case GL_NEAREST_MIPMAP_NEAREST: return { minFilter: "nearest", mipmapFilter: "nearest", useMipmaps: true }; case GL_LINEAR_MIPMAP_NEAREST: return { minFilter: "linear", mipmapFilter: "nearest", useMipmaps: true }; case GL_NEAREST_MIPMAP_LINEAR: return { minFilter: "nearest", mipmapFilter: "linear", useMipmaps: true }; case GL_LINEAR_MIPMAP_LINEAR: default: return { minFilter: "linear", mipmapFilter: "linear", useMipmaps: true }; } }; var inferMimeTypeFromUri = (uri) => { if (!uri) return void 0; const u = uri.toLowerCase(); if (u.endsWith(".png")) return "image/png"; if (u.endsWith(".jpg") || u.endsWith(".jpeg")) return "image/jpeg"; if (u.endsWith(".webp")) return "image/webp"; if (u.endsWith(".gif")) return "image/gif"; return void 0; }; var getSceneIndex = (json, opts) => { if (opts?.sceneIndex !== void 0) return opts.sceneIndex | 0; if (json.scene !== void 0) return json.scene | 0; return 0; }; var getKHRLightsFromRoot = (json) => { const ext = json.extensions?.["KHR_lights_punctual"]; if (!ext) return null; return ext; }; var getNodeKHRLight = (node) => { const ext = node.extensions?.["KHR_lights_punctual"]; if (!ext) return null; return ext; }; var isMaterialUnlit = (mat) => { const exts = mat.extensions; return !!exts?.["KHR_materials_unlit"]; }; var applyNodeMatrixViaWasmDecompose = (t, m) => { const matPtr = wasm.allocF32(16); if (!matPtr) throw new Error("applyNodeMatrixViaWasmDecompose: matrix scratch allocation failed."); let trsPtr = 0; try { trsPtr = wasm.allocF32(10); if (!trsPtr) throw new Error("applyNodeMatrixViaWasmDecompose: TRS scratch allocation failed."); const mat = wasm.f32view(matPtr, 16); for (let i = 0; i < 16; i++) mat[i] = m[i] ?? (i % 5 === 0 ? 1 : 0); mat4f.decomposeTRS(trsPtr, matPtr); const out = wasm.f32view(trsPtr, 10); t.setPosition(out[0], out[1], out[2]); t.setRotation(out[3], out[4], out[5], out[6]); t.setScale(out[7], out[8], out[9]); } finally { if (trsPtr) wasm.freeF32(trsPtr, 10); wasm.freeF32(matPtr, 16); } }; var getXmpPacketIndex = (source) => { const ext = source?.extensions?.["KHR_xmp_json_ld"]; return typeof ext?.packet === "number" ? ext.packet : null; }; var resolveXmpPacket = (packets, source) => { const packetIndex = getXmpPacketIndex(source); return packetIndex !== null && packetIndex >= 0 && packetIndex < packets.length ? packets[packetIndex] : null; }; var buildMetadataRecord = (index, source, packets = []) => { return { index, name: source?.name, extras: source?.extras, extensions: source?.extensions, xmp: resolveXmpPacket(packets, source) }; }; var buildMeshMetadata = (index, mesh, packets) => { return { ...buildMetadataRecord(index, mesh, packets), primitives: mesh.primitives.map((primitive, primitiveIndex) => ({ ...buildMetadataRecord(primitiveIndex, primitive, packets), material: primitive.material })) }; }; var GLTF_EXTENSION_SUPPORT_STATES = { KHR_lights_punctual: "supported", KHR_mesh_quantization: "supported", KHR_materials_unlit: "supported", KHR_materials_emissive_strength: "supported", KHR_materials_pbrSpecularGlossiness: "partial", KHR_materials_clearcoat: "supported", KHR_materials_transmission: "supported", KHR_materials_volume: "supported", KHR_materials_diffuse_transmission: "supported", KHR_materials_dispersion: "supported", KHR_materials_specular: "supported", KHR_materials_sheen: "supported", KHR_materials_iridescence: "supported", KHR_materials_anisotropy: "supported", KHR_materials_ior: "supported", KHR_materials_variants: "supported", KHR_gaussian_splatting: "partial", KHR_node_visibility: "supported", KHR_animation_pointer: "partial", KHR_xmp_json_ld: "supported", KHR_draco_mesh_compression: "deferred", KHR_texture_basisu: "deferred", KHR_texture_transform: "supported", EXT_mesh_gpu_instancing: "deferred", EXT_meshopt_compression: "deferred", EXT_texture_webp: "deferred" }; var KHR_ANIMATION_POINTER = "KHR_animation_pointer"; var KHR_GAUSSIAN_SPLATTING_EXTENSION = "KHR_gaussian_splatting"; var KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS = "KHR_materials_pbrSpecularGlossiness"; var KHR_DRACO_MESH_COMPRESSION = "KHR_draco_mesh_compression"; var KHR_TEXTURE_BASISU = "KHR_texture_basisu"; var EXT_MESH_GPU_INSTANCING = "EXT_mesh_gpu_instancing"; var EXT_MESHOPT_COMPRESSION = "EXT_meshopt_compression"; var EXT_TEXTURE_WEBP = "EXT_texture_webp"; var GLTF_EXTENSION_SUPPORT_RANK = { supported: 0, partial: 1, deferred: 2, unsupported: 3 }; var getExtension = (source, name) => source?.extensions?.[name]; var pointerTokens = (pointer) => { if (typeof pointer !== "string") return null; if (pointer === "") return []; if (!pointer.startsWith("/")) return null; return pointer.slice(1).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")); }; var pointerIndex = (tokens, index) => { const token = tokens[index]; if (token === void 0 || !/^(0|[1-9]\d*)$/.test(token)) return null; const value = Number(token); return Number.isSafeInteger(value) ? value : null; }; var accessorComponentCount = (type) => { switch (type) { case "SCALAR": return 1; case "VEC2": return 2; case "VEC3": return 3; case "VEC4": return 4; case "MAT2": return 4; case "MAT3": return 9; case "MAT4": return 16; default: return null; } }; var getAnimationPointerRuntimeTargets = (json, opts) => { const targets = { morphTargetCounts: /* @__PURE__ */ new Map(), materials: /* @__PURE__ */ new Set(), cameras: /* @__PURE__ */ new Set(), lights: /* @__PURE__ */ new Set() }; const sceneIndex = getSceneIndex(json, opts); const visited = /* @__PURE__ */ new Set(); const visit = (nodeIndex) => { if (visited.has(nodeIndex)) return; visited.add(nodeIndex); const node = json.nodes?.[nodeIndex]; if (!node) return; if (opts.importCameras && node.camera !== void 0) { const camera = json.cameras?.[node.camera]; const hasRuntimeCamera = camera?.type === "perspective" && !!camera.perspective || camera?.type === "orthographic" && !!camera.orthographic; if (hasRuntimeCamera) targets.cameras.add(node.camera); } if (opts.importLights) { const nodeLight = getNodeKHRLight(node); const light = nodeLight ? getKHRLightsFromRoot(json)?.lights?.[nodeLight.light] : void 0; if (nodeLight && light && (light.type === "directional" || light.type === "point" || light.type === "spot")) targets.lights.add(nodeLight.light); } const mesh = node.mesh !== void 0 ? json.meshes?.[node.mesh] : void 0; for (const primitive of mesh?.primitives ?? []) { const mode = primitive.mode ?? 4; const positionIndex = primitive.attributes?.POSITION; if (getExtension(primitive, KHR_GAUSSIAN_SPLATTING_EXTENSION) !== void 0 || mode !== 4 && mode !== 5 && mode !== 6 || positionIndex === void 0 || !json.accessors?.[positionIndex]) continue; const morphTargetCount = primitive.targets?.length ?? 0; if (morphTargetCount > (targets.morphTargetCounts.get(nodeIndex) ?? 0)) targets.morphTargetCounts.set(nodeIndex, morphTargetCount); if (primitive.material !== void 0 && json.materials?.[primitive.material]) targets.materials.add(primitive.material); const variants = getExtension(primitive, "KHR_materials_variants"); for (const mapping of Array.isArray(variants?.mappings) ? variants.mappings : []) if (typeof mapping.material === "number" && Number.isFinite(mapping.material) && json.materials?.[mapping.material | 0]) targets.materials.add(mapping.material | 0); } for (const child of node.children ?? []) visit(child); }; for (const root of json.scenes?.[sceneIndex]?.nodes ?? []) visit(root); return targets; }; var getMaterialPointerShape = (json, tokens, targets) => { const materialIndex = pointerIndex(tokens, 1); const material = materialIndex !== null ? json.materials?.[materialIndex] : void 0; if (materialIndex === null || !material || !targets.materials.has(materialIndex)) return null; const unlit = isMaterialUnlit(material); const pbr = material.pbrMetallicRoughness; if (tokens[2] === "pbrMetallicRoughness") { if (!pbr) return null; if (tokens.length === 4 && tokens[3] === "baseColorFactor") return { valueSize: 4 }; if (!unlit && tokens.length === 4 && (tokens[3] === "metallicFactor" || tokens[3] === "roughnessFactor")) return { valueSize: 1 }; if (tokens.length === 7 && tokens[4] === "extensions" && tokens[5] === "KHR_texture_transform") { const info = pbr[tokens[3]]; if ((!unlit || tokens[3] === "baseColorTexture") && getExtension(info, "KHR_texture_transform") && (tokens[6] === "rotation" || tokens[6] === "offset" || tokens[6] === "scale")) return { valueSize: tokens[6] === "rotation" ? 1 : 2 }; } return null; } if (tokens.length === 3 && tokens[2] === "alphaCutoff") return { valueSize: 1 }; if (unlit) return null; if (tokens.length === 3 && tokens[2] === "emissiveFactor") return { valueSize: 3 }; if (tokens.length === 4 && tokens[2] === "normalTexture" && tokens[3] === "scale" && material.normalTexture) return { valueSize: 1 }; if (tokens.length === 4 && tokens[2] === "occlusionTexture" && tokens[3] === "strength" && material.occlusionTexture) return { valueSize: 1 }; if (tokens.length === 6 && tokens[3] === "extensions" && tokens[4] === "KHR_texture_transform") { const info = material[tokens[2]]; if (getExtension(info, "KHR_texture_transform") && (tokens[5] === "rotation" || tokens[5] === "offset" || tokens[5] === "scale")) return { valueSize: tokens[5] === "rotation" ? 1 : 2 }; } if (tokens[2] !== "extensions" || tokens.length < 5 || !material.extensions?.[tokens[3]]) return null; const extensionProperties = { KHR_materials_anisotropy: { anisotropyStrength: 1, anisotropyRotation: 1 }, KHR_materials_clearcoat: { clearcoatFactor: 1, clearcoatRoughnessFactor: 1 }, KHR_materials_dispersion: { dispersion: 1 }, KHR_materials_emissive_strength: { emissiveStrength: 1 }, KHR_materials_ior: { ior: 1 }, KHR_materials_iridescence: { iridescenceFactor: 1, iridescenceIor: 1, iridescenceThicknessMinimum: 1, iridescenceThicknessMaximum: 1 }, KHR_materials_sheen: { sheenColorFactor: 3, sheenRoughnessFactor: 1 }, KHR_materials_specular: { specularFactor: 1, specularColorFactor: 3 }, KHR_materials_transmission: { transmissionFactor: 1 }, KHR_materials_volume: { thicknessFactor: 1, attenuationDistance: 1, attenuationColor: 3 }, KHR_materials_diffuse_transmission: { diffuseTransmissionFactor: 1, diffuseTransmissionColorFactor: 3 } }; const property = extensionProperties[tokens[3]]?.[tokens[4]]; if (tokens.length === 5 && property !== void 0) return { valueSize: property }; if (tokens.length === 6 && tokens[5] === "scale" && tokens[3] === "KHR_materials_clearcoat" && tokens[4] === "clearcoatNormalTexture") return { valueSize: 1 }; if (tokens.length === 8 && tokens[5] === "extensions" && tokens[6] === "KHR_texture_transform") { const extension = material.extensions[tokens[3]]; const info = extension?.[tokens[4]]; if (getExtension(info, "KHR_texture_transform") && (tokens[7] === "rotation" || tokens[7] === "offset" || tokens[7] === "scale")) return { valueSize: tokens[7] === "rotation" ? 1 : 2 }; } return null; }; var getAnimationPointerShape = (json, tokens, opts, targets) => { if (tokens[0] === "nodes") { const nodeIndex = pointerIndex(tokens, 1); const node = nodeIndex !== null ? json.nodes?.[nodeIndex] : void 0; if (nodeIndex === null || !node) return null; if (tokens.length === 3 && tokens[2] === "translation") return { valueSize: 3 }; if (tokens.length === 3 && (tokens[2] === "rotation" || tokens[2] === "scale")) return node.matrix ? null : { valueSize: tokens[2] === "rotation" ? 4 : 3 }; const targetCount = targets.morphTargetCounts.get(nodeIndex) ?? 0; if (tokens.length === 3 && tokens[2] === "weights") return targetCount > 0 ? { valueSize: targetCount } : null; if (tokens.length === 4 && tokens[2] === "weights") { const weightIndex = pointerIndex(tokens, 3); return weightIndex !== null && weightIndex < targetCount ? { valueSize: 1 } : null; } if (tokens.length === 5 && tokens[2] === "extensions" && tokens[3] === "KHR_node_visibility" && tokens[4] === "visible") return getExtension(node, "KHR_node_visibility") ? { valueSize: 1, requiresStep: true } : null; return null; } if (tokens[0] === "materials") return getMaterialPointerShape(json, tokens, targets); if (tokens[0] === "cameras") { if (!opts.importCameras) return null; const cameraIndex = pointerIndex(tokens, 1); const camera = cameraIndex !== null ? json.cameras?.[cameraIndex] : void 0; if (cameraIndex === null || !camera || !targets.cameras.has(cameraIndex) || tokens.length !== 4) return null; const family = tokens[2]; const property = tokens[3]; if (family === "perspective" && camera.type === "perspective" && camera.perspective && ["yfov", "znear", "zfar", "aspectRatio"].includes(property)) { if ((property === "aspectRatio" || property === "zfar") && camera.perspective?.[property] === void 0) return null; return { valueSize: 1 }; } if (family === "orthographic" && camera.type === "orthographic" && camera.orthographic && ["xmag", "ymag", "znear", "zfar"].includes(property)) return { valueSize: 1 }; return null; } if (tokens[0] === "extensions" && tokens[1] === "KHR_lights_punctual" && tokens[2] === "lights") { if (!opts.importLights) return null; const lightIndex = pointerIndex(tokens, 3); const root = getExtension(json, "KHR_lights_punctual"); const light = lightIndex !== null ? root?.lights?.[lightIndex] : void 0; if (lightIndex === null || !light || !targets.lights.has(lightIndex)) return null; if (tokens.length === 5 && ["color", "intensity"].includes(tokens[4])) return { valueSize: tokens[4] === "color" ? 3 : 1 }; if (tokens.length === 5 && tokens[4] === "range" && (light.type === "point" || light.type === "spot")) return { valueSize: 1 }; if (tokens.length === 6 && tokens[4] === "spot" && light.type === "spot" && ["innerConeAngle", "outerConeAngle"].includes(tokens[5])) return { valueSize: 1 }; } return null; }; var assessAnimationPointers = (json, opts) => { let occurrenceCount = 0; let unsupportedReason; const targets = getAnimationPointerRuntimeTargets(json, opts); for (let animationIndex = 0; animationIndex < (json.animations?.length ?? 0); animationIndex++) { const animation = json.animations[animationIndex]; for (let channelIndex = 0; channelIndex < animation.channels.length; channelIndex++) { const channel = animation.channels[channelIndex]; if (channel.target.path !== "pointer") continue; occurrenceCount++; const pointer = channel.target.extensions?.[KHR_ANIMATION_POINTER]?.pointer; const tokens = pointerTokens(pointer); const targetShape = tokens ? getAnimationPointerShape(json, tokens, opts, targets) : null; const sampler = animation.samplers[channel.sampler]; const input = sampler ? json.accessors?.[sampler.input] : void 0; const output = sampler ? json.accessors?.[sampler.output] : void 0; const interpolation = sampler?.interpolation ?? "LINEAR"; const componentCount = accessorComponentCount(output?.type); const expectedOutputCount = input ? input.count * (interpolation === "CUBICSPLINE" ? 3 : 1) : -1; const outputCount = output?.count ?? -1; const validInterpolation = interpolation === "LINEAR" || interpolation === "STEP" || interpolation === "CUBICSPLINE"; const validInput = input?.type === "SCALAR" && input.componentType === 5126; const actualValueSize = validInput && validInterpolation && componentCount !== null && input && expectedOutputCount > 0 && outputCount >= 0 && outputCount % expectedOutputCount === 0 ? componentCount * (outputCount / expectedOutputCount) : null; const valueSize = actualValueSize; const shapeMatches = channel.target.node === void 0 && !!targetShape && (targetShape.valueSize === null || targetShape.valueSize === valueSize) && (!targetShape.requiresStep || interpolation === "STEP"); if (!shapeMatches) unsupportedReason ??= `animation ${animationIndex} channel ${channelIndex} has an unsupported pointer target or sampler shape`; } } if (occurrenceCount === 0) return { state: "partial", reason: "no implemented pointer occurrence was found" }; return unsupportedReason ? { state: "partial", reason: unsupportedReason } : { state: "supported" }; }; var assessGaussianSplatting = (json) => { let occurrenceCount = 0; let unsupportedReason; const requiredAttributes = [ { semantic: "POSITION", type: "VEC3", supportedEncoding: isFloatNonNormalizedEncoding }, { semantic: "KHR_gaussian_splatting:ROTATION", type: "VEC4", supportedEncoding: (componentType, normalized) => isFloatEncoding(componentType) || isNormalizedSignedByteOrShort(componentType, normalized) }, { semantic: "KHR_gaussian_splatting:SCALE", type: "VEC3", supportedEncoding: (componentType) => isFloatEncoding(componentType) || isUnsignedByteOrShort(componentType) }, { semantic: "KHR_gaussian_splatting:OPACITY", type: "SCALAR", supportedEncoding: (componentType, normalized) => isFloatEncoding(componentType) || isNormalizedUnsignedByteOrShort(componentType, normalized) }, { semantic: "KHR_gaussian_splatting:SH_DEGREE_0_COEF_0", type: "VEC3", supportedEncoding: isFloatNonNormalizedEncoding } ]; for (let meshIndex = 0; meshIndex < (json.meshes?.length ?? 0); meshIndex++) { const mesh = json.meshes[meshIndex]; for (let primitiveIndex = 0; primitiveIndex < mesh.primitives.length; primitiveIndex++) { const primitive = mesh.primitives[primitiveIndex]; const extension = getExtension(primitive, KHR_GAUSSIAN_SPLATTING_EXTENSION); if (extension === void 0) continue; occurrenceCount++; const mode = primitive.mode ?? 4; let reason; if (!extension || Array.isArray(extension)) reason = "extension object is required."; else if (extension.kernel !== "ellipse") reason = `kernel '${String(extension.kernel)}' is not supported; expected 'ellipse'`; else if (extension.colorSpace !== "lin_rec709_display" && extension.colorSpace !== "srgb_rec709_display") reason = `colorSpace '${String(extension.colorSpace)}' is not supported`; else if (extension.projection !== void 0 && extension.projection !== "perspective") reason = `projection '${String(extension.projection)}' is not supported; expected 'perspective'`; else if (extension.sortingMethod !== void 0 && extension.sortingMethod !== "cameraDistance") reason = `sortingMethod '${String(extension.sortingMethod)}' is not supported; expected 'cameraDistance'`; else if (mode !== 0) reason = `primitive mode must be POINTS (0), got ${mode}.`; else { const attributes = primitive.attributes; let sourceCount = null; for (const { semantic, type, supportedEncoding } of requiredAttributes) { const accessorIndex = attributes?.[semantic]; const accessor = typeof accessorIndex === "number" ? json.accessors?.[accessorIndex] : void 0; if (accessorIndex === void 0) { reason = `missing required attribute '${semantic}'.`; break; } if (!accessor) { reason = `attribute '${semantic}' references missing accessor ${accessorIndex}.`; break; } if (accessor.type !== type) { reason = `attribute '${semantic}' must use accessor type ${type}, got ${accessor.type}.`; break; } if (!supportedEncoding(accessor.componentType, accessor.normalized === true)) { reason = `attribute '${semantic}' has unsupported accessor encoding componentType=${accessor.componentType} normalized=${accessor.normalized === true}.`; break; } if (sourceCount === null) sourceCount = accessor.count; else if (accessor.count !== sourceCount) { reason = `attribute '${semantic}' count ${accessor.count} does not match POSITION count ${sourceCount}.`; break; } } if (!reason && attributes && sourceCount !== null) { try { const sh0Accessor = attributes["KHR_gaussian_splatting:SH_DEGREE_0_COEF_0"]; const shAttributes = resolveGaussianSplatSHAttributes(attributes, sh0Accessor, `mesh ${meshIndex} primitive ${primitiveIndex}`); for (const [semantic, accessorIndex] of shAttributes.accessors) { const accessor = json.accessors?.[accessorIndex]; if (!accessor) { reason = `attribute '${semantic}' references missing accessor ${accessorIndex}.`; break; } if (accessor.type !== "VEC3") { reason = `attribute '${semantic}' must use accessor type VEC3, got ${accessor.type}.`; break; } if (!isFloatNonNormalizedEncoding(accessor.componentType, accessor.normalized === true)) { reason = `attribute '${semantic}' has unsupported accessor encoding componentType=${accessor.componentType} normalized=${accessor.normalized === true}.`; break; } if (accessor.count !== sourceCount) { reason = `attribute '${semantic}' count ${accessor.count} does not match POSITION count ${sourceCount}.`; break; } } } catch (error) { reason = error instanceof Error ? error.message : String(error); } } } if (reason) unsupportedReason ??= `mesh ${meshIndex} primitive ${primitiveIndex}: ${reason}`; } } if (occurrenceCount === 0) return { state: "partial", reason: "no implemented Gaussian splat occurrence was found" }; return unsupportedReason ? { state: "partial", reason: unsupportedReason } : { state: "supported" }; }; var getGltfMaterialRequiredTextureCount = (material) => { const contributingExtensions = /* @__PURE__ */ new Set(); const pbr = material.pbrMetallicRoughness; const ext = material.extensions; const clearcoat = ext?.KHR_materials_clearcoat; const specular = ext?.KHR_materials_specular; const sheen = ext?.KHR_materials_sheen; const iridescence = ext?.KHR_materials_iridescence; const anisotropy = ext?.KHR_materials_anisotropy; const transmission = ext?.KHR_materials_transmission; const volume = ext?.KHR_materials_volume; const diffuseTransmission = ext?.KHR_materials_diffuse_transmission; let featureMask = 0; if (pbr?.baseColorTexture) featureMask |= 1 /* BaseColorTexture */; if (pbr?.metallicRoughnessTexture) featureMask |= 2 /* MetallicRoughnessTexture */; if (material.normalTexture) featureMask |= 4 /* NormalTexture */; if (material.occlusionTexture) featureMask |= 8 /* OcclusionTexture */; if (material.emissiveTexture) featureMask |= 16 /* EmissiveTexture */; if (clearcoat?.clearcoatTexture) featureMask |= 64 /* ClearcoatTexture */; if (clearcoat?.clearcoatRoughnessTexture) featureMask |= 128 /* ClearcoatRoughnessTexture */; if (clearcoat?.clearcoatNormalTexture) featureMask |= 256 /* ClearcoatNormalTexture */; if (specular?.specularTexture) featureMask |= 16384 /* SpecularTexture */; if (specular?.specularColorTexture) featureMask |= 32768 /* SpecularColorTexture */; if (sheen?.sheenColorTexture) featureMask |= 131072 /* SheenColorTexture */; if (sheen?.sheenRoughnessTexture) featureMask |= 262144 /* SheenRoughnessTexture */; if (iridescence?.iridescenceTexture) featureMask |= 1048576 /* IridescenceTexture */; if (iridescence?.iridescenceThicknessTexture) featureMask |= 2097152 /* IridescenceThicknessTexture */; if (anisotropy?.anisotropyTexture) featureMask |= 8388608 /* AnisotropyTexture */; if (transmission !== void 0) featureMask |= 512 /* Transmission */; if (transmission?.transmissionTexture) featureMask |= 1024 /* TransmissionTexture */; if (volume?.thicknessTexture) featureMask |= 4096 /* ThicknessTexture */; if (diffuseTransmission !== void 0) featureMask |= 67108864 /* DiffuseTransmission */; if (diffuseTransmission?.diffuseTransmissionTexture) featureMask |= 134217728 /* DiffuseTransmissionTexture */; if (diffuseTransmission?.diffuseTransmissionColorTexture) featureMask |= 268435456 /* DiffuseTransmissionColorTexture */; const extensionSlots = [ ["KHR_materials_clearcoat", !!(featureMask & (64 /* ClearcoatTexture */ | 128 /* ClearcoatRoughnessTexture */ | 256 /* ClearcoatNormalTexture */))], ["KHR_materials_specular", !!(featureMask & (16384 /* SpecularTexture */ | 32768 /* SpecularColorTexture */))], ["KHR_materials_sheen", !!(featureMask & (131072 /* SheenColorTexture */ | 262144 /* SheenRoughnessTexture */))], ["KHR_materials_iridescence", !!(featureMask & (1048576 /* IridescenceTexture */ | 2097152 /* IridescenceThicknessTexture */))], ["KHR_materials_anisotropy", !!(featureMask & 8388608 /* AnisotropyTexture */)], ["KHR_materials_transmission", !!(featureMask & 512 /* Transmission */)], ["KHR_materials_volume", !!(featureMask & 4096 /* ThicknessTexture */)], ["KHR_materials_diffuse_transmission", !!(featureMask & 67108864 /* DiffuseTransmission */)], ["KHR_materials_dispersion", ext?.KHR_materials_dispersion !== void 0 && !!(featureMask & (512 /* Transmission */ | 67108864 /* DiffuseTransmission */))] ]; for (const [name, contributes] of extensionSlots) if (contributes) contributingExtensions.add(name); const plan = planStandardMaterialLayout(featureMask); return { sampledTextureCount: plan.sampledTextureCount, samplerCount: plan.samplerCount, contributingExtensions: Array.from(contributingExtensions) }; }; var getMaterialTextureCombinationLosses = (material, materialIndex) => { const losses = []; const required = getGltfMaterialRequiredTextureCount(material); if (required.sampledTextureCount > WEBGPU_BASELINE_MAX_SAMPLED_TEXTURES_PER_SHADER_STAGE || required.samplerCount > WEBGPU_BASELINE_MAX_SAMPLERS_PER_SHADER_STAGE) { const reason = `material ${materialIndex} combines textures requiring ${required.sampledTextureCount} sampled textures and ${required.samplerCount} samplers, which exceeds the WebGPU baseline limits of ${WEBGPU_BASELINE_MAX_SAMPLED_TEXTURES_PER_SHADER_STAGE} and ${WEBGPU_BASELINE_MAX_SAMPLERS_PER_SHADER_STAGE}`; for (const extName of required.contributingExtensions) losses.push({ name: extName, reason }); } return losses; }; var assessMaterialTextureCombinations = (json) => { const assessments = /* @__PURE__ */ new Map(); for (let materialIndex = 0; materialIndex < (json.materials?.length ?? 0); materialIndex++) for (const { name, reason } of getMaterialTextureCombinationLosses(json.materials[materialIndex], materialIndex)) if (!assessments.has(name)) assessments.set(name, { state: "partial", reason }); return assessments; }; var assessAssetExtensions = (json, opts) => { const assessments = /* @__PURE__ */ new Map(); if ((json.extensionsUsed ?? []).includes(KHR_ANIMATION_POINTER) || (json.extensionsRequired ?? []).includes(KHR_ANIMATION_POINTER)) assessments.set(KHR_ANIMATION_POINTER, assessAnimationPointers(json, opts)); if ((json.extensionsUsed ?? []).includes(KHR_GAUSSIAN_SPLATTING_EXTENSION) || (json.extensionsRequired ?? []).includes(KHR_GAUSSIAN_SPLATTING_EXTENSION)) assessments.set(KHR_GAUSSIAN_SPLATTING_EXTENSION, assessGaussianSplatting(json)); for (const [name, assessment] of assessMaterialTextureCombinations(json)) assessments.set(name, assessment); return assessments; }; var buildExtensionsMetadata = (json, opts) => { const used = [...json.extensionsUsed ?? []]; const required = [...json.extensionsRequired ?? []]; const names = /* @__PURE__ */ new Set([...used, ...required]); const support = {}; for (const name of names) support[name] = GLTF_EXTENSION_SUPPORT_STATES[name] ?? "unsupported"; const assessments = assessAssetExtensions(json, opts); for (const [name, assessment] of assessments) support[name] = assessment.state; return { metadata: { used, required, support }, assessments }; }; var markExtensionSupport = (extensions, name, state) => { const current = extensions.support[name]; if (!current || GLTF_EXTENSION_SUPPORT_RANK[state] > GLTF_EXTENSION_SUPPORT_RANK[current]) extensions.support[name] = state; }; var isExtensionRequired = (json, name) => (json.extensionsRequired ?? []).includes(name); var reportAnimationPointerLoss = (json, extensions, opts, message) => { markExtensionSupport(extensions, KHR_ANIMATION_POINTER, "partial"); if (isExtensionRequired(json, KHR_ANIMATION_POINTER)) throw new Error(`Required glTF extension '${KHR_ANIMATION_POINTER}' cannot be imported without semantic loss: ${message}`); warn2(opts, `${KHR_ANIMATION_POINTER}: ${message}`); }; var enforceRequiredExtensions = (json, extensions, assessments, opts) => { for (const name of json.extensionsRequired ?? []) { const state = extensions.support[name] ?? "unsupported"; if (state === "supported") continue; if (name === KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS && state === "partial") { warn2(opts, `Required glTF extension '${name}' is only partially supported; importing with diffuse/base-color and glossiness-to-roughness approximations. Specular contribution and specularGlossinessTexture are not fully represented.`); continue; } const reason = assessments.get(name)?.reason; if (state === "deferred") throw new Error(`Required glTF extension '${name}' is deferred: WasmGPU does not implement its defining behavior${reason ? ` (${reason})` : ""}.`); throw new Error(`Required glTF extension '${name}' is ${state} for this asset and cannot be imported without semantic loss${reason ? `: ${reason}` : "."}`); } }; var buildXmpMetadata = (json) => { const rootExt = json.extensions?.["KHR_xmp_json_ld"]; const packets = Array.isArray(rootExt?.packets) ? [...rootExt.packets] : []; const packet = resolveXmpPacket(packets, json.asset); return { packets, packet }; }; var createVariantsController = (initialItems = []) => { const items = [...initialItems]; const registrations = []; let activeIndex = null; const ensureKnownItem = (index) => { if (items.some((item) => item.index === index)) return; items.push({ index, name: `variant_${index}` }); items.sort((a, b) => a.index - b.index); }; const findItemByName = (name) => items.find((item) => item.name === name); const getActiveName = () => activeIndex === null ? null : items.find((item) => item.index === activeIndex)?.name ?? `variant_${activeIndex}`; const applyVariant = (index) => { activeIndex = index; for (const registration of registrations) { if (registration.mesh.destroyed) continue; const nextMaterial = index !== null ? registration.variants.get(index) ?? registration.baselineMaterial : registration.baselineMaterial; if (registration.mesh.material === nextMaterial) continue; nextMaterial.retain(); registration.mesh.setMaterial(nextMaterial); } }; return { public: { get items() { return items.map((item) => ({ ...item })); }, get names() { return items.map((item) => item.name ?? `variant_${item.index}`); }, get activeName() { return getActiveName(); }, get activeIndex() { return activeIndex; }, setActive(name) { if (name === null) { applyVariant(null); return; } const item = findItemByName(name); if (!item) throw new Error(`glTF variants: unknown variant '${name}'.`); applyVariant(item.index); }, setActiveIndex(index) { if (index === null) { applyVariant(null); return; } if (!items.some((item) => item.index === index)) throw new Error(`glTF variants: unknown variant index ${index}.`); applyVariant(index); }, clear() { applyVariant(null); } }, register(mesh, baselineMaterial, variants = /* @__PURE__ */ new Map()) { if (variants.size === 0) return; const retainedMaterials = Array.from(/* @__PURE__ */ new Set([baselineMaterial, ...variants.values()])); let retainedCount = 0; try { for (const material of retainedMaterials) { material.retain(); retainedCount++; } } catch (error) { for (let i = retainedCount - 1; i >= 0; i--) retainedMaterials[i].release(); throw error; } registrations.push({ mesh, baselineMaterial, variants, retainedMaterials }); for (const index of variants.keys()) ensureKnownItem(index); if (activeIndex !== null) applyVariant(activeIndex); }, destroy() { for (const registration of registrations) for (const material of registration.retainedMaterials) material.release(); registrations.length = 0; } }; }; var getDeclaredVariants = (json, packets) => { const rootExt = json.extensions?.["KHR_materials_variants"]; const variants = Array.isArray(rootExt?.variants) ? rootExt.variants : []; return variants.map((variant, index) => ({ ...buildMetadataRecord(index, variant, packets), name: variant?.name ?? `variant_${index}` })); }; var buildImportMetadata = (json, sceneIndex, extensions, xmp, variants) => { const scene = json.scenes?.[sceneIndex]; const packets = xmp.packets; return { asset: buildMetadataRecord(0, json.asset, packets), scene: scene ? buildMetadataRecord(sceneIndex, scene, packets) : null, nodes: (json.nodes ?? []).map((node, index) => buildMetadataRecord(index, node, packets)), meshes: (json.meshes ?? []).map((mesh, index) => buildMeshMetadata(index, mesh, packets)), materials: (json.materials ?? []).map((material, index) => buildMetadataRecord(index, material, packets)), textures: (json.textures ?? []).map((texture, index) => buildMetadataRecord(index, texture, packets)), images: (json.images ?? []).map((image, index) => buildMetadataRecord(index, image, packets)), cameras: (json.cameras ?? []).map((camera, index) => buildMetadataRecord(index, camera, packets)), skins: (json.skins ?? []).map((skin, index) => buildMetadataRecord(index, skin, packets)), animations: (json.animations ?? []).map((animation, index) => buildMetadataRecord(index, animation, packets)), extensions, xmp, variants }; }; var resolveMorphWeights = (weights, targetCount, opts, context) => { const out = new Float32Array(targetCount); if (!weights || targetCount <= 0) return out; const srcCount = weights.length | 0; const copyCount = Math.min(srcCount, targetCount); for (let i = 0; i < copyCount; i++) out[i] = Number(weights[i] ?? 0) || 0; if (srcCount < targetCount) warn2(opts, `${context}: morph weights length ${srcCount} is smaller than target count ${targetCount}; padding with zeros.`); else if (srcCount > targetCount) warn2(opts, `${context}: morph weights length ${srcCount} exceeds target count ${targetCount}; truncating extra values.`); return out; }; var normalizeWeightsTo4 = (weights) => { const out = new Float32Array(weights); for (let i = 0; i < out.length; i += 4) { const w0 = out[i + 0] ?? 0; const w1 = out[i + 1] ?? 0; const w2 = out[i + 2] ?? 0; const w3 = out[i + 3] ?? 0; const sum = w0 + w1 + w2 + w3; if (sum > 0) { const inv = 1 / sum; out[i + 0] = w0 * inv; out[i + 1] = w1 * inv; out[i + 2] = w2 * inv; out[i + 3] = w3 * inv; } else { out[i + 0] = 1; out[i + 1] = 0; out[i + 2] = 0; out[i + 3] = 0; } } return out; }; var normalizeWeightsTo8 = (weights0, weights1) => { const out0 = new Float32Array(weights0); const out1 = new Float32Array(weights1); for (let i = 0; i < out0.length; i += 4) { const w0 = out0[i + 0] ?? 0; const w1 = out0[i + 1] ?? 0; const w2 = out0[i + 2] ?? 0; const w3 = out0[i + 3] ?? 0; const w4 = out1[i + 0] ?? 0; const w5 = out1[i + 1] ?? 0; const w6 = out1[i + 2] ?? 0; const w7 = out1[i + 3] ?? 0; const sum = w0 + w1 + w2 + w3 + w4 + w5 + w6 + w7; if (sum > 0) { const inv = 1 / sum; out0[i + 0] = w0 * inv; out0[i + 1] = w1 * inv; out0[i + 2] = w2 * inv; out0[i + 3] = w3 * inv; out1[i + 0] = w4 * inv; out1[i + 1] = w5 * inv; out1[i + 2] = w6 * inv; out1[i + 3] = w7 * inv; } else { out0[i + 0] = 1; out0[i + 1] = 0; out0[i + 2] = 0; out0[i + 3] = 0; out1[i + 0] = 0; out1[i + 1] = 0; out1[i + 2] = 0; out1[i + 3] = 0; } } return { weights0: out0, weights1: out1 }; }; var triangulateStrip = (indices) => { const tris = []; for (let i = 0; i + 2 < indices.length; i++) { const a = indices[i]; const b = indices[i + 1]; const c = indices[i + 2]; if (a === b || b === c || a === c) continue; if ((i & 1) === 0) tris.push(a, b, c); else tris.push(b, a, c); } return new Uint32Array(tris); }; var triangulateFan = (indices) => { const tris = []; if (indices.length < 3) return new Uint32Array(0); const a0 = indices[0]; for (let i = 1; i + 1 < indices.length; i++) { const b = indices[i]; const c = indices[i + 1]; if (a0 === b || b === c || a0 === c) continue; tris.push(a0, b, c); } return new Uint32Array(tris); }; var getMaterialTangentTexCoords = (mat) => { if (!mat || isMaterialUnlit(mat)) return []; const texCoords = []; const addTexCoord = (texCoord) => { const resolvedTexCoord = texCoord === 1 ? 1 : 0; if (!texCoords.includes(resolvedTexCoord)) texCoords.push(resolvedTexCoord); }; const addInfo = (info) => { if (!info) return; addTexCoord(getTextureInfoTexCoord(info)); }; addInfo(mat.normalTexture); const clearcoat = mat.extensions?.KHR_materials_clearcoat; addInfo(clearcoat?.clearcoatNormalTexture); const anisotropy = mat.extensions?.KHR_materials_anisotropy; if (anisotropy?.anisotropyTexture) addInfo(anisotropy.anisotropyTexture); else if (anisotropy && texCoords.length === 0) addTexCoord(0); return texCoords; }; var getPrimitiveTangentTexCoords = (json, prim) => { const texCoords = []; const add = (material) => { for (const texCoord of getMaterialTangentTexCoords(material)) if (!texCoords.includes(texCoord)) texCoords.push(texCoord); }; add(prim.material !== void 0 ? json.materials?.[prim.material] : void 0); const mappings = prim.extensions?.["KHR_materials_variants"]?.mappings; for (const mapping of Array.isArray(mappings) ? mappings : []) if (typeof mapping.material === "number" && Number.isSafeInteger(mapping.material)) add(json.materials?.[mapping.material]); return texCoords; }; var getOrCreateMaterial = (doc, json, materialIndex, materialCache, textureCache, imageSourceCache, tx, opts) => { const ownReference = (material) => tx.own(material, `material ${materialIndex ?? "default"} reference`, (resource) => resource.release()); if (materialIndex === void 0) return ownReference(new StandardMaterial({ label: "glTF default material" })); const existing = materialCache.get(materialIndex); if (existing) return ownReference(existing.retain()); const mat = json.materials?.[materialIndex]; if (!mat) { const created2 = new StandardMaterial({ label: `glTF material ${materialIndex}` }); const owned2 = ownReference(created2); materialCache.set(materialIndex, created2); return owned2; } const textureCombinationLosses = getMaterialTextureCombinationLosses(mat, materialIndex); if (textureCombinationLosses.length > 0) warn2(opts, `${textureCombinationLosses.map((loss) => loss.name).join(", ")}: ${textureCombinationLosses[0].reason}`); const getOrCreateTextureByIndex = (textureIndex, transferFunction, usage) => { if (textureIndex === void 0) return null; const cacheKey = `${textureIndex}:${transferFunction}`; const cached = textureCache.get(cacheKey); if (cached) return cached; const texDef = json.textures?.[textureIndex]; if (!texDef) { warn2(opts, `glTF texture index ${textureIndex} missing (usage=${usage}).`); return null; } const textureExtensions = texDef.extensions; const alternativeSourceExtensions = [KHR_TEXTURE_BASISU, EXT_TEXTURE_WEBP].filter((name) => textureExtensions?.[name] !== void 0); const imageIndex = texDef.source; const img = imageIndex !== void 0 ? json.images?.[imageIndex] : void 0; const hasCoreImageSource = !!img && (typeof img.uri === "string" && img.uri.length > 0 || img.bufferView !== void 0); for (const extensionName of alternativeSourceExtensions) { if (hasCoreImageSource) warn2(opts, `glTF texture ${textureIndex}: ignoring optional ${extensionName} alternative source and using the core texture.source (usage=${usage}).`); else warn2(opts, `glTF texture ${textureIndex}: optional ${extensionName} has no usable core texture.source; its deferred alternative source is unavailable (usage=${usage}).`); } if (imageIndex === void 0 || !img) { warn2(opts, `glTF texture ${textureIndex} has no valid source image (usage=${usage}).`); return null; } let source = imageIndex !== void 0 ? imageSourceCache.get(imageIndex) ?? null : null; if (!source) { const loadedBytes = doc.images?.[imageIndex]; const mimeType = img.mimeType ?? inferMimeTypeFromUri(img.uri); if (loadedBytes && loadedBytes.byteLength > 0) { source = { kind: "bytes", bytes: loadedBytes, mimeType }; } else if (img.bufferView !== void 0) { const bv = json.bufferViews?.[img.bufferView]; const buf = bv ? doc.buffers[bv.buffer] : void 0; const bufferLength = bv ? json.buffers?.[bv.buffer]?.byteLength : void 0; const start = bv?.byteOffset ?? 0; const byteLength = bv?.byteLength; const startValue = typeof start === "number" && Number.isSafeInteger(start) && start >= 0 ? start : -1; const byteLengthValue = typeof byteLength === "number" && Number.isSafeInteger(byteLength) && byteLength >= 0 ? byteLength : -1; const bufferLengthValue = typeof bufferLength === "number" && Number.isSafeInteger(bufferLength) && bufferLength >= 0 ? bufferLength : -1; const end = startValue >= 0 && byteLengthValue >= 0 ? startValue + byteLengthValue : -1; if (bv && buf && startValue >= 0 && byteLengthValue >= 0 && bufferLengthValue >= 0 && Number.isSafeInteger(end) && end <= bufferLengthValue && end <= buf.byteLength) source = { kind: "bytes", bytes: buf.slice(startValue, end), mimeType }; else warn2(opts, `glTF image bufferView ${img.bufferView} missing (texture=${textureIndex}, usage=${usage}).`); } else if (img.uri) { if (isDataUri(img.uri)) { const decoded = decodeDataUri(img.uri); source = { kind: "bytes", bytes: decoded.data, mimeType: mimeType ?? decoded.mimeType ?? void 0 }; } else { const url = resolveUri(doc.resourceBaseUrl, img.uri); source = { kind: "url", url, mimeType }; } } if (source && imageIndex !== void 0) imageSourceCache.set(imageIndex, source); } if (!source) { warn2(opts, `Could not resolve image source for texture=${textureIndex} (usage=${usage}).`); return null; } const sampler = texDef.sampler !== void 0 ? json.samplers?.[texDef.sampler] : void 0; const addressModeU = gltfWrapToAddressMode(sampler?.wrapS); const addressModeV = gltfWrapToAddressMode(sampler?.wrapT); const magFilter = gltfMagToFilterMode(sampler?.magFilter); const { minFilter, mipmapFilter, useMipmaps } = gltfMinToFilterModes(sampler?.minFilter); const created2 = Texture2D.createFrom({ source, mipmaps: useMipmaps, sampler: { addressModeU, addressModeV, magFilter, minFilter, mipmapFilter }, imageDecode: { colorSpaceConversion: "none", fallbackWithoutOptions: false } }); tx.own(created2, `texture ${textureIndex} (${transferFunction})`, (texture) => texture.destroy()); textureCache.set(cacheKey, created2); return created2; }; const getTex = (info, slot) => { if (!info) return null; return getOrCreateTextureByIndex(info.index, getStandardMaterialTextureColorSpace(slot), slot); }; const getTextureTransform = (info) => { if (!info) return null; const ext = info.extensions; const transform = ext?.KHR_texture_transform; const texCoord = getTextureInfoTexCoord(info); const resolvedTexCoord = texCoord === 1 ? 1 : 0; if (!transform) return resolvedTexCoord === 1 ? { texCoord: 1 } : null; return { offset: [Number(transform.offset?.[0] ?? 0), Number(transform.offset?.[1] ?? 0)], rotation: Number(transform.rotation ?? 0), scale: [Number(transform.scale?.[0] ?? 1), Number(transform.scale?.[1] ?? 1)], texCoord: resolvedTexCoord }; }; const alphaMode = mat.alphaMode ?? "OPAQUE"; const alphaCutoff = alphaMode === "MASK" ? mat.alphaCutoff ?? 0.5 : 0; const blendMode = alphaMode === "BLEND" ? "transparent" /* Transparent */ : "opaque" /* Opaque */; const cullMode = mat.doubleSided ? "none" /* None */ : "back" /* Back */; const pbr = mat.pbrMetallicRoughness; const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness; if (!pbr && specGloss) { warn2(opts, `Material '${mat.name ?? materialIndex}' uses KHR_materials_pbrSpecularGlossiness; approximating using diffuse as baseColor. Specular/glossiness are not fully supported yet.`); if (specGloss.specularGlossinessTexture) warn2(opts, `Material '${mat.name ?? materialIndex}' has specularGlossinessTexture; currently ignored (highlights/roughness may look off).`); } const baseColorFactor = pbr?.baseColorFactor ?? specGloss?.diffuseFactor ?? [1, 1, 1, 1]; const baseColorTextureInfo = pbr?.baseColorTexture ?? specGloss?.diffuseTexture; const baseColorTexture = getTex(baseColorTextureInfo, "baseColor"); const baseColorTextureTransform = getTextureTransform(baseColorTextureInfo); let metallicFactor = 1; let roughnessFactor = 1; if (pbr) { metallicFactor = pbr.metallicFactor ?? 1; roughnessFactor = pbr.roughnessFactor ?? 1; } else if (specGloss) { metallicFactor = 0; const gloss = specGloss.glossinessFactor ?? 1; roughnessFactor = 1 - gloss; if (roughnessFactor < 0) roughnessFactor = 0; if (roughnessFactor > 1) roughnessFactor = 1; } const metallicRoughnessTextureInfo = pbr?.metallicRoughnessTexture; const normalTextureInfo = mat.normalTexture; const occlusionTextureInfo = mat.occlusionTexture; const emissiveTextureInfo = mat.emissiveTexture; const metallicRoughnessTexture = pbr ? getTex(metallicRoughnessTextureInfo, "metallicRoughness") : null; const metallicRoughnessTextureTransform = pbr ? getTextureTransform(metallicRoughnessTextureInfo) : null; const normalTexture = getTex(normalTextureInfo, "normal"); const normalTextureTransform = getTextureTransform(normalTextureInfo); const occlusionTexture = getTex(occlusionTextureInfo, "occlusion"); const occlusionTextureTransform = getTextureTransform(occlusionTextureInfo); const emissiveTexture = getTex(emissiveTextureInfo, "emissive"); const emissiveTextureTransform = getTextureTransform(emissiveTextureInfo); const normalScale = mat.normalTexture?.scale ?? 1; const occlusionStrength = mat.occlusionTexture?.strength ?? 1; const emissiveFactor = mat.emissiveFactor ?? [0, 0, 0]; const materialExtensions = mat.extensions ?? {}; const emissiveStrengthExt = materialExtensions.KHR_materials_emissive_strength; const emissiveStrength = emissiveStrengthExt?.emissiveStrength ?? 1; const clearcoatExt = materialExtensions.KHR_materials_clearcoat; const specularExt = materialExtensions.KHR_materials_specular; const sheenExt = materialExtensions.KHR_materials_sheen; const iridescenceExt = materialExtensions.KHR_materials_iridescence; const anisotropyExt = materialExtensions.KHR_materials_anisotropy; const transmissionExt = materialExtensions.KHR_materials_transmission; const volumeExt = materialExtensions.KHR_materials_volume; const diffuseTransmissionExt = materialExtensions.KHR_materials_diffuse_transmission; const dispersionExt = materialExtensions.KHR_materials_dispersion; const iorExt = materialExtensions.KHR_materials_ior; const emissiveIntensity = 1; const standardMaterialExtensions = {}; if (clearcoatExt) { standardMaterialExtensions.clearcoat = { factor: clearcoatExt.clearcoatFactor ?? 0, texture: getTex(clearcoatExt.clearcoatTexture, "clearcoat"), textureTransform: getTextureTransform(clearcoatExt.clearcoatTexture), roughness: clearcoatExt.clearcoatRoughnessFactor ?? 0, roughnessTexture: getTex(clearcoatExt.clearcoatRoughnessTexture, "clearcoatRoughness"), roughnessTextureTransform: getTextureTransform(clearcoatExt.clearcoatRoughnessTexture), normalTexture: getTex(clearcoatExt.clearcoatNormalTexture, "clearcoatNormal"), normalTextureTransform: getTextureTransform(clearcoatExt.clearcoatNormalTexture), normalScale: clearcoatExt.clearcoatNormalTexture?.scale ?? 1 }; } if (specularExt) { const specularColorFactor = Array.isArray(specularExt.specularColorFactor) ? specularExt.specularColorFactor : [1, 1, 1]; standardMaterialExtensions.specular = { factor: specularExt.specularFactor ?? 1, texture: getTex(specularExt.specularTexture, "specular"), textureTransform: getTextureTransform(specularExt.specularTexture), color: [specularColorFactor[0] ?? 1, specularColorFactor[1] ?? 1, specularColorFactor[2] ?? 1], colorTexture: getTex(specularExt.specularColorTexture, "specularColor"), colorTextureTransform: getTextureTransform(specularExt.specularColorTexture) }; } if (sheenExt) { const sheenColorFactor = Array.isArray(sheenExt.sheenColorFactor) ? sheenExt.sheenColorFactor : [0, 0, 0]; standardMaterialExtensions.sheen = { color: [sheenColorFactor[0] ?? 0, sheenColorFactor[1] ?? 0, sheenColorFactor[2] ?? 0], colorTexture: getTex(sheenExt.sheenColorTexture, "sheenColor"), colorTextureTransform: getTextureTransform(sheenExt.sheenColorTexture), roughness: sheenExt.sheenRoughnessFactor ?? 0, roughnessTexture: getTex(sheenExt.sheenRoughnessTexture, "sheenRoughness"), roughnessTextureTransform: getTextureTransform(sheenExt.sheenRoughnessTexture) }; } if (iridescenceExt) { standardMaterialExtensions.iridescence = { factor: iridescenceExt.iridescenceFactor ?? 0, texture: getTex(iridescenceExt.iridescenceTexture, "iridescence"), textureTransform: getTextureTransform(iridescenceExt.iridescenceTexture), ior: iridescenceExt.iridescenceIor ?? 1.3, thicknessMinimum: iridescenceExt.iridescenceThicknessMinimum ?? 100, thicknessMaximum: iridescenceExt.iridescenceThicknessMaximum ?? 400, thicknessTexture: getTex(iridescenceExt.iridescenceThicknessTexture, "iridescenceThickness"), thicknessTextureTransform: getTextureTransform(iridescenceExt.iridescenceThicknessTexture) }; } if (anisotropyExt) { standardMaterialExtensions.anisotropy = { strength: anisotropyExt.anisotropyStrength ?? 0, rotation: anisotropyExt.anisotropyRotation ?? 0, texture: getTex(anisotropyExt.anisotropyTexture, "anisotropy"), textureTransform: getTextureTransform(anisotropyExt.anisotropyTexture) }; } if (transmissionExt) { standardMaterialExtensions.transmission = { factor: transmissionExt.transmissionFactor ?? 0, texture: getTex(transmissionExt.transmissionTexture, "transmission"), textureTransform: getTextureTransform(transmissionExt.transmissionTexture) }; } if (volumeExt) { const attenuationColor = Array.isArray(volumeExt.attenuationColor) ? volumeExt.attenuationColor : [1, 1, 1]; standardMaterialExtensions.volume = { thicknessFactor: volumeExt.thicknessFactor ?? 0, thicknessTexture: getTex(volumeExt.thicknessTexture, "volumeThickness"), thicknessTextureTransform: getTextureTransform(volumeExt.thicknessTexture), attenuationDistance: volumeExt.attenuationDistance ?? Infinity, attenuationColor: [attenuationColor[0] ?? 1, attenuationColor[1] ?? 1, attenuationColor[2] ?? 1] }; } if (diffuseTransmissionExt) { const diffuseTransmissionColorFactor = Array.isArray(diffuseTransmissionExt.diffuseTransmissionColorFactor) ? diffuseTransmissionExt.diffuseTransmissionColorFactor : [1, 1, 1]; standardMaterialExtensions.diffuseTransmission = { factor: diffuseTransmissionExt.diffuseTransmissionFactor ?? 0, texture: getTex(diffuseTransmissionExt.diffuseTransmissionTexture, "diffuseTransmission"), textureTransform: getTextureTransform(diffuseTransmissionExt.diffuseTransmissionTexture), color: [diffuseTransmissionColorFactor[0] ?? 1, diffuseTransmissionColorFactor[1] ?? 1, diffuseTransmissionColorFactor[2] ?? 1], colorTexture: getTex(diffuseTransmissionExt.diffuseTransmissionColorTexture, "diffuseTransmissionColor"), colorTextureTransform: getTextureTransform(diffuseTransmissionExt.diffuseTransmissionColorTexture) }; } if (dispersionExt) standardMaterialExtensions.dispersion = { dispersion: dispersionExt.dispersion ?? 0 }; if (iorExt) standardMaterialExtensions.ior = { ior: iorExt.ior ?? 1.5 }; if (emissiveStrengthExt) standardMaterialExtensions.emissiveStrength = { strength: emissiveStrength }; const isUnlit = isMaterialUnlit(mat); const depthWrite = blendMode === "opaque" /* Opaque */; let created; if (isUnlit) { created = new UnlitMaterial({ color: [baseColorFactor[0] ?? 1, baseColorFactor[1] ?? 1, baseColorFactor[2] ?? 1], opacity: baseColorFactor[3] ?? 1, baseColorTexture, baseColorTextureTransform, alphaCutoff, blendMode, cullMode, depthWrite }); } else { created = new StandardMaterial({ label: mat.name ? `${mat.name} (glTF material ${materialIndex})` : `glTF material ${materialIndex}`, color: [baseColorFactor[0] ?? 1, baseColorFactor[1] ?? 1, baseColorFactor[2] ?? 1], opacity: baseColorFactor[3] ?? 1, metallic: metallicFactor, roughness: roughnessFactor, emissive: [emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0], emissiveIntensity, baseColorTexture, metallicRoughnessTexture, normalTexture, occlusionTexture, emissiveTexture, baseColorTextureTransform, metallicRoughnessTextureTransform, normalTextureTransform, occlusionTextureTransform, emissiveTextureTransform, normalScale, occlusionStrength, alphaCutoff, extensions: Object.keys(standardMaterialExtensions).length > 0 ? standardMaterialExtensions : void 0, blendMode, cullMode, depthWrite }); } const owned = ownReference(created); materialCache.set(materialIndex, created); return owned; }; var getPrimitiveVariantMaterials = (doc, json, prim, materialCache, textureCache, imageSourceCache, tx, opts, context) => { const ext = prim.extensions?.["KHR_materials_variants"]; const mappings = Array.isArray(ext?.mappings) ? ext.mappings : []; const variantMaterialIndices = /* @__PURE__ */ new Map(); for (const mapping of mappings) { if (typeof mapping.material !== "number" || !Number.isFinite(mapping.material) || !Array.isArray(mapping.variants)) continue; const materialIndex = mapping.material | 0; for (const variantIndex of mapping.variants) { if (typeof variantIndex !== "number" || !Number.isFinite(variantIndex)) continue; variantMaterialIndices.set(variantIndex | 0, materialIndex); } } const variants = /* @__PURE__ */ new Map(); const materialByIndex = /* @__PURE__ */ new Map(); for (const [variantIndex, materialIndex] of variantMaterialIndices) { let ownedMaterial = materialByIndex.get(materialIndex); if (!ownedMaterial) { validateMaterialTextureCoordinates(json.materials?.[materialIndex], prim.attributes, opts, `${context} variant material ${materialIndex}`); ownedMaterial = getOrCreateMaterial(doc, json, materialIndex, materialCache, textureCache, imageSourceCache, tx, opts); materialByIndex.set(materialIndex, ownedMaterial); } variants.set(variantIndex, ownedMaterial.value); } return { variants, ownedMaterials: Array.from(materialByIndex.values()) }; }; var getGaussianSplattingExtension = (prim) => prim.extensions?.[KHR_GAUSSIAN_SPLATTING]; var getPrimitiveAccessorIndices = (prim) => { const indices = []; if (typeof prim.indices === "number") indices.push(prim.indices); for (const accessorIndex of Object.values(prim.attributes ?? {})) if (typeof accessorIndex === "number") indices.push(accessorIndex); for (const target of prim.targets ?? []) for (const accessorIndex of Object.values(target)) if (typeof accessorIndex === "number") indices.push(accessorIndex); return indices; }; var primitiveUsesMeshopt = (json, prim) => { for (const accessorIndex of getPrimitiveAccessorIndices(prim)) { const accessor = json.accessors?.[accessorIndex]; if (!accessor) continue; const bufferViewIndices = [accessor.bufferView, accessor.sparse?.indices.bufferView, accessor.sparse?.values.bufferView]; for (const bufferViewIndex of bufferViewIndices) { const bufferView = bufferViewIndex !== void 0 ? json.bufferViews?.[bufferViewIndex] : void 0; if (bufferView?.extensions?.[EXT_MESHOPT_COMPRESSION] !== void 0) return true; } } return false; }; var failGaussianSplatting = (context, message) => { throw new Error(`${KHR_GAUSSIAN_SPLATTING}: ${context}: ${message}`); }; var handleUnsupportedGaussianSplatting = (json, extensions, opts, context, message) => { markExtensionSupport(extensions, KHR_GAUSSIAN_SPLATTING, "partial"); if (isExtensionRequired(json, KHR_GAUSSIAN_SPLATTING)) failGaussianSplatting(context, message); warn2(opts, `${KHR_GAUSSIAN_SPLATTING}: ${context}: ${message}; skipping primitive. WasmGPU does not implement optional sparse point-cloud fallback conversion for unsupported Gaussian splat primitives in this MVP.`); return null; }; var requireSplatAttribute = (attrs, semantic, context) => { const accessorIndex = attrs[semantic]; if (accessorIndex === void 0) return failGaussianSplatting(context, `missing required attribute '${semantic}'.`); return accessorIndex; }; var validateSplatAccessor = (json, accessorIndex, expectedType, isSupportedEncoding, context, semantic) => { const accessor = json.accessors?.[accessorIndex]; if (!accessor) return failGaussianSplatting(context, `attribute '${semantic}' references missing accessor ${accessorIndex}.`); if (accessor.type !== expectedType) failGaussianSplatting(context, `attribute '${semantic}' must use accessor type ${expectedType}, got ${accessor.type}.`); const normalized = accessor.normalized === true; if (!isSupportedEncoding(accessor.componentType, normalized)) failGaussianSplatting(context, `attribute '${semantic}' has unsupported accessor encoding componentType=${accessor.componentType} normalized=${normalized}.`); return accessor.count | 0; }; var validateSplatAttributeCount = (count, expectedCount, context, semantic) => { if (count !== expectedCount) failGaussianSplatting(context, `attribute '${semantic}' count ${count} does not match POSITION count ${expectedCount}.`); }; var isFloatEncoding = (componentType) => componentType === 5126; var isFloatNonNormalizedEncoding = (componentType, normalized) => componentType === 5126 && !normalized; var isNormalizedSignedByteOrShort = (componentType, normalized) => normalized && (componentType === 5120 || componentType === 5122); var isUnsignedByteOrShort = (componentType) => componentType === 5121 || componentType === 5123; var isNormalizedUnsignedByteOrShort = (componentType, normalized) => normalized && isUnsignedByteOrShort(componentType); var validateDecodedSplatAttributeLength = (data, expectedCount, componentCount, context, semantic) => { const expectedLength = expectedCount * componentCount; if (data.length !== expectedLength) failGaussianSplatting(context, `attribute '${semantic}' decoded length ${data.length} does not match expected length ${expectedLength}.`); }; var gatherFloatAttribute = (src, componentCount, indices) => { if (!indices) return src; const out = new Float32Array(indices.length * componentCount); for (let i = 0; i < indices.length; i++) { const srcBase = (indices[i] ?? 0) * componentCount; const dstBase = i * componentCount; for (let c = 0; c < componentCount; c++) out[dstBase + c] = src[srcBase + c] ?? 0; } return out; }; var expandIndexedAttribute = (src, itemSize, indices, context) => { if (itemSize <= 0 || src.length % itemSize !== 0) throw new Error(`${context}: attribute length ${src.length} is not divisible by item size ${itemSize}.`); const sourceCount = src.length / itemSize; const out = src instanceof Uint16Array ? new Uint16Array(indices.length * itemSize) : new Float32Array(indices.length * itemSize); for (let i = 0; i < indices.length; i++) { const sourceIndex = indices[i] ?? 0; if (sourceIndex >= sourceCount) throw new Error(`${context}: index ${sourceIndex} at ${i} is out of range for ${sourceCount} vertices.`); const sourceBase = sourceIndex * itemSize; const targetBase = i * itemSize; for (let component = 0; component < itemSize; component++) out[targetBase + component] = src[sourceBase + component] ?? 0; } return out; }; var validatePrimitiveAccessor = (json, accessorIndex, expectedType, expectedCount, opts, context, semantic) => { const accessor = Number.isSafeInteger(accessorIndex) && accessorIndex >= 0 ? json.accessors?.[accessorIndex] : void 0; if (!accessor) { warn2(opts, `${context}: ${semantic} references missing accessor ${accessorIndex}; ignoring ${semantic}.`); return null; } if (accessor.type !== expectedType) { warn2(opts, `${context}: ${semantic} accessor ${accessorIndex} has type ${accessor.type}; expected ${expectedType}. Ignoring ${semantic}.`); return null; } if (!Number.isSafeInteger(accessor.count) || accessor.count < 0) { warn2(opts, `${context}: ${semantic} accessor ${accessorIndex} has invalid count ${String(accessor.count)}; ignoring ${semantic}.`); return null; } if (expectedCount !== void 0 && accessor.count !== expectedCount) { warn2(opts, `${context}: ${semantic} accessor ${accessorIndex} count ${accessor.count} does not match expected count ${expectedCount}; ignoring ${semantic}.`); return null; } return accessor; }; var requirePrimitiveAccessor = (json, accessorIndex, expectedType, context, semantic) => { const accessor = Number.isSafeInteger(accessorIndex) && accessorIndex >= 0 ? json.accessors?.[accessorIndex] : void 0; if (!accessor) throw new Error(`${context}: Invalid accessor index: ${accessorIndex} for ${semantic}.`); if (accessor.type !== expectedType) throw new Error(`${context}: ${semantic} accessor ${accessorIndex} must have type ${expectedType}, got ${accessor.type}.`); if (!Number.isSafeInteger(accessor.count) || accessor.count < 0) throw new Error(`${context}: ${semantic} accessor ${accessorIndex} has invalid count ${String(accessor.count)}.`); return accessor; }; var readPrimitiveFloatAttribute = (doc, json, accessorIndex, expectedType, componentCount, expectedCount, opts, context, semantic, required = false) => { const accessor = validatePrimitiveAccessor(json, accessorIndex, expectedType, expectedCount, opts, context, semantic); if (!accessor) return null; let data; try { data = readAccessorAsFloat32(doc, accessorIndex); } catch (error) { const detail = error instanceof Error ? error.message : String(error); const message = `${context}: ${semantic} accessor ${accessorIndex} could not be decoded (${detail}); ignoring ${semantic}.`; if (required) throw new Error(message); warn2(opts, message); return null; } const expectedLength = expectedCount * componentCount; if (data.length !== expectedLength) { const message = `${context}: ${semantic} accessor ${accessorIndex} decoded length ${data.length} does not match expected length ${expectedLength}; ignoring ${semantic}.`; if (required) throw new Error(message); warn2(opts, message); return null; } return data; }; var validatePrimitiveIndexAccessor = (json, accessorIndex, context) => { const accessor = json.accessors?.[accessorIndex]; if (!accessor) throw new Error(`${context}: indices references missing accessor ${accessorIndex}.`); if (accessor.type !== "SCALAR") throw new Error(`${context}: indices accessor ${accessorIndex} must have type SCALAR, got ${accessor.type}.`); if (accessor.componentType !== 5121 && accessor.componentType !== 5123 && accessor.componentType !== 5125) throw new Error(`${context}: indices accessor ${accessorIndex} has unsupported componentType ${accessor.componentType}.`); if (!Number.isSafeInteger(accessor.count) || accessor.count < 0) throw new Error(`${context}: indices accessor ${accessorIndex} has invalid count ${String(accessor.count)}.`); if (accessor.normalized === true) throw new Error(`${context}: indices accessor ${accessorIndex} must not be normalized.`); }; var isJointAccessorEncoding = (accessor) => (accessor.componentType === 5121 || accessor.componentType === 5123) && accessor.normalized !== true; var isWeightAccessorEncoding = (accessor) => accessor.componentType === 5126 && accessor.normalized !== true || (accessor.componentType === 5121 || accessor.componentType === 5123) && accessor.normalized === true; var readGltfColorAccessor = (doc, json, accessorIndex, baseVertexCount, morph, opts, context) => { const accessor = json.accessors?.[accessorIndex]; if (!accessor) { warn2(opts, `${context}: COLOR_0 references missing accessor ${accessorIndex}; ignoring colors.`); return null; } const componentCount = accessor.type === "VEC3" ? 3 : accessor.type === "VEC4" ? 4 : 0; const validFloat = accessor.componentType === 5126 && accessor.normalized !== true; const validNormalized = accessor.normalized === true && (morph ? [5120, 5121, 5122, 5123].includes(accessor.componentType) : [5121, 5123].includes(accessor.componentType)); if (componentCount === 0 || !validFloat && !validNormalized) { warn2(opts, `${context}: COLOR_0 accessor ${accessorIndex} has unsupported type/encoding; expected VEC3/VEC4 float${morph ? " or normalized byte/short" : " or normalized unsigned byte/short"}.`); return null; } if (accessor.count !== baseVertexCount) { warn2(opts, `${context}: COLOR_0 accessor ${accessorIndex} count ${accessor.count} does not match base vertex count ${baseVertexCount}; ignoring colors.`); return null; } let source; try { source = readAccessorAsFloat32(doc, accessorIndex); } catch (error) { const detail = error instanceof Error ? error.message : String(error); warn2(opts, `${context}: COLOR_0 accessor ${accessorIndex} could not be decoded (${detail}); ignoring colors.`); return null; } if (source.length !== baseVertexCount * componentCount) { warn2(opts, `${context}: COLOR_0 accessor ${accessorIndex} decoded length ${source.length} does not match expected length ${baseVertexCount * componentCount}; ignoring colors.`); return null; } const out = new Float32Array(baseVertexCount * 4); const finiteOrZero = (value) => Number.isFinite(value) ? value : 0; for (let vertex = 0; vertex < baseVertexCount; vertex++) { const sourceBase = vertex * componentCount; const targetBase = vertex * 4; out[targetBase + 0] = finiteOrZero(source[sourceBase + 0] ?? 0); out[targetBase + 1] = finiteOrZero(source[sourceBase + 1] ?? 0); out[targetBase + 2] = finiteOrZero(source[sourceBase + 2] ?? 0); out[targetBase + 3] = componentCount === 4 ? finiteOrZero(source[sourceBase + 3] ?? 0) : morph ? 0 : 1; if (!morph) { out[targetBase + 0] = Math.max(0, Math.min(1, out[targetBase + 0])); out[targetBase + 1] = Math.max(0, Math.min(1, out[targetBase + 1])); out[targetBase + 2] = Math.max(0, Math.min(1, out[targetBase + 2])); out[targetBase + 3] = Math.max(0, Math.min(1, out[targetBase + 3])); } } return out; }; var validateSplatIndices = (indices, sourceCount, context) => { if (!indices) return; for (let i = 0; i < indices.length; i++) { const index = indices[i]; if (index >= sourceCount) failGaussianSplatting(context, `indices[${i}] value ${index} is out of range for splat attribute count ${sourceCount}.`); } }; var validateFiniteSplatValues = (data, context, semantic) => { for (let i = 0; i < data.length; i++) if (!Number.isFinite(data[i])) failGaussianSplatting(context, `attribute '${semantic}' contains non-finite value at component ${i}.`); }; var validateSplatScaleValues = (scales, context) => { validateFiniteSplatValues(scales, context, "KHR_gaussian_splatting:SCALE"); for (let i = 0; i < scales.length; i++) if ((scales[i] ?? 0) < 0) failGaussianSplatting(context, `attribute 'KHR_gaussian_splatting:SCALE' contains negative value at component ${i}.`); }; var validateSplatOpacityValues = (opacities, context) => { validateFiniteSplatValues(opacities, context, "KHR_gaussian_splatting:OPACITY"); for (let i = 0; i < opacities.length; i++) { const value = opacities[i] ?? 0; if (value < 0 || value > 1) failGaussianSplatting(context, `attribute 'KHR_gaussian_splatting:OPACITY' contains value outside [0, 1] at component ${i}.`); } }; var shDegreeCoeffCount = (degree) => { switch (degree) { case 0: return 1; case 1: return 3; case 2: return 5; case 3: return 7; } }; var shSemantic = (degree, coeff) => `${KHR_GAUSSIAN_SPLATTING}:SH_DEGREE_${degree}_COEF_${coeff}`; var resolveGaussianSplatSHAttributes = (attrs, sh0Acc, context) => { const complete = [true, false, false, false]; const accessors = /* @__PURE__ */ new Map([[shSemantic(0, 0), sh0Acc]]); for (const semantic of Object.keys(attrs)) { if (attrs[semantic] === void 0) continue; const match = /^KHR_gaussian_splatting:SH_DEGREE_(\d+)_COEF_(\d+)$/.exec(semantic); if (!match) continue; const degree2 = Number(match[1]); const coeff = Number(match[2]); if (degree2 < 0 || degree2 > 3) failGaussianSplatting(context, `attribute '${semantic}' uses unsupported spherical harmonic degree ${degree2}.`); if (coeff < 0 || coeff >= shDegreeCoeffCount(degree2)) failGaussianSplatting(context, `attribute '${semantic}' uses unsupported coefficient index ${coeff} for degree ${degree2}.`); } for (const degree2 of [1, 2, 3]) { const required = shDegreeCoeffCount(degree2); let present = 0; let firstMissing = null; for (let coeff = 0; coeff < required; coeff++) { const semantic = shSemantic(degree2, coeff); const accessorIndex = attrs[semantic]; if (accessorIndex === void 0) { if (!firstMissing) firstMissing = semantic; continue; } present++; accessors.set(semantic, accessorIndex); } if (present > 0 && present !== required) failGaussianSplatting(context, `degree ${degree2} spherical harmonic attributes must be complete; missing '${firstMissing}'.`); complete[degree2] = present === required; } if (complete[2] && !complete[1]) failGaussianSplatting(context, "degree 2 spherical harmonic attributes require complete degree 1 attributes."); if (complete[3] && (!complete[1] || !complete[2])) failGaussianSplatting(context, "degree 3 spherical harmonic attributes require complete degree 1 and degree 2 attributes."); const degree = complete[3] ? 3 : complete[2] ? 2 : complete[1] ? 1 : 0; return { degree, accessors }; }; var gatherSHDegreeData = (coefficients, indices) => { if (coefficients.length === 0) return new Float32Array(0); const gathered = coefficients.map((coeff) => gatherFloatAttribute(coeff, 3, indices)); const count = gathered[0].length / 3 | 0; const out = new Float32Array(count * coefficients.length * 3); for (let i = 0; i < count; i++) { for (let coeff = 0; coeff < gathered.length; coeff++) { const src = gathered[coeff]; const srcBase = i * 3; const dstBase = (i * gathered.length + coeff) * 3; out[dstBase + 0] = src[srcBase + 0] ?? 0; out[dstBase + 1] = src[srcBase + 1] ?? 0; out[dstBase + 2] = src[srcBase + 2] ?? 0; } } return out; }; var resolveGaussianSplatColorSpace = (value) => { if (value === "lin_rec709_display") return "linear"; if (value === "srgb_rec709_display") return "srgb"; return null; }; var createSplatFieldFromPrimitive = (doc, json, gltfMesh, meshIndex, prim, primIndex, node, nodeT, extensions, tx, opts) => { const context = `Mesh '${gltfMesh.name ?? meshIndex}' primitive ${primIndex}`; const extValue = getGaussianSplattingExtension(prim); if (!extValue || typeof extValue !== "object" || Array.isArray(extValue)) failGaussianSplatting(context, "extension object is required."); const ext = extValue; if (ext.kernel === void 0) failGaussianSplatting(context, "missing required property 'kernel'."); if (ext.kernel !== "ellipse") return handleUnsupportedGaussianSplatting(json, extensions, opts, context, `kernel '${String(ext.kernel)}' is not supported; expected 'ellipse'`); if (ext.colorSpace === void 0) failGaussianSplatting(context, "missing required property 'colorSpace'."); const colorSpace = resolveGaussianSplatColorSpace(ext.colorSpace); if (!colorSpace) return handleUnsupportedGaussianSplatting(json, extensions, opts, context, `colorSpace '${String(ext.colorSpace)}' is not supported`); if (ext.projection !== void 0 && ext.projection !== "perspective") return handleUnsupportedGaussianSplatting(json, extensions, opts, context, `projection '${String(ext.projection)}' is not supported; expected 'perspective'`); if (ext.sortingMethod !== void 0 && ext.sortingMethod !== "cameraDistance") return handleUnsupportedGaussianSplatting(json, extensions, opts, context, `sortingMethod '${String(ext.sortingMethod)}' is not supported; expected 'cameraDistance'`); const mode = prim.mode ?? 4; if (mode !== GL_POINTS) failGaussianSplatting(context, `primitive mode must be POINTS (0), got ${mode}.`); const attrs = prim.attributes; const positionAcc = requireSplatAttribute(attrs, "POSITION", context); const rotationAcc = requireSplatAttribute(attrs, "KHR_gaussian_splatting:ROTATION", context); const scaleAcc = requireSplatAttribute(attrs, "KHR_gaussian_splatting:SCALE", context); const opacityAcc = requireSplatAttribute(attrs, "KHR_gaussian_splatting:OPACITY", context); const sh0Acc = requireSplatAttribute(attrs, "KHR_gaussian_splatting:SH_DEGREE_0_COEF_0", context); const shAttrs = resolveGaussianSplatSHAttributes(attrs, sh0Acc, context); const sourceCount = validateSplatAccessor(json, positionAcc, "VEC3", (componentType, normalized) => isFloatNonNormalizedEncoding(componentType, normalized), context, "POSITION"); validateSplatAttributeCount(validateSplatAccessor(json, rotationAcc, "VEC4", (componentType, normalized) => isFloatEncoding(componentType) || isNormalizedSignedByteOrShort(componentType, normalized), context, "KHR_gaussian_splatting:ROTATION"), sourceCount, context, "KHR_gaussian_splatting:ROTATION"); validateSplatAttributeCount(validateSplatAccessor(json, scaleAcc, "VEC3", (componentType) => isFloatEncoding(componentType) || isUnsignedByteOrShort(componentType), context, "KHR_gaussian_splatting:SCALE"), sourceCount, context, "KHR_gaussian_splatting:SCALE"); validateSplatAttributeCount(validateSplatAccessor(json, opacityAcc, "SCALAR", (componentType, normalized) => isFloatEncoding(componentType) || isNormalizedUnsignedByteOrShort(componentType, normalized), context, "KHR_gaussian_splatting:OPACITY"), sourceCount, context, "KHR_gaussian_splatting:OPACITY"); for (const [semantic, accessorIndex] of shAttrs.accessors) validateSplatAttributeCount(validateSplatAccessor(json, accessorIndex, "VEC3", (componentType, normalized) => isFloatNonNormalizedEncoding(componentType, normalized), context, semantic), sourceCount, context, semantic); markExtensionSupport(extensions, KHR_GAUSSIAN_SPLATTING, "supported"); const indices = prim.indices !== void 0 ? readIndicesAsUint32(doc, prim.indices) : null; validateSplatIndices(indices, sourceCount, context); const splatCount = indices ? indices.length : sourceCount; const sourcePositions = readAccessorAsFloat32(doc, positionAcc); const sourceRotations = readAccessorAsFloat32(doc, rotationAcc); const sourceScales = readAccessorAsFloat32(doc, scaleAcc); const sourceOpacities = readAccessorAsFloat32(doc, opacityAcc); validateDecodedSplatAttributeLength(sourcePositions, sourceCount, 3, context, "POSITION"); validateDecodedSplatAttributeLength(sourceRotations, sourceCount, 4, context, "KHR_gaussian_splatting:ROTATION"); validateDecodedSplatAttributeLength(sourceScales, sourceCount, 3, context, "KHR_gaussian_splatting:SCALE"); validateDecodedSplatAttributeLength(sourceOpacities, sourceCount, 1, context, "KHR_gaussian_splatting:OPACITY"); const positions = gatherFloatAttribute(sourcePositions, 3, indices); const rotations = gatherFloatAttribute(sourceRotations, 4, indices); const scales = gatherFloatAttribute(sourceScales, 3, indices); const opacities = gatherFloatAttribute(sourceOpacities, 1, indices); validateFiniteSplatValues(positions, context, "POSITION"); validateFiniteSplatValues(rotations, context, "KHR_gaussian_splatting:ROTATION"); validateSplatScaleValues(scales, context); validateSplatOpacityValues(opacities, context); const readSHCoeff = (degree, coeff) => { const semantic = shSemantic(degree, coeff); const accessorIndex = shAttrs.accessors.get(semantic) ?? failGaussianSplatting(context, `missing required attribute '${semantic}'.`); const data = readAccessorAsFloat32(doc, accessorIndex); validateDecodedSplatAttributeLength(data, sourceCount, 3, context, semantic); validateFiniteSplatValues(data, context, semantic); return data; }; const sh0 = gatherSHDegreeData([readSHCoeff(0, 0)], indices); const sh1 = shAttrs.degree >= 1 ? gatherSHDegreeData([readSHCoeff(1, 0), readSHCoeff(1, 1), readSHCoeff(1, 2)], indices) : void 0; const sh2 = shAttrs.degree >= 2 ? gatherSHDegreeData([readSHCoeff(2, 0), readSHCoeff(2, 1), readSHCoeff(2, 2), readSHCoeff(2, 3), readSHCoeff(2, 4)], indices) : void 0; const sh3 = shAttrs.degree >= 3 ? gatherSHDegreeData([readSHCoeff(3, 0), readSHCoeff(3, 1), readSHCoeff(3, 2), readSHCoeff(3, 3), readSHCoeff(3, 4), readSHCoeff(3, 5), readSHCoeff(3, 6)], indices) : void 0; const field = new SplatField({ name: node.name ?? gltfMesh.name ?? `gltf_splatfield_${meshIndex}_${primIndex}`, positions, rotations, scales, opacities, sh0, sh1, sh2, sh3, shDegree: shAttrs.degree, splatCount, colorSpace }); tx.own(field, `${context} splat field`, (resource) => resource.destroy()); field.transform.setParent(nodeT); return field; }; var buildGeometryFromPrimitive = (doc, json, prim, computeMissingNormals, opts) => { const attrs = prim.attributes; const posAcc = attrs["POSITION"]; if (posAcc === void 0) { warn2(opts, "Primitive missing POSITION; skipping"); return null; } const context = "Primitive"; const positionAccessor = requirePrimitiveAccessor(json, posAcc, "VEC3", context, "POSITION"); const baseVertexCount = positionAccessor.count; let positions = readPrimitiveFloatAttribute(doc, json, posAcc, "VEC3", 3, baseVertexCount, opts, context, "POSITION", true); if (!positions) return null; let normals = null; let nAcc; const normalAcc = attrs["NORMAL"]; if (normalAcc !== void 0) { const normalData = readPrimitiveFloatAttribute(doc, json, normalAcc, "VEC3", 3, baseVertexCount, opts, context, "NORMAL"); if (normalData) { nAcc = normalAcc; normals = normalData; } } let tangents = null; const tangentAcc = attrs["TANGENT"]; if (tangentAcc !== void 0) tangents = readPrimitiveFloatAttribute(doc, json, tangentAcc, "VEC4", 4, baseVertexCount, opts, context, "TANGENT"); let colors = null; const colorAcc = attrs["COLOR_0"]; if (colorAcc !== void 0) colors = readGltfColorAccessor(doc, json, colorAcc, baseVertexCount, false, opts, "Primitive"); let uvs = null; const uvAcc = attrs["TEXCOORD_0"]; if (uvAcc !== void 0) uvs = readPrimitiveFloatAttribute(doc, json, uvAcc, "VEC2", 2, baseVertexCount, opts, context, "TEXCOORD_0"); let uvs1 = null; const uv1Acc = attrs["TEXCOORD_1"]; if (uv1Acc !== void 0) uvs1 = readPrimitiveFloatAttribute(doc, json, uv1Acc, "VEC2", 2, baseVertexCount, opts, context, "TEXCOORD_1"); let joints = null; let weights = null; let joints1 = null; let weights1 = null; const jAcc0 = attrs["JOINTS_0"]; const wAcc0 = attrs["WEIGHTS_0"]; const jAcc1 = attrs["JOINTS_1"]; const wAcc1 = attrs["WEIGHTS_1"]; const joints0Accessor = jAcc0 !== void 0 ? validatePrimitiveAccessor(json, jAcc0, "VEC4", baseVertexCount, opts, context, "JOINTS_0") : null; const weights0Accessor = wAcc0 !== void 0 ? validatePrimitiveAccessor(json, wAcc0, "VEC4", baseVertexCount, opts, context, "WEIGHTS_0") : null; if (jAcc0 !== void 0 && wAcc0 !== void 0 && joints0Accessor && weights0Accessor && isJointAccessorEncoding(joints0Accessor) && isWeightAccessorEncoding(weights0Accessor)) { try { const joints0 = readAccessorAsUint16(doc, jAcc0); const weights0 = readAccessorAsFloat32(doc, wAcc0); const joints1Accessor = jAcc1 !== void 0 ? validatePrimitiveAccessor(json, jAcc1, "VEC4", baseVertexCount, opts, context, "JOINTS_1") : null; const weights1Accessor = wAcc1 !== void 0 ? validatePrimitiveAccessor(json, wAcc1, "VEC4", baseVertexCount, opts, context, "WEIGHTS_1") : null; if (jAcc1 !== void 0 && wAcc1 !== void 0 && joints1Accessor && weights1Accessor && isJointAccessorEncoding(joints1Accessor) && isWeightAccessorEncoding(weights1Accessor)) { const joints1Raw = readAccessorAsUint16(doc, jAcc1); const weights1Raw = readAccessorAsFloat32(doc, wAcc1); if (joints1Raw.length === joints0.length && weights1Raw.length === weights0.length) { const norm = normalizeWeightsTo8(weights0, weights1Raw); joints = joints0; weights = norm.weights0; joints1 = joints1Raw; weights1 = norm.weights1; } else { warn2(opts, "Primitive has JOINTS_1/WEIGHTS_1 but lengths don't match JOINTS_0/WEIGHTS_0; ignoring additional influences"); joints = joints0; weights = normalizeWeightsTo4(weights0); } } else if (jAcc1 !== void 0 || wAcc1 !== void 0) { warn2(opts, "Primitive has JOINTS_1/WEIGHTS_1 mismatch; ignoring additional influences"); joints = joints0; weights = normalizeWeightsTo4(weights0); } else { joints = joints0; weights = normalizeWeightsTo4(weights0); } } catch (error) { const detail = error instanceof Error ? error.message : String(error); warn2(opts, `Primitive skinning accessors could not be decoded (${detail}); ignoring skinning attributes.`); } } else if (jAcc0 !== void 0 || wAcc0 !== void 0) { warn2(opts, "Primitive has JOINTS_0/WEIGHTS_0 mismatch; ignoring skinning attributes for this primitive"); } const mode = prim.mode ?? 4; let indices = null; if (prim.indices !== void 0) { validatePrimitiveIndexAccessor(json, prim.indices, context); indices = readIndicesAsUint32(doc, prim.indices); } else { const vcount = positions.length / 3 | 0; const seq = new Uint32Array(vcount); for (let i = 0; i < vcount; i++) seq[i] = i >>> 0; indices = mode === 4 ? null : seq; } if (indices) { for (let index = 0; index < indices.length; index++) { const vertexIndex = indices[index]; if (vertexIndex >= baseVertexCount) throw new Error(`Primitive index ${vertexIndex} at ${index} is out of range for ${baseVertexCount} vertices.`); } } if (mode === 5) { const idx = indices ?? new Uint32Array(0); indices = triangulateStrip(idx); } else if (mode === 6) { const idx = indices ?? new Uint32Array(0); indices = triangulateFan(idx); } else if (mode !== 4) { warn2(opts, `Unsupported primitive mode=${mode} (only triangles/strip/fan supported); skipping primitive`); return null; } const morphTargets = []; if (prim.targets && prim.targets.length > 0) { for (let targetIndex = 0; targetIndex < prim.targets.length; targetIndex++) { const targetAttrs = prim.targets[targetIndex]; const target = {}; const targetPosAcc = targetAttrs["POSITION"]; const targetNormalAcc = targetAttrs["NORMAL"]; if (targetPosAcc !== void 0) { const targetPositions = readPrimitiveFloatAttribute(doc, json, targetPosAcc, "VEC3", 3, baseVertexCount, opts, `Primitive morph target ${targetIndex}`, "POSITION"); if (targetPositions) target.positions = targetPositions; } if (targetNormalAcc !== void 0) { if (nAcc === void 0) warn2(opts, `Primitive morph target ${targetIndex} provides NORMAL deltas while base NORMAL is absent; ignoring NORMAL deltas for flat-normal generation.`); else { const targetNormals = readPrimitiveFloatAttribute(doc, json, targetNormalAcc, "VEC3", 3, baseVertexCount, opts, `Primitive morph target ${targetIndex}`, "NORMAL"); if (targetNormals) target.normals = targetNormals; } } const targetColorAcc = targetAttrs["COLOR_0"]; if (targetColorAcc !== void 0) { if (!colors) warn2(opts, `Primitive morph target ${targetIndex} provides COLOR_0 but the base primitive has no valid COLOR_0; ignoring color deltas.`); else target.colors = readGltfColorAccessor(doc, json, targetColorAcc, baseVertexCount, true, opts, `Primitive morph target ${targetIndex}`) ?? void 0; } if (targetAttrs["TANGENT"] !== void 0) warn2(opts, `Primitive morph target ${targetIndex} provides TANGENT deltas; WasmGPU ignores tangent morph data.`); if (!target.positions && !target.normals && !target.colors) warn2(opts, `Primitive morph target ${targetIndex} has no supported POSITION, NORMAL, or COLOR_0 deltas; preserving target slot with no runtime effect.`); morphTargets.push(target); } } const tangentTexCoords = getPrimitiveTangentTexCoords(json, prim); const tangentSpaceNeeded = tangentTexCoords.length > 0; const missingNormals = nAcc === void 0; if (missingNormals) tangents = null; const generateMissingNormals = missingNormals && (computeMissingNormals || tangentSpaceNeeded); if (generateMissingNormals && indices) { positions = expandIndexedAttribute(positions, 3, indices, "Primitive POSITION"); if (uvs) uvs = expandIndexedAttribute(uvs, 2, indices, "Primitive TEXCOORD_0"); if (uvs1) uvs1 = expandIndexedAttribute(uvs1, 2, indices, "Primitive TEXCOORD_1"); if (colors) colors = expandIndexedAttribute(colors, 4, indices, "Primitive COLOR_0"); if (joints) joints = expandIndexedAttribute(joints, 4, indices, "Primitive JOINTS_0"); if (weights) weights = expandIndexedAttribute(weights, 4, indices, "Primitive WEIGHTS_0"); if (joints1) joints1 = expandIndexedAttribute(joints1, 4, indices, "Primitive JOINTS_1"); if (weights1) weights1 = expandIndexedAttribute(weights1, 4, indices, "Primitive WEIGHTS_1"); for (const target of morphTargets) { if (target.positions) target.positions = expandIndexedAttribute(target.positions, 3, indices, "Primitive morph POSITION"); if (target.colors) target.colors = expandIndexedAttribute(target.colors, 4, indices, "Primitive morph COLOR_0"); } indices = null; } if (generateMissingNormals) normals = computeGeometryVertexNormals(positions, null); if (!tangents && tangentSpaceNeeded) { const tangentTexCoord = tangentTexCoords[0]; if (tangentTexCoords.length > 1) warn2(opts, "Primitive uses tangent-space textures on multiple texture coordinate sets; shader will fall back to derivative tangent space."); else { const tangentUvs = tangentTexCoord === 1 ? uvs1 : uvs; if (normals && tangentUvs) tangents = computeGeometryTangents(positions, normals, tangentUvs, indices); else warn2(opts, `Primitive uses tangent-space material features but is missing NORMAL or TEXCOORD_${tangentTexCoord}; shader will fall back to derivative tangent space.`); } } return new Geometry({ positions, normals: normals ?? void 0, tangents: tangents ?? void 0, colors: colors ?? void 0, uvs: uvs ?? void 0, uvs1: uvs1 ?? void 0, joints: joints ?? void 0, weights: weights ?? void 0, joints1: joints1 ?? void 0, weights1: weights1 ?? void 0, indices: indices ?? void 0, morphTargets, authoredNormals: nAcc !== void 0 || !generateMissingNormals }); }; var instantiateMeshNode = (doc, json, nodeIndex, node, nodeT, materialCache, textureCache, imageSourceCache, geometryCache, variantsController, extensions, tx, opts) => { if (node.mesh === void 0) return { meshes: [], splatFields: [] }; const gltfMesh = json.meshes?.[node.mesh]; if (!gltfMesh) { warn2(opts, `nodes[].mesh=${node.mesh} missing; skipping mesh node`); return { meshes: [], splatFields: [] }; } const meshes = []; const splatFields = []; const computeMissingNormals = opts.computeMissingNormals !== false; for (let primIndex = 0; primIndex < gltfMesh.primitives.length; primIndex++) { const prim = gltfMesh.primitives[primIndex]; const hasOptionalMeshopt = primitiveUsesMeshopt(json, prim) && !isExtensionRequired(json, EXT_MESHOPT_COMPRESSION); if (hasOptionalMeshopt) warn2(opts, `Mesh ${gltfMesh.name ?? node.mesh} primitive ${primIndex}: ignoring optional ${EXT_MESHOPT_COMPRESSION} payload and attempting the uncompressed core accessor representation.`); if (prim.extensions?.[KHR_DRACO_MESH_COMPRESSION]) { const hasCorePosition = prim.attributes?.POSITION !== void 0 && !!json.accessors?.[prim.attributes.POSITION]; if (!hasCorePosition) { warn2(opts, `Mesh ${gltfMesh.name ?? node.mesh} primitive ${primIndex}: ${KHR_DRACO_MESH_COMPRESSION} has no usable uncompressed core POSITION; skipping primitive.`); continue; } warn2(opts, `Mesh ${gltfMesh.name ?? node.mesh} primitive ${primIndex}: ignoring optional ${KHR_DRACO_MESH_COMPRESSION} payload and using the uncompressed core primitive.`); } if (getGaussianSplattingExtension(prim) !== void 0) { const field = createSplatFieldFromPrimitive(doc, json, gltfMesh, node.mesh, prim, primIndex, node, nodeT, extensions, tx, opts); if (field) splatFields.push(field); continue; } const cacheKey = `${node.mesh ?? -1}:${primIndex}`; const hasCachedGeometry = geometryCache.has(cacheKey); let geom = geometryCache.get(cacheKey); let geometryOwnership = null; const meshName = `${gltfMesh.name ?? `mesh_${node.mesh}`}_${primIndex}`; const matJson = prim.material !== void 0 ? json.materials?.[prim.material] : void 0; validateMaterialTextureCoordinates(matJson, prim.attributes, opts, `Mesh '${gltfMesh.name ?? node.mesh}' primitive ${primIndex}`); if (!hasCachedGeometry) { let built; try { built = buildGeometryFromPrimitive(doc, json, prim, computeMissingNormals, opts); } catch (error) { if (!hasOptionalMeshopt) throw error; const detail = error instanceof Error ? error.message : String(error); warn2(opts, `Mesh ${gltfMesh.name ?? node.mesh} primitive ${primIndex}: ${EXT_MESHOPT_COMPRESSION} has no usable uncompressed core representation; skipping primitive (${detail}).`); built = null; } geom = built; if (geom) geometryOwnership = tx.own(geom, `Mesh '${meshName}' geometry reference`, (resource) => resource.release()); geometryCache.set(cacheKey, geom); } if (!geom) continue; if (hasCachedGeometry) geometryOwnership = tx.own(geom.retain(), `Mesh '${meshName}' geometry reference`, (resource) => resource.release()); if (!geometryOwnership) throw new Error(`Mesh '${meshName}': geometry ownership was not registered.`); const materialOwnership = getOrCreateMaterial(doc, json, prim.material, materialCache, textureCache, imageSourceCache, tx, opts); const mat = materialOwnership.value; const mesh = new Mesh(geom, mat); tx.own(mesh, `Mesh '${meshName}'`, (resource) => resource.destroy()); geometryOwnership.transfer(); materialOwnership.transfer(); mesh.name = node.name ?? gltfMesh.name ?? `gltf_mesh_${node.mesh}_${primIndex}`; mesh.transform.setParent(nodeT); const resolvedWeights = resolveMorphWeights(node.weights ?? gltfMesh.weights, geom.morphTargets.length | 0, opts, `Mesh '${mesh.name}' primitive ${primIndex}`); if (geom.morphTargets.length > 0) initializeMeshMorphRuntime(mesh, resolvedWeights); mesh.userData.gltf = { nodeIndex, meshIndex: node.mesh, primitiveIndex: primIndex, resolvedWeights: Array.from(resolvedWeights), extras: { node: node.extras, mesh: gltfMesh.extras, primitive: prim.extras, material: matJson?.extras }, extensions: { node: node.extensions, mesh: gltfMesh.extensions, primitive: prim.extensions, material: matJson?.extensions } }; meshes.push(mesh); const variantMaterials = getPrimitiveVariantMaterials(doc, json, prim, materialCache, textureCache, imageSourceCache, tx, opts, `Mesh '${gltfMesh.name ?? node.mesh}' primitive ${primIndex}`); variantsController.register(mesh, mesh.material, variantMaterials.variants); for (const material of variantMaterials.ownedMaterials) material.dispose(); } return { meshes, splatFields }; }; var instantiateCameraNode = (json, node, nodeT, tx, opts) => { if (node.camera === void 0) return null; const cam = json.cameras?.[node.camera]; if (!cam) { warn2(opts, `nodes[].camera=${node.camera} missing; skipping camera`); return null; } let out; if (cam.type === "perspective") { const p = cam.perspective; if (!p) { warn2(opts, `camera[${node.camera}] missing perspective block; skipping`); return null; } out = new PerspectiveCamera({ fov: p.yfov * 180 / Math.PI, aspect: p.aspectRatio, autoAspect: p.aspectRatio === void 0, near: p.znear, far: p.zfar ?? Infinity }); } else { const o = cam.orthographic; if (!o) { warn2(opts, `camera[${node.camera}] missing orthographic block; skipping`); return null; } out = new OrthographicCamera({ left: -o.xmag, right: o.xmag, top: o.ymag, bottom: -o.ymag, near: o.znear, far: o.zfar }); } tx.own(out, `camera ${node.camera}`, (camera) => camera.destroy()); out.transform.setParent(nodeT); return out; }; var instantiateLightNode = (light, nodeT) => { const color = light.color ?? [1, 1, 1]; const intensity = light.intensity ?? 1; if (light.type === "directional") { const wm = nodeT.worldMatrix; const zx = wm[8] ?? 0; const zy = wm[9] ?? 0; const zz = wm[10] ?? -1; const dx = -zx, dy = -zy, dz = -zz; const inv = 1 / (Math.hypot(dx, dy, dz) || 1); return new DirectionalLight({ direction: [dx * inv, dy * inv, dz * inv], color: [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1], intensity }); } if (light.type === "point") { const pos = nodeT.worldPosition; return new PointLight({ position: [pos[0] ?? 0, pos[1] ?? 0, pos[2] ?? 0], color: [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1], intensity, range: light.range ?? 0 }); } if (light.type === "spot") { const pos = nodeT.worldPosition; const wm = nodeT.worldMatrix; const dx = -(wm[8] ?? 0); const dy = -(wm[9] ?? 0); const dz = -(wm[10] ?? -1); const inv = 1 / (Math.hypot(dx, dy, dz) || 1); return new SpotLight({ position: [pos[0] ?? 0, pos[1] ?? 0, pos[2] ?? 0], direction: [dx * inv, dy * inv, dz * inv], color: [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1], intensity, range: light.range ?? 0, innerCone: light.spot?.innerConeAngle ?? 0, outerCone: light.spot?.outerConeAngle ?? Math.PI / 4 }); } return null; }; var parseSkins = (doc, json, nodes, tx, opts) => { const skins = json.skins ?? []; const out = []; for (let i = 0; i < skins.length; i++) { const s = skins[i]; const joints = []; let missingJoint = false; for (let jointSlot = 0; jointSlot < s.joints.length; jointSlot++) { const j = s.joints[jointSlot]; const t = nodes[j]?.transform; if (!t) { warn2(opts, `skin[${i}] joint slot ${jointSlot} references missing node ${j}; skipping skin runtime to avoid remapped joint indices.`); missingJoint = true; continue; } joints.push(t); } let inverseBind; if (s.inverseBindMatrices !== void 0) inverseBind = readAccessorAsFloat32(doc, s.inverseBindMatrices); let runtimeInverseBind = inverseBind; if (inverseBind && inverseBind.length !== s.joints.length * 16) { warn2(opts, `skin[${i}] inverseBindMatrices length ${inverseBind.length} does not match ${s.joints.length} joints; using identity inverse binds.`); runtimeInverseBind = void 0; } const skel = s.skeleton !== void 0 ? nodes[s.skeleton]?.transform : void 0; const runtime = missingJoint || joints.length === 0 ? null : new Skin(s.name ?? `skin_${i}`, joints, runtimeInverseBind ?? null); if (runtime) tx.own(runtime, `skin ${i}`, (skin) => skin.dispose()); if (!runtime) warn2(opts, `skin[${i}] has no valid runtime; meshes referencing it will render unskinned.`); out.push({ name: s.name, joints, inverseBindMatrices: inverseBind, skeleton: skel, runtime }); } return out; }; var decodeJsonPointer = (pointer) => { if (pointer === "") return []; if (!pointer.startsWith("/")) return null; return pointer.slice(1).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")); }; var parsePointerIndex = (tokens, index) => { const token = tokens[index]; if (token === void 0 || !/^(0|[1-9]\d*)$/.test(token)) return null; const value = Number(token); return Number.isSafeInteger(value) ? value : null; }; var getChannelPointer = (channel) => { const ext = channel.target.extensions?.["KHR_animation_pointer"]; return typeof ext?.pointer === "string" ? ext.pointer : null; }; var getTextureTransformValueSize = (property) => { if (property === "rotation") return 1; if (property === "offset" || property === "scale") return 2; return null; }; var makeTextureTransformPatch = (current, property, value) => { const out = { offset: [current?.offset?.[0] ?? 0, current?.offset?.[1] ?? 0], rotation: current?.rotation ?? 0, scale: [current?.scale?.[0] ?? 1, current?.scale?.[1] ?? 1], texCoord: current?.texCoord }; if (property === "offset") out.offset = [value[0] ?? 0, value[1] ?? 0]; else if (property === "rotation") out.rotation = value[0] ?? 0; else if (property === "scale") out.scale = [value[0] ?? 1, value[1] ?? 1]; return out; }; var patchStandardMaterialExtensions = (material, update) => { const extensions = material.extensions; update(extensions); material.setExtensions(extensions); }; var makeStandardExtensionValueTarget = (material, extensionName, valueSize, update, allowDuplicateTarget = false) => { if (!(material instanceof StandardMaterial)) return null; const extensions = material.extensions; if (!extensions[extensionName]) return null; return { kind: "pointer", canonical: "", valueSize, allowDuplicateTarget, setValue: (value) => { patchStandardMaterialExtensions(material, (next) => { const extension = next[extensionName]; if (extension) update(extension, value); }); } }; }; var makeStandardExtensionTextureTransformTarget = (material, extensionName, transformField, property) => { const valueSize = getTextureTransformValueSize(property); if (valueSize === null) return null; return makeStandardExtensionValueTarget(material, extensionName, valueSize, (extension, value) => { extension[transformField] = makeTextureTransformPatch(extension[transformField], property, value); }, true); }; var resolveMaterialTextureTransformPointer = (material, slot, property) => { const valueSize = getTextureTransformValueSize(property); if (valueSize === null) return null; const setTransform = (getCurrent, setCurrent) => ({ kind: "pointer", canonical: "", valueSize, allowDuplicateTarget: true, setValue: (value) => setCurrent(makeTextureTransformPatch(getCurrent(), property, value)) }); if (slot === "baseColorTexture" && (material instanceof StandardMaterial || material instanceof UnlitMaterial)) return setTransform(() => material.baseColorTextureTransform, (next) => { material.baseColorTextureTransform = next; }); if (!(material instanceof StandardMaterial)) return null; switch (slot) { case "metallicRoughnessTexture": return setTransform(() => material.metallicRoughnessTextureTransform, (next) => { material.metallicRoughnessTextureTransform = next; }); case "normalTexture": return setTransform(() => material.normalTextureTransform, (next) => { material.normalTextureTransform = next; }); case "occlusionTexture": return setTransform(() => material.occlusionTextureTransform, (next) => { material.occlusionTextureTransform = next; }); case "emissiveTexture": return setTransform(() => material.emissiveTextureTransform, (next) => { material.emissiveTextureTransform = next; }); default: return null; } }; var hasTextureTransformExtension = (info) => { return !!info?.extensions?.KHR_texture_transform; }; var resolveMaterialPointer = (ctx, tokens, canonical) => { const materialIndex = parsePointerIndex(tokens, 1); const matJson = materialIndex !== null ? ctx.json.materials?.[materialIndex] : void 0; const material = materialIndex !== null ? ctx.materialCache.get(materialIndex) : void 0; if (materialIndex === null || !matJson || !material) { warn2(ctx.opts, `KHR_animation_pointer: material pointer '${canonical}' does not resolve to an imported runtime material.`); return null; } const withCanonical = (target) => { if (target) target.canonical = canonical; return target; }; const pbr = matJson.pbrMetallicRoughness; if (tokens[2] === "pbrMetallicRoughness") { if (!pbr) return null; if (tokens.length === 4 && tokens[3] === "baseColorFactor" && (material instanceof StandardMaterial || material instanceof UnlitMaterial)) { return withCanonical({ kind: "pointer", canonical, valueSize: 4, setValue: (value) => { material.color = [value[0] ?? 1, value[1] ?? 1, value[2] ?? 1]; material.opacity = value[3] ?? 1; } }); } if (tokens.length === 4 && tokens[3] === "metallicFactor" && material instanceof StandardMaterial) return withCanonical({ kind: "pointer", canonical, valueSize: 1, setValue: (value) => { material.metallic = value[0] ?? 0; } }); if (tokens.length === 4 && tokens[3] === "roughnessFactor" && material instanceof StandardMaterial) return withCanonical({ kind: "pointer", canonical, valueSize: 1, setValue: (value) => { material.roughness = value[0] ?? 1; } }); if (tokens.length === 7 && tokens[4] === "extensions" && tokens[5] === "KHR_texture_transform" && hasTextureTransformExtension(pbr[tokens[3]])) return withCanonical(resolveMaterialTextureTransformPointer(material, tokens[3], tokens[6])); return null; } if (tokens.length === 3 && tokens[2] === "alphaCutoff" && (material instanceof StandardMaterial || material instanceof UnlitMaterial)) return withCanonical({ kind: "pointer", canonical, valueSize: 1, setValue: (value) => { material.alphaCutoff = value[0] ?? 0; } }); if (tokens.length === 3 && tokens[2] === "emissiveFactor" && material instanceof StandardMaterial) return withCanonical({ kind: "pointer", canonical, valueSize: 3, setValue: (value) => { material.emissive = [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0]; } }); if (tokens.length === 4 && tokens[2] === "normalTexture" && tokens[3] === "scale" && matJson.normalTexture && material instanceof StandardMaterial) return withCanonical({ kind: "pointer", canonical, valueSize: 1, setValue: (value) => { material.normalScale = value[0] ?? 1; } }); if (tokens.length === 4 && tokens[2] === "occlusionTexture" && tokens[3] === "strength" && matJson.occlusionTexture && material instanceof StandardMaterial) return withCanonical({ kind: "pointer", canonical, valueSize: 1, setValue: (value) => { material.occlusionStrength = value[0] ?? 1; } }); if (tokens.length === 6 && tokens[3] === "extensions" && tokens[4] === "KHR_texture_transform" && hasTextureTransformExtension(matJson[tokens[2]])) return withCanonical(resolveMaterialTextureTransformPointer(material, tokens[2], tokens[5])); if (tokens[2] !== "extensions" || tokens.length < 5) return null; const extensions = matJson.extensions; const extName = tokens[3]; const extJson = extensions?.[extName]; if (!extJson || !(material instanceof StandardMaterial)) return null; const prop = tokens[4]; const standardExt = material.extensions; if (tokens.length === 5) { switch (extName) { case "KHR_materials_anisotropy": if (prop === "anisotropyStrength") return withCanonical(makeStandardExtensionValueTarget(material, "anisotropy", 1, (ext, value) => { ext.strength = value[0] ?? 0; })); if (prop === "anisotropyRotation") return withCanonical(makeStandardExtensionValueTarget(material, "anisotropy", 1, (ext, value) => { ext.rotation = value[0] ?? 0; })); break; case "KHR_materials_clearcoat": if (prop === "clearcoatFactor") return withCanonical(makeStandardExtensionValueTarget(material, "clearcoat", 1, (ext, value) => { ext.factor = value[0] ?? 0; })); if (prop === "clearcoatRoughnessFactor") return withCanonical(makeStandardExtensionValueTarget(material, "clearcoat", 1, (ext, value) => { ext.roughness = value[0] ?? 0; })); break; case "KHR_materials_dispersion": if (prop === "dispersion") return withCanonical(makeStandardExtensionValueTarget(material, "dispersion", 1, (ext, value) => { ext.dispersion = value[0] ?? 0; })); break; case "KHR_materials_emissive_strength": if (prop === "emissiveStrength") return withCanonical(makeStandardExtensionValueTarget(material, "emissiveStrength", 1, (ext, value) => { ext.strength = value[0] ?? 1; })); break; case "KHR_materials_ior": if (prop === "ior") return withCanonical(makeStandardExtensionValueTarget(material, "ior", 1, (ext, value) => { ext.ior = value[0] ?? 1.5; })); break; case "KHR_materials_iridescence": if (prop === "iridescenceFactor") return withCanonical(makeStandardExtensionValueTarget(material, "iridescence", 1, (ext, value) => { ext.factor = value[0] ?? 0; })); if (prop === "iridescenceIor") return withCanonical(makeStandardExtensionValueTarget(material, "iridescence", 1, (ext, value) => { ext.ior = value[0] ?? 1.3; })); if (prop === "iridescenceThicknessMinimum") return withCanonical(makeStandardExtensionValueTarget(material, "iridescence", 1, (ext, value) => { ext.thicknessMinimum = value[0] ?? 100; })); if (prop === "iridescenceThicknessMaximum") return withCanonical(makeStandardExtensionValueTarget(material, "iridescence", 1, (ext, value) => { ext.thicknessMaximum = value[0] ?? 400; })); break; case "KHR_materials_sheen": if (prop === "sheenColorFactor") return withCanonical(makeStandardExtensionValueTarget(material, "sheen", 3, (ext, value) => { ext.color = [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0]; })); if (prop === "sheenRoughnessFactor") return withCanonical(makeStandardExtensionValueTarget(material, "sheen", 1, (ext, value) => { ext.roughness = value[0] ?? 0; })); break; case "KHR_materials_specular": if (prop === "specularFactor") return withCanonical(makeStandardExtensionValueTarget(material, "specular", 1, (ext, value) => { ext.factor = value[0] ?? 1; })); if (prop === "specularColorFactor") return withCanonical(makeStandardExtensionValueTarget(material, "specular", 3, (ext, value) => { ext.color = [value[0] ?? 1, value[1] ?? 1, value[2] ?? 1]; })); break; case "KHR_materials_transmission": if (prop === "transmissionFactor") return withCanonical(makeStandardExtensionValueTarget(material, "transmission", 1, (ext, value) => { ext.factor = value[0] ?? 0; })); break; case "KHR_materials_volume": if (prop === "thicknessFactor") return withCanonical(makeStandardExtensionValueTarget(material, "volume", 1, (ext, value) => { ext.thicknessFactor = value[0] ?? 0; })); if (prop === "attenuationDistance") return withCanonical(makeStandardExtensionValueTarget(material, "volume", 1, (ext, value) => { ext.attenuationDistance = value[0] ?? Infinity; })); if (prop === "attenuationColor") return withCanonical(makeStandardExtensionValueTarget(material, "volume", 3, (ext, value) => { ext.attenuationColor = [value[0] ?? 1, value[1] ?? 1, value[2] ?? 1]; })); break; case "KHR_materials_diffuse_transmission": if (prop === "diffuseTransmissionFactor") return withCanonical(makeStandardExtensionValueTarget(material, "diffuseTransmission", 1, (ext, value) => { ext.factor = value[0] ?? 0; })); if (prop === "diffuseTransmissionColorFactor") return withCanonical(makeStandardExtensionValueTarget(material, "diffuseTransmission", 3, (ext, value) => { ext.color = [value[0] ?? 1, value[1] ?? 1, value[2] ?? 1]; })); break; } } if (tokens.length === 6 && tokens[5] === "scale" && extName === "KHR_materials_clearcoat" && prop === "clearcoatNormalTexture" && extJson.clearcoatNormalTexture && standardExt.clearcoat) return withCanonical(makeStandardExtensionValueTarget(material, "clearcoat", 1, (ext, value) => { ext.normalScale = value[0] ?? 1; })); if (tokens.length === 8 && tokens[5] === "extensions" && tokens[6] === "KHR_texture_transform" && hasTextureTransformExtension(extJson[prop])) { const transformFields = { KHR_materials_anisotropy: { anisotropyTexture: "textureTransform" }, KHR_materials_clearcoat: { clearcoatTexture: "textureTransform", clearcoatRoughnessTexture: "roughnessTextureTransform", clearcoatNormalTexture: "normalTextureTransform" }, KHR_materials_iridescence: { iridescenceTexture: "textureTransform", iridescenceThicknessTexture: "thicknessTextureTransform" }, KHR_materials_sheen: { sheenColorTexture: "colorTextureTransform", sheenRoughnessTexture: "roughnessTextureTransform" }, KHR_materials_specular: { specularTexture: "textureTransform", specularColorTexture: "colorTextureTransform" }, KHR_materials_transmission: { transmissionTexture: "textureTransform" }, KHR_materials_volume: { thicknessTexture: "thicknessTextureTransform" }, KHR_materials_diffuse_transmission: { diffuseTransmissionTexture: "textureTransform", diffuseTransmissionColorTexture: "colorTextureTransform" } }; const extensionFields = { KHR_materials_anisotropy: "anisotropy", KHR_materials_clearcoat: "clearcoat", KHR_materials_iridescence: "iridescence", KHR_materials_sheen: "sheen", KHR_materials_specular: "specular", KHR_materials_transmission: "transmission", KHR_materials_volume: "volume", KHR_materials_diffuse_transmission: "diffuseTransmission" }; const transformField = transformFields[extName]?.[prop]; const extensionField = extensionFields[extName]; if (transformField && extensionField) return withCanonical(makeStandardExtensionTextureTransformTarget(material, extensionField, transformField, tokens[7])); } return null; }; var resolveNodePointer = (ctx, tokens, canonical) => { const nodeIndex = parsePointerIndex(tokens, 1); const importedNode = nodeIndex !== null ? ctx.nodes[nodeIndex] : void 0; const nodeJson = nodeIndex !== null ? ctx.json.nodes?.[nodeIndex] : void 0; if (nodeIndex === null || !importedNode || !nodeJson) { warn2(ctx.opts, `KHR_animation_pointer: node pointer '${canonical}' does not resolve to an imported node.`); return null; } if (tokens.length === 3) { const path = tokens[2]; if (path === "translation") return { kind: "trs", canonical, targetIndex: importedNode.transform.index >>> 0, pathCode: 0 }; if (path === "rotation") { if (nodeJson.matrix) return null; return { kind: "trs", canonical, targetIndex: importedNode.transform.index >>> 0, pathCode: 1 }; } if (path === "scale") { if (nodeJson.matrix) return null; return { kind: "trs", canonical, targetIndex: importedNode.transform.index >>> 0, pathCode: 2 }; } if (path === "weights") { const morphMeshes = importedNode.meshes.filter((mesh) => mesh.geometry.morphTargets.length > 0); return morphMeshes.length > 0 ? { kind: "weights", canonical, meshes: morphMeshes } : null; } } if (tokens.length === 4 && tokens[2] === "weights") { const weightIndex = parsePointerIndex(tokens, 3); if (weightIndex === null) return null; const morphMeshes = importedNode.meshes.filter((mesh) => mesh.geometry.morphTargets.length > weightIndex); if (morphMeshes.length === 0) return null; return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { const weight = value[0] ?? 0; for (const mesh of morphMeshes) setMeshMorphWeight(mesh, weightIndex, weight); } }; } if (tokens.length === 5 && tokens[2] === "extensions" && tokens[3] === "KHR_node_visibility" && tokens[4] === "visible") { if (!nodeJson.extensions?.["KHR_node_visibility"]) return null; return { kind: "pointer", canonical, valueSize: 1, requiresStep: true, setValue: (value) => { importedNode.visible = (value[0] ?? 0) !== 0; } }; } return null; }; var resolveCameraPointer = (ctx, tokens, canonical) => { const cameraIndex = parsePointerIndex(tokens, 1); const cameraJson = cameraIndex !== null ? ctx.json.cameras?.[cameraIndex] : void 0; const cameras = cameraIndex !== null ? ctx.cameraRuntimeMap.get(cameraIndex) ?? [] : []; if (cameraIndex === null || !cameraJson || cameras.length === 0) return null; if (tokens.length !== 4) return null; const family = tokens[2]; const prop = tokens[3]; if (family === "perspective" && cameraJson.type === "perspective") { const perspectiveCameras = cameras.filter((camera) => camera instanceof PerspectiveCamera); if (perspectiveCameras.length === 0) return null; if (prop === "aspectRatio" && cameraJson.perspective?.aspectRatio === void 0) return null; if (prop === "zfar" && cameraJson.perspective?.zfar === void 0) return null; const setters = { aspectRatio: (camera, value) => { camera.aspect = value; }, yfov: (camera, value) => { camera.fov = value * 180 / Math.PI; }, znear: (camera, value) => { camera.near = value; }, zfar: (camera, value) => { camera.far = value; } }; const setter = setters[prop]; if (!setter) return null; return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const camera of perspectiveCameras) setter(camera, value[0] ?? 0); } }; } if (family === "orthographic" && cameraJson.type === "orthographic") { const orthographicCameras = cameras.filter((camera) => camera instanceof OrthographicCamera); if (orthographicCameras.length === 0) return null; const setters = { xmag: (camera, value) => { camera.left = -value; camera.right = value; }, ymag: (camera, value) => { camera.top = value; camera.bottom = -value; }, znear: (camera, value) => { camera.near = value; }, zfar: (camera, value) => { camera.far = value; } }; const setter = setters[prop]; if (!setter) return null; return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const camera of orthographicCameras) setter(camera, value[0] ?? 0); } }; } return null; }; var resolveLightPointer = (ctx, tokens, canonical) => { if (tokens.length < 5 || tokens[0] !== "extensions" || tokens[1] !== "KHR_lights_punctual" || tokens[2] !== "lights") return null; const lightIndex = parsePointerIndex(tokens, 3); const root = getKHRLightsFromRoot(ctx.json); const lightJson = lightIndex !== null ? root?.lights?.[lightIndex] : void 0; const lights = lightIndex !== null ? ctx.lightRuntimeMap.get(lightIndex) ?? [] : []; if (lightIndex === null || !lightJson || lights.length === 0) return null; if (tokens.length === 5) { const prop = tokens[4]; if (prop === "color") return { kind: "pointer", canonical, valueSize: 3, setValue: (value) => { for (const light of lights) light.color = [value[0] ?? 1, value[1] ?? 1, value[2] ?? 1]; } }; if (prop === "intensity") return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const light of lights) light.intensity = value[0] ?? 1; } }; if (prop === "range") { const rangedLights = lights.filter((light) => light instanceof PointLight || light instanceof SpotLight); if (rangedLights.length === 0) return null; return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const light of rangedLights) light.range = value[0] ?? 0; } }; } } if (tokens.length === 6 && tokens[4] === "spot" && lightJson.type === "spot") { const spotLights = lights.filter((light) => light instanceof SpotLight); if (tokens[5] === "innerConeAngle") return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const light of spotLights) light.innerCone = value[0] ?? 0; } }; if (tokens[5] === "outerConeAngle") return { kind: "pointer", canonical, valueSize: 1, setValue: (value) => { for (const light of spotLights) light.outerCone = value[0] ?? Math.PI / 4; } }; } return null; }; var canonicalizePointerTokens = (tokens) => { return `/${tokens.map((token) => token.replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`; }; var resolveAnimationPointer = (ctx, pointer) => { const tokens = decodeJsonPointer(pointer); if (!tokens) { warn2(ctx.opts, `KHR_animation_pointer: invalid JSON pointer '${pointer}'.`); return null; } const canonical = canonicalizePointerTokens(tokens); if (tokens[0] === "nodes") return resolveNodePointer(ctx, tokens, canonical); if (tokens[0] === "materials") return resolveMaterialPointer(ctx, tokens, canonical); if (tokens[0] === "cameras") return resolveCameraPointer(ctx, tokens, canonical); if (tokens[0] === "extensions") return resolveLightPointer(ctx, tokens, canonical); return null; }; var trackAnimationTarget = (seen, canonical, animationName, opts, allowDuplicateTarget) => { const weightElementMatch = canonical.match(/^\/nodes\/(\d+)\/weights\/(\d+)$/); const weightBase = weightElementMatch ? `/nodes/${weightElementMatch[1]}/weights` : canonical.match(/^\/nodes\/(\d+)\/weights$/)?.[0] ?? null; for (const target of seen) { if (target === canonical) { if (!allowDuplicateTarget) throw new Error(`glTF animation '${animationName}' targets '${canonical}' more than once.`); warn2(opts, `KHR_animation_pointer: animation '${animationName}' targets '${canonical}' more than once; applying duplicate pointer channels in file order.`); continue; } if (weightBase && (canonical === weightBase ? target.startsWith(`${weightBase}/`) : target === weightBase)) throw new Error(`glTF animation '${animationName}' targets overlapping morph weight paths '${target}' and '${canonical}'.`); } seen.add(canonical); }; var parseAnimations = (doc, json, nodes, materialCache, cameraRuntimeMap, lightRuntimeMap, extensions, tx, opts) => { const anims = json.animations ?? []; const out = []; const interpToCode = (interp) => { switch (interp) { case "STEP": return 0; case "CUBICSPLINE": return 2; case "LINEAR": default: return 1; } }; const pathToCode = (path) => { switch (path) { case "translation": return 0; case "rotation": return 1; case "scale": return 2; default: return -1; } }; try { for (let i = 0; i < anims.length; i++) { const a = anims[i]; const samplers = []; const valueSamplers = []; const pointerChannels = []; const channels = []; const seenTargets = /* @__PURE__ */ new Set(); const animationName = a.name ?? `anim_${i}`; const samplerCount = a.samplers.length | 0; const ownedF32Allocs = []; const ownedU32Allocs = []; const allocOwnedF32 = (len, label) => { const length = len >>> 0; const ptr = wasm.allocF32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly f32 allocation failed (${length} elements).`); if (ptr) ownedF32Allocs.push({ ptr, len: length }); return ptr; }; const allocOwnedU32 = (len, label) => { const length = len >>> 0; const ptr = wasm.allocU32(length); if (!ptr && length !== 0) throw new Error(`${label}: WebAssembly u32 allocation failed (${length} elements).`); if (ptr) ownedU32Allocs.push({ ptr, len: length }); return ptr; }; let samplerTablePtr = 0; let allocationsTransferred = false; try { if (samplerCount > 0) samplerTablePtr = allocOwnedU32(samplerCount * 5, `animation '${animationName}' samplers`); let startTime = Number.POSITIVE_INFINITY; let endTime = Number.NEGATIVE_INFINITY; for (let si = 0; si < a.samplers.length; si++) { const s = a.samplers[si]; const input = readAccessorAsFloat32(doc, s.input); const outView = readAccessor(doc, s.output); const output = readAccessorAsFloat32(doc, s.output); samplers.push({ interpolation: s.interpolation ?? "LINEAR", input, output }); const interpolation = s.interpolation ?? "LINEAR"; const denom = interpolation === "CUBICSPLINE" ? Math.max(1, (input.length | 0) * 3) : Math.max(1, input.length | 0); const valueSize = Math.max(0, Math.floor(output.length / denom)); valueSamplers.push({ interpolation, input, output, valueSize }); if (input.length > 0) { startTime = Math.min(startTime, input[0]); endTime = Math.max(endTime, input[input.length - 1]); } if (samplerCount > 0) { const timesPtr = allocOwnedF32(input.length, `animation '${animationName}' sampler ${si} times`); wasm.f32view(timesPtr, input.length).set(input); const valuesPtr = allocOwnedF32(output.length, `animation '${animationName}' sampler ${si} values`); wasm.f32view(valuesPtr, output.length).set(output); const samplerTable = wasm.u32view(samplerTablePtr, samplerCount * 5); const base = si * 5; samplerTable[base + 0] = timesPtr >>> 0; samplerTable[base + 1] = (input.length | 0) >>> 0; samplerTable[base + 2] = valuesPtr >>> 0; samplerTable[base + 3] = (outView.numComponents | 0) >>> 0; samplerTable[base + 4] = interpToCode(s.interpolation ?? "LINEAR") >>> 0; } } const runtimeChannels = []; const runtimeWeightChannels = []; const pointerContext = { json, nodes, materialCache, cameraRuntimeMap, lightRuntimeMap, opts }; for (let ci = 0; ci < a.channels.length; ci++) { const c = a.channels[ci]; const nodeIndex = c.target.node; const importedNode = nodeIndex !== void 0 ? nodes[nodeIndex] ?? null : null; const t = importedNode?.transform ?? null; const chan = { sampler: c.sampler | 0, targetNode: t, path: c.target.path }; if (c.target.path === "pointer") chan.targetPointer = getChannelPointer(c) ?? void 0; channels.push(chan); if (c.target.path === "pointer") { if (nodeIndex !== void 0) { reportAnimationPointerLoss(json, extensions, opts, `animation '${animationName}' channel ${ci} sets target.node; skipping pointer channel.`); continue; } const pointer = getChannelPointer(c); if (!pointer) { reportAnimationPointerLoss(json, extensions, opts, `animation '${animationName}' channel ${ci} is missing extensions.KHR_animation_pointer.pointer.`); continue; } const resolved = resolveAnimationPointer(pointerContext, pointer); if (!resolved) { reportAnimationPointerLoss(json, extensions, opts, `animation '${animationName}' channel ${ci} pointer '${pointer}' is not supported by this importer.`); continue; } trackAnimationTarget(seenTargets, resolved.canonical, animationName, opts, resolved.kind === "pointer" && resolved.allowDuplicateTarget === true); if (resolved.kind === "trs") { runtimeChannels.push({ sampler: chan.sampler | 0, targetIndex: resolved.targetIndex, pathCode: resolved.pathCode }); continue; } if (resolved.kind === "weights") { runtimeWeightChannels.push({ sampler: chan.sampler | 0, meshes: resolved.meshes }); continue; } const sampler = valueSamplers[chan.sampler]; if (!sampler) { reportAnimationPointerLoss(json, extensions, opts, `animation '${animationName}' channel ${ci} references missing sampler ${chan.sampler}.`); continue; } if (resolved.requiresStep && sampler.interpolation !== "STEP") { reportAnimationPointerLoss(json, extensions, opts, `boolean pointer '${resolved.canonical}' requires STEP interpolation; skipping channel.`); continue; } if ((sampler.valueSize | 0) !== (resolved.valueSize | 0)) { reportAnimationPointerLoss(json, extensions, opts, `pointer '${resolved.canonical}' expects ${resolved.valueSize} output component(s), got ${sampler.valueSize}; skipping channel.`); continue; } pointerChannels.push({ sampler: chan.sampler | 0, scratch: new Float32Array(resolved.valueSize), setValue: resolved.setValue }); continue; } const pathCode = pathToCode(chan.path); if (t && pathCode >= 0) { if (nodeIndex !== void 0) trackAnimationTarget(seenTargets, `/nodes/${nodeIndex}/${chan.path}`, animationName, opts, false); runtimeChannels.push({ sampler: chan.sampler | 0, targetIndex: t.index >>> 0, pathCode }); } else if (chan.path === "weights" && nodeIndex !== void 0) { trackAnimationTarget(seenTargets, `/nodes/${nodeIndex}/weights`, animationName, opts, false); const meshes = (nodes[nodeIndex]?.meshes ?? []).filter((mesh) => mesh.geometry.morphTargets.length > 0); if (meshes.length > 0) runtimeWeightChannels.push({ sampler: chan.sampler | 0, meshes }); } } let clip = null; const channelCount = runtimeChannels.length | 0; const weightChannelCount = runtimeWeightChannels.length | 0; const pointerChannelCount = pointerChannels.length | 0; if (samplerCount > 0 && (channelCount > 0 || weightChannelCount > 0 || pointerChannelCount > 0)) { let channelsPtr = 0; if (channelCount > 0) { channelsPtr = allocOwnedU32(channelCount * 3, `animation '${animationName}' channels`); const ch = wasm.u32view(channelsPtr, channelCount * 3); for (let ci = 0; ci < channelCount; ci++) { const rc = runtimeChannels[ci]; const base = ci * 3; ch[base + 0] = rc.sampler >>> 0; ch[base + 1] = rc.targetIndex >>> 0; ch[base + 2] = rc.pathCode >>> 0; } } if (!Number.isFinite(startTime)) startTime = 0; if (!Number.isFinite(endTime)) endTime = 0; clip = new AnimationClip({ name: a.name ?? `anim_${i}`, samplerCount, channelCount, samplersPtr: samplerTablePtr, channelsPtr, startTime, endTime, ownedF32Allocs, ownedU32Allocs, weightSamplers: valueSamplers, weightChannels: runtimeWeightChannels.map((channel) => ({ sampler: channel.sampler, meshes: channel.meshes, scratch: new Float32Array(valueSamplers[channel.sampler]?.valueSize ?? 0) })), pointerSamplers: valueSamplers, pointerChannels }); tx.own(clip, `animation clip '${clip.name}'`, (resource) => resource.dispose()); allocationsTransferred = true; } out.push({ name: a.name, samplers, channels, clip }); } finally { if (!allocationsTransferred) { for (let ai = ownedF32Allocs.length - 1; ai >= 0; ai--) wasm.freeF32(ownedF32Allocs[ai].ptr, ownedF32Allocs[ai].len); for (let ai = ownedU32Allocs.length - 1; ai >= 0; ai--) wasm.freeU32(ownedU32Allocs[ai].ptr, ownedU32Allocs[ai].len); } } } } catch (error) { throw error; } return out; }; var importGltf = (doc, opts = {}) => { const json = doc.json; validateGltfCompatibility(json); const { metadata: extensions, assessments } = buildExtensionsMetadata(json, opts); enforceRequiredExtensions(json, extensions, assessments, opts); const tx = new ImportTransaction(); try { const scene = opts.targetScene ?? new Scene(); const addToScene = opts.addToScene !== false; const sceneIndex = getSceneIndex(json, opts); const gltfNodes = json.nodes ?? []; const nodes = new Array(gltfNodes.length); for (let i = 0; i < gltfNodes.length; i++) { const n = gltfNodes[i]; const t = new Transform(); tx.own(t, `node ${i} transform`, (transform) => transform.dispose()); if (n.matrix && n.matrix.length >= 16) applyNodeMatrixViaWasmDecompose(t, n.matrix); else { const tr = n.translation ?? [0, 0, 0]; const ro = n.rotation ?? [0, 0, 0, 1]; const sc = n.scale ?? [1, 1, 1]; t.setPosition(tr[0], tr[1], tr[2]); t.setRotation(ro[0], ro[1], ro[2], ro[3]); t.setScale(sc[0], sc[1], sc[2]); } nodes[i] = new GltfImportedNode(i, t, n); } for (let i = 0; i < gltfNodes.length; i++) { const n = gltfNodes[i]; const parentNode = nodes[i]; for (const child of n.children ?? []) { const childNode = nodes[child]; if (childNode) { childNode.transform.setParent(parentNode.transform); childNode.parentIndex = i; childNode.setParentNode(parentNode); } else warn2(opts, `Node ${i} child ${child} missing transform`); } } const xmp = buildXmpMetadata(json); const variantsController = createVariantsController(getDeclaredVariants(json, xmp.packets)); tx.own(variantsController, "material variants controller", (controller) => controller.destroy()); const skins = parseSkins(doc, json, nodes, tx, opts); const materialCache = /* @__PURE__ */ new Map(); const textureCache = /* @__PURE__ */ new Map(); const imageSourceCache = /* @__PURE__ */ new Map(); const geometryCache = /* @__PURE__ */ new Map(); const meshes = []; const splatFields = []; const cameras = []; const lights = []; const cameraRuntimeMap = /* @__PURE__ */ new Map(); const lightRuntimeMap = /* @__PURE__ */ new Map(); const khrLights = getKHRLightsFromRoot(json); const instantiateNodeRecursive = (nodeIndex) => { const node = gltfNodes[nodeIndex]; if (!node) return; const importedNode = nodes[nodeIndex]; const nodeT = importedNode?.transform; if (!importedNode || !nodeT) return; if (node.extensions?.[EXT_MESH_GPU_INSTANCING] !== void 0 && !isExtensionRequired(json, EXT_MESH_GPU_INSTANCING)) warn2(opts, `Node ${node.name ?? nodeIndex}: ignoring optional ${EXT_MESH_GPU_INSTANCING}; using the single core node instance because instancing is deferred.`); const createdObjects = instantiateMeshNode(doc, json, nodeIndex, node, nodeT, materialCache, textureCache, imageSourceCache, geometryCache, variantsController, extensions, tx, opts); const createdMeshes = createdObjects.meshes; const createdSplatFields = createdObjects.splatFields; importedNode.meshes = createdMeshes; importedNode.splatFields = createdSplatFields; importedNode.applyVisibility(); const skinIndex = node.skin !== void 0 ? node.skin | 0 : void 0; if (skinIndex !== void 0) { const skinDef = skins[skinIndex]; if (!skinDef || !skinDef.runtime) warn2(opts, `nodes[${nodeIndex}].skin=${skinIndex} missing or invalid; skipping skin binding`); else { for (const m of createdMeshes) { if (m.geometry.joints === null || m.geometry.weights === null) { warn2(opts, `Mesh '${m.name}' is skinned (node.skin) but is missing JOINTS_0/WEIGHTS_0; it will render unskinned.`); continue; } m.skin = skinDef.runtime.createInstance(m.transform); } } } for (const m of createdMeshes) { meshes.push(m); if (addToScene) { scene.add(m); tx.defer(`scene mesh '${m.name}' membership`, () => scene.remove(m)); } } for (const s of createdSplatFields) { splatFields.push(s); if (addToScene) { scene.add(s); tx.defer(`scene splat field '${s.name}' membership`, () => scene.remove(s)); } } if (opts.importCameras) { const cam = instantiateCameraNode(json, node, nodeT, tx, opts); if (cam) { cameras.push(cam); importedNode.camera = cam; if (node.camera !== void 0) { const cameraIndex = node.camera | 0; const list = cameraRuntimeMap.get(cameraIndex) ?? []; list.push(cam); cameraRuntimeMap.set(cameraIndex, list); } } } if (opts.importLights && khrLights) { const nodeLight = getNodeKHRLight(node); if (nodeLight) { const lightDef = khrLights.lights[nodeLight.light]; if (!lightDef) warn2(opts, `KHR_lights_punctual node references missing light ${nodeLight.light}`); else { const created = instantiateLightNode(lightDef, nodeT); if (created) { bindLightToTransform(created, nodeT); tx.defer(`light ${nodeLight.light} transform binding`, () => unbindLightTransform(created)); lights.push(created); importedNode.light = created; const lightIndex = nodeLight.light | 0; const list = lightRuntimeMap.get(lightIndex) ?? []; list.push(created); lightRuntimeMap.set(lightIndex, list); importedNode.applyVisibility(); if (addToScene) { scene.addLight(created); tx.defer(`scene light ${nodeLight.light} membership`, () => scene.removeLight(created)); } } else warn2(opts, `Light '${node.name ?? `index ${nodeIndex}`}' has unsupported type '${lightDef.type}' and was skipped.`); } } } for (const child of node.children ?? []) instantiateNodeRecursive(child); }; const gltfScene = json.scenes?.[sceneIndex]; const roots = gltfScene?.nodes ?? []; for (const root of roots) instantiateNodeRecursive(root); const animations = parseAnimations(doc, json, nodes, materialCache, cameraRuntimeMap, lightRuntimeMap, extensions, tx, opts); const clips = animations.map((a) => a.clip).filter((c) => c !== null); const metadata = buildImportMetadata(json, sceneIndex, extensions, xmp, variantsController.public); const destroy = tx.commit(); return { scene, meshes, splatFields, nodes, lights, cameras, skins, animations, clips, metadata, destroy }; } catch (error) { return tx.rollback(error); } }; // typescript/overlay/projection.ts var EPSILON = 1e-8; var projectWorldToScreen = (camera, width, height, point) => { const w = Math.max(1, width); const h = Math.max(1, height); const m = camera.viewProjectionMatrix; const x = point[0] ?? 0; const y = point[1] ?? 0; const z = point[2] ?? 0; const clipX = m[0] * x + m[4] * y + m[8] * z + m[12]; const clipY = m[1] * x + m[5] * y + m[9] * z + m[13]; const clipZ = m[2] * x + m[6] * y + m[10] * z + m[14]; const clipW = m[3] * x + m[7] * y + m[11] * z + m[15]; if (!Number.isFinite(clipW) || Math.abs(clipW) <= EPSILON) return null; const invW = 1 / clipW; const ndcX = clipX * invW; const ndcY = clipY * invW; const ndcZ = clipZ * invW; const sx = (ndcX * 0.5 + 0.5) * w; const sy = (1 - (ndcY * 0.5 + 0.5)) * h; const inFront = clipW > 0; const insideClip = ndcX >= -1 && ndcX <= 1 && ndcY >= -1 && ndcY <= 1 && ndcZ >= 0 && ndcZ <= 1; return { x: sx, y: sy, ndcX, ndcY, ndcZ, clipW, inFront, insideClip, visible: inFront && insideClip }; }; var writeCameraSignature = (camera, out) => { if (out.length < 17) throw new Error("writeCameraSignature: expected Float64Array length >= 17."); out[0] = camera.type === "perspective" ? 1 : 2; const vp = camera.viewProjectionMatrix; for (let i = 0; i < 16; i++) out[i + 1] = vp[i] ?? 0; }; var cameraSignatureEquals = (a, b, epsilon = 1e-6) => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (Math.abs(a[i] - b[i]) > epsilon) return false; return true; }; var resolveScreenAnchorPoint = (anchor, width, height) => { const w = Math.max(1, width); const h = Math.max(1, height); const a = anchor ?? { kind: "screen", corner: "bottom-left", offsetPx: [16, -16] }; const ox = a.offsetPx?.[0] ?? 0; const oy = a.offsetPx?.[1] ?? 0; if (typeof a.x === "number" || typeof a.y === "number") { const x = (a.x ?? 0) + ox; const y = (a.y ?? 0) + oy; return [x, y]; } const corner = a.corner ?? "bottom-left"; if (corner === "top-left") return [0 + ox, 0 + oy]; if (corner === "top-right") return [w + ox, 0 + oy]; if (corner === "bottom-right") return [w + ox, h + oy]; return [0 + ox, h + oy]; }; // typescript/overlay/system.ts var addReasons = (set, reasons) => { if (!reasons) return; if (Array.isArray(reasons)) { for (let i = 0; i < reasons.length; i++) set.add(reasons[i]); return; } set.add(reasons); }; var OverlaySystem = class { canvas; parent; root; interactionThrottleMs; autoUpdate; layers = /* @__PURE__ */ new Map(); dirtyReasons = /* @__PURE__ */ new Set(["layout"]); resizeObserver = null; winResizeListener = null; winScrollListener = null; rafId = null; currentCamera = null; currentScene = null; dpr = 1; width = 1; height = 1; lastLeft = Number.NaN; lastTop = Number.NaN; lastUpdateMs = Number.NaN; interactionActive = false; pendingInteractionFlush = false; controlsUnsubChange = null; controlsUnsubInteraction = null; cameraSigA = new Float64Array(17); cameraSigB = new Float64Array(17); hasCameraSig = false; constructor(desc) { if (typeof document === "undefined") throw new Error("OverlaySystem requires a DOM environment."); this.canvas = desc.canvas; this.parent = desc.parent ?? this.canvas.parentElement ?? document.body; this.interactionThrottleMs = Math.max(0, Math.round(desc.interactionThrottleMs ?? 24)); this.autoUpdate = desc.autoUpdate ?? true; if (this.parent !== document.body && this.parent !== document.documentElement) { const cs = getComputedStyle(this.parent); if (cs.position === "static") this.parent.style.position = "relative"; } const root = document.createElement("div"); root.className = desc.className ?? "wasmgpu-overlay-root"; root.style.position = this.parent === document.body || this.parent === document.documentElement ? "fixed" : "absolute"; root.style.pointerEvents = "none"; root.style.overflow = "hidden"; root.style.contain = "layout style paint"; root.style.left = "0"; root.style.top = "0"; root.style.width = "1px"; root.style.height = "1px"; root.style.zIndex = String(desc.zIndex ?? 20); this.parent.appendChild(root); this.root = root; this.currentCamera = desc.camera ?? null; this.currentScene = desc.scene ?? null; this.bindControls(desc.controls ?? null); this.setupResizeEvents(); this.syncRootBounds(); this.invalidate("layout"); } get layerCount() { return this.layers.size; } get isInteractionActive() { return this.interactionActive; } getLayer(id) { return this.layers.get(id)?.layer ?? null; } isLayerEnabled(id) { return this.layers.get(id)?.enabled ?? false; } setLayerEnabled(id, enabled) { const registration = this.layers.get(id); if (!registration || registration.enabled === enabled) return this; registration.enabled = enabled; registration.wrapper.style.display = enabled ? "" : "none"; this.invalidate("manual"); return this; } setView(camera, scene = null) { this.currentCamera = camera; this.currentScene = scene ?? null; this.invalidate("camera"); return this; } bindControls(controls) { this.controlsUnsubChange?.(); this.controlsUnsubInteraction?.(); this.controlsUnsubChange = null; this.controlsUnsubInteraction = null; if (!controls) return this; const anyControls = controls; if (typeof anyControls.onChange === "function") this.controlsUnsubChange = anyControls.onChange(() => this.invalidate("camera")); if (typeof anyControls.onInteractionState === "function") this.controlsUnsubInteraction = anyControls.onInteractionState((active) => this.setInteractionActive(active)); return this; } addLayer(layer) { if (this.layers.has(layer.id)) throw new Error(`OverlaySystem: duplicate layer id '${layer.id}'.`); const wrapper = document.createElement("div"); wrapper.className = "wasmgpu-overlay-layer"; wrapper.dataset.overlayLayerId = layer.id; wrapper.style.position = "absolute"; wrapper.style.inset = "0"; wrapper.style.pointerEvents = "none"; this.root.appendChild(wrapper); try { layer.setSystem?.(this); layer.attach(wrapper); } catch (error) { layer.setSystem?.(null); wrapper.remove(); throw error; } this.layers.set(layer.id, { layer, enabled: true, wrapper }); this.invalidate("manual"); return this; } removeLayer(id) { const registration = this.layers.get(id); if (!registration) return this; registration.layer.setSystem?.(null); registration.layer.detach(); registration.wrapper.remove(); this.layers.delete(id); this.invalidate("manual"); return this; } clearLayers() { for (const registration of this.layers.values()) { registration.layer.setSystem?.(null); registration.layer.detach(); registration.wrapper.remove(); } this.layers.clear(); this.invalidate("manual"); return this; } invalidate(reason = "manual") { this.dirtyReasons.add(reason); this.requestFrame(); } setInteractionActive(active) { if (this.interactionActive === active) return this; this.interactionActive = active; if (!active) this.pendingInteractionFlush = true; this.invalidate("interaction"); return this; } update(request = {}) { if (request.camera !== void 0) this.currentCamera = request.camera; if (request.scene !== void 0) this.currentScene = request.scene; addReasons(this.dirtyReasons, request.reasons); this.syncRootBounds(); const camera = this.currentCamera; if (!camera) return false; const time = request.nowMs ?? nowMs(); writeCameraSignature(camera, this.cameraSigB); if (!this.hasCameraSig || !cameraSignatureEquals(this.cameraSigA, this.cameraSigB, 1e-6)) { this.cameraSigA.set(this.cameraSigB); this.hasCameraSig = true; this.dirtyReasons.add("camera"); } const force = !!request.force; if (this.dirtyReasons.size === 0 && !this.pendingInteractionFlush && !force) return false; if (this.interactionActive && this.interactionThrottleMs > 0 && !force) { if (Number.isFinite(this.lastUpdateMs) && time - this.lastUpdateMs < this.interactionThrottleMs) return false; } const reasons = new Set(this.dirtyReasons); if (this.pendingInteractionFlush) reasons.add("interaction"); for (const registration of this.layers.values()) { if (!registration.enabled) continue; registration.layer.update({ camera, scene: this.currentScene ?? null, width: this.width, height: this.height, dpr: this.dpr, nowMs: time, reasons, root: this.root }); } this.lastUpdateMs = time; this.pendingInteractionFlush = false; this.dirtyReasons.clear(); return true; } destroy() { this.cancelFrame(); this.controlsUnsubChange?.(); this.controlsUnsubInteraction?.(); this.controlsUnsubChange = null; this.controlsUnsubInteraction = null; if (this.resizeObserver) { try { this.resizeObserver.disconnect(); } catch { } this.resizeObserver = null; } if (this.winResizeListener) window.removeEventListener("resize", this.winResizeListener); if (this.winScrollListener) window.removeEventListener("scroll", this.winScrollListener, true); this.winResizeListener = null; this.winScrollListener = null; this.clearLayers(); this.cancelFrame(); this.root.remove(); } requestFrame() { if (!this.autoUpdate || this.rafId !== null) return; if (typeof requestAnimationFrame !== "function") return; this.rafId = requestAnimationFrame(() => { this.rafId = null; this.update(); }); } cancelFrame() { if (this.rafId === null) return; if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.rafId); this.rafId = null; } setupResizeEvents() { const onResize = () => { this.syncRootBounds(); this.invalidate("viewport"); }; this.winResizeListener = onResize; this.winScrollListener = onResize; window.addEventListener("resize", onResize); window.addEventListener("scroll", onResize, true); if (typeof ResizeObserver !== "undefined") { this.resizeObserver = new ResizeObserver(onResize); this.resizeObserver.observe(this.canvas); } } syncRootBounds() { const canvasRect = this.canvas.getBoundingClientRect(); const parentRect = this.parent === document.body || this.parent === document.documentElement ? { left: 0, top: 0 } : this.parent.getBoundingClientRect(); const left = canvasRect.left - parentRect.left + (this.parent.scrollLeft ?? 0); const top = canvasRect.top - parentRect.top + (this.parent.scrollTop ?? 0); const width = Math.max(1, Math.round(canvasRect.width || this.canvas.clientWidth || 1)); const height = Math.max(1, Math.round(canvasRect.height || this.canvas.clientHeight || 1)); const dpr = Math.max(1, window.devicePixelRatio || 1); const moved = !Number.isFinite(this.lastLeft) || !Number.isFinite(this.lastTop) || Math.abs(left - this.lastLeft) > 0.5 || Math.abs(top - this.lastTop) > 0.5; const resized = width !== this.width || height !== this.height || Math.abs(dpr - this.dpr) > 1e-6; if (!moved && !resized) return; this.lastLeft = left; this.lastTop = top; this.width = width; this.height = height; this.dpr = dpr; this.root.style.left = `${left}px`; this.root.style.top = `${top}px`; this.root.style.width = `${width}px`; this.root.style.height = `${height}px`; this.dirtyReasons.add(moved ? "layout" : "viewport"); } }; // typescript/overlay/axisTriadLayer.ts var AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; var AXIS_NAMES = ["x", "y", "z"]; var applyStyle = (node, style) => { if (node && style) Object.assign(node.style, style); }; var clearStyle = (node, style) => { if (node && style) for (const property of Object.keys(style)) node.style[property] = ""; }; var styleEquals = (a, b) => JSON.stringify(a) === JSON.stringify(b); var tupleEquals = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]); var AxisTriadLayer = class { id; anchor; lengthWorld; sizePx; lineWidthPx; labels; negativeLabels; colors; labelOffsetPx; font; directions; arrowSizePx; originSizePx; className; style; container = null; originEl = null; nodes = []; _system = null; constructor(desc = {}) { this.id = desc.id ?? "overlay-axis-triad"; this.anchor = desc.anchor ?? { kind: "screen", corner: "bottom-left", offsetPx: [26, -26] }; this.lengthWorld = Math.max(1e-6, desc.lengthWorld ?? 1); this.sizePx = Math.max(8, desc.sizePx ?? 56); this.lineWidthPx = Math.max(1, desc.lineWidthPx ?? 2); this.labels = [...desc.labels ?? ["X", "Y", "Z"]]; this.negativeLabels = [...desc.negativeLabels ?? this.labels.map((label) => `-${label}`)]; this.colors = [...desc.colors ?? ["#ff5f56", "#3fd77a", "#4ca7ff"]]; this.labelOffsetPx = Math.max(0, desc.labelOffsetPx ?? 8); this.font = desc.font ?? "11px monospace"; this.directions = { x: desc.directions?.x ?? "positive", y: desc.directions?.y ?? "positive", z: desc.directions?.z ?? "positive" }; this.arrowSizePx = Math.max(2, desc.arrowSizePx ?? 7); this.originSizePx = Math.max(2, desc.originSizePx ?? 7); this.className = desc.className ?? ""; this.style = desc.style ?? {}; } setSystem(system) { this._system = system; } attach(root) { if (this.container) this.detach(); const container = document.createElement("div"); container.className = `wasmgpu-overlay-axis-triad${this.className ? ` ${this.className}` : ""}`; container.style.position = "absolute"; container.style.inset = "0"; container.style.pointerEvents = "none"; applyStyle(container, this.style.container); container.style.position = "absolute"; container.style.inset = "0"; container.style.pointerEvents = "none"; root.appendChild(container); this.container = container; this.nodes = []; for (let axis = 0; axis < 3; axis++) for (const sign of [1, -1]) { const suffix = sign > 0 ? "positive" : "negative"; const line = document.createElement("div"); line.className = `wasmgpu-overlay-axis-triad-line wasmgpu-overlay-axis-triad-${AXIS_NAMES[axis]} wasmgpu-overlay-axis-triad-${suffix}`; line.style.position = "absolute"; line.style.transformOrigin = "0 50%"; applyStyle(line, this.style.axisLine); container.appendChild(line); const arrow = document.createElement("div"); arrow.className = `wasmgpu-overlay-axis-triad-arrowhead wasmgpu-overlay-axis-triad-${AXIS_NAMES[axis]} wasmgpu-overlay-axis-triad-${suffix}`; arrow.style.position = "absolute"; arrow.style.clipPath = "polygon(0 0, 100% 50%, 0 100%)"; applyStyle(arrow, this.style.arrowhead); container.appendChild(arrow); const label = document.createElement("div"); label.className = `wasmgpu-overlay-axis-triad-label wasmgpu-overlay-axis-triad-${AXIS_NAMES[axis]} wasmgpu-overlay-axis-triad-${suffix}`; label.style.position = "absolute"; label.style.whiteSpace = "nowrap"; applyStyle(label, this.style.label); container.appendChild(label); this.nodes.push({ axis, sign, line, arrow, label }); } const origin = document.createElement("div"); origin.className = "wasmgpu-overlay-axis-triad-origin"; origin.style.position = "absolute"; origin.style.borderRadius = "50%"; applyStyle(origin, this.style.originMarker); container.appendChild(origin); this.originEl = origin; } detach() { this.container?.remove(); this.container = null; this.originEl = null; this.nodes = []; } update(ctx) { if (!this.container) return; if (this.anchor.kind === "world") this.updateWorld(ctx, this.anchor); else this.updateScreen(ctx); } setAnchor(anchor) { if (JSON.stringify(anchor) === JSON.stringify(this.anchor)) return this; this.anchor = anchor; return this.changed("layout"); } setDirections(directions) { const next = { x: directions.x ?? this.directions.x, y: directions.y ?? this.directions.y, z: directions.z ?? this.directions.z }; if (next.x === this.directions.x && next.y === this.directions.y && next.z === this.directions.z) return this; this.directions = next; return this.changed(); } setLabels(labels, negativeLabels) { const negative = negativeLabels ?? labels.map((label) => `-${label}`); if (tupleEquals(labels, this.labels) && tupleEquals(negative, this.negativeLabels)) return this; this.labels = [...labels]; this.negativeLabels = [...negative]; return this.changed(); } setColors(colors) { if (tupleEquals(colors, this.colors)) return this; this.colors = [...colors]; return this.changed(); } setLengthWorld(value) { const next = Math.max(1e-6, value); if (next === this.lengthWorld) return this; this.lengthWorld = next; return this.changed(); } setSizePx(value) { const next = Math.max(8, value); if (next === this.sizePx) return this; this.sizePx = next; return this.changed("layout"); } setLineWidth(value) { const next = Math.max(1, value); if (next === this.lineWidthPx) return this; this.lineWidthPx = next; return this.changed(); } setArrowSize(value) { const next = Math.max(2, value); if (next === this.arrowSizePx) return this; this.arrowSizePx = next; return this.changed(); } setOriginSize(value) { const next = Math.max(2, value); if (next === this.originSizePx) return this; this.originSizePx = next; return this.changed(); } setLabelAppearance(offsetPx, font = this.font) { const nextOffset = Math.max(0, offsetPx); if (nextOffset === this.labelOffsetPx && font === this.font) return this; this.labelOffsetPx = nextOffset; this.font = font; return this.changed("layout"); } setClassName(className) { if (className === this.className) return this; this.className = className; this.updateContainerClass(); return this.changed("layout"); } setStyle(style) { if (styleEquals(style, this.style)) return this; const previous = this.style; this.style = style; this.applyCurrentStyles(previous); return this.changed("layout"); } changed(reason = "manual") { this._system?.invalidate(reason); return this; } directionVisible(axis, sign) { const direction = this.directions[AXIS_NAMES[axis]]; return direction === "both" || (sign > 0 ? direction === "positive" : direction === "negative"); } updateWorld(ctx, anchor) { const origin = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, anchor.position); if (!origin || !origin.inFront) { this.hideAll(); return; } this.drawOrigin(origin.x, origin.y); for (const node of this.nodes) { if (!this.directionVisible(node.axis, node.sign)) { this.hideNode(node); continue; } const axis = AXES[node.axis]; const endpoint = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, [anchor.position[0] + axis[0] * this.lengthWorld * node.sign, anchor.position[1] + axis[1] * this.lengthWorld * node.sign, anchor.position[2] + axis[2] * this.lengthWorld * node.sign]); if (!endpoint || !endpoint.inFront) { this.hideNode(node); continue; } this.drawNode(node, origin.x, origin.y, endpoint.x, endpoint.y, endpoint.ndcZ); } } updateScreen(ctx) { const [cx, cy] = resolveScreenAnchorPoint(this.anchor.kind === "screen" ? this.anchor : void 0, ctx.width, ctx.height); this.drawOrigin(cx, cy); const m = ctx.camera.viewMatrix; for (const node of this.nodes) { if (!this.directionVisible(node.axis, node.sign)) { this.hideNode(node); continue; } const axis = AXES[node.axis]; const vx = (m[0] * axis[0] + m[4] * axis[1] + m[8] * axis[2]) * node.sign; const vy = (m[1] * axis[0] + m[5] * axis[1] + m[9] * axis[2]) * node.sign; const vz = (m[2] * axis[0] + m[6] * axis[1] + m[10] * axis[2]) * node.sign; this.drawNode(node, cx, cy, cx + vx * this.sizePx, cy - vy * this.sizePx, -vz); } } drawNode(node, x0, y0, x1, y1, depth) { const dx = x1 - x0; const dy = y1 - y0; const len = Math.hypot(dx, dy); if (!Number.isFinite(len) || len < 0.75) { this.hideNode(node); return; } const color = this.colors[node.axis]; const angle = Math.atan2(dy, dx); const lineWidth = this.lineWidthPx; const ux = dx / len; const uy = dy / len; const arrowLength = Math.min(this.arrowSizePx, len * 0.75); const arrowHalfWidth = arrowLength * 0.55; const baseX = x1 - ux * arrowLength; const baseY = y1 - uy * arrowLength; const shaftLength = Math.hypot(baseX - x0, baseY - y0); node.line.style.background = color; node.arrow.style.background = color; node.label.style.font = this.font; node.label.style.color = color; applyStyle(node.line, this.style.axisLine); applyStyle(node.arrow, this.style.arrowhead); applyStyle(node.label, this.style.label); node.line.style.display = ""; node.arrow.style.display = ""; node.label.style.display = ""; node.line.style.position = "absolute"; node.line.style.transformOrigin = "0 50%"; node.line.style.left = `${x0}px`; node.line.style.top = `${y0}px`; node.line.style.width = `${shaftLength}px`; node.line.style.height = `${lineWidth}px`; node.line.style.transform = `translateY(${-lineWidth * 0.5}px) rotate(${angle}rad)`; node.arrow.style.position = "absolute"; node.arrow.style.left = `${baseX}px`; node.arrow.style.top = `${baseY - arrowHalfWidth}px`; node.arrow.style.width = `${arrowLength}px`; node.arrow.style.height = `${arrowHalfWidth * 2}px`; node.arrow.style.clipPath = "polygon(0 0, 100% 50%, 0 100%)"; node.arrow.style.transformOrigin = "0 50%"; node.arrow.style.transform = `rotate(${angle}rad)`; node.label.textContent = node.sign > 0 ? this.labels[node.axis] : this.negativeLabels[node.axis]; node.label.style.position = "absolute"; node.label.style.whiteSpace = "nowrap"; node.label.style.left = `${x1 + ux * this.labelOffsetPx}px`; node.label.style.top = `${y1 + uy * this.labelOffsetPx}px`; node.label.style.transform = `translate(${ux < -0.15 ? "-100%" : ux <= 0.15 ? "-50%" : "0"}, ${uy < -0.15 ? "-100%" : uy <= 0.15 ? "-50%" : "0"})`; const z = Math.round(100 - depth * 10); node.line.style.zIndex = `${z}`; node.arrow.style.zIndex = `${z}`; node.label.style.zIndex = `${z + 1}`; } drawOrigin(x, y) { if (!this.originEl) return; const s = this.originSizePx; this.originEl.style.background = "#eef5ff"; applyStyle(this.originEl, this.style.originMarker); this.originEl.style.display = ""; this.originEl.style.position = "absolute"; this.originEl.style.borderRadius = "50%"; this.originEl.style.left = `${x - s * 0.5}px`; this.originEl.style.top = `${y - s * 0.5}px`; this.originEl.style.width = `${s}px`; this.originEl.style.height = `${s}px`; this.originEl.style.zIndex = "200"; } hideNode(node) { node.line.style.display = "none"; node.arrow.style.display = "none"; node.label.style.display = "none"; } hideAll() { for (const node of this.nodes) this.hideNode(node); if (this.originEl) this.originEl.style.display = "none"; } updateContainerClass() { if (this.container) this.container.className = `wasmgpu-overlay-axis-triad${this.className ? ` ${this.className}` : ""}`; } applyCurrentStyles(previous = {}) { clearStyle(this.container, previous.container); applyStyle(this.container, this.style.container); if (this.container) { this.container.style.position = "absolute"; this.container.style.inset = "0"; this.container.style.pointerEvents = "none"; } clearStyle(this.originEl, previous.originMarker); applyStyle(this.originEl, this.style.originMarker); for (const node of this.nodes) { clearStyle(node.line, previous.axisLine); clearStyle(node.arrow, previous.arrowhead); clearStyle(node.label, previous.label); applyStyle(node.line, this.style.axisLine); applyStyle(node.arrow, this.style.arrowhead); applyStyle(node.label, this.style.label); } } }; // typescript/overlay/pool.ts var DOMNodePool = class { constructor(parent, create, maxNodes) { this.parent = parent; this.create = create; this.maxNodes = maxNodes; } parent; create; maxNodes; nodes = []; used = 0; beginFrame() { this.used = 0; } acquire() { if (this.used < this.nodes.length) { const node2 = this.nodes[this.used++]; node2.style.display = ""; return node2; } if (this.nodes.length >= this.maxNodes) { throw new Error(`DOMNodePool: exceeded max node budget (${this.maxNodes}).`); } const node = this.create(); this.parent.appendChild(node); this.nodes.push(node); this.used++; return node; } endFrame() { for (let i = this.used; i < this.nodes.length; i++) this.nodes[i].style.display = "none"; } get size() { return this.nodes.length; } clear(removeFromDom = false) { this.used = 0; for (let i = 0; i < this.nodes.length; i++) { if (removeFromDom) this.nodes[i].remove(); else this.nodes[i].style.display = "none"; } if (removeFromDom) this.nodes.length = 0; } }; // typescript/overlay/gridLayer.ts var formatTick = (value) => { if (!Number.isFinite(value)) return "nan"; const abs = Math.abs(value); if (abs >= 1e4 || abs > 0 && abs < 1e-3) return value.toExponential(2); const rounded = Math.round(value * 1e3) / 1e3; return `${rounded}`; }; var applyStyle2 = (node, style) => { if (node && style) Object.assign(node.style, style); }; var clearStyle2 = (node, style) => { if (node && style) for (const property of Object.keys(style)) node.style[property] = ""; }; var styleEquals2 = (a, b) => JSON.stringify(a) === JSON.stringify(b); var sideIncludes = (side, target) => side === "both" || side === target; var sideCount = (side) => side === "both" ? 2 : side === "none" ? 0 : 1; var metadataText = (metadata) => metadata.name ? metadata.unit ? `${metadata.name} (${metadata.unit})` : metadata.name : metadata.unit ?? ""; var intersects = (a, b) => a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; var tuple3Equals = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; var metadataEquals = (a, b) => a.name === b.name && a.unit === b.unit && a.labelSide === b.labelSide; var niceStep = (target) => { const x = Math.max(1e-9, Math.abs(target)); const exponent = Math.floor(Math.log10(x)); const base = Math.pow(10, exponent); const scaled = x / base; const nice = scaled <= 1 ? 1 : scaled <= 2 ? 2 : scaled <= 5 ? 5 : 10; return nice * base; }; var axesForPlane = (plane) => { if (plane === "xy") return { u: [1, 0, 0], v: [0, 1, 0] }; if (plane === "xz") return { u: [1, 0, 0], v: [0, 0, 1] }; return { u: [0, 1, 0], v: [0, 0, 1] }; }; var uvFromBounds = (plane, bounds) => { if (plane === "xy") return { uMin: bounds.boxMin[0], uMax: bounds.boxMax[0], vMin: bounds.boxMin[1], vMax: bounds.boxMax[1] }; if (plane === "xz") return { uMin: bounds.boxMin[0], uMax: bounds.boxMax[0], vMin: bounds.boxMin[2], vMax: bounds.boxMax[2] }; return { uMin: bounds.boxMin[1], uMax: bounds.boxMax[1], vMin: bounds.boxMin[2], vMax: bounds.boxMax[2] }; }; var worldFromUV = (plane, origin, u, v) => { if (plane === "xy") return [origin[0] + u, origin[1] + v, origin[2]]; if (plane === "xz") return [origin[0] + u, origin[1], origin[2] + v]; return [origin[0], origin[1] + u, origin[2] + v]; }; var drawLine = (node, x0, y0, x1, y1, color, widthPx, style) => { const dx = x1 - x0; const dy = y1 - y0; const len = Math.hypot(dx, dy); if (!Number.isFinite(len) || len <= 1e-5) { node.style.display = "none"; return; } node.style.background = color; applyStyle2(node, style); node.style.display = ""; node.style.position = "absolute"; node.style.transformOrigin = "0 50%"; node.style.left = `${x0}px`; node.style.top = `${y0}px`; node.style.width = `${len}px`; node.style.height = `${Math.max(1, widthPx)}px`; node.style.transform = `translateY(${-0.5 * Math.max(1, widthPx)}px) rotate(${Math.atan2(dy, dx)}rad)`; }; var signedZero = (x, eps) => Math.abs(x) <= eps ? 0 : x; var isNear = (a, b, eps) => Math.abs(a - b) <= eps; var tickEpsilon = (span, step) => Math.max(1e-9, Math.abs(span) * 1e-9, Math.abs(step) * 1e-6); var isMajorTick = (value, majorStep, eps) => { if (!Number.isFinite(majorStep) || majorStep <= eps) return false; const q = value / majorStep; return Math.abs(q - Math.round(q)) <= 1e-4; }; var countInteriorTicks = (min, max, step) => { const span = Math.max(0, max - min); const eps = tickEpsilon(span, step); if (step <= eps) return 0; let count = 0; let value = Math.ceil((min + eps) / step) * step; const limit = max - eps; for (let i = 0; i < 1e6 && value <= limit; i++, value += step) if (value > min + eps && value < max - eps) count++; return count; }; var buildEdgeAlignedTicks = (min, max, step) => { const span = Math.max(0, max - min); const eps = tickEpsilon(span, step); if (span <= eps) return [min]; const ticks = [min]; if (step > eps) { let value = Math.ceil((min + eps) / step) * step; const limit = max - eps; for (let i = 0; i < 1e6 && value <= limit; i++, value += step) { if (value <= min + eps || value >= max - eps) continue; ticks.push(signedZero(value, eps)); } } ticks.push(max); return ticks; }; var GridLayer = class { id; plane; origin; extentMode; fixedUMin; fixedUMax; fixedVMin; fixedVMax; targetMinorSpacingPx; majorStepFactor; minLabelSpacingPx; maxLines; maxLabels; minorColor; majorColor; axisColor; labelColor; lineWidthMinorPx; lineWidthMajorPx; font; tickFormatter; uAxis; vAxis; className; style; _system = null; container = null; linePool = null; labelPool = null; measureCtx = null; constructor(desc = {}) { this.id = desc.id ?? "overlay-grid"; this.plane = desc.plane ?? "xy"; this.origin = desc.origin ?? [0, 0, 0]; this.extentMode = desc.extentMode ?? "scene-fit"; this.fixedUMin = desc.fixedUMin ?? -10; this.fixedUMax = desc.fixedUMax ?? 10; this.fixedVMin = desc.fixedVMin ?? -10; this.fixedVMax = desc.fixedVMax ?? 10; this.targetMinorSpacingPx = Math.max(6, desc.targetMinorSpacingPx ?? 30); this.majorStepFactor = Math.max(2, Math.round(desc.majorStepFactor ?? 5)); this.minLabelSpacingPx = Math.max(8, desc.minLabelSpacingPx ?? 58); this.maxLines = Math.max(4, Math.round(desc.maxLines ?? 160)); this.maxLabels = Math.max(2, Math.round(desc.maxLabels ?? 60)); this.minorColor = desc.minorColor ?? "rgba(180, 210, 255, 0.17)"; this.majorColor = desc.majorColor ?? "rgba(180, 210, 255, 0.36)"; this.axisColor = desc.axisColor ?? "rgba(220, 235, 255, 0.8)"; this.labelColor = desc.labelColor ?? "rgba(220, 235, 255, 0.9)"; this.lineWidthMinorPx = Math.max(1, desc.lineWidthMinorPx ?? 1); this.lineWidthMajorPx = Math.max(1, desc.lineWidthMajorPx ?? 2); this.font = desc.font ?? "11px monospace"; this.tickFormatter = desc.tickFormatter ?? ((value) => formatTick(value)); this.uAxis = { ...desc.uAxis, labelSide: desc.uAxis?.labelSide ?? "min" }; this.vAxis = { ...desc.vAxis, labelSide: desc.vAxis?.labelSide ?? "min" }; this.className = desc.className ?? ""; this.style = desc.style ?? {}; } setSystem(system) { this._system = system; } attach(root) { const container = document.createElement("div"); container.className = `wasmgpu-overlay-grid${this.className ? ` ${this.className}` : ""}`; container.style.position = "absolute"; container.style.inset = "0"; container.style.pointerEvents = "none"; applyStyle2(container, this.style.container); container.style.position = "absolute"; container.style.inset = "0"; container.style.pointerEvents = "none"; root.appendChild(container); this.container = container; this.linePool = new DOMNodePool(container, () => { const node = document.createElement("div"); node.style.position = "absolute"; node.style.transformOrigin = "0 50%"; node.className = "wasmgpu-overlay-grid-line"; return node; }, this.maxLines); this.labelPool = new DOMNodePool(container, () => { const node = document.createElement("div"); node.style.position = "absolute"; node.style.color = this.labelColor; node.style.font = this.font; node.style.whiteSpace = "nowrap"; node.className = "wasmgpu-overlay-grid-tick-label"; return node; }, this.maxLabels); this.measureCtx = document.createElement("canvas").getContext("2d"); } detach() { this.linePool?.clear(true); this.labelPool?.clear(true); this.linePool = null; this.labelPool = null; this.measureCtx = null; this.container?.remove(); this.container = null; } setPlane(plane) { if (plane === this.plane) return this; this.plane = plane; return this.changed("layout"); } setOrigin(origin) { if (tuple3Equals(origin, this.origin)) return this; this.origin = [...origin]; return this.changed("layout"); } setExtentMode(mode) { if (mode === this.extentMode) return this; this.extentMode = mode; return this.changed("layout"); } setFixedExtent(uMin, uMax, vMin, vMax) { if (uMin === this.fixedUMin && uMax === this.fixedUMax && vMin === this.fixedVMin && vMax === this.fixedVMax) return this; this.fixedUMin = uMin; this.fixedUMax = uMax; this.fixedVMin = vMin; this.fixedVMax = vMax; return this.changed("layout"); } setSpacing(targetMinorSpacingPx, majorStepFactor = this.majorStepFactor, minLabelSpacingPx = this.minLabelSpacingPx) { const spacing = Math.max(6, targetMinorSpacingPx); const factor = Math.max(2, Math.round(majorStepFactor)); const labelSpacing = Math.max(8, minLabelSpacingPx); if (spacing === this.targetMinorSpacingPx && factor === this.majorStepFactor && labelSpacing === this.minLabelSpacingPx) return this; this.targetMinorSpacingPx = spacing; this.majorStepFactor = factor; this.minLabelSpacingPx = labelSpacing; return this.changed("layout"); } setColors(minorColor, majorColor, axisColor, labelColor = this.labelColor) { if (minorColor === this.minorColor && majorColor === this.majorColor && axisColor === this.axisColor && labelColor === this.labelColor) return this; this.minorColor = minorColor; this.majorColor = majorColor; this.axisColor = axisColor; this.labelColor = labelColor; return this.changed(); } setLineWidths(minorPx, majorPx) { const minor = Math.max(1, minorPx); const major = Math.max(1, majorPx); if (minor === this.lineWidthMinorPx && major === this.lineWidthMajorPx) return this; this.lineWidthMinorPx = minor; this.lineWidthMajorPx = major; return this.changed(); } setFont(font) { if (font === this.font) return this; this.font = font; return this.changed("layout"); } setTickFormatter(formatter) { if (formatter === this.tickFormatter) return this; this.tickFormatter = formatter; return this.changed("layout"); } setAxisMetadata(axis, metadata) { const current = axis === "u" ? this.uAxis : this.vAxis; const next = { ...metadata, labelSide: metadata.labelSide ?? current.labelSide ?? "min" }; if (metadataEquals(next, current)) return this; if (axis === "u") this.uAxis = next; else this.vAxis = next; return this.changed("layout"); } setLabelSides(u, v) { if (u === this.uAxis.labelSide && v === this.vAxis.labelSide) return this; this.uAxis = { ...this.uAxis, labelSide: u }; this.vAxis = { ...this.vAxis, labelSide: v }; return this.changed("layout"); } setClassName(className) { if (className === this.className) return this; this.className = className; if (this.container) this.container.className = `wasmgpu-overlay-grid${className ? ` ${className}` : ""}`; return this.changed("layout"); } setStyle(style) { if (styleEquals2(style, this.style)) return this; const previous = this.style; this.style = style; this.applyCurrentStyles(previous); return this.changed("layout"); } changed(reason = "manual") { this._system?.invalidate(reason); return this; } update(ctx) { if (!this.container || !this.linePool || !this.labelPool) return; const { uMin, uMax, vMin, vMax } = this.resolveExtent(ctx); const spanU = Math.max(1e-6, uMax - uMin); const spanV = Math.max(1e-6, vMax - vMin); const pxPerUnit = this.estimatePixelsPerUnitAxes(ctx); const referencePxPerUnit = Math.max(pxPerUnit.u, pxPerUnit.v); let minorStep = niceStep(this.targetMinorSpacingPx / Math.max(1e-6, referencePxPerUnit)); const reservedBoundaryLines = (spanU > 1e-9 ? 2 : 1) + (spanV > 1e-9 ? 2 : 1); const maxInteriorLines = Math.max(0, this.maxLines - reservedBoundaryLines); let interiorU = countInteriorTicks(uMin, uMax, minorStep); let interiorV = countInteriorTicks(vMin, vMax, minorStep); for (let i = 0; i < 32 && interiorU + interiorV > maxInteriorLines; i++) { minorStep = niceStep(minorStep * 1.5); interiorU = countInteriorTicks(uMin, uMax, minorStep); interiorV = countInteriorTicks(vMin, vMax, minorStep); } const majorStep = minorStep * this.majorStepFactor; const majorSpacingPxU = majorStep * pxPerUnit.u; const majorSpacingPxV = majorStep * pxPerUnit.v; let labelStrideU = Math.max(1, Math.ceil(this.minLabelSpacingPx / Math.max(1e-6, majorSpacingPxU))); let labelStrideV = Math.max(1, Math.ceil(this.minLabelSpacingPx / Math.max(1e-6, majorSpacingPxV))); const uTicks = buildEdgeAlignedTicks(uMin, uMax, minorStep); const vTicks = buildEdgeAlignedTicks(vMin, vMax, minorStep); const edgeEpsU = tickEpsilon(spanU, minorStep); const edgeEpsV = tickEpsilon(spanV, minorStep); const majorEpsU = Math.max(edgeEpsU, Math.abs(majorStep) * 1e-6); const majorEpsV = Math.max(edgeEpsV, Math.abs(majorStep) * 1e-6); const axisEpsU = Math.max(edgeEpsU, Math.abs(minorStep) * 1e-3); const axisEpsV = Math.max(edgeEpsV, Math.abs(minorStep) * 1e-3); const majorCountU = uTicks.reduce((n, u) => n + (isMajorTick(u, majorStep, majorEpsU) ? 1 : 0), 0); const majorCountV = vTicks.reduce((n, v) => n + (isMajorTick(v, majorStep, majorEpsV) ? 1 : 0), 0); const sidesU = sideCount(this.uAxis.labelSide ?? "min"); const sidesV = sideCount(this.vAxis.labelSide ?? "min"); const titleCount = (metadataText(this.uAxis) ? sidesU : 0) + (metadataText(this.vAxis) ? sidesV : 0); for (let i = 0; i < 32; i++) { const estLabelsU = majorCountU > 0 ? Math.ceil(majorCountU / labelStrideU) * sidesU : 0; const estLabelsV = majorCountV > 0 ? Math.ceil(majorCountV / labelStrideV) * sidesV : 0; if (estLabelsU + estLabelsV + titleCount <= this.maxLabels) break; if (estLabelsU >= estLabelsV) labelStrideU++; else labelStrideV++; } this.linePool.beginFrame(); this.labelPool.beginFrame(); const tickCandidates = []; const titleCandidates = []; let uMajorIndex = 0; for (let i = 0; i < uTicks.length; i++) { const u = uTicks[i]; const seg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, u, vMin), worldFromUV(this.plane, this.origin, u, vMax)); if (!seg) continue; const p0 = seg.p0; const p1 = seg.p1; const major = isMajorTick(u, majorStep, majorEpsU); const axis = Math.abs(u) <= axisEpsU; const edge = isNear(u, uMin, edgeEpsU) || isNear(u, uMax, edgeEpsU); const line = this.linePool.acquire(); line.className = axis ? "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-zero-axis-line" : major || edge ? "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-major-line" : "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-minor-line"; drawLine(line, p0.x, p0.y, p1.x, p1.y, axis ? this.axisColor : major || edge ? this.majorColor : this.minorColor, major || axis || edge ? this.lineWidthMajorPx : this.lineWidthMinorPx, axis ? this.style.zeroAxisLine : major || edge ? this.style.majorLine : this.style.minorLine); if (sidesU > 0 && major && uMajorIndex % labelStrideU === 0) { const text = this.tickFormatter(u, "u"); if (sideIncludes(this.uAxis.labelSide ?? "min", "min")) tickCandidates.push(this.createLabelCandidate(text, p0.x, p0.y, p0.x - p1.x, p0.y - p1.y, "u-min", false)); if (sideIncludes(this.uAxis.labelSide ?? "min", "max")) tickCandidates.push(this.createLabelCandidate(text, p1.x, p1.y, p1.x - p0.x, p1.y - p0.y, "u-max", false)); } if (major) uMajorIndex++; } let vMajorIndex = 0; for (let i = 0; i < vTicks.length; i++) { const v = vTicks[i]; const seg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, uMin, v), worldFromUV(this.plane, this.origin, uMax, v)); if (!seg) continue; const p0 = seg.p0; const p1 = seg.p1; const major = isMajorTick(v, majorStep, majorEpsV); const axis = Math.abs(v) <= axisEpsV; const edge = isNear(v, vMin, edgeEpsV) || isNear(v, vMax, edgeEpsV); const line = this.linePool.acquire(); line.className = axis ? "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-zero-axis-line" : major || edge ? "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-major-line" : "wasmgpu-overlay-grid-line wasmgpu-overlay-grid-minor-line"; drawLine(line, p0.x, p0.y, p1.x, p1.y, axis ? this.axisColor : major || edge ? this.majorColor : this.minorColor, major || axis || edge ? this.lineWidthMajorPx : this.lineWidthMinorPx, axis ? this.style.zeroAxisLine : major || edge ? this.style.majorLine : this.style.minorLine); if (sidesV > 0 && major && vMajorIndex % labelStrideV === 0) { const text = this.tickFormatter(v, "v"); if (sideIncludes(this.vAxis.labelSide ?? "min", "min")) tickCandidates.push(this.createLabelCandidate(text, p0.x, p0.y, p0.x - p1.x, p0.y - p1.y, "v-min", false)); if (sideIncludes(this.vAxis.labelSide ?? "min", "max")) tickCandidates.push(this.createLabelCandidate(text, p1.x, p1.y, p1.x - p0.x, p1.y - p0.y, "v-max", false)); } if (major) vMajorIndex++; } const uTitle = metadataText(this.uAxis); const vTitle = metadataText(this.vAxis); if (uTitle) { const minSeg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, uMin, vMin), worldFromUV(this.plane, this.origin, uMax, vMin)); const maxSeg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, uMin, vMax), worldFromUV(this.plane, this.origin, uMax, vMax)); if (minSeg && maxSeg) { const minX = (minSeg.p0.x + minSeg.p1.x) * 0.5, minY = (minSeg.p0.y + minSeg.p1.y) * 0.5; const maxX = (maxSeg.p0.x + maxSeg.p1.x) * 0.5, maxY = (maxSeg.p0.y + maxSeg.p1.y) * 0.5; if (sideIncludes(this.uAxis.labelSide ?? "min", "min")) titleCandidates.push(this.createLabelCandidate(uTitle, minX, minY, minX - maxX, minY - maxY, "u-min", true)); if (sideIncludes(this.uAxis.labelSide ?? "min", "max")) titleCandidates.push(this.createLabelCandidate(uTitle, maxX, maxY, maxX - minX, maxY - minY, "u-max", true)); } } if (vTitle) { const minSeg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, uMin, vMin), worldFromUV(this.plane, this.origin, uMin, vMax)); const maxSeg = this.projectFrontClippedSegment(ctx, worldFromUV(this.plane, this.origin, uMax, vMin), worldFromUV(this.plane, this.origin, uMax, vMax)); if (minSeg && maxSeg) { const minX = (minSeg.p0.x + minSeg.p1.x) * 0.5, minY = (minSeg.p0.y + minSeg.p1.y) * 0.5; const maxX = (maxSeg.p0.x + maxSeg.p1.x) * 0.5, maxY = (maxSeg.p0.y + maxSeg.p1.y) * 0.5; if (sideIncludes(this.vAxis.labelSide ?? "min", "min")) titleCandidates.push(this.createLabelCandidate(vTitle, minX, minY, minX - maxX, minY - maxY, "v-min", true)); if (sideIncludes(this.vAxis.labelSide ?? "min", "max")) titleCandidates.push(this.createLabelCandidate(vTitle, maxX, maxY, maxX - minX, maxY - minY, "v-max", true)); } } const acceptedLabels = this.selectLabelCandidates(titleCandidates, tickCandidates); this.container.dataset.labelCandidateCount = `${tickCandidates.length}`; this.container.dataset.labelAcceptedCount = `${acceptedLabels.length}`; for (const candidate of acceptedLabels) { const label = this.labelPool.acquire(); label.className = candidate.title ? "wasmgpu-overlay-grid-axis-title" : "wasmgpu-overlay-grid-tick-label"; label.dataset.side = candidate.side; label.textContent = candidate.text; label.style.color = this.labelColor; label.style.font = this.font; applyStyle2(label, candidate.title ? this.style.axisTitle : this.style.tickLabel); label.style.position = "absolute"; label.style.whiteSpace = "nowrap"; label.style.left = `${candidate.left}px`; label.style.top = `${candidate.top}px`; } this.linePool.endFrame(); this.labelPool.endFrame(); } createLabelCandidate(text, anchorX, anchorY, outwardX, outwardY, side, title) { const { width, height } = this.measureLabel(text, title); const length = Math.hypot(outwardX, outwardY); const ox = length > 1e-6 ? outwardX / length : 0; const oy = length > 1e-6 ? outwardY / length : 1; const offset = title ? 16 : 7; const x = anchorX + ox * offset; const y = anchorY + oy * offset; const left = x + (ox < -0.2 ? -width : ox > 0.2 ? 0 : -width * 0.5); const top = y + (oy < -0.2 ? -height : oy > 0.2 ? 0 : -height * 0.5); return { text, left, top, right: left + width, bottom: top + height, side, title }; } measureLabel(text, title) { const style = title ? this.style.axisTitle : this.style.tickLabel; let font = style?.font ?? this.font; if (style?.fontSize) font = /\d+(?:\.\d+)?px/.test(font) ? font.replace(/\d+(?:\.\d+)?px/, style.fontSize) : `${style.fontSize} ${style.fontFamily ?? "sans-serif"}`; if (style?.fontFamily) font = /\d+(?:\.\d+)?px/.test(font) ? font.replace(/(\d+(?:\.\d+)?px).*/, `$1 ${style.fontFamily}`) : `${font} ${style.fontFamily}`; if (style?.fontWeight && !style.font) font = `${style.fontWeight} ${font}`; if (style?.fontStyle && !style.font) font = `${style.fontStyle} ${font}`; if (this.measureCtx) this.measureCtx.font = font; const fontPx = Number.parseFloat(style?.fontSize ?? font) || 11; const letterSpacing = Number.parseFloat(style?.letterSpacing ?? "0") || 0; const measured = this.measureCtx?.measureText(text).width ?? text.length * fontPx * 0.62; return { width: Math.max(fontPx, measured + Math.max(0, text.length - 1) * letterSpacing) + 4, height: fontPx * 1.35 + 2 }; } selectLabelCandidates(titles, ticks) { const accepted = []; for (const candidate of [...titles, ...ticks]) { if (accepted.length >= this.maxLabels) break; if (accepted.some((existing) => existing.side === candidate.side && intersects(existing, candidate))) continue; accepted.push(candidate); } return accepted; } applyCurrentStyles(previous) { clearStyle2(this.container, previous.container); applyStyle2(this.container, this.style.container); if (!this.container) return; this.container.style.position = "absolute"; this.container.style.inset = "0"; this.container.style.pointerEvents = "none"; const replaceAll = (selector, before, after) => { for (const node of this.container.querySelectorAll(selector)) { clearStyle2(node, before); applyStyle2(node, after); } }; replaceAll(".wasmgpu-overlay-grid-minor-line", previous.minorLine, this.style.minorLine); replaceAll(".wasmgpu-overlay-grid-major-line", previous.majorLine, this.style.majorLine); replaceAll(".wasmgpu-overlay-grid-zero-axis-line", previous.zeroAxisLine, this.style.zeroAxisLine); replaceAll(".wasmgpu-overlay-grid-tick-label", previous.tickLabel, this.style.tickLabel); replaceAll(".wasmgpu-overlay-grid-axis-title", previous.axisTitle, this.style.axisTitle); } projectFrontClippedSegment(ctx, worldA, worldB) { const near = this.getCameraNear(ctx.camera); const nearZ = -(near > 0 ? near + Math.max(near * 1e-4, 1e-6) : 0); const view = ctx.camera.viewMatrix; let a = [worldA[0], worldA[1], worldA[2]]; let b = [worldB[0], worldB[1], worldB[2]]; let va = this.transformPoint(view, a); let vb = this.transformPoint(view, b); if (va[2] > nearZ || vb[2] > nearZ) { if (va[2] > nearZ && vb[2] > nearZ) return null; if (va[2] > nearZ) { const denom = vb[2] - va[2]; if (!Number.isFinite(denom) || Math.abs(denom) <= 1e-8) return null; const t = clamp((nearZ - va[2]) / denom, 0, 1); a = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; va = [va[0] + (vb[0] - va[0]) * t, va[1] + (vb[1] - va[1]) * t, nearZ]; } if (vb[2] > nearZ) { const denom = va[2] - vb[2]; if (!Number.isFinite(denom) || Math.abs(denom) <= 1e-8) return null; const t = clamp((nearZ - vb[2]) / denom, 0, 1); b = [b[0] + (a[0] - b[0]) * t, b[1] + (a[1] - b[1]) * t, b[2] + (a[2] - b[2]) * t]; vb = [vb[0] + (va[0] - vb[0]) * t, vb[1] + (va[1] - vb[1]) * t, nearZ]; } } const p0 = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, a); const p1 = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, b); if (!p0 || !p1 || !p0.inFront || !p1.inFront) return null; if (!Number.isFinite(p0.x) || !Number.isFinite(p0.y) || !Number.isFinite(p1.x) || !Number.isFinite(p1.y)) return null; return { p0, p1 }; } getCameraNear(camera) { const near = camera.near; if (typeof near !== "number" || !Number.isFinite(near)) return 0; return Math.max(0, near); } transformPoint(matrix, p) { const x = p[0] ?? 0; const y = p[1] ?? 0; const z = p[2] ?? 0; return [matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12], matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]]; } resolveExtent(ctx) { if (this.extentMode === "fixed") return { uMin: Math.min(this.fixedUMin, this.fixedUMax), uMax: Math.max(this.fixedUMin, this.fixedUMax), vMin: Math.min(this.fixedVMin, this.fixedVMax), vMax: Math.max(this.fixedVMin, this.fixedVMax) }; const scene = ctx.scene; if (!scene) return { uMin: -10, uMax: 10, vMin: -10, vMax: 10 }; const bounds = scene.getBounds(); if (bounds.empty) return { uMin: -10, uMax: 10, vMin: -10, vMax: 10 }; const uv = uvFromBounds(this.plane, bounds); const marginU = Math.max(1e-3, (uv.uMax - uv.uMin) * 0.1); const marginV = Math.max(1e-3, (uv.vMax - uv.vMin) * 0.1); return { uMin: uv.uMin - marginU, uMax: uv.uMax + marginU, vMin: uv.vMin - marginV, vMax: uv.vMax + marginV }; } estimatePixelsPerUnitAxes(ctx) { const axes = axesForPlane(this.plane); const p0 = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, this.origin); if (!p0 || !p0.inFront || p0.ndcZ < 0 || p0.ndcZ > 1) return { u: 1, v: 1 }; const pu = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, [this.origin[0] + axes.u[0], this.origin[1] + axes.u[1], this.origin[2] + axes.u[2]]); const pv = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, [this.origin[0] + axes.v[0], this.origin[1] + axes.v[1], this.origin[2] + axes.v[2]]); const du = pu && pu.inFront && pu.ndcZ >= 0 && pu.ndcZ <= 1 && Number.isFinite(pu.x) && Number.isFinite(pu.y) ? Math.hypot(pu.x - p0.x, pu.y - p0.y) : 1; const dv = pv && pv.inFront && pv.ndcZ >= 0 && pv.ndcZ <= 1 && Number.isFinite(pv.x) && Number.isFinite(pv.y) ? Math.hypot(pv.x - p0.x, pv.y - p0.y) : 1; return { u: clamp(du, 1e-6, 1e9), v: clamp(dv, 1e-6, 1e9) }; } }; // typescript/overlay/legendLayer.ts var formatDefault = (value) => { if (!Number.isFinite(value)) return "nan"; const abs = Math.abs(value); if (abs >= 1e4 || abs > 0 && abs < 1e-3) return value.toExponential(3); const rounded = Math.round(value * 1e6) / 1e6; return `${rounded}`; }; var applyStyle3 = (node, style) => { if (node && style) Object.assign(node.style, style); }; var clearStyle3 = (node, style) => { if (node && style) for (const property of Object.keys(style)) node.style[property] = ""; }; var styleEquals3 = (a, b) => JSON.stringify(a) === JSON.stringify(b); var sampleCustomStops = (tIn, stopsIn) => sampleColorStops(tIn, stopsIn); var serializeTransform = (transform) => { return [ transform.mode, transform.clampMode, transform.valueMode, transform.componentCount, transform.componentIndex, transform.stride, transform.offset, transform.domainMin, transform.domainMax, transform.clampMin, transform.clampMax, transform.percentileLow, transform.percentileHigh, transform.logBase, transform.symlogLinThresh, transform.gamma, transform.invert ? 1 : 0 ].join("|"); }; var toEmitterSource = (source) => { if (source instanceof NodeLink) return source; const maybe = source; if (maybe.nodelink instanceof NodeLink) return maybe.nodelink; return source; }; var subscribeSource = (source, callback) => { const emitter = toEmitterSource(source); if (typeof emitter.onVisualChange !== "function") return null; return emitter.onVisualChange(() => callback()) ?? null; }; var resolveSource = (source, strictParity) => { if (source instanceof PointCloud) { const transform2 = normalizeScaleTransform(source.scaleTransform); if (source.colormap === "custom") { const stops = source.colormapStops.slice(); return { transform: transform2, signature: `pointcloud|custom|${serializeTransform(transform2)}|${JSON.stringify(stops)}`, sample: (t) => sampleCustomStops(t, stops) }; } const colormap2 = source.getColormapForBinding(); if (strictParity && !colormap2.canSampleCPU) throw new Error("LegendLayer: bound point cloud colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform: transform2, signature: `pointcloud|cm:${colormap2.id}|f:${colormap2.filter}|w:${colormap2.width}|${serializeTransform(transform2)}`, sample: (t) => colormap2.sampleCPU(t) }; } if (source instanceof GlyphField) { const transform2 = normalizeScaleTransform(source.scaleTransform); if (source.colorMode === "scalar" && source.colormap === "custom") { const stops = source.colormapStops.slice(); return { transform: transform2, signature: `glyphfield|custom|${serializeTransform(transform2)}|${JSON.stringify(stops)}`, sample: (t) => sampleCustomStops(t, stops) }; } const colormap2 = source.getColormapForBinding(); if (strictParity && !colormap2.canSampleCPU) throw new Error("LegendLayer: bound glyph colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform: transform2, signature: `glyphfield|cm:${colormap2.id}|f:${colormap2.filter}|w:${colormap2.width}|${serializeTransform(transform2)}`, sample: (t) => colormap2.sampleCPU(t) }; } if (source instanceof LatticeSpace) { const transform2 = normalizeScaleTransform(source.scaleTransform); if (source.colorMode === "scalar" && source.colormap === "custom") { const stops = source.colormapStops.slice(); return { transform: transform2, signature: `latticespace|custom|${serializeTransform(transform2)}|${JSON.stringify(stops)}`, sample: (t) => sampleCustomStops(t, stops) }; } const colormap2 = source.getColormapForBinding(); if (strictParity && !colormap2.canSampleCPU) throw new Error("LegendLayer: bound latticespace colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform: transform2, signature: `latticespace|cm:${colormap2.id}|f:${colormap2.filter}|w:${colormap2.width}|${serializeTransform(transform2)}`, sample: (t) => colormap2.sampleCPU(t) }; } if (source instanceof NodeLink || source.nodelink instanceof NodeLink) { const obj = source instanceof NodeLink ? source : source.nodelink; const component = source instanceof NodeLink ? "node" : source.component ?? "node"; const transform2 = normalizeScaleTransform(component === "edge" ? obj.edgeScaleTransform : obj.nodeScaleTransform); const colormap2 = component === "edge" ? obj.edgeColormap : obj.nodeColormap; const stops = component === "edge" ? obj.edgeColormapStops : obj.nodeColormapStops; if (typeof colormap2 === "string" && colormap2 === "custom") return { transform: transform2, signature: `nodelink|${component}|custom|${serializeTransform(transform2)}|${JSON.stringify(stops)}`, sample: (t) => sampleCustomStops(t, stops) }; const resolved = component === "edge" ? obj.getEdgeColormapForBinding() : obj.getNodeColormapForBinding(); if (strictParity && !resolved.canSampleCPU) throw new Error("LegendLayer: bound nodelink colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform: transform2, signature: `nodelink|${component}|cm:${resolved.id}|f:${resolved.filter}|w:${resolved.width}|${serializeTransform(transform2)}`, sample: (t) => resolved.sampleCPU(t) }; } if (source instanceof DataMaterial) { const transform2 = normalizeScaleTransform(source.scaleTransform); const colormap2 = source.getColormapForBinding(); if (strictParity && !colormap2.canSampleCPU) throw new Error("LegendLayer: bound data-material colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform: transform2, signature: `datamaterial|cm:${colormap2.id}|f:${colormap2.filter}|w:${colormap2.width}|${serializeTransform(transform2)}`, sample: (t) => colormap2.sampleCPU(t) }; } const explicit = source; const transform = normalizeScaleTransform(explicit.scaleTransform); if (explicit.colormapStops && explicit.colormapStops.length >= 2) { const stops = explicit.colormapStops.slice(); return { transform, signature: `explicit|stops|${serializeTransform(transform)}|${JSON.stringify(stops)}`, sample: (t) => sampleCustomStops(t, stops) }; } const colormap = typeof explicit.colormap === "string" ? Colormap.builtin(explicit.colormap) : explicit.colormap; if (strictParity && !colormap.canSampleCPU) throw new Error("LegendLayer: explicit colormap is GPU-only and cannot be sampled on CPU in strict parity mode."); return { transform, signature: `explicit|cm:${colormap.id}|f:${colormap.filter}|w:${colormap.width}|${serializeTransform(transform)}`, sample: (t) => colormap.sampleCPU(t) }; }; var LegendLayer = class { id; source; strictParity; widthPx; heightPx; tickCount; font; formatValue; title; subtitle; units; orientation; anchor; className; style; _system = null; unsubscribeSource = null; sourceDirty = true; lastSignature = null; container = null; titleEl = null; subtitleEl = null; unitsEl = null; gradientWrap = null; gradientCanvas = null; gradientCtx = null; messageEl = null; tickMarkPool = null; tickLabelPool = null; constructor(desc) { this.id = desc.id ?? "overlay-legend"; this.source = desc.source; this.strictParity = desc.strictParity ?? true; this.widthPx = Math.max(8, Math.round(desc.widthPx ?? 26)); this.heightPx = Math.max(8, Math.round(desc.heightPx ?? 240)); this.tickCount = Math.max(2, Math.round(desc.tickCount ?? 7)); this.font = desc.font ?? "11px monospace"; this.formatValue = desc.formatValue ?? formatDefault; this.title = desc.title ?? "Legend"; this.anchor = desc.anchor ?? { kind: "screen", corner: "top-right", offsetPx: [-16, 16] }; this.subtitle = desc.subtitle ?? ""; this.units = desc.units ?? ""; this.orientation = desc.orientation ?? "vertical"; this.className = desc.className ?? ""; this.style = desc.style ?? {}; } setSystem(system) { this._system = system; } setSource(source) { if (source === this.source) return; this.source = source; if (this.container) this.bindSource(source); this.sourceDirty = true; this._system?.invalidate("scale"); } setOrientation(orientation) { if (orientation === this.orientation) return this; this.orientation = orientation; this.presentationChanged(); return this; } setTitle(title) { if (title === this.title) return this; this.title = title; if (this.titleEl) this.titleEl.textContent = title; return this.presentationChanged(); } setSubtitle(subtitle) { if (subtitle === this.subtitle) return this; this.subtitle = subtitle; if (this.subtitleEl) { this.subtitleEl.textContent = subtitle; this.subtitleEl.style.display = subtitle ? "" : "none"; } return this.presentationChanged(); } setUnits(units) { if (units === this.units) return this; this.units = units; if (this.unitsEl) { this.unitsEl.textContent = units; this.unitsEl.style.display = units ? "" : "none"; } return this.presentationChanged(); } setAnchor(anchor) { if (JSON.stringify(anchor) === JSON.stringify(this.anchor)) return this; this.anchor = anchor; return this.presentationChanged(); } setGradientSize(widthPx, heightPx) { const width = Math.max(8, Math.round(widthPx)); const height = Math.max(8, Math.round(heightPx)); if (width === this.widthPx && height === this.heightPx) return this; this.widthPx = width; this.heightPx = height; return this.presentationChanged(); } setTickPresentation(tickCount, formatValue = this.formatValue, font = this.font) { const count = Math.max(2, Math.round(tickCount)); if (count === this.tickCount && formatValue === this.formatValue && font === this.font) return this; this.tickCount = count; this.formatValue = formatValue; this.font = font; this.rebuildTickPools(); return this.presentationChanged(); } setClassName(className) { if (className === this.className) return this; this.className = className; if (this.container) this.container.className = `wasmgpu-overlay-legend${className ? ` ${className}` : ""}`; return this.presentationChanged(); } setStyle(style) { if (styleEquals3(style, this.style)) return this; const previous = this.style; this.style = style; this.applyCurrentStyles(previous); return this.presentationChanged(); } presentationChanged() { this.lastSignature = null; this.sourceDirty = true; this.layoutElements(); this._system?.invalidate("layout"); return this; } attach(root) { if (this.container) this.detach(); const container = document.createElement("div"); container.className = `wasmgpu-overlay-legend${this.className ? ` ${this.className}` : ""}`; container.style.position = "absolute"; container.style.pointerEvents = "none"; container.style.padding = "8px"; container.style.border = "1px solid rgba(190, 215, 255, 0.35)"; container.style.background = "rgba(7, 13, 24, 0.78)"; container.style.borderRadius = "6px"; container.style.color = "#e3eeff"; container.style.font = this.font; applyStyle3(container, this.style.container); container.style.position = "absolute"; container.style.pointerEvents = "none"; root.appendChild(container); this.container = container; const titleEl = document.createElement("div"); titleEl.className = "wasmgpu-overlay-legend-title"; titleEl.textContent = this.title; titleEl.style.marginBottom = "6px"; titleEl.style.font = this.font; applyStyle3(titleEl, this.style.title); container.appendChild(titleEl); this.titleEl = titleEl; const subtitleEl = document.createElement("div"); subtitleEl.className = "wasmgpu-overlay-legend-subtitle"; subtitleEl.textContent = this.subtitle; subtitleEl.style.marginBottom = "6px"; subtitleEl.style.opacity = "0.82"; subtitleEl.style.display = this.subtitle ? "" : "none"; applyStyle3(subtitleEl, this.style.subtitle); container.appendChild(subtitleEl); this.subtitleEl = subtitleEl; const gradientWrap = document.createElement("div"); gradientWrap.className = "wasmgpu-overlay-legend-gradient-wrap"; gradientWrap.style.position = "relative"; gradientWrap.style.width = `${this.widthPx + 64}px`; gradientWrap.style.height = `${this.heightPx}px`; container.appendChild(gradientWrap); this.gradientWrap = gradientWrap; const canvas = document.createElement("canvas"); canvas.width = this.widthPx; canvas.height = this.heightPx; canvas.style.position = "absolute"; canvas.style.left = "0"; canvas.style.top = "0"; canvas.style.width = `${this.widthPx}px`; canvas.style.height = `${this.heightPx}px`; canvas.style.border = "1px solid rgba(180, 210, 255, 0.35)"; canvas.style.borderRadius = "2px"; canvas.className = "wasmgpu-overlay-legend-gradient"; applyStyle3(canvas, this.style.gradient); gradientWrap.appendChild(canvas); this.gradientCanvas = canvas; this.gradientCtx = canvas.getContext("2d", { willReadFrequently: true }); const messageEl = document.createElement("div"); messageEl.style.position = "absolute"; messageEl.style.left = "0"; messageEl.style.top = `${this.heightPx + 6}px`; messageEl.style.color = "rgba(255, 191, 191, 0.95)"; messageEl.style.maxWidth = `${this.widthPx + 64}px`; gradientWrap.appendChild(messageEl); this.messageEl = messageEl; const unitsEl = document.createElement("div"); unitsEl.className = "wasmgpu-overlay-legend-units"; unitsEl.textContent = this.units; unitsEl.style.marginTop = "6px"; unitsEl.style.opacity = "0.88"; unitsEl.style.display = this.units ? "" : "none"; applyStyle3(unitsEl, this.style.units); container.appendChild(unitsEl); this.unitsEl = unitsEl; this.tickMarkPool = new DOMNodePool(gradientWrap, () => { const el = document.createElement("div"); el.style.position = "absolute"; el.className = "wasmgpu-overlay-legend-tick-mark"; return el; }, this.tickCount); this.tickLabelPool = new DOMNodePool(gradientWrap, () => { const el = document.createElement("div"); el.style.position = "absolute"; el.style.font = this.font; el.style.color = "#e3eeff"; el.className = "wasmgpu-overlay-legend-tick-label"; return el; }, this.tickCount); this.bindSource(this.source); this.sourceDirty = true; this.layoutElements(); } detach() { this.unsubscribeSource?.(); this.unsubscribeSource = null; this.tickMarkPool?.clear(true); this.tickLabelPool?.clear(true); this.tickMarkPool = null; this.tickLabelPool = null; this.container?.remove(); this.container = null; this.titleEl = null; this.subtitleEl = null; this.unitsEl = null; this.gradientWrap = null; this.gradientCanvas = null; this.gradientCtx = null; this.messageEl = null; } update(ctx) { if (!this.container) return; this.positionContainer(ctx); const reasonChanged = ctx.reasons.has("scale") || ctx.reasons.has("colormap") || ctx.reasons.has("manual") || ctx.reasons.has("viewport") || ctx.reasons.has("layout"); if (!reasonChanged && !this.sourceDirty) return; this.sourceDirty = false; this.renderLegend(ctx.dpr); } positionContainer(ctx) { if (!this.container) return; const [x, y] = resolveScreenAnchorPoint(this.anchor, ctx.width, ctx.height); const corner = this.anchor?.corner ?? "top-right"; const translateX = this.anchor.x === void 0 && corner.includes("right") ? "-100%" : "0"; const translateY = this.anchor.y === void 0 && corner.includes("bottom") ? "-100%" : "0"; this.container.style.left = `${x}px`; this.container.style.top = `${y}px`; this.container.style.transform = `translate(${translateX}, ${translateY})`; } bindSource(source) { this.unsubscribeSource?.(); this.unsubscribeSource = subscribeSource(source, () => { this.sourceDirty = true; this._system?.invalidate("scale"); }); } renderLegend(dpr) { if (!this.gradientCanvas || !this.gradientCtx || !this.tickMarkPool || !this.tickLabelPool) return; try { const resolved = resolveSource(this.source, this.strictParity); const signature = `${resolved.signature}|${this.orientation}|${this.widthPx}x${this.heightPx}|dpr:${dpr}|ticks:${this.tickCount}`; if (signature !== this.lastSignature) { this.lastSignature = signature; this.layoutElements(dpr); this.renderGradient(resolved); this.renderTicks(resolved); } if (this.messageEl) this.messageEl.textContent = ""; } catch (error) { if (this.messageEl) this.messageEl.textContent = `${error instanceof Error ? error.message : String(error)}`; } } renderGradient(resolved) { if (!this.gradientCtx || !this.gradientCanvas) return; const w = this.gradientCanvas.width; const h = this.gradientCanvas.height; const image = this.gradientCtx.createImageData(w, h); const writeColor = (x, y, c) => { const r = Math.max(0, Math.min(255, Math.round(c[0] * 255))); const g = Math.max(0, Math.min(255, Math.round(c[1] * 255))); const b = Math.max(0, Math.min(255, Math.round(c[2] * 255))); const a = Math.max(0, Math.min(255, Math.round(c[3] * 255))); const o = (y * w + x) * 4; image.data[o + 0] = r; image.data[o + 1] = g; image.data[o + 2] = b; image.data[o + 3] = a; }; if (this.orientation === "vertical") { for (let y = 0; y < h; y++) { const c = resolved.sample(1 - y / Math.max(1, h - 1)); for (let x = 0; x < w; x++) writeColor(x, y, c); } } else { for (let x = 0; x < w; x++) { const c = resolved.sample(x / Math.max(1, w - 1)); for (let y = 0; y < h; y++) writeColor(x, y, c); } } this.gradientCtx.putImageData(image, 0, 0); } renderTicks(resolved) { if (!this.tickMarkPool || !this.tickLabelPool || !this.gradientCanvas) return; this.tickMarkPool.beginFrame(); this.tickLabelPool.beginFrame(); const vertical = this.orientation === "vertical"; const tickData = Array.from({ length: this.tickCount }, (_, i) => { const alpha = i / Math.max(1, this.tickCount - 1); const t = vertical ? 1 - alpha : alpha; const value = invertScaleTransformCPU(clamp01(t), resolved.transform); return { alpha, text: this.formatValue(value) }; }); const visibleLabels = /* @__PURE__ */ new Set(); if (vertical) for (let i = 0; i < tickData.length; i++) visibleLabels.add(i); else { const fontPx = Number.parseFloat(this.style.tickLabel?.fontSize ?? this.style.tickLabel?.font ?? this.font) || 11; const letterSpacing = Number.parseFloat(this.style.tickLabel?.letterSpacing ?? "0") || 0; const widths = tickData.map(({ text }) => Math.max(fontPx, text.length * fontPx * 0.62 + Math.max(0, text.length - 1) * letterSpacing)); visibleLabels.add(0); let lastRight = widths[0]; const finalLeft = this.widthPx - widths[widths.length - 1]; const keepFinal = lastRight + 4 <= finalLeft; for (let i = 1; i < tickData.length - 1; i++) { const center = tickData[i].alpha * this.widthPx; const left = center - widths[i] * 0.5; const right = center + widths[i] * 0.5; if (left >= lastRight + 4 && (!keepFinal || right <= finalLeft - 4)) { visibleLabels.add(i); lastRight = right; } } if (keepFinal) visibleLabels.add(tickData.length - 1); } for (let i = 0; i < this.tickCount; i++) { const { alpha, text } = tickData[i]; const x = alpha * this.widthPx; const y = alpha * this.heightPx; const mark = this.tickMarkPool.acquire(); mark.style.background = "#dce9ff"; applyStyle3(mark, this.style.tickMark); mark.style.position = "absolute"; mark.style.left = `${vertical ? this.widthPx + 4 : x}px`; mark.style.top = `${vertical ? y : this.heightPx + 4}px`; mark.style.width = vertical ? "8px" : "1px"; mark.style.height = vertical ? "1px" : "8px"; if (!visibleLabels.has(i)) continue; const label = this.tickLabelPool.acquire(); label.style.font = this.font; applyStyle3(label, this.style.tickLabel); label.style.position = "absolute"; label.style.left = `${vertical ? this.widthPx + 16 : x}px`; label.style.top = `${vertical ? y - 6 : this.heightPx + 16}px`; label.style.transform = vertical ? "" : i === 0 ? "" : i === this.tickCount - 1 ? "translateX(-100%)" : "translateX(-50%)"; label.textContent = text; } this.tickMarkPool.endFrame(); this.tickLabelPool.endFrame(); } layoutElements(dpr = 1) { if (!this.gradientCanvas || !this.gradientWrap) return; const scale = Math.max(1, dpr); this.gradientCanvas.width = Math.max(1, Math.round(this.widthPx * scale)); this.gradientCanvas.height = Math.max(1, Math.round(this.heightPx * scale)); this.gradientCanvas.style.width = `${this.widthPx}px`; this.gradientCanvas.style.height = `${this.heightPx}px`; this.gradientWrap.style.width = `${this.orientation === "vertical" ? this.widthPx + 76 : this.widthPx}px`; this.gradientWrap.style.height = `${this.orientation === "vertical" ? this.heightPx : this.heightPx + 36}px`; if (this.messageEl) { this.messageEl.style.top = `${this.heightPx + 34}px`; this.messageEl.style.maxWidth = `${Math.max(this.widthPx, 100)}px`; } } rebuildTickPools() { if (!this.gradientWrap) return; this.tickMarkPool?.clear(true); this.tickLabelPool?.clear(true); this.tickMarkPool = new DOMNodePool(this.gradientWrap, () => { const el = document.createElement("div"); el.className = "wasmgpu-overlay-legend-tick-mark"; el.style.position = "absolute"; return el; }, this.tickCount); this.tickLabelPool = new DOMNodePool(this.gradientWrap, () => { const el = document.createElement("div"); el.className = "wasmgpu-overlay-legend-tick-label"; el.style.position = "absolute"; el.style.font = this.font; el.style.color = "#e3eeff"; return el; }, this.tickCount); } applyCurrentStyles(previous) { clearStyle3(this.container, previous.container); if (this.container) { this.container.style.padding = "8px"; this.container.style.border = "1px solid rgba(190, 215, 255, 0.35)"; this.container.style.background = "rgba(7, 13, 24, 0.78)"; this.container.style.borderRadius = "6px"; this.container.style.color = "#e3eeff"; this.container.style.font = this.font; } applyStyle3(this.container, this.style.container); if (this.container) { this.container.style.position = "absolute"; this.container.style.pointerEvents = "none"; } clearStyle3(this.titleEl, previous.title); if (this.titleEl) { this.titleEl.style.marginBottom = "6px"; this.titleEl.style.font = this.font; } applyStyle3(this.titleEl, this.style.title); clearStyle3(this.subtitleEl, previous.subtitle); if (this.subtitleEl) { this.subtitleEl.style.marginBottom = "6px"; this.subtitleEl.style.opacity = "0.82"; } applyStyle3(this.subtitleEl, this.style.subtitle); clearStyle3(this.gradientCanvas, previous.gradient); if (this.gradientCanvas) { this.gradientCanvas.style.border = "1px solid rgba(180, 210, 255, 0.35)"; this.gradientCanvas.style.borderRadius = "2px"; } applyStyle3(this.gradientCanvas, this.style.gradient); clearStyle3(this.unitsEl, previous.units); if (this.unitsEl) { this.unitsEl.style.marginTop = "6px"; this.unitsEl.style.opacity = "0.88"; } applyStyle3(this.unitsEl, this.style.units); if (this.container) { for (const node of this.container.querySelectorAll(".wasmgpu-overlay-legend-tick-mark")) { clearStyle3(node, previous.tickMark); applyStyle3(node, this.style.tickMark); } for (const node of this.container.querySelectorAll(".wasmgpu-overlay-legend-tick-label")) { clearStyle3(node, previous.tickLabel); applyStyle3(node, this.style.tickLabel); } } } }; // typescript/overlay/annotation/types.ts var AnnotationMode = { Idle: "idle", Marker: "marker", Distance: "distance", Angle: "angle" }; var AnnotationKind = { Marker: "marker", Distance: "distance", Angle: "angle" }; var AnnotationAngleUnit = { Degrees: "deg", Radians: "rad" }; var cloneAnnotationColor = (color) => [color[0], color[1], color[2], color[3]]; var cloneAnnotationVec3 = (v) => [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0]; var clonePickAttributes = (attributes) => { if (!attributes) return null; return { scalar: attributes.scalar ?? null, vector: attributes.vector ? [attributes.vector[0], attributes.vector[1], attributes.vector[2], attributes.vector[3]] : null, packedPoint: attributes.packedPoint ? [attributes.packedPoint[0], attributes.packedPoint[1], attributes.packedPoint[2], attributes.packedPoint[3]] : null, component: attributes.component ?? null, componentIndex: attributes.componentIndex ?? null, color: attributes.color ? [attributes.color[0], attributes.color[1], attributes.color[2], attributes.color[3]] : null, edgeEndpoints: attributes.edgeEndpoints ? [attributes.edgeEndpoints[0], attributes.edgeEndpoints[1]] : null, edgePositions: attributes.edgePositions ? [ attributes.edgePositions[0], attributes.edgePositions[1], attributes.edgePositions[2], attributes.edgePositions[3], attributes.edgePositions[4], attributes.edgePositions[5] ] : null }; }; var clonePickPayload = (pick) => { if (!pick) return null; return { kind: pick.kind, objectId: pick.objectId, elementIndex: pick.elementIndex, ndIndex: pick.ndIndex ? pick.ndIndex.slice() : null, attributes: clonePickAttributes(pick.attributes) }; }; var cloneAnnotationAnchor = (anchor) => { return { position: cloneAnnotationVec3(anchor.position), pick: clonePickPayload(anchor.pick) }; }; var annotationAnchorFromHit = (hit) => { return { position: cloneAnnotationVec3(hit.worldPosition), pick: { kind: hit.kind, objectId: hit.objectId, elementIndex: hit.elementIndex, ndIndex: hit.ndIndex ? hit.ndIndex.slice() : null, attributes: clonePickAttributes(hit.attributes) } }; }; var colorToCssRgba = (color) => { const r = Math.max(0, Math.min(255, Math.round(color[0] * 255))); const g = Math.max(0, Math.min(255, Math.round(color[1] * 255))); const b = Math.max(0, Math.min(255, Math.round(color[2] * 255))); const a = Math.max(0, Math.min(1, color[3])); return `rgba(${r}, ${g}, ${b}, ${a})`; }; // typescript/overlay/annotation/units.ts var METRIC_PREFIXES = [ { exponent: -12, symbol: "p", factor: 1e-12 }, { exponent: -9, symbol: "n", factor: 1e-9 }, { exponent: -6, symbol: "u", factor: 1e-6 }, { exponent: -3, symbol: "m", factor: 1e-3 }, { exponent: 0, symbol: "", factor: 1 }, { exponent: 3, symbol: "k", factor: 1e3 }, { exponent: 6, symbol: "M", factor: 1e6 }, { exponent: 9, symbol: "G", factor: 1e9 }, { exponent: 12, symbol: "T", factor: 1e12 } ]; var trimFixed = (text) => { if (!text.includes(".")) return text; return text.replace(/(\.\d*?[1-9])0+$/g, "$1").replace(/\.0+$/g, ""); }; var formatFiniteNumber = (value, decimals) => { if (!Number.isFinite(value)) return "nan"; const abs = Math.abs(value); const digits = clampInt(decimals, 0, 12); if (abs >= 1e7 || abs > 0 && abs < 1e-5) return value.toExponential(Math.max(1, Math.min(6, digits))); return trimFixed(value.toFixed(digits)); }; var resolveAnnotationUnits = (desc = {}) => { const worldUnitsPerUnit = Number.isFinite(desc.worldUnitsPerUnit) && desc.worldUnitsPerUnit > 0 ? desc.worldUnitsPerUnit : 1; const symbol = typeof desc.symbol === "string" ? desc.symbol : "wu"; const decimals = clampInt(desc.decimals ?? 3, 0, 12); const autoMetric = !!desc.autoMetric; const angleUnit = desc.angleUnit ?? AnnotationAngleUnit.Degrees; const angleDecimals = clampInt(desc.angleDecimals ?? 2, 0, 12); return { worldUnitsPerUnit, symbol, decimals, autoMetric, angleUnit, angleDecimals }; }; var pickMetricPrefix = (value) => { const abs = Math.abs(value); if (!Number.isFinite(abs) || abs <= 0) return METRIC_PREFIXES[4]; const exponent = clampInt(Math.floor(Math.log10(abs) / 3) * 3, METRIC_PREFIXES[0].exponent, METRIC_PREFIXES[METRIC_PREFIXES.length - 1].exponent); for (let i = 0; i < METRIC_PREFIXES.length; i++) if (METRIC_PREFIXES[i].exponent === exponent) return METRIC_PREFIXES[i]; return METRIC_PREFIXES[4]; }; var formatDistanceWorld = (distanceWorld, desc = {}) => { const units = resolveAnnotationUnits(desc); if (!Number.isFinite(distanceWorld)) return { worldDistance: distanceWorld, value: Number.NaN, unitSymbol: units.symbol, text: `nan ${units.symbol}` }; const baseValue = distanceWorld / units.worldUnitsPerUnit; if (!units.autoMetric) { const text2 = `${formatFiniteNumber(baseValue, units.decimals)} ${units.symbol}`.trim(); return { worldDistance: distanceWorld, value: baseValue, unitSymbol: units.symbol, text: text2 }; } const prefix = pickMetricPrefix(baseValue); const scaled = baseValue / prefix.factor; const unitSymbol = `${prefix.symbol}${units.symbol}`; const text = `${formatFiniteNumber(scaled, units.decimals)} ${unitSymbol}`.trim(); return { worldDistance: distanceWorld, value: scaled, unitSymbol, text }; }; var formatAngleRadians = (angleRadians, desc = {}) => { const units = resolveAnnotationUnits(desc); if (!Number.isFinite(angleRadians)) return { radians: angleRadians, value: Number.NaN, unitSymbol: units.angleUnit, text: `nan ${units.angleUnit}` }; if (units.angleUnit === AnnotationAngleUnit.Radians) { const text2 = `${formatFiniteNumber(angleRadians, units.angleDecimals)} rad`; return { radians: angleRadians, value: angleRadians, unitSymbol: "rad", text: text2 }; } const value = angleRadians * (180 / Math.PI); const text = `${formatFiniteNumber(value, units.angleDecimals)} deg`; return { radians: angleRadians, value, unitSymbol: "deg", text }; }; var formatWorldVector = (v, decimals = 5) => { if (!v) return "null"; return `[${formatFiniteNumber(v[0] ?? Number.NaN, decimals)}, ${formatFiniteNumber(v[1] ?? Number.NaN, decimals)}, ${formatFiniteNumber(v[2] ?? Number.NaN, decimals)}]`; }; // typescript/overlay/annotation/labelLayer.ts var formatNdIndex = (ndIndex) => { if (!ndIndex || ndIndex.length === 0) return "null"; return `[${ndIndex.join(", ")}]`; }; var formatAttributes = (attributes) => { if (!attributes) return ["attributes: null"]; const lines = []; if (attributes.scalar !== void 0 && attributes.scalar !== null) lines.push(`scalar: ${formatFiniteNumber(attributes.scalar, 6)}`); if (attributes.vector) lines.push(`vector: [${formatFiniteNumber(attributes.vector[0], 4)}, ${formatFiniteNumber(attributes.vector[1], 4)}, ${formatFiniteNumber(attributes.vector[2], 4)}, ${formatFiniteNumber(attributes.vector[3], 4)}]`); if (attributes.packedPoint) lines.push(`packedPoint: [${formatFiniteNumber(attributes.packedPoint[0], 4)}, ${formatFiniteNumber(attributes.packedPoint[1], 4)}, ${formatFiniteNumber(attributes.packedPoint[2], 4)}, ${formatFiniteNumber(attributes.packedPoint[3], 4)}]`); if (attributes.component) lines.push(`component: ${attributes.component}`); if (attributes.componentIndex !== void 0 && attributes.componentIndex !== null) lines.push(`componentIndex: ${attributes.componentIndex}`); if (attributes.color) lines.push(`color: [${formatFiniteNumber(attributes.color[0], 4)}, ${formatFiniteNumber(attributes.color[1], 4)}, ${formatFiniteNumber(attributes.color[2], 4)}, ${formatFiniteNumber(attributes.color[3], 4)}]`); if (attributes.edgeEndpoints) lines.push(`edgeEndpoints: [${attributes.edgeEndpoints[0]}, ${attributes.edgeEndpoints[1]}]`); if (attributes.edgePositions) lines.push(`edgePositions: [${formatFiniteNumber(attributes.edgePositions[0], 4)}, ${formatFiniteNumber(attributes.edgePositions[1], 4)}, ${formatFiniteNumber(attributes.edgePositions[2], 4)}, ${formatFiniteNumber(attributes.edgePositions[3], 4)}, ${formatFiniteNumber(attributes.edgePositions[4], 4)}, ${formatFiniteNumber(attributes.edgePositions[5], 4)}]`); if (lines.length === 0) lines.push("attributes: {}"); return lines; }; var cloneProbeAttributes = (attributes) => { if (!attributes) return null; return { ...attributes, vector: attributes.vector ? [...attributes.vector] : null, packedPoint: attributes.packedPoint ? [...attributes.packedPoint] : null, color: attributes.color ? [...attributes.color] : null, edgeEndpoints: attributes.edgeEndpoints ? [...attributes.edgeEndpoints] : null, edgePositions: attributes.edgePositions ? [...attributes.edgePositions] : null }; }; var formatProbe = (title, readout) => { if (!readout || !readout.hit) return `${title}: miss`; return [ `${title}: hit`, `kind: ${readout.kind}`, `objectId: ${readout.objectId}`, `elementIndex: ${readout.elementIndex}`, `world: ${formatWorldVector(readout.worldPosition, 5)}`, `ndIndex: ${formatNdIndex(readout.ndIndex)}`, ...formatAttributes(readout.attributes) ].join("\n"); }; var formatSelection = (title, readout) => { if (!readout || !readout.hit) return `${title}: miss`; const base = formatProbe(title, readout).split("\n"); if (readout.annotationId) base.push(`annotationId: ${readout.annotationId}`); if (readout.annotationKind) base.push(`annotationKind: ${readout.annotationKind}`); if (readout.anchorRole) base.push(`anchorRole: ${readout.anchorRole}`); return base.join("\n"); }; var AnnotationLabelLayer = class { id; maxLabels; font; labelOffsetPx; readoutAnchor; readoutWidthPx; container = null; labelPool = null; hoverReadoutEl = null; selectionReadoutEl = null; _system = null; entries = []; entriesRevision = -1; entriesDirty = true; hoverReadout = null; selectionReadout = null; readoutDirty = true; hoverTextCache = ""; selectionTextCache = ""; constructor(desc = {}) { this.id = desc.id ?? "annotation-label-layer"; this.maxLabels = Math.max(1, Math.round(desc.maxLabels ?? 256)); this.font = desc.font ?? "11px monospace"; this.labelOffsetPx = desc.labelOffsetPx ?? [8, -8]; this.readoutAnchor = desc.readoutAnchor ?? { kind: "screen", corner: "top-left", offsetPx: [12, 12] }; this.readoutWidthPx = Math.max(180, Math.round(desc.readoutWidthPx ?? 330)); } get pooledNodeCount() { return this.labelPool?.size ?? 0; } setSystem(system) { this._system = system; } attach(root) { const container = document.createElement("div"); container.style.position = "absolute"; container.style.inset = "0"; container.style.pointerEvents = "none"; root.appendChild(container); this.container = container; this.labelPool = new DOMNodePool(container, () => { const node = document.createElement("div"); node.style.position = "absolute"; node.style.whiteSpace = "nowrap"; node.style.font = this.font; node.style.textShadow = "0 1px 1px rgba(0, 0, 0, 0.8)"; node.style.willChange = "transform,left,top"; return node; }, this.maxLabels); this.hoverReadoutEl = document.createElement("div"); this.selectionReadoutEl = document.createElement("div"); for (const node of [this.hoverReadoutEl, this.selectionReadoutEl]) { node.style.position = "absolute"; node.style.pointerEvents = "none"; node.style.whiteSpace = "pre-line"; node.style.font = this.font; node.style.color = "#dce9ff"; node.style.padding = "6px 7px"; node.style.border = "1px solid rgba(180, 210, 255, 0.28)"; node.style.background = "rgba(5, 11, 20, 0.72)"; node.style.borderRadius = "4px"; node.style.minWidth = `${Math.floor(this.readoutWidthPx * 0.5)}px`; node.style.maxWidth = `${this.readoutWidthPx}px`; container.appendChild(node); } this.entriesDirty = true; this.readoutDirty = true; } detach() { this.labelPool?.clear(true); this.labelPool = null; this.hoverReadoutEl = null; this.selectionReadoutEl = null; this.container?.remove(); this.container = null; this.hoverTextCache = ""; this.selectionTextCache = ""; } setEntries(entries, revision) { this.entries = new Array(Math.min(this.maxLabels, entries.length)); for (let i = 0; i < this.entries.length; i++) { const src = entries[i]; this.entries[i] = { key: src.key, text: src.text, color: src.color, position: [src.position[0], src.position[1], src.position[2]] }; } if (revision !== this.entriesRevision) { this.entriesRevision = revision; this.entriesDirty = true; } this._system?.invalidate("manual"); } setHoverReadout(readout) { this.hoverReadout = readout ? { ...readout, worldPosition: readout.worldPosition ? [readout.worldPosition[0], readout.worldPosition[1], readout.worldPosition[2]] : null, ndIndex: readout.ndIndex ? readout.ndIndex.slice() : null, attributes: cloneProbeAttributes(readout.attributes) } : null; this.readoutDirty = true; this._system?.invalidate("manual"); } setSelectionReadout(readout) { this.selectionReadout = readout ? { ...readout, worldPosition: readout.worldPosition ? [readout.worldPosition[0], readout.worldPosition[1], readout.worldPosition[2]] : null, ndIndex: readout.ndIndex ? readout.ndIndex.slice() : null, attributes: cloneProbeAttributes(readout.attributes) } : null; this.readoutDirty = true; this._system?.invalidate("manual"); } update(ctx) { if (!this.container || !this.labelPool) return; const positionReasons = ctx.reasons.has("camera") || ctx.reasons.has("viewport") || ctx.reasons.has("layout") || ctx.reasons.has("manual") || ctx.reasons.has("interaction"); if (positionReasons || this.entriesDirty) this.renderLabels(ctx); if (this.readoutDirty || ctx.reasons.has("viewport") || ctx.reasons.has("layout") || ctx.reasons.has("manual")) this.renderReadouts(ctx); } renderLabels(ctx) { if (!this.labelPool) return; this.labelPool.beginFrame(); for (let i = 0; i < this.entries.length; i++) { const entry = this.entries[i]; const projected = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, entry.position); if (!projected || !projected.visible) continue; const node = this.labelPool.acquire(); if (this.entriesDirty || node.dataset.annotationKey !== entry.key) { node.dataset.annotationKey = entry.key; node.style.color = entry.color; node.textContent = entry.text; } node.style.left = `${projected.x + this.labelOffsetPx[0]}px`; node.style.top = `${projected.y + this.labelOffsetPx[1]}px`; } this.labelPool.endFrame(); this.entriesDirty = false; } renderReadouts(ctx) { if (!this.hoverReadoutEl || !this.selectionReadoutEl) return; const [x, y] = resolveScreenAnchorPoint(this.readoutAnchor, ctx.width, ctx.height); this.hoverReadoutEl.style.left = `${x}px`; this.hoverReadoutEl.style.top = `${y}px`; this.selectionReadoutEl.style.left = `${x}px`; this.selectionReadoutEl.style.top = `${y + 122}px`; const hoverText = formatProbe("Hover", this.hoverReadout); const selectionText = formatSelection("Selection", this.selectionReadout); if (hoverText !== this.hoverTextCache) { this.hoverTextCache = hoverText; this.hoverReadoutEl.textContent = hoverText; } if (selectionText !== this.selectionTextCache) { this.selectionTextCache = selectionText; this.selectionReadoutEl.textContent = selectionText; } this.readoutDirty = false; } }; // typescript/overlay/annotation/markerRenderer.ts var resolveMarkerScale = (input) => { if (Array.isArray(input)) return [Math.max(1e-6, input[0] ?? 1), Math.max(1e-6, input[1] ?? 1), Math.max(1e-6, input[2] ?? 1)]; const s = Math.max(1e-6, input ?? 0.16); return [s, s, s]; }; var pushMarkerInstance = (out, id, kind, role, anchor, color) => { out.push({ key: `${id}:${role}`, annotationId: id, annotationKind: kind, role, anchor: cloneAnnotationAnchor(anchor), color: cloneAnnotationColor(color) }); }; var collectAnnotationMarkerInstances = (records) => { const out = []; for (let i = 0; i < records.length; i++) { const record = records[i]; if (!record.visible) continue; if (record.kind === "marker") { pushMarkerInstance(out, record.id, record.kind, "marker", record.anchor, record.color); continue; } if (record.kind === "distance") { pushMarkerInstance(out, record.id, record.kind, "start", record.start, record.color); pushMarkerInstance(out, record.id, record.kind, "end", record.end, record.color); continue; } pushMarkerInstance(out, record.id, record.kind, "a", record.a, record.color); pushMarkerInstance(out, record.id, record.kind, "b", record.b, record.color); pushMarkerInstance(out, record.id, record.kind, "c", record.c, record.color); } return out; }; var AnnotationMarkerRenderer = class { glyphField; maxInstances; markerScale; keepCPUData; ownsGlyphField; attachedScene = null; appliedRevision = -1; updateCount = 0; instances = []; constructor(desc = {}) { this.markerScale = resolveMarkerScale(desc.markerScale); this.maxInstances = Math.max(1, Math.round(desc.maxInstances ?? 4096)); this.keepCPUData = desc.keepCPUData ?? true; this.ownsGlyphField = !desc.glyphField; this.glyphField = desc.glyphField ?? new GlyphField({ shape: desc.shape ?? "ellipsoid", instanceCount: 0, colorMode: "rgba", lit: false, depthWrite: true, depthTest: true, keepCPUData: this.keepCPUData, name: desc.name ?? "annotation-markers", scaleTransform: { componentCount: 4, componentIndex: 0, valueMode: "component", stride: 4, offset: 0, mode: "linear", clampMode: "none" } }); } get revision() { return this.appliedRevision; } get syncCount() { return this.updateCount; } get instanceCount() { return this.instances.length; } attach(scene) { if (this.attachedScene === scene) return this; this.detach(); scene.add(this.glyphField); this.attachedScene = scene; return this; } detach() { if (this.attachedScene) this.attachedScene.remove(this.glyphField); this.attachedScene = null; return this; } destroy() { this.detach(); if (this.ownsGlyphField) this.glyphField.destroy(); this.instances = []; this.appliedRevision = -1; } getInstance(index) { if (!Number.isInteger(index) || index < 0 || index >= this.instances.length) return null; const item = this.instances[index]; return { key: item.key, annotationId: item.annotationId, annotationKind: item.annotationKind, role: item.role, anchor: cloneAnnotationAnchor(item.anchor), color: cloneAnnotationColor(item.color) }; } sync(records, revision) { assert(Number.isFinite(revision), "AnnotationMarkerRenderer.sync: revision must be finite."); if (revision === this.appliedRevision) return false; const collected = collectAnnotationMarkerInstances(records); const instances = collected.length > this.maxInstances ? collected.slice(0, this.maxInstances) : collected; this.instances = instances; const count = instances.length; if (count <= 0) { this.glyphField.setCPUData(null, null, null, null, { instanceCount: 0, keepCPUData: this.keepCPUData }); this.glyphField.visible = false; this.appliedRevision = revision; this.updateCount++; return true; } const positions = new Float32Array(count * 4); const rotations = new Float32Array(count * 4); const scales = new Float32Array(count * 4); const attributes = new Float32Array(count * 4); for (let i = 0; i < count; i++) { const o = i * 4; const instance = instances[i]; positions[o + 0] = instance.anchor.position[0]; positions[o + 1] = instance.anchor.position[1]; positions[o + 2] = instance.anchor.position[2]; positions[o + 3] = 0; rotations[o + 0] = 0; rotations[o + 1] = 0; rotations[o + 2] = 0; rotations[o + 3] = 1; scales[o + 0] = this.markerScale[0]; scales[o + 1] = this.markerScale[1]; scales[o + 2] = this.markerScale[2]; scales[o + 3] = 0; attributes[o + 0] = instance.color[0]; attributes[o + 1] = instance.color[1]; attributes[o + 2] = instance.color[2]; attributes[o + 3] = instance.color[3]; } this.glyphField.visible = true; this.glyphField.setCPUData(positions, rotations, scales, attributes, { keepCPUData: this.keepCPUData }); this.appliedRevision = revision; this.updateCount++; return true; } }; // typescript/overlay/annotation/store.ts var DEFAULT_COLORS = { marker: [0.9, 0.8, 0.2, 1], distance: [0.2, 0.8, 1, 1], angle: [1, 0.5, 0.2, 1] }; var vecSub = (a, b) => [(a[0] ?? 0) - (b[0] ?? 0), (a[1] ?? 0) - (b[1] ?? 0), (a[2] ?? 0) - (b[2] ?? 0)]; var vecDot = (a, b) => (a[0] ?? 0) * (b[0] ?? 0) + (a[1] ?? 0) * (b[1] ?? 0) + (a[2] ?? 0) * (b[2] ?? 0); var vecLen = (v) => Math.hypot(v[0] ?? 0, v[1] ?? 0, v[2] ?? 0); var cloneRecord = (record) => { if (record.kind === "marker") return { ...record, color: cloneAnnotationColor(record.color), anchor: cloneAnnotationAnchor(record.anchor) }; if (record.kind === "distance") return { ...record, color: cloneAnnotationColor(record.color), start: cloneAnnotationAnchor(record.start), end: cloneAnnotationAnchor(record.end) }; return { ...record, color: cloneAnnotationColor(record.color), a: cloneAnnotationAnchor(record.a), b: cloneAnnotationAnchor(record.b), c: cloneAnnotationAnchor(record.c) }; }; var normalizedColor = (input, kind) => { const source = input ?? DEFAULT_COLORS[kind]; return [clamp(source[0], 0, 1), clamp(source[1], 0, 1), clamp(source[2], 0, 1), clamp(source[3], 0, 1)]; }; var normalizeLabel = (label) => { if (label == null) return null; const trimmed = `${label}`.trim(); return trimmed.length > 0 ? trimmed : null; }; var normalizeVisible = (visible) => visible === void 0 ? true : !!visible; var computeDistanceWorld = (a, b) => { const dx = (a[0] ?? 0) - (b[0] ?? 0); const dy = (a[1] ?? 0) - (b[1] ?? 0); const dz = (a[2] ?? 0) - (b[2] ?? 0); return Math.hypot(dx, dy, dz); }; var computeAngleRadians = (a, b, c) => { const ba = vecSub(a, b); const bc = vecSub(c, b); const lenBA = vecLen(ba); const lenBC = vecLen(bc); if (lenBA <= 1e-12 || lenBC <= 1e-12) return 0; const cos = clamp(vecDot(ba, bc) / (lenBA * lenBC), -1, 1); return Math.acos(cos); }; var createAnnotationAnchor = (position, pick = null) => { return { position: cloneAnnotationVec3(position), pick: pick ? { ...pick, ndIndex: pick.ndIndex ? pick.ndIndex.slice() : null, attributes: pick.attributes ? { ...pick.attributes, vector: pick.attributes.vector ? [...pick.attributes.vector] : null, packedPoint: pick.attributes.packedPoint ? [...pick.attributes.packedPoint] : null } : null } : null }; }; var AnnotationStore = class { records = /* @__PURE__ */ new Map(); order = []; listeners = /* @__PURE__ */ new Set(); nowMs; idPrefix; idCounter = 1; _revision = 0; constructor(desc = {}) { this.nowMs = desc.nowMs ?? nowMs; this.idPrefix = `${desc.idPrefix ?? "ann"}`.trim(); } get size() { return this.order.length; } get revision() { return this._revision; } onChange(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } has(id) { return this.records.has(id); } ids() { return this.order.slice(); } get(id) { const record = this.records.get(id); return record ? cloneRecord(record) : null; } values() { const out = new Array(this.order.length); for (let i = 0; i < this.order.length; i++) out[i] = cloneRecord(this.records.get(this.order[i])); return out; } clear() { if (this.records.size === 0) return this; this.records.clear(); this.order.length = 0; this.bumpRevision(); return this; } createMarker(anchor, opts = {}) { const now = this.nowMs(); const record = { id: this.nextId("marker"), kind: "marker", label: normalizeLabel(opts.label), visible: normalizeVisible(opts.visible), color: normalizedColor(opts.color, "marker"), createdAtMs: now, updatedAtMs: now, anchor: cloneAnnotationAnchor(anchor) }; this.push(record); return cloneRecord(record); } createDistance(start, end, opts = {}) { const now = this.nowMs(); const s = cloneAnnotationAnchor(start); const e = cloneAnnotationAnchor(end); const record = { id: this.nextId("distance"), kind: "distance", label: normalizeLabel(opts.label), visible: normalizeVisible(opts.visible), color: normalizedColor(opts.color, "distance"), createdAtMs: now, updatedAtMs: now, start: s, end: e, distanceWorld: computeDistanceWorld(s.position, e.position) }; this.push(record); return cloneRecord(record); } createAngle(a, b, c, opts = {}) { const now = this.nowMs(); const p0 = cloneAnnotationAnchor(a); const p1 = cloneAnnotationAnchor(b); const p2 = cloneAnnotationAnchor(c); const record = { id: this.nextId("angle"), kind: "angle", label: normalizeLabel(opts.label), visible: normalizeVisible(opts.visible), color: normalizedColor(opts.color, "angle"), createdAtMs: now, updatedAtMs: now, a: p0, b: p1, c: p2, angleRadians: computeAngleRadians(p0.position, p1.position, p2.position) }; this.push(record); return cloneRecord(record); } updateMarker(id, patch) { const record = this.records.get(id); if (!record || record.kind !== "marker") return null; this.applyCommonPatch(record, patch); if (patch.anchor) record.anchor = cloneAnnotationAnchor(patch.anchor); record.updatedAtMs = this.nowMs(); this.bumpRevision(); return cloneRecord(record); } updateDistance(id, patch) { const record = this.records.get(id); if (!record || record.kind !== "distance") return null; this.applyCommonPatch(record, patch); if (patch.start) record.start = cloneAnnotationAnchor(patch.start); if (patch.end) record.end = cloneAnnotationAnchor(patch.end); record.distanceWorld = computeDistanceWorld(record.start.position, record.end.position); record.updatedAtMs = this.nowMs(); this.bumpRevision(); return cloneRecord(record); } updateAngle(id, patch) { const record = this.records.get(id); if (!record || record.kind !== "angle") return null; this.applyCommonPatch(record, patch); if (patch.a) record.a = cloneAnnotationAnchor(patch.a); if (patch.b) record.b = cloneAnnotationAnchor(patch.b); if (patch.c) record.c = cloneAnnotationAnchor(patch.c); record.angleRadians = computeAngleRadians(record.a.position, record.b.position, record.c.position); record.updatedAtMs = this.nowMs(); this.bumpRevision(); return cloneRecord(record); } remove(id) { const existed = this.records.delete(id); if (!existed) return false; const idx = this.order.indexOf(id); if (idx !== -1) this.order.splice(idx, 1); this.bumpRevision(); return true; } push(record) { assert(!this.records.has(record.id), `AnnotationStore: duplicate id '${record.id}'.`); this.records.set(record.id, record); this.order.push(record.id); this.bumpRevision(); } applyCommonPatch(record, patch) { if (patch.label !== void 0) record.label = normalizeLabel(patch.label); if (patch.color !== void 0) record.color = normalizedColor(patch.color, record.kind); if (patch.visible !== void 0) record.visible = !!patch.visible; } nextId(kind) { const token = String(this.idCounter++).padStart(6, "0"); const prefix = this.idPrefix.length > 0 ? `${this.idPrefix}-` : ""; return `${prefix}${kind}-${token}`; } bumpRevision() { this._revision++; for (const listener of this.listeners) try { listener(this._revision); } catch { } } }; // typescript/overlay/annotation/toolkit.ts var TOOLKIT_ID = 1; var midpoint = (a, b) => [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]; var clonePickAttributesLocal = (attributes) => { if (!attributes) return null; return { ...attributes, vector: attributes.vector ? [...attributes.vector] : null, packedPoint: attributes.packedPoint ? [...attributes.packedPoint] : null, color: attributes.color ? [...attributes.color] : null, edgeEndpoints: attributes.edgeEndpoints ? [...attributes.edgeEndpoints] : null, edgePositions: attributes.edgePositions ? [...attributes.edgePositions] : null }; }; var probeReadoutFromHit = (hit) => { if (!hit) { return { hit: false, kind: null, objectId: null, elementIndex: null, worldPosition: null, ndIndex: null, attributes: null }; } return { hit: true, kind: hit.kind, objectId: hit.objectId, elementIndex: hit.elementIndex, worldPosition: [hit.worldPosition[0], hit.worldPosition[1], hit.worldPosition[2]], ndIndex: hit.ndIndex ? hit.ndIndex.slice() : null, attributes: clonePickAttributesLocal(hit.attributes) }; }; var selectionReadoutFromHit = (hit, meta) => { const probe = probeReadoutFromHit(hit); return { ...probe, annotationId: meta.annotationId, annotationKind: meta.annotationKind, anchorRole: meta.anchorRole }; }; var clonePending = (pending) => { const out = new Array(pending.length); for (let i = 0; i < pending.length; i++) { const src = pending[i]; out[i] = { position: [src.position[0], src.position[1], src.position[2]], pick: src.pick ? { kind: src.pick.kind, objectId: src.pick.objectId, elementIndex: src.pick.elementIndex, ndIndex: src.pick.ndIndex ? src.pick.ndIndex.slice() : null, attributes: clonePickAttributesLocal(src.pick.attributes) } : null }; } return out; }; var buildLabelEntries = (records, units) => { const out = []; for (let i = 0; i < records.length; i++) { const record = records[i]; if (!record.visible) continue; if (record.kind === "marker") { out.push({ key: record.id, text: record.label ?? record.id, color: colorToCssRgba(record.color), position: [record.anchor.position[0], record.anchor.position[1], record.anchor.position[2]] }); continue; } if (record.kind === "distance") { const metric2 = formatDistanceWorld(record.distanceWorld, units); const text2 = record.label ? `${record.label}: ${metric2.text}` : metric2.text; out.push({ key: record.id, text: text2, color: colorToCssRgba(record.color), position: midpoint(record.start.position, record.end.position) }); continue; } const metric = formatAngleRadians(record.angleRadians, units); const text = record.label ? `${record.label}: ${metric.text}` : metric.text; out.push({ key: record.id, text, color: colorToCssRgba(record.color), position: [record.b.position[0], record.b.position[1], record.b.position[2]] }); } return out; }; var AnnotationToolkit = class { store; markerRenderer; labelLayer; runtime; annotationsListeners = /* @__PURE__ */ new Set(); modeListeners = /* @__PURE__ */ new Set(); hoverListeners = /* @__PURE__ */ new Set(); selectionListeners = /* @__PURE__ */ new Set(); stagingListeners = /* @__PURE__ */ new Set(); autoCreateOverlay; overlaySystemOptions; modeValue = AnnotationMode.Idle; unitsValue; scene = null; camera = null; controls = null; overlaySystem = null; ownsOverlaySystem = false; canvas = null; pointerTarget = null; hoverReadout = probeReadoutFromHit(null); selectionReadout = selectionReadoutFromHit(null, { annotationId: null, annotationKind: null, anchorRole: null }); pendingAnchors = []; boundPointerEvents = false; autoBindPointerEvents; hoverPickToken = 0; clickPickToken = 0; constructor(runtime, desc = {}) { this.runtime = runtime; const toolkitId = TOOLKIT_ID++; this.store = new AnnotationStore({ idPrefix: desc.storeIdPrefix ?? `ann${toolkitId}` }); this.markerRenderer = new AnnotationMarkerRenderer({ ...desc.markerRenderer, name: desc.markerRenderer?.name ?? `annotation-markers-${toolkitId}` }); this.labelLayer = new AnnotationLabelLayer({ ...desc.labelLayer, id: desc.labelLayer?.id ?? `annotation-label-layer-${toolkitId}` }); this.autoBindPointerEvents = desc.autoBindPointerEvents ?? true; this.autoCreateOverlay = desc.autoCreateOverlay ?? true; this.overlaySystemOptions = desc.overlaySystemOptions ?? {}; this.unitsValue = desc.units ?? {}; this.controls = desc.controls ?? null; this.canvas = desc.canvas ?? null; this.pointerTarget = desc.pointerTarget ?? this.canvas; if (desc.overlaySystem) { this.overlaySystem = desc.overlaySystem; this.ownsOverlaySystem = false; } this.store.onChange(() => this.handleStoreChanged()); this.labelLayer.setHoverReadout(this.hoverReadout); this.labelLayer.setSelectionReadout(this.selectionReadout); if (desc.scene && desc.camera) this.attach({ scene: desc.scene, camera: desc.camera, controls: this.controls, overlaySystem: this.overlaySystem, pointerTarget: this.pointerTarget }); } get mode() { return this.modeValue; } get units() { return resolveAnnotationUnits(this.unitsValue); } get revision() { return this.store.revision; } get pendingCount() { return this.pendingAnchors.length; } get hoverProbe() { return { ...this.hoverReadout, worldPosition: this.hoverReadout.worldPosition ? [this.hoverReadout.worldPosition[0], this.hoverReadout.worldPosition[1], this.hoverReadout.worldPosition[2]] : null, ndIndex: this.hoverReadout.ndIndex ? this.hoverReadout.ndIndex.slice() : null, attributes: clonePickAttributesLocal(this.hoverReadout.attributes) }; } get selectionProbe() { return { ...this.selectionReadout, worldPosition: this.selectionReadout.worldPosition ? [this.selectionReadout.worldPosition[0], this.selectionReadout.worldPosition[1], this.selectionReadout.worldPosition[2]] : null, ndIndex: this.selectionReadout.ndIndex ? this.selectionReadout.ndIndex.slice() : null, attributes: clonePickAttributesLocal(this.selectionReadout.attributes) }; } onAnnotationsChange(listener) { this.annotationsListeners.add(listener); return () => this.annotationsListeners.delete(listener); } onModeChange(listener) { this.modeListeners.add(listener); return () => this.modeListeners.delete(listener); } onHoverReadout(listener) { this.hoverListeners.add(listener); return () => this.hoverListeners.delete(listener); } onSelectionReadout(listener) { this.selectionListeners.add(listener); return () => this.selectionListeners.delete(listener); } onStagingChange(listener) { this.stagingListeners.add(listener); return () => this.stagingListeners.delete(listener); } setUnits(units) { this.unitsValue = units; this.handleStoreChanged(); return this; } setMode(mode) { if (mode === this.modeValue) return this; this.modeValue = mode; this.clearPending(); for (const listener of this.modeListeners) try { listener(this.modeValue); } catch { } return this; } cancel() { this.modeValue = AnnotationMode.Idle; this.clearPending(); for (const listener of this.modeListeners) try { listener(this.modeValue); } catch { } return this; } attach(desc) { this.scene = desc.scene; this.camera = desc.camera; if (desc.controls !== void 0) this.controls = desc.controls ?? null; if (desc.pointerTarget !== void 0) this.pointerTarget = desc.pointerTarget ?? this.pointerTarget; if (desc.overlaySystem !== void 0) { this.detachOverlayLayer(); this.overlaySystem = desc.overlaySystem ?? null; this.ownsOverlaySystem = false; } this.markerRenderer.attach(this.scene); this.ensureOverlaySystem(); this.ensureOverlayLayer(); this.overlaySystem?.setView(this.camera, this.scene); this.handleStoreChanged(); if (this.autoBindPointerEvents) this.bindPointerEvents(); return this; } setView(camera, scene = this.scene) { this.camera = camera; if (scene) this.scene = scene; if (this.scene) this.markerRenderer.attach(this.scene); this.overlaySystem?.setView(this.camera, this.scene); this.overlaySystem?.invalidate("camera"); return this; } detach() { this.unbindPointerEvents(); this.detachOverlayLayer(); if (this.ownsOverlaySystem) this.overlaySystem?.destroy(); this.overlaySystem = null; this.ownsOverlaySystem = false; this.markerRenderer.detach(); this.scene = null; this.camera = null; this.clearPending(); this.hoverPickToken++; this.clickPickToken++; this.setHoverReadout(probeReadoutFromHit(null)); this.setSelectionReadout(selectionReadoutFromHit(null, { annotationId: null, annotationKind: null, anchorRole: null })); return this; } destroy() { this.detach(); this.markerRenderer.destroy(); } bindPointerTarget(target) { this.unbindPointerEvents(); this.pointerTarget = target; if (this.autoBindPointerEvents) this.bindPointerEvents(); return this; } setAutoBindPointerEvents(enabled) { this.autoBindPointerEvents = !!enabled; if (!this.autoBindPointerEvents) this.unbindPointerEvents(); else this.bindPointerEvents(); return this; } getAnnotations() { return this.store.values(); } createMarker(anchor, opts = {}) { return this.store.createMarker(anchor, opts); } createDistance(start, end, opts = {}) { return this.store.createDistance(start, end, opts); } createAngle(a, b, c, opts = {}) { return this.store.createAngle(a, b, c, opts); } updateAnnotation(id, patch) { const current = this.store.get(id); if (!current) return null; if (current.kind === "marker") return this.store.updateMarker(id, patch); if (current.kind === "distance") return this.store.updateDistance(id, patch); return this.store.updateAngle(id, patch); } removeAnnotation(id) { return this.store.remove(id); } removeSelectionAnnotation() { const annotationId = this.selectionReadout.annotationId; if (!annotationId) return false; return this.store.remove(annotationId); } clearAnnotations() { this.store.clear(); return this; } ingestHoverHit(hit) { const readout = probeReadoutFromHit(hit); this.setHoverReadout(readout); return readout; } ingestSelectionHit(hit) { const meta = this.resolveSelectionMeta(hit); this.setSelectionReadout(selectionReadoutFromHit(hit, meta)); if (!hit) { if (this.modeValue === AnnotationMode.Idle) this.clearPending(); return null; } if (this.modeValue === AnnotationMode.Marker) return this.store.createMarker(annotationAnchorFromHit(hit)); if (this.modeValue === AnnotationMode.Distance) { this.pendingAnchors.push(annotationAnchorFromHit(hit)); this.emitStaging(); if (this.pendingAnchors.length < 2) return null; const record = this.store.createDistance(this.pendingAnchors[0], this.pendingAnchors[1]); this.clearPending(); return record; } if (this.modeValue === AnnotationMode.Angle) { this.pendingAnchors.push(annotationAnchorFromHit(hit)); this.emitStaging(); if (this.pendingAnchors.length < 3) return null; const record = this.store.createAngle(this.pendingAnchors[0], this.pendingAnchors[1], this.pendingAnchors[2]); this.clearPending(); return record; } this.clearPending(); return null; } async pickHoverAt(x, y) { const scene = this.scene; const camera = this.camera; if (!scene || !camera) return this.ingestHoverHit(null); const token = ++this.hoverPickToken; try { const hit = await this.runtime.pick(scene, camera, x, y, { includeAttributes: true }); if (token !== this.hoverPickToken) return this.hoverProbe; return this.ingestHoverHit(hit); } catch { if (token !== this.hoverPickToken) return this.hoverProbe; return this.ingestHoverHit(null); } } async pickAtAndCommit(x, y) { const scene = this.scene; const camera = this.camera; if (!scene || !camera) return this.ingestSelectionHit(null); const token = ++this.clickPickToken; try { const hit = await this.runtime.pick(scene, camera, x, y, { includeAttributes: true }); if (token !== this.clickPickToken) return null; return this.ingestSelectionHit(hit); } catch { if (token !== this.clickPickToken) return null; return this.ingestSelectionHit(null); } } handleStoreChanged() { const records = this.store.values(); this.markerRenderer.sync(records, this.store.revision); this.labelLayer.setEntries(buildLabelEntries(records, this.unitsValue), this.store.revision); this.overlaySystem?.invalidate("manual"); for (const listener of this.annotationsListeners) try { listener(records, this.store.revision); } catch { } } ensureOverlaySystem() { if (this.overlaySystem) return; if (!this.autoCreateOverlay) return; if (!this.runtime.createOverlay || !this.camera) return; this.overlaySystem = this.runtime.createOverlay.system({ controls: this.controls ?? void 0, camera: this.camera, scene: this.scene, autoUpdate: true, ...this.overlaySystemOptions }); this.ownsOverlaySystem = true; } ensureOverlayLayer() { if (!this.overlaySystem) return; this.overlaySystem.removeLayer(this.labelLayer.id); this.overlaySystem.addLayer(this.labelLayer); } detachOverlayLayer() { if (!this.overlaySystem) return; this.overlaySystem.removeLayer(this.labelLayer.id); } setHoverReadout(readout) { this.hoverReadout = readout; this.labelLayer.setHoverReadout(readout); for (const listener of this.hoverListeners) try { listener(this.hoverProbe); } catch { } } setSelectionReadout(readout) { this.selectionReadout = readout; this.labelLayer.setSelectionReadout(readout); for (const listener of this.selectionListeners) try { listener(this.selectionProbe); } catch { } } resolveSelectionMeta(hit) { if (!hit) return { annotationId: null, annotationKind: null, anchorRole: null }; if (hit.kind !== "glyphfield" || hit.object !== this.markerRenderer.glyphField) return { annotationId: null, annotationKind: null, anchorRole: null }; const marker = this.markerRenderer.getInstance(hit.elementIndex); if (!marker) return { annotationId: null, annotationKind: null, anchorRole: null }; return { annotationId: marker.annotationId, annotationKind: marker.annotationKind, anchorRole: marker.role }; } clearPending() { if (this.pendingAnchors.length === 0) return; this.pendingAnchors.length = 0; this.emitStaging(); } emitStaging() { const pending = clonePending(this.pendingAnchors); for (const listener of this.stagingListeners) try { listener(this.modeValue, pending); } catch { } } bindPointerEvents() { if (this.boundPointerEvents || !this.pointerTarget) return; this.pointerTarget.addEventListener("pointermove", this.onPointerMove); this.pointerTarget.addEventListener("pointerleave", this.onPointerLeave); this.pointerTarget.addEventListener("click", this.onClick); this.boundPointerEvents = true; } unbindPointerEvents() { if (!this.boundPointerEvents || !this.pointerTarget) return; this.pointerTarget.removeEventListener("pointermove", this.onPointerMove); this.pointerTarget.removeEventListener("pointerleave", this.onPointerLeave); this.pointerTarget.removeEventListener("click", this.onClick); this.boundPointerEvents = false; } clientToLocal(clientX, clientY) { const rect = this.pointerTarget?.getBoundingClientRect(); if (!rect) return { x: clientX, y: clientY }; return { x: clientX - rect.left, y: clientY - rect.top }; } onPointerMove = (event) => { if (!this.scene || !this.camera) return; const { x, y } = this.clientToLocal(event.clientX, event.clientY); void this.pickHoverAt(x, y); }; onPointerLeave = () => { this.hoverPickToken++; this.ingestHoverHit(null); }; onClick = (event) => { if (event.button !== 0) return; if (!this.scene || !this.camera) return; const { x, y } = this.clientToLocal(event.clientX, event.clientY); void this.pickAtAndCommit(x, y); }; }; var mapAnnotationProbeReadout = probeReadoutFromHit; // typescript/python/index.ts var isPyProxyLike = (x) => typeof x === "object" && x !== null && typeof x.getBuffer === "function"; var isPyBufferLike = (x) => { const value = x; return typeof value === "object" && value !== null && ArrayBuffer.isView(value.data) && Array.isArray(value.shape) && Array.isArray(value.strides); }; var dtypeOfTypedArray = (view) => { if (view instanceof Int8Array) return "i8"; if (view instanceof Uint8Array && !(view instanceof Uint8ClampedArray)) return "u8"; if (view instanceof Int16Array) return "i16"; if (view instanceof Uint16Array) return "u16"; if (view instanceof Int32Array) return "i32"; if (view instanceof Uint32Array) return "u32"; if (view instanceof Float32Array) return "f32"; if (view instanceof Float64Array) return "f64"; return null; }; var validateShape2 = (value) => { assert(Array.isArray(value), "Python buffer shape must be an array"); const shape = new Array(value.length); for (let i = 0; i < value.length; i++) { const dim = value[i]; assert(Number.isSafeInteger(dim) && dim >= 0, `Python buffer shape[${i}] must be a non-negative safe integer`); assert(dim <= 4294967295, `Python buffer shape[${i}] must fit in u32`); shape[i] = dim; } return shape; }; var numelOf = (shape) => { let numel = 1; for (const dim of shape) { numel *= dim; assert(Number.isSafeInteger(numel), "Python buffer shape product exceeds JavaScript's safe integer range"); if (dim === 0) return 0; } return numel; }; var contiguousStridesBytes = (shape, bytesPerElement2) => { const strides = new Array(shape.length); let stride = bytesPerElement2; for (let i = shape.length - 1; i >= 0; i--) { assert(Number.isSafeInteger(stride) && stride <= 2147483647, "Python buffer contiguous stride exceeds the supported i32 range"); strides[i] = stride; stride *= shape[i]; } return strides; }; var resolveBuffer2 = (buffer) => { assert(!(buffer.data instanceof DataView), "Python buffer DataView sources are not supported"); const dtype = dtypeOfTypedArray(buffer.data); assert(dtype !== null, "Unsupported Python buffer dtype, expected i8, u8, i16, u16, i32, u32, f32, or f64"); if (buffer.format !== void 0) assert(buffer.format.slice(-1) !== "?", "Unsupported Python buffer dtype bool, cast to uint8 before importing"); const info = dtypeInfo(dtype); const shape = validateShape2(buffer.shape); assert(buffer.strides.length === shape.length, "Python buffer strides/shape rank mismatch"); if (buffer.ndim !== void 0) assert(Number.isSafeInteger(buffer.ndim) && buffer.ndim === shape.length, "Python buffer ndim does not match shape rank"); if (buffer.itemsize !== void 0) assert(Number.isSafeInteger(buffer.itemsize) && buffer.itemsize === info.bytesPerElement, "Python buffer itemsize does not match its typed data"); assert(buffer.c_contiguous !== false, "Python buffer must be C-contiguous"); const numel = numelOf(shape); const byteLength = numel * info.bytesPerElement; assert(Number.isSafeInteger(byteLength), "Python buffer byte length exceeds JavaScript's safe integer range"); if (buffer.nbytes !== void 0) assert(Number.isSafeInteger(buffer.nbytes) && buffer.nbytes === byteLength, "Python buffer nbytes does not match shape and dtype"); let expectedStride = 1; for (let i = shape.length - 1; i >= 0; i--) { const stride = buffer.strides[i]; assert(Number.isSafeInteger(stride), `Python buffer strides[${i}] must be a safe integer`); if (numel !== 0 && shape[i] > 1) assert(stride === expectedStride, "Python buffer must be C-contiguous"); expectedStride *= shape[i]; assert(Number.isSafeInteger(expectedStride), "Python buffer stride calculation overflowed"); } const offset = buffer.offset ?? 0; assert(Number.isSafeInteger(offset) && offset >= 0, "Python buffer offset must be a non-negative safe integer"); const byteOffset = buffer.data.byteOffset + offset * info.bytesPerElement; const byteEnd = byteOffset + byteLength; assert(Number.isSafeInteger(byteOffset) && Number.isSafeInteger(byteEnd), "Python buffer range arithmetic overflowed"); assert(byteOffset >= buffer.data.byteOffset && byteEnd <= buffer.data.byteOffset + buffer.data.byteLength, "Python buffer offset/range is outside its typed data view"); assert(byteOffset % info.bytesPerElement === 0, "Python buffer offset is not aligned for its dtype"); const ctor = dtypeInfo(dtype).ctor; const data = new ctor(buffer.data.buffer, byteOffset, numel); return { dtype, shape, data, byteLength }; }; var withResolvedSource = (src, operation) => { const acquired = isPyProxyLike(src); const buffer = acquired ? src.getBuffer() : src; let operationError; try { assert(isPyBufferLike(buffer), "Expected a Pyodide proxy with getBuffer() or a PyBufferLike object"); return operation(resolveBuffer2(buffer)); } catch (error) { operationError = error; throw error; } finally { if (acquired && typeof buffer?.release === "function") { try { buffer.release(); } catch (releaseError) { if (operationError === void 0) throw releaseError; } } } }; var sameShape = (a, b) => a.length === b.length && a.every((dim, i) => dim === b[i]); var gpuWriteSource = (data) => { if ((data.byteOffset & 3) === 0) return data; const copy = new Uint8Array(data.byteLength); copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); return copy; }; var PythonInterop = class { compute; constructor(compute) { this.compute = compute; } toCPU(src) { return withResolvedSource(src, ({ dtype, shape, data }) => { const dst = CPUndarray.empty(dtype, { shape }); try { dst.data().set(data); return dst; } catch (error) { dst.destroy(); throw error; } }); } toGPU(src, options = {}) { return withResolvedSource(src, ({ dtype, shape, data, byteLength }) => { let buffer = null; try { buffer = new StorageBuffer(this.compute.device, this.compute.queue, { label: options.label, data, copyDst: options.copyDst ?? true, copySrc: options.copySrc ?? true, usage: options.usage }); return new GPUndarray(dtype, shape, contiguousStridesBytes(shape, dtypeInfo(dtype).bytesPerElement), 0, byteLength, buffer, 0, true, this.compute.readback); } catch (error) { buffer?.destroy(); throw error; } }); } copyInto(dst, src) { withResolvedSource(src, ({ dtype, shape, data, byteLength }) => { assert(dst instanceof CPUndarray || dst instanceof GPUndarray, "PythonInterop.copyInto() destination must be a CPUndarray or GPUndarray"); assert(dst.dtype === dtype, `PythonInterop.copyInto() dtype mismatch: destination is ${dst.dtype}, source is ${dtype}`); assert(sameShape(dst.shape, shape), `PythonInterop.copyInto() shape mismatch: destination is [${dst.shape}], source is [${shape}]`); assert(dst.isContiguousC, "PythonInterop.copyInto() destination must be C-contiguous"); assert(dst.byteLength === byteLength, "PythonInterop.copyInto() byte length mismatch"); if (dst instanceof CPUndarray) { dst.data().set(data); return; } assert((dst.buffer.usage & GPUBufferUsage.COPY_DST) !== 0, "PythonInterop.copyInto() GPU destination requires COPY_DST usage"); dst.buffer.write(gpuWriteSource(data), dst.baseOffsetBytes, 0, byteLength); }); } }; // typescript/world/controls.ts var AxisConventions = { Y_UP_RH: { right: [1, 0, 0], up: [0, 1, 0], forward: [0, 0, 1] }, Z_UP_RH: { right: [1, 0, 0], up: [0, 0, 1], forward: [0, -1, 0] }, X_UP_RH: { right: [0, 0, 1], up: [1, 0, 0], forward: [0, 1, 0] } }; var EPSILON2 = 1e-6; var ORBIT_POLE_EPS = 1e-3; var DEFAULT_TRANSITION_SECONDS = 0.35; var clamp2 = (x, min, max) => Math.max(min, Math.min(max, x)); var clamp012 = (x) => clamp2(x, 0, 1); var lerp2 = (a, b, t) => a + (b - a) * t; var easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) * 0.5; var vec3clone = (v) => [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0]; var vec3add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; var vec3sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; var vec3scl = (v, s) => [v[0] * s, v[1] * s, v[2] * s]; var vec3dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; var vec3cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; var vec3mag = (v) => Math.hypot(v[0], v[1], v[2]); var vec3normalize = (v, fallback = [0, 0, 1]) => { const len = vec3mag(v); if (len <= EPSILON2) return vec3clone(fallback); const inv = 1 / len; return [v[0] * inv, v[1] * inv, v[2] * inv]; }; var vec3lerp = (a, b, t) => [lerp2(a[0], b[0], t), lerp2(a[1], b[1], t), lerp2(a[2], b[2], t)]; var vec3rotate = (v, axisInput, angle) => { const axis = vec3normalize(axisInput, [0, 1, 0]); const c = Math.cos(angle); const s = Math.sin(angle); const d = vec3dot(axis, v); const cross = vec3cross(axis, v); return [v[0] * c + cross[0] * s + axis[0] * d * (1 - c), v[1] * c + cross[1] * s + axis[1] * d * (1 - c), v[2] * c + cross[2] * s + axis[2] * d * (1 - c)]; }; var quatmul = (ax, ay, az, aw, bx, by, bz, bw) => { return [aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw, aw * bw - ax * bx - ay * by - az * bz]; }; var quatinvert = (x, y, z, w) => [-x, -y, -z, w]; var quatnormalize = (x, y, z, w) => { const len = Math.hypot(x, y, z, w); if (len <= EPSILON2) return [0, 0, 0, 1]; const inv = 1 / len; return [x * inv, y * inv, z * inv, w * inv]; }; var quatrotvec = (vx, vy, vz, qx, qy, qz, qw) => { const tx = 2 * (qy * vz - qz * vy); const ty = 2 * (qz * vx - qx * vz); const tz = 2 * (qx * vy - qy * vx); return [vx + qw * tx + (qy * tz - qz * ty), vy + qw * ty + (qz * tx - qx * tz), vz + qw * tz + (qx * ty - qy * tx)]; }; var quatisid = (x, y, z, w) => Math.abs(x) < EPSILON2 && Math.abs(y) < EPSILON2 && Math.abs(z) < EPSILON2 && Math.abs(1 - w) < EPSILON2; var quatslerpid = (x, y, z, w, t) => { const tt = clamp012(t); let cosHalfTheta = clamp2(w, -1, 1); let qx = x; let qy = y; let qz = z; let qw = w; if (cosHalfTheta < 0) { cosHalfTheta = -cosHalfTheta; qx = -qx; qy = -qy; qz = -qz; qw = -qw; } if (cosHalfTheta >= 0.9995) return quatnormalize(qx * tt, qy * tt, qz * tt, 1 - tt + qw * tt); const halfTheta = Math.acos(cosHalfTheta); const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta); if (sinHalfTheta <= EPSILON2) return [0, 0, 0, 1]; const a = Math.sin((1 - tt) * halfTheta) / sinHalfTheta; const b = Math.sin(tt * halfTheta) / sinHalfTheta; return [qx * b, qy * b, qz * b, a + qw * b]; }; var resolveAxisConvention = (input) => { if (!input || input === "y-up-rh") return { right: [1, 0, 0], up: [0, 1, 0], forward: [0, 0, 1] }; if (input === "z-up-rh") return { right: [1, 0, 0], up: [0, 0, 1], forward: [0, -1, 0] }; if (input === "x-up-rh") return { right: [0, 0, 1], up: [1, 0, 0], forward: [0, 1, 0] }; const rightHint = input.right ?? vec3cross(input.up, input.forward); const right = vec3normalize(rightHint, [1, 0, 0]); const up = vec3normalize(input.up, [0, 1, 0]); const forward = vec3normalize(vec3cross(right, up), input.forward); const correctedRight = vec3normalize(vec3cross(up, forward), right); const correctedUp = vec3normalize(vec3cross(forward, correctedRight), up); return { right: correctedRight, up: correctedUp, forward }; }; var FLY_KEY_ACTIONS = ["forward", "backward", "left", "right", "up", "down", "rollLeft", "rollRight", "fast", "slow"]; var DEFAULT_FLY_KEY_MAP = { forward: ["KeyW", "ArrowUp"], backward: ["KeyS", "ArrowDown"], left: ["KeyA", "ArrowLeft"], right: ["KeyD", "ArrowRight"], up: ["KeyE"], down: ["KeyQ"], rollLeft: ["KeyZ"], rollRight: ["KeyC"], fast: ["ShiftLeft", "ShiftRight"], slow: ["AltLeft", "AltRight"] }; var resolveFlyKeyMap = (input) => { const out = {}; for (const action of FLY_KEY_ACTIONS) out[action] = [...input?.[action] ?? DEFAULT_FLY_KEY_MAP[action]]; return out; }; var collectFlyKeyCodes = (keyMap) => { const codes = /* @__PURE__ */ new Set(); for (const action of FLY_KEY_ACTIONS) for (const code of keyMap[action]) codes.add(code); return codes; }; var normalizePointerLockMode = (mode) => { if (mode === true) return "on-click"; if (mode === "on-click" || mode === "on-drag") return mode; return false; }; var defaultKeyboardTarget = (mode) => mode === "fly" && typeof window !== "undefined" ? window : null; var isEditableEventTarget = (target) => { if (!target || typeof target !== "object") return false; const candidate = target; if (candidate.isContentEditable) return true; const tag = typeof candidate.tagName === "string" ? candidate.tagName.toLowerCase() : ""; if (tag === "input" || tag === "textarea" || tag === "select") return true; const editable = candidate.getAttribute?.("contenteditable"); return editable === "" || editable === "true"; }; var NavigationControls = class { camera; domElement; target; enabled = true; enableRotate = true; enablePan = true; enableZoom = true; rotateSpeed = 1; panSpeed = 1; zoomSpeed = 1; zoomOnCursor = false; enableDamping = false; dampingFactor = 0.1; minDistance = 0; maxDistance = Infinity; minZoom = 0.01; maxZoom = Infinity; minPolarAngle = 0; maxPolarAngle = Math.PI; minAzimuthAngle = -Infinity; maxAzimuthAngle = Infinity; mouseButtons = { rotate: 0, zoom: 1, pan: 2 }; enableKeyboard = true; enablePointer = true; enableWheel = true; moveSpeed = 10; lookSpeed = 2e-3; rollSpeed = 1.5; fastMultiplier = 4; slowMultiplier = 0.25; wheelSpeedFactor = 1.1; minMoveSpeed = 1e-3; maxMoveSpeed = Infinity; invertY = false; yawMode = "global"; mouseButton = 0; pointerLock = false; keyMap = resolveFlyKeyMap(void 0); keyboardTarget = null; preventDefaultKeys = false; _mode; _axisConvention = resolveAxisConvention(void 0); _state = "none"; _pointerId = null; _pointerX = 0; _pointerY = 0; _zoomCursorClientX = 0; _zoomCursorClientY = 0; _zoomCursorValid = false; _transition = null; _theta = 0; _phi = Math.PI * 0.5; _thetaDelta = 0; _phiDelta = 0; _trackballRotateStart = [0, 0, 1]; _trackballRotationDelta = [0, 0, 0, 1]; _trackballEye = [0, 0, 1]; _trackballUp = [0, 1, 0]; _radius = 1; _zoom = 1; _dollyDelta = 0; _panOffset = [0, 0, 0]; _flyYawDelta = 0; _flyPitchDelta = 0; _flyPressedKeys = /* @__PURE__ */ new Set(); _flyMappedKeys = collectFlyKeyCodes(this.keyMap); _flyPointerLocked = false; _flyPointerLockRequested = false; _pointerLockDocument = null; _documentMouseMoveAttached = false; _keyboardTargetExplicit = false; _keyboardListenerTarget = null; _orthoBaseLeft = -1; _orthoBaseRight = 1; _orthoBaseTop = 1; _orthoBaseBottom = -1; _savedTarget = [0, 0, 0]; _savedPosition = [0, 0, 1]; _savedUp = [0, 1, 0]; _savedProjection = { type: "perspective", near: 0.1, far: 1e3 }; _wheelListenerOptions = { passive: false }; _changeListeners = /* @__PURE__ */ new Set(); _interactionListeners = /* @__PURE__ */ new Set(); _interactionActive = false; _wheelInteractionTimer = null; constructor(camera, domElement, desc = {}) { this.camera = camera; this.domElement = domElement; this.target = desc.target ? vec3clone(desc.target) : [0, 0, 0]; this._mode = desc.mode ?? "orbit"; this._axisConvention = resolveAxisConvention(desc.axisConvention ?? "y-up-rh"); if (desc.enabled !== void 0) this.enabled = desc.enabled; if (desc.enableRotate !== void 0) this.enableRotate = desc.enableRotate; if (desc.enablePan !== void 0) this.enablePan = desc.enablePan; if (desc.enableZoom !== void 0) this.enableZoom = desc.enableZoom; if (desc.rotateSpeed !== void 0) this.rotateSpeed = desc.rotateSpeed; if (desc.panSpeed !== void 0) this.panSpeed = desc.panSpeed; if (desc.zoomSpeed !== void 0) this.zoomSpeed = desc.zoomSpeed; if (desc.zoomOnCursor !== void 0) this.zoomOnCursor = desc.zoomOnCursor; if (desc.enableDamping !== void 0) this.enableDamping = desc.enableDamping; if (desc.dampingFactor !== void 0) this.dampingFactor = desc.dampingFactor; if (desc.minDistance !== void 0) this.minDistance = desc.minDistance; if (desc.maxDistance !== void 0) this.maxDistance = desc.maxDistance; if (desc.minZoom !== void 0) this.minZoom = desc.minZoom; if (desc.maxZoom !== void 0) this.maxZoom = desc.maxZoom; if (desc.minPolarAngle !== void 0) this.minPolarAngle = desc.minPolarAngle; if (desc.maxPolarAngle !== void 0) this.maxPolarAngle = desc.maxPolarAngle; if (desc.minAzimuthAngle !== void 0) this.minAzimuthAngle = desc.minAzimuthAngle; if (desc.maxAzimuthAngle !== void 0) this.maxAzimuthAngle = desc.maxAzimuthAngle; if (desc.mouseButtons) { if (desc.mouseButtons.rotate !== void 0) this.mouseButtons.rotate = desc.mouseButtons.rotate; if (desc.mouseButtons.zoom !== void 0) this.mouseButtons.zoom = desc.mouseButtons.zoom; if (desc.mouseButtons.pan !== void 0) this.mouseButtons.pan = desc.mouseButtons.pan; } if (desc.enableKeyboard !== void 0) this.enableKeyboard = desc.enableKeyboard; if (desc.enablePointer !== void 0) this.enablePointer = desc.enablePointer; if (desc.enableWheel !== void 0) this.enableWheel = desc.enableWheel; if (desc.moveSpeed !== void 0) this.moveSpeed = desc.moveSpeed; if (desc.lookSpeed !== void 0) this.lookSpeed = desc.lookSpeed; if (desc.rollSpeed !== void 0) this.rollSpeed = desc.rollSpeed; if (desc.fastMultiplier !== void 0) this.fastMultiplier = desc.fastMultiplier; if (desc.slowMultiplier !== void 0) this.slowMultiplier = desc.slowMultiplier; if (desc.wheelSpeedFactor !== void 0) this.wheelSpeedFactor = Math.max(EPSILON2, desc.wheelSpeedFactor); if (desc.minMoveSpeed !== void 0) this.minMoveSpeed = Math.max(EPSILON2, desc.minMoveSpeed); if (desc.maxMoveSpeed !== void 0) this.maxMoveSpeed = Math.max(this.minMoveSpeed, desc.maxMoveSpeed); if (desc.invertY !== void 0) this.invertY = desc.invertY; if (desc.yawMode !== void 0) this.yawMode = desc.yawMode; if (desc.mouseButton !== void 0) this.mouseButton = desc.mouseButton; this.pointerLock = normalizePointerLockMode(desc.pointerLock); this.keyMap = resolveFlyKeyMap(desc.keyMap); this._flyMappedKeys = collectFlyKeyCodes(this.keyMap); this._keyboardTargetExplicit = desc.keyboardTarget !== void 0; this.keyboardTarget = this._keyboardTargetExplicit ? desc.keyboardTarget ?? null : defaultKeyboardTarget(this._mode); if (desc.preventDefaultKeys !== void 0) this.preventDefaultKeys = desc.preventDefaultKeys; this.domElement.style.touchAction = "none"; this.syncFromCameraState(); this.saveState(); this.domElement.addEventListener("pointerdown", this.onPointerDown); this.domElement.addEventListener("pointermove", this.onPointerMove); this.domElement.addEventListener("pointerup", this.onPointerUp); this.domElement.addEventListener("pointercancel", this.onPointerUp); this.domElement.addEventListener("wheel", this.onWheel, this._wheelListenerOptions); this.domElement.addEventListener("contextmenu", this.onContextMenu); this._pointerLockDocument = this.getPointerLockDocument(); this._pointerLockDocument?.addEventListener("pointerlockchange", this.onPointerLockChange); this._pointerLockDocument?.addEventListener("pointerlockerror", this.onPointerLockError); this.syncKeyboardListeners(); } dispose() { this.cancelTransition(); this.clearWheelInteractionTimer(); this.exitFlyPointerLock(); this.detachPointerLockMouseMove(); this.setInteractionState(false); this.domElement.removeEventListener("pointerdown", this.onPointerDown); this.domElement.removeEventListener("pointermove", this.onPointerMove); this.domElement.removeEventListener("pointerup", this.onPointerUp); this.domElement.removeEventListener("pointercancel", this.onPointerUp); this.domElement.removeEventListener("wheel", this.onWheel, this._wheelListenerOptions); this.domElement.removeEventListener("contextmenu", this.onContextMenu); this._pointerLockDocument?.removeEventListener("pointerlockchange", this.onPointerLockChange); this._pointerLockDocument?.removeEventListener("pointerlockerror", this.onPointerLockError); this.detachKeyboardListeners(); this._flyPressedKeys.clear(); this._changeListeners.clear(); this._interactionListeners.clear(); } onChange(listener) { this._changeListeners.add(listener); return () => { this._changeListeners.delete(listener); }; } onInteractionState(listener) { this._interactionListeners.add(listener); return () => { this._interactionListeners.delete(listener); }; } get mode() { return this._mode; } set mode(value) { this.setMode(value); } get axisConvention() { return { right: vec3clone(this._axisConvention.right), up: vec3clone(this._axisConvention.up), forward: vec3clone(this._axisConvention.forward) }; } set axisConvention(value) { this._axisConvention = resolveAxisConvention(value); this.syncFromCamera(); } get azimuthAngle() { return this._theta; } set azimuthAngle(value) { this._theta = value; } get polarAngle() { return this._phi; } set polarAngle(value) { this._phi = value; } get distance() { return this._radius; } set distance(value) { const next = Math.max(EPSILON2, value); this._radius = next; if (this._mode === "trackball") { const eye = vec3normalize(this._trackballEye, this._axisConvention.forward); this._trackballEye = vec3scl(eye, next); } } get zoom() { return this._zoom; } set zoom(value) { this._zoom = clamp2(value, this.minZoom, this.maxZoom); } get hasActiveTransition() { return this._transition !== null; } setCamera(camera) { this.camera = camera; this.cancelTransition(); this.syncFromCamera(); this.emitChange(); return this; } setMode(mode) { if (mode === this._mode) return this; this.syncFromCamera(); if (this._mode === "fly") this.stopFlyInteraction(); this._mode = mode; this.updateImplicitKeyboardTarget(); this.syncKeyboardListeners(); this.syncFromCamera(); return this; } syncFromCamera() { this.syncFromCameraState(); } syncFromCameraState() { const position = vec3clone(this.camera.position); const offset = vec3sub(position, this.target); this._radius = Math.max(EPSILON2, vec3mag(offset)); if (this.camera.type === "orthographic") { const ortho = this.camera; this._orthoBaseLeft = ortho.left; this._orthoBaseRight = ortho.right; this._orthoBaseTop = ortho.top; this._orthoBaseBottom = ortho.bottom; this._zoom = 1; } const spherical = this.offsetToSpherical(offset); let theta = spherical.theta; const localX = vec3dot(offset, this._axisConvention.right); const localY = vec3dot(offset, this._axisConvention.up); const localZ = vec3dot(offset, this._axisConvention.forward); const horizontalRadius = Math.hypot(localX, localZ); const poleThreshold = Math.max(EPSILON2, this._radius * EPSILON2); if (horizontalRadius <= poleThreshold && Math.abs(localY) > EPSILON2) { const poleSign = localY >= 0 ? 1 : -1; const cameraUp = vec3normalize(this.camera.up, this._axisConvention.forward); const horizontalDirection = vec3scl(cameraUp, -poleSign); const hx = vec3dot(horizontalDirection, this._axisConvention.right); const hz = vec3dot(horizontalDirection, this._axisConvention.forward); if (Math.hypot(hx, hz) > EPSILON2) theta = Math.atan2(hx, hz); } this._theta = theta; this._phi = spherical.phi; this._thetaDelta = 0; this._phiDelta = 0; this._dollyDelta = 0; this._panOffset = [0, 0, 0]; this._flyYawDelta = 0; this._flyPitchDelta = 0; this._trackballEye = offset; this._trackballUp = vec3normalize(this.camera.up, this._axisConvention.up); this._trackballRotateStart = [0, 0, 1]; this._trackballRotationDelta = [0, 0, 0, 1]; } saveState() { const position = vec3clone(this.camera.position); this._savedPosition = position; this._savedTarget = this._mode === "fly" ? vec3add(position, this.computeFlyBasis().forward) : vec3clone(this.target); this._savedUp = vec3normalize(this.camera.up, this._axisConvention.up); this._savedProjection = this.captureProjectionState(); } reset() { this.cancelTransition(); this.applyPose(this._savedPosition, this._savedTarget, this._savedUp); this.applyProjectionState(this._savedProjection); this.syncFromCamera(); this.emitChange(); } setTarget(xOrTarget, y, z) { if (typeof xOrTarget === "number") this.target = [xOrTarget, y ?? 0, z ?? 0]; else this.target = vec3clone(xOrTarget); return this; } cancelTransition() { this._transition = null; return this; } update(dtSeconds = 0) { if (!this.enabled) return; const dt = dtSeconds > 0 ? dtSeconds : 1 / 60; if (this._transition) { this.updateTransition(dt); this.emitChange(); return; } if (this._mode === "orbit") this.updateOrbit(dt); else if (this._mode === "trackball") this.updateTrackball(dt); else this.updateFly(dt); this.emitChange(); } setView(view, options = {}) { const direction = this.getInspectionViewDirection(view); const up = options.up ? vec3normalize(options.up, this._axisConvention.up) : this.getInspectionViewUp(view); const target = options.target ? vec3clone(options.target) : vec3clone(this.target); const distance = Math.max(EPSILON2, options.distance ?? this._radius); const position = vec3add(target, vec3scl(direction, distance)); this.applyPoseOrTransition(position, target, up, this.captureProjectionState(), options.animate, options.duration); return this; } fitScene(scene, options = {}) { return this.fitToBounds(scene.getBounds(), options); } viewBounds(view, source, options = {}) { return this.fitToBounds(source, { ...options, view }); } fitToBounds(source, options = {}) { const bounds = this.resolveBounds(source); if (bounds.empty) return bounds; const result = this.solveFit(bounds, options); this.applyPoseOrTransition(result.position, result.target, result.up, result.projection, options.animate, options.duration); return bounds; } updateTransition(dt) { const transition = this._transition; if (!transition) return; transition.elapsed += dt; const rawT = clamp012(transition.elapsed / Math.max(EPSILON2, transition.duration)); const t = easeInOutCubic(rawT); const position = vec3lerp(transition.fromPosition, transition.toPosition, t); const target = vec3lerp(transition.fromTarget, transition.toTarget, t); const up = vec3normalize(vec3lerp(transition.fromUp, transition.toUp, t), transition.toUp); this.applyPose(position, target, up); this.applyProjectionState(this.lerpProjectionState(transition.fromProjection, transition.toProjection, t)); if (rawT >= 1) { this._transition = null; this.syncFromCamera(); } } onPointerDown = (event) => { if (!this.enabled || this._pointerId !== null) return; this.cancelTransition(); if (this._mode === "fly") { this.onFlyPointerDown(event); return; } this._pointerId = event.pointerId; this.domElement.setPointerCapture(this._pointerId); this._pointerX = event.clientX; this._pointerY = event.clientY; this._zoomCursorClientX = event.clientX; this._zoomCursorClientY = event.clientY; this._zoomCursorValid = true; if (event.button === this.mouseButtons.rotate) this._state = "rotate"; else if (event.button === this.mouseButtons.pan) this._state = "pan"; else if (event.button === this.mouseButtons.zoom) this._state = "zoom"; else this._state = "none"; if (this._state !== "none") this.setInteractionState(true); if (this._state === "rotate" && this._mode === "trackball") this._trackballRotateStart = this.getTrackballVector(event.clientX, event.clientY); event.preventDefault(); }; onPointerMove = (event) => { if (this._mode === "fly") { this.onFlyPointerMove(event); return; } if (!this.enabled || this._pointerId === null || event.pointerId !== this._pointerId) return; const dx = event.clientX - this._pointerX; const dy = event.clientY - this._pointerY; this._pointerX = event.clientX; this._pointerY = event.clientY; this._zoomCursorClientX = event.clientX; this._zoomCursorClientY = event.clientY; this._zoomCursorValid = true; if (dx === 0 && dy === 0) return; if (this._state === "rotate" && this.enableRotate) { if (this._mode === "orbit") { const h = Math.max(1, this.getViewportHeight()); const s = 2 * Math.PI / h; this._thetaDelta += -dx * s * this.rotateSpeed; this._phiDelta += -dy * s * this.rotateSpeed; } else { const v = this.getTrackballVector(event.clientX, event.clientY); const q = this.rotationFromTrackballDrag(this._trackballRotateStart, v); if (q) this._trackballRotationDelta = quatnormalize(...quatmul(q[0], q[1], q[2], q[3], this._trackballRotationDelta[0], this._trackballRotationDelta[1], this._trackballRotationDelta[2], this._trackballRotationDelta[3])); this._trackballRotateStart = v; } } else if (this._state === "pan" && this.enablePan) { if (this._mode === "orbit") this.panOrbit(dx, dy); else this.panTrackball(dx, dy); } else if (this._state === "zoom" && this.enableZoom) this._dollyDelta += dy * this.zoomSpeed * 2e-3; event.preventDefault(); }; onPointerUp = (event) => { if (this._mode === "fly") { this.onFlyPointerUp(event); return; } if (this._pointerId === null || event.pointerId !== this._pointerId) return; this.domElement.releasePointerCapture(this._pointerId); this._pointerId = null; this._state = "none"; this.setInteractionState(false); event.preventDefault(); }; onWheel = (event) => { if (this._mode === "fly") { this.onFlyWheel(event); return; } if (!this.enabled || !this.enableZoom) return; this.cancelTransition(); this._dollyDelta += event.deltaY * this.zoomSpeed * 1e-3; this._zoomCursorClientX = event.clientX; this._zoomCursorClientY = event.clientY; this._zoomCursorValid = true; this.setInteractionState(true); this.scheduleWheelInteractionEnd(); event.preventDefault(); event.stopPropagation(); }; onContextMenu = (event) => { event.preventDefault(); }; onFlyPointerDown(event) { if (!this.enablePointer || event.button !== this.mouseButton) return; this._pointerId = event.pointerId; try { this.domElement.setPointerCapture(this._pointerId); } catch { } this._pointerX = event.clientX; this._pointerY = event.clientY; this._state = "fly"; this.setInteractionState(true); if (this.pointerLock !== false && !this._flyPointerLocked) this.requestFlyPointerLock(); event.preventDefault(); } onFlyPointerMove(event) { if (!this.enabled || !this.enablePointer) return; if (!this._flyPointerLocked && (this._pointerId === null || event.pointerId !== this._pointerId)) return; const dx = this._flyPointerLocked ? event.movementX ?? 0 : event.clientX - this._pointerX; const dy = this._flyPointerLocked ? event.movementY ?? 0 : event.clientY - this._pointerY; this._pointerX = event.clientX; this._pointerY = event.clientY; this.queueFlyLook(dx, dy); event.preventDefault(); } onFlyPointerUp(event) { if (this._pointerId === null || event.pointerId !== this._pointerId) return; try { this.domElement.releasePointerCapture(this._pointerId); } catch { } this._pointerId = null; if (this.pointerLock === "on-drag") this.exitFlyPointerLock(); const keepLocked = this.pointerLock === "on-click" && (this._flyPointerLocked || this._flyPointerLockRequested); if (!keepLocked) { this._state = "none"; this.setInteractionState(false); } event.preventDefault(); } onFlyWheel(event) { if (!this.enabled || !this.enableWheel) return; this.cancelTransition(); const lineHeight = 16; const pageHeight = this.getViewportHeight(); const raw = event.deltaMode === 1 ? event.deltaY * lineHeight : event.deltaMode === 2 ? event.deltaY * pageHeight : event.deltaY; const steps = clamp2(raw / 100, -1, 1); if (Math.abs(steps) > EPSILON2) this.moveSpeed = clamp2(this.moveSpeed * Math.pow(this.wheelSpeedFactor, steps), this.minMoveSpeed, this.maxMoveSpeed); this.setInteractionState(true); this.scheduleWheelInteractionEnd(); event.preventDefault(); event.stopPropagation(); } onKeyDown = (event) => { if (!this.enabled || this._mode !== "fly" || !this.enableKeyboard || isEditableEventTarget(event.target)) return; if (!this._flyMappedKeys.has(event.code)) return; this._flyPressedKeys.add(event.code); this.setInteractionState(true); if (this.preventDefaultKeys) event.preventDefault(); }; onKeyUp = (event) => { if (!this._flyMappedKeys.has(event.code)) return; this._flyPressedKeys.delete(event.code); if (this._flyPressedKeys.size === 0 && this._pointerId === null && this._state === "none") this.setInteractionState(false); if (this.enabled && this._mode === "fly" && this.enableKeyboard && this.preventDefaultKeys && !isEditableEventTarget(event.target)) event.preventDefault(); }; onDocumentMouseMove = (event) => { if (!this.enabled || this._mode !== "fly" || !this.enablePointer || !this._flyPointerLocked) return; this.queueFlyLook(event.movementX ?? 0, event.movementY ?? 0); event.preventDefault(); }; onPointerLockChange = () => { const doc = this._pointerLockDocument; const locked = !!doc && doc.pointerLockElement === this.domElement; this._flyPointerLocked = locked; this._flyPointerLockRequested = false; if (locked) { this.attachPointerLockMouseMove(); this._state = "fly"; this.setInteractionState(true); return; } this.detachPointerLockMouseMove(); if (this._state === "fly" && this._pointerId === null) { this._state = "none"; this.setInteractionState(false); } }; onPointerLockError = () => { this._flyPointerLockRequested = false; if (this._state === "fly" && this._pointerId === null) { this._state = "none"; this.setInteractionState(false); } }; queueFlyLook(dx, dy) { if (dx === 0 && dy === 0) return; this._flyYawDelta += -dx * this.lookSpeed; this._flyPitchDelta += (this.invertY ? dy : -dy) * this.lookSpeed; } requestFlyPointerLock() { const request = this.domElement.requestPointerLock; if (typeof request !== "function") return; this._flyPointerLockRequested = true; try { const result = request.call(this.domElement); if (result && typeof result.catch === "function") result.catch(() => { this.onPointerLockError(); }); } catch { this.onPointerLockError(); } } exitFlyPointerLock() { if (!this._flyPointerLocked && !this._flyPointerLockRequested) return; const doc = this._pointerLockDocument; this._flyPointerLockRequested = false; if (doc?.pointerLockElement === this.domElement && typeof doc.exitPointerLock === "function") try { doc.exitPointerLock(); } catch { } this._flyPointerLocked = false; this.detachPointerLockMouseMove(); } attachPointerLockMouseMove() { if (this._documentMouseMoveAttached || !this._pointerLockDocument) return; this._pointerLockDocument.addEventListener("mousemove", this.onDocumentMouseMove); this._documentMouseMoveAttached = true; } detachPointerLockMouseMove() { if (!this._documentMouseMoveAttached || !this._pointerLockDocument) return; this._pointerLockDocument.removeEventListener("mousemove", this.onDocumentMouseMove); this._documentMouseMoveAttached = false; } updateImplicitKeyboardTarget() { if (!this._keyboardTargetExplicit) this.keyboardTarget = defaultKeyboardTarget(this._mode); } syncKeyboardListeners() { const target = this._mode === "fly" ? this.keyboardTarget : null; if (target === this._keyboardListenerTarget) return; this.detachKeyboardListeners(); if (!target) return; target.addEventListener("keydown", this.onKeyDown); target.addEventListener("keyup", this.onKeyUp); this._keyboardListenerTarget = target; } detachKeyboardListeners() { if (!this._keyboardListenerTarget) return; this._keyboardListenerTarget.removeEventListener("keydown", this.onKeyDown); this._keyboardListenerTarget.removeEventListener("keyup", this.onKeyUp); this._keyboardListenerTarget = null; } stopFlyInteraction() { this._flyPressedKeys.clear(); this._flyYawDelta = 0; this._flyPitchDelta = 0; this.exitFlyPointerLock(); if (this._pointerId !== null) try { this.domElement.releasePointerCapture(this._pointerId); } catch { } this._state = "none"; this._pointerId = null; this.setInteractionState(false); } getPointerLockDocument() { const doc = this.domElement.ownerDocument ?? (typeof document !== "undefined" ? document : null); if (!doc || typeof doc.addEventListener !== "function" || typeof doc.removeEventListener !== "function") return null; return doc; } getViewportRect() { const rect = this.domElement.getBoundingClientRect(); const width = Math.max(1, rect.width || this.domElement.clientWidth || 1); const height = Math.max(1, rect.height || this.domElement.clientHeight || 1); return { left: rect.left, top: rect.top, width, height }; } getViewportWidth() { return this.getViewportRect().width; } getViewportHeight() { return this.getViewportRect().height; } getAspect(aspectOverride) { if (aspectOverride && aspectOverride > 0) return aspectOverride; if (this.camera instanceof PerspectiveCamera && !this.camera.autoAspect && Number.isFinite(this.camera.aspect) && this.camera.aspect > 0) return this.camera.aspect; return this.getViewportWidth() / Math.max(1, this.getViewportHeight()); } captureProjectionState() { if (this.camera.type === "orthographic") { const camera2 = this.camera; return { type: "orthographic", left: camera2.left, right: camera2.right, top: camera2.top, bottom: camera2.bottom, near: camera2.near, far: camera2.far }; } const camera = this.camera; return { type: "perspective", near: camera.near, far: camera.far }; } applyProjectionState(state) { if (state.type === "orthographic" && this.camera.type === "orthographic") { const camera = this.camera; camera.left = state.left; camera.right = state.right; camera.top = state.top; camera.bottom = state.bottom; camera.near = state.near; camera.far = state.far; } else if (state.type === "perspective" && this.camera.type === "perspective") { const camera = this.camera; camera.near = state.near; camera.far = state.far; } } lerpProjectionState(a, b, t) { if (a.type === "orthographic" && b.type === "orthographic") { return { type: "orthographic", left: lerp2(a.left, b.left, t), right: lerp2(a.right, b.right, t), top: lerp2(a.top, b.top, t), bottom: lerp2(a.bottom, b.bottom, t), near: lerp2(a.near, b.near, t), far: lerp2(a.far, b.far, t) }; } const lerpFar = (farA, farB, factor) => { const invA = Number.isFinite(farA) && farA > 0 ? 1 / farA : 0; const invB = Number.isFinite(farB) && farB > 0 ? 1 / farB : 0; const inv = lerp2(invA, invB, factor); return inv <= 1e-12 ? Infinity : 1 / inv; }; return { type: "perspective", near: lerp2(a.near, b.near, t), far: lerpFar(a.far, b.far, t) }; } applyPose(position, target, up) { this.target = vec3clone(target); this.camera.setWorldPosition(position[0], position[1], position[2]); this.camera.lookAtWithUp(target, up); } applyPoseOrTransition(position, target, up, projection, animate, duration) { const shouldAnimate = animate ?? true; if (!shouldAnimate || (duration ?? DEFAULT_TRANSITION_SECONDS) <= 0) { this.cancelTransition(); this.applyPose(position, target, up); this.applyProjectionState(projection); this.syncFromCamera(); this.emitChange(); return; } this._transition = { elapsed: 0, duration: duration ?? DEFAULT_TRANSITION_SECONDS, fromPosition: vec3clone(this.camera.position), toPosition: vec3clone(position), fromTarget: vec3clone(this.target), toTarget: vec3clone(target), fromUp: vec3normalize(this.camera.up, this._axisConvention.up), toUp: vec3normalize(up, this._axisConvention.up), fromProjection: this.captureProjectionState(), toProjection: projection }; } updateFly(dt) { const damping = this.enableDamping ? 1 - Math.pow(1 - clamp012(this.dampingFactor), dt * 60) : 1; const yaw = this.enablePointer ? this._flyYawDelta * damping : 0; const pitch = this.enablePointer ? this._flyPitchDelta * damping : 0; const roll = this.enableKeyboard ? this.getFlyAxis("rollRight", "rollLeft") * this.rollSpeed * dt : 0; if (Math.abs(yaw) > EPSILON2 || Math.abs(pitch) > EPSILON2 || Math.abs(roll) > EPSILON2) this.applyFlyRotation(yaw, pitch, roll); if (this.enablePointer) { this._flyYawDelta *= 1 - damping; this._flyPitchDelta *= 1 - damping; } else { this._flyYawDelta = 0; this._flyPitchDelta = 0; } if (!this.enableKeyboard) return; const x = this.getFlyAxis("right", "left"); const y = this.getFlyAxis("up", "down"); const z = this.getFlyAxis("forward", "backward"); const mag = Math.hypot(x, y, z); if (mag <= EPSILON2) return; const basis = this.computeFlyBasis(); const direction = vec3scl(vec3add(vec3add(vec3scl(basis.right, x), vec3scl(basis.up, y)), vec3scl(basis.forward, z)), 1 / mag); const speed = this.moveSpeed * this.getFlySpeedMultiplier() * dt; const position = vec3add(this.camera.position, vec3scl(direction, speed)); this.applyFlyPose(position, basis.forward, basis.up); } updateOrbit(dt) { const damping = this.enableDamping ? 1 - Math.pow(1 - clamp012(this.dampingFactor), dt * 60) : 1; const hasThetaInput = this.enableRotate && Math.abs(this._thetaDelta) > EPSILON2; const hasPhiInput = this.enableRotate && Math.abs(this._phiDelta) > EPSILON2; const hasRotationInput = hasThetaInput || hasPhiInput; const atTopPole = this._phi <= EPSILON2; const atBottomPole = this._phi >= Math.PI - EPSILON2; if (hasThetaInput && (atTopPole || atBottomPole)) { const interactiveMinPhi = Math.max(this.minPolarAngle, ORBIT_POLE_EPS); const interactiveMaxPhi = Math.min(this.maxPolarAngle, Math.PI - ORBIT_POLE_EPS); if (interactiveMinPhi <= interactiveMaxPhi) this._phi = atTopPole ? interactiveMinPhi : interactiveMaxPhi; } if (this.enableRotate) { this._theta += this._thetaDelta * damping; this._phi += this._phiDelta * damping; this._thetaDelta *= 1 - damping; this._phiDelta *= 1 - damping; } else { this._thetaDelta = 0; this._phiDelta = 0; } const exactPoleIdle = !hasRotationInput && (this._phi <= EPSILON2 || this._phi >= Math.PI - EPSILON2); const minPhi = exactPoleIdle ? Math.max(0, this.minPolarAngle) : Math.max(this.minPolarAngle, ORBIT_POLE_EPS); const maxPhi = exactPoleIdle ? Math.min(Math.PI, this.maxPolarAngle) : Math.min(this.maxPolarAngle, Math.PI - ORBIT_POLE_EPS); if (minPhi <= maxPhi) this._phi = clamp2(this._phi, minPhi, maxPhi); else this._phi = clamp2(this._phi, this.minPolarAngle, this.maxPolarAngle); this._theta = clamp2(this._theta, this.minAzimuthAngle, this.maxAzimuthAngle); this.applyDolly(damping, this.computeOrbitBasis()); this.applyPan(damping); this._radius = clamp2(this._radius, this.minDistance, this.maxDistance); const offset = this.sphericalToOffset(this._theta, this._phi, Math.max(EPSILON2, this._radius)); const position = vec3add(this.target, offset); const forward = vec3normalize(vec3sub(this.target, position), vec3scl(this._axisConvention.forward, -1)); const up = this.getOrbitUp(forward); this.camera.setWorldPosition(position[0], position[1], position[2]); this.camera.lookAtWithUp(this.target, up); if (this.camera.type === "orthographic") this.applyOrthographicZoom(); else this.relaxPerspectiveClipForZoom(); } updateTrackball(dt) { const damping = this.enableDamping ? 1 - Math.pow(1 - clamp012(this.dampingFactor), dt * 60) : 1; if (this.enableRotate) { const delta = this._trackballRotationDelta; if (!quatisid(delta[0], delta[1], delta[2], delta[3])) { const step = quatslerpid(delta[0], delta[1], delta[2], delta[3], damping); this._trackballEye = quatrotvec(this._trackballEye[0], this._trackballEye[1], this._trackballEye[2], step[0], step[1], step[2], step[3]); this._trackballUp = vec3normalize(quatrotvec(this._trackballUp[0], this._trackballUp[1], this._trackballUp[2], step[0], step[1], step[2], step[3]), this._axisConvention.up); const remainder = quatmul(delta[0], delta[1], delta[2], delta[3], ...quatinvert(step[0], step[1], step[2], step[3])); this._trackballRotationDelta = quatnormalize(remainder[0], remainder[1], remainder[2], remainder[3]); } } else this._trackballRotationDelta = [0, 0, 0, 1]; this.applyDolly(damping, this.computeTrackballBasis()); this.applyPan(damping); const radius = vec3mag(this._trackballEye); this._radius = clamp2(Math.max(EPSILON2, radius), this.minDistance, this.maxDistance); if (radius > EPSILON2) this._trackballEye = vec3scl(this._trackballEye, this._radius / radius); else this._trackballEye = vec3scl(this._axisConvention.forward, this._radius); const position = vec3add(this.target, this._trackballEye); this.camera.setWorldPosition(position[0], position[1], position[2]); this.camera.lookAtWithUp(this.target, this._trackballUp); if (this.camera.type === "orthographic") this.applyOrthographicZoom(); else this.relaxPerspectiveClipForZoom(); } emitChange() { for (const listener of this._changeListeners) try { listener(); } catch { } } setInteractionState(active) { if (this._interactionActive === active) return; this._interactionActive = active; for (const listener of this._interactionListeners) try { listener(active); } catch { } } clearWheelInteractionTimer() { if (this._wheelInteractionTimer === null) return; clearTimeout(this._wheelInteractionTimer); this._wheelInteractionTimer = null; } scheduleWheelInteractionEnd() { this.clearWheelInteractionTimer(); this._wheelInteractionTimer = setTimeout(() => { this._wheelInteractionTimer = null; if (this._pointerId === null && this._state === "none") this.setInteractionState(false); }, 120); } applyDolly(damping, basis) { if (!this.enableZoom) { this._dollyDelta = 0; return; } const dolly = this._dollyDelta * damping; if (Math.abs(dolly) <= EPSILON2) { this._dollyDelta *= 1 - damping; return; } const prevRadius = this._radius; const prevZoom = this._zoom; if (this.camera.type === "orthographic") this._zoom = clamp2(this._zoom * Math.exp(-dolly), this.minZoom, this.maxZoom); else { const next = clamp2(this._radius * Math.exp(dolly), this.minDistance, this.maxDistance); if (this._mode === "trackball") { const current = Math.max(EPSILON2, vec3mag(this._trackballEye)); this._trackballEye = vec3scl(this._trackballEye, next / current); } this._radius = next; } if (this.zoomOnCursor && this._zoomCursorValid) this.applyZoomOnCursor(prevRadius, prevZoom, basis); this._dollyDelta *= 1 - damping; } applyPan(damping) { if (!this.enablePan) { this._panOffset = [0, 0, 0]; return; } this.target[0] += this._panOffset[0] * damping; this.target[1] += this._panOffset[1] * damping; this.target[2] += this._panOffset[2] * damping; this._panOffset = vec3scl(this._panOffset, 1 - damping); } panOrbit(deltaX, deltaY) { const basis = this.computeOrbitBasis(); this.queuePan(deltaX, deltaY, basis); } panTrackball(deltaX, deltaY) { const basis = this.computeTrackballBasis(); this.queuePan(deltaX, deltaY, basis); } queuePan(deltaX, deltaY, basis) { const w = Math.max(1, this.getViewportWidth()); const h = Math.max(1, this.getViewportHeight()); let panX = 0; let panY = 0; if (this.camera.type === "orthographic") { const viewW = (this._orthoBaseRight - this._orthoBaseLeft) / Math.max(EPSILON2, this._zoom); const viewH = (this._orthoBaseTop - this._orthoBaseBottom) / Math.max(EPSILON2, this._zoom); panX = deltaX * viewW / w * this.panSpeed; panY = deltaY * viewH / h * this.panSpeed; } else { const camera = this.camera; const targetDistance = this._radius * Math.tan(camera.fov * Math.PI / 180 * 0.5); const aspect = !camera.autoAspect && Number.isFinite(camera.aspect) && camera.aspect > 0 ? camera.aspect : w / h; panX = 2 * deltaX * targetDistance * aspect / w * this.panSpeed; panY = 2 * deltaY * targetDistance / h * this.panSpeed; } this._panOffset = vec3add(this._panOffset, vec3add(vec3scl(basis.right, -panX), vec3scl(basis.up, panY))); } applyZoomOnCursor(prevRadius, prevZoom, basis) { const rect = this.getViewportRect(); const x01 = (this._zoomCursorClientX - rect.left) / rect.width; const y01 = (this._zoomCursorClientY - rect.top) / rect.height; const ndcX = x01 * 2 - 1; const ndcY = 1 - y01 * 2; if (this.camera.type === "orthographic") { const baseW = this._orthoBaseRight - this._orthoBaseLeft; const baseH = this._orthoBaseTop - this._orthoBaseBottom; const oldHalfW2 = baseW / Math.max(EPSILON2, prevZoom) * 0.5; const newHalfW2 = baseW / Math.max(EPSILON2, this._zoom) * 0.5; const oldHalfH2 = baseH / Math.max(EPSILON2, prevZoom) * 0.5; const newHalfH2 = baseH / Math.max(EPSILON2, this._zoom) * 0.5; this.target = vec3add(this.target, vec3add(vec3scl(basis.right, ndcX * (oldHalfW2 - newHalfW2)), vec3scl(basis.up, ndcY * (oldHalfH2 - newHalfH2)))); return; } const camera = this.camera; const tanHalfFov = Math.tan(camera.fov * Math.PI / 180 * 0.5); const aspect = !camera.autoAspect && Number.isFinite(camera.aspect) && camera.aspect > 0 ? camera.aspect : rect.width / rect.height; const oldHalfH = prevRadius * tanHalfFov; const newHalfH = this._radius * tanHalfFov; const oldHalfW = oldHalfH * aspect; const newHalfW = newHalfH * aspect; this.target = vec3add(this.target, vec3add(vec3scl(basis.right, ndcX * (oldHalfW - newHalfW)), vec3scl(basis.up, ndcY * (oldHalfH - newHalfH)))); } applyOrthographicZoom() { if (this.camera.type !== "orthographic") return; const camera = this.camera; const cx = (this._orthoBaseLeft + this._orthoBaseRight) * 0.5; const cy = (this._orthoBaseBottom + this._orthoBaseTop) * 0.5; const width = (this._orthoBaseRight - this._orthoBaseLeft) / Math.max(EPSILON2, this._zoom); const height = (this._orthoBaseTop - this._orthoBaseBottom) / Math.max(EPSILON2, this._zoom); camera.left = cx - width * 0.5; camera.right = cx + width * 0.5; camera.bottom = cy - height * 0.5; camera.top = cy + height * 0.5; } relaxPerspectiveClipForZoom() { if (this.camera.type !== "perspective") return; const camera = this.camera; const minNear = 1e-5; const desiredNear = Math.max(minNear, this._radius * 1e-3); const maxNear = Number.isFinite(camera.far) ? Math.max(minNear, camera.far - 0.01) : desiredNear; const nextNear = clamp2(desiredNear, minNear, maxNear); if (Math.abs(nextNear - camera.near) > Math.max(minNear, camera.near) * 1e-3) camera.near = nextNear; if (Number.isFinite(camera.far)) { const desiredFar = Math.max(this._radius * 4, camera.near + 0.01); if (desiredFar > camera.far) camera.far = desiredFar; } } offsetToSpherical(offset) { const localX = vec3dot(offset, this._axisConvention.right); const localY = vec3dot(offset, this._axisConvention.up); const localZ = vec3dot(offset, this._axisConvention.forward); const radius = Math.max(EPSILON2, Math.hypot(localX, localY, localZ)); return { theta: Math.atan2(localX, localZ), phi: Math.acos(clamp2(localY / radius, -1, 1)) }; } sphericalToOffset(theta, phi, radius) { const sinPhi = Math.sin(phi); const x = radius * sinPhi * Math.sin(theta); const y = radius * Math.cos(phi); const z = radius * sinPhi * Math.cos(theta); return vec3add(vec3add(vec3scl(this._axisConvention.right, x), vec3scl(this._axisConvention.up, y)), vec3scl(this._axisConvention.forward, z)); } computeLookBasis(forwardHint, upHint) { const forward = vec3normalize(forwardHint, vec3scl(this._axisConvention.forward, -1)); let up = vec3normalize(upHint, this._axisConvention.up); if (Math.abs(vec3dot(forward, up)) > 0.999) up = Math.abs(forward[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]; const right = vec3normalize(vec3cross(forward, up), this._axisConvention.right); const correctedUp = vec3normalize(vec3cross(right, forward), up); return { right, up: correctedUp, forward }; } computeOrbitBasis() { const offset = this.sphericalToOffset(this._theta, this._phi, Math.max(EPSILON2, this._radius)); const forward = vec3normalize(vec3scl(offset, -1), vec3scl(this._axisConvention.forward, -1)); return this.computeLookBasis(forward, this.getOrbitUp(forward)); } computeTrackballBasis() { const forward = vec3normalize(vec3scl(this._trackballEye, -1), vec3scl(this._axisConvention.forward, -1)); const basis = this.computeLookBasis(forward, this._trackballUp); this._trackballUp = basis.up; return basis; } computeFlyBasis() { const q = this.camera.transform.worldRotation; const forward = quatrotvec(0, 0, -1, q[0], q[1], q[2], q[3]); const up = quatrotvec(0, 1, 0, q[0], q[1], q[2], q[3]); return this.computeLookBasis(forward, up); } applyFlyRotation(yaw, pitch, roll) { let basis = this.computeFlyBasis(); let { right, up, forward } = basis; const globalYaw = this.yawMode === "global"; if (Math.abs(yaw) > EPSILON2) { const yawAxis = globalYaw ? this._axisConvention.up : up; forward = vec3rotate(forward, yawAxis, yaw); basis = this.computeLookBasis(forward, yawAxis); right = basis.right; up = basis.up; } if (Math.abs(pitch) > EPSILON2) { forward = vec3rotate(forward, right, pitch); up = vec3rotate(up, right, pitch); basis = this.computeLookBasis(forward, globalYaw ? this._axisConvention.up : up); right = basis.right; up = basis.up; } if (Math.abs(roll) > EPSILON2) { right = vec3rotate(right, forward, roll); up = vec3rotate(up, forward, roll); } basis = this.computeLookBasis(forward, up); this.applyFlyPose(vec3clone(this.camera.position), basis.forward, basis.up); } applyFlyPose(position, forward, up) { this.camera.setWorldPosition(position[0], position[1], position[2]); this.camera.lookAtWithUp(vec3add(position, forward), up); this.target = vec3add(position, vec3scl(forward, Math.max(EPSILON2, this._radius))); } getFlyAxis(positive, negative) { return (this.isFlyActionPressed(positive) ? 1 : 0) - (this.isFlyActionPressed(negative) ? 1 : 0); } getFlySpeedMultiplier() { let multiplier = 1; if (this.isFlyActionPressed("fast")) multiplier *= this.fastMultiplier; if (this.isFlyActionPressed("slow")) multiplier *= this.slowMultiplier; return multiplier; } isFlyActionPressed(action) { for (const code of this.keyMap[action]) if (this._flyPressedKeys.has(code)) return true; return false; } getOrbitUp(forward) { const worldUp = this._axisConvention.up; let upProj = vec3sub(worldUp, vec3scl(forward, vec3dot(worldUp, forward))); let mag = vec3mag(upProj); if (mag > EPSILON2) return vec3scl(upProj, 1 / mag); const currentUp = this.camera.up; upProj = vec3sub(currentUp, vec3scl(forward, vec3dot(currentUp, forward))); mag = vec3mag(upProj); if (mag > EPSILON2) return vec3scl(upProj, 1 / mag); const forwardAxis = this._axisConvention.forward; upProj = vec3sub(forwardAxis, vec3scl(forward, vec3dot(forwardAxis, forward))); mag = vec3mag(upProj); if (mag > EPSILON2) return vec3scl(upProj, 1 / mag); return vec3clone(this._axisConvention.right); } getTrackballVector(clientX, clientY) { const rect = this.getViewportRect(); const x = (clientX - rect.left) / rect.width * 2 - 1; const y = 1 - (clientY - rect.top) / rect.height * 2; const len2 = x * x + y * y; if (len2 <= 1) return [x, y, Math.sqrt(1 - len2)]; const inv = 1 / Math.sqrt(len2); return [x * inv, y * inv, 0]; } rotationFromTrackballDrag(start, end) { const dot = clamp2(vec3dot(start, end), -1, 1); let angle = Math.acos(dot); if (angle <= EPSILON2) return null; angle *= this.rotateSpeed; let axis = vec3cross(start, end); const axisLength = vec3mag(axis); if (axisLength <= EPSILON2) return null; axis = vec3scl(axis, 1 / axisLength); const basis = this.computeTrackballBasis(); const worldAxis = vec3normalize(vec3add(vec3add(vec3scl(basis.right, axis[0]), vec3scl(basis.up, axis[1])), vec3scl(vec3scl(basis.forward, -1), axis[2])), basis.right); const half = angle * 0.5; const sinHalf = Math.sin(half); return [worldAxis[0] * sinHalf, worldAxis[1] * sinHalf, worldAxis[2] * sinHalf, Math.cos(half)]; } resolveBounds(source) { return normalizeBounds(source); } getInspectionViewDirection(view) { switch (view) { case "front": return vec3clone(this._axisConvention.forward); case "back": return vec3scl(this._axisConvention.forward, -1); case "right": return vec3clone(this._axisConvention.right); case "left": return vec3scl(this._axisConvention.right, -1); case "top": return vec3clone(this._axisConvention.up); case "bottom": return vec3scl(this._axisConvention.up, -1); } } getInspectionViewUp(view) { if (view === "top") return vec3scl(this._axisConvention.forward, -1); if (view === "bottom") return vec3clone(this._axisConvention.forward); return vec3clone(this._axisConvention.up); } getCurrentViewOrientation() { const position = vec3clone(this.camera.position); const direction = vec3normalize(vec3sub(position, this.target), this._axisConvention.forward); return { direction, up: vec3normalize(this.camera.up, this.getOrbitUp(vec3scl(direction, -1))) }; } solveFit(bounds, options) { const padding = Math.max(1, options.padding ?? 1.1); const aspect = this.getAspect(options.aspect); const orientation = options.view ? { direction: this.getInspectionViewDirection(options.view), up: options.up ? vec3normalize(options.up, this._axisConvention.up) : this.getInspectionViewUp(options.view) } : { direction: options.eyeDirection ? vec3normalize(options.eyeDirection, this._axisConvention.forward) : this.getCurrentViewOrientation().direction, up: options.up ? vec3normalize(options.up, this._axisConvention.up) : this.getCurrentViewOrientation().up }; const working = options.boundsMode === "sphere" ? boundsFromSphere(bounds.sphereCenter, bounds.sphereRadius * padding, bounds.partial) : expandBounds(bounds, padding); const basis = this.computeLookBasis(vec3scl(orientation.direction, -1), orientation.up); const target = options.boundsMode === "sphere" ? vec3clone(bounds.sphereCenter) : getBoundsCenter(working); if (this.camera.type === "orthographic") return this.solveFitOrthographic(working, bounds, basis, target, aspect, options.minNear); return this.solveFitPerspective(working, bounds, basis, target, aspect, options.minNear); } solveFitPerspective(working, sourceBounds, basis, target, aspect, minNear) { const corners = getBoundsCorners(working); const camera = this.camera; const tanHalfV = Math.tan(camera.fov * Math.PI / 180 * 0.5); const tanHalfH = tanHalfV * Math.max(aspect, EPSILON2); let distance = sourceBounds.sphereRadius; const zs = []; for (const corner of corners) { const delta = vec3sub(corner, target); const x = vec3dot(delta, basis.right); const y = vec3dot(delta, basis.up); const z = vec3dot(delta, basis.forward); zs.push(z); distance = Math.max(distance, Math.abs(x) / Math.max(tanHalfH, EPSILON2) - z, Math.abs(y) / Math.max(tanHalfV, EPSILON2) - z); } distance = Math.max(distance, sourceBounds.sphereRadius, minNear ?? 0.01); const minDepth = Math.min(...zs.map((z) => distance + z)); const maxDepth = Math.max(...zs.map((z) => distance + z)); const nearFloor = minNear ?? 0.01; const depthPadding = Math.max(sourceBounds.sphereRadius * 0.05, 0.01); const stableRadius = Math.max(working.sphereRadius, sourceBounds.sphereRadius, 0.01); const stablePadding = Math.max(stableRadius * 0.1, depthPadding); const stableNear = distance - stableRadius - stablePadding; const stableFar = distance + stableRadius + stablePadding; const near = Math.max(nearFloor, Math.min(minDepth - depthPadding, stableNear)); const far = Math.max(near + 0.01, Math.max(maxDepth + depthPadding, stableFar)); return { position: vec3add(target, vec3scl(vec3scl(basis.forward, -1), distance)), target, up: basis.up, projection: { type: "perspective", near, far } }; } solveFitOrthographic(working, sourceBounds, basis, target, aspect, minNear) { const corners = getBoundsCorners(working); let halfWidth = 0; let halfHeight = 0; let minZ = Infinity; let maxZ = -Infinity; for (const corner of corners) { const delta = vec3sub(corner, target); const x = vec3dot(delta, basis.right); const y = vec3dot(delta, basis.up); const z = vec3dot(delta, basis.forward); halfWidth = Math.max(halfWidth, Math.abs(x)); halfHeight = Math.max(halfHeight, Math.abs(y)); if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; } if (aspect >= 1) halfWidth = Math.max(halfWidth, halfHeight * aspect); else halfHeight = Math.max(halfHeight, halfWidth / Math.max(aspect, EPSILON2)); const halfDepth = Math.max(Math.abs(minZ), Math.abs(maxZ), sourceBounds.sphereRadius); const distance = Math.max(halfDepth * 2 + Math.max(halfWidth, halfHeight), sourceBounds.sphereRadius * 2, 0.1); const nearFloor = minNear ?? 0.01; const depthPadding = Math.max(sourceBounds.sphereRadius * 0.05, 0.01); const stableRadius = Math.max(working.sphereRadius, sourceBounds.sphereRadius, 0.01); const stablePadding = Math.max(stableRadius * 0.1, depthPadding); const near = Math.max(nearFloor, Math.min(distance + minZ - depthPadding, distance - stableRadius - stablePadding)); const far = Math.max(near + 0.01, Math.max(distance + maxZ + depthPadding, distance + stableRadius + stablePadding)); return { position: vec3add(target, vec3scl(vec3scl(basis.forward, -1), distance)), target, up: basis.up, projection: { type: "orthographic", left: -Math.max(halfWidth, 0.01), right: Math.max(halfWidth, 0.01), bottom: -Math.max(halfHeight, 0.01), top: Math.max(halfHeight, 0.01), near, far } }; } }; var OrbitControls = class extends NavigationControls { constructor(camera, domElement, desc = {}) { super(camera, domElement, { ...desc, mode: "orbit" }); } }; var TrackballControls = class extends NavigationControls { constructor(camera, domElement, desc = {}) { super(camera, domElement, { ...desc, mode: "trackball" }); } }; var FlyControls = class extends NavigationControls { constructor(camera, domElement, desc = {}) { super(camera, domElement, { ...desc, mode: "fly" }); } }; // typescript/world/picking.ts var selectionKey = (objectId, elementIndex) => `${objectId}:${elementIndex}`; var toArray = (x) => { if (!x) return []; return Array.isArray(x) ? x : [x]; }; var toEntry = (hit) => ({ ...hit, key: selectionKey(hit.objectId, hit.elementIndex) }); var SelectionStore = class { entries = /* @__PURE__ */ new Map(); get size() { return this.entries.size; } has(objectId, elementIndex) { return this.entries.has(selectionKey(objectId, elementIndex)); } values() { return Array.from(this.entries.values()); } clear() { this.entries.clear(); return this; } replace(hit) { this.entries.clear(); this.add(hit); return this; } add(hit) { for (const h of toArray(hit)) this.entries.set(selectionKey(h.objectId, h.elementIndex), toEntry(h)); return this; } remove(hit) { for (const h of toArray(hit)) this.entries.delete(selectionKey(h.objectId, h.elementIndex)); return this; } toggle(hit) { for (const h of toArray(hit)) { const key = selectionKey(h.objectId, h.elementIndex); if (this.entries.has(key)) this.entries.delete(key); else this.entries.set(key, toEntry(h)); } return this; } apply(mode, hit) { switch (mode) { case "replace": return this.replace(hit); case "add": return this.add(hit); case "toggle": return this.toggle(hit); case "remove": return this.remove(hit); } } }; // typescript/core/engine.ts var WasmGPU = class _WasmGPU { renderer; effects; compute; python; scale; _performanceStats = null; _isRunning = false; _lastTime = 0; _frameCallback = null; _animationFrameId = null; constructor(renderer, desc) { this.renderer = renderer; this.effects = renderer.effects; const gpu = renderer.gpu; this.compute = new Compute(gpu.device, gpu.queue, desc); this.python = new PythonInterop(this.compute); this.scale = new ScaleService(this.compute); } static async create(canvas, descriptor = {}) { await initWebAssembly(); const renderer = await Renderer.create(canvas, descriptor); return new _WasmGPU(renderer, descriptor); } run(callback) { if (this._isRunning) return; this._isRunning = true; this._frameCallback = callback; this._lastTime = performance.now(); const loop = (now) => { if (!this._isRunning) return; frameArena.reset(); const dt = (now - this._lastTime) / 1e3; this._lastTime = now; const cpuStart = performance.now(); this._frameCallback?.(dt, now / 1e3, this); const cpuMs = performance.now() - cpuStart; this._performanceStats?.update(dt, cpuMs); this._animationFrameId = requestAnimationFrame(loop); }; this._animationFrameId = requestAnimationFrame(loop); } stop() { this._isRunning = false; if (this._animationFrameId !== null) { cancelAnimationFrame(this._animationFrameId); this._animationFrameId = null; } } get gpu() { return this.renderer.gpu; } get isRunning() { return this._isRunning; } get cullingStats() { return this.renderer.cullingStats; } static get driver() { return driver; } get driver() { return driver; } static get webassembly() { return webassemblyInterop; } get webassembly() { return webassemblyInterop; } static get webgpu() { return webgpuInterop; } get webgpu() { return webgpuInterop; } static get math() { return { mat4, mat4f, mat4d, quat, quatf, quatd, vec3, vec3f, vec3d }; } get math() { return { mat4, mat4f, mat4d, quat, quatf, quatd, vec3, vec3f, vec3d }; } static createHeapArena(capBytes, align = 16) { return driver.createHeapArena(capBytes, align); } createHeapArena(capBytes, align = 16) { return driver.createHeapArena(capBytes, align); } static get frameArena() { return frameArena; } get frameArena() { return frameArena; } static createSelectionStore() { return new SelectionStore(); } createSelectionStore() { return new SelectionStore(); } createPerformanceStats(desc = {}) { this._performanceStats?.destroy(); this.renderer.enableGpuTiming(desc.showGpuTime ?? true); const stats = new PerformanceStats({ getGpuTimeNs: () => this.renderer.gpuTimeNs, getCullingStats: () => this.renderer.cullingStats }, { canvas: this.renderer.canvas, ...desc }); this._performanceStats = stats; return stats; } get performanceStats() { return this._performanceStats; } destroyPerformanceStats() { this._performanceStats?.destroy(); this._performanceStats = null; this.renderer.enableGpuTiming(false); } render(scene, camera) { if (!this._isRunning) frameArena.reset(); this.renderer.render(scene, camera); } async warmup(options = {}) { const compute = options.compute ?? false; if (compute) throw new Error("WasmGPU.warmup: compute warmup is not implemented yet."); const render = options.render ?? true; if (!render) return; const { scene, camera } = options; if (!scene || !camera) throw new Error("WasmGPU.warmup: scene and camera are required when render warmup is enabled."); if (!this._isRunning) frameArena.reset(); this.renderer.warmup(scene, camera); } buildPickNdIndex(hit) { if (hit.kind === "pointcloud") return hit.object.mapLinearIndexToNd(hit.elementIndex); if (hit.kind === "glyphfield") return hit.object.mapLinearIndexToNd(hit.elementIndex); if (hit.kind === "splatfield") return hit.object.mapLinearIndexToNd(hit.elementIndex); if (hit.kind === "latticespace") return hit.object.mapLinearIndexToCell(hit.elementIndex); if (hit.kind === "nodelink") { const decoded = hit.object.decodePickElement(hit.elementIndex); if (!decoded || decoded.component !== "node") return null; return hit.object.mapLinearNodeIndexToNd(decoded.componentIndex); } return null; } buildPickAttributes(hit, includeAttributes) { if (!includeAttributes) return null; if (hit.kind === "pointcloud") { const rec = hit.object.getPointRecord(hit.elementIndex); if (!rec) return null; return { scalar: rec.scalar, packedPoint: rec.packed }; } if (hit.kind === "glyphfield") { const vector = hit.object.getAttributeRecord(hit.elementIndex); if (!vector) return null; return { vector }; } if (hit.kind === "nodelink") { const decoded = hit.object.decodePickElement(hit.elementIndex); if (!decoded) return null; if (decoded.component === "node") { const rec2 = hit.object.getNodeRecord(decoded.componentIndex); return { component: "node", componentIndex: decoded.componentIndex, scalar: rec2?.scalar ?? null, color: rec2?.color ?? null }; } const rec = hit.object.getEdgeRecord(decoded.componentIndex); return { component: "edge", componentIndex: decoded.componentIndex, scalar: rec?.scalar ?? null, color: rec?.color ?? null, edgeEndpoints: rec ? [rec.src, rec.dst] : null, edgePositions: rec && rec.srcPosition && rec.dstPosition ? [rec.srcPosition[0], rec.srcPosition[1], rec.srcPosition[2], rec.dstPosition[0], rec.dstPosition[1], rec.dstPosition[2]] : null }; } if (hit.kind === "splatfield") { const rec = hit.object.getSplatRecord(hit.elementIndex); if (!rec) return null; return { position: rec.position, rotation: rec.rotation, scale: rec.scale, opacity: rec.opacity, packedSplat: rec.packed, color: rec.color, sphericalHarmonicsDegree: rec.sphericalHarmonicsDegree, sphericalHarmonics: rec.sphericalHarmonics }; } if (hit.kind === "latticespace") { const record = hit.object.getCellRecord(hit.elementIndex); if (!record || record.values.length === 0) return null; const vector = [record.values[0] ?? 0, record.values[1] ?? 0, record.values[2] ?? 0, record.values[3] ?? 0]; return { scalar: record.scalar, vector, color: record.color }; } return null; } buildPickHit(hit, includeAttributes) { return { kind: hit.kind, object: hit.object, objectId: hit.objectId, elementIndex: hit.elementIndex, worldPosition: [hit.worldPosition[0], hit.worldPosition[1], hit.worldPosition[2]], ndIndex: this.buildPickNdIndex(hit), attributes: this.buildPickAttributes(hit, includeAttributes) }; } async pick(scene, camera, x, y, opts = {}) { const hit = await this.renderer.pick(scene, camera, x, y, opts); if (!hit) return null; const includeAttributes = opts.includeAttributes ?? true; return this.buildPickHit(hit, includeAttributes); } async pickRect(scene, camera, x0, y0, x1, y1, opts = {}) { const includeAttributes = opts.includeAttributes ?? true; const result = await this.renderer.pickRect(scene, camera, x0, y0, x1, y1, opts); return { mode: result.mode, hits: result.hits.map((hit) => this.buildPickHit(hit, includeAttributes)), truncated: result.truncated, bounds: { x: result.bounds.x, y: result.bounds.y, width: result.bounds.width, height: result.bounds.height }, sampledPixels: result.sampledPixels }; } async pickLasso(scene, camera, points, opts = {}) { const includeAttributes = opts.includeAttributes ?? true; const result = await this.renderer.pickLasso(scene, camera, points, opts); return { mode: result.mode, hits: result.hits.map((hit) => this.buildPickHit(hit, includeAttributes)), truncated: result.truncated, bounds: { x: result.bounds.x, y: result.bounds.y, width: result.bounds.width, height: result.bounds.height }, sampledPixels: result.sampledPixels }; } createScene(background) { return new Scene({ background }); } createCamera = { perspective: (options) => { return new PerspectiveCamera(options); }, orthographic: (options) => { return new OrthographicCamera(options); } }; createControls = { navigation: (camera, domElement, options) => { return new NavigationControls(camera, domElement, options); }, orbit: (camera, domElement, options) => { return new OrbitControls(camera, domElement, options); }, trackball: (camera, domElement, options) => { return new TrackballControls(camera, domElement, options); }, fly: (camera, domElement, options) => { return new FlyControls(camera, domElement, options); } }; createOverlay = { system: (options = {}) => { return new OverlaySystem({ canvas: this.renderer.canvas, ...options }); }, axisTriad: (descriptor = {}) => { return new AxisTriadLayer(descriptor); }, grid: (descriptor = {}) => { return new GridLayer(descriptor); }, legend: (descriptor) => { return new LegendLayer(descriptor); } }; createAnnotation = { toolkit: (options = {}) => { return new AnnotationToolkit({ pick: this.pick.bind(this), createOverlay: this.createOverlay }, { canvas: this.renderer.canvas, ...options }); } }; geometry = { custom: (descriptor) => { return new Geometry(descriptor); }, point: (size, plane, doubleSided) => { return Geometry.point(size, plane, doubleSided); }, line: (length, thickness, plane, doubleSided) => { return Geometry.line(length, thickness, plane, doubleSided); }, plane: (width, height, widthSegments, heightSegments) => { return Geometry.plane(width, height, widthSegments, heightSegments); }, triangle: (width, height, plane, doubleSided) => { return Geometry.triangle(width, height, plane, doubleSided); }, rectangle: (width, height, plane, doubleSided) => { return Geometry.rectangle(width, height, plane, doubleSided); }, circle: (radius, segments, plane, doubleSided) => { return Geometry.circle(radius, segments, plane, doubleSided); }, ellipse: (radiusX, radiusY, segments, plane, doubleSided) => { return Geometry.ellipse(radiusX, radiusY, segments, plane, doubleSided); }, box: (width, height, depth) => { return Geometry.box(width, height, depth); }, sphere: (radius, widthSegments, heightSegments) => { return Geometry.sphere(radius, widthSegments, heightSegments); }, cylinder: (radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded) => { return Geometry.cylinder(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded); }, pyramid: (baseWidth, baseDepth, height) => { return Geometry.pyramid(baseWidth, baseDepth, height); }, torus: (radius, tube, radialSegments, tubularSegments) => { return Geometry.torus(radius, tube, radialSegments, tubularSegments); }, prism: (radius, height, sides) => { return Geometry.prism(radius, height, sides); }, cartesianCurve: (descriptor) => { return Geometry.cartesianCurve(descriptor); }, cartesianSurface: (descriptor) => { return Geometry.cartesianSurface(descriptor); }, parametricCurve: (descriptor) => { return Geometry.parametricCurve(descriptor); }, parametricSurface: (descriptor) => { return Geometry.parametricSurface(descriptor); } }; material = { unlit: (options) => { return new UnlitMaterial(options); }, standard: (options) => { return new StandardMaterial(options); }, data: (options) => { return new DataMaterial(options); }, custom: (options) => { return new CustomMaterial(options); } }; texture = { create2D: (descriptor) => { return Texture2D.createFrom(descriptor); } }; createTransform() { return new Transform(); } createMesh(geometry, material) { return new Mesh(geometry, material); } createPointCloud(descriptor) { return new PointCloud(descriptor); } createGlyphField(descriptor) { return new GlyphField(descriptor); } createNodeLink(descriptor) { return new NodeLink(descriptor); } createSplatField(descriptor) { return new SplatField(descriptor); } createLatticeSpace(descriptor) { return new LatticeSpace(descriptor); } colormap = { builtin: (name) => { return Colormap.builtin(name); }, grayscale: () => { return Colormap.builtin("grayscale"); }, turbo: () => { return Colormap.builtin("turbo"); }, viridis: () => { return Colormap.builtin("viridis"); }, magma: () => { return Colormap.builtin("magma"); }, plasma: () => { return Colormap.builtin("plasma"); }, inferno: () => { return Colormap.builtin("inferno"); }, fromStops: (stops, desc = {}) => { return Colormap.fromStops(stops, desc); }, fromPalette: (colors, desc = {}) => { return Colormap.fromPalette(colors, desc); } }; createLight = { ambient: (options) => { return new AmbientLight(options); }, directional: (options) => { return new DirectionalLight(options); }, point: (options) => { return new PointLight(options); }, spot: (options) => { return new SpotLight(options); } }; gltf = { load: async (source, options) => { return loadGltf(source, options); }, import: async (doc, options) => { return importGltf(doc, options); }, loadAndImport: async (source, options = {}) => { const doc = await loadGltf(source, options.load); return importGltf(doc, options.import); }, parseGLB: (glb) => { return parseGLB(glb); }, readAccessor: (doc, accessorIndex) => { return readAccessor(doc, accessorIndex); }, readAccessorAsFloat32: (doc, accessorIndex) => { return readAccessorAsFloat32(doc, accessorIndex); }, readAccessorAsUint16: (doc, accessorIndex) => { return readAccessorAsUint16(doc, accessorIndex); }, readIndicesAsUint32: (doc, accessorIndex) => { return readIndicesAsUint32(doc, accessorIndex); } }; animation = { createClip: (descriptor) => { return new AnimationClip(descriptor); }, createPlayer: (clip, options) => { return new AnimationPlayer(clip, options); }, createSkin: (name, joints, inverseBindMatrices) => { return new Skin(name, joints, inverseBindMatrices); } }; destroy() { this.stop(); this.destroyPerformanceStats(); this.scale.clearCache(); this.compute.destroy(); this.renderer.destroy(); } }; export { AmbientLight, AnimationClip, AnimationPlayer, AnnotationAngleUnit, AnnotationKind, AnnotationLabelLayer, AnnotationMarkerRenderer, AnnotationMode, AnnotationStore, AnnotationToolkit, AxisConventions, AxisTriadLayer, BlendMode, CPUndarray, Camera, Colormap, Compute, ComputeKernels, ComputePipeline, CullMode, CustomMaterial, DataMaterial, DirectionalLight, FlyControls, GPUndarray, Geometry, GlyphField, GridLayer, LatticeSpace, LegendLayer, Light, Material, Mesh, NavigationControls, Ndarray, NodeLink, OrbitControls, OrthographicCamera, OverlaySystem, PerformanceStats, PerspectiveCamera, PointCloud, PointLight, PythonInterop, ReadbackRing, RenderEffects, Renderer, SCALE_UNIFORM_FLOAT_COUNT, ScaleService, Scene, SelectionStore, ShadowSystem, Skin, SkinInstance, SplatField, SpotLight, StandardMaterial, StorageBuffer, Texture2D, TrackballControls, Transform, TransformStore, UniformBuffer, UnlitMaterial, WasmGPU, WasmHeapArena, WasmMemoryView, WasmModule, WasmSlice, annotationAnchorFromHit, applyScaleTransformCPU, boundsFromBox, boundsFromBoxAndSphere, boundsFromSphere, cloneBounds, cloneScaleTransform, colorToCssRgba, computeAngleRadians, computeDistanceWorld, createAnnotationAnchor, cullf, decodeDataUri, WasmGPU as default, defaultScaleTransform, dirnameUrl, driver, dtypeInfo, emptyBounds, expandBounds, formatAngleRadians, formatDistanceWorld, formatFiniteNumber, formatWorldVector, frameArena, frustumf, getBoundsCenter, getBoundsCorners, getBoundsSize, importGltf, initWebAssembly, invertScaleTransformCPU, isDataUri, loadGltf, makeWorkgroupCounts, makeWorkgroupSize, mapAnnotationProbeReadout, mat4, ndarrayf, normalizeBounds, normalizeDirectoryUrl, normalizeScaleTransform, normalizeWorkgroups, packScaleTransform, parseGLB, quat, readAccessor, readAccessorAsFloat32, readAccessorAsUint16, readIndicesAsUint32, resolveAnnotationUnits, resolveScaleTransformDomainCPU, resolveUri, scaleClampModeToId, scaleModeToId, scaleValueModeToId, transformBounds, unionBounds, vec3, wasm, webassemblyInterop, webgpuInterop, workgroups1D, workgroups2D, workgroups3D };