/*! * WasmGPU v0.8.0 * 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://www.github.com/Zushah/WasmGPU * Website: https://zushah.github.io/WasmGPU */ // src/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 nextPow2 = (x) => { let v = Math.max(1, x | 0); v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; }; 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 }); // src/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/src/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 "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() { 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; this.freed = true; HEAP_SLICE_FINALIZER?.unregister(this); 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 "u32": wasm2.freeU32(ptr, len); break; case "i32": wasm2.freeU32(ptr, len); break; case "u8": wasm2.freeBytes(ptr, len); break; } 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) 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; 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()); } 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 "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); }, 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()); }, 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), 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), 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; }, readF32Array: (ptr, len) => Array.from(ensure().f32view(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; }, 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 = { deinterleave: (outPtr, srcPtr, count, numComponents, componentBytes, byteStride) => { ensure().accessor_deinterleave(outPtr >>> 0, srcPtr >>> 0, count >>> 0, numComponents >>> 0, componentBytes >>> 0, byteStride >>> 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().mat4_abs(out >>> 0, m >>> 0); }, add: (out, a, b) => { ensure().mat4_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, m) => { ensure().mat4_copy(out >>> 0, m >>> 0); }, decomposeTRS: (outTrs, m) => { ensure().mat4_decompose_trs(outTrs >>> 0, m >>> 0); }, det: (m) => ensure().mat4_det(m >>> 0), identity: (out) => { ensure().mat4_identity(out >>> 0); }, init: (out, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) => { ensure().mat4_init(out >>> 0, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15); }, invert: (out, m) => { ensure().mat4_invert(out >>> 0, m >>> 0); }, isEqual: (a, b) => bool(ensure().mat4_isEqual(a >>> 0, b >>> 0)), isIdentity: (m) => bool(ensure().mat4_isIdentity(m >>> 0)), isInverse: (a, b) => bool(ensure().mat4_isInverse(a >>> 0, b >>> 0)), isZero: (m) => bool(ensure().mat4_isZero(m >>> 0)), lookAt: (out, eye3, center3, up3) => { ensure().mat4_lookAt(out >>> 0, eye3 >>> 0, center3 >>> 0, up3 >>> 0); }, mul: (out, a, b) => { ensure().mat4_mul(out >>> 0, a >>> 0, b >>> 0); }, mulVec4: (outVec4, m, v4) => { ensure().mat4_mul_vec4(outVec4 >>> 0, m >>> 0, v4 >>> 0); }, neg: (out, m) => { ensure().mat4_neg(out >>> 0, m >>> 0); }, norm: (m) => ensure().mat4_norm(m >>> 0), normalize: (out, m) => { ensure().mat4_normalize(out >>> 0, m >>> 0); }, normsq: (m) => ensure().mat4_normsq(m >>> 0), perspective: (out, fovY, aspect, near, far) => { ensure().mat4_perspective(out >>> 0, fovY, aspect, near, far); }, random: (out) => { ensure().mat4_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().mat4_random_range(out >>> 0, min, max); }, rotateX: (out, m, angle) => { ensure().mat4_rotateX(out >>> 0, m >>> 0, angle); }, rotateY: (out, m, angle) => { ensure().mat4_rotateY(out >>> 0, m >>> 0, angle); }, rotateZ: (out, m, angle) => { ensure().mat4_rotateZ(out >>> 0, m >>> 0, angle); }, round: (out, m) => { ensure().mat4_round(out >>> 0, m >>> 0); }, scl: (out, m, scalar) => { ensure().mat4_scl(out >>> 0, m >>> 0, scalar); }, sub: (out, a, b) => { ensure().mat4_sub(out >>> 0, a >>> 0, b >>> 0); }, trace: (m) => ensure().mat4_trace(m >>> 0), translate: (out, m, v3) => { ensure().mat4_translate(out >>> 0, m >>> 0, v3 >>> 0); }, transpose: (out, m) => { ensure().mat4_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 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().quat_abs(out >>> 0, q >>> 0); }, add: (out, a, b) => { ensure().quat_add(out >>> 0, a >>> 0, b >>> 0); }, copy: (out, q) => { ensure().quat_copy(out >>> 0, q >>> 0); }, dist: (a, b) => ensure().quat_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().quat_distsq(a >>> 0, b >>> 0), fromAxisAngle: (out, axis3, angle) => { ensure().quat_fromAxisAngle(out >>> 0, axis3 >>> 0, angle); }, init: (out, x, y, z, w) => { ensure().quat_init(out >>> 0, x, y, z, w); }, invert: (out, q) => { ensure().quat_invert(out >>> 0, q >>> 0); }, isEqual: (a, b) => bool(ensure().quat_isEqual(a >>> 0, b >>> 0)), isNormalized: (q) => bool(ensure().quat_isNormalized(q >>> 0)), isZero: (q) => bool(ensure().quat_isZero(q >>> 0)), mul: (out, a, b) => { ensure().quat_mul(out >>> 0, a >>> 0, b >>> 0); }, neg: (out, q) => { ensure().quat_neg(out >>> 0, q >>> 0); }, norm: (q) => ensure().quat_norm(q >>> 0), normalize: (out, q) => { ensure().quat_normalize(out >>> 0, q >>> 0); }, normscl: (out, q, scalar) => { ensure().quat_normscl(out >>> 0, q >>> 0, scalar); }, normsq: (q) => ensure().quat_normsq(q >>> 0), random: (out) => { ensure().quat_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().quat_random_range(out >>> 0, min, max); }, round: (out, q) => { ensure().quat_round(out >>> 0, q >>> 0); }, scl: (out, q, scalar) => { ensure().quat_scl(out >>> 0, q >>> 0, scalar); }, slerp: (out, a, b, t) => { ensure().quat_slerp(out >>> 0, a >>> 0, b >>> 0, t); }, sub: (out, a, b) => { ensure().quat_sub(out >>> 0, a >>> 0, b >>> 0); }, toRotation: (outVec3, q, v3) => { ensure().quat_toRotation(outVec3 >>> 0, q >>> 0, v3 >>> 0); }, print: (q) => { const a = wasm.f32view(q, 4); console.log(`[ ${a[0]} ${a[1]} ${a[2]} ${a[3]} ]`); } }; 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(4), view3: (ptr) => wasm.f32view(ptr, 3), view4: (ptr) => wasm.f32view(ptr, 4), set3: (ptr, src) => wasm.writeF32(ptr, 3, src), abs: (out, v) => { ensure().vec3_abs(out >>> 0, v >>> 0); }, add: (out, a, b) => { ensure().vec3_add(out >>> 0, a >>> 0, b >>> 0); }, ang: (out, v) => { ensure().vec3_ang(out >>> 0, v >>> 0); }, angBetween: (a, b) => ensure().vec3_angBetween(a >>> 0, b >>> 0), copy: (out, v) => { ensure().vec3_copy(out >>> 0, v >>> 0); }, cross: (out, a, b) => { ensure().vec3_cross(out >>> 0, a >>> 0, b >>> 0); }, dist: (a, b) => ensure().vec3_dist(a >>> 0, b >>> 0), distsq: (a, b) => ensure().vec3_distsq(a >>> 0, b >>> 0), dot: (a, b) => ensure().vec3_dot(a >>> 0, b >>> 0), init: (out, x, y, z) => { ensure().vec3_init(out >>> 0, x, y, z); }, interp: (out, v, a, b, c) => { ensure().vec3_interp(out >>> 0, v >>> 0, a, b, c); }, isEqual: (a, b) => bool(ensure().vec3_isEqual(a >>> 0, b >>> 0)), isNormalized: (v) => bool(ensure().vec3_isNormalized(v >>> 0)), isOrthogonal: (a, b) => bool(ensure().vec3_isOrthogonal(a >>> 0, b >>> 0)), isParallel: (a, b) => bool(ensure().vec3_isParallel(a >>> 0, b >>> 0)), isZero: (v) => bool(ensure().vec3_isZero(v >>> 0)), neg: (out, v) => { ensure().vec3_neg(out >>> 0, v >>> 0); }, norm: (v) => ensure().vec3_norm(v >>> 0), normalize: (out, v) => { ensure().vec3_normalize(out >>> 0, v >>> 0); }, normscl: (out, v, scalar) => { ensure().vec3_normscl(out >>> 0, v >>> 0, scalar); }, normsq: (v) => ensure().vec3_normsq(v >>> 0), oproj: (out, a, b) => { ensure().vec3_oproj(out >>> 0, a >>> 0, b >>> 0); }, proj: (out, a, b) => { ensure().vec3_proj(out >>> 0, a >>> 0, b >>> 0); }, random: (out) => { ensure().vec3_random(out >>> 0); }, randomRange: (out, min, max) => { ensure().vec3_random_range(out >>> 0, min, max); }, reflect: (out, a, b) => { ensure().vec3_reflect(out >>> 0, a >>> 0, b >>> 0); }, refract: (out, a, b, refractiveIndex) => { ensure().vec3_refract(out >>> 0, a >>> 0, b >>> 0, refractiveIndex); }, round: (out, v) => { ensure().vec3_round(out >>> 0, v >>> 0); }, scl: (out, v, scalar) => { ensure().vec3_scl(out >>> 0, v >>> 0, scalar); }, sub: (out, a, b) => { ensure().vec3_sub(out >>> 0, a >>> 0, b >>> 0); }, print: (v) => { const a = wasm.f32view(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) }; // src/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.fill(0); dstBytes.set(new Uint8Array(src.buffer, src.offset, src.size), 0); 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.fill(0); dstBytes.set(new Uint8Array(src.buffer, src.offset, src.size), 0); buffer.unmap(); } super(device, queue, buffer, byteLength, usage); this.label = desc.label ?? null; } }; // src/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: "f64" } }; 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})`); 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 ND_ERROR = 4294967295; var CPUndarray = class _CPUndarray extends Ndarray { basePtrBytes; shapePtr; stridesPtr; indicesPtr; _buf = null; _all = null; constructor(dtype, shape, stridesBytes, offsetBytes, byteLength, basePtrBytes, shapePtr, stridesPtr, indicesPtr) { super(dtype, shape, stridesBytes, offsetBytes, byteLength); this.basePtrBytes = basePtrBytes; this.shapePtr = shapePtr; this.stridesPtr = stridesPtr; this.indicesPtr = indicesPtr; } 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 shapePtr = wasm.allocU32(shape.length >>> 0); const stridesPtr = wasm.allocU32(shape.length >>> 0); const indicesPtr = wasm.allocU32(shape.length >>> 0); const shapeView = wasm.u32view(shapePtr, shape.length >>> 0); for (let i = 0; i < shape.length; i++) shapeView[i] = shape[i] >>> 0; const strideView = wasm.i32view(stridesPtr, shape.length >>> 0); for (let i = 0; i < stridesBytes.length; i++) strideView[i] = stridesBytes[i] | 0; const basePtrBytes = byteLength > 0 ? wasm.allocBytes(byteLength >>> 0) : 0; return new _CPUndarray(dtype, shape, stridesBytes, offsetBytes, byteLength, basePtrBytes, shapePtr, stridesPtr, indicesPtr); } static zeros(dtype, layout) { const a = _CPUndarray.empty(dtype, layout); a.zero_(); return a; } static fromArray(dtype, shape, src) { const dst = _CPUndarray.empty(dtype, { shape }); 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; } get residency() { return "cpu-webassembly"; } ensureAllView() { 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() { if (this.byteLength === 0) return new Uint8Array(wasm.memory().buffer, 0, 0); return wasm.u8view(this.basePtrBytes, this.byteLength >>> 0); } data() { 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) { assert(indices.length === this.ndim, `expected ${this.ndim} indices, got ${indices.length}`); if (this.ndim === 0) return this.offsetBytes; const idxView = wasm.u32view(this.indicesPtr, this.ndim >>> 0); 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})`); idxView[i] = v >>> 0; } const off = ndarrayf.offsetBytes(this.shapePtr, this.stridesPtr, this.indicesPtr, this.ndim >>> 0, this.offsetBytes >>> 0); assert(off !== ND_ERROR, "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_() { 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); } }; var GPUndarray = class _GPUndarray extends Ndarray { buffer; baseOffsetBytes; owned; constructor(dtype, shape, stridesBytes, offsetBytes, byteLength, buffer, baseOffsetBytes = 0, owned = false) { 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; } 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); } 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 bytes = await this.buffer.read(this.baseOffsetBytes, this.byteLength); const cpu = CPUndarray.empty(this.dtype, { shape: this.shape, stridesBytes: this.stridesBytes, offsetBytes: this.offsetBytes }); cpu.backingBytes().set(new Uint8Array(bytes), 0); return cpu; } destroy() { if (this.owned) this.buffer.destroy(); } }; // src/wasm/interop.ts 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 = { fromInstance: (instance, options = {}) => WasmModule.fromInstance(instance, options), fromExports: (exportsObject, options = {}) => WasmModule.fromExports(exportsObject, options), fromMemory: (memory, options = {}) => WasmModule.fromMemory(memory, options) }; // src/core/transform.ts var NO_PARENT = 4294967295; 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) { this.posPtr = wasm.allocF32(cap * 3); this.rotPtr = wasm.allocF32(cap * 4); this.sclPtr = wasm.allocF32(cap * 3); this.localPtr = wasm.allocF32(cap * 16); this.worldPtr = wasm.allocF32(cap * 16); this.parentPtr = wasm.allocU32(cap); this.orderPtr = wasm.allocU32(cap); this.tmpAxisPtr = wasm.allocF32(4); this.tmpQuatPtr = wasm.allocF32(4); this.ensureViews(); const u32 = this.u32(); const parentBase = this.parentPtr >>> 2; for (let i = 0; i < cap; i++) u32[parentBase + i] = NO_PARENT; } 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; this.dirtyIndicesPtr = wasm.allocU32(cap); this.dirtyIndicesCap = cap; } 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 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.cap = newCap; this.allocateArrays(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); 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; 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; } get worldPosition() { this.assertAlive(); const T = TransformStore.global(); T.updateIfNeeded(); const base = (T.worldPtr >>> 2) + this.index * 16; const f32 = T.f32(); return [f32[base + 12], f32[base + 13], f32[base + 14]]; } 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; } }; // src/graphics/geometry.ts var _boundsPosPtr = 0; var _boundsPosCap = 0; var _boundsBoxMinPtr = 0; var _boundsBoxMaxPtr = 0; var _boundsSphereCenterPtr = 0; var _boundsSphereRadiusPtr = 0; var _normPosPtr = 0; var _normPosCap = 0; var _normIdxPtr = 0; var _normIdxCap = 0; var _normOutPtr = 0; var _normOutCap = 0; var ensureBoundsScratch = (posLenF32) => { if (_boundsPosCap < posLenF32) { _boundsPosCap = nextPow2(posLenF32); _boundsPosPtr = wasm.allocF32(_boundsPosCap); } if (_boundsBoxMinPtr === 0) _boundsBoxMinPtr = wasm.allocF32(3); if (_boundsBoxMaxPtr === 0) _boundsBoxMaxPtr = wasm.allocF32(3); if (_boundsSphereCenterPtr === 0) _boundsSphereCenterPtr = wasm.allocF32(3); if (_boundsSphereRadiusPtr === 0) _boundsSphereRadiusPtr = wasm.allocF32(1); }; var ensureNormalScratch = (posLenF32, idxLenU32) => { if (_normPosCap < posLenF32) { _normPosCap = nextPow2(posLenF32); _normPosPtr = wasm.allocF32(_normPosCap); } if (_normOutCap < posLenF32) { _normOutCap = nextPow2(posLenF32); _normOutPtr = wasm.allocF32(_normOutCap); } if (idxLenU32 > 0 && _normIdxCap < idxLenU32) { _normIdxCap = nextPow2(idxLenU32); _normIdxPtr = wasm.allocU32(_normIdxCap); } }; var computeGeometryBounds = (positions) => { const vertexCount = positions.length / 3 | 0; if (vertexCount <= 0) return { boxMin: [0, 0, 0], boxMax: [0, 0, 0], sphereCenter: [0, 0, 0], sphereRadius: 0 }; ensureBoundsScratch(positions.length); wasm.f32view(_boundsPosPtr, positions.length).set(positions); boundsf.geometryPositions(_boundsBoxMinPtr, _boundsBoxMaxPtr, _boundsSphereCenterPtr, _boundsSphereRadiusPtr, _boundsPosPtr, vertexCount); const boxMin = wasm.f32view(_boundsBoxMinPtr, 3); const boxMax = wasm.f32view(_boundsBoxMaxPtr, 3); const sphereCenter = wasm.f32view(_boundsSphereCenterPtr, 3); const sphereRadius = wasm.f32view(_boundsSphereRadiusPtr, 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] }; }; var computeGeometryVertexNormals = (positions, indices) => { const vertexCount = positions.length / 3 | 0; const idxLen = indices ? indices.length | 0 : 0; ensureNormalScratch(positions.length, idxLen); wasm.f32view(_normPosPtr, positions.length).set(positions); const idxPtr = indices && idxLen > 0 ? _normIdxPtr : 0; if (indices && idxLen > 0) wasm.u32view(_normIdxPtr, idxLen).set(indices); meshf.computeVertexNormals(_normOutPtr, _normPosPtr, vertexCount, idxPtr, idxLen); const out = new Float32Array(positions.length); out.set(wasm.f32view(_normOutPtr, positions.length)); return out; }; 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 = class _Geometry { positions; normals; tangents; uvs; uvs1; joints; weights; joints1; weights1; _jointsBuffer = null; _weightsBuffer = null; _joints1Buffer = null; _weights1Buffer = null; _skinInfluenceBuffer = null; indices; morphTargets; authoredNormals; vertexCount; indexCount; _boundsMin; _boundsMax; _boundsCenter; _boundsRadius; _positionBuffer = null; _normalBuffer = null; _tangentBuffer = null; _uvBuffer = null; _uv1Buffer = null; _indexBuffer = null; _device = null; _refCount = 1; _destroyed = false; constructor(descriptor) { this.positions = descriptor.positions; this.vertexCount = Math.floor(this.positions.length / 3); if (this.positions.length !== this.vertexCount * 3) console.warn(`[Geometry] positions length ${this.positions.length} is not divisible by 3; trailing components ignored.`); const expectedNormalLength = this.vertexCount * 3; let authoredNormals = descriptor.normals ? descriptor.authoredNormals ?? true : false; let normals = descriptor.normals ?? new Float32Array(expectedNormalLength); let fallbackNormals = !descriptor.normals; if (normals.length !== expectedNormalLength) { console.warn(`[Geometry] normals length mismatch (got ${normals.length}, expected ${expectedNormalLength}). Using fallback normals.`); normals = new Float32Array(expectedNormalLength); authoredNormals = false; fallbackNormals = true; } if (fallbackNormals) for (let i = 1; i < normals.length; i += 3) normals[i] = 1; this.authoredNormals = authoredNormals; this.normals = normals; const expectedTangentLength = this.vertexCount * 4; let tangents = descriptor.tangents ?? null; if (tangents && tangents.length !== expectedTangentLength) { console.warn(`[Geometry] tangents length mismatch (got ${tangents.length}, expected ${expectedTangentLength}). Using fallback tangents.`); tangents = null; } const expectedUvLength = this.vertexCount * 2; let uvs = descriptor.uvs ?? new Float32Array(expectedUvLength); if (uvs.length !== expectedUvLength) { console.warn(`[Geometry] uvs length mismatch (got ${uvs.length}, expected ${expectedUvLength}). TEXCOORD_0 disabled.`); uvs = new Float32Array(expectedUvLength); } this.uvs = uvs; this.tangents = tangents ?? createDerivativeFallbackTangents(this.vertexCount); let uvs1 = descriptor.uvs1 ?? new Float32Array(expectedUvLength); if (uvs1.length !== expectedUvLength) { console.warn(`[Geometry] uvs1 length mismatch (got ${uvs1.length}, expected ${expectedUvLength}). TEXCOORD_1 disabled.`); uvs1 = new Float32Array(expectedUvLength); } this.uvs1 = uvs1; this.morphTargets = descriptor.morphTargets ?? []; let joints = descriptor.joints ?? null; let weights = descriptor.weights ?? null; const expected = this.vertexCount * 4; if (joints && !weights || !joints && weights) { 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; weights = null; } if (weights && weights.length !== expected) { console.warn(`[Geometry] weights length mismatch (got ${weights.length}, expected ${expected}). Skinning disabled.`); joints = null; weights = null; } this.joints = joints; this.weights = weights; let joints1 = descriptor.joints1 ?? null; let weights1 = descriptor.weights1 ?? null; if (joints1 && !weights1 || !joints1 && weights1) { console.warn(`[Geometry] JOINTS_1/WEIGHTS_1 must be provided together. Ignoring additional influences.`); joints1 = null; weights1 = null; } if ((joints1 || weights1) && (!joints || !weights)) { 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; weights1 = null; } if (weights1 && weights1.length !== expected) { console.warn(`[Geometry] weights1 length mismatch (got ${weights1.length}, expected ${expected}). Ignoring additional influences.`); joints1 = null; weights1 = null; } this.joints1 = joints1; this.weights1 = weights1; this.indices = descriptor.indices ?? null; this.indexCount = this.indices?.length ?? this.vertexCount; const bounds = computeGeometryBounds(this.positions); this._boundsMin = bounds.boxMin; this._boundsMax = bounds.boxMax; this._boundsCenter = bounds.sphereCenter; this._boundsRadius = bounds.sphereRadius; } assertAlive(action) { if (this._destroyed) throw new Error(`Geometry: cannot ${action}; resource has already been released.`); } 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"); if (this._device === device) return; this._device = device; this._positionBuffer = createBuffer(device, this.positions, GPUBufferUsage.VERTEX); this._normalBuffer = createBuffer(device, this.normals, GPUBufferUsage.VERTEX); this._tangentBuffer = createBuffer(device, this.tangents, GPUBufferUsage.VERTEX); this._uvBuffer = createBuffer(device, this.uvs, GPUBufferUsage.VERTEX); this._uv1Buffer = createBuffer(device, this.uvs1, GPUBufferUsage.VERTEX); if (this.joints) this._jointsBuffer = createBuffer(device, this.joints, GPUBufferUsage.VERTEX); if (this.weights) this._weightsBuffer = createBuffer(device, this.weights, GPUBufferUsage.VERTEX); if (this.joints1) this._joints1Buffer = createBuffer(device, this.joints1, GPUBufferUsage.VERTEX); if (this.weights1) this._weights1Buffer = createBuffer(device, this.weights1, GPUBufferUsage.VERTEX); if (this.joints && this.weights) this._skinInfluenceBuffer = createBuffer(device, packSkinInfluences(this.joints, this.weights, this.joints1, this.weights1), GPUBufferUsage.VERTEX); if (this.indices) this._indexBuffer = createBuffer(device, this.indices, GPUBufferUsage.INDEX); } 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 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 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._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._uvBuffer = null; this._uv1Buffer = null; this._indexBuffer = null; this._device = null; } 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; 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 }; } }; // src/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); }; // src/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) => { if (!runtime.dirty) return false; runtime.positions.set(geometry.positions); 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; } if (runtime.recomputeNormals) runtime.normals.set(computeGeometryVertexNormals(runtime.positions, geometry.indices)); else if (runtime.hasNormalTargets) { runtime.normals.set(geometry.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.normals); 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.gpuDirty = true; return true; }; var destroyMeshMorphBuffers = (runtime) => { runtime.positionBuffer?.destroy(); runtime.normalBuffer?.destroy(); runtime.positionBuffer = null; runtime.normalBuffer = null; runtime.device = null; }; var initializeMeshMorphRuntime = (mesh, weights) => { const targetCount = mesh.geometry.morphTargets.length | 0; if (targetCount <= 0) return; const runtime = { targetCount, weights: resolveWeights(weights, targetCount), positions: new Float32Array(mesh.geometry.positions), normals: new Float32Array(mesh.geometry.normals), device: null, positionBuffer: null, normalBuffer: null, dirty: true, gpuDirty: true, hasNormalTargets: mesh.geometry.morphTargets.some((target) => !!target.normals), recomputeNormals: !mesh.geometry.authoredNormals, 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; }; 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; }; 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 }; } const updated = updateMeshMorphCPUState(runtime, mesh.geometry); if (runtime.device !== device || !runtime.positionBuffer || !runtime.normalBuffer) { 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.device = device; runtime.gpuDirty = false; } else if (updated || runtime.gpuDirty) { queue.writeBuffer(runtime.positionBuffer, 0, runtime.positions.buffer, runtime.positions.byteOffset, runtime.positions.byteLength); queue.writeBuffer(runtime.normalBuffer, 0, runtime.normals.buffer, runtime.normals.byteOffset, runtime.normals.byteLength); runtime.gpuDirty = false; } return { positionBuffer: runtime.positionBuffer, normalBuffer: runtime.normalBuffer }; }; 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); }; // src/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") }) }; // src/wgsl/graphics/unlit.wgsl var unlit_default = "struct MaterialUniforms { color: vec4f, params: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseSampler: sampler; @group(1) @binding(2) var baseTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) uv1: vec2f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; fn linearToSrgb(c: vec3f) -> vec3f { return pow(c, vec3f(1.0 / 2.2)); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; out.position = camera.viewProjection * model.model * vec4f(in.position, 1.0); out.normal = (model.normalMatrix * vec4f(in.normal, 0.0)).xyz; out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let texColor = textureSample(baseTex, baseSampler, baseUv); var outColor = material.color * texColor; let alphaCutoff = material.params.x; if (alphaCutoff > 0.0 && outColor.a < alphaCutoff) { discard; } return vec4f(linearToSrgb(outColor.rgb), outColor.a); }"; // src/wgsl/graphics/unlit-instanced.wgsl var unlit_instanced_default = "struct MaterialUniforms { color: vec4f, params: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseSampler: sampler; @group(1) @binding(2) var baseTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(3) m0: vec4f, @location(4) m1: vec4f, @location(5) m2: vec4f, @location(6) m3: vec4f, @location(7) n0: vec4f, @location(8) n1: vec4f, @location(9) n2: vec4f, @location(10) n3: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) uv1: vec2f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; @group(0) @binding(0) var camera: CameraUniforms; fn linearToSrgb(c: vec3f) -> vec3f { return pow(c, vec3f(1.0 / 2.2)); } @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let modelM = mat4x4f(in.m0, in.m1, in.m2, in.m3); let normalM = mat4x4f(in.n0, in.n1, in.n2, in.n3); out.position = camera.viewProjection * modelM * vec4f(in.position, 1.0); out.normal = (normalM * vec4f(in.normal, 0.0)).xyz; out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let texColor = textureSample(baseTex, baseSampler, baseUv); var outColor = material.color * texColor; let alphaCutoff = material.params.x; if (alphaCutoff > 0.0 && outColor.a < alphaCutoff) { discard; } return vec4f(linearToSrgb(outColor.rgb), outColor.a); }"; // src/wgsl/graphics/unlit-skinned.wgsl var unlit_skinned_default = "struct MaterialUniforms { color: vec4f, params: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTexture: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(3) joints: vec4u, @location(4) weights: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, @location(1) uv1: vec2f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec4f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; struct SkinBuffer { joints: array, }; @group(2) @binding(0) var skin: SkinBuffer; @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 localPos = m * vec4f(in.position, 1.0); out.position = camera.viewProjection * model.model * localPos; out.uv = in.uv; out.uv1 = in.uv1; return out; } fn linearToSrgb(c: vec3f) -> vec3f { return pow(c, vec3f(1.0 / 2.2)); } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let baseColorSample = textureSample(baseColorTexture, baseColorSampler, baseUv); var outColor = material.color * baseColorSample; let alphaCutoff = material.params.x; if (alphaCutoff > 0.0 && outColor.a < alphaCutoff) { discard; } return vec4f(linearToSrgb(outColor.rgb), outColor.a); }"; // src/wgsl/graphics/unlit-skinned8.wgsl var unlit_skinned8_default = "struct MaterialUniforms { color: vec4f, params: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTexture: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(3) joints0: vec4u, @location(4) weights0: vec4f, @location(5) joints1: vec4u, @location(6) weights1: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, @location(1) uv1: vec2f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec4f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; struct SkinBuffer { joints: array, }; @group(2) @binding(0) var skin: SkinBuffer; @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 localPos = m * vec4f(in.position, 1.0); out.position = camera.viewProjection * model.model * localPos; out.uv = in.uv; out.uv1 = in.uv1; return out; } fn linearToSrgb(c: vec3f) -> vec3f { return pow(c, vec3f(1.0 / 2.2)); } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let baseColorSample = textureSample(baseColorTexture, baseColorSampler, baseUv); var outColor = material.color * baseColorSample; let alphaCutoff = material.params.x; if (alphaCutoff > 0.0 && outColor.a < alphaCutoff) { discard; } return vec4f(linearToSrgb(outColor.rgb), outColor.a); }"; // src/wgsl/graphics/standard.wgsl var standard_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var sheenColorSampler: sampler; @group(1) @binding(22) var sheenColorTex: texture_2d; @group(1) @binding(23) var sheenRoughnessSampler: sampler; @group(1) @binding(24) var sheenRoughnessTex: texture_2d; @group(1) @binding(25) var iridescenceSampler: sampler; @group(1) @binding(26) var iridescenceTex: texture_2d; @group(1) @binding(27) var iridescenceThicknessSampler: sampler; @group(1) @binding(28) var iridescenceThicknessTex: texture_2d; @group(1) @binding(29) var anisotropySampler: sampler; @group(1) @binding(30) var anisotropyTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let worldPos4 = model.model * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(in.normal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb * textureSample(sheenColorTex, sheenColorSampler, sheenColorUv).rgb; let sheenRoughness = clamp(material.sheenParams.w * textureSample(sheenRoughnessTex, sheenRoughnessSampler, sheenRoughnessUv).a, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x * textureSample(iridescenceTex, iridescenceSampler, iridescenceUv).r, 0.0, 1.0); let iridescenceThicknessSample = textureSample(iridescenceThicknessTex, iridescenceThicknessSampler, iridescenceThicknessUv).g; let iridescenceThickness = mix(material.iridescenceParams.z, material.iridescenceParams.w, iridescenceThicknessSample); let anisotropySample = textureSample(anisotropyTex, anisotropySampler, anisotropyUv).rgb; let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let baseContribution = (kD * albedo / PI + specularBrdf + sheenBrdf) * radiance * NdotL; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-instanced.wgsl var standard_instanced_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var sheenColorSampler: sampler; @group(1) @binding(22) var sheenColorTex: texture_2d; @group(1) @binding(23) var sheenRoughnessSampler: sampler; @group(1) @binding(24) var sheenRoughnessTex: texture_2d; @group(1) @binding(25) var iridescenceSampler: sampler; @group(1) @binding(26) var iridescenceTex: texture_2d; @group(1) @binding(27) var iridescenceThicknessSampler: sampler; @group(1) @binding(28) var iridescenceThicknessTex: texture_2d; @group(1) @binding(29) var anisotropySampler: sampler; @group(1) @binding(30) var anisotropyTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) m0: vec4f, @location(4) m1: vec4f, @location(5) m2: vec4f, @location(6) m3: vec4f, @location(7) n0: vec4f, @location(8) n1: vec4f, @location(9) n2: vec4f, @location(10) n3: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(2) var lighting: LightingUniforms; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let modelM = mat4x4f(in.m0, in.m1, in.m2, in.m3); let normalM = mat4x4f(in.n0, in.n1, in.n2, in.n3); let worldPos4 = modelM * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((normalM * vec4f(in.normal, 0.0)).xyz); out.tangent = vec4f((normalM * vec4f(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb * textureSample(sheenColorTex, sheenColorSampler, sheenColorUv).rgb; let sheenRoughness = clamp(material.sheenParams.w * textureSample(sheenRoughnessTex, sheenRoughnessSampler, sheenRoughnessUv).a, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x * textureSample(iridescenceTex, iridescenceSampler, iridescenceUv).r, 0.0, 1.0); let iridescenceThicknessSample = textureSample(iridescenceThicknessTex, iridescenceThicknessSampler, iridescenceThicknessUv).g; let iridescenceThickness = mix(material.iridescenceParams.z, material.iridescenceParams.w, iridescenceThicknessSample); let anisotropySample = textureSample(anisotropyTex, anisotropySampler, anisotropyUv).rgb; let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let baseContribution = (kD * albedo / PI + specularBrdf + sheenBrdf) * radiance * NdotL; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-skinned.wgsl var standard_skinned_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var sheenColorSampler: sampler; @group(1) @binding(22) var sheenColorTex: texture_2d; @group(1) @binding(23) var sheenRoughnessSampler: sampler; @group(1) @binding(24) var sheenRoughnessTex: texture_2d; @group(1) @binding(25) var iridescenceSampler: sampler; @group(1) @binding(26) var iridescenceTex: texture_2d; @group(1) @binding(27) var iridescenceThicknessSampler: sampler; @group(1) @binding(28) var iridescenceThicknessTex: texture_2d; @group(1) @binding(29) var anisotropySampler: sampler; @group(1) @binding(30) var anisotropyTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) joints: vec4u, @location(4) weights: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; struct SkinBuffer { joints: array }; @group(2) @binding(0) var skin: SkinBuffer; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j = in.joints; let w = in.weights; let skinMatrix = 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 localPos = skinMatrix * vec4f(in.position, 1.0); let localNormal = (skinMatrix * vec4f(in.normal, 0.0)).xyz; let localTangent = (skinMatrix * vec4f(in.tangent.xyz, 0.0)).xyz; let worldPos4 = model.model * localPos; out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(localNormal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(localTangent, 0.0)).xyz, in.tangent.w); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb * textureSample(sheenColorTex, sheenColorSampler, sheenColorUv).rgb; let sheenRoughness = clamp(material.sheenParams.w * textureSample(sheenRoughnessTex, sheenRoughnessSampler, sheenRoughnessUv).a, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x * textureSample(iridescenceTex, iridescenceSampler, iridescenceUv).r, 0.0, 1.0); let iridescenceThicknessSample = textureSample(iridescenceThicknessTex, iridescenceThicknessSampler, iridescenceThicknessUv).g; let iridescenceThickness = mix(material.iridescenceParams.z, material.iridescenceParams.w, iridescenceThicknessSample); let anisotropySample = textureSample(anisotropyTex, anisotropySampler, anisotropyUv).rgb; let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let baseContribution = (kD * albedo / PI + specularBrdf + sheenBrdf) * radiance * NdotL; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-skinned8.wgsl var standard_skinned8_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var sheenColorSampler: sampler; @group(1) @binding(22) var sheenColorTex: texture_2d; @group(1) @binding(23) var sheenRoughnessSampler: sampler; @group(1) @binding(24) var sheenRoughnessTex: texture_2d; @group(1) @binding(25) var iridescenceSampler: sampler; @group(1) @binding(26) var iridescenceTex: texture_2d; @group(1) @binding(27) var iridescenceThicknessSampler: sampler; @group(1) @binding(28) var iridescenceThicknessTex: texture_2d; @group(1) @binding(29) var anisotropySampler: sampler; @group(1) @binding(30) var anisotropyTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) joints0: vec4u, @location(4) weights0: vec4f, @location(5) joints1: vec4u, @location(6) weights1: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; struct SkinBuffer { joints: array }; @group(2) @binding(0) var skin: SkinBuffer; const PI: f32 = 3.14159265359; @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 skinMatrix = 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 localPos = skinMatrix * vec4f(in.position, 1.0); let localNormal = (skinMatrix * vec4f(in.normal, 0.0)).xyz; let localTangent = (skinMatrix * vec4f(in.tangent.xyz, 0.0)).xyz; let worldPos4 = model.model * localPos; out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(localNormal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(localTangent, 0.0)).xyz, in.tangent.w); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb * textureSample(sheenColorTex, sheenColorSampler, sheenColorUv).rgb; let sheenRoughness = clamp(material.sheenParams.w * textureSample(sheenRoughnessTex, sheenRoughnessSampler, sheenRoughnessUv).a, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x * textureSample(iridescenceTex, iridescenceSampler, iridescenceUv).r, 0.0, 1.0); let iridescenceThicknessSample = textureSample(iridescenceThicknessTex, iridescenceThicknessSampler, iridescenceThicknessUv).g; let iridescenceThickness = mix(material.iridescenceParams.z, material.iridescenceParams.w, iridescenceThicknessSample); let anisotropySample = textureSample(anisotropyTex, anisotropySampler, anisotropyUv).rgb; let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao; for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let baseContribution = (kD * albedo / PI + specularBrdf + sheenBrdf) * radiance * NdotL; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-transmission.wgsl var standard_transmission_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f, transmissionParams: vec4f, diffuseTransmissionColor: vec4f, volumeAttenuation: vec4f, transmissionTransform0: vec4f, transmissionTransform1: vec4f, volumeThicknessTransform0: vec4f, volumeThicknessTransform1: vec4f, diffuseTransmissionTransform0: vec4f, diffuseTransmissionTransform1: vec4f, diffuseTransmissionColorTransform0: vec4f, diffuseTransmissionColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var transmissionSampler: sampler; @group(1) @binding(22) var transmissionTex: texture_2d; @group(1) @binding(23) var volumeThicknessSampler: sampler; @group(1) @binding(24) var volumeThicknessTex: texture_2d; @group(1) @binding(25) var diffuseTransmissionSampler: sampler; @group(1) @binding(26) var diffuseTransmissionTex: texture_2d; @group(1) @binding(27) var diffuseTransmissionColorSampler: sampler; @group(1) @binding(28) var diffuseTransmissionColorTex: texture_2d; @group(1) @binding(29) var transmissionSourceSampler: sampler; @group(1) @binding(30) var transmissionSourceTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f, @location(5) modelScale: vec3f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let worldPos4 = model.model * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(in.normal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.modelScale = vec3f(length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz)); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } fn screenUvFromFragment(position: vec4f) -> vec2f { let dims = vec2f(textureDimensions(transmissionSourceTex, 0)); return clamp(position.xy / max(dims, vec2f(1.0)), vec2f(0.0), vec2f(1.0)); } fn projectWorldToScreenUv(worldPos: vec3f) -> vec2f { let clip = camera.viewProjection * vec4f(worldPos, 1.0); let invW = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * invW * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2f(0.0), vec2f(1.0)); } fn transmissionScreenUv(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, thickness: f32, modelScale: vec3f) -> vec2f { let baseUv = screenUvFromFragment(position); if (thickness <= 1e-5) { return baseUv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-V, N, eta); let rayLength2 = dot(ray, ray); if (rayLength2 <= 1e-8) { ray = -V; } else { ray = ray * inverseSqrt(rayLength2); } let transmissionRay = ray * max(thickness, 0.0) * max(modelScale, vec3f(1e-4)); return projectWorldToScreenUv(worldPos + transmissionRay); } fn transmissionSourceToLinear(color: vec3f) -> vec3f { return pow(clamp(color, vec3f(0.0), vec3f(1.0)), vec3f(2.2)); } fn sampleTransmissionSourceAt(uv: vec2f) -> vec3f { let sourceColor = textureSampleLevel(transmissionSourceTex, transmissionSourceSampler, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0).rgb; return transmissionSourceToLinear(sourceColor); } fn dispersionIors(ior: f32, dispersion: f32) -> vec3f { let halfSpread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3f(ior - halfSpread, ior, ior + halfSpread), vec3f(1.0)); } fn sampleTransmissionSource(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, dispersion: f32, thickness: f32, modelScale: vec3f) -> vec3f { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, ior, thickness, modelScale)); } let iors = dispersionIors(ior, dispersion); let r = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.r, thickness, modelScale)).r; let g = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.g, thickness, modelScale)).g; let b = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.b, thickness, modelScale)).b; return vec3f(r, g, b); } fn volumeTransmissionAttenuation(thickness: f32, attenuationDistance: f32, attenuationColor: vec3f) -> vec3f { if (thickness <= 1e-5 || attenuationDistance <= 1e-5) { return vec3f(1.0); } return pow(max(attenuationColor, vec3f(1e-4)), vec3f(thickness / attenuationDistance)); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let transmissionUv = applyTextureTransform(in.uv, in.uv1, material.transmissionTransform0, material.transmissionTransform1); let volumeThicknessUv = applyTextureTransform(in.uv, in.uv1, material.volumeThicknessTransform0, material.volumeThicknessTransform1); let diffuseTransmissionUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionTransform0, material.diffuseTransmissionTransform1); let diffuseTransmissionColorUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionColorTransform0, material.diffuseTransmissionColorTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb; let sheenRoughness = clamp(material.sheenParams.w, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x, 0.0, 1.0); let iridescenceThickness = material.iridescenceParams.w; let anisotropySample = vec3f(1.0, 0.5, 1.0); let transmission = clamp(material.transmissionParams.x * textureSample(transmissionTex, transmissionSampler, transmissionUv).r, 0.0, 1.0); let diffuseTransmission = clamp(material.transmissionParams.y * textureSample(diffuseTransmissionTex, diffuseTransmissionSampler, diffuseTransmissionUv).a, 0.0, 1.0); let volumeThickness = max(material.transmissionParams.z * textureSample(volumeThicknessTex, volumeThicknessSampler, volumeThicknessUv).g, 0.0); let dispersion = max(material.transmissionParams.w, 0.0); let diffuseTransmissionColor = material.diffuseTransmissionColor.rgb * textureSample(diffuseTransmissionColorTex, diffuseTransmissionColorSampler, diffuseTransmissionColorUv).rgb; let volumeAttenuation = volumeTransmissionAttenuation(volumeThickness, material.diffuseTransmissionColor.w, material.volumeAttenuation.rgb); let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } var viewFresnel = fresnelSchlick(viewNdotV, F0, F90); if (iridescence > 1e-5) { viewFresnel = mix(viewFresnel, iridescenceFresnelColor, iridescence); } let transmissionWeight = transmission * (1.0 - metallic) * max(1.0 - maxComponent(viewFresnel), 0.0); let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let frontDiffuse = (1.0 - diffuseTransmission) * kD * albedo * radiance * NdotL / PI; let backDiffuse = diffuseTransmission * kD * diffuseTransmissionColor * radiance * max(dot(-N, L), 0.0) / PI; let diffuseContribution = (frontDiffuse + backDiffuse) * (1.0 - transmission); let specularContribution = (specularBrdf + sheenBrdf) * radiance * NdotL; let baseContribution = diffuseContribution + specularContribution; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } if (transmissionWeight > 1e-5) { let transmittedSource = sampleTransmissionSource(in.position, in.worldPos, N, V, material.extensionParams.x, dispersion, volumeThickness, in.modelScale); Lo += transmittedSource * albedo * volumeAttenuation * transmissionWeight * (1.0 - clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-transmission-instanced.wgsl var standard_transmission_instanced_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f, transmissionParams: vec4f, diffuseTransmissionColor: vec4f, volumeAttenuation: vec4f, transmissionTransform0: vec4f, transmissionTransform1: vec4f, volumeThicknessTransform0: vec4f, volumeThicknessTransform1: vec4f, diffuseTransmissionTransform0: vec4f, diffuseTransmissionTransform1: vec4f, diffuseTransmissionColorTransform0: vec4f, diffuseTransmissionColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var transmissionSampler: sampler; @group(1) @binding(22) var transmissionTex: texture_2d; @group(1) @binding(23) var volumeThicknessSampler: sampler; @group(1) @binding(24) var volumeThicknessTex: texture_2d; @group(1) @binding(25) var diffuseTransmissionSampler: sampler; @group(1) @binding(26) var diffuseTransmissionTex: texture_2d; @group(1) @binding(27) var diffuseTransmissionColorSampler: sampler; @group(1) @binding(28) var diffuseTransmissionColorTex: texture_2d; @group(1) @binding(29) var transmissionSourceSampler: sampler; @group(1) @binding(30) var transmissionSourceTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) m0: vec4f, @location(4) m1: vec4f, @location(5) m2: vec4f, @location(6) m3: vec4f, @location(7) n0: vec4f, @location(8) n1: vec4f, @location(9) n2: vec4f, @location(10) n3: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f, @location(5) modelScale: vec3f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(2) var lighting: LightingUniforms; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let modelM = mat4x4f(in.m0, in.m1, in.m2, in.m3); let normalM = mat4x4f(in.n0, in.n1, in.n2, in.n3); let worldPos4 = modelM * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((normalM * vec4f(in.normal, 0.0)).xyz); out.tangent = vec4f((normalM * vec4f(in.tangent.xyz, 0.0)).xyz, in.tangent.w); out.modelScale = vec3f(length(modelM[0].xyz), length(modelM[1].xyz), length(modelM[2].xyz)); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } fn screenUvFromFragment(position: vec4f) -> vec2f { let dims = vec2f(textureDimensions(transmissionSourceTex, 0)); return clamp(position.xy / max(dims, vec2f(1.0)), vec2f(0.0), vec2f(1.0)); } fn projectWorldToScreenUv(worldPos: vec3f) -> vec2f { let clip = camera.viewProjection * vec4f(worldPos, 1.0); let invW = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * invW * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2f(0.0), vec2f(1.0)); } fn transmissionScreenUv(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, thickness: f32, modelScale: vec3f) -> vec2f { let baseUv = screenUvFromFragment(position); if (thickness <= 1e-5) { return baseUv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-V, N, eta); let rayLength2 = dot(ray, ray); if (rayLength2 <= 1e-8) { ray = -V; } else { ray = ray * inverseSqrt(rayLength2); } let transmissionRay = ray * max(thickness, 0.0) * max(modelScale, vec3f(1e-4)); return projectWorldToScreenUv(worldPos + transmissionRay); } fn transmissionSourceToLinear(color: vec3f) -> vec3f { return pow(clamp(color, vec3f(0.0), vec3f(1.0)), vec3f(2.2)); } fn sampleTransmissionSourceAt(uv: vec2f) -> vec3f { let sourceColor = textureSampleLevel(transmissionSourceTex, transmissionSourceSampler, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0).rgb; return transmissionSourceToLinear(sourceColor); } fn dispersionIors(ior: f32, dispersion: f32) -> vec3f { let halfSpread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3f(ior - halfSpread, ior, ior + halfSpread), vec3f(1.0)); } fn sampleTransmissionSource(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, dispersion: f32, thickness: f32, modelScale: vec3f) -> vec3f { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, ior, thickness, modelScale)); } let iors = dispersionIors(ior, dispersion); let r = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.r, thickness, modelScale)).r; let g = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.g, thickness, modelScale)).g; let b = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.b, thickness, modelScale)).b; return vec3f(r, g, b); } fn volumeTransmissionAttenuation(thickness: f32, attenuationDistance: f32, attenuationColor: vec3f) -> vec3f { if (thickness <= 1e-5 || attenuationDistance <= 1e-5) { return vec3f(1.0); } return pow(max(attenuationColor, vec3f(1e-4)), vec3f(thickness / attenuationDistance)); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let transmissionUv = applyTextureTransform(in.uv, in.uv1, material.transmissionTransform0, material.transmissionTransform1); let volumeThicknessUv = applyTextureTransform(in.uv, in.uv1, material.volumeThicknessTransform0, material.volumeThicknessTransform1); let diffuseTransmissionUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionTransform0, material.diffuseTransmissionTransform1); let diffuseTransmissionColorUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionColorTransform0, material.diffuseTransmissionColorTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb; let sheenRoughness = clamp(material.sheenParams.w, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x, 0.0, 1.0); let iridescenceThickness = material.iridescenceParams.w; let anisotropySample = vec3f(1.0, 0.5, 1.0); let transmission = clamp(material.transmissionParams.x * textureSample(transmissionTex, transmissionSampler, transmissionUv).r, 0.0, 1.0); let diffuseTransmission = clamp(material.transmissionParams.y * textureSample(diffuseTransmissionTex, diffuseTransmissionSampler, diffuseTransmissionUv).a, 0.0, 1.0); let volumeThickness = max(material.transmissionParams.z * textureSample(volumeThicknessTex, volumeThicknessSampler, volumeThicknessUv).g, 0.0); let dispersion = max(material.transmissionParams.w, 0.0); let diffuseTransmissionColor = material.diffuseTransmissionColor.rgb * textureSample(diffuseTransmissionColorTex, diffuseTransmissionColorSampler, diffuseTransmissionColorUv).rgb; let volumeAttenuation = volumeTransmissionAttenuation(volumeThickness, material.diffuseTransmissionColor.w, material.volumeAttenuation.rgb); let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } var viewFresnel = fresnelSchlick(viewNdotV, F0, F90); if (iridescence > 1e-5) { viewFresnel = mix(viewFresnel, iridescenceFresnelColor, iridescence); } let transmissionWeight = transmission * (1.0 - metallic) * max(1.0 - maxComponent(viewFresnel), 0.0); let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let frontDiffuse = (1.0 - diffuseTransmission) * kD * albedo * radiance * NdotL / PI; let backDiffuse = diffuseTransmission * kD * diffuseTransmissionColor * radiance * max(dot(-N, L), 0.0) / PI; let diffuseContribution = (frontDiffuse + backDiffuse) * (1.0 - transmission); let specularContribution = (specularBrdf + sheenBrdf) * radiance * NdotL; let baseContribution = diffuseContribution + specularContribution; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } if (transmissionWeight > 1e-5) { let transmittedSource = sampleTransmissionSource(in.position, in.worldPos, N, V, material.extensionParams.x, dispersion, volumeThickness, in.modelScale); Lo += transmittedSource * albedo * volumeAttenuation * transmissionWeight * (1.0 - clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-transmission-skinned.wgsl var standard_transmission_skinned_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f, transmissionParams: vec4f, diffuseTransmissionColor: vec4f, volumeAttenuation: vec4f, transmissionTransform0: vec4f, transmissionTransform1: vec4f, volumeThicknessTransform0: vec4f, volumeThicknessTransform1: vec4f, diffuseTransmissionTransform0: vec4f, diffuseTransmissionTransform1: vec4f, diffuseTransmissionColorTransform0: vec4f, diffuseTransmissionColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var transmissionSampler: sampler; @group(1) @binding(22) var transmissionTex: texture_2d; @group(1) @binding(23) var volumeThicknessSampler: sampler; @group(1) @binding(24) var volumeThicknessTex: texture_2d; @group(1) @binding(25) var diffuseTransmissionSampler: sampler; @group(1) @binding(26) var diffuseTransmissionTex: texture_2d; @group(1) @binding(27) var diffuseTransmissionColorSampler: sampler; @group(1) @binding(28) var diffuseTransmissionColorTex: texture_2d; @group(1) @binding(29) var transmissionSourceSampler: sampler; @group(1) @binding(30) var transmissionSourceTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) joints: vec4u, @location(4) weights: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f, @location(5) modelScale: vec3f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; struct SkinBuffer { joints: array }; @group(2) @binding(0) var skin: SkinBuffer; const PI: f32 = 3.14159265359; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let j = in.joints; let w = in.weights; let skinMatrix = 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 localPos = skinMatrix * vec4f(in.position, 1.0); let localNormal = (skinMatrix * vec4f(in.normal, 0.0)).xyz; let localTangent = (skinMatrix * vec4f(in.tangent.xyz, 0.0)).xyz; let worldPos4 = model.model * localPos; out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(localNormal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(localTangent, 0.0)).xyz, in.tangent.w); out.modelScale = vec3f(length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz)); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } fn screenUvFromFragment(position: vec4f) -> vec2f { let dims = vec2f(textureDimensions(transmissionSourceTex, 0)); return clamp(position.xy / max(dims, vec2f(1.0)), vec2f(0.0), vec2f(1.0)); } fn projectWorldToScreenUv(worldPos: vec3f) -> vec2f { let clip = camera.viewProjection * vec4f(worldPos, 1.0); let invW = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * invW * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2f(0.0), vec2f(1.0)); } fn transmissionScreenUv(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, thickness: f32, modelScale: vec3f) -> vec2f { let baseUv = screenUvFromFragment(position); if (thickness <= 1e-5) { return baseUv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-V, N, eta); let rayLength2 = dot(ray, ray); if (rayLength2 <= 1e-8) { ray = -V; } else { ray = ray * inverseSqrt(rayLength2); } let transmissionRay = ray * max(thickness, 0.0) * max(modelScale, vec3f(1e-4)); return projectWorldToScreenUv(worldPos + transmissionRay); } fn transmissionSourceToLinear(color: vec3f) -> vec3f { return pow(clamp(color, vec3f(0.0), vec3f(1.0)), vec3f(2.2)); } fn sampleTransmissionSourceAt(uv: vec2f) -> vec3f { let sourceColor = textureSampleLevel(transmissionSourceTex, transmissionSourceSampler, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0).rgb; return transmissionSourceToLinear(sourceColor); } fn dispersionIors(ior: f32, dispersion: f32) -> vec3f { let halfSpread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3f(ior - halfSpread, ior, ior + halfSpread), vec3f(1.0)); } fn sampleTransmissionSource(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, dispersion: f32, thickness: f32, modelScale: vec3f) -> vec3f { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, ior, thickness, modelScale)); } let iors = dispersionIors(ior, dispersion); let r = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.r, thickness, modelScale)).r; let g = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.g, thickness, modelScale)).g; let b = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.b, thickness, modelScale)).b; return vec3f(r, g, b); } fn volumeTransmissionAttenuation(thickness: f32, attenuationDistance: f32, attenuationColor: vec3f) -> vec3f { if (thickness <= 1e-5 || attenuationDistance <= 1e-5) { return vec3f(1.0); } return pow(max(attenuationColor, vec3f(1e-4)), vec3f(thickness / attenuationDistance)); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let transmissionUv = applyTextureTransform(in.uv, in.uv1, material.transmissionTransform0, material.transmissionTransform1); let volumeThicknessUv = applyTextureTransform(in.uv, in.uv1, material.volumeThicknessTransform0, material.volumeThicknessTransform1); let diffuseTransmissionUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionTransform0, material.diffuseTransmissionTransform1); let diffuseTransmissionColorUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionColorTransform0, material.diffuseTransmissionColorTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb; let sheenRoughness = clamp(material.sheenParams.w, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x, 0.0, 1.0); let iridescenceThickness = material.iridescenceParams.w; let anisotropySample = vec3f(1.0, 0.5, 1.0); let transmission = clamp(material.transmissionParams.x * textureSample(transmissionTex, transmissionSampler, transmissionUv).r, 0.0, 1.0); let diffuseTransmission = clamp(material.transmissionParams.y * textureSample(diffuseTransmissionTex, diffuseTransmissionSampler, diffuseTransmissionUv).a, 0.0, 1.0); let volumeThickness = max(material.transmissionParams.z * textureSample(volumeThicknessTex, volumeThicknessSampler, volumeThicknessUv).g, 0.0); let dispersion = max(material.transmissionParams.w, 0.0); let diffuseTransmissionColor = material.diffuseTransmissionColor.rgb * textureSample(diffuseTransmissionColorTex, diffuseTransmissionColorSampler, diffuseTransmissionColorUv).rgb; let volumeAttenuation = volumeTransmissionAttenuation(volumeThickness, material.diffuseTransmissionColor.w, material.volumeAttenuation.rgb); let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } var viewFresnel = fresnelSchlick(viewNdotV, F0, F90); if (iridescence > 1e-5) { viewFresnel = mix(viewFresnel, iridescenceFresnelColor, iridescence); } let transmissionWeight = transmission * (1.0 - metallic) * max(1.0 - maxComponent(viewFresnel), 0.0); let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let frontDiffuse = (1.0 - diffuseTransmission) * kD * albedo * radiance * NdotL / PI; let backDiffuse = diffuseTransmission * kD * diffuseTransmissionColor * radiance * max(dot(-N, L), 0.0) / PI; let diffuseContribution = (frontDiffuse + backDiffuse) * (1.0 - transmission); let specularContribution = (specularBrdf + sheenBrdf) * radiance * NdotL; let baseContribution = diffuseContribution + specularContribution; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } if (transmissionWeight > 1e-5) { let transmittedSource = sampleTransmissionSource(in.position, in.worldPos, N, V, material.extensionParams.x, dispersion, volumeThickness, in.modelScale); Lo += transmittedSource * albedo * volumeAttenuation * transmissionWeight * (1.0 - clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/standard-transmission-skinned8.wgsl var standard_transmission_skinned8_default = "struct MaterialUniforms { color: vec4f, emissive: vec4f, params: vec4f, params2: vec4f, baseColorTransform0: vec4f, baseColorTransform1: vec4f, metallicRoughnessTransform0: vec4f, metallicRoughnessTransform1: vec4f, normalTransform0: vec4f, normalTransform1: vec4f, occlusionTransform0: vec4f, occlusionTransform1: vec4f, emissiveTransform0: vec4f, emissiveTransform1: vec4f, clearcoatParams: vec4f, specularParams: vec4f, extensionParams: vec4f, clearcoatTransform0: vec4f, clearcoatTransform1: vec4f, clearcoatRoughnessTransform0: vec4f, clearcoatRoughnessTransform1: vec4f, clearcoatNormalTransform0: vec4f, clearcoatNormalTransform1: vec4f, specularTransform0: vec4f, specularTransform1: vec4f, specularColorTransform0: vec4f, specularColorTransform1: vec4f, sheenParams: vec4f, iridescenceParams: vec4f, anisotropyParams: vec4f, sheenColorTransform0: vec4f, sheenColorTransform1: vec4f, sheenRoughnessTransform0: vec4f, sheenRoughnessTransform1: vec4f, iridescenceTransform0: vec4f, iridescenceTransform1: vec4f, iridescenceThicknessTransform0: vec4f, iridescenceThicknessTransform1: vec4f, anisotropyTransform0: vec4f, anisotropyTransform1: vec4f, transmissionParams: vec4f, diffuseTransmissionColor: vec4f, volumeAttenuation: vec4f, transmissionTransform0: vec4f, transmissionTransform1: vec4f, volumeThicknessTransform0: vec4f, volumeThicknessTransform1: vec4f, diffuseTransmissionTransform0: vec4f, diffuseTransmissionTransform1: vec4f, diffuseTransmissionColorTransform0: vec4f, diffuseTransmissionColorTransform1: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var baseColorSampler: sampler; @group(1) @binding(2) var baseColorTex: texture_2d; @group(1) @binding(3) var metallicRoughnessSampler: sampler; @group(1) @binding(4) var metallicRoughnessTex: texture_2d; @group(1) @binding(5) var normalSampler: sampler; @group(1) @binding(6) var normalTex: texture_2d; @group(1) @binding(7) var occlusionSampler: sampler; @group(1) @binding(8) var occlusionTex: texture_2d; @group(1) @binding(9) var emissiveSampler: sampler; @group(1) @binding(10) var emissiveTex: texture_2d; @group(1) @binding(11) var clearcoatSampler: sampler; @group(1) @binding(12) var clearcoatTex: texture_2d; @group(1) @binding(13) var clearcoatRoughnessSampler: sampler; @group(1) @binding(14) var clearcoatRoughnessTex: texture_2d; @group(1) @binding(15) var clearcoatNormalSampler: sampler; @group(1) @binding(16) var clearcoatNormalTex: texture_2d; @group(1) @binding(17) var specularSampler: sampler; @group(1) @binding(18) var specularTex: texture_2d; @group(1) @binding(19) var specularColorSampler: sampler; @group(1) @binding(20) var specularColorTex: texture_2d; @group(1) @binding(21) var transmissionSampler: sampler; @group(1) @binding(22) var transmissionTex: texture_2d; @group(1) @binding(23) var volumeThicknessSampler: sampler; @group(1) @binding(24) var volumeThicknessTex: texture_2d; @group(1) @binding(25) var diffuseTransmissionSampler: sampler; @group(1) @binding(26) var diffuseTransmissionTex: texture_2d; @group(1) @binding(27) var diffuseTransmissionColorSampler: sampler; @group(1) @binding(28) var diffuseTransmissionColorTex: texture_2d; @group(1) @binding(29) var transmissionSourceSampler: sampler; @group(1) @binding(30) var transmissionSourceTex: texture_2d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(11) uv1: vec2f, @location(12) tangent: vec4f, @location(3) joints0: vec4u, @location(4) weights0: vec4f, @location(5) joints1: vec4u, @location(6) weights1: vec4f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f, @location(3) uv1: vec2f, @location(4) tangent: vec4f, @location(5) modelScale: vec3f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, direction: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; struct SkinBuffer { joints: array }; @group(2) @binding(0) var skin: SkinBuffer; const PI: f32 = 3.14159265359; @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 skinMatrix = 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 localPos = skinMatrix * vec4f(in.position, 1.0); let localNormal = (skinMatrix * vec4f(in.normal, 0.0)).xyz; let localTangent = (skinMatrix * vec4f(in.tangent.xyz, 0.0)).xyz; let worldPos4 = model.model * localPos; out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(localNormal, 0.0)).xyz); out.tangent = vec4f((model.normalMatrix * vec4f(localTangent, 0.0)).xyz, in.tangent.w); out.modelScale = vec3f(length(model.model[0].xyz), length(model.model[1].xyz), length(model.model[2].xyz)); out.uv = in.uv; out.uv1 = in.uv1; return out; } fn applyTextureTransform(uv0: vec2f, uv1: vec2f, transform0: vec4f, transform1: vec4f) -> vec2f { let uv = select(uv0, uv1, transform1.z >= 0.5); let scaled = uv * transform1.xy; let rotated = vec2f( transform0.z * scaled.x + transform0.w * scaled.y, -transform0.w * scaled.x + transform0.z * scaled.y ); return rotated + transform0.xy; } fn fresnelSchlick(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (F90 - F0) * pow(oneMinusCos, 5.0); } fn maxComponent(v: vec3f) -> f32 { return max(max(v.x, v.y), v.z); } fn distributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom = NdotH2 * (a2 - 1.0) + 1.0; return a2 / (PI * denom * denom); } fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } struct TangentFrame { t: vec3f, b: vec3f, n: vec3f }; fn fallbackTangentFrame(N: vec3f) -> TangentFrame { let n = normalize(N); let axis = select(vec3f(0.0, 1.0, 0.0), vec3f(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 derivativeTangentFrame(N: vec3f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let dp1 = dpdx(worldPos); let dp2 = dpdy(worldPos); 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 frameLength2 = max(dot(T, T), dot(B, B)); if (frameLength2 <= 1e-20) { return fallbackTangentFrame(n); } let frameScale = 1.0 / sqrt(frameLength2); return TangentFrame(T * frameScale, B * frameScale, n); } fn buildTangentFrame(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f) -> TangentFrame { let n = normalize(N); let derivativeFrame = derivativeTangentFrame(n, worldPos, uv); var t = tangent.xyz - n * dot(n, tangent.xyz); let tLen2 = dot(t, t); if (tLen2 <= 1e-20) { return derivativeFrame; } t = t * inverseSqrt(tLen2); let b = normalize(cross(n, t)) * select(-1.0, 1.0, tangent.w >= 0.0); return TangentFrame(t, b, n); } fn applyNormalMap(N: vec3f, tangent: vec4f, worldPos: vec3f, uv: vec2f, normalSample: vec3f, normalScale: f32) -> vec3f { if (normalScale == 0.0) { return normalize(N); } let frame = buildTangentFrame(N, tangent, worldPos, uv); var ns = normalSample * 2.0 - vec3f(1.0); ns = vec3f(ns.x * normalScale, ns.y * normalScale, 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 iorToFresnel0(transmittedIor: f32, incidentIor: f32) -> f32 { let r = (transmittedIor - incidentIor) / (transmittedIor + incidentIor); return r * r; } fn iorToFresnel0Vec(transmittedIor: vec3f, incidentIor: f32) -> vec3f { let r = (transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)); return r * r; } fn fresnel0ToIor(F0: vec3f) -> vec3f { let sqrtF0 = sqrt(clamp(F0, vec3f(0.0), vec3f(0.9999))); return (vec3f(1.0) + sqrtF0) / max(vec3f(1.0) - sqrtF0, vec3f(1e-4)); } fn fresnelSchlickScalar(cosTheta: f32, F0: f32) -> f32 { let oneMinusCos = 1.0 - clamp(cosTheta, 0.0, 1.0); return F0 + (1.0 - F0) * pow(oneMinusCos, 5.0); } fn sanitizeReflectance(value: vec3f, fallback: vec3f) -> vec3f { var result = clamp(fallback, vec3f(0.0), vec3f(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 evalIridescenceSensitivity(OPD: f32, shift: vec3f) -> vec3f { let phase = 2.0 * PI * OPD * 1.0e-9; let phase2 = phase * phase; let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); let variance = vec3f(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 vec3f( 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 iridescentFresnel(outsideIor: f32, iridescenceIor: f32, baseF0: vec3f, thickness: f32, cosTheta1: f32) -> vec3f { let safeCosTheta1 = clamp(cosTheta1, 0.0, 1.0); let thinFilmIor = mix(outsideIor, iridescenceIor, smoothstep(0.0, 0.03, thickness)); let R0 = iorToFresnel0(thinFilmIor, outsideIor); let R12 = fresnelSchlickScalar(safeCosTheta1, R0); let T121 = 1.0 - R12; let baseIor = fresnel0ToIor(baseF0); let R1 = iorToFresnel0Vec(baseIor, thinFilmIor); let eta = outsideIor / thinFilmIor; let sinTheta2Sq = eta * eta * (1.0 - safeCosTheta1 * safeCosTheta1); let cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2 = sqrt(cosTheta2Sq); let R23 = fresnelSchlick(cosTheta2, R1, vec3f(1.0)); let phi12 = select(0.0, PI, thinFilmIor < outsideIor); let phi21 = PI - phi12; let phi23 = vec3f( select(0.0, PI, baseIor.x < thinFilmIor), select(0.0, PI, baseIor.y < thinFilmIor), select(0.0, PI, baseIor.z < thinFilmIor) ); let phi = vec3f(phi21) + phi23; let OPD = 2.0 * thinFilmIor * thickness * cosTheta2; let R123 = clamp(vec3f(R12) * R23, vec3f(1e-5), vec3f(0.9999)); let r123 = sqrt(R123); let Rs = sqr(T121) * R23 / (vec3f(1.0) - R123); var I = vec3f(R12) + Rs; var Cm = Rs - vec3f(T121); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(OPD, phi); Cm *= r123; I += Cm * 2.0 * evalIridescenceSensitivity(2.0 * OPD, 2.0 * phi); return sanitizeReflectance(I, baseF0); } fn distributionGGXAnisotropic(NdotH: f32, TdotH: f32, BdotH: f32, at: f32, ab: f32) -> f32 { let a2 = at * ab; let f = vec3f(ab * TdotH, at * BdotH, a2 * NdotH); let w2 = a2 / max(dot(f, f), 1e-8); return a2 * w2 * w2 / PI; } fn visibilityGGXAnisotropic(NdotL: f32, NdotV: f32, BdotV: f32, TdotV: f32, TdotL: f32, BdotL: f32, at: f32, ab: f32) -> f32 { let GGXV = NdotL * length(vec3f(at * TdotV, ab * BdotV, NdotV)); let GGXL = NdotV * length(vec3f(at * TdotL, ab * BdotL, NdotL)); return clamp(0.5 / max(GGXV + GGXL, 1e-8), 0.0, 1.0); } fn sheenDistribution(NdotH: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let invR = 1.0 / alphaG; let sin2h = max(1.0 - NdotH * NdotH, 0.0); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenL(cosTheta: f32, alphaG: f32) -> f32 { let oneMinusAlphaSq = sqr(1.0 - alphaG); let a = mix(21.5473, 25.3245, oneMinusAlphaSq); let b = mix(3.82987, 3.32435, oneMinusAlphaSq); let c = mix(0.19823, 0.16801, oneMinusAlphaSq); let d = mix(-1.97760, -1.27393, oneMinusAlphaSq); let e = mix(-4.32054, -4.85967, oneMinusAlphaSq); return a / (1.0 + b * pow(cosTheta, c)) + d * cosTheta + e; } fn sheenLambda(cosTheta: f32, alphaG: f32) -> f32 { let safeCosTheta = clamp(cosTheta, 1e-4, 1.0); if (safeCosTheta < 0.5) { return exp(sheenL(safeCosTheta, alphaG)); } return exp(2.0 * sheenL(0.5, alphaG) - sheenL(1.0 - safeCosTheta, alphaG)); } fn sheenVisibility(NdotL: f32, NdotV: f32, sheenRoughness: f32) -> f32 { let alphaG = max(sheenRoughness * sheenRoughness, 1e-4); let visibility = 1.0 + sheenLambda(NdotV, alphaG) + sheenLambda(NdotL, alphaG); return clamp(1.0 / max(visibility * 4.0 * NdotV * NdotL, 1e-6), 0.0, 1.0); } fn dielectricF0FromIor(ior: f32) -> f32 { if (ior == 0.0) { return 1.0; } let safeIor = max(ior, 1.0); let r = (safeIor - 1.0) / (safeIor + 1.0); return r * r; } fn computeRangeAttenuation(distance: f32, range: f32) -> f32 { let invSq = 1.0 / max(distance * distance, 0.0001); if (range <= 0.0) { return invSq; } let fade = clamp(1.0 - distance / range, 0.0, 1.0); return invSq * fade * fade; } fn computeSpotFactor(L: vec3f, direction: vec3f, cosInner: f32, cosOuter: f32) -> f32 { let angleCos = dot(-L, normalize(direction)); if (cosInner <= cosOuter) { return select(0.0, 1.0, angleCos >= cosOuter); } return clamp((angleCos - cosOuter) / max(cosInner - cosOuter, 1e-4), 0.0, 1.0); } fn screenUvFromFragment(position: vec4f) -> vec2f { let dims = vec2f(textureDimensions(transmissionSourceTex, 0)); return clamp(position.xy / max(dims, vec2f(1.0)), vec2f(0.0), vec2f(1.0)); } fn projectWorldToScreenUv(worldPos: vec3f) -> vec2f { let clip = camera.viewProjection * vec4f(worldPos, 1.0); let invW = 1.0 / max(abs(clip.w), 1e-5); let ndc = clip.xy * invW * select(-1.0, 1.0, clip.w >= 0.0); return clamp(vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5), vec2f(0.0), vec2f(1.0)); } fn transmissionScreenUv(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, thickness: f32, modelScale: vec3f) -> vec2f { let baseUv = screenUvFromFragment(position); if (thickness <= 1e-5) { return baseUv; } let eta = 1.0 / max(ior, 1.0001); var ray = refract(-V, N, eta); let rayLength2 = dot(ray, ray); if (rayLength2 <= 1e-8) { ray = -V; } else { ray = ray * inverseSqrt(rayLength2); } let transmissionRay = ray * max(thickness, 0.0) * max(modelScale, vec3f(1e-4)); return projectWorldToScreenUv(worldPos + transmissionRay); } fn transmissionSourceToLinear(color: vec3f) -> vec3f { return pow(clamp(color, vec3f(0.0), vec3f(1.0)), vec3f(2.2)); } fn sampleTransmissionSourceAt(uv: vec2f) -> vec3f { let sourceColor = textureSampleLevel(transmissionSourceTex, transmissionSourceSampler, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0).rgb; return transmissionSourceToLinear(sourceColor); } fn dispersionIors(ior: f32, dispersion: f32) -> vec3f { let halfSpread = max(ior - 1.0, 0.0) * 0.025 * max(dispersion, 0.0); return max(vec3f(ior - halfSpread, ior, ior + halfSpread), vec3f(1.0)); } fn sampleTransmissionSource(position: vec4f, worldPos: vec3f, N: vec3f, V: vec3f, ior: f32, dispersion: f32, thickness: f32, modelScale: vec3f) -> vec3f { if (dispersion <= 1e-5 || thickness <= 1e-5) { return sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, ior, thickness, modelScale)); } let iors = dispersionIors(ior, dispersion); let r = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.r, thickness, modelScale)).r; let g = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.g, thickness, modelScale)).g; let b = sampleTransmissionSourceAt(transmissionScreenUv(position, worldPos, N, V, iors.b, thickness, modelScale)).b; return vec3f(r, g, b); } fn volumeTransmissionAttenuation(thickness: f32, attenuationDistance: f32, attenuationColor: vec3f) -> vec3f { if (thickness <= 1e-5 || attenuationDistance <= 1e-5) { return vec3f(1.0); } return pow(max(attenuationColor, vec3f(1e-4)), vec3f(thickness / attenuationDistance)); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let baseUv = applyTextureTransform(in.uv, in.uv1, material.baseColorTransform0, material.baseColorTransform1); let mrUv = applyTextureTransform(in.uv, in.uv1, material.metallicRoughnessTransform0, material.metallicRoughnessTransform1); let normalUv = applyTextureTransform(in.uv, in.uv1, material.normalTransform0, material.normalTransform1); let occlusionUv = applyTextureTransform(in.uv, in.uv1, material.occlusionTransform0, material.occlusionTransform1); let emissiveUv = applyTextureTransform(in.uv, in.uv1, material.emissiveTransform0, material.emissiveTransform1); let clearcoatUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatTransform0, material.clearcoatTransform1); let clearcoatRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatRoughnessTransform0, material.clearcoatRoughnessTransform1); let clearcoatNormalUv = applyTextureTransform(in.uv, in.uv1, material.clearcoatNormalTransform0, material.clearcoatNormalTransform1); let specularUv = applyTextureTransform(in.uv, in.uv1, material.specularTransform0, material.specularTransform1); let specularColorUv = applyTextureTransform(in.uv, in.uv1, material.specularColorTransform0, material.specularColorTransform1); let sheenColorUv = applyTextureTransform(in.uv, in.uv1, material.sheenColorTransform0, material.sheenColorTransform1); let sheenRoughnessUv = applyTextureTransform(in.uv, in.uv1, material.sheenRoughnessTransform0, material.sheenRoughnessTransform1); let iridescenceUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceTransform0, material.iridescenceTransform1); let iridescenceThicknessUv = applyTextureTransform(in.uv, in.uv1, material.iridescenceThicknessTransform0, material.iridescenceThicknessTransform1); let anisotropyUv = applyTextureTransform(in.uv, in.uv1, material.anisotropyTransform0, material.anisotropyTransform1); let transmissionUv = applyTextureTransform(in.uv, in.uv1, material.transmissionTransform0, material.transmissionTransform1); let volumeThicknessUv = applyTextureTransform(in.uv, in.uv1, material.volumeThicknessTransform0, material.volumeThicknessTransform1); let diffuseTransmissionUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionTransform0, material.diffuseTransmissionTransform1); let diffuseTransmissionColorUv = applyTextureTransform(in.uv, in.uv1, material.diffuseTransmissionColorTransform0, material.diffuseTransmissionColorTransform1); let baseSample = textureSample(baseColorTex, baseColorSampler, baseUv); let baseColor = material.color * baseSample; let alphaCutoff = material.params2.x; if (alphaCutoff > 0.0 && baseColor.a < alphaCutoff) { discard; } let mrSample = textureSample(metallicRoughnessTex, metallicRoughnessSampler, mrUv); let metallic = clamp(material.params.x * mrSample.b, 0.0, 1.0); let roughness = clamp(material.params.y * mrSample.g, 0.04, 1.0); let normalSample = textureSample(normalTex, normalSampler, normalUv).xyz; let N = applyNormalMap(in.normal, in.tangent, in.worldPos, normalUv, normalSample, material.params.z); let occlSample = textureSample(occlusionTex, occlusionSampler, occlusionUv).r; let ao = 1.0 + material.params.w * (occlSample - 1.0); let emissiveSample = textureSample(emissiveTex, emissiveSampler, emissiveUv).rgb; let emissive = emissiveSample * material.emissive.rgb * material.emissive.a * material.extensionParams.y; let clearcoat = clamp(material.clearcoatParams.x * textureSample(clearcoatTex, clearcoatSampler, clearcoatUv).r, 0.0, 1.0); let clearcoatRoughness = clamp(material.clearcoatParams.y * textureSample(clearcoatRoughnessTex, clearcoatRoughnessSampler, clearcoatRoughnessUv).g, 0.04, 1.0); let clearcoatNormalSample = textureSample(clearcoatNormalTex, clearcoatNormalSampler, clearcoatNormalUv).xyz; let clearcoatNormal = applyNormalMap(in.normal, in.tangent, in.worldPos, clearcoatNormalUv, clearcoatNormalSample, material.clearcoatParams.z); let specularStrength = clamp(material.specularParams.x * textureSample(specularTex, specularSampler, specularUv).a, 0.0, 1.0); let specularColor = material.specularParams.yzw * textureSample(specularColorTex, specularColorSampler, specularColorUv).rgb; let sheenColor = material.sheenParams.rgb; let sheenRoughness = clamp(material.sheenParams.w, 0.0, 1.0); let iridescence = clamp(material.iridescenceParams.x, 0.0, 1.0); let iridescenceThickness = material.iridescenceParams.w; let anisotropySample = vec3f(1.0, 0.5, 1.0); let transmission = clamp(material.transmissionParams.x * textureSample(transmissionTex, transmissionSampler, transmissionUv).r, 0.0, 1.0); let diffuseTransmission = clamp(material.transmissionParams.y * textureSample(diffuseTransmissionTex, diffuseTransmissionSampler, diffuseTransmissionUv).a, 0.0, 1.0); let volumeThickness = max(material.transmissionParams.z * textureSample(volumeThicknessTex, volumeThicknessSampler, volumeThicknessUv).g, 0.0); let dispersion = max(material.transmissionParams.w, 0.0); let diffuseTransmissionColor = material.diffuseTransmissionColor.rgb * textureSample(diffuseTransmissionColorTex, diffuseTransmissionColorSampler, diffuseTransmissionColorUv).rgb; let volumeAttenuation = volumeTransmissionAttenuation(volumeThickness, material.diffuseTransmissionColor.w, material.volumeAttenuation.rgb); let anisotropyStrength = clamp(material.anisotropyParams.x * anisotropySample.b, 0.0, 1.0); var anisotropyDirection = anisotropySample.rg * 2.0 - vec2f(1.0); let anisotropyDirectionLength2 = dot(anisotropyDirection, anisotropyDirection); anisotropyDirection = select(vec2f(1.0, 0.0), anisotropyDirection * inverseSqrt(max(anisotropyDirectionLength2, 1e-8)), anisotropyDirectionLength2 > 1e-8); anisotropyDirection = vec2f( material.anisotropyParams.y * anisotropyDirection.x - material.anisotropyParams.z * anisotropyDirection.y, material.anisotropyParams.z * anisotropyDirection.x + material.anisotropyParams.y * anisotropyDirection.y ); let albedo = baseColor.rgb; let V = normalize(camera.position - in.worldPos); let dielectricF0 = dielectricF0FromIor(material.extensionParams.x); let dielectricF0Color = min(vec3f(dielectricF0) * specularColor, vec3f(1.0)) * specularStrength; let F0 = mix(dielectricF0Color, albedo, metallic); let F90 = mix(vec3f(specularStrength), vec3f(1.0), metallic); let viewNdotV = max(dot(N, V), 0.0); var iridescenceFresnelColor = F0; if (iridescence > 1e-5 && iridescenceThickness > 0.0) { iridescenceFresnelColor = iridescentFresnel(1.0, material.iridescenceParams.y, F0, iridescenceThickness, viewNdotV); } var viewFresnel = fresnelSchlick(viewNdotV, F0, F90); if (iridescence > 1e-5) { viewFresnel = mix(viewFresnel, iridescenceFresnelColor, iridescence); } let transmissionWeight = transmission * (1.0 - metallic) * max(1.0 - maxComponent(viewFresnel), 0.0); let geometricN = normalize(in.normal); let anisotropyFrame = buildTangentFrame(geometricN, in.tangent, in.worldPos, anisotropyUv); let clearcoatViewFresnel = clamp(clearcoat * fresnelSchlickScalar(abs(dot(V, clearcoatNormal)), 0.04), 0.0, 1.0); var Lo = lighting.ambient.rgb * albedo * ao * (1.0 - transmission); for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let distance = length(lightDir); if (distance <= 1e-5) { continue; } L = lightDir / distance; attenuation = computeRangeAttenuation(distance, light.params.x); if (light.position.w == 2.0) { attenuation = attenuation * computeSpotFactor(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; let NdotL = max(dot(N, L), 0.0); let NdotV = viewNdotV; let NdotH = max(dot(N, H), 0.0); let VdotH = max(dot(V, H), 0.0); let baseF = fresnelSchlick(VdotH, F0, F90); var F = baseF; if (iridescence > 1e-5) { F = mix(baseF, iridescenceFresnelColor, iridescence); } var specularBrdf: vec3f; if (anisotropyStrength > 1e-5) { let anisotropicT = normalize(anisotropyFrame.t * anisotropyDirection.x + anisotropyFrame.b * anisotropyDirection.y); let anisotropicB = normalize(cross(geometricN, anisotropicT)); let TdotV = dot(anisotropicT, V); let BdotV = dot(anisotropicB, V); let TdotL = dot(anisotropicT, L); let BdotL = dot(anisotropicB, L); let TdotH = dot(anisotropicT, H); let BdotH = dot(anisotropicB, H); let alphaRoughness = max(roughness * roughness, 0.001); let at = mix(alphaRoughness, 1.0, anisotropyStrength * anisotropyStrength); let ab = alphaRoughness; let D = distributionGGXAnisotropic(NdotH, TdotH, BdotH, at, ab); let Vg = visibilityGGXAnisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL, at, ab); specularBrdf = F * D * Vg; } else { let NDF = distributionGGX(N, H, roughness); let G = geometrySmith(N, V, L, roughness); let numerator = NDF * G * F; let denominator = 4.0 * NdotV * NdotL + 0.0001; specularBrdf = numerator / denominator; } let sheenD = sheenDistribution(NdotH, sheenRoughness); let sheenV = sheenVisibility(NdotL, NdotV, sheenRoughness); let sheenBrdf = sheenColor * sheenD * sheenV; let diffuseEnergy = max(1.0 - maxComponent(F), 0.0); let kD = vec3f(diffuseEnergy) * (1.0 - metallic); let frontDiffuse = (1.0 - diffuseTransmission) * kD * albedo * radiance * NdotL / PI; let backDiffuse = diffuseTransmission * kD * diffuseTransmissionColor * radiance * max(dot(-N, L), 0.0) / PI; let diffuseContribution = (frontDiffuse + backDiffuse) * (1.0 - transmission); let specularContribution = (specularBrdf + sheenBrdf) * radiance * NdotL; let baseContribution = diffuseContribution + specularContribution; let clearcoatNdotL = max(dot(clearcoatNormal, L), 0.0); let clearcoatNdotV = max(dot(clearcoatNormal, V), 0.0); let clearcoatNDF = distributionGGX(clearcoatNormal, H, clearcoatRoughness); let clearcoatG = geometrySmith(clearcoatNormal, V, L, clearcoatRoughness); let clearcoatBrdf = clearcoatNDF * clearcoatG / (4.0 * clearcoatNdotV * clearcoatNdotL + 0.0001); let clearcoatContribution = vec3f(clearcoatBrdf) * radiance * clearcoatNdotL; Lo += mix(baseContribution, clearcoatContribution, clearcoatViewFresnel); } if (transmissionWeight > 1e-5) { let transmittedSource = sampleTransmissionSource(in.position, in.worldPos, N, V, material.extensionParams.x, dispersion, volumeThickness, in.modelScale); Lo += transmittedSource * albedo * volumeAttenuation * transmissionWeight * (1.0 - clearcoatViewFresnel); } Lo += emissive * (1.0 - clearcoatViewFresnel); Lo = Lo / (Lo + vec3f(1.0)); Lo = pow(Lo, vec3f(1.0 / 2.2)); return vec4f(Lo, baseColor.a); }"; // src/wgsl/graphics/data.wgsl var data_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct MaterialUniforms { scaleSource: vec4f, scaleDomain: vec4f, scaleClamp: vec4f, scaleParams: vec4f, scaleFlags: vec4f, colorParams: vec4f }; @group(1) @binding(0) var material: MaterialUniforms; @group(1) @binding(1) var data: array; @group(1) @binding(2) var colormapSampler: sampler; @group(1) @binding(3) var colormapTex: texture_1d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) dataValue: vec4f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; struct Light { position: vec4f, color: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; fn srgbFromLinear(c: vec3f) -> vec3f { let a = vec3f(0.055); return select(12.92 * c, (1.0 + a) * pow(c, vec3f(1.0 / 2.4)) - a, c > vec3f(0.0031308)); } fn luminance(rgb: vec3f) -> f32 { return dot(rgb, vec3f(0.2126, 0.7152, 0.0722)); } @vertex fn vs_main(in: VertexInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var out: VertexOutput; let worldPos4 = model.model * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos4; out.worldPos = worldPos4.xyz; out.normal = normalize((model.normalMatrix * vec4f(in.normal, 0.0)).xyz); let componentCount = max(1u, min(4u, u32(material.scaleSource.x + 0.5))); let stride = max(1u, u32(material.scaleSource.w + 0.5)); let dataOffset = u32(material.scaleDomain.z + 0.5); let base = vertexIndex * stride + dataOffset; var x: f32 = data[base + 0u]; var y: f32 = 0.0; var z: f32 = 0.0; var w: f32 = 0.0; if (componentCount > 1u) { y = data[base + 1u]; } if (componentCount > 2u) { z = data[base + 2u]; } if (componentCount > 3u) { w = data[base + 3u]; } out.dataValue = vec4f(x, y, z, w); return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let componentCount = max(1u, min(4u, u32(material.scaleSource.x + 0.5))); let componentIndex = min(3u, u32(material.scaleSource.y + 0.5)); let valueMode = u32(material.scaleSource.z + 0.5); let v = scale_select_value(in.dataValue, componentCount, componentIndex, valueMode); if (!scale_is_finite(v)) { discard; } let t = scale_apply_transform(v, vec4f(material.scaleDomain.x, material.scaleDomain.y, 0.0, material.scaleDomain.w), material.scaleClamp, material.scaleParams, material.scaleFlags); var cmap = textureSample(colormapTex, colormapSampler, t); let shading = scale_clamp01(material.colorParams.y); if (shading > 0.0) { let N = normalize(in.normal); var lightFactor: f32 = luminance(lighting.ambient.rgb); for (var i = 0u; i < lighting.lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - in.worldPos; let dist = length(lightDir); L = normalize(lightDir); 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; lightFactor += lum * attenuation * ndotl; } let shadedRgb = cmap.rgb * lightFactor; cmap = vec4f(mix(cmap.rgb, shadedRgb, shading), cmap.a); } let opacity = scale_clamp01(material.colorParams.x); let finalA = cmap.a * opacity; let finalRgb = clamp(cmap.rgb, vec3f(0.0), vec3f(1.0)); cmap = vec4f(finalRgb, finalA); return vec4f(srgbFromLinear(cmap.rgb), cmap.a); }"; // src/wgsl/graphics/custom-default-vertex.wgsl var custom_default_vertex_default = "struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f }; struct CameraUniforms { viewProjection: mat4x4f, position: vec3f }; struct ModelUniforms { model: mat4x4f, normalMatrix: mat4x4f }; @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 worldPos = model.model * vec4f(in.position, 1.0); out.position = camera.viewProjection * worldPos; out.worldPos = worldPos.xyz; out.normal = normalize((model.normalMatrix * vec4f(in.normal, 0.0)).xyz); out.uv = in.uv; return out; }"; // src/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; }; // src/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 }; } }; // src/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 { blendMode; cullMode; depthWrite; depthTest; pipeline = null; bindGroup = null; bindGroupKey = null; uniformBuffer = null; _uniformDataCache = null; _dirty = true; _refCount = 1; _destroyed = false; constructor(descriptor = {}) { 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) => { return normalizeStandardMaterialExtensions(extensions); }; 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; static _cachedBindGroupLayout = null; static _cachedLayoutDevice = null; static UNIFORM_FLOAT_COUNT = 204; static TEXTURE_BINDING_COUNT = 15; 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.bindGroupKey = null; this._dirty = true; } 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) { this._extensions = normalizeStandardMaterialExtensions(descriptor); this.invalidateBindings(); 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) { mask |= 32 /* 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) { mask |= 2048 /* Volume */; if (volume.thicknessTexture) mask |= 4096 /* ThicknessTexture */; } const specular = this._extensions.specular; if (specular) { mask |= 8192 /* Specular */; if (specular.texture) mask |= 16384 /* SpecularTexture */; if (specular.colorTexture) mask |= 32768 /* SpecularColorTexture */; } const sheen = this._extensions.sheen; if (sheen) { mask |= 65536 /* Sheen */; if (sheen.colorTexture) mask |= 131072 /* SheenColorTexture */; if (sheen.roughnessTexture) mask |= 262144 /* SheenRoughnessTexture */; } const iridescence = this._extensions.iridescence; if (iridescence) { mask |= 524288 /* Iridescence */; if (iridescence.texture) mask |= 1048576 /* IridescenceTexture */; if (iridescence.thicknessTexture) mask |= 2097152 /* IridescenceThicknessTexture */; } const anisotropy = this._extensions.anisotropy; if (anisotropy) { mask |= 4194304 /* 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 */; } if (this._extensions.dispersion) mask |= 536870912 /* Dispersion */; if (this._extensions.ior) mask |= 16777216 /* Ior */; if (this._extensions.emissiveStrength) mask |= 33554432 /* EmissiveStrength */; 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) { if (_StandardMaterial._cachedBindGroupLayout && _StandardMaterial._cachedLayoutDevice === device) return _StandardMaterial._cachedBindGroupLayout; const entries = [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]; for (let i = 0; i < _StandardMaterial.TEXTURE_BINDING_COUNT; i++) { const samplerBinding = 1 + i * 2; entries.push({ binding: samplerBinding, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }); entries.push({ binding: samplerBinding + 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } }); } const layout = device.createBindGroupLayout({ entries }); _StandardMaterial._cachedBindGroupLayout = layout; _StandardMaterial._cachedLayoutDevice = device; return layout; } usesTransmissionLayout() { const transmissionFactor = this._extensions.transmission?.factor ?? 0; const diffuseTransmissionFactor = this._extensions.diffuseTransmission?.factor ?? 0; return transmissionFactor > 0 || diffuseTransmissionFactor > 0; } getShaderCode(opts = {}) { if (opts.transmission) { if (opts.instanced) return standard_transmission_instanced_default; if (opts.skinned8) return standard_transmission_skinned8_default; if (opts.skinned) return standard_transmission_skinned_default; return standard_transmission_default; } if (opts.instanced) return standard_instanced_default; if (opts.skinned8) return standard_skinned8_default; if (opts.skinned) return standard_skinned_default; return standard_default; } }; 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 CustomMaterial = class extends Material { _vertexShader; _fragmentShader; _uniforms; _uniformLayout = null; _cachedBindGroupLayout = null; _cachedLayoutDevice = null; constructor(descriptor) { super(descriptor); this._vertexShader = descriptor.vertexShader ?? this.defaultVertexShader(); this._fragmentShader = descriptor.fragmentShader; this._uniforms = descriptor.uniforms ?? {}; } setUniform(name, value) { if (this._uniforms[name]) { this._uniforms[name].value = value; this._dirty = true; } } getUniform(name) { return this._uniforms[name]?.value; } getUniformSize(type) { switch (type) { case "f32": return 4; case "vec2f": return 8; case "vec3f": return 12; case "vec4f": return 16; case "mat4x4f": return 64; } } getUniformAlignment(type) { switch (type) { case "f32": return 4; case "vec2f": return 8; case "vec3f": return 16; case "vec4f": return 16; case "mat4x4f": return 16; } } getUniformLayout() { if (this._uniformLayout) return this._uniformLayout; let offset = 0; const offsets = {}; for (const [name, def] of Object.entries(this._uniforms)) { const align = this.getUniformAlignment(def.type); const size = this.getUniformSize(def.type); offset = this.alignTo(offset, align); offsets[name] = offset; offset += size; } const sizeBytes = Math.ceil(offset / 16) * 16 || 16; this._uniformLayout = { size: sizeBytes, offsets }; return this._uniformLayout; } alignTo(n, alignment) { return n + alignment - 1 & ~(alignment - 1); } getUniformBufferSize() { return this.getUniformLayout().size; } getUniformData() { const layout = this.getUniformLayout(); const data = this.getUniformDataCache(layout.size / 4); data.fill(0); for (const [name, def] of Object.entries(this._uniforms)) { const floatOffset = layout.offsets[name] >>> 2; if (typeof def.value === "number") data[floatOffset] = def.value; else data.set(def.value, floatOffset); } return data; } createBindGroupLayout(device) { if (this._cachedBindGroupLayout && this._cachedLayoutDevice === device) return this._cachedBindGroupLayout; const layout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } } ] }); this._cachedBindGroupLayout = layout; this._cachedLayoutDevice = device; return layout; } defaultVertexShader() { return custom_default_vertex_default; } getShaderCode(opts = {}) { let uniformStruct = "struct CustomUniforms {\n"; const uniformEntries = Object.entries(this._uniforms); if (uniformEntries.length === 0) uniformStruct += " _pad: f32\n"; else for (const [name, def] of uniformEntries) uniformStruct += ` ${name}: ${def.type}, `; uniformStruct += "};\n\n@group(1) @binding(0) var custom: CustomUniforms;\n\n"; return this._vertexShader + "\n" + uniformStruct + this._fragmentShader; } }; // src/world/pointcloud.ts var UNIFORM_FLOAT_COUNT = 4 + SCALE_UNIFORM_FLOAT_COUNT + 4 + 8 * 4; var UNIFORM_BYTE_SIZE = UNIFORM_FLOAT_COUNT * 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 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; _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; _colorsExternal = false; 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) this._colorMode = "rgba"; if (desc.softness !== void 0) this._softness = desc.softness; if (desc.keepCPUData !== void 0) this._keepCPUData = !!desc.keepCPUData; if (desc.ndShape !== void 0) this.ndShape = desc.ndShape; this.applyExplicitBounds(desc); if (desc.data) this.setData(desc.data, { 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); } 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.colorsBuffer) this.setColorsBuffer(resolveGPUBuffer(desc.colorsBuffer)); } 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; } 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._CPUData = data; this._pointCount = data.length / 4; this.clearColorsIfCountMismatch(); this._pointsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this._scaleRevision++; this.clearComputedBoundsIfNeeded(); } setPointsBuffer(buffer, pointCount) { assert(pointCount > 0, "PointCloud: pointCount must be > 0."); this._CPUData = null; this._pointCount = pointCount; this.clearColorsIfCountMismatch(); this.pointsBuffer = buffer; 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._colorsCPU = new Float32Array(data); this._colorsExternal = false; this._colorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; this.bindGroupKey = null; } setColorsBuffer(buffer) { if (buffer) assert(this._pointCount > 0, "PointCloud: pointCount must be > 0 when using colorsBuffer."); this.colorsBuffer = buffer; this._colorsCPU = null; this._colorsExternal = !!buffer; this._colorsDirty = false; this.bindGroupKey = null; } 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 | 0; if (pointCount <= 0) return; const pointsPtr = wasm.allocF32(data.length); const boxMinPtr = wasm.allocF32(3); const boxMaxPtr = wasm.allocF32(3); const sphereCenterPtr = wasm.allocF32(3); const sphereRadiusPtr = wasm.allocF32(1); try { wasm.f32view(pointsPtr, data.length).set(data); boundsf.pointcloudXYZS(boxMinPtr, boxMaxPtr, sphereCenterPtr, sphereRadiusPtr, pointsPtr, pointCount, 4); const boxMin = wasm.f32view(boxMinPtr, 3); const boxMax = wasm.f32view(boxMaxPtr, 3); const sphereCenter = wasm.f32view(sphereCenterPtr, 3); const sphereRadius = wasm.f32view(sphereRadiusPtr, 1)[0]; this.setBounds(boundsFromBoxAndSphere([boxMin[0], boxMin[1], boxMin[2]], [boxMax[0], boxMax[1], boxMax[2]], [sphereCenter[0], sphereCenter[1], sphereCenter[2]], sphereRadius), "computed"); } finally { wasm.freeF32(sphereRadiusPtr, 1); wasm.freeF32(sphereCenterPtr, 3); wasm.freeF32(boxMaxPtr, 3); wasm.freeF32(boxMinPtr, 3); wasm.freeF32(pointsPtr, data.length); } } 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(); } upload(device, queue) { 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.pointsBuffer = createBuffer(device, data, usage); else try { queue.writeBuffer(this.pointsBuffer, 0, data.buffer, data.byteOffset, data.byteLength); } catch { this.pointsBuffer.destroy(); this.pointsBuffer = createBuffer(device, data, usage); } if (!this._keepCPUData) this._CPUData = null; this._pointsDirty = false; this.bindGroupKey = null; } } } 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.colorsBuffer = createBuffer(device, colors, usage); else try { queue.writeBuffer(this.colorsBuffer, 0, colors.buffer, colors.byteOffset, colors.byteLength); } catch { this.colorsBuffer.destroy(); this.colorsBuffer = createBuffer(device, colors, usage); } 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 { } } destroy() { this.pointsBuffer?.destroy(); this.colorsBuffer?.destroy(); 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._ndShape = null; this._pointCount = 0; this._visualChangeListeners.clear(); this.transform.dispose(); } }; // src/world/glyphfield.ts var UNIFORM_FLOAT_COUNT2 = 5 * 4 + 4 + 4 + 8 * 4; var UNIFORM_BYTE_SIZE2 = UNIFORM_FLOAT_COUNT2 * 4; var normalizeGlyphScaleTransform = (transform) => { return 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; _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]; 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; 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; 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); } 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); } 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 }); } else if (desc.instanceCount !== void 0) { this._instanceCount = desc.instanceCount | 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; } 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._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._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) { const count = instanceCount | 0; assert(count > 0, "GlyphField: instanceCount must be > 0."); this._instanceCount = count; this.positionsBuffer = positions; this.rotationsBuffer = rotations; this.scalesBuffer = scales; this.attributesBuffer = 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; } computeBoundsFromCPUData() { const positions = this._positionsCPU; const scales = this._scalesCPU; const count = this._instanceCount; if (!positions || !scales || count <= 0) return; const rotations = this._rotationsCPU; const positionsPtr = wasm.allocF32(positions.length); const scalesPtr = wasm.allocF32(scales.length); let rotationsPtr = 0; const glyphCenterPtr = wasm.allocF32(3); const boxMinPtr = wasm.allocF32(3); const boxMaxPtr = wasm.allocF32(3); const sphereCenterPtr = wasm.allocF32(3); const sphereRadiusPtr = wasm.allocF32(1); try { wasm.f32view(positionsPtr, positions.length).set(positions); wasm.f32view(scalesPtr, scales.length).set(scales); if (rotations) { rotationsPtr = wasm.allocF32(rotations.length); wasm.f32view(rotationsPtr, rotations.length).set(rotations); } wasm.writeF32(glyphCenterPtr, 3, this.geometry.boundsCenter); boundsf.glyphInstances(boxMinPtr, boxMaxPtr, sphereCenterPtr, sphereRadiusPtr, positionsPtr, scalesPtr, rotationsPtr, count, glyphCenterPtr, this.geometry.boundsRadius); const boxMin = wasm.f32view(boxMinPtr, 3); const boxMax = wasm.f32view(boxMaxPtr, 3); this.setBounds(boundsFromBox([boxMin[0], boxMin[1], boxMin[2]], [boxMax[0], boxMax[1], boxMax[2]]), "computed"); } finally { wasm.freeF32(sphereRadiusPtr, 1); wasm.freeF32(sphereCenterPtr, 3); wasm.freeF32(boxMaxPtr, 3); wasm.freeF32(boxMinPtr, 3); wasm.freeF32(glyphCenterPtr, 3); if (rotationsPtr && rotations) wasm.freeF32(rotationsPtr, rotations.length); wasm.freeF32(scalesPtr, scales.length); wasm.freeF32(positionsPtr, positions.length); } } 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(); } upload(device, queue) { if (this._usingExternalBuffers) return; if (!this._dataDirty) return; if (this._instanceCount <= 0) { this._dataDirty = false; return; } const bytes = driver.bytes(); const requiredBytes = this._instanceCount * 16; const uploadSoA = (buf, cpu, ptr, label) => { if (!cpu && !ptr) return buf; const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; const byteLength = requiredBytes; const source = cpu ?? new Uint8Array(bytes.buffer, ptr >>> 0, byteLength); if (!buf) return createBuffer(device, source, usage, label); try { queue.writeBuffer(buf, 0, source.buffer, source.byteOffset, Math.min(source.byteLength, byteLength)); } catch { buf.destroy(); return createBuffer(device, source, usage, label); } return buf; }; this.positionsBuffer = uploadSoA(this.positionsBuffer, this._positionsCPU, this._positionsPtr, "GlyphField.positions"); this.rotationsBuffer = uploadSoA(this.rotationsBuffer, this._rotationsCPU, this._rotationsPtr, "GlyphField.rotations"); this.scalesBuffer = uploadSoA(this.scalesBuffer, this._scalesCPU, this._scalesPtr, "GlyphField.scales"); if (this._attributesCPU || this._attributesPtr) this.attributesBuffer = uploadSoA(this.attributesBuffer, this._attributesCPU, this._attributesPtr, "GlyphField.attributes"); else if (!this.attributesBuffer) this.attributesBuffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "GlyphField.attributesDummy" }); if (!this._keepCPUData) { this._positionsCPU = null; this._rotationsCPU = null; this._scalesCPU = null; this._attributesCPU = null; } this._dataDirty = false; 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 { } } } destroy() { this.positionsBuffer?.destroy(); this.rotationsBuffer?.destroy(); this.scalesBuffer?.destroy(); this.attributesBuffer?.destroy(); 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._ndShape = null; this._instanceCount = 0; this._visualChangeListeners.clear(); this.transform.dispose(); } }; // src/world/scene.ts var Scene = class _Scene { _meshes = []; _pointClouds = []; _glyphFields = []; _nodeLinks = []; _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; } 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 (!this._nodeLinks.includes(obj)) this._nodeLinks.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 { const idx = this._nodeLinks.indexOf(obj); if (idx !== -1) this._nodeLinks.splice(idx, 1); } return this; } clear() { for (const mesh of this._meshes) unregisterMeshSceneOwner(mesh, this); this._meshes = []; this._pointClouds = []; this._glyphFields = []; this._nodeLinks = []; return this; } clearPointClouds() { this._pointClouds = []; return this; } clearGlyphFields() { this._glyphFields = []; return this; } clearNodeLinks() { this._nodeLinks = []; 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); } 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 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; 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()); 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); } 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(); this._meshes = []; this._pointClouds = []; this._glyphFields = []; this._nodeLinks = []; this._lights = []; } }; // src/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; } }; // src/world/nodelink.ts var UNIFORM_FLOAT_COUNT3 = 128; var UNIFORM_BYTE_SIZE3 = UNIFORM_FLOAT_COUNT3 * 4; var normalizeNodeScaleTransform = (transform) => { return 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) => { return 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; _nodePositionsExternal = false; _nodeScalarsExternal = false; _nodeColorsExternal = false; _nodeRadiiExternal = false; _edgesExternal = false; _edgeScalarsExternal = false; _edgeColorsExternal = false; _nodePositionsDirty = true; _nodeScalarsDirty = true; _nodeColorsDirty = true; _nodeRadiiDirty = true; _edgesDirty = true; _edgeScalarsDirty = true; _edgeColorsDirty = true; _uniformDirty = true; _boundsSource = "none"; _keepCPUData = 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; 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); if (desc.nodePositions) this.setNodePositions(desc.nodePositions, { stride: desc.nodePositionsStride ?? 3, keepCPUData: this._keepCPUData }); else if (desc.nodePositionsBuffer) this.setNodePositionsBuffer(resolveGPUBuffer(desc.nodePositionsBuffer), desc.nodeCount ?? 0); 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.edgesBuffer) this.setEdgesBuffer(resolveGPUBuffer(desc.edgesBuffer), desc.edgeCount ?? 0); 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.nodeScalarsBuffer) this.setNodeScalarsBuffer(resolveGPUBuffer(desc.nodeScalarsBuffer)); if (desc.nodeColors) this.setNodeColors(desc.nodeColors, { keepCPUData: this._keepCPUData }); else if (desc.nodeColorsBuffer) this.setNodeColorsBuffer(resolveGPUBuffer(desc.nodeColorsBuffer)); if (desc.nodeRadii) this.setNodeRadii(desc.nodeRadii, { stride: desc.nodeRadiiStride ?? 3, keepCPUData: this._keepCPUData }); else if (desc.nodeRadiiBuffer) this.setNodeRadiiBuffer(resolveGPUBuffer(desc.nodeRadiiBuffer)); if (desc.edgeScalars) this.setEdgeScalars(desc.edgeScalars, { keepCPUData: this._keepCPUData }); else if (desc.edgeScalarsBuffer) this.setEdgeScalarsBuffer(resolveGPUBuffer(desc.edgeScalarsBuffer)); if (desc.edgeColors) this.setEdgeColors(desc.edgeColors, { keepCPUData: this._keepCPUData }); else if (desc.edgeColorsBuffer) this.setEdgeColorsBuffer(resolveGPUBuffer(desc.edgeColorsBuffer)); } 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; } 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; } 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._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) { assert(!!buffer, "NodeLink: nodePositionsBuffer is required."); assert(Number.isInteger(nodeCount) && nodeCount >= 0, "NodeLink: nodeCount must be an integer >= 0."); this.nodePositionsBuffer = buffer; 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._nodeScalarsCPU = new Float32Array(data); this._nodeScalarsExternal = false; this._nodeScalarsDirty = true; this._nodeScaleRevision++; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { this.nodeScalarsBuffer = buffer; 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._nodeColorsCPU = new Float32Array(data); this._nodeColorsExternal = false; this._nodeColorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { this.nodeColorsBuffer = buffer; 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._nodeRadiiCPU = this.packVec4FromStride(data, stride); this._nodeRadiiExternal = false; this._nodeRadiiDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { this.nodeRadiiBuffer = buffer; 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._edgeCount = u32.length / 2 | 0; this._edgesCPU = new Uint32Array(u32); this._edgesExternal = false; this._edgesDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { assert(!!buffer, "NodeLink: edgesBuffer is required."); assert(Number.isInteger(edgeCount) && edgeCount >= 0, "NodeLink: edgeCount must be an integer >= 0."); this.edgesBuffer = buffer; 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._edgeScalarsCPU = new Float32Array(data); this._edgeScalarsExternal = false; this._edgeScalarsDirty = true; this._edgeScaleRevision++; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { this.edgeScalarsBuffer = buffer; 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._edgeColorsCPU = new Float32Array(data); this._edgeColorsExternal = false; this._edgeColorsDirty = true; this._keepCPUData = opts.keepCPUData ?? this._keepCPUData; } 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) { this.edgeColorsBuffer = buffer; this._edgeColorsCPU = null; this._edgeColorsExternal = !!buffer; this._edgeColorsDirty = false; this.bindGroupKey = null; } 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(); } upload(device, queue) { const uploadF32 = (buf, cpu, dirty) => { if (!dirty || !cpu) return buf; if (!buf) return createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST); try { queue.writeBuffer(buf, 0, cpu.buffer, cpu.byteOffset, cpu.byteLength); return buf; } catch { buf.destroy(); return createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST); } }; const uploadU32 = (buf, cpu, dirty) => { if (!dirty || !cpu) return buf; if (!buf) return createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST); try { queue.writeBuffer(buf, 0, cpu.buffer, cpu.byteOffset, cpu.byteLength); return buf; } catch { buf.destroy(); return createBuffer(device, cpu, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST); } }; if (!this._nodePositionsExternal) this.nodePositionsBuffer = uploadF32(this.nodePositionsBuffer, this._nodePositionsCPU, this._nodePositionsDirty); if (!this._nodeScalarsExternal) this.nodeScalarsBuffer = uploadF32(this.nodeScalarsBuffer, this._nodeScalarsCPU, this._nodeScalarsDirty); if (!this._nodeColorsExternal) this.nodeColorsBuffer = uploadF32(this.nodeColorsBuffer, this._nodeColorsCPU, this._nodeColorsDirty); if (!this._nodeRadiiExternal) this.nodeRadiiBuffer = uploadF32(this.nodeRadiiBuffer, this._nodeRadiiCPU, this._nodeRadiiDirty); if (!this._edgesExternal) this.edgesBuffer = uploadU32(this.edgesBuffer, this._edgesCPU, this._edgesDirty); if (!this._edgeScalarsExternal) this.edgeScalarsBuffer = uploadF32(this.edgeScalarsBuffer, this._edgeScalarsCPU, this._edgeScalarsDirty); if (!this._edgeColorsExternal) this.edgeColorsBuffer = uploadF32(this.edgeColorsBuffer, this._edgeColorsCPU, this._edgeColorsDirty); 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; 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); } destroy() { this.nodePositionsBuffer?.destroy(); this.nodeScalarsBuffer?.destroy(); this.nodeColorsBuffer?.destroy(); this.nodeRadiiBuffer?.destroy(); this.edgesBuffer?.destroy(); this.edgeScalarsBuffer?.destroy(); this.edgeColorsBuffer?.destroy(); 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.transform.dispose(); } emitVisualChange(kind) { for (const listener of this._visualChangeListeners) { try { listener(kind); } catch { } } } }; // src/wgsl/core/smaa.wgsl var smaa_default = "struct Params { rtMetrics: vec4f, threshold: f32, _pad0: f32, _pad1: f32, _pad2: f32 }; @group(0) @binding(0) var params: Params; @group(0) @binding(1) var sampLinear: sampler; @group(0) @binding(2) var sampPoint: sampler; @group(0) @binding(3) var sceneTex: texture_2d; @group(0) @binding(4) var edgesTex: texture_2d; @group(0) @binding(5) var blendTex: texture_2d; struct VertexOutput { @builtin(position) pos: vec4f, @location(0) uv: vec2f }; @vertex fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VertexOutput { var positions = array( vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0) ); var uvs = array( vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0) ); var out: VertexOutput; out.pos = vec4f(positions[vi], 0.0, 1.0); out.uv = uvs[vi]; return out; } fn luma(rgb: vec3f) -> f32 { return dot(rgb, vec3f(0.2126, 0.7152, 0.0722)); } @fragment fn fs_smaa_edges(in: VertexOutput) -> @location(0) vec4f { let t = params.rtMetrics.xy; let c = textureSampleLevel(sceneTex, sampPoint, in.uv, 0.0).rgb; let l = luma(c); let lLeft = luma(textureSampleLevel(sceneTex, sampPoint, in.uv + vec2f(-t.x, 0.0), 0.0).rgb); let lTop = luma(textureSampleLevel(sceneTex, sampPoint, in.uv + vec2f(0.0, -t.y), 0.0).rgb); let dLeft = abs(l - lLeft); let dTop = abs(l - lTop); let eV = select(0.0, 1.0, dLeft >= params.threshold); let eH = select(0.0, 1.0, dTop >= params.threshold); return vec4f(eV, eH, 0.0, 0.0); } fn edgeV(uv: vec2f) -> bool { return textureSampleLevel(edgesTex, sampPoint, uv, 0.0).r > 0.5; } fn edgeH(uv: vec2f) -> bool { return textureSampleLevel(edgesTex, sampPoint, uv, 0.0).g > 0.5; } @fragment fn fs_smaa_weights(in: VertexOutput) -> @location(0) vec4f { let t = params.rtMetrics.xy; let e = textureSampleLevel(edgesTex, sampPoint, in.uv, 0.0); var wLeft: f32 = 0.0; var wTop: 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 (!edgeV(in.uv + vec2f(0.0, -t.y * f32(s)))) { break; } up = up + 1; } for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edgeV(in.uv + vec2f(0.0, t.y * f32(s)))) { break; } down = down + 1; } let len = f32(up + down + 1); wLeft = 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 (!edgeH(in.uv + vec2f(-t.x * f32(s), 0.0))) { break; } left = left + 1; } for (var s: i32 = 1; s <= 8; s = s + 1) { if (!edgeH(in.uv + vec2f(t.x * f32(s), 0.0))) { break; } right = right + 1; } let len = f32(left + right + 1); wTop = clamp(len / 17.0, 0.0, 1.0) * 0.5; } return vec4f(wLeft, wTop, 0.0, 0.0); } @fragment fn fs_smaa_neighborhood(in: VertexOutput) -> @location(0) vec4f { let t = params.rtMetrics.xy; let c = textureSampleLevel(sceneTex, sampLinear, in.uv, 0.0); let w = textureSampleLevel(blendTex, sampPoint, in.uv, 0.0); let wL = w.r; let wT = w.g; let wR = textureSampleLevel(blendTex, sampPoint, in.uv + vec2f(t.x, 0.0), 0.0).r; let wB = textureSampleLevel(blendTex, sampPoint, in.uv + vec2f(0.0, t.y), 0.0).g; var bestW: f32 = 0.0; var dir: i32 = -1; if (wL > bestW) { bestW = wL; dir = 0; } if (wR > bestW) { bestW = wR; dir = 1; } if (wT > bestW) { bestW = wT; dir = 2; } if (wB > bestW) { bestW = wB; dir = 3; } if (bestW <= 0.0) { return c; } var n: vec4f = c; if (dir == 0) { n = textureSampleLevel(sceneTex, sampLinear, in.uv + vec2f(-t.x, 0.0), 0.0); } else if (dir == 1) { n = textureSampleLevel(sceneTex, sampLinear, in.uv + vec2f(t.x, 0.0), 0.0); } else if (dir == 2) { n = textureSampleLevel(sceneTex, sampLinear, in.uv + vec2f(0.0, -t.y), 0.0); } else { n = textureSampleLevel(sceneTex, sampLinear, in.uv + vec2f(0.0, t.y), 0.0); } return mix(c, n, bestW); }"; // src/wgsl/world/pointcloud.wgsl var pointcloud_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct PointData { position: vec3f, scalar: f32 }; @group(1) @binding(0) var points: array; @group(1) @binding(4) var pointColors: array; struct PointCloudUniforms { sizeParams: vec4f, scaleSource: vec4f, scaleDomain: vec4f, scaleClamp: vec4f, scaleParams: vec4f, scaleFlags: vec4f, visual: vec4f, colors: array }; @group(1) @binding(1) var pc: PointCloudUniforms; @group(1) @binding(2) var colormapSampler: sampler; @group(1) @binding(3) var colormapTex: texture_1d; struct VertexOutput { @builtin(position) position: vec4f, @location(0) col: vec4f, @location(1) pointCoord: vec2f, }; struct CameraUniforms { viewProj: mat4x4, position: vec3f, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; fn srgbFromLinear(linear: vec3f) -> vec3f { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3f(1.0 / 2.4)) - vec3f(a); let useHi = linear > vec3f(0.0031308); return select(lo, hi, useHi); } fn sampleCustomStops(t: f32, stopCount: u32) -> vec4f { let n = min(stopCount, 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(tIn: f32) -> vec4f { let t = scale_clamp01(tIn); let stopCount = u32(pc.visual.z + 0.5); if (stopCount >= 2u) { return sampleCustomStops(t, stopCount); } return textureSampleLevel(colormapTex, colormapSampler, t, 0.0); } fn vec4Component(v: vec4f, 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 shiftedValueVector(v: vec4f, offsetFloats: f32) -> vec4f { let o = min(3u, u32(offsetFloats + 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 vec4f(vec4Component(v, i0), vec4Component(v, i1), vec4Component(v, i2), vec4Component(v, i3)); } @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { let p = points[instanceIndex]; let worldPos = model.model * vec4f(p.position, 1.0); let clip = camera.viewProj * worldPos; let baseSize = pc.sizeParams.x; let minSize = pc.sizeParams.y; let maxSize = pc.sizeParams.z; let atten = pc.sizeParams.w; var sizePx = baseSize; if (atten > 0.0) { let dist = distance(camera.position, worldPos.xyz); sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); let uv = vec2f( f32((vertexIndex + 2u) / 3u % 2u), f32((vertexIndex + 1u) / 3u % 2u) ); let row0 = vec3f(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3f(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; let colorMode = u32(pc.visual.w + 0.5); var c: vec4f; if (colorMode == 0u) { c = pointColors[instanceIndex]; } else { let rawVec = shiftedValueVector(vec4f(p.position, p.scalar), pc.scaleDomain.z); let componentCount = u32(pc.scaleSource.x + 0.5); let componentIndex = u32(pc.scaleSource.y + 0.5); let valueMode = u32(pc.scaleSource.z + 0.5); let rawValue = scale_select_value(rawVec, componentCount, componentIndex, valueMode); let finiteRaw = scale_is_finite(rawValue); var t = scale_apply_transform(rawValue, vec4f(pc.scaleDomain.x, pc.scaleDomain.y, 0.0, pc.scaleDomain.w), pc.scaleClamp, pc.scaleParams, pc.scaleFlags); c = colormap(t); if (!finiteRaw) { c = vec4f(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 + vec4f(offsetX, offsetY, 0.0, 0.0); out.pointCoord = uv * 2.0 - vec2f(1.0, 1.0); out.col = vec4f(srgbFromLinear(max(c.rgb, vec3f(0.0))), alpha); return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let uv = in.pointCoord; let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } let falloff = (1.0 - r2); let alpha = falloff * falloff; return vec4f(in.col.rgb, in.col.a * alpha); }"; // src/wgsl/world/glyphfield.wgsl var glyphfield_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } @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; struct GlyphFieldUniforms { scaleSource: vec4f, scaleDomain: vec4f, scaleClamp: vec4f, scaleParams: vec4f, scaleFlags: vec4f, visual: vec4f, solidColor: vec4f, colors: array }; @group(1) @binding(4) var glyph: GlyphFieldUniforms; @group(1) @binding(5) var colormapSampler: sampler; @group(1) @binding(6) var colormapTex: texture_1d; struct VertexInput { @location(0) position: vec3f, @location(1) normal: vec3f }; struct VertexOutput { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) @interpolate(flat) attrib: vec4f }; struct CameraUniforms { viewProj: mat4x4f, position: vec3f, _pad0: f32 }; struct ModelUniforms { model: mat4x4f, normal: mat4x4f }; struct Light { position: vec4f, color: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: 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; fn srgbFromLinear(linear: vec3f) -> vec3f { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3f(1.0 / 2.4)) - vec3f(a); let useHi = linear > vec3f(0.0031308); return select(lo, hi, useHi); } fn rotateByQuat(v: vec3f, q: vec4f) -> vec3f { let u = q.xyz; let s = q.w; let t = 2.0 * cross(u, v); return v + s * t + cross(u, t); } fn sampleCustomStops(t: f32) -> vec4f { 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(tIn: f32) -> vec4f { let t = scale_clamp01(tIn); let stopCount = u32(glyph.visual.y + 0.5); if (stopCount >= 2u) { return sampleCustomStops(t); } return textureSample(colormapTex, colormapSampler, t); } fn applyLighting(worldPos: vec3f, N: vec3f, baseColor: vec3f) -> vec3f { var Lo = lighting.ambient.rgb * baseColor; let lightCount = min(lighting.lightCount, 8u); for (var i = 0u; i < lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - worldPos; let distance = length(lightDir); L = select(vec3f(0.0, 1.0, 0.0), lightDir / 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 NdotL = max(dot(N, L), 0.0); let radiance = light.color.rgb * light.color.a * attenuation; Lo += baseColor * radiance * NdotL; } return Lo; } fn vec4Component(v: vec4f, 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 shiftedValueVector(v: vec4f, offsetFloats: f32) -> vec4f { let o = min(3u, u32(offsetFloats + 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 vec4f(vec4Component(v, i0), vec4Component(v, i1), vec4Component(v, i2), vec4Component(v, i3)); } @vertex fn vs_main(in: VertexInput, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { let p4 = positions[instanceIndex]; let q = rotations[instanceIndex]; let s4 = scales[instanceIndex]; let a4 = attributes[instanceIndex]; let scl = s4.xyz; let localPos = rotateByQuat(in.position * scl, q) + p4.xyz; let worldPos4 = model.model * vec4f(localPos, 1.0); let worldPos = worldPos4.xyz; let invScale = 1.0 / max(abs(scl), vec3f(1e-6)); let localN = in.normal * invScale; let instN = rotateByQuat(localN, q); let worldN = normalize((model.normal * vec4f(instN, 0.0)).xyz); var out: VertexOutput; out.position = camera.viewProj * vec4f(worldPos, 1.0); out.worldPos = worldPos; out.normal = worldN; out.attrib = a4; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let colorMode = u32(round(glyph.visual.z)); let lit = glyph.visual.w > 0.5; var baseColor: vec3f; var alpha: f32 = 1.0; if (colorMode == 0u) { baseColor = in.attrib.rgb; alpha = in.attrib.a; } else if (colorMode == 1u) { let shifted = shiftedValueVector(in.attrib, glyph.scaleDomain.z); let componentCount = u32(glyph.scaleSource.x + 0.5); let componentIndex = u32(glyph.scaleSource.y + 0.5); let valueMode = u32(glyph.scaleSource.z + 0.5); let rawValue = scale_select_value(shifted, componentCount, componentIndex, valueMode); if (!scale_is_finite(rawValue)) { discard; } let t = scale_apply_transform(rawValue, vec4f(glyph.scaleDomain.x, glyph.scaleDomain.y, 0.0, glyph.scaleDomain.w), glyph.scaleClamp, glyph.scaleParams, glyph.scaleFlags); let cmap = colormap(t); baseColor = cmap.rgb; alpha = cmap.a; } else { baseColor = glyph.solidColor.rgb; alpha = glyph.solidColor.a; } baseColor = max(baseColor, vec3f(0.0)); alpha = scale_clamp01(alpha) * scale_clamp01(glyph.visual.x); var shaded = baseColor; if (lit) { shaded = applyLighting(in.worldPos, normalize(in.normal), baseColor); } return vec4f(srgbFromLinear(shaded), alpha); }"; // src/wgsl/world/nodelink.wgsl var nodelink_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct CameraUniforms { viewProj: mat4x4f, position: vec3f, _pad0: f32 }; struct ModelUniforms { model: mat4x4f, normal: mat4x4f }; struct Light { position: vec4f, color: vec4f, params: vec4f }; struct LightingUniforms { ambient: vec4f, lightCount: u32, _pad0: u32, _pad1: u32, _pad2: u32, lights: array }; struct NodeLinkUniforms { global: vec4f, nodeScaleSource: vec4f, nodeScaleDomain: vec4f, nodeScaleClamp: vec4f, nodeScaleParams: vec4f, nodeScaleFlags: vec4f, nodeVisual: vec4f, edgeScaleSource: vec4f, edgeScaleDomain: vec4f, edgeScaleClamp: vec4f, edgeScaleParams: vec4f, edgeScaleFlags: vec4f, edgeVisual: vec4f, nodeSolid: vec4f, edgeSolid: vec4f, pointParams: vec4f, nodeStops: array, edgeStops: 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 nodePositions: array; @group(1) @binding(1) var nodeScalars: array; @group(1) @binding(2) var nodeColors: array; @group(1) @binding(3) var nodeRadii: array; @group(1) @binding(4) var edges: array; @group(1) @binding(5) var edgeScalars: array; @group(1) @binding(6) var edgeColors: array; @group(1) @binding(7) var nl: NodeLinkUniforms; @group(1) @binding(8) var nodeColormapSampler: sampler; @group(1) @binding(9) var nodeColormapTex: texture_1d; @group(1) @binding(10) var edgeColormapSampler: sampler; @group(1) @binding(11) var edgeColormapTex: texture_1d; fn srgbFromLinear(linear: vec3f) -> vec3f { let a = 0.055; let lo = 12.92 * linear; let hi = (1.0 + a) * pow(linear, vec3f(1.0 / 2.4)) - vec3f(a); let useHi = linear > vec3f(0.0031308); return select(lo, hi, useHi); } fn sampleCustomStops(tIn: f32, stops: array, stopCountIn: u32) -> vec4f { let n = min(8u, max(2u, stopCountIn)); let x = scale_clamp01(tIn) * 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 sampleNodeColormap(t: f32) -> vec4f { let stopCount = u32(nl.nodeVisual.y + 0.5); if (stopCount >= 2u) { return sampleCustomStops(t, nl.nodeStops, stopCount); } return textureSampleLevel(nodeColormapTex, nodeColormapSampler, scale_clamp01(t), 0.0); } fn sampleEdgeColormap(t: f32) -> vec4f { let stopCount = u32(nl.edgeVisual.y + 0.5); if (stopCount >= 2u) { return sampleCustomStops(t, nl.edgeStops, stopCount); } return textureSampleLevel(edgeColormapTex, edgeColormapSampler, scale_clamp01(t), 0.0); } fn nodeColor(index: u32) -> vec4f { let mode = u32(round(nl.nodeVisual.x)); if (mode == 0u) { return nodeColors[index]; } if (mode == 1u) { let rawValue = nodeScalars[index]; if (!scale_is_finite(rawValue)) { return vec4f(0.0, 0.0, 0.0, 0.0); } let t = scale_apply_transform(rawValue, vec4f(nl.nodeScaleDomain.x, nl.nodeScaleDomain.y, 0.0, nl.nodeScaleDomain.w), nl.nodeScaleClamp, nl.nodeScaleParams, nl.nodeScaleFlags); return sampleNodeColormap(t); } return nl.nodeSolid; } fn edgeColor(index: u32) -> vec4f { let mode = u32(round(nl.edgeVisual.x)); if (mode == 0u) { return edgeColors[index]; } if (mode == 1u) { let rawValue = edgeScalars[index]; if (!scale_is_finite(rawValue)) { return vec4f(0.0, 0.0, 0.0, 0.0); } let t = scale_apply_transform(rawValue, vec4f(nl.edgeScaleDomain.x, nl.edgeScaleDomain.y, 0.0, nl.edgeScaleDomain.w), nl.edgeScaleClamp, nl.edgeScaleParams, nl.edgeScaleFlags); return sampleEdgeColormap(t); } return nl.edgeSolid; } fn applyLighting(worldPos: vec3f, N: vec3f, baseColor: vec3f) -> vec3f { var Lo = lighting.ambient.rgb * baseColor; let lightCount = min(lighting.lightCount, 8u); for (var i = 0u; i < lightCount; i++) { let light = lighting.lights[i]; var L: vec3f; var attenuation: f32 = 1.0; if (light.position.w == 0.0) { L = normalize(-light.position.xyz); } else { let lightDir = light.position.xyz - worldPos; let distance = length(lightDir); L = select(vec3f(0.0, 1.0, 0.0), lightDir / 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 NdotL = max(dot(N, L), 0.0); let radiance = light.color.rgb * light.color.a * attenuation; Lo += baseColor * radiance * NdotL; } return Lo; } fn buildEdgeFrame(src: vec3f, dst: vec3f) -> mat3x3f { let yAxis = normalize(dst - src); var z = vec3f(0.0, 0.0, 1.0); if (abs(dot(z, yAxis)) > 0.99) { z = vec3f(1.0, 0.0, 0.0); } let xAxis = normalize(cross(z, yAxis)); let zAxis = normalize(cross(yAxis, xAxis)); return mat3x3f(xAxis, yAxis, zAxis); } struct NodeVertexOut { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) @interpolate(flat) nodeIndex: u32, @location(3) pointCoord: vec2f, @location(4) @interpolate(flat) isPoint: f32 }; struct EdgeVertexOut { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, @location(2) @interpolate(flat) edgeIndex: u32, @location(3) @interpolate(flat) litEnabled: f32 }; @vertex fn vs_node_points(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> NodeVertexOut { let p = nodePositions[instanceIndex].xyz; let worldPos4 = model.model * vec4f(p, 1.0); let clip = camera.viewProj * worldPos4; let baseSize = nl.global.x; let minSize = nl.pointParams.x; let maxSize = nl.pointParams.y; let atten = nl.pointParams.z; var sizePx = baseSize; if (atten > 0.0) { let dist = distance(camera.position, worldPos4.xyz); sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); let uv = vec2f(f32((vertexIndex + 2u) / 3u % 2u), f32((vertexIndex + 1u) / 3u % 2u)); let row0 = vec3f(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3f(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; var out: NodeVertexOut; out.position = clip + vec4f(offsetX, offsetY, 0.0, 0.0); out.worldPos = worldPos4.xyz; out.normal = vec3f(0.0, 0.0, 1.0); out.nodeIndex = instanceIndex; out.pointCoord = uv * 2.0 - vec2f(1.0, 1.0); out.isPoint = 1.0; return out; } @vertex fn vs_node_solid(@location(0) position: vec3f, @location(1) normal: vec3f, @builtin(instance_index) instanceIndex: u32) -> NodeVertexOut { let center = nodePositions[instanceIndex].xyz; let mode = u32(round(nl.nodeVisual.z)); let useRadii = nl.nodeVisual.w > 0.5; var scaleVec = vec3f(max(nl.global.x, 1e-6)); if (useRadii) { let rv = max(nodeRadii[instanceIndex].xyz, vec3f(1e-6)); if (mode == 2u) { scaleVec = rv * max(nl.global.x, 1e-6); } else { scaleVec = vec3f(rv.x * max(nl.global.x, 1e-6)); } } let objPos = center + (position * scaleVec); let worldPos4 = model.model * vec4f(objPos, 1.0); let localN = normalize(normal / scaleVec); let worldN = normalize((model.normal * vec4f(localN, 0.0)).xyz); var out: NodeVertexOut; out.position = camera.viewProj * worldPos4; out.worldPos = worldPos4.xyz; out.normal = worldN; out.nodeIndex = instanceIndex; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @vertex fn vs_edge_lines(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> EdgeVertexOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let objPos = select(src, dst, (vertexIndex & 1u) == 1u); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: EdgeVertexOut; out.position = camera.viewProj * worldPos4; out.worldPos = worldPos4.xyz; out.normal = vec3f(0.0, 1.0, 0.0); out.edgeIndex = instanceIndex; out.litEnabled = 0.0; return out; } @vertex fn vs_edge_cylinders(@location(0) position: vec3f, @location(1) normal: vec3f, @builtin(instance_index) instanceIndex: u32) -> EdgeVertexOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let seg = dst - src; let segLen = max(length(seg), 1e-6); let basis = buildEdgeFrame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3f(position.x * radius, position.y * segLen, position.z * radius); let objPos = ((src + dst) * 0.5) + (basis * local); let worldPos4 = model.model * vec4f(objPos, 1.0); let localN = normalize(basis * vec3f(normal.x, 0.0, normal.z)); let worldN = normalize((model.normal * vec4f(localN, 0.0)).xyz); var out: EdgeVertexOut; out.position = camera.viewProj * worldPos4; out.worldPos = worldPos4.xyz; out.normal = worldN; out.edgeIndex = instanceIndex; out.litEnabled = 1.0; return out; } @fragment fn fs_node(in: NodeVertexOut) -> @location(0) vec4f { var c = nodeColor(in.nodeIndex); if (in.isPoint > 0.5) { let r2 = dot(in.pointCoord, in.pointCoord); if (r2 > 1.0) { discard; } let falloff = (1.0 - r2); c = vec4f(c.rgb, c.a * (falloff * falloff)); } else if (nl.global.w > 0.5) { let litRgb = applyLighting(in.worldPos, normalize(in.normal), max(c.rgb, vec3f(0.0))); c = vec4f(litRgb, c.a); } c = vec4f(c.rgb, c.a * scale_clamp01(nl.global.z)); return vec4f(srgbFromLinear(max(c.rgb, vec3f(0.0))), c.a); } @fragment fn fs_edge(in: EdgeVertexOut) -> @location(0) vec4f { var c = edgeColor(in.edgeIndex); if (nl.global.w > 0.5 && in.litEnabled > 0.5) { let litRgb = applyLighting(in.worldPos, normalize(in.normal), max(c.rgb, vec3f(0.0))); c = vec4f(litRgb, c.a); } c = vec4f(c.rgb, c.a * scale_clamp01(nl.global.z)); return vec4f(srgbFromLinear(max(c.rgb, vec3f(0.0))), c.a); }"; // src/wgsl/core/picking-mesh.wgsl var picking_mesh_default = "enable primitive_index; struct CameraUniforms { viewProj: mat4x4, position: vec3, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var pick: PickUniforms; struct VertexOutput { @builtin(position) position: vec4 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; @vertex fn vs_main(@location(0) position: vec3) -> VertexOutput { var out: VertexOutput; out.position = camera.viewProj * model.model * vec4(position, 1.0); return out; } @fragment fn fs_main(@builtin(position) fragCoord: vec4, @builtin(primitive_index) primitiveIndex: u32) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + primitiveIndex); out.depth = fragCoord.z; return out; }"; // src/wgsl/core/picking-mesh-skinned.wgsl var picking_mesh_skinned_default = "enable primitive_index; struct CameraUniforms { viewProj: mat4x4, position: vec3, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; struct SkinBuffer { joints: array> }; @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; struct VertexOutput { @builtin(position) position: vec4 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; @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 localPos = m * vec4(position, 1.0); out.position = camera.viewProj * model.model * localPos; return out; } @fragment fn fs_main(@builtin(position) fragCoord: vec4, @builtin(primitive_index) primitiveIndex: u32) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + primitiveIndex); out.depth = fragCoord.z; return out; }"; // src/wgsl/core/picking-mesh-skinned8.wgsl var picking_mesh_skinned8_default = "enable primitive_index; struct CameraUniforms { viewProj: mat4x4, position: vec3, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; struct SkinBuffer { joints: array> }; @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; struct VertexOutput { @builtin(position) position: vec4 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; @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 localPos = m * vec4(position, 1.0); out.position = camera.viewProj * model.model * localPos; return out; } @fragment fn fs_main(@builtin(position) fragCoord: vec4, @builtin(primitive_index) primitiveIndex: u32) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + primitiveIndex); out.depth = fragCoord.z; return out; }"; // src/wgsl/world/picking-pointcloud.wgsl var picking_pointcloud_default = "struct PointData { position: vec3, scalar: f32 }; struct PointCloudUniforms { sizeParams: vec4, scalarParams: vec4, options: vec4, colors: array, 8> }; struct CameraUniforms { viewProj: mat4x4, position: vec3, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; @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; struct VertexOutput { @builtin(position) position: vec4, @location(0) pointCoord: vec2, @location(1) @interpolate(flat) pointIndex: u32 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { let p = points[instanceIndex]; let worldPos = model.model * vec4(p.position, 1.0); let clip = camera.viewProj * worldPos; let dist = distance(camera.position, worldPos.xyz); let baseSize = pc.sizeParams.x; let minSize = pc.sizeParams.y; let maxSize = pc.sizeParams.z; let atten = pc.sizeParams.w; var sizePx = baseSize; if (atten > 0.0) { sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); var uv = vec2(0.0); if (vertexIndex == 0u) { uv = vec2(0.0, 0.0); } else if (vertexIndex == 1u) { uv = vec2(1.0, 0.0); } else if (vertexIndex == 2u) { uv = vec2(0.0, 1.0); } else if (vertexIndex == 3u) { uv = vec2(1.0, 0.0); } else if (vertexIndex == 4u) { uv = vec2(1.0, 1.0); } else if (vertexIndex == 5u) { uv = vec2(0.0, 1.0); } let row0 = vec3(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; var out: VertexOutput; out.position = clip + vec4(offsetX, offsetY, 0.0, 0.0); out.pointCoord = uv; out.pointIndex = instanceIndex; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { let uv = in.pointCoord * 2.0 - vec2(1.0, 1.0); let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + in.pointIndex); out.depth = in.position.z; return out; }"; // src/wgsl/world/picking-glyphfield.wgsl var picking_glyphfield_default = "@group(1) @binding(0) var positions: array>; @group(1) @binding(1) var rotations: array>; @group(1) @binding(2) var scales: array>; struct CameraUniforms { viewProj: mat4x4, position: vec3, _pad0: f32 }; struct ModelUniforms { model: mat4x4, normal: mat4x4 }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(2) @binding(0) var pick: PickUniforms; struct VertexInput { @location(0) position: vec3 }; struct VertexOutput { @builtin(position) position: vec4, @location(0) @interpolate(flat) instanceIndex: u32 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; fn rotateByQuat(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) instanceIndex: u32) -> VertexOutput { let p4 = positions[instanceIndex]; let q = rotations[instanceIndex]; let s4 = scales[instanceIndex]; let localPos = rotateByQuat(in.position * s4.xyz, q) + p4.xyz; let worldPos = model.model * vec4(localPos, 1.0); var out: VertexOutput; out.position = camera.viewProj * worldPos; out.instanceIndex = instanceIndex; return out; } @fragment fn fs_main(in: VertexOutput) -> FragmentOutput { var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + in.instanceIndex); out.depth = in.position.z; return out; }"; // src/wgsl/world/picking-nodelink.wgsl var picking_nodelink_default = "struct CameraUniforms { viewProj: mat4x4f, position: vec3f, _pad0: f32 }; struct ModelUniforms { model: mat4x4f, normal: mat4x4f }; struct PickUniforms { objectId: u32, elementBase: u32, _pad0: u32, _pad1: u32 }; struct NodeLinkUniforms { global: vec4f, nodeScaleSource: vec4f, nodeScaleDomain: vec4f, nodeScaleClamp: vec4f, nodeScaleParams: vec4f, nodeScaleFlags: vec4f, nodeVisual: vec4f, edgeScaleSource: vec4f, edgeScaleDomain: vec4f, edgeScaleClamp: vec4f, edgeScaleParams: vec4f, edgeScaleFlags: vec4f, edgeVisual: vec4f, nodeSolid: vec4f, edgeSolid: vec4f, pointParams: vec4f, nodeStops: array, edgeStops: array }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var nodePositions: array; @group(1) @binding(3) var nodeRadii: array; @group(1) @binding(4) var edges: array; @group(1) @binding(7) var nl: NodeLinkUniforms; @group(2) @binding(0) var pick: PickUniforms; struct PickOut { @builtin(position) position: vec4f, @location(0) @interpolate(flat) index: u32, @location(1) pointCoord: vec2f, @location(2) @interpolate(flat) isPoint: f32 }; struct FragmentOutput { @location(0) id: vec2, @location(1) depth: f32 }; fn buildEdgeFrame(src: vec3f, dst: vec3f) -> mat3x3f { let yAxis = normalize(dst - src); var fallbackAxis = vec3f(0.0, 0.0, 1.0); if (abs(dot(fallbackAxis, yAxis)) > 0.99) { fallbackAxis = vec3f(1.0, 0.0, 0.0); } let xAxis = normalize(cross(fallbackAxis, yAxis)); let zAxis = normalize(cross(yAxis, xAxis)); return mat3x3f(xAxis, yAxis, zAxis); } @vertex fn vs_pick_node_points(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> PickOut { let p = nodePositions[instanceIndex].xyz; let worldPos4 = model.model * vec4f(p, 1.0); let clip = camera.viewProj * worldPos4; let baseSize = nl.global.x; let minSize = nl.pointParams.x; let maxSize = nl.pointParams.y; let atten = nl.pointParams.z; var sizePx = baseSize; if (atten > 0.0) { let dist = distance(camera.position, worldPos4.xyz); sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); let uv = vec2f(f32((vertexIndex + 2u) / 3u % 2u), f32((vertexIndex + 1u) / 3u % 2u)); let row0 = vec3f(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3f(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; var out: PickOut; out.position = clip + vec4f(offsetX, offsetY, 0.0, 0.0); out.index = instanceIndex; out.pointCoord = uv * 2.0 - vec2f(1.0, 1.0); out.isPoint = 1.0; return out; } @vertex fn vs_pick_node_solid(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> PickOut { let center = nodePositions[instanceIndex].xyz; let mode = u32(round(nl.nodeVisual.z)); let useRadii = nl.nodeVisual.w > 0.5; var scaleVec = vec3f(max(nl.global.x, 1e-6)); if (useRadii) { let rv = max(nodeRadii[instanceIndex].xyz, vec3f(1e-6)); if (mode == 2u) { scaleVec = rv * max(nl.global.x, 1e-6); } else { scaleVec = vec3f(rv.x * max(nl.global.x, 1e-6)); } } let objPos = center + (position * scaleVec); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: PickOut; out.position = camera.viewProj * worldPos4; out.index = instanceIndex; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @vertex fn vs_pick_edge_lines(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> PickOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let objPos = select(src, dst, (vertexIndex & 1u) == 1u); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: PickOut; out.position = camera.viewProj * worldPos4; out.index = instanceIndex; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @vertex fn vs_pick_edge_cylinders(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> PickOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let seg = dst - src; let segLen = max(length(seg), 1e-6); let basis = buildEdgeFrame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3f(position.x * radius, position.y * segLen, position.z * radius); let objPos = ((src + dst) * 0.5) + (basis * local); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: PickOut; out.position = camera.viewProj * worldPos4; out.index = instanceIndex; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @fragment fn fs_pick(in: PickOut) -> FragmentOutput { if (in.isPoint > 0.5) { let r2 = dot(in.pointCoord, in.pointCoord); if (r2 > 1.0) { discard; } } var out: FragmentOutput; out.id = vec2(pick.objectId, pick.elementBase + in.index); out.depth = in.position.z; return out; }"; // src/wgsl/core/occlusion-mesh.wgsl var occlusion_mesh_default = "struct CameraUniforms { viewProj: 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; struct VertexOutput { @builtin(position) position: vec4 }; @vertex fn vs_main(@location(0) position: vec3) -> VertexOutput { var out: VertexOutput; out.position = camera.viewProj * model.model * vec4(position, 1.0); return out; } @fragment fn fs_main(@builtin(position) fragCoord: vec4) -> @location(0) f32 { return fragCoord.z; }"; // src/wgsl/core/occlusion-reduce.wgsl var occlusion_reduce_default = "@group(0) @binding(0) var srcTex: texture_2d; struct VSOut { @builtin(position) pos: vec4f }; @vertex fn vs_main(@builtin(vertex_index) idx: u32) -> VSOut { var positions = array( vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0) ); var out: VSOut; out.pos = vec4f(positions[idx], 0.0, 1.0); return out; } @fragment fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) f32 { let srcSize = textureDimensions(srcTex); let dstCoord = vec2i(i32(fragCoord.x), i32(fragCoord.y)); let base = dstCoord * 2; let x1 = min(base.x + 1, i32(srcSize.x) - 1); let y1 = min(base.y + 1, i32(srcSize.y) - 1); let d00 = textureLoad(srcTex, base, 0).x; let d10 = textureLoad(srcTex, vec2i(x1, base.y), 0).x; let d01 = textureLoad(srcTex, vec2i(base.x, y1), 0).x; let d11 = textureLoad(srcTex, vec2i(x1, y1), 0).x; return max(max(d00, d10), max(d01, d11)); }"; // src/wgsl/world/occlusion-pointcloud.wgsl var occlusion_pointcloud_default = "struct PointData { position: vec3, scalar: f32 }; struct PointCloudUniforms { sizeParams: vec4, scalarParams: vec4, options: vec4, colors: array, 8> }; struct CameraUniforms { viewProj: 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; struct VertexOutput { @builtin(position) position: vec4, @location(0) pointCoord: vec2 }; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { let p = points[instanceIndex]; let worldPos = model.model * vec4(p.position, 1.0); let clip = camera.viewProj * worldPos; let dist = distance(camera.position, worldPos.xyz); let baseSize = pc.sizeParams.x; let minSize = pc.sizeParams.y; let maxSize = pc.sizeParams.z; let atten = pc.sizeParams.w; var sizePx = baseSize; if (atten > 0.0) { sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); var uv = vec2(0.0); if (vertexIndex == 0u) { uv = vec2(0.0, 0.0); } else if (vertexIndex == 1u) { uv = vec2(1.0, 0.0); } else if (vertexIndex == 2u) { uv = vec2(0.0, 1.0); } else if (vertexIndex == 3u) { uv = vec2(1.0, 0.0); } else if (vertexIndex == 4u) { uv = vec2(1.0, 1.0); } else if (vertexIndex == 5u) { uv = vec2(0.0, 1.0); } let row0 = vec3(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; var out: VertexOutput; out.position = clip + vec4(offsetX, offsetY, 0.0, 0.0); out.pointCoord = uv; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(position) fragCoord: vec4) -> @location(0) f32 { let uv = in.pointCoord * 2.0 - vec2(1.0, 1.0); let r2 = dot(uv, uv); if (r2 > 1.0) { discard; } return fragCoord.z; }"; // src/wgsl/world/occlusion-glyphfield.wgsl var occlusion_glyphfield_default = "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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn vec4Component(v: vec4f, 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 shiftedValueVector(v: vec4f, offsetFloats: f32) -> vec4f { let o = min(3u, u32(offsetFloats + 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 vec4f(vec4Component(v, i0), vec4Component(v, i1), vec4Component(v, i2), vec4Component(v, i3)); } @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>; struct GlyphFieldUniforms { scaleSource: vec4f, scaleDomain: vec4f, scaleClamp: vec4f, scaleParams: vec4f, scaleFlags: vec4f, visual: vec4f, solidColor: vec4f, colors: array }; @group(1) @binding(4) var glyph: GlyphFieldUniforms; struct CameraUniforms { viewProj: 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; struct VertexInput { @location(0) position: vec3 }; struct VertexOutput { @builtin(position) position: vec4, @location(0) @interpolate(flat) attrib: vec4f }; fn rotateByQuat(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) instanceIndex: u32) -> VertexOutput { let p4 = positions[instanceIndex]; let q = rotations[instanceIndex]; let s4 = scales[instanceIndex]; let localPos = rotateByQuat(in.position * s4.xyz, q) + p4.xyz; let worldPos = model.model * vec4(localPos, 1.0); var out: VertexOutput; out.position = camera.viewProj * worldPos; out.attrib = attributes[instanceIndex]; return out; } @fragment fn fs_main(in: VertexOutput, @builtin(position) fragCoord: vec4) -> @location(0) f32 { let colorMode = u32(round(glyph.visual.z)); if (colorMode == 1u) { let shifted = shiftedValueVector(in.attrib, glyph.scaleDomain.z); let componentCount = u32(glyph.scaleSource.x + 0.5); let componentIndex = u32(glyph.scaleSource.y + 0.5); let valueMode = u32(glyph.scaleSource.z + 0.5); let rawValue = scale_select_value(shifted, componentCount, componentIndex, valueMode); if (!scale_is_finite(rawValue)) { discard; } } return fragCoord.z; }"; // src/wgsl/world/occlusion-nodelink.wgsl var occlusion_nodelink_default = "struct CameraUniforms { viewProj: mat4x4f, position: vec3f, _pad0: f32 }; struct ModelUniforms { model: mat4x4f, normal: mat4x4f }; struct NodeLinkUniforms { global: vec4f, nodeScaleSource: vec4f, nodeScaleDomain: vec4f, nodeScaleClamp: vec4f, nodeScaleParams: vec4f, nodeScaleFlags: vec4f, nodeVisual: vec4f, edgeScaleSource: vec4f, edgeScaleDomain: vec4f, edgeScaleClamp: vec4f, edgeScaleParams: vec4f, edgeScaleFlags: vec4f, edgeVisual: vec4f, nodeSolid: vec4f, edgeSolid: vec4f, pointParams: vec4f, nodeStops: array, edgeStops: array }; @group(0) @binding(0) var camera: CameraUniforms; @group(0) @binding(1) var model: ModelUniforms; @group(1) @binding(0) var nodePositions: array; @group(1) @binding(3) var nodeRadii: array; @group(1) @binding(4) var edges: array; @group(1) @binding(7) var nl: NodeLinkUniforms; struct OcclusionOut { @builtin(position) position: vec4f, @location(0) pointCoord: vec2f, @location(1) @interpolate(flat) isPoint: f32 }; fn buildEdgeFrame(src: vec3f, dst: vec3f) -> mat3x3f { let yAxis = normalize(dst - src); var fallbackAxis = vec3f(0.0, 0.0, 1.0); if (abs(dot(fallbackAxis, yAxis)) > 0.99) { fallbackAxis = vec3f(1.0, 0.0, 0.0); } let xAxis = normalize(cross(fallbackAxis, yAxis)); let zAxis = normalize(cross(yAxis, xAxis)); return mat3x3f(xAxis, yAxis, zAxis); } @vertex fn vs_node_points(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> OcclusionOut { let p = nodePositions[instanceIndex].xyz; let worldPos4 = model.model * vec4f(p, 1.0); let clip = camera.viewProj * worldPos4; let baseSize = nl.global.x; let minSize = nl.pointParams.x; let maxSize = nl.pointParams.y; let atten = nl.pointParams.z; var sizePx = baseSize; if (atten > 0.0) { let dist = distance(camera.position, worldPos4.xyz); sizePx = baseSize * (atten / max(dist, 1e-6)); } sizePx = clamp(sizePx, minSize, maxSize); let uv = vec2f(f32((vertexIndex + 2u) / 3u % 2u), f32((vertexIndex + 1u) / 3u % 2u)); let row0 = vec3f(camera.viewProj[0][0], camera.viewProj[1][0], camera.viewProj[2][0]); let row1 = vec3f(camera.viewProj[0][1], camera.viewProj[1][1], camera.viewProj[2][1]); let aspect = length(row1) / max(length(row0), 1e-6); let ndcSize = (sizePx * 2.0) / max(camera._pad0, 1.0); let offsetX = (uv.x - 0.5) * ndcSize / aspect * clip.w; let offsetY = -(uv.y - 0.5) * ndcSize * clip.w; var out: OcclusionOut; out.position = clip + vec4f(offsetX, offsetY, 0.0, 0.0); out.pointCoord = uv * 2.0 - vec2f(1.0, 1.0); out.isPoint = 1.0; return out; } @vertex fn vs_node_solid(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> OcclusionOut { let center = nodePositions[instanceIndex].xyz; let mode = u32(round(nl.nodeVisual.z)); let useRadii = nl.nodeVisual.w > 0.5; var scaleVec = vec3f(max(nl.global.x, 1e-6)); if (useRadii) { let rv = max(nodeRadii[instanceIndex].xyz, vec3f(1e-6)); if (mode == 2u) { scaleVec = rv * max(nl.global.x, 1e-6); } else { scaleVec = vec3f(rv.x * max(nl.global.x, 1e-6)); } } let objPos = center + (position * scaleVec); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: OcclusionOut; out.position = camera.viewProj * worldPos4; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @vertex fn vs_edge_lines(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> OcclusionOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let objPos = select(src, dst, (vertexIndex & 1u) == 1u); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: OcclusionOut; out.position = camera.viewProj * worldPos4; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @vertex fn vs_edge_cylinders(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> OcclusionOut { let edge = edges[instanceIndex]; let src = nodePositions[edge.x].xyz; let dst = nodePositions[edge.y].xyz; let seg = dst - src; let segLen = max(length(seg), 1e-6); let basis = buildEdgeFrame(src, dst); let radius = max(nl.global.y, 1e-6); let local = vec3f(position.x * radius, position.y * segLen, position.z * radius); let objPos = ((src + dst) * 0.5) + (basis * local); let worldPos4 = model.model * vec4f(objPos, 1.0); var out: OcclusionOut; out.position = camera.viewProj * worldPos4; out.pointCoord = vec2f(0.0, 0.0); out.isPoint = 0.0; return out; } @fragment fn fs_main(in: OcclusionOut, @builtin(position) fragCoord: vec4f) -> @location(0) f32 { if (in.isPoint > 0.5) { let r2 = dot(in.pointCoord, in.pointCoord); if (r2 > 1.0) { discard; } } return fragCoord.z; }"; // src/core/renderer.ts var occlusionHashScratch = new ArrayBuffer(4); var occlusionHashF32 = new Float32Array(occlusionHashScratch); var occlusionHashU32 = new Uint32Array(occlusionHashScratch); var Renderer = class _Renderer { canvas; 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; globalBindGroups = []; skinBindGroupLayout; cameraUniformBuffer; modelUniformBuffers = []; modelBufferIndex = 0; MODEL_BUFFER_POOL_SIZE = 64; lightingUniformBuffer; instanceBuffer = null; instanceBufferCapacityBytes = 0; instanceBufferOffset = 0; INSTANCE_STRIDE_BYTES = 128; pipelineCache = /* @__PURE__ */ new Map(); shaderCache = /* @__PURE__ */ new Map(); drawItemPool = []; drawItemPoolUsed = 0; opaqueDrawList = []; transparentDrawList = []; pointCloudBindGroupLayout = null; pointCloudDummyColorsBuffer = null; pointCloudDrawItemPool = []; pointCloudDrawItemPoolUsed = 0; opaquePointCloudDrawList = []; transparentPointCloudDrawList = []; 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; 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; 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; } 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); 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() { if (!this.gpuTimingSupported) return; if (this.gpuQuerySet) return; try { this.gpuQuerySet = this.device.createQuerySet({ type: "timestamp", count: 2 }); this.gpuResolveBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC }); this.gpuResultBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); } catch (e) { this.gpuQuerySet = null; this.gpuResolveBuffer?.destroy(); this.gpuResolveBuffer = null; this.gpuResultBuffer?.destroy(); this.gpuResultBuffer = null; this.gpuTimingSupported = false; this.gpuTimingEnabled = false; console.warn("Renderer: failed to initialize GPU timing resources:", e); } } tryReadGpuTiming() { if (!this.gpuResultPending) return; const buf = this.gpuResultBuffer; if (!buf) return; if (buf.mapState !== "unmapped") return; this.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; this._gpuTimeNs = Number.isFinite(ns) ? ns : 0; } catch { } finally { try { buf.unmap(); } catch { } } }).catch(() => { try { buf.unmap(); } catch { } }); } 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(); this.resizePickTargets(); this.invalidateOcclusionResources(); } get aspectRatio() { return this.width / this.height; } refreshWasmStagingViews() { const buf = wasm.memory().buffer; const needRefresh = buf !== this._wasmBuffer || !this.cameraUniformStagingView || this.cameraUniformStagingView.byteOffset !== this.cameraUniformStagingPtr || !this.lightingUniformStagingView || this.lightingUniformStagingView.byteOffset !== this.lightingUniformStagingPtr || !this.modelUniformStagingView || this.modelUniformStagingView.byteOffset !== this.modelUniformStagingPtr; if (!needRefresh) return; this._wasmBuffer = buf; this.cameraUniformStagingView = wasm.f32view(this.cameraUniformStagingPtr, 20); this.lightingUniformStagingView = wasm.f32view(this.lightingUniformStagingPtr, 8 + Scene.MAX_LIGHTS * 16); this.lightingCountView = wasm.u32view(this.lightingUniformStagingPtr + 16, 1); this.modelUniformStagingView = wasm.f32view(this.modelUniformStagingPtr, 32); } getObjectId(obj) { let id = this.objectIds.get(obj); if (id !== void 0) return id; id = this.nextObjectId++; this.objectIds.set(obj, id); this.objectsById.set(id, obj); return id; } acquireDrawItem() { const i = this.drawItemPoolUsed++; let item = this.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, sortKey: 0 }; this.drawItemPool[i] = item; } return item; } acquirePointCloudDrawItem() { const i = this.pointCloudDrawItemPoolUsed++; let item = this.pointCloudDrawItemPool[i]; if (!item) { item = { cloud: null, pipeline: null, pipelineId: 0, cloudId: 0, sortKey: 0 }; this.pointCloudDrawItemPool[i] = item; } return item; } acquireGlyphFieldDrawItem() { const idx = this.glyphFieldDrawItemPoolUsed++; if (idx >= this.glyphFieldDrawItemPool.length) this.glyphFieldDrawItemPool.push({}); return this.glyphFieldDrawItemPool[idx]; } acquireNodeLinkDrawItem() { const idx = this.nodeLinkDrawItemPoolUsed++; if (idx >= this.nodeLinkDrawItemPool.length) this.nodeLinkDrawItemPool.push({}); return this.nodeLinkDrawItemPool[idx]; } ensureCullingCapacity(count) { if (count <= this.cullCapacity) return; let cap = Math.max(1, this.cullCapacity); while (cap < count) cap *= 2; this.cullCentersPtr = wasm.allocF32(cap * 3); this.cullRadiiPtr = wasm.allocF32(cap); this.cullCapacity = cap; } prepareSceneFrameBase(scene, camera) { this.modelBufferIndex = 0; this.instanceBufferOffset = 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 ("aspect" in camera) camera.aspect = this.aspectRatio; Transform.updateAll(); this.writeCameraUniforms(camera); this.writeLightingUniforms(scene); this.buildDrawLists(scene, camera); this.buildPointCloudDrawLists(scene, camera); this.buildGlyphFieldDrawLists(scene, camera); this.buildNodeLinkDrawLists(scene, camera); } 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); this.applyRenderCullingAndStats(camera); const encoder = this.device.createCommandEncoder(); 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); 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); 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); if (this.occlusionCullingEnabled) this.ensureOcclusionResources(); const hasTransmission = this.hasOpticalTransmissionDrawItems(); if (hasTransmission) this.ensureTransmissionTargets(!this.smaaEnabled); this.warmMeshDrawList(this.opaqueDrawList); this.warmMeshDrawList(this.transparentDrawList); this.warmPointCloudDrawList(this.opaquePointCloudDrawList); this.warmPointCloudDrawList(this.transparentPointCloudDrawList); this.warmGlyphFieldDrawList(this.opaqueGlyphFieldDrawList); this.warmGlyphFieldDrawList(this.transparentGlyphFieldDrawList); this.warmNodeLinkDrawList(this.opaqueNodeLinkDrawList); this.warmNodeLinkDrawList(this.transparentNodeLinkDrawList); } 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(() => this.runPick(scene, camera, x, y)); } pickRect(scene, camera, x0, y0, x1, y1, opts = {}) { return this.schedulePick(() => this.runPickRect(scene, camera, x0, y0, x1, y1, opts)); } pickLasso(scene, camera, points, opts = {}) { return this.schedulePick(() => this.runPickLasso(scene, camera, points, opts)); } clamp(x, min, max) { if (x < min) return min; if (x > max) return max; return x; } alignTo256(x) { return x + 255 & ~255; } getPickMaxHits(opts) { const v = opts.maxHits; if (!Number.isFinite(v)) return 1e4; return Math.max(1, Math.floor(v)); } toFramebufferPixel(clientCoord, clientSize, framebufferSize) { const size = Math.max(1, framebufferSize | 0); const t = clientCoord / Math.max(1, clientSize) * size; const p = Math.floor(t); if (p < 0) return 0; if (p >= size) return size - 1; return p; } toClientBounds(minX, minY, maxX, maxY, clientW, clientH) { const x = this.clamp(minX, 0, clientW); const y = this.clamp(minY, 0, clientH); const right = this.clamp(maxX, 0, clientW); const bottom = this.clamp(maxY, 0, clientH); return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) }; } resolveSinglePixel(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 = this.toFramebufferPixel(x, clientW, this.width); const py = this.toFramebufferPixel(y, clientH, this.height); return { px, py }; } resolveRectPickQuery(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 = this.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 = this.resolveSinglePixel(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 = this.toFramebufferPixel(bounds.x, clientW, this.width); const py0 = this.toFramebufferPixel(bounds.y, clientH, this.height); const px1 = this.toFramebufferPixel(maxClientX, clientW, this.width); const py1 = this.toFramebufferPixel(maxClientY, clientH, this.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 }; } resolveLassoPickQuery(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 = this.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, this.width); const fy = p.y / Math.max(1, clientH) * Math.max(1, this.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 = this.clamp(Math.floor(minFx), 0, Math.max(0, this.width - 1)); const py0 = this.clamp(Math.floor(minFy), 0, Math.max(0, this.height - 1)); const px1 = this.clamp(Math.floor(maxFx), 0, Math.max(0, this.width - 1)); const py1 = this.clamp(Math.floor(maxFy), 0, Math.max(0, this.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 }; } preparePickFrame(scene, camera) { this.prepareSceneFrameBase(scene, camera); if (!this.pickIdView || !this.pickDepthView || !this.pickDepthPayloadView) this.resizePickTargets(); } resolveRendererPickHit(camera, sample) { const obj = this.objectsById.get(sample.objectId); if (!obj) return null; const worldPosition = this.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 }; } return null; } 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; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; const intersects = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi || 1e-12) + xi; if (intersects) inside = !inside; } return inside; } async executePickRegion(scene, camera, query) { if (query.width <= 0 || query.height <= 0) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; this.preparePickFrame(scene, camera); if (!this.pickIdView || !this.pickDepthView || !this.pickDepthPayloadView) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; const readback = this.ensurePickReadbackBuffers(query.width, query.height); if (!this.pickIdTexture || !this.pickDepthPayloadTexture || !this.pickIdReadbackBuffer || !this.pickDepthReadbackBuffer) return { mode: query.mode, hits: [], truncated: false, bounds: query.bounds, sampledPixels: 0 }; if (this.pickIdReadbackBuffer.mapState !== "unmapped") try { this.pickIdReadbackBuffer.unmap(); } catch { } if (this.pickDepthReadbackBuffer.mapState !== "unmapped") try { this.pickDepthReadbackBuffer.unmap(); } catch { } const encoder = this.device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: this.pickIdView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }, { view: this.pickDepthPayloadView, clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: this.pickDepthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" } }); pass.setScissorRect(query.x, query.y, query.width, query.height); this.executeMeshPickDrawList(pass, this.opaqueDrawList); this.executeMeshPickDrawList(pass, this.transparentDrawList); this.executeGlyphPickDrawList(pass, this.opaqueGlyphFieldDrawList); this.executeGlyphPickDrawList(pass, this.transparentGlyphFieldDrawList); this.executePointCloudPickDrawList(pass, this.opaquePointCloudDrawList); this.executePointCloudPickDrawList(pass, this.transparentPointCloudDrawList); this.executeNodeLinkPickDrawList(pass, this.opaqueNodeLinkDrawList); this.executeNodeLinkPickDrawList(pass, this.transparentNodeLinkDrawList); pass.end(); encoder.copyTextureToBuffer( { texture: this.pickIdTexture, origin: { x: query.x, y: query.y, z: 0 } }, { buffer: this.pickIdReadbackBuffer, bytesPerRow: readback.idBytesPerRow, rowsPerImage: query.height }, { width: query.width, height: query.height, depthOrArrayLayers: 1 } ); encoder.copyTextureToBuffer( { texture: this.pickDepthPayloadTexture, origin: { x: query.x, y: query.y, z: 0 } }, { buffer: this.pickDepthReadbackBuffer, bytesPerRow: readback.depthBytesPerRow, rowsPerImage: query.height }, { width: query.width, height: query.height, depthOrArrayLayers: 1 } ); this.queue.submit([encoder.finish()]); await Promise.all([ this.pickIdReadbackBuffer.mapAsync(GPUMapMode.READ, 0, readback.idSizeBytes), this.pickDepthReadbackBuffer.mapAsync(GPUMapMode.READ, 0, readback.depthSizeBytes) ]); let truncated = false; let sampledPixels = 0; const samples = /* @__PURE__ */ new Map(); try { const idWords = new Uint32Array(this.pickIdReadbackBuffer.getMappedRange(0, readback.idSizeBytes)); const depthWords = new Float32Array(this.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 && !this.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 { this.pickIdReadbackBuffer.unmap(); } catch { } try { this.pickDepthReadbackBuffer.unmap(); } catch { } } const hits = []; for (const sample of samples.values()) { const hit = this.resolveRendererPickHit(camera, sample); if (hit) hits.push(hit); } return { mode: query.mode, hits, truncated, bounds: query.bounds, sampledPixels }; } async runPick(scene, camera, x, y) { frameArena.reset(); this.resize(); const clientW = Math.max(1, this.canvas.clientWidth || this.width); const clientH = Math.max(1, this.canvas.clientHeight || this.height); const pixel = this.resolveSinglePixel(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 this.executePickRegion(scene, camera, query); return result.hits.length > 0 ? result.hits[0] : null; } async runPickRect(scene, camera, x0, y0, x1, y1, opts) { frameArena.reset(); this.resize(); const clientW = Math.max(1, this.canvas.clientWidth || this.width); const clientH = Math.max(1, this.canvas.clientHeight || this.height); const query = this.resolveRectPickQuery(x0, y0, x1, y1, this.getPickMaxHits(opts), clientW, clientH); return this.executePickRegion(scene, camera, query); } async runPickLasso(scene, camera, points, opts) { frameArena.reset(); this.resize(); const clientW = Math.max(1, this.canvas.clientWidth || this.width); const clientH = Math.max(1, this.canvas.clientHeight || this.height); const query = this.resolveLassoPickQuery(points, this.getPickMaxHits(opts), clientW, clientH); return this.executePickRegion(scene, camera, query); } destroy() { if (this.destroyed) return; this.destroyed = true; 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(); for (const buffer of this.modelUniformBuffers) buffer.destroy(); for (const buffer of this.pickUniformBuffers) buffer.destroy(); this.modelUniformBuffers = []; 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; this.pendingOcclusionFrameState = null; this.lightingUniformBuffer?.destroy(); this.instanceBuffer?.destroy(); this.instanceBuffer = null; this.instanceBufferCapacityBytes = 0; this.globalBindGroups = []; this.pipelineCache.clear(); this.shaderCache.clear(); this.pointCloudBindGroupLayout = null; this.pointCloudDummyColorsBuffer?.destroy(); this.pointCloudDummyColorsBuffer = 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.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() { this.globalBindGroupLayout = this.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 80 } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform", minBindingSize: 128 } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: (8 + Scene.MAX_LIGHTS * 16) * 4 } } ] }); } createSkinBindGroupLayout() { this.skinBindGroupLayout = this.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }] }); } createUniformBuffers() { this.cameraUniformBuffer = this.device.createBuffer({ size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.lightingUniformBuffer = this.device.createBuffer({ size: (8 + Scene.MAX_LIGHTS * 16) * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.modelUniformBuffers = []; this.globalBindGroups = []; this.pickUniformBuffers = []; this.pickBindGroups = []; const pickLayout = this.getPickBindGroupLayout(); for (let i = 0; i < this.MODEL_BUFFER_POOL_SIZE; i++) { const modelBuffer = this.device.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.modelUniformBuffers.push(modelBuffer); this.globalBindGroups.push(this.device.createBindGroup({ layout: this.globalBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.cameraUniformBuffer } }, { binding: 1, resource: { buffer: modelBuffer } }, { binding: 2, resource: { buffer: this.lightingUniformBuffer } } ] })); const pickBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.pickUniformBuffers.push(pickBuffer); this.pickBindGroups.push(this.device.createBindGroup({ layout: pickLayout, entries: [{ binding: 0, resource: { buffer: pickBuffer } }] })); } this.cameraUniformStagingPtr = 0; this.lightingUniformStagingPtr = 0; this.modelUniformStagingPtr = 0; this._wasmBuffer = null; } getPickBindGroupLayout() { if (this.pickBindGroupLayout) return this.pickBindGroupLayout; this.pickBindGroupLayout = this.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform", minBindingSize: 16 } }] }); return this.pickBindGroupLayout; } ensureModelBufferPool(requiredCount) { const current = this.modelUniformBuffers.length; if (requiredCount <= current) return; let newSize = Math.max(1, current); while (newSize < requiredCount) newSize *= 2; this.modelUniformBuffers.length = newSize; this.globalBindGroups.length = newSize; this.pickUniformBuffers.length = newSize; this.pickBindGroups.length = newSize; const pickLayout = this.getPickBindGroupLayout(); for (let i = current; i < newSize; i++) { const modelBuffer = this.device.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.modelUniformBuffers[i] = modelBuffer; this.globalBindGroups[i] = this.device.createBindGroup({ layout: this.globalBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.cameraUniformBuffer } }, { binding: 1, resource: { buffer: modelBuffer } }, { binding: 2, resource: { buffer: this.lightingUniformBuffer } } ] }); const pickBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.pickUniformBuffers[i] = pickBuffer; this.pickBindGroups[i] = this.device.createBindGroup({ layout: pickLayout, entries: [{ binding: 0, resource: { buffer: pickBuffer } }] }); } } createFallbackTextures() { this.fallbackSampler = this.device.createSampler({ addressModeU: "repeat", addressModeV: "repeat", magFilter: "linear", minFilter: "linear", mipmapFilter: "linear" }); const create1x1 = (rgba, wantSrgbView) => { const tex = this.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]; this.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); this.fallbackWhiteTexture = white.tex; this.fallbackWhiteViewLinear = white.linear; this.fallbackWhiteViewSrgb = white.srgb; const normal = create1x1([128, 128, 255, 255], false); this.fallbackNormalTexture = normal.tex; this.fallbackNormalViewLinear = normal.linear; const mr = create1x1([0, 255, 255, 255], false); this.fallbackMRTex = mr.tex; this.fallbackMRViewLinear = mr.linear; const occ = create1x1([255, 0, 0, 255], false); this.fallbackOcclusionTex = occ.tex; this.fallbackOcclusionViewLinear = occ.linear; const anisotropy = create1x1([255, 128, 255, 255], false); this.fallbackAnisotropyTexture = anisotropy.tex; this.fallbackAnisotropyViewLinear = anisotropy.linear; } createSmaaResources() { if (this.smaaParamsBuffer) return; this.smaaParamsBuffer = this.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.smaaSamplerPoint = this.device.createSampler({ addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge", magFilter: "nearest", minFilter: "nearest" }); this.smaaSamplerLinear = this.device.createSampler({ addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge", magFilter: "linear", minFilter: "linear" }); const shaderCode = smaa_default; this.smaaShaderModule = this.device.createShaderModule({ code: shaderCode }); this.smaaEdgeBindGroupLayout = this.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" } } ] }); this.smaaWeightBindGroupLayout = this.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" } } ] }); this.smaaNeighborhoodBindGroupLayout = this.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 = this.device.createPipelineLayout({ bindGroupLayouts: [this.smaaEdgeBindGroupLayout] }); const weightLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.smaaWeightBindGroupLayout] }); const neighLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.smaaNeighborhoodBindGroupLayout] }); this.smaaEdgePipeline = this.device.createRenderPipeline({ layout: edgeLayout, vertex: { module: this.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: this.smaaShaderModule, entryPoint: "fs_smaa_edges", targets: [{ format: "rgba8unorm" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); this.smaaWeightPipeline = this.device.createRenderPipeline({ layout: weightLayout, vertex: { module: this.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: this.smaaShaderModule, entryPoint: "fs_smaa_weights", targets: [{ format: "rgba8unorm" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); this.smaaNeighborhoodPipeline = this.device.createRenderPipeline({ layout: neighLayout, vertex: { module: this.smaaShaderModule, entryPoint: "vs_fullscreen" }, fragment: { module: this.smaaShaderModule, entryPoint: "fs_smaa_neighborhood", targets: [{ format: this.format }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); } resizeSmaaTargets() { if (!this.smaaEnabled) return; if (!this.smaaParamsBuffer) this.createSmaaResources(); this.smaaSceneColorTexture?.destroy(); this.smaaEdgesTexture?.destroy(); this.smaaBlendTexture?.destroy(); const w = this.width | 0; const h = this.height | 0; if (w <= 0 || h <= 0) return; this.smaaSceneColorTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: this.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); this.smaaSceneColorView = this.smaaSceneColorTexture.createView(); const intermediateFormat = "rgba8unorm"; this.smaaEdgesTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: intermediateFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); this.smaaEdgesView = this.smaaEdgesTexture.createView(); this.smaaBlendTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: intermediateFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); this.smaaBlendView = this.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; this.queue.writeBuffer(this.smaaParamsBuffer, 0, params); this.smaaEdgeBindGroup = this.device.createBindGroup({ layout: this.smaaEdgeBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.smaaParamsBuffer } }, { binding: 2, resource: this.smaaSamplerPoint }, { binding: 3, resource: this.smaaSceneColorView } ] }); this.smaaWeightBindGroup = this.device.createBindGroup({ layout: this.smaaWeightBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.smaaParamsBuffer } }, { binding: 2, resource: this.smaaSamplerPoint }, { binding: 4, resource: this.smaaEdgesView } ] }); this.smaaNeighborhoodBindGroup = this.device.createBindGroup({ layout: this.smaaNeighborhoodBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.smaaParamsBuffer } }, { binding: 1, resource: this.smaaSamplerLinear }, { binding: 2, resource: this.smaaSamplerPoint }, { binding: 3, resource: this.smaaSceneColorView }, { binding: 5, resource: this.smaaBlendView } ] }); } ensureTransmissionTargets(needSceneTarget) { const haveSource = this.transmissionSourceTexture !== null && this.transmissionSourceView !== null; const haveSceneTarget = !needSceneTarget || this.transmissionSceneColorTexture !== null && this.transmissionSceneColorView !== null; if (haveSource && haveSceneTarget) return; this.resizeTransmissionTargets(needSceneTarget); } resizeTransmissionTargets(needSceneTarget) { this.transmissionSceneColorTexture?.destroy(); this.transmissionSourceTexture?.destroy(); this.transmissionSceneColorTexture = null; this.transmissionSceneColorView = null; this.transmissionSourceTexture = null; this.transmissionSourceView = null; const w = this.width | 0; const h = this.height | 0; if (w <= 0 || h <= 0) { this.transmissionSourceRevision++; return; } if (needSceneTarget) { this.transmissionSceneColorTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: this.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); this.transmissionSceneColorView = this.transmissionSceneColorTexture.createView(); } this.transmissionSourceTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: this.format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST }); this.transmissionSourceView = this.transmissionSourceTexture.createView(); this.transmissionSourceRevision++; } resizePickTargets() { const w = this.width | 0; const h = this.height | 0; if (w <= 0 || h <= 0) return; this.pickIdTexture?.destroy(); this.pickDepthTexture?.destroy(); this.pickDepthPayloadTexture?.destroy(); this.pickIdTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: "rg32uint", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC }); this.pickIdView = this.pickIdTexture.createView(); this.pickDepthTexture = createDepthTexture(this.device, w, h); this.pickDepthView = this.pickDepthTexture.createView(); this.pickDepthPayloadTexture = this.device.createTexture({ size: { width: w, height: h, depthOrArrayLayers: 1 }, format: "r32float", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC }); this.pickDepthPayloadView = this.pickDepthPayloadTexture.createView(); this.ensurePickReadbackBuffers(1, 1); } ensurePickReadbackBuffers(copyWidth, copyHeight) { const width = Math.max(1, copyWidth | 0); const height = Math.max(1, copyHeight | 0); const idBytesPerRow = this.alignTo256(width * 8); const depthBytesPerRow = this.alignTo256(width * 4); const idSizeBytes = idBytesPerRow * height; const depthSizeBytes = depthBytesPerRow * height; if (!this.pickIdReadbackBuffer || this.pickIdReadbackCapacityBytes < idSizeBytes) { this.pickIdReadbackBuffer?.destroy(); this.pickIdReadbackBuffer = this.device.createBuffer({ size: idSizeBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); this.pickIdReadbackCapacityBytes = idSizeBytes; } if (!this.pickDepthReadbackBuffer || this.pickDepthReadbackCapacityBytes < depthSizeBytes) { this.pickDepthReadbackBuffer?.destroy(); this.pickDepthReadbackBuffer = this.device.createBuffer({ size: depthSizeBytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); this.pickDepthReadbackCapacityBytes = depthSizeBytes; } return { idBytesPerRow, depthBytesPerRow, idSizeBytes, depthSizeBytes }; } writePickUniform(slot, objectId, elementBase = 0) { if (slot >= this.pickUniformBuffers.length) this.ensureModelBufferPool(slot + 1); const data = new Uint32Array([objectId >>> 0, elementBase >>> 0, 0, 0]); this.queue.writeBuffer(this.pickUniformBuffers[slot], 0, data.buffer, data.byteOffset, data.byteLength); } writeModelUniformSlot(slot, modelPtr) { if (slot >= this.modelUniformBuffers.length) this.ensureModelBufferPool(slot + 1); const modelBuffer = this.modelUniformBuffers[slot]; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); const bytes = driver.bytes(); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); } 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) { if (!this.smaaEdgePipeline || !this.smaaWeightPipeline || !this.smaaNeighborhoodPipeline) return; if (!this.smaaEdgeBindGroup || !this.smaaWeightBindGroup || !this.smaaNeighborhoodBindGroup) return; if (!this.smaaEdgesView || !this.smaaBlendView) return; const edgePass = encoder.beginRenderPass({ colorAttachments: [ { view: this.smaaEdgesView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); edgePass.setPipeline(this.smaaEdgePipeline); edgePass.setBindGroup(0, this.smaaEdgeBindGroup); edgePass.draw(3); edgePass.end(); const weightPass = encoder.beginRenderPass({ colorAttachments: [ { view: this.smaaBlendView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); weightPass.setPipeline(this.smaaWeightPipeline); weightPass.setBindGroup(0, this.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(this.smaaNeighborhoodPipeline); neighborhoodPass.setBindGroup(0, this.smaaNeighborhoodBindGroup); neighborhoodPass.draw(3); neighborhoodPass.end(); } writeCameraUniforms(camera) { this.refreshWasmStagingViews(); const proj = camera.getProjectionMatrix(); this.modelUniformStagingView.set(proj, 0); const viewPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(viewPtr, camera.transform.worldMatrixPtr); mat4f.mul(this.cameraUniformStagingPtr, this.modelUniformStagingPtr, viewPtr); const store = TransformStore.global(); const storeF32 = store.f32(); const base = (store.worldPtr >>> 2) + camera.transform.index * 16; this.cameraUniformStagingView[16] = storeF32[base + 12]; this.cameraUniformStagingView[17] = storeF32[base + 13]; this.cameraUniformStagingView[18] = storeF32[base + 14]; this.cameraUniformStagingView[19] = this.height; this.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraUniformStagingView); } writeLightingUniforms(scene) { const { ambient, lights } = scene.getLightingData(); this.refreshWasmStagingViews(); const data = this.lightingUniformStagingView; data.fill(0); data[0] = ambient[0]; data[1] = ambient[1]; data[2] = ambient[2]; data[3] = 1; this.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; } this.queue.writeBuffer(this.lightingUniformBuffer, 0, data); } recordFrustumCounts(tested, visible) { this.frameFrustumTested += tested; this.frameFrustumVisible += visible; } mixOcclusionHash(hash, value) { return Math.imul((hash ^ value >>> 0) >>> 0, 16777619) >>> 0; } blendModeHash(mode) { return mode === "opaque" /* Opaque */ ? 1 : mode === "transparent" /* Transparent */ ? 2 : 3; } cullModeHash(mode) { return mode === "back" /* Back */ ? 1 : mode === "front" /* Front */ ? 2 : 3; } mixOcclusionHashF32(hash, value) { occlusionHashF32[0] = Number.isFinite(value) ? value : 0; return this.mixOcclusionHash(hash, occlusionHashU32[0] >>> 0); } hashWorldMatrix(ptr) { const m = wasm.f32view(ptr, 16); let hash = 2166136261 >>> 0; for (let i = 0; i < 16; i++) hash = this.mixOcclusionHashF32(hash, m[i]); return hash >>> 0; } createOcclusionHierarchyLayout(width, height) { const widths = []; const heights = []; let w = Math.max(1, width | 0); let 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 }; } destroyOcclusionTextures() { this.occlusionHierarchyTexture?.destroy(); this.occlusionDepthTexture?.destroy(); this.occlusionHierarchyTexture = null; this.occlusionHierarchyMipViews = []; this.occlusionDepthTexture = null; this.occlusionDepthView = null; this.occlusionHierarchyLayout = null; this.occlusionWidth = 0; this.occlusionHeight = 0; this.occlusionReduceBindGroups.clear(); } invalidateOcclusionResources() { this.destroyOcclusionTextures(); this.latestOcclusionHierarchy = null; this.latestOcclusionHierarchySerial = 0; this.pendingOcclusionFrameState = null; for (const slot of this.occlusionReadbackSlots) { slot.metadata = null; slot.data = null; if (slot.state === "ready") slot.state = "idle"; } } ensureOcclusionResources() { if (!this.occlusionCullingEnabled) return; let targetW = this.width; let targetH = this.height; if (targetW >= targetH) { const scale = Math.min(1, this.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, this.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 = this.createOcclusionHierarchyLayout(targetW, targetH); if (this.occlusionHierarchyTexture && this.occlusionDepthTexture && this.occlusionWidth === targetW && this.occlusionHeight === targetH && this.occlusionHierarchyLayout?.mipCount === layout.mipCount) return; this.destroyOcclusionTextures(); this.occlusionWidth = targetW; this.occlusionHeight = targetH; this.occlusionHierarchyLayout = layout; this.occlusionHierarchyTexture = this.device.createTexture({ size: { width: targetW, height: targetH, depthOrArrayLayers: 1 }, format: "r32float", mipLevelCount: layout.mipCount, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC }); this.occlusionHierarchyMipViews = []; for (let i = 0; i < layout.mipCount; i++) { this.occlusionHierarchyMipViews.push(this.occlusionHierarchyTexture.createView({ dimension: "2d", baseMipLevel: i, mipLevelCount: 1 })); } this.occlusionDepthTexture = createDepthTexture(this.device, targetW, targetH); this.occlusionDepthView = this.occlusionDepthTexture.createView(); while (this.occlusionReadbackSlots.length < this.OCCLUSION_READBACK_RING_SIZE) { this.occlusionReadbackSlots.push({ buffer: null, capacityBytes: 0, pending: null, state: "idle", metadata: null, data: null, serial: 0 }); } } ensureOcclusionReadbackBuffer(slot, bytes) { if (slot.buffer && slot.capacityBytes >= bytes) return; slot.buffer?.destroy(); slot.buffer = this.device.createBuffer({ size: Math.max(256, alignTo(bytes, 256)), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); slot.capacityBytes = Math.max(256, alignTo(bytes, 256)); } getIdleOcclusionReadbackSlot() { for (const slot of this.occlusionReadbackSlots) if (!slot.pending && slot.state !== "mapping") return slot; return null; } buildOcclusionFrameState() { let signature = 2166136261 >>> 0; const candidates = this.occlusionCandidateScratch; candidates.length = 0; const meshOccluders = []; const pointCloudOccluders = []; const glyphOccluders = []; const nodeLinkOccluders = []; const candidateSeen = this.occlusionVisibleObjectIds; candidateSeen.clear(); for (const item of this.opaqueDrawList) { if (this.isSafeMeshOccluder(item)) { meshOccluders.push(item); signature = this.mixOcclusionHash(signature, 1); signature = this.mixOcclusionHash(signature, this.getObjectId(item.mesh)); signature = this.mixOcclusionHash(signature, this.getMeshOccluderToken(item)); } if (!candidateSeen.has(this.getObjectId(item.mesh)) && this.tryPushMeshOcclusionCandidate(item, candidates)) candidateSeen.add(this.getObjectId(item.mesh)); } for (const item of this.opaquePointCloudDrawList) { if (this.isSafePointCloudOccluder(item)) { pointCloudOccluders.push(item); signature = this.mixOcclusionHash(signature, 2); signature = this.mixOcclusionHash(signature, this.getObjectId(item.cloud)); signature = this.mixOcclusionHash(signature, item.cloud.occluderRevision >>> 0); signature = this.mixOcclusionHash(signature, this.hashWorldMatrix(item.cloud.transform.worldMatrixPtr)); } if (!candidateSeen.has(this.getObjectId(item.cloud)) && this.tryPushPointCloudOcclusionCandidate(item.cloud, candidates)) candidateSeen.add(this.getObjectId(item.cloud)); } for (const item of this.opaqueGlyphFieldDrawList) { if (this.isSafeGlyphOccluder(item)) { glyphOccluders.push(item); signature = this.mixOcclusionHash(signature, 3); signature = this.mixOcclusionHash(signature, this.getObjectId(item.field)); signature = this.mixOcclusionHash(signature, item.field.occluderRevision >>> 0); signature = this.mixOcclusionHash(signature, this.hashWorldMatrix(item.field.transform.worldMatrixPtr)); signature = this.mixOcclusionHash(signature, this.getObjectId(item.geometry)); } if (!candidateSeen.has(this.getObjectId(item.field)) && this.tryPushGlyphOcclusionCandidate(item.field, candidates)) candidateSeen.add(this.getObjectId(item.field)); } for (const item of this.opaqueNodeLinkDrawList) { if (this.isSafeNodeLinkOccluder(item)) { nodeLinkOccluders.push(item); signature = this.mixOcclusionHash(signature, 4); signature = this.mixOcclusionHash(signature, this.getObjectId(item.link)); signature = this.mixOcclusionHash(signature, item.link.occluderRevision >>> 0); signature = this.mixOcclusionHash(signature, this.hashWorldMatrix(item.link.transform.worldMatrixPtr)); signature = this.mixOcclusionHash(signature, item.passKind === "node-points" ? 1 : item.passKind === "node-solid" ? 2 : item.passKind === "edge-lines" ? 3 : 4); } if (!candidateSeen.has(this.getObjectId(item.link)) && this.tryPushNodeLinkOcclusionCandidate(item.link, candidates)) candidateSeen.add(this.getObjectId(item.link)); } candidateSeen.clear(); return { signature: signature >>> 0, candidates, meshOccluders, pointCloudOccluders, glyphOccluders, nodeLinkOccluders }; } tryPushMeshOcclusionCandidate(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: this.getObjectId(mesh), worldMatrixPtr: mesh.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: bounds.boundsRadius }); return true; } tryPushPointCloudOcclusionCandidate(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: this.getObjectId(cloud), worldMatrixPtr: cloud.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: cloud.boundsRadius }); return true; } tryPushGlyphOcclusionCandidate(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: this.getObjectId(field), worldMatrixPtr: field.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: field.boundsRadius }); return true; } tryPushNodeLinkOcclusionCandidate(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: this.getObjectId(link), worldMatrixPtr: link.transform.worldMatrixPtr, boundsCenter: [center[0], center[1], center[2]], boundsRadius: link.boundsRadius }); return true; } viewProjectionMatches(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]) > this.OCCLUSION_VIEW_PROJ_EPSILON) return false; return true; } getValidOcclusionHierarchy(camera, signature) { const latest = this.latestOcclusionHierarchy; if (!latest || !this.occlusionHierarchyLayout) return null; const meta = latest.metadata; if (meta.viewportWidth !== this.width || meta.viewportHeight !== this.height) return null; if (meta.hierarchyWidth !== this.occlusionWidth || meta.hierarchyHeight !== this.occlusionHeight) return null; if (meta.cameraType !== camera.type) return null; if (meta.occluderSignature !== signature) return null; if (!this.viewProjectionMatches(this.cameraUniformStagingPtr, meta.viewProjection)) return null; return latest; } applyOcclusionFiltering(camera, candidates, hierarchy) { if (candidates.length === 0) return; this.ensureCullingCapacity(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(this.cullCentersPtr, this.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, candidates.length); const layout = hierarchy.metadata.layout; const depthPtr = frameArena.allocF32(hierarchy.data.length); 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, this.cullCentersPtr, this.cullRadiiPtr, candidates.length, this.cameraUniformStagingPtr, this.width, this.height, mipOffsetsPtr, mipWidthsPtr, mipHeightsPtr, layout.mipCount, depthPtr, hierarchy.data.length, this.OCCLUSION_NEAR_EPSILON, this.OCCLUSION_MAX_SCREEN_COVERAGE, this.OCCLUSION_DEPTH_BIAS); if (this.occlusionCullingStatsEnabled) { const stats = wasm.u32view(statsPtr, 3); this.cullingStats.occlusion.tested = stats[0] >>> 0; this.cullingStats.occlusion.visible = stats[1] >>> 0; this.cullingStats.occlusion.occluded = stats[2] >>> 0; } const visibleSet = this.occlusionVisibleObjectIds; const candidateSet = this.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); this.filterOpaqueDrawListInPlace(this.opaqueDrawList, (item) => { const id = this.getObjectId(item.mesh); return !candidateSet.has(id) || visibleSet.has(id); }); this.filterOpaqueDrawListInPlace(this.opaquePointCloudDrawList, (item) => { const id = this.getObjectId(item.cloud); return !candidateSet.has(id) || visibleSet.has(id); }); this.filterOpaqueDrawListInPlace(this.opaqueGlyphFieldDrawList, (item) => { const id = this.getObjectId(item.field); return !candidateSet.has(id) || visibleSet.has(id); }); this.filterOpaqueDrawListInPlace(this.opaqueNodeLinkDrawList, (item) => { const id = this.getObjectId(item.link); return !candidateSet.has(id) || visibleSet.has(id); }); visibleSet.clear(); candidateSet.clear(); } 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; } 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; } isSafeMeshOccluder(item) { const material = item.material; if (material.blendMode !== "opaque" /* Opaque */) return false; if (!material.depthWrite || !material.depthTest) return false; if (this.isOpticallyTransmissiveMaterial(material)) return false; if (!this.isCoverageStableMeshMaterial(material)) return false; if (item.skinned) return false; return true; } getMeshOccluderToken(item) { const mesh = item.mesh; const material = item.material; let hash = 2166136261 >>> 0; hash = this.mixOcclusionHash(hash, this.getObjectId(item.geometry)); hash = this.mixOcclusionHash(hash, this.getObjectId(getMeshVertexSource(mesh))); hash = this.mixOcclusionHash(hash, this.getObjectId(material)); hash = this.mixOcclusionHash(hash, this.blendModeHash(material.blendMode)); hash = this.mixOcclusionHash(hash, material.depthWrite ? 1 : 0); hash = this.mixOcclusionHash(hash, material.depthTest ? 1 : 0); hash = this.mixOcclusionHash(hash, this.cullModeHash(material.cullMode)); hash = this.mixOcclusionHash(hash, item.mirrored ? 1 : 0); hash = this.mixOcclusionHash(hash, item.skinned ? 1 : 0); hash = this.mixOcclusionHash(hash, hasMeshMorphRuntime(mesh) ? 1 : 0); hash = this.mixOcclusionHash(hash, this.hashWorldMatrix(mesh.transform.worldMatrixPtr)); if (material instanceof UnlitMaterial) hash = this.mixOcclusionHashF32(hash, material.alphaCutoff); if (material instanceof StandardMaterial) { hash = this.mixOcclusionHashF32(hash, material.alphaCutoff); hash = this.mixOcclusionHash(hash, material.getFeatureMask() >>> 0); hash = this.mixOcclusionHash(hash, material.usesTransmissionLayout() ? 1 : 0); } return hash >>> 0; } isSafePointCloudOccluder(item) { return item.cloud.blendMode === "opaque" /* Opaque */ && item.cloud.depthWrite && item.cloud.depthTest; } isSafeGlyphOccluder(item) { return item.field.blendMode === "opaque" /* Opaque */ && item.field.depthWrite && item.field.depthTest; } isSafeNodeLinkOccluder(item) { return item.link.blendMode === "opaque" /* Opaque */ && item.link.depthWrite && item.link.depthTest; } buildDrawLists(scene, camera) { this.drawItemPoolUsed = 0; this.opaqueDrawList.length = 0; this.transparentDrawList.length = 0; const candidates = this.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 (this.frustumCullingEnabled) { this.ensureCullingCapacity(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(this.cullCentersPtr, this.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, count); const frustumPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(frustumPtr, this.cameraUniformStagingPtr); const outPtr = frameArena.alloc(count * 4, 4); visibleCount = cullf.spheresFrustum(outPtr, this.cullCentersPtr, this.cullRadiiPtr, count, frustumPtr); visibleIndicesBase = outPtr >>> 2; } this.recordFrustumCounts(count, visibleCount); const pushMesh = (mesh) => { const geometry = mesh.geometry; const material = mesh.material; const skinned = mesh.skin !== null && geometry.joints !== null && geometry.weights !== null && this.materialSupportsSkinning(material); const skinned8 = skinned && geometry.joints1 !== null && geometry.weights1 !== null; const wb = mesh.transform.worldMatrixPtr >>> 2; const mirrored = this.isMirroredWorldMatrix(storeF32, wb); const opticalTransmission = this.isOpticallyTransmissiveMaterial(material); const forceNoDepthWrite = opticalTransmission && material.blendMode !== "opaque" /* Opaque */; const pipeline = this.getOrCreatePipeline(material, false, skinned, skinned8, mirrored, forceNoDepthWrite); const item = this.acquireDrawItem(); item.mesh = mesh; item.geometry = geometry; item.material = material; item.pipeline = pipeline; item.pipelineId = this.getObjectId(pipeline); item.materialId = this.getObjectId(material); item.geometryId = this.getObjectId(geometry); item.vertexSourceId = this.getObjectId(getMeshVertexSource(mesh)); item.skinned = skinned; item.skinned8 = skinned8; item.mirrored = mirrored; item.sortKey = 0; if (material.blendMode === "opaque" /* Opaque */ && !opticalTransmission) this.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; this.transparentDrawList.push(item); } }; if (!this.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]]); } this.opaqueDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.materialId - b.materialId || a.vertexSourceId - b.vertexSourceId); this.transparentDrawList.sort((a, b) => b.sortKey - a.sortKey || a.pipelineId - b.pipelineId || a.materialId - b.materialId || a.vertexSourceId - b.vertexSourceId); } isOpticallyTransmissiveMaterial(material) { if (!(material instanceof StandardMaterial)) return false; const transmission = material.extensions.transmission; return (transmission?.factor ?? 0) > 0; } hasOpticalTransmissionDrawItems() { for (const item of this.transparentDrawList) if (this.isOpticallyTransmissiveMaterial(item.material)) return true; return false; } buildPointCloudDrawLists(scene, camera) { this.pointCloudDrawItemPoolUsed = 0; this.opaquePointCloudDrawList.length = 0; this.transparentPointCloudDrawList.length = 0; this.transparentMergedDrawList.length = 0; this.cullPointCloudScratch.length = 0; for (const pc of scene.pointClouds) { if (!pc.visible) continue; if (pc.pointCount <= 0) continue; this.cullPointCloudScratch.push(pc); } if (this.cullPointCloudScratch.length === 0) return; const ts = TransformStore.global(); const storeF32 = ts.f32(); const storeU32 = ts.u32(); const m = this.cameraUniformStagingView; const camX = m[16]; const camY = m[17]; const camZ = m[18]; const visible = []; if (this.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const pc of this.cullPointCloudScratch) { if (pc.boundsRadius > 0) bounded.push(pc); else unbounded.push(pc); } if (bounded.length > 0) { this.ensureCullingCapacity(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(this.cullCentersPtr, this.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const frustumPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(frustumPtr, this.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, this.cullCentersPtr, this.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 this.cullPointCloudScratch) visible.push(pc); this.recordFrustumCounts(this.cullPointCloudScratch.length, visible.length); for (const pc of visible) { const pipeline = this.getOrCreatePointCloudPipeline(pc); const pipelineId = this.getObjectId(pipeline); const cloudId = this.getObjectId(pc); const item = this.acquirePointCloudDrawItem(); item.cloud = pc; item.pipeline = pipeline; item.pipelineId = pipelineId; item.cloudId = cloudId; if (pc.blendMode === "opaque" /* Opaque */) { item.sortKey = 0; this.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; this.transparentPointCloudDrawList.push(item); } } this.opaquePointCloudDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.cloudId - b.cloudId); this.transparentPointCloudDrawList.sort((a, b) => b.sortKey - a.sortKey || a.pipelineId - b.pipelineId || a.cloudId - b.cloudId); } buildGlyphFieldDrawLists(scene, camera) { this.glyphFieldDrawItemPoolUsed = 0; this.opaqueGlyphFieldDrawList.length = 0; this.transparentGlyphFieldDrawList.length = 0; this.cullGlyphFieldScratch.length = 0; for (const gf of scene.glyphFields) { if (!gf.visible) continue; if (gf.instanceCount <= 0) continue; this.cullGlyphFieldScratch.push(gf); } if (this.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 (this.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const gf of this.cullGlyphFieldScratch) { if (gf.boundsRadius > 0) bounded.push(gf); else unbounded.push(gf); } if (bounded.length > 0) { this.ensureCullingCapacity(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(this.cullCentersPtr, this.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, this.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, this.cullCentersPtr, this.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 this.cullGlyphFieldScratch) visible.push(gf); this.recordFrustumCounts(this.cullGlyphFieldScratch.length, visible.length); for (const gf of visible) { const geometry = gf.geometry; const pipeline = this.getOrCreateGlyphFieldPipeline(gf); const item = this.acquireGlyphFieldDrawItem(); item.field = gf; item.geometry = geometry; item.pipeline = pipeline; item.pipelineId = this.getObjectId(pipeline); item.geometryId = this.getObjectId(geometry); item.fieldId = this.getObjectId(gf); if (gf.blendMode === "opaque" /* Opaque */) { item.sortKey = 0; this.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; this.transparentGlyphFieldDrawList.push(item); } } if (this.opaqueGlyphFieldDrawList.length > 0) { this.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 (this.transparentGlyphFieldDrawList.length > 0) { this.transparentGlyphFieldDrawList.sort((a, b) => { const d0 = b.sortKey - a.sortKey; if (d0 !== 0) return d0; const d1 = a.pipelineId - b.pipelineId; if (d1 !== 0) return d1; const d2 = a.geometryId - b.geometryId; if (d2 !== 0) return d2; return a.fieldId - b.fieldId; }); } } getNodeLinkNodeGeometry(mode) { if (mode === "cubes") { this.nodeLinkCubeGeometry ??= Geometry.box(1, 1, 1); return this.nodeLinkCubeGeometry; } this.nodeLinkSphereGeometry ??= Geometry.sphere(0.5, 16, 12); return this.nodeLinkSphereGeometry; } getNodeLinkEdgeCylinderGeometry() { this.nodeLinkCylinderGeometry ??= Geometry.cylinder(1, 1, 1, 14, 1, false); return this.nodeLinkCylinderGeometry; } buildNodeLinkDrawLists(scene, camera) { this.nodeLinkDrawItemPoolUsed = 0; this.opaqueNodeLinkDrawList.length = 0; this.transparentNodeLinkDrawList.length = 0; this.cullNodeLinkScratch.length = 0; for (const link of scene.nodeLinks) { if (!link.visible) continue; if (link.nodeCount <= 0 && link.edgeCount <= 0) continue; this.cullNodeLinkScratch.push(link); } if (this.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 (this.frustumCullingEnabled) { const bounded = []; const unbounded = []; for (const link of this.cullNodeLinkScratch) { if (link.boundsRadius > 0) bounded.push(link); else unbounded.push(link); } if (bounded.length > 0) { this.ensureCullingCapacity(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(this.cullCentersPtr, this.cullRadiiPtr, worldPtrsPtr, localCentersPtr, localRadiiPtr, bcount); const planesPtr = frameArena.allocF32(24); frustumf.writePlanesFromViewProjection(planesPtr, this.cameraUniformStagingPtr); const outPtr = frameArena.alloc(bounded.length * 4, 4); const numVisible = cullf.spheresFrustum(outPtr, this.cullCentersPtr, this.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 this.cullNodeLinkScratch) visible.push(link); this.recordFrustumCounts(this.cullNodeLinkScratch.length, visible.length); const pushItem = (link, passKind, geometry) => { const pipeline = this.getOrCreateNodeLinkPipeline(link, passKind); const item = this.acquireNodeLinkDrawItem(); item.link = link; item.pipeline = pipeline; item.pipelineId = this.getObjectId(pipeline); item.linkId = this.getObjectId(link); item.passKind = passKind; item.geometry = geometry; item.geometryId = geometry ? this.getObjectId(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 */) this.opaqueNodeLinkDrawList.push(item); else this.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", this.getNodeLinkNodeGeometry(link.nodeGeometryMode)); } if (link.edgeCount > 0) { if (link.edgeGeometryMode === "lines") pushItem(link, "edge-lines", null); else pushItem(link, "edge-cylinders", this.getNodeLinkEdgeCylinderGeometry()); } } this.opaqueNodeLinkDrawList.sort((a, b) => a.pipelineId - b.pipelineId || a.geometryId - b.geometryId || a.linkId - b.linkId); this.transparentNodeLinkDrawList.sort((a, b) => b.sortKey - a.sortKey || a.pipelineId - b.pipelineId || a.geometryId - b.geometryId || a.linkId - b.linkId); } captureOcclusionHierarchy(camera) { const frameState = this.pendingOcclusionFrameState; if (!frameState) return; const safeOccluderCount = frameState.meshOccluders.length + frameState.pointCloudOccluders.length + frameState.glyphOccluders.length + frameState.nodeLinkOccluders.length; if (safeOccluderCount <= 0) return; this.ensureOcclusionResources(); if (!this.occlusionHierarchyTexture || !this.occlusionDepthView || !this.occlusionHierarchyLayout) return; const slot = this.getIdleOcclusionReadbackSlot(); if (!slot) return; this.ensureOcclusionReadbackBuffer(slot, this.occlusionHierarchyLayout.totalBytes); if (!slot.buffer) return; this.modelBufferIndex = 0; const encoder = this.device.createCommandEncoder(); const capturePass = encoder.beginRenderPass({ colorAttachments: [ { view: this.occlusionHierarchyMipViews[0], clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ], depthStencilAttachment: { view: this.occlusionDepthView, depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" } }); this.executeOcclusionMeshDrawList(capturePass, frameState.meshOccluders); this.executeOcclusionGlyphFieldDrawList(capturePass, frameState.glyphOccluders); this.executeOcclusionPointCloudDrawList(capturePass, frameState.pointCloudOccluders); this.executeOcclusionNodeLinkDrawList(capturePass, frameState.nodeLinkOccluders); capturePass.end(); for (let mip = 1; mip < this.occlusionHierarchyLayout.mipCount; mip++) { const pass = encoder.beginRenderPass({ colorAttachments: [ { view: this.occlusionHierarchyMipViews[mip], clearValue: { r: 1, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" } ] }); pass.setPipeline(this.getOrCreateOcclusionReducePipeline()); pass.setBindGroup(0, this.getOrCreateOcclusionReduceBindGroup(mip - 1)); pass.draw(3); pass.end(); } for (let mip = 0; mip < this.occlusionHierarchyLayout.mipCount; mip++) { encoder.copyTextureToBuffer( { texture: this.occlusionHierarchyTexture, mipLevel: mip }, { buffer: slot.buffer, offset: this.occlusionHierarchyLayout.copyOffsets[mip], bytesPerRow: this.occlusionHierarchyLayout.rowBytes[mip], rowsPerImage: this.occlusionHierarchyLayout.heights[mip] }, { width: this.occlusionHierarchyLayout.widths[mip], height: this.occlusionHierarchyLayout.heights[mip], depthOrArrayLayers: 1 } ); } const metadata = { viewportWidth: this.width, viewportHeight: this.height, hierarchyWidth: this.occlusionWidth, hierarchyHeight: this.occlusionHeight, cameraType: camera.type, occluderSignature: frameState.signature, viewProjection: new Float32Array(wasm.f32view(this.cameraUniformStagingPtr, 16)), layout: this.occlusionHierarchyLayout }; slot.metadata = metadata; slot.data = null; slot.serial = ++this.occlusionCaptureSerial; slot.state = "mapping"; this.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.serial >= this.latestOcclusionHierarchySerial) { this.latestOcclusionHierarchySerial = slot.serial; this.latestOcclusionHierarchy = { metadata: slot.metadata, data }; } }).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; }); } executeOcclusionMeshDrawList(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(this.device); getMeshVertexBuffers(item.mesh, this.device, this.queue); lastGeometry = geometry; lastVertexSourceId = item.vertexSourceId; } const pipeline = this.getOrCreateOcclusionMeshPipeline(item); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } const slot = this.modelBufferIndex++; this.writeModelUniformSlot(slot, item.mesh.transform.worldMatrixPtr); pass.setBindGroup(0, this.globalBindGroups[slot]); const geometryBuffers = getMeshVertexBuffers(item.mesh, this.device, this.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); } } executeOcclusionPointCloudDrawList(pass, items) { let lastCloud = null; for (let i = 0; i < items.length; i++) { const item = items[i]; const cloud = item.cloud; this.ensurePointCloudBindGroup(cloud); if (!cloud.bindGroup) continue; const slot = this.modelBufferIndex++; this.writeModelUniformSlot(slot, cloud.transform.worldMatrixPtr); pass.setPipeline(this.getOrCreateOcclusionPointCloudPipeline()); pass.setBindGroup(0, this.globalBindGroups[slot]); if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; } pass.draw(6, cloud.pointCount); } } executeOcclusionGlyphFieldDrawList(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; this.ensureGlyphFieldBindGroup(field); if (!field.bindGroup) continue; const pipeline = this.getOrCreateOcclusionGlyphFieldPipeline(field); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (item.geometry !== lastGeometry) { item.geometry.upload(this.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); lastGeometry = item.geometry; } const slot = this.modelBufferIndex++; this.writeModelUniformSlot(slot, field.transform.worldMatrixPtr); pass.setBindGroup(0, this.globalBindGroups[slot]); 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); } } executeOcclusionNodeLinkDrawList(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; this.ensureNodeLinkBindGroup(link); if (!link.bindGroup) continue; const pipeline = this.getOrCreateOcclusionNodeLinkPipeline(link, item.passKind); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (item.geometry && item.geometry !== lastGeometry) { item.geometry.upload(this.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); lastGeometry = item.geometry; } const slot = this.modelBufferIndex++; this.writeModelUniformSlot(slot, link.transform.worldMatrixPtr); pass.setBindGroup(0, this.globalBindGroups[slot]); 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); } } } warmMeshDrawList(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; if (geometry !== lastGeometry || vertexSourceId !== lastVertexSourceId) { geometry.upload(this.device); getMeshVertexBuffers(first.mesh, this.device, this.queue); lastGeometry = geometry; lastVertexSourceId = vertexSourceId; } if (material !== lastMaterial) { this.ensureMaterialBindGroup(material); lastMaterial = material; } const canInstance = runCount > 1 && !first.skinned && !hasMeshMorphRuntime(first.mesh) && this.materialSupportsInstancing(material) && items === this.opaqueDrawList; if (canInstance) { this.getOrCreatePipeline(material, true, false, false, first.mirrored); this.warmInstancedRunResources(items, i, runCount); } else if (first.skinned) { for (let k = i; k < j; k++) { const skin = items[k].mesh.skin; if (skin) this.warmSkinResources(skin); } } i = j; } } warmSkinResources(skin) { if (!skin) return; skin.ensureGpuResources(this.device, this.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.bindMatrixPtr); const bytes = driver.bytes(); this.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); } warmInstancedRunResources(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 * this.INSTANCE_STRIDE_BYTES; const dstOffset = this.instanceBufferOffset; const dstEnd = dstOffset + outBytes; this.ensureInstanceBuffer(dstEnd); const bytes = driver.bytes(); this.queue.writeBuffer(this.instanceBuffer, dstOffset, bytes, outPtr, outBytes); this.instanceBufferOffset = dstEnd; } warmPointCloudDrawList(items) { for (const item of items) { const cloud = item.cloud; if (!cloud.visible) continue; if (cloud.pointCount <= 0) continue; this.ensurePointCloudBindGroup(cloud); } } warmGlyphFieldDrawList(items) { for (const item of items) { const field = item.field; if (!field.visible) continue; if (field.instanceCount <= 0) continue; item.geometry.upload(this.device); this.ensureGlyphFieldBindGroup(field); } } warmNodeLinkDrawList(items) { for (const item of items) { this.ensureNodeLinkBindGroup(item.link); if (item.geometry) item.geometry.upload(this.device); } } executeDrawList(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; if (geometry !== lastGeometry || vertexSourceId !== lastVertexSourceId) geometry.upload(this.device); if (material !== lastMaterial) this.ensureMaterialBindGroup(material); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; } if (material !== lastMaterial) { pass.setBindGroup(1, material.bindGroup); lastMaterial = material; } if (geometry !== lastGeometry || vertexSourceId !== lastVertexSourceId) { const buffers = getMeshVertexBuffers(first.mesh, this.device, this.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); if (first.skinned) { if (standardMaterial) pass.setVertexBuffer(5, geometry.skinInfluenceBuffer); else { pass.setVertexBuffer(4, geometry.jointsBuffer); pass.setVertexBuffer(5, geometry.weightsBuffer); if (first.skinned8) { pass.setVertexBuffer(6, geometry.joints1Buffer); pass.setVertexBuffer(7, geometry.weights1Buffer); } } } if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; lastVertexSourceId = vertexSourceId; } const canInstance = runCount > 1 && !first.skinned && !hasMeshMorphRuntime(first.mesh) && this.materialSupportsInstancing(material) && items === this.opaqueDrawList; if (canInstance) { const instancedPipeline = this.getOrCreatePipeline(material, true, false, false, first.mirrored); if (instancedPipeline !== lastPipeline) { pass.setPipeline(instancedPipeline); lastPipeline = instancedPipeline; } this.drawInstancedRun(pass, geometry, material, items, i, runCount); } else { for (let k = i; k < j; k++) { if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const bytes = driver.bytes(); const mesh = items[k].mesh; const skin = first.skinned ? mesh.skin : null; if (skin) { skin.ensureGpuResources(this.device, this.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.bindMatrixPtr); this.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); pass.setBindGroup(2, skin.bindGroup); } const modelPtr = mesh.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount); else pass.draw(geometry.vertexCount); } } i = j; } } executePointCloudDrawList(pass, items) { if (items.length === 0) return; const bytes = driver.bytes(); const mat42 = mat4f; 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; this.ensurePointCloudBindGroup(cloud); if (!cloud.bindGroup) continue; if (item.pipeline !== lastPipeline) { pass.setPipeline(item.pipeline); lastPipeline = item.pipeline; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = cloud.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat42.invert(invPtr, modelPtr); mat42.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); pass.setBindGroup(1, cloud.bindGroup); pass.draw(6, cloud.pointCount); } } executeGlyphFieldDrawList(pass, list) { if (list.length === 0) return; const bytes = driver.bytes(); 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; this.ensureGlyphFieldBindGroup(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(this.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; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = field.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, field.instanceCount); else pass.draw(geometry.vertexCount, field.instanceCount); } } executeNodeLinkDrawList(pass, list) { if (list.length === 0) return; const bytes = driver.bytes(); 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; this.ensureNodeLinkBindGroup(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(this.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; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const slot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[slot]; const globalBindGroup = this.globalBindGroups[slot]; const modelPtr = link.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); 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); } } } executeTransparentMergedDrawList(pass) { this.transparentMergedDrawList.length = 0; for (const item of this.transparentDrawList) this.transparentMergedDrawList.push(item); for (const item of this.transparentGlyphFieldDrawList) this.transparentMergedDrawList.push(item); for (const item of this.transparentPointCloudDrawList) this.transparentMergedDrawList.push(item); for (const item of this.transparentNodeLinkDrawList) this.transparentMergedDrawList.push(item); if (this.transparentMergedDrawList.length === 0) return; const typeOrder = (x) => { if ("mesh" in x) return 0; if ("field" in x) return 1; if ("cloud" in x) return 2; return 3; }; this.transparentMergedDrawList.sort((a, b) => { const d0 = b.sortKey - a.sortKey; if (d0 !== 0) return d0; const d1 = a.pipelineId - b.pipelineId; if (d1 !== 0) return d1; const aIsMesh = "mesh" in a; const bIsMesh = "mesh" in b; if (aIsMesh && bIsMesh) { const am = a; const bm = b; return am.materialId - bm.materialId || am.geometryId - bm.geometryId || am.vertexSourceId - bm.vertexSourceId || (am.skinned ? 1 : 0) - (bm.skinned ? 1 : 0) || (am.skinned8 ? 1 : 0) - (bm.skinned8 ? 1 : 0); } const aIsCloud = "cloud" in a; const bIsCloud = "cloud" in b; if (aIsCloud && bIsCloud) { const ap = a; const bp = b; return ap.cloudId - bp.cloudId; } const aIsGlyph = "field" in a; const bIsGlyph = "field" in b; if (aIsGlyph && bIsGlyph) { const ag = a; const bg = b; return ag.geometryId - bg.geometryId || ag.fieldId - bg.fieldId; } const aIsNodeLink = "link" in a; const bIsNodeLink = "link" in b; if (aIsNodeLink && bIsNodeLink) { const an = a; const bn = b; return an.geometryId - bn.geometryId || an.linkId - bn.linkId; } return typeOrder(a) - typeOrder(b); }); 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 lastGlyph = null; let lastNodeLink = null; for (let i = 0; i < this.transparentMergedDrawList.length; i++) { const item = this.transparentMergedDrawList[i]; 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; lastGlyph = null; lastNodeLink = null; } if (geometry !== lastGeometry) geometry.upload(this.device); if (material !== lastMaterial) this.ensureMaterialBindGroup(material); if (material !== lastMaterial) { pass.setBindGroup(1, material.bindGroup); lastMaterial = material; } if (geometry !== lastGeometry || drawItem.vertexSourceId !== lastVertexSourceId || drawItem.skinned !== lastSkinned || drawItem.skinned8 !== lastSkinned8) { const buffers = getMeshVertexBuffers(mesh, this.device, this.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); if (drawItem.skinned) { if (standardMaterial) pass.setVertexBuffer(5, geometry.skinInfluenceBuffer); else { pass.setVertexBuffer(4, geometry.jointsBuffer); pass.setVertexBuffer(5, geometry.weightsBuffer); if (drawItem.skinned8) { pass.setVertexBuffer(6, geometry.joints1Buffer); pass.setVertexBuffer(7, geometry.weights1Buffer); } } } if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; lastVertexSourceId = drawItem.vertexSourceId; lastSkinned = drawItem.skinned; lastSkinned8 = drawItem.skinned8; } if (drawItem.skinned) { const skin = mesh.skin; if (skin) { skin.ensureGpuResources(this.device, this.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.bindMatrixPtr ); this.queue.writeBuffer(skin.boneBuffer, 0, bytes, jointMatPtr, jointCount * 64); pass.setBindGroup(2, skin.bindGroup); } } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = mesh.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount); else pass.draw(geometry.vertexCount); continue; } if ("field" in item) { const drawItem = item; const field = drawItem.field; const geometry = drawItem.geometry; if (!field.visible) continue; if (field.instanceCount <= 0) continue; this.ensureGlyphFieldBindGroup(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; lastGlyph = null; } if (geometry !== lastGeometry) { geometry.upload(this.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; lastMaterial = null; lastNodeLink = null; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = field.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); 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; this.ensurePointCloudBindGroup(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; lastGlyph = null; lastNodeLink = null; } if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; lastGlyph = null; lastMaterial = null; lastNodeLink = null; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = cloud.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); pass.draw(6, cloud.pointCount); continue; } if ("link" in item) { const drawItem = item; const link = drawItem.link; this.ensureNodeLinkBindGroup(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; lastGlyph = null; lastNodeLink = null; } if (drawItem.geometry && drawItem.geometry !== lastGeometry) { drawItem.geometry.upload(this.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; lastGlyph = null; lastMaterial = null; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const modelSlot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[modelSlot]; const globalBindGroup = this.globalBindGroups[modelSlot]; const modelPtr = link.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); pass.setBindGroup(0, globalBindGroup); 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; } } } executeMeshPickDrawList(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 = this.getOrCreatePickMeshPipeline(item.material, item.skinned, item.skinned8, item.mirrored); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastVertexSourceId = -1; lastSkinned = false; lastSkinned8 = false; } if (geometry !== lastGeometry || item.vertexSourceId !== lastVertexSourceId || item.skinned !== lastSkinned || item.skinned8 !== lastSkinned8) { geometry.upload(this.device); const buffers = getMeshVertexBuffers(mesh, this.device, this.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; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const slot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[slot]; const globalBindGroup = this.globalBindGroups[slot]; const modelPtr = mesh.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); this.writePickUniform(slot, this.getObjectId(mesh), 0); pass.setBindGroup(0, globalBindGroup); pass.setBindGroup(1, this.pickBindGroups[slot]); if (item.skinned) { const skin = mesh.skin; if (skin) { skin.ensureGpuResources(this.device, this.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.bindMatrixPtr ); this.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); } } executePointCloudPickDrawList(pass, items) { const bytes = driver.bytes(); 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; this.ensurePointCloudBindGroup(cloud); if (!cloud.bindGroup) continue; const pipeline = this.getOrCreatePickPointCloudPipeline(); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastCloud = null; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const slot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[slot]; const globalBindGroup = this.globalBindGroups[slot]; const modelPtr = cloud.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); this.writePickUniform(slot, this.getObjectId(cloud), 0); pass.setBindGroup(0, globalBindGroup); if (cloud !== lastCloud) { pass.setBindGroup(1, cloud.bindGroup); lastCloud = cloud; } pass.setBindGroup(2, this.pickBindGroups[slot]); pass.draw(6, cloud.pointCount); } } executeGlyphPickDrawList(pass, items) { const bytes = driver.bytes(); 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; this.ensureGlyphFieldBindGroup(field); if (!field.bindGroup) continue; const pipeline = this.getOrCreatePickGlyphFieldPipeline(field); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastField = null; } if (geometry !== lastGeometry) { geometry.upload(this.device); pass.setVertexBuffer(0, geometry.positionBuffer); if (geometry.isIndexed) pass.setIndexBuffer(geometry.indexBuffer, "uint32"); lastGeometry = geometry; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const slot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[slot]; const globalBindGroup = this.globalBindGroups[slot]; const modelPtr = field.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); this.writePickUniform(slot, this.getObjectId(field), 0); pass.setBindGroup(0, globalBindGroup); if (field !== lastField) { pass.setBindGroup(1, field.bindGroup); lastField = field; } pass.setBindGroup(2, this.pickBindGroups[slot]); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, field.instanceCount); else pass.draw(geometry.vertexCount, field.instanceCount); } } executeNodeLinkPickDrawList(pass, items) { const bytes = driver.bytes(); 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; this.ensureNodeLinkBindGroup(link); if (!link.bindGroup) continue; const pipeline = this.getOrCreatePickNodeLinkPipeline(item.passKind, link); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = pipeline; lastGeometry = null; lastLink = null; } if (item.geometry && item.geometry !== lastGeometry) { item.geometry.upload(this.device); pass.setVertexBuffer(0, item.geometry.positionBuffer); if (item.geometry.isIndexed) pass.setIndexBuffer(item.geometry.indexBuffer, "uint32"); lastGeometry = item.geometry; } if (this.modelBufferIndex >= this.modelUniformBuffers.length) this.ensureModelBufferPool(this.modelBufferIndex + 1); const slot = this.modelBufferIndex++; const modelBuffer = this.modelUniformBuffers[slot]; const globalBindGroup = this.globalBindGroups[slot]; const modelPtr = link.transform.worldMatrixPtr; const invPtr = this.modelUniformStagingPtr; const normalPtr = this.modelUniformStagingPtr + 16 * 4; mat4f.invert(invPtr, modelPtr); mat4f.transpose(normalPtr, invPtr); this.queue.writeBuffer(modelBuffer, 0, bytes, modelPtr, 16 * 4); this.queue.writeBuffer(modelBuffer, 16 * 4, bytes, normalPtr, 16 * 4); const elementBase = item.passKind === "edge-lines" || item.passKind === "edge-cylinders" ? link.nodeCount : 0; this.writePickUniform(slot, this.getObjectId(link), elementBase); pass.setBindGroup(0, globalBindGroup); if (link !== lastLink) { pass.setBindGroup(1, link.bindGroup); lastLink = link; } pass.setBindGroup(2, this.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); } } } drawInstancedRun(pass, geometry, material, 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 * this.INSTANCE_STRIDE_BYTES; const dstOffset = this.instanceBufferOffset; const dstEnd = dstOffset + outBytes; this.ensureInstanceBuffer(dstEnd); const bytes = driver.bytes(); this.queue.writeBuffer(this.instanceBuffer, dstOffset, bytes, outPtr, outBytes); pass.setBindGroup(0, this.globalBindGroups[0]); if (material instanceof StandardMaterial) { pass.setVertexBuffer(4, geometry.tangentBuffer); pass.setVertexBuffer(5, this.instanceBuffer, dstOffset, outBytes); } else pass.setVertexBuffer(4, this.instanceBuffer, dstOffset, outBytes); if (geometry.isIndexed) pass.drawIndexed(geometry.indexCount, count); else pass.draw(geometry.vertexCount, count); this.instanceBufferOffset = dstEnd; } getOcclusionReduceBindGroupLayout() { if (this.occlusionReduceBindGroupLayout) return this.occlusionReduceBindGroupLayout; this.occlusionReduceBindGroupLayout = this.device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float", viewDimension: "2d" } }] }); return this.occlusionReduceBindGroupLayout; } getOrCreateOcclusionReducePipeline() { if (this.occlusionReducePipeline) return this.occlusionReducePipeline; let shaderModule = this.shaderCache.get(occlusion_reduce_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: occlusion_reduce_default }); this.shaderCache.set(occlusion_reduce_default, shaderModule); } this.occlusionReducePipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.getOcclusionReduceBindGroupLayout()] }), vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format: "r32float" }] }, primitive: { topology: "triangle-list", cullMode: "none" } }); return this.occlusionReducePipeline; } getOrCreateOcclusionReduceBindGroup(srcMip) { if (!this.occlusionHierarchyTexture) throw new Error("Renderer: occlusion hierarchy texture is not initialized."); const key = `occlusion-reduce:${this.getObjectId(this.occlusionHierarchyTexture)}:${srcMip}`; const cached = this.occlusionReduceBindGroups.get(key); if (cached) return cached; const bindGroup = this.device.createBindGroup({ layout: this.getOcclusionReduceBindGroupLayout(), entries: [{ binding: 0, resource: this.occlusionHierarchyMipViews[srcMip] }] }); this.occlusionReduceBindGroups.set(key, bindGroup); return bindGroup; } getOrCreateOcclusionMeshPipeline(item) { const cullMode = this.getCullMode(item.material.cullMode); const key = `occlusion:mesh:${cullMode}:${item.mirrored ? "cw" : "ccw"}`; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(occlusion_mesh_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: occlusion_mesh_default }); this.shaderCache.set(occlusion_mesh_default, shaderModule); } const pipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreateOcclusionPointCloudPipeline() { const key = "occlusion:pointcloud"; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(occlusion_pointcloud_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: occlusion_pointcloud_default }); this.shaderCache.set(occlusion_pointcloud_default, shaderModule); } const pipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getPointCloudBindGroupLayout()] }), 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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreateOcclusionGlyphFieldPipeline(field) { const cullMode = this.getCullMode(field.cullMode); const key = `occlusion:glyphfield:${cullMode}`; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(occlusion_glyphfield_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: occlusion_glyphfield_default }); this.shaderCache.set(occlusion_glyphfield_default, shaderModule); } const pipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getGlyphFieldBindGroupLayout()] }), 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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreateOcclusionNodeLinkPipeline(link, passKind) { const key = `occlusion:nodelink:${passKind}:${link.cullMode}`; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(occlusion_nodelink_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: occlusion_nodelink_default }); this.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 = this.getCullMode(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 = this.getCullMode(link.cullMode); } const pipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getNodeLinkBindGroupLayout()] }), 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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreatePipeline(material, instanced = false, skinned = false, skinned8 = false, mirrored = false, forceNoDepthWrite = false) { if (instanced && skinned) throw new Error("Renderer: instanced + skinned pipelines are not supported (attribute layout conflict)."); if (skinned8 && !skinned) skinned = true; const key = this.getPipelineCacheKey(material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite); let pipeline = this.pipelineCache.get(key); if (pipeline) return pipeline; const transmissionShader = material instanceof StandardMaterial && material.usesTransmissionLayout(); const shaderCode = material.getShaderCode({ instanced, skinned, skinned8, transmission: transmissionShader }); let shaderModule = this.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: shaderCode }); this.shaderCache.set(shaderCode, shaderModule); } const materialBindGroupLayout = material.createBindGroupLayout(this.device); const bindGroupLayouts = [this.globalBindGroupLayout, materialBindGroupLayout]; if (skinned) bindGroupLayouts.push(this.skinBindGroupLayout); const pipelineLayout = this.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: this.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: this.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: 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: 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: 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: 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" }] } ]; } 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" }] } ]; } pipeline = this.device.createRenderPipeline({ layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [ { format: this.format, blend: this.getBlendState(material.blendMode) } ] }, primitive: { topology: "triangle-list", cullMode: this.getCullMode(material.cullMode), frontFace: mirrored ? "cw" : "ccw" }, depthStencil: { format: "depth24plus", depthWriteEnabled: forceNoDepthWrite ? false : material.depthWrite, depthCompare: material.depthTest ? "less" : "always" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreatePickMeshPipeline(material, skinned, skinned8, mirrored = false) { if (skinned8 && !skinned) skinned = true; const cullMode = this.getCullMode(material.cullMode); const key = `pick:mesh:${cullMode}:${mirrored ? "cw" : "ccw"}:${skinned8 ? "skin8" : skinned ? "skin4" : "noskin"}`; const cached = this.pipelineCache.get(key); if (cached) return cached; const shaderCode = skinned8 ? picking_mesh_skinned8_default : skinned ? picking_mesh_skinned_default : picking_mesh_default; let shaderModule = this.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: shaderCode }); this.shaderCache.set(shaderCode, shaderModule); } const bindGroupLayouts = [this.globalBindGroupLayout, this.getPickBindGroupLayout()]; if (skinned) bindGroupLayouts.push(this.skinBindGroupLayout); const pipelineLayout = this.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 = this.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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreatePickPointCloudPipeline() { const key = "pick:pointcloud"; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(picking_pointcloud_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: picking_pointcloud_default }); this.shaderCache.set(picking_pointcloud_default, shaderModule); } const pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getPointCloudBindGroupLayout(), this.getPickBindGroupLayout()] }); const pipeline = this.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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreatePickGlyphFieldPipeline(field) { const cullMode = this.getCullMode(field.cullMode); const key = `pick:glyphfield:${cullMode}`; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(picking_glyphfield_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: picking_glyphfield_default }); this.shaderCache.set(picking_glyphfield_default, shaderModule); } const pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getGlyphFieldBindGroupLayout(), this.getPickBindGroupLayout()] }); const pipeline = this.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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getOrCreatePickNodeLinkPipeline(passKind, link) { const cullMode = passKind === "node-solid" || passKind === "edge-cylinders" ? this.getCullMode(link.cullMode) : "none"; const key = `pick:nodelink:${passKind}:${cullMode}`; const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(picking_nodelink_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: picking_nodelink_default }); this.shaderCache.set(picking_nodelink_default, shaderModule); } const layout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getNodeLinkBindGroupLayout(), this.getPickBindGroupLayout()] }); 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 = this.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" } }); this.pipelineCache.set(key, pipeline); return pipeline; } getPipelineCacheKey(material, instanced, skinned, skinned8, mirrored, forceNoDepthWrite = false) { const ctorId = this.getObjectId(material.constructor); const isBuiltin = material.constructor === UnlitMaterial || material.constructor === StandardMaterial || material.constructor === DataMaterial; const matKey = isBuiltin ? `${ctorId}` : `${ctorId}_${this.getObjectId(material)}`; const depthWriteKey = forceNoDepthWrite ? "no-depth-write" : material.depthWrite ? "depth-write" : "no-depth-write"; const transmissionShader = material instanceof StandardMaterial && material.usesTransmissionLayout(); return `${matKey}_${material.blendMode}_${material.cullMode}_${depthWriteKey}_${material.depthTest}_${transmissionShader ? "transmission" : "standard"}_${mirrored ? "cw" : "ccw"}_${instanced ? "inst" : "mesh"}_${skinned8 ? "skin8" : skinned ? "skin4" : "noskin"}`; } isMirroredWorldMatrix(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; } getBlendState(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" } }; } } getCullMode(mode) { switch (mode) { case "none" /* None */: return "none"; case "back" /* Back */: return "back"; case "front" /* Front */: return "front"; } } getMaterialBindGroupKey(material) { if (material instanceof UnlitMaterial) { const bc = material.baseColorTexture; return `unlit:${bc?.id ?? 0}:${bc?.revision ?? 0}`; } if (material instanceof StandardMaterial) { const bc = material.baseColorTexture; const mr = material.metallicRoughnessTexture; const n = material.normalTexture; const o = material.occlusionTexture; const e = material.emissiveTexture; const extensions = material.extensions; const cc = extensions.clearcoat; const sp = extensions.specular; const sh = extensions.sheen; const ir = extensions.iridescence; const an = extensions.anisotropy; const tr = extensions.transmission; const vol = extensions.volume; const dt = extensions.diffuseTransmission; const ccTex = cc?.texture ?? null; const ccRough = cc?.roughnessTexture ?? null; const ccNormal = cc?.normalTexture ?? null; const spTex = sp?.texture ?? null; const spColor = sp?.colorTexture ?? null; const shColor = sh?.colorTexture ?? null; const shRough = sh?.roughnessTexture ?? null; const irTex = ir?.texture ?? null; const irThick = ir?.thicknessTexture ?? null; const anTex = an?.texture ?? null; const trTex = tr?.texture ?? null; const thicknessTex = vol?.thicknessTexture ?? null; const dtTex = dt?.texture ?? null; const dtColor = dt?.colorTexture ?? null; return `standard:${bc?.id ?? 0}:${bc?.revision ?? 0}:${mr?.id ?? 0}:${mr?.revision ?? 0}:${n?.id ?? 0}:${n?.revision ?? 0}:${o?.id ?? 0}:${o?.revision ?? 0}:${e?.id ?? 0}:${e?.revision ?? 0}:${ccTex?.id ?? 0}:${ccTex?.revision ?? 0}:${ccRough?.id ?? 0}:${ccRough?.revision ?? 0}:${ccNormal?.id ?? 0}:${ccNormal?.revision ?? 0}:${spTex?.id ?? 0}:${spTex?.revision ?? 0}:${spColor?.id ?? 0}:${spColor?.revision ?? 0}:${shColor?.id ?? 0}:${shColor?.revision ?? 0}:${shRough?.id ?? 0}:${shRough?.revision ?? 0}:${irTex?.id ?? 0}:${irTex?.revision ?? 0}:${irThick?.id ?? 0}:${irThick?.revision ?? 0}:${anTex?.id ?? 0}:${anTex?.revision ?? 0}:${trTex?.id ?? 0}:${trTex?.revision ?? 0}:${thicknessTex?.id ?? 0}:${thicknessTex?.revision ?? 0}:${dtTex?.id ?? 0}:${dtTex?.revision ?? 0}:${dtColor?.id ?? 0}:${dtColor?.revision ?? 0}:${this.transmissionSourceRevision}`; } if (material instanceof DataMaterial) { const bufId = material.dataBuffer ? this.getObjectId(material.dataBuffer) : 0; return `data:${bufId}:${material.getColormapKey()}`; } return "custom"; } ensureMaterialBindGroup(material) { if (!material.uniformBuffer) { material.uniformBuffer = this.device.createBuffer({ size: material.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); } if (material.dirty) { const data = material.getUniformData(); this.queue.writeBuffer(material.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); material.markClean(); } if (material instanceof DataMaterial) { material.upload(this.device, this.queue); } const key = this.getMaterialBindGroupKey(material); if (material.bindGroup && material.bindGroupKey === key) return; const layout = material.createBindGroupLayout(this.device); if (material instanceof UnlitMaterial) { const tex = material.baseColorTexture; const sampler = tex ? tex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler; const view = tex ? tex.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb; material.bindGroup = this.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 bc = material.baseColorTexture; const mr = material.metallicRoughnessTexture; const n = material.normalTexture; const o = material.occlusionTexture; const e = material.emissiveTexture; const extensions = material.extensions; const cc = extensions.clearcoat; const sp = extensions.specular; const sh = extensions.sheen; const ir = extensions.iridescence; const an = extensions.anisotropy; const tr = extensions.transmission; const vol = extensions.volume; const dt = extensions.diffuseTransmission; const ccTex = cc?.texture ?? null; const ccRough = cc?.roughnessTexture ?? null; const ccNormal = cc?.normalTexture ?? null; const spTex = sp?.texture ?? null; const spColor = sp?.colorTexture ?? null; const shColor = sh?.colorTexture ?? null; const shRough = sh?.roughnessTexture ?? null; const irTex = ir?.texture ?? null; const irThick = ir?.thicknessTexture ?? null; const anTex = an?.texture ?? null; const trTex = tr?.texture ?? null; const thicknessTex = vol?.thicknessTexture ?? null; const dtTex = dt?.texture ?? null; const dtColor = dt?.colorTexture ?? null; const entries = [ { binding: 0, resource: { buffer: material.uniformBuffer } }, { binding: 1, resource: bc ? bc.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 2, resource: bc ? bc.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb }, { binding: 3, resource: mr ? mr.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 4, resource: mr ? mr.getView(this.device, this.queue, "linear", this.fallbackMRViewLinear) : this.fallbackMRViewLinear }, { binding: 5, resource: n ? n.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 6, resource: n ? n.getView(this.device, this.queue, "linear", this.fallbackNormalViewLinear) : this.fallbackNormalViewLinear }, { binding: 7, resource: o ? o.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 8, resource: o ? o.getView(this.device, this.queue, "linear", this.fallbackOcclusionViewLinear) : this.fallbackOcclusionViewLinear }, { binding: 9, resource: e ? e.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 10, resource: e ? e.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb }, { binding: 11, resource: ccTex ? ccTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 12, resource: ccTex ? ccTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 13, resource: ccRough ? ccRough.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 14, resource: ccRough ? ccRough.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 15, resource: ccNormal ? ccNormal.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 16, resource: ccNormal ? ccNormal.getView(this.device, this.queue, "linear", this.fallbackNormalViewLinear) : this.fallbackNormalViewLinear }, { binding: 17, resource: spTex ? spTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 18, resource: spTex ? spTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 19, resource: spColor ? spColor.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 20, resource: spColor ? spColor.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb } ]; if (material.usesTransmissionLayout()) { entries.push( { binding: 21, resource: trTex ? trTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 22, resource: trTex ? trTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 23, resource: thicknessTex ? thicknessTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 24, resource: thicknessTex ? thicknessTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 25, resource: dtTex ? dtTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 26, resource: dtTex ? dtTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 27, resource: dtColor ? dtColor.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 28, resource: dtColor ? dtColor.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb }, { binding: 29, resource: this.fallbackSampler }, { binding: 30, resource: this.transmissionSourceView ?? this.fallbackWhiteViewLinear } ); } else { entries.push( { binding: 21, resource: shColor ? shColor.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 22, resource: shColor ? shColor.getView(this.device, this.queue, "srgb", this.fallbackWhiteViewSrgb) : this.fallbackWhiteViewSrgb }, { binding: 23, resource: shRough ? shRough.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 24, resource: shRough ? shRough.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 25, resource: irTex ? irTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 26, resource: irTex ? irTex.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 27, resource: irThick ? irThick.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 28, resource: irThick ? irThick.getView(this.device, this.queue, "linear", this.fallbackWhiteViewLinear) : this.fallbackWhiteViewLinear }, { binding: 29, resource: anTex ? anTex.getSampler(this.device, this.fallbackSampler) : this.fallbackSampler }, { binding: 30, resource: anTex ? anTex.getView(this.device, this.queue, "linear", this.fallbackAnisotropyViewLinear) : this.fallbackAnisotropyViewLinear } ); } material.bindGroup = this.device.createBindGroup({ layout, entries }); material.bindGroupKey = key; return; } if (material instanceof DataMaterial) { if (!this.dataMaterialDummyDataBuffer) { this.dataMaterialDummyDataBuffer = this.device.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); this.queue.writeBuffer(this.dataMaterialDummyDataBuffer, 0, new Uint8Array(4)); } const dataBuffer = material.dataBuffer ?? this.dataMaterialDummyDataBuffer; const cmap = material.getColormapForBinding().getGPUResources(this.device, this.queue); material.bindGroup = this.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 = this.device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer: material.uniformBuffer } }] }); material.bindGroupKey = key; } materialSupportsInstancing(material) { return material instanceof UnlitMaterial || material instanceof StandardMaterial; } materialSupportsSkinning(material) { return material instanceof UnlitMaterial || material instanceof StandardMaterial; } ensureInstanceBuffer(byteLength) { if (this.instanceBuffer && this.instanceBufferCapacityBytes >= byteLength) return; this.instanceBuffer?.destroy(); let cap = this.instanceBufferCapacityBytes || this.INSTANCE_STRIDE_BYTES * 256; while (cap < byteLength) cap *= 2; this.instanceBufferCapacityBytes = cap; this.instanceBuffer = this.device.createBuffer({ size: cap, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); } getPointCloudBindGroupLayout() { if (this.pointCloudBindGroupLayout) return this.pointCloudBindGroupLayout; this.pointCloudBindGroupLayout = this.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 this.pointCloudBindGroupLayout; } getPointCloudPipelineCacheKey(cloud) { return ["pointcloud", `blend=${cloud.blendMode}`, `depthTest=${cloud.depthTest ? 1 : 0}`, `depthWrite=${cloud.depthWrite ? 1 : 0}`, `fmt=${this.format}`].join("|"); } getOrCreatePointCloudPipeline(cloud) { const key = this.getPointCloudPipelineCacheKey(cloud); const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(pointcloud_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: pointcloud_default }); this.shaderCache.set(pointcloud_default, shaderModule); } const bindGroupLayout = this.getPointCloudBindGroupLayout(); const pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, bindGroupLayout] }); const blend = this.getBlendState(cloud.blendMode); const pipeline = this.device.createRenderPipeline({ label: key, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vs_main", buffers: [] }, fragment: { module: shaderModule, entryPoint: "fs_main", targets: [ { format: this.format, blend } ] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: cloud.depthTest ? { format: "depth24plus", depthWriteEnabled: cloud.depthWrite, depthCompare: "less" } : void 0 }); this.pipelineCache.set(key, pipeline); return pipeline; } getPointCloudBindGroupKey(cloud) { const points = cloud.pointsBuffer; const colors = cloud.colorsBuffer; const uniforms = cloud.uniformBuffer; return `pointcloud:${points ? this.getObjectId(points) : 0}:${colors ? this.getObjectId(colors) : 0}:${uniforms ? this.getObjectId(uniforms) : 0}:${cloud.getColormapKey()}`; } ensurePointCloudBindGroup(cloud) { cloud.upload(this.device, this.queue); if (!cloud.pointsBuffer) return; if (cloud.pointCount <= 0) return; if (!cloud.uniformBuffer) { cloud.uniformBuffer = this.device.createBuffer({ size: cloud.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); cloud.bindGroupKey = null; } if (cloud.dirtyUniforms) { const data = cloud.getUniformData(); this.queue.writeBuffer(cloud.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); cloud.markUniformsClean(); } if (!this.pointCloudDummyColorsBuffer) { this.pointCloudDummyColorsBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const white = new Float32Array([1, 1, 1, 1]); this.queue.writeBuffer(this.pointCloudDummyColorsBuffer, 0, white.buffer, white.byteOffset, white.byteLength); } const key = this.getPointCloudBindGroupKey(cloud); if (cloud.bindGroup && cloud.bindGroupKey === key) return; const layout = this.getPointCloudBindGroupLayout(); const cmapGPU = cloud.getColormapForBinding().getGPUResources(this.device, this.queue); cloud.bindGroup = this.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 ?? this.pointCloudDummyColorsBuffer } } ] }); cloud.bindGroupKey = key; } getGlyphFieldBindGroupLayout() { if (this.glyphFieldBindGroupLayout) return this.glyphFieldBindGroupLayout; this.glyphFieldBindGroupLayout = this.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 this.glyphFieldBindGroupLayout; } getOrCreateGlyphFieldPipeline(field) { const key = `glyphfield:${this.format}:${field.blendMode}:${field.depthWrite ? 1 : 0}:${field.depthTest ? 1 : 0}:${field.cullMode}`; const cached = this.pipelineCache.get(key); if (cached) return cached; const shaderCode = glyphfield_default; let shaderModule = this.shaderCache.get(shaderCode); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: shaderCode }); this.shaderCache.set(shaderCode, shaderModule); } const layout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getGlyphFieldBindGroupLayout()] }); const pipeline = this.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: this.format, blend: this.getBlendState(field.blendMode) }] }, primitive: { topology: "triangle-list", cullMode: this.getCullMode(field.cullMode) }, depthStencil: field.depthTest || field.depthWrite ? { format: "depth24plus", depthWriteEnabled: field.depthWrite, depthCompare: field.depthTest ? "less" : "always" } : void 0 }); this.pipelineCache.set(key, pipeline); return pipeline; } getGlyphFieldBindGroupKey(field) { const p = field.positionsBuffer; const r = field.rotationsBuffer; const s = field.scalesBuffer; const a = field.attributesBuffer; const u = field.uniformBuffer; return `glyphfield:${p ? this.getObjectId(p) : 0}:${r ? this.getObjectId(r) : 0}:${s ? this.getObjectId(s) : 0}:${a ? this.getObjectId(a) : 0}:${u ? this.getObjectId(u) : 0}:${field.getColormapKey()}`; } ensureGlyphFieldBindGroup(field) { field.upload(this.device, this.queue); if (!field.positionsBuffer) return; if (!field.rotationsBuffer) return; if (!field.scalesBuffer) return; if (field.instanceCount <= 0) return; if (!field.uniformBuffer) { field.uniformBuffer = this.device.createBuffer({ size: field.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); field.bindGroupKey = null; } if (field.dirtyUniforms) { const data = field.getUniformData(); this.queue.writeBuffer(field.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); field.markUniformsClean(); } if (!field.attributesBuffer) { if (!this.glyphFieldDummyAttributesBuffer) { this.glyphFieldDummyAttributesBuffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); } field.attributesBuffer = this.glyphFieldDummyAttributesBuffer; field.bindGroupKey = null; } const key = this.getGlyphFieldBindGroupKey(field); if (field.bindGroup && field.bindGroupKey === key) return; const layout = this.getGlyphFieldBindGroupLayout(); const cmapGPU = field.getColormapForBinding().getGPUResources(this.device, this.queue); field.bindGroup = this.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; } getNodeLinkBindGroupLayout() { if (this.nodeLinkBindGroupLayout) return this.nodeLinkBindGroupLayout; this.nodeLinkBindGroupLayout = this.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 this.nodeLinkBindGroupLayout; } getNodeLinkPipelineCacheKey(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=${this.format}`].join("|"); } getOrCreateNodeLinkPipeline(link, passKind) { const key = this.getNodeLinkPipelineCacheKey(link, passKind); const cached = this.pipelineCache.get(key); if (cached) return cached; let shaderModule = this.shaderCache.get(nodelink_default); if (!shaderModule) { shaderModule = this.device.createShaderModule({ code: nodelink_default }); this.shaderCache.set(nodelink_default, shaderModule); } const layout = this.device.createPipelineLayout({ bindGroupLayouts: [this.globalBindGroupLayout, this.getNodeLinkBindGroupLayout()] }); 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 = this.getCullMode(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 = this.getCullMode(link.cullMode); } const pipeline = this.device.createRenderPipeline({ label: key, layout, vertex: { module: shaderModule, entryPoint: vertexEntry, buffers }, fragment: { module: shaderModule, entryPoint: fragmentEntry, targets: [{ format: this.format, blend: this.getBlendState(link.blendMode) }] }, primitive: { topology, cullMode }, depthStencil: link.depthTest || link.depthWrite ? { format: "depth24plus", depthWriteEnabled: link.depthWrite, depthCompare: link.depthTest ? "less" : "always" } : void 0 }); this.pipelineCache.set(key, pipeline); return pipeline; } getNodeLinkBindGroupKey(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 ? this.getObjectId(np) : 0}:${ns ? this.getObjectId(ns) : 0}:${nc ? this.getObjectId(nc) : 0}:${nr ? this.getObjectId(nr) : 0}:${ep ? this.getObjectId(ep) : 0}:${es ? this.getObjectId(es) : 0}:${ec ? this.getObjectId(ec) : 0}:${u ? this.getObjectId(u) : 0}:${link.getNodeColormapKey()}:${link.getEdgeColormapKey()}`; } ensureNodeLinkBindGroup(link) { link.upload(this.device, this.queue); if (!link.nodePositionsBuffer) return; if (!link.uniformBuffer) { link.uniformBuffer = this.device.createBuffer({ size: link.getUniformBufferSize(), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); link.bindGroupKey = null; } if (link.dirtyUniforms) { const data = link.getUniformData(); this.queue.writeBuffer(link.uniformBuffer, 0, data.buffer, data.byteOffset, data.byteLength); link.markUniformsClean(); } if (!this.nodeLinkDummyF32Buffer) this.nodeLinkDummyF32Buffer = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); if (!this.nodeLinkDummyU32Buffer) this.nodeLinkDummyU32Buffer = this.device.createBuffer({ size: 8, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const key = this.getNodeLinkBindGroupKey(link); if (link.bindGroup && link.bindGroupKey === key) return; const nodeCmap = link.getNodeColormapForBinding().getGPUResources(this.device, this.queue); const edgeCmap = link.getEdgeColormapForBinding().getGPUResources(this.device, this.queue); link.bindGroup = this.device.createBindGroup({ layout: this.getNodeLinkBindGroupLayout(), entries: [ { binding: 0, resource: { buffer: link.nodePositionsBuffer ?? this.nodeLinkDummyF32Buffer } }, { binding: 1, resource: { buffer: link.nodeScalarsBuffer ?? this.nodeLinkDummyF32Buffer } }, { binding: 2, resource: { buffer: link.nodeColorsBuffer ?? this.nodeLinkDummyF32Buffer } }, { binding: 3, resource: { buffer: link.nodeRadiiBuffer ?? this.nodeLinkDummyF32Buffer } }, { binding: 4, resource: { buffer: link.edgesBuffer ?? this.nodeLinkDummyU32Buffer } }, { binding: 5, resource: { buffer: link.edgeScalarsBuffer ?? this.nodeLinkDummyF32Buffer } }, { binding: 6, resource: { buffer: link.edgeColorsBuffer ?? this.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; } }; // src/core/stats.ts var RollingAverage = class { constructor(capacity) { this.capacity = capacity; this.values = new Float64Array(Math.max(1, capacity | 0)); } 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"); } }; // src/compute/pipeline.ts var storageBufferLayout = (opts) => { 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) => { return { binding: opts.binding, visibility: opts.visibility ?? GPUShaderStage.COMPUTE, buffer: { type: "uniform", hasDynamicOffset: opts.hasDynamicOffset ?? false, minBindingSize: opts.minBindingSize } }; }; var resolveBufferBinding = (resource) => { if (isGPUBuffer(resource)) return { buffer: resource }; if (resource.buffer !== void 0 && resource.device !== void 0) { return { buffer: resolveGPUBuffer(resource) }; } const bb = resource; return { buffer: resolveGPUBuffer(bb.buffer), offset: bb.offset, size: bb.size }; }; var resourcesToEntries = (resources) => { const entries = []; if (Array.isArray(resources)) { for (const e of resources) { entries.push({ binding: e.binding, resource: resolveBufferBinding(e.resource) }); } return entries; } const keys = Object.keys(resources).map((k) => Number(k)).filter((n) => Number.isFinite(n)); keys.sort((a, b) => a - b); for (const binding of keys) { const resource = resources[binding]; if (!resource) continue; entries.push({ binding, resource: resolveBufferBinding(resource) }); } return entries; }; 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((g) => device.createBindGroupLayout({ label: g.label, entries: g.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 = resourcesToEntries(resources); return this.device.createBindGroup({ label, layout, entries }); } }; // src/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]; }; // src/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) => { const { x, y, z } = normalizeWorkgroups(workgroups); const max = device.limits.maxComputeWorkgroupsPerDimension; assert(x <= max && y <= max && z <= max, `dispatchWorkgroups exceeds device.limits.maxComputeWorkgroupsPerDimension (${max})`); }; var resolvePipeline = (p) => { return 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 { x, y, z } = normalizeWorkgroups(cmd.workgroups); if (x > 0 && y > 0 && z > 0) pass.dispatchWorkgroups(x, y, z); pass.end(); }; var encodeDispatchBatch = (encoder, commands, label) => { const pass = encoder.beginComputePass({ label }); let lastPipeline = null; for (const cmd of commands) { const pipeline = resolvePipeline(cmd.pipeline); if (pipeline !== lastPipeline) { pass.setPipeline(pipeline); lastPipeline = 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 { x, y, z } = normalizeWorkgroups(cmd.workgroups); 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(); }; // src/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(); } }; // src/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]; } }"; // src/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]; } }"; // src/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]; } }"; // src/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]; } }"; // src/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]; } }"; // src/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]; } }"; // src/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 isNan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalidPair() -> Pair { return Pair(-0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let aNan = isNan(a.value); let bNan = isNan(b.value); if (aNan && bNan) { if (a.index <= b.index) { return a; } return b; } if (aNan) { return b; } if (bNan) { 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 = invalidPair(); var b = invalidPair(); 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]; } }"; // src/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 isNan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalidPair() -> Pair { return Pair(-0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let aNan = isNan(a.value); let bNan = isNan(b.value); if (aNan && bNan) { if (a.index <= b.index) { return a; } return b; } if (aNan) { return b; } if (bNan) { 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 = invalidPair(); var b = invalidPair(); 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]; } }"; // src/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 isNan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalidPair() -> Pair { return Pair(0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let aNan = isNan(a.value); let bNan = isNan(b.value); if (aNan && bNan) { if (a.index <= b.index) { return a; } return b; } if (aNan) { return b; } if (bNan) { 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 = invalidPair(); var b = invalidPair(); 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]; } }"; // src/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 isNan(val: f32) -> bool { let u = bitcast(val); return (u & 0x7F800000u) == 0x7F800000u && (u & 0x007FFFFFu) != 0u; } fn invalidPair() -> Pair { return Pair(0x1.fffffep+127f, 0xFFFFFFFFu); } fn better(a: Pair, b: Pair) -> Pair { let aNan = isNan(a.value); let bNan = isNan(b.value); if (aNan && bNan) { if (a.index <= b.index) { return a; } return b; } if (aNan) { return b; } if (bNan) { 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 = invalidPair(); var b = invalidPair(); 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]; } }"; // src/wgsl/compute/scan-block-exclusive-u32.wgsl var scan_block_exclusive_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; @group(0) @binding(2) var blockSums: 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 ai = base + tid; let bi = ai + WORKGROUP_SIZE; temp[tid] = select(0u, input[ai], ai < n); temp[tid + WORKGROUP_SIZE] = select(0u, input[bi], bi < n); var offset = 1u; var d = WORKGROUP_SIZE; 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) { blockSums[wid.x] = temp[ELEMENTS_PER_WORKGROUP - 1u]; temp[ELEMENTS_PER_WORKGROUP - 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(); if (ai < n) { output[ai] = temp[tid]; } if (bi < n) { output[bi] = temp[tid + WORKGROUP_SIZE]; } }"; // src/wgsl/compute/scan-add-block-offsets-u32.wgsl var scan_add_block_offsets_u32_default = "const ELEMENTS_PER_WORKGROUP: u32 = 512u; @group(0) @binding(0) var data: array; @group(0) @binding(1) var blockOffsets: 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 = blockOffsets[block]; data[i] = data[i] + off; }"; // src/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); } }"; // src/wgsl/compute/histogram-u32.wgsl var histogram_u32_default = "@group(0) @binding(0) var keys: array; @group(0) @binding(1) var bins: array>; @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); } }"; // src/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; @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]; } }"; // src/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; @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]; } }"; // src/wgsl/compute/sort-radix-flags-u32.wgsl var sort_radix_flags_u32_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keys: array; @group(0) @binding(1) var flags: array; @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 isZero = ((k >> BIT) & 1u) == 0u; flags[i] = select(0u, 1u, isZero); }"; // src/wgsl/compute/sort-radix-scatter-u32.wgsl var sort_radix_scatter_u32_default = "override BIT: u32 = 0u; @group(0) @binding(0) var keysIn: array; @group(0) @binding(1) var prefix: array; @group(0) @binding(2) var zerosCount: array; @group(0) @binding(3) var keysOut: array; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&keysIn); if (i >= n) { return; } let k = keysIn[i]; let isZero = ((k >> BIT) & 1u) == 0u; let zeroPos = prefix[i]; let z = zerosCount[0]; let onePos = z + (i - zeroPos); let dst = select(onePos, zeroPos, isZero); keysOut[dst] = k; }"; // src/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]; } }"; // src/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]; } }"; // src/wgsl/compute/scale-extract-f32.wgsl var scale_extract_f32_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct Params { count: u32, componentCount: u32, componentIndex: u32, valueMode: u32, stride: u32, offset: u32, _pad0: u32, _pad1: u32 }; @group(0) @binding(0) var src: array; @group(0) @binding(1) var outValues: array; @group(0) @binding(2) var outFlags: array; @group(0) @binding(3) var params: Params; @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.componentCount)); let ci = min(3u, params.componentIndex); 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(vec4f(x, y, z, w), cc, ci, params.valueMode); if (scale_is_finite(raw)) { outValues[i] = raw; outFlags[i] = 1u; } else { outValues[i] = 0.0; outFlags[i] = 0u; } }"; // src/wgsl/compute/scale-histogram-f32.wgsl var scale_histogram_f32_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct Params { count: u32, binCount: u32, minValue: f32, maxValue: f32 }; @group(0) @binding(0) var values: array; @group(0) @binding(1) var bins: array>; @group(0) @binding(2) var params: Params; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; if (i >= params.count) { return; } if (params.binCount == 0u) { return; } let minValue = params.minValue; let maxValue = params.maxValue; if (!(maxValue > minValue)) { return; } let v = values[i]; if (!scale_is_finite(v)) { return; } let t = clamp((v - minValue) / (maxValue - minValue), 0.0, 0.99999994); let b = min(params.binCount - 1u, u32(t * f32(params.binCount))); atomicAdd(&bins[b], 1u); }"; // src/wgsl/compute/scale-remap-f32.wgsl var scale_remap_f32_default = "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, modeId: u32, linthresh: f32, base: f32) -> f32 { if (modeId == 0u) { return x; } if (modeId == 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: vec4f, componentCountIn: u32, componentIndexIn: u32, valueMode: u32) -> f32 { let componentCount = max(1u, min(4u, componentCountIn)); let componentIndex = min(3u, componentIndexIn); if (valueMode == 1u) { if (componentCount == 1u) { return abs(v.x); } if (componentCount == 2u) { return length(v.xy); } if (componentCount == 3u) { return length(v.xyz); } return length(v); } if (componentIndex == 0u) { return v.x; } if (componentIndex == 1u) { return v.y; } if (componentIndex == 2u) { return v.z; } return v.w; } fn scale_apply_transform(rawValue: f32, domain: vec4f, clampConfig: vec4f, params: vec4f, flags: vec4f) -> f32 { if (!scale_is_finite(rawValue)) { return 0.0; } var v = rawValue; let clampMode = u32(domain.w + 0.5); let clampMin = clampConfig.x; let clampMax = clampConfig.y; if (clampMode != 0u && clampMax > clampMin) { v = clamp(v, clampMin, clampMax); } var d0 = domain.x; var d1 = domain.y; if (d1 <= d0 && clampMax > clampMin) { d0 = clampMin; d1 = clampMax; } let modeId = 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, modeId, linthresh, base); let b = scale_apply_mode(d1, modeId, linthresh, base); let x = scale_apply_mode(v, modeId, 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); } struct Params { count: u32, _pad0: u32, _pad1: u32, _pad2: u32, domain: vec4f, clampConfig: vec4f, scaleParams: vec4f, scaleFlags: vec4f }; @group(0) @binding(0) var values: array; @group(0) @binding(1) var outValues: array; @group(0) @binding(2) var params: Params; @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)) { outValues[i] = 0.0; return; } outValues[i] = scale_apply_transform(v, params.domain, params.clampConfig, params.scaleParams, params.scaleFlags); }"; // src/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(); } }"; // src/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(); } } }"; // src/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; } }"; // src/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; } }"; // src/wgsl/compute/lu-factor-complex64.wgsl var lu_factor_complex64_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: 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; } @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 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(); } }"; // src/wgsl/compute/lu-factor-lead-complex64.wgsl var lu_factor_lead_complex64_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, } 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; } @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 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(); } } }"; // src/wgsl/compute/lu-factor-upper-complex64.wgsl var lu_factor_upper_complex64_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, } 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); } @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, 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; } }"; // src/wgsl/compute/lu-factor-trailing-complex64.wgsl var lu_factor_trailing_complex64_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, } 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); } @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>; @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; } }"; // src/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 baseRhs = b * n; let baseIpiv = b * n; for (var i = 0u; i < n; i = i + 1u) { x[baseRhs + i] = rhs[baseRhs + i]; } for (var kk = 0u; kk < n - 1u; kk = kk + 1u) { let p = ipiv[baseIpiv + kk]; if (p != kk) { let t = x[baseRhs + kk]; x[baseRhs + kk] = x[baseRhs + p]; x[baseRhs + p] = t; } } for (var i = 0u; i < n; i = i + 1u) { var sum = x[baseRhs + i]; for (var j = 0u; j < i; j = j + 1u) { sum = sum - lu[base + i * n + j] * x[baseRhs + j]; } x[baseRhs + i] = sum; } for (var ii = n; ii > 0u; ii = ii - 1u) { let i = ii - 1u; var sum = x[baseRhs + i]; for (var j = i + 1u; j < n; j = j + 1u) { sum = sum - lu[base + i * n + j] * x[baseRhs + j]; } x[baseRhs + i] = sum / lu[base + i * n + i]; } }"; // src/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 baseRhs = b * n; let baseIpiv = 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[baseRhs + t]; } } } workgroupBarrier(); if (lid == 0u) { for (var k: u32 = 0u; k < n; k = k + 1u) { let p = ipiv[baseIpiv + 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[baseRhs + w] = wg_x[w]; } } } }"; // src/wgsl/compute/lu-solve-large-complex64.wgsl var lu_solve_large_complex64_default = "struct LuBatchedParams { batch_count: u32, n: u32, elems_per_matrix: u32, _pad: 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); } @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 baseRhs = b * n; let baseIpiv = b * n; for (var i = 0u; i < n; i = i + 1u) { x[baseRhs + i] = rhs[baseRhs + i]; } for (var kk = 0u; kk < n - 1u; kk = kk + 1u) { let p = ipiv[baseIpiv + kk]; if (p != kk) { let t = x[baseRhs + kk]; x[baseRhs + kk] = x[baseRhs + p]; x[baseRhs + p] = t; } } for (var i = 0u; i < n; i = i + 1u) { var sum = x[baseRhs + i]; for (var j = 0u; j < i; j = j + 1u) { sum = sum - cx_mul(lu[base + i * n + j], x[baseRhs + j]); } x[baseRhs + i] = sum; } for (var ii = n; ii > 0u; ii = ii - 1u) { let i = ii - 1u; var sum = x[baseRhs + i]; for (var j = i + 1u; j < n; j = j + 1u) { sum = sum - cx_mul(lu[base + i * n + j], x[baseRhs + j]); } x[baseRhs + i] = cx_div(sum, lu[base + i * n + i]); } }"; // src/wgsl/compute/lu-solve-shared-complex64.wgsl var lu_solve_shared_complex64_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, } 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); } @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>; @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 baseRhs = b * n; let baseIpiv = 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[baseRhs + t]; } } } workgroupBarrier(); if (lid == 0u) { for (var k: u32 = 0u; k < n; k = k + 1u) { let p = ipiv[baseIpiv + 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[baseRhs + w] = wg_x[w]; } } } }"; // src/compute/kernels.ts var bytesPerElement = (type) => { return 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; /** Reused by batched LU kernels to avoid create/destroy per dispatch. */ luBatchedParamsBuffer; /** Reused by blocked LU phases (needs kk/pw). */ luBlockedParamsBuffer; 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; } destroy() { this.scratch.destroy(); this.pipelines.clear(); this.luBatchedParamsBuffer?.destroy(); this.luBatchedParamsBuffer = null; this.luBlockedParamsBuffer?.destroy(); this.luBlockedParamsBuffer = 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; } 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(); if (opts?.validateLimits) for (const cmd of commands) validateWorkgroupsForDevice(this.device, cmd.workgroups); encodeDispatchBatch(encoder, commands, opts?.label); 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, 512); 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` }); } } 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() { const key = "kernels:histogram:u32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: histogram_u32_default, entryPoint: "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 }) ] } ] }); }); } 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 }) ] } ] }); }); } 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: true }), storageBufferLayout({ binding: 3, 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() { const key = "kernels:scale:histogramF32"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: scale_histogram_f32_default, entryPoint: "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 }) ] } ] }); }); } 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 }) ] } ] }); }); } getLuFactorComplexLeadPipeline() { const key = "kernels:lu:factorComplexLead"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_lead_complex64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuFactorComplexUpperPipeline() { const key = "kernels:lu:factorComplexUpper"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_upper_complex64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuFactorComplexTrailingPipeline() { const key = "kernels:lu:factorComplexTrailing"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_trailing_complex64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 32 }), storageBufferLayout({ binding: 1, readOnly: false }) ] } ] }); }); } getLuFactorComplexSmallPipeline() { const key = "kernels:lu:factorComplexSmall"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_factor_complex64_default, entryPoint: "main", bindGroups: [ { label: `${key}:bg0`, entries: [ uniformBufferLayout({ binding: 0, minBindingSize: 16 }), storageBufferLayout({ binding: 1, readOnly: false }), storageBufferLayout({ binding: 2, readOnly: false }) ] } ] }); }); } getLuSolveComplexLargePipeline() { const key = "kernels:lu:solveComplexLarge"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_large_complex64_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 }) ] } ] }); }); } getLuSolveComplexPipeline() { const key = "kernels:lu:solveComplex"; return this.getPipeline(key, () => { return new ComputePipeline(this.device, { label: key, code: lu_solve_shared_complex64_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` }); } 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.device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "scale:extract:params" }); this.queue.writeBuffer(params, 0, new Uint32Array([ count >>> 0, componentCount >>> 0, componentIndex >>> 0, scaleValueModeToId(valueMode) >>> 0, stride >>> 0, offset >>> 0, 0, 0 ])); 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); params.destroy(); 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 = []; if (binCount > 0 && (opts.clear ?? true)) { 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" }); } let params = null; if (count > 0 && binCount > 0 && Number.isFinite(opts.minValue) && Number.isFinite(opts.maxValue) && opts.maxValue > opts.minValue) { params = this.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "histogramF32:params" }); const raw = new ArrayBuffer(16); const dv = new DataView(raw); 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 pipelineHist = this.getScaleHistogramF32Pipeline(); 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, 256), label: "histogramF32:accum" }); } this.execute(commands, opts); params?.destroy(); 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 = new Float32Array(20); packScaleTransform(transform, packed, 0); const params = this.device.createBuffer({ size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "scale:remap:params" }); const raw = new ArrayBuffer(80); const dv = new DataView(raw); dv.setUint32(0, count >>> 0, true); const f32 = new Float32Array(raw); 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); params.destroy(); 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 = []; if (binCount > 0 && (opts.clear ?? true)) { 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 pipelineHist = this.getHistogramPipeline(); 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, 256), label: "histogramU32:accum" }); } this.execute(commands, 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`); this.encodeReduceScalar(commands, "u32", "sum", flags, count, countOut, `compact:${type}:count`); { 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) }, `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 flags = this.scratch.acquire(count * 4, "radix:flags"); const prefix = this.scratch.acquire(count * 4, "radix:prefix"); const zerosCount = this.scratch.acquire(4, "radix:zerosCount"); 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++) { { const pipeline = this.getRadixFlagsPipeline(bit); const bg = pipeline.createBindGroup(0, { 0: this.bindSized(inBuf, count * 4), 1: this.bindSized(flags, count * 4) }, `radix:bit${bit}:flags:bg`); commands.push({ pipeline, bindGroups: [bg], workgroups: workgroups1D(count, 256), label: `radix:bit${bit}:flags` }); } this.encodeScanExclusiveU32Into(commands, flags, count, prefix, `radix:bit${bit}:scan`); this.encodeReduceScalar(commands, "u32", "sum", flags, count, zerosCount, `radix:bit${bit}:zerosCount`); { 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(zerosCount, 4), 3: 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; } /** * In-place batched LU factorization with **partial pivoting**. * Matrices are `(batchCount, n, n)` row-major `f32`. On return each block holds compact * `L` (strict lower, unit diagonal implicit) and `U` (upper including diagonal) of **P A** * where row swaps are recorded in `ipiv` (`ipiv[b*n + k]` = row swapped with row `k` * at step `k`, 0-based). For `n < 160` this uses the small-matrix path; larger systems use * the blocked panel/update path. */ 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); } } /** * Batched solve `A x = b` given `lu` and `ipiv` from {@link luFactorF32Batched} for the * same `batchCount` and `n`. `rhs` is `(batchCount, n)` contiguous `f32`; `outX` receives * the solution. For `n <= 512` this uses the shared-memory path; larger systems use the * large-matrix fallback. `lu`, `ipiv`, `rhs`, and `outX` must be pairwise distinct `StorageBuffer` instances. */ 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); } /** * In-place batched **complex64** LU (`interleaved re,im` as **8 bytes** per matrix entry, * row-major `(batchCount, n, n)`). Same pivot convention as {@link luFactorF32Batched}; * `ipiv` is `batchCount * n` **u32** (bytes = `batchCount * n * 4`). */ luFactorComplex64Batched(matrices, ipiv, batchCount, n, opts = {}) { assert(!opts.encoder, "luFactorComplex64Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luFactorComplex64Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luFactorComplex64Batched: n must be an integer >= 0 (got ${n})`); assert(matrices !== ipiv, "luFactorComplex64Batched: matrices and ipiv must be distinct StorageBuffer instances"); if (batchCount === 0 || n === 0) return; const elemsPerMatrix = n * n; assert(Number.isFinite(elemsPerMatrix) && elemsPerMatrix <= 4294967295, "luFactorComplex64Batched: n*n overflow"); const needBytes = batchCount * elemsPerMatrix * 8; const ipivBytes = batchCount * n * 4; assert(matrices.byteLength >= needBytes, `luFactorComplex64Batched: matrices buffer too small (need ${needBytes} bytes, have ${matrices.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luFactorComplex64Batched: 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.getLuFactorComplexSmallPipeline(); const bg = pipeline.createBindGroup(0, { 0: { buffer: params2, size: 16 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorComplex64Batched:bg"); this.execute([{ pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luFactorComplex64Batched" }], opts); return; } const params = this.getLuBlockedParamsBuffer(); const leadPipe = this.getLuFactorComplexLeadPipeline(); const upperPipe = this.getLuFactorComplexUpperPipeline(); const trailingPipe = this.getLuFactorComplexTrailingPipeline(); const bgLead = leadPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes), 2: this.bindSized(ipiv, ipivBytes) }, "luFactorComplex64:lead:bg"); const bgUpper = upperPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorComplex64:upper:bg"); const bgTrailing = trailingPipe.createBindGroup(0, { 0: { buffer: params, size: 32 }, 1: this.bindSized(matrices, needBytes) }, "luFactorComplex64: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: `luFactorComplex64:lead:kk${kk}` } ]; if (trail > 0) { const totalTrsm = batchCount * trail; cmds.push({ pipeline: upperPipe, bindGroups: [bgUpper], workgroups: workgroups1D(totalTrsm, 256), label: `luFactorComplex64: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: `luFactorComplex64:trailing:kk${kk}` }); } this.execute(cmds, opts); } } /** * Batched **complex64** solve `A x = b` using `lu` + `ipiv` from {@link luFactorComplex64Batched}. * `rhs` / `outX` are `(batchCount, n)` with **8 bytes** per component (re, im) per entry. * For `n <= 512`, this picks the shared-memory path; larger systems use the * large-matrix path that does not rely on workgroup-local storage. */ luSolveComplex64Batched(lu, ipiv, rhs, outX, batchCount, n, opts = {}) { assert(!opts.encoder, "luSolveComplex64Batched does not support opts.encoder"); assert(Number.isInteger(batchCount) && batchCount >= 0, `luSolveComplex64Batched: batchCount must be an integer >= 0 (got ${batchCount})`); assert(Number.isInteger(n) && n >= 0, `luSolveComplex64Batched: n must be an integer >= 0 (got ${n})`); assert(lu !== rhs && lu !== outX && lu !== ipiv && rhs !== outX && rhs !== ipiv && outX !== ipiv, "luSolveComplex64Batched: 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, "luSolveComplex64Batched: n*n overflow"); const luBytes = batchCount * elemsPerMatrix * 8; const rhsBytes = batchCount * n * 8; const ipivBytes = batchCount * n * 4; assert(lu.byteLength >= luBytes, `luSolveComplex64Batched: lu buffer too small (need ${luBytes} bytes, have ${lu.byteLength})`); assert(ipiv.byteLength >= ipivBytes, `luSolveComplex64Batched: ipiv buffer too small (need ${ipivBytes} bytes, have ${ipiv.byteLength})`); assert(rhs.byteLength >= rhsBytes, `luSolveComplex64Batched: rhs buffer too small (need ${rhsBytes} bytes, have ${rhs.byteLength})`); assert(outX.byteLength >= rhsBytes, `luSolveComplex64Batched: 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.getLuSolveComplexPipeline() : this.getLuSolveComplexLargePipeline(); 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) }, "luSolveComplex64Batched:bg"); this.execute([ { pipeline, bindGroups: [bg], workgroups: { x: batchCount, y: 1, z: 1 }, label: "luSolveComplex64Batched" } ], opts); } }; // src/wgsl/compute/blitRGBA8.wgsl var blitRGBA8_default = "struct Params { p0 : vec4f, p1 : vec4f } @group(0) @binding(0) var params: Params; @group(0) @binding(1) var pixels: array; fn unpackRGBA8(x: u32) -> vec4f { 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 vec4f(r, g, b, a); } struct VSOut { @builtin(position) position: vec4f } @vertex fn vs_main(@builtin(vertex_index) vid: u32) -> VSOut { var pos = array( vec2f(-1.0, -1.0), vec2f( 3.0, -1.0), vec2f(-1.0, 3.0) ); var out: VSOut; out.position = vec4f(pos[vid], 0.0, 1.0); return out; } @fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { let displayW = max(1.0, params.p0.x); let displayH = max(1.0, params.p0.y); let outW = max(1.0, params.p0.z); let outH = max(1.0, params.p0.w); let flipY = params.p1.x > 0.5; let xOut = clamp(i32(floor(pos.x * outW / displayW)), 0, i32(outW) - 1); var yOut = clamp(i32(floor(pos.y * outH / displayH)), 0, i32(outH) - 1); if (flipY) { yOut = i32(outH) - 1 - yOut; } let idx = u32(yOut) * u32(outW) + u32(xOut); return unpackRGBA8(pixels[idx]); }"; // src/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: blitRGBA8_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; } }; // src/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"); } 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"); } async read(src, srcOffsetBytes = 0, sizeBytes, opts = {}) { 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})`); const alignedSize = Math.max(4, alignTo(size, 4)); assert(alignedSize <= remaining, `Aligned copy size (${alignedSize}) exceeds remaining bytes (${remaining}). copyBufferToBuffer requires multiples of 4.`); 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 = mapped.slice(0, size); slot.buffer.unmap(); return out; } catch (e) { try { slot.buffer.unmap(); } catch { } throw e; } }; const job = slot.tail.then(run, run); slot.tail = job.then(() => { }, () => { }); return job; } 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; } }; // src/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) { if (validateLimits) for (const cmd of commands) validateWorkgroupsForDevice(this.device, cmd.workgroups); encodeDispatchBatch(encoder, commands, label); } 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(); } }; // src/gltf/uri.ts var isDataUri = (uri) => { return uri.startsWith("data:"); }; var decodeDataUri = (uri) => { const m = uri.match(/^data:([^,]*),([\s\S]*)$/); if (!m) throw new Error(`Invalid data URI: ${uri.slice(0, 64)}...`); const meta = m[1] ?? ""; const payload = m[2] ?? ""; const parts = meta.split(";").filter((p) => p.length > 0); let mimeType = null; let isBase64 = false; for (const p of parts) { if (p === "base64") isBase64 = true; else mimeType = p; } if (isBase64) { const binStr = atob(payload); const bytes2 = new Uint8Array(binStr.length); for (let i = 0; i < binStr.length; i++) bytes2[i] = binStr.charCodeAt(i) & 255; return { mimeType, data: bytes2.buffer }; } const decoded = decodeURIComponent(payload); const bytes = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i) & 255; return { mimeType, data: bytes.buffer }; }; var dirnameUrl = (url) => { const idx = url.lastIndexOf("/"); if (idx < 0) return ""; return url.slice(0, idx + 1); }; var resolveUri = (baseUrl, uri) => { if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("blob:")) return uri; if (uri.startsWith("/")) return uri; if (!baseUrl) return uri; return baseUrl + uri; }; // src/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 jsonText = new TextDecoder("utf-8").decode(jsonChunk); const json = JSON.parse(jsonText); return { json, binChunk }; }; // src/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: return 1; } }; 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 copyBytesToWasm = (buffer, byteOffset, byteLength) => { const ptr = wasm.allocBytes(byteLength); const src = new Uint8Array(buffer, byteOffset, byteLength); wasm.u8view(ptr, byteLength).set(src); return ptr; }; var copyBytesFromWasm = (ptr, byteLength) => { 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 = accessor.count | 0; const type = accessor.type; const numComps = gltfNumComponents(type); const normalized = accessor.normalized === true; const elemByteSize = info.bytes * numComps; let base; if (accessor.bufferView === void 0) base = new info.ctor(new ArrayBuffer(count * numComps * info.bytes), 0, count * numComps); else { const bv = getBufferView(json, accessor.bufferView); if (bv.extensions?.["EXT_meshopt_compression"]) throw new Error("EXT_meshopt_compression is not supported yet. Please provide an uncompressed glTF/GLB."); const buffer = doc.buffers[bv.buffer]; if (!buffer) throw new Error(`Missing buffer[${bv.buffer}]`); const bvOffset = (bv.byteOffset ?? 0) | 0; const accOffset = (accessor.byteOffset ?? 0) | 0; const start = bvOffset + accOffset; const byteStride = bv.byteStride ?? elemByteSize; if (byteStride < elemByteSize) throw new Error(`Invalid bufferView.byteStride (${byteStride}) < element byte size (${elemByteSize})`); const isTight = byteStride === elemByteSize; const isAligned = start % info.bytes === 0; if (isTight && isAligned) base = new info.ctor(buffer, start, count * numComps); else { if (count <= 0) base = new info.ctor(new ArrayBuffer(0), 0, 0); else { const elemByteSize2 = info.bytes * numComps; const srcByteLength = (count - 1) * byteStride + elemByteSize2; const srcPtr = copyBytesToWasm(buffer, start, srcByteLength); const outByteLength = count * elemByteSize2; const outPtr = wasm.allocBytes(outByteLength); try { accessorf.deinterleave(outPtr, srcPtr, count, numComps, info.bytes, byteStride); const outBytes = copyBytesFromWasm(outPtr, outByteLength); const outBuffer = new ArrayBuffer(outByteLength); new Uint8Array(outBuffer).set(outBytes); base = new info.ctor(outBuffer, 0, count * numComps); } finally { wasm.freeBytes(outPtr, outByteLength); wasm.freeBytes(srcPtr, srcByteLength); } } } } 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 scount = sparse.count | 0; if (scount <= 0) return; const idxBv = getBufferView(doc.json, sparse.indices.bufferView); if (idxBv.extensions?.["EXT_meshopt_compression"]) throw new Error("EXT_meshopt_compression sparse indices are not supported yet."); const idxBuf = doc.buffers[idxBv.buffer]; if (!idxBuf) throw new Error(`Missing buffer[${idxBv.buffer}] for sparse indices`); const idxOffset = (idxBv.byteOffset ?? 0) + (sparse.indices.byteOffset ?? 0); const idxComponent = sparse.indices.componentType; const idxInfo = COMPONENT_INFO[idxComponent]; if (!idxInfo) throw new Error(`Unsupported sparse indices componentType: ${idxComponent}`); const idxStride = idxInfo.bytes; const valBv = getBufferView(doc.json, sparse.values.bufferView); if (valBv.extensions?.["EXT_meshopt_compression"]) throw new Error("EXT_meshopt_compression sparse values are not supported yet."); const valBuf = doc.buffers[valBv.buffer]; if (!valBuf) throw new Error(`Missing buffer[${valBv.buffer}] for sparse values`); const valOffset = (valBv.byteOffset ?? 0) + (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 = componentCount * componentBytes; const outPtr = wasm.allocBytes(outByteLength); wasm.u8view(outPtr, outByteLength).set(new Uint8Array(out.buffer, out.byteOffset, outByteLength)); const idxByteLength = scount * idxStride; const idxPtr = copyBytesToWasm(idxBuf, idxOffset, idxByteLength); const valuesByteLength = scount * numComps * componentBytes; const valuesPtr = copyBytesToWasm(valBuf, valOffset, valuesByteLength); try { 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 { wasm.freeBytes(valuesPtr, valuesByteLength); wasm.freeBytes(idxPtr, idxByteLength); 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; const srcByteLength = view.array.length * info.bytes; const srcPtr = wasm.allocBytes(srcByteLength); wasm.u8view(srcPtr, srcByteLength).set(new Uint8Array(view.array.buffer, view.array.byteOffset, srcByteLength)); const outPtr = wasm.allocF32(view.array.length); 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; const srcByteLength = view.array.length * info.bytes; const srcPtr = wasm.allocBytes(srcByteLength); wasm.u8view(srcPtr, srcByteLength).set(new Uint8Array(view.array.buffer, view.array.byteOffset, srcByteLength)); const outByteLength = view.array.length * 2; const outPtr = wasm.allocBytes(outByteLength); 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; const srcByteLength = view.array.length * info.bytes; const srcPtr = wasm.allocBytes(srcByteLength); wasm.u8view(srcPtr, srcByteLength).set(new Uint8Array(view.array.buffer, view.array.byteOffset, srcByteLength)); const outByteLength = view.array.length * 4; const outPtr = wasm.allocBytes(outByteLength); 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); } }; // src/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}`); return await res.arrayBuffer(); }; var fetchJson = 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}`); return await res.json(); }; var resolveBuffers = async (json, baseUrl, opts, glbBinChunk) => { const buffers = json.buffers ?? []; const out = new Array(buffers.length); for (let i = 0; i < buffers.length; i++) { const b = buffers[i]; if (!b.uri) { if (!glbBinChunk) throw new Error(`buffers[${i}] has no uri but no GLB BIN chunk was provided`); out[i] = glbBinChunk; continue; } if (isDataUri(b.uri)) { out[i] = decodeDataUri(b.uri).data; continue; } const url = resolveUri(baseUrl, b.uri); out[i] = await fetchArrayBuffer(url, opts); } return out; }; var resolveImages = async (json, buffers, baseUrl, 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) { if (isDataUri(img.uri)) { out[i] = decodeDataUri(img.uri).data; } else { const url = resolveUri(baseUrl, img.uri); out[i] = await fetchArrayBuffer(url, opts); } 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 = (bv.byteOffset ?? 0) | 0; const length = bv.byteLength | 0; const copy = new Uint8Array(length); copy.set(new Uint8Array(buffer, start, length)); out[i] = copy.buffer; continue; } warn(opts, `images[${i}] has neither uri nor bufferView; skipping`); out[i] = new ArrayBuffer(0); } return out; }; var loadGltf = async (source, opts) => { if (typeof source === "string") { const url = source; const baseUrl2 = opts?.baseUrl ?? dirnameUrl(url); if (url.toLowerCase().endsWith(".glb")) { const glb = await fetchArrayBuffer(url, opts); const { json: json3, binChunk } = parseGLB(glb); const buffers3 = await resolveBuffers(json3, baseUrl2, opts, binChunk); const doc3 = { json: json3, buffers: buffers3, baseUrl: baseUrl2 }; if (opts?.loadImages) doc3.images = await resolveImages(json3, buffers3, baseUrl2, opts); return doc3; } const json2 = await fetchJson(url, opts); const buffers2 = await resolveBuffers(json2, baseUrl2, opts, null); const doc2 = { json: json2, buffers: buffers2, baseUrl: baseUrl2 }; if (opts?.loadImages) doc2.images = await resolveImages(json2, buffers2, baseUrl2, opts); return doc2; } const ab = source; const dv = new DataView(ab); const magic = dv.byteLength >= 4 ? dv.getUint32(0, true) : 0; const baseUrl = opts?.baseUrl ?? ""; if (magic === 1179937895) { const { json: json2, binChunk } = parseGLB(ab); const buffers2 = await resolveBuffers(json2, baseUrl, opts, binChunk); const doc2 = { json: json2, buffers: buffers2, baseUrl }; if (opts?.loadImages) doc2.images = await resolveImages(json2, buffers2, baseUrl, opts); return doc2; } const jsonText = new TextDecoder("utf-8").decode(ab); const json = JSON.parse(jsonText); const buffers = await resolveBuffers(json, baseUrl, opts, null); const doc = { json, buffers, baseUrl }; if (opts?.loadImages) doc.images = await resolveImages(json, buffers, baseUrl, opts); return doc; }; // src/wgsl/graphics/mipmap.wgsl var mipmap_default = "@group(0) @binding(0) var samp: sampler; @group(0) @binding(1) var tex: texture_2d; struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f }; @vertex fn vs_main(@builtin(vertex_index) idx: u32) -> VSOut { var positions = array( vec2f(-1.0, -1.0), vec2f( 3.0, -1.0), vec2f(-1.0, 3.0) ); var uvs = array( vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0) ); var o: VSOut; o.pos = vec4f(positions[idx], 0.0, 1.0); o.uv = uvs[idx]; return o; } @fragment fn fs_main(in: VSOut) -> @location(0) vec4f { return textureSample(tex, samp, in.uv); }"; // src/graphics/texture.ts var __texture2d_id = 1; var hasCreateImageBitmap = () => typeof globalThis.createImageBitmap === "function"; 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) => { return 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; _mipmapColorSpace = null; samplerDesc; _gpuTexture = null; _viewLinear = null; _viewSrgb = null; _sampler = null; _uploadPromise = null; _uploadStarted = false; _revision = 0; _width = 0; _height = 0; constructor(desc) { this._source = desc.source; this._mipmaps = desc.mipmaps ?? true; 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; } 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._gpuTexture) { if (colorSpace === "srgb") return this._viewSrgb ?? fallbackView; return this._viewLinear ?? fallbackView; } this.ensureUploaded(device, queue, colorSpace); return fallbackView; } destroy() { 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._uploadStarted) return; this._uploadStarted = true; this._mipmapColorSpace = colorSpace; this._uploadPromise = (async () => { let bitmap = null; let texture = null; try { bitmap = await this.decodeBitmap(); 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: this._mipmaps ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT : GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, 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"); 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) { this._uploadStarted = false; this._uploadPromise = null; this._mipmapColorSpace = null; try { texture?.destroy(); } catch { } throw e; } finally { if (bitmap && this._source.kind !== "bitmap") try { bitmap.close?.(); } catch { } } })(); this._uploadPromise.catch((e) => console.warn("Texture2D upload failed: ", e)); } 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 options = { premultiplyAlpha: "none", imageOrientation: "none", colorSpaceConversion: this._mipmapColorSpace === "srgb" ? "default" : "none" }; if (src.kind === "url") { const res = await fetch(src.url); if (!res.ok) throw new Error(`Failed to fetch texture: ${res.status} ${res.statusText}`); const blob2 = await res.blob(); try { return await createImageBitmap(blob2, options); } catch { return await createImageBitmap(blob2); } } const blob = new Blob([src.bytes], { type: src.mimeType ?? "application/octet-stream" }); try { return await createImageBitmap(blob, options); } catch { return await createImageBitmap(blob); } } 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()]); } }; // src/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); } sample(timeSeconds) { 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; this._disposed = true; 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._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; this.jointIndicesPtr = wasm.allocU32(this.jointCount); const u32 = wasm.u32view(this.jointIndicesPtr, this.jointCount); for (let i = 0; i < this.jointCount; i++) u32[i] = joints[i].index >>> 0; this.invBindPtr = wasm.allocF32(this.jointCount * 16); const f32 = wasm.f32view(this.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; } } } createInstance(meshTransform) { return new SkinInstance(this, meshTransform); } dispose() { if (this._disposed) return; this._disposed = true; if (this.jointIndicesPtr) wasm.freeU32(this.jointIndicesPtr, this.jointCount); if (this.invBindPtr) wasm.freeF32(this.invBindPtr, this.jointCount * 16); } }; var SkinInstance = class { skin; meshTransform; bindMatrixPtr = 0; boneBuffer = null; bindGroup = null; constructor(skin, meshTransform) { this.skin = skin; this.meshTransform = meshTransform; this.bindMatrixPtr = wasm.allocF32(16); const dst = wasm.f32view(this.bindMatrixPtr, 16); const m = meshTransform.worldMatrix; for (let i = 0; i < 16; i++) dst[i] = m[i] ?? (i % 5 === 0 ? 1 : 0); } get jointCount() { return this.skin.jointCount; } ensureGpuResources(device, layout) { 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() { this.boneBuffer?.destroy(); this.boneBuffer = null; this.bindGroup = null; if (this.bindMatrixPtr) { wasm.freeF32(this.bindMatrixPtr, 16); this.bindMatrixPtr = 0; } } }; // src/world/camera.ts var Camera = class _Camera { transform; type; _projectionMatrix = null; _viewMatrix = null; _viewProjectionMatrix = null; _projectionDirty = true; constructor(type) { this.type = type; this.transform = new Transform(); } get destroyed() { return this.transform.disposed; } get viewMatrix() { const world = this.transform.worldMatrix; this._viewMatrix = mat4.invert(world); 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; } get up() { const m = this.transform.worldMatrix; return [m[4], m[5], m[6]]; } 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); this.transform.setRotation(quat2[0], quat2[1], quat2[2], quat2[3]); 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; _near; _far; constructor(descriptor = {}) { super("perspective"); this._fov = descriptor.fov ?? 60; this._aspect = descriptor.aspect ?? 16 / 9; 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 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 ]; } }; // src/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; 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.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; 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 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 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 _tmpMat4Ptr = 0; var _tmpTRSPtr = 0; var ensureDecomposeScratch = () => { if (_tmpMat4Ptr !== 0 && _tmpTRSPtr !== 0) return; _tmpMat4Ptr = wasm.allocF32(16); _tmpTRSPtr = wasm.allocF32(10); }; var applyNodeMatrixViaWasmDecompose = (t, m) => { ensureDecomposeScratch(); const mat = wasm.f32view(_tmpMat4Ptr, 16); for (let i = 0; i < 16; i++) mat[i] = m[i] ?? (i % 5 === 0 ? 1 : 0); mat4f.decomposeTRS(_tmpTRSPtr, _tmpMat4Ptr); const out = wasm.f32view(_tmpTRSPtr, 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]); }; 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_node_visibility: "supported", KHR_animation_pointer: "supported", 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 buildExtensionsMetadata = (json) => { 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"; return { used, required, support }; }; 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()])); for (const material of retainedMaterials) material.retain(); 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 getOrCreateMaterial = (doc, json, materialIndex, materialCache, textureCache, opts) => { if (materialIndex === void 0) return new StandardMaterial({}); const existing = materialCache.get(materialIndex); if (existing) return existing.retain(); const mat = json.materials?.[materialIndex]; if (!mat) { const created2 = new StandardMaterial({}); materialCache.set(materialIndex, created2); return created2; } const getOrCreateTextureByIndex = (textureIndex, usage) => { if (textureIndex === void 0) return null; const cached = textureCache.get(textureIndex); if (cached) return cached; const texDef = json.textures?.[textureIndex]; if (!texDef) { warn2(opts, `glTF texture index ${textureIndex} missing (usage=${usage}).`); return null; } const imageIndex = texDef.source; const img = imageIndex !== void 0 ? json.images?.[imageIndex] : void 0; if (imageIndex === void 0 || !img) { warn2(opts, `glTF texture ${textureIndex} has no valid source image (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); let source = null; const loadedBytes = doc.images?.[imageIndex]; const mimeType = img.mimeType ?? inferMimeTypeFromUri(img.uri); if (loadedBytes) { 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; if (bv && buf) { const start = (bv.byteOffset ?? 0) | 0; source = { kind: "bytes", bytes: buf.slice(start, start + bv.byteLength), 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.baseUrl, img.uri); source = { kind: "url", url, mimeType }; } } if (!source) { warn2(opts, `Could not resolve image source for texture=${textureIndex} (usage=${usage}).`); return null; } const created2 = Texture2D.createFrom({ source, mipmaps: useMipmaps, sampler: { addressModeU, addressModeV, magFilter, minFilter, mipmapFilter } }); textureCache.set(textureIndex, created2); return created2; }; const getTex = (info, usage) => { if (!info) return null; return getOrCreateTextureByIndex(info.index, usage); }; 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({ 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 }); } materialCache.set(materialIndex, created); return created; }; var getPrimitiveVariantMaterials = (doc, json, prim, materialCache, textureCache, 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 material = materialByIndex.get(materialIndex); if (!material) { validateMaterialTextureCoordinates(json.materials?.[materialIndex], prim.attributes, opts, `${context} variant material ${materialIndex}`); material = getOrCreateMaterial(doc, json, materialIndex, materialCache, textureCache, opts); materialByIndex.set(materialIndex, material); } variants.set(variantIndex, material); } return { variants, ownedMaterials: [...materialByIndex.values()] }; }; 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 positions = readAccessorAsFloat32(doc, posAcc); let normals = null; const nAcc = attrs["NORMAL"]; if (nAcc !== void 0) normals = readAccessorAsFloat32(doc, nAcc); let tangents = null; const tangentAcc = attrs["TANGENT"]; if (tangentAcc !== void 0) tangents = readAccessorAsFloat32(doc, tangentAcc); let uvs = null; const uvAcc = attrs["TEXCOORD_0"]; if (uvAcc !== void 0) uvs = readAccessorAsFloat32(doc, uvAcc); let uvs1 = null; const uv1Acc = attrs["TEXCOORD_1"]; if (uv1Acc !== void 0) uvs1 = readAccessorAsFloat32(doc, uv1Acc); 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"]; if (jAcc0 !== void 0 && wAcc0 !== void 0) { const joints0 = readAccessorAsUint16(doc, jAcc0); const weights0 = readAccessorAsFloat32(doc, wAcc0); if (jAcc1 !== void 0 && wAcc1 !== void 0) { 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); } } 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) { 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 (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 = readAccessorAsFloat32(doc, targetPosAcc); if (targetPositions.length === positions.length) target.positions = targetPositions; else warn2(opts, `Primitive morph target ${targetIndex} POSITION length ${targetPositions.length} does not match base POSITION length ${positions.length}; ignoring POSITION deltas.`); } if (targetNormalAcc !== void 0) { const targetNormals = readAccessorAsFloat32(doc, targetNormalAcc); if (targetNormals.length === positions.length) target.normals = targetNormals; else warn2(opts, `Primitive morph target ${targetIndex} NORMAL length ${targetNormals.length} does not match base NORMAL length ${positions.length}; ignoring NORMAL deltas.`); } if (targetAttrs["TANGENT"] !== void 0) warn2(opts, `Primitive morph target ${targetIndex} provides TANGENT deltas; WasmGPU ignores tangent morph data.`); if (!target.positions && !target.normals) warn2(opts, `Primitive morph target ${targetIndex} has no supported POSITION or NORMAL deltas; preserving target slot with no runtime effect.`); morphTargets.push(target); } } const tangentTexCoords = getMaterialTangentTexCoords(prim.material !== void 0 ? json.materials?.[prim.material] : void 0); const tangentSpaceNeeded = tangentTexCoords.length > 0; if (!normals && (computeMissingNormals || tangentSpaceNeeded)) normals = computeGeometryVertexNormals(positions, indices); 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, 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 }); }; var instantiateMeshNode = (doc, json, nodeIndex, node, nodeT, materialCache, textureCache, geometryCache, variantsController, opts) => { if (node.mesh === void 0) return []; const gltfMesh = json.meshes?.[node.mesh]; if (!gltfMesh) { warn2(opts, `nodes[].mesh=${node.mesh} missing; skipping mesh node`); return []; } const out = []; const computeMissingNormals = opts.computeMissingNormals !== false; for (let primIndex = 0; primIndex < gltfMesh.primitives.length; primIndex++) { const prim = gltfMesh.primitives[primIndex]; if (prim.extensions?.["KHR_draco_mesh_compression"]) { warn2(opts, `Mesh ${gltfMesh.name ?? node.mesh} primitive ${primIndex}: KHR_draco_mesh_compression not supported; skipping primitive`); continue; } const cacheKey = `${node.mesh ?? -1}:${primIndex}`; const hasCachedGeometry = geometryCache.has(cacheKey); let geom = geometryCache.get(cacheKey); 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) { const built = buildGeometryFromPrimitive(doc, json, prim, computeMissingNormals, opts); geom = built; geometryCache.set(cacheKey, geom); } if (!geom) continue; if (hasCachedGeometry) geom.retain(); const mat = getOrCreateMaterial(doc, json, prim.material, materialCache, textureCache, opts); const mesh = new Mesh(geom, mat); 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 } }; out.push(mesh); const variantMaterials = getPrimitiveVariantMaterials(doc, json, prim, materialCache, textureCache, opts, `Mesh '${gltfMesh.name ?? node.mesh}' primitive ${primIndex}`); variantsController.register(mesh, mesh.material, variantMaterials.variants); for (const material of variantMaterials.ownedMaterials) material.release(); } return out; }; var instantiateCameraNode = (json, node, nodeT, 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, near: p.znear, far: p.zfar ?? 1e3 }); } 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 }); } 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, 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) 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, 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; } }; 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 samplerTablePtr = samplerCount > 0 ? wasm.allocU32(samplerCount * 5) : 0; const ownedF32Allocs = []; const ownedU32Allocs = []; if (samplerCount > 0) ownedU32Allocs.push({ ptr: samplerTablePtr, len: samplerCount * 5 }); 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 = wasm.allocF32(input.length); wasm.f32view(timesPtr, input.length).set(input); ownedF32Allocs.push({ ptr: timesPtr, len: input.length }); const valuesPtr = wasm.allocF32(output.length); wasm.f32view(valuesPtr, output.length).set(output); ownedF32Allocs.push({ ptr: valuesPtr, len: output.length }); 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) { warn2(opts, `KHR_animation_pointer: animation '${animationName}' channel ${ci} sets target.node; skipping pointer channel.`); continue; } const pointer = getChannelPointer(c); if (!pointer) { warn2(opts, `KHR_animation_pointer: animation '${animationName}' channel ${ci} is missing extensions.KHR_animation_pointer.pointer.`); continue; } const resolved = resolveAnimationPointer(pointerContext, pointer); if (!resolved) { warn2(opts, `KHR_animation_pointer: 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) { warn2(opts, `KHR_animation_pointer: animation '${animationName}' channel ${ci} references missing sampler ${chan.sampler}.`); continue; } if (resolved.requiresStep && sampler.interpolation !== "STEP") { warn2(opts, `KHR_animation_pointer: boolean pointer '${resolved.canonical}' requires STEP interpolation; skipping channel.`); continue; } if ((sampler.valueSize | 0) !== (resolved.valueSize | 0)) { warn2(opts, `KHR_animation_pointer: 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 = wasm.allocU32(channelCount * 3); const ch = wasm.u32view(channelsPtr, channelCount * 3); ownedU32Allocs.push({ ptr: channelsPtr, len: 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 }); } else { for (const a2 of ownedF32Allocs) wasm.freeF32(a2.ptr, a2.len); for (const a2 of ownedU32Allocs) wasm.freeU32(a2.ptr, a2.len); } out.push({ name: a.name, samplers, channels, clip }); } return out; }; var importGltf = (doc, opts = {}) => { const json = doc.json; 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(); 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 extensions = buildExtensionsMetadata(json); const xmp = buildXmpMetadata(json); const variantsController = createVariantsController(getDeclaredVariants(json, xmp.packets)); const skins = parseSkins(doc, json, nodes, opts); const materialCache = /* @__PURE__ */ new Map(); const textureCache = /* @__PURE__ */ new Map(); const geometryCache = /* @__PURE__ */ new Map(); const meshes = []; const cameras = []; const lights = []; const cameraRuntimeMap = /* @__PURE__ */ new Map(); const lightRuntimeMap = /* @__PURE__ */ new Map(); const khrLights = getKHRLightsFromRoot(json); const instantiateNodeRecursive = (nodeIndex, inheritedSkinIndex) => { const node = gltfNodes[nodeIndex]; if (!node) return; const importedNode = nodes[nodeIndex]; const nodeT = importedNode?.transform; if (!importedNode || !nodeT) return; const createdMeshes = instantiateMeshNode(doc, json, nodeIndex, node, nodeT, materialCache, textureCache, geometryCache, variantsController, opts); importedNode.meshes = createdMeshes; importedNode.applyVisibility(); const skinIndex = node.skin !== void 0 ? node.skin | 0 : inheritedSkinIndex; 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); } if (opts.importCameras) { const cam = instantiateCameraNode(json, node, nodeT, 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); 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); } else warn2(opts, `Light '${node.name ?? `index ${nodeIndex}`}' has unsupported type '${lightDef.type}' and was skipped.`); } } } for (const child of node.children ?? []) instantiateNodeRecursive(child, skinIndex); }; const gltfScene = json.scenes?.[sceneIndex]; const roots = gltfScene?.nodes ?? []; for (const root of roots) instantiateNodeRecursive(root, void 0); const animations = parseAnimations(doc, json, nodes, materialCache, cameraRuntimeMap, lightRuntimeMap, opts); const clips = animations.map((a) => a.clip).filter((c) => c !== null); const metadata = buildImportMetadata(json, sceneIndex, extensions, xmp, variantsController.public); let destroyed = false; return { scene, meshes, nodes, lights, cameras, skins, animations, clips, metadata, destroy() { if (destroyed) return; destroyed = true; if (addToScene) { for (const m of meshes) scene.remove(m); for (const light of lights) scene.removeLight(light); } for (const light of lights) unbindLightTransform(light); for (const m of meshes) m.destroy(); for (const camera of cameras) camera.destroy(); for (const a of animations) a.clip?.dispose(); for (const s of skins) s.runtime?.dispose(); variantsController.destroy(); for (const tex of textureCache.values()) tex.destroy(); for (const node of nodes) node.transform.dispose(); } }; }; // src/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]; }; // src/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; } 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}'.`); this.layers.set(layer.id, layer); layer.setSystem?.(this); layer.attach(this.root); this.invalidate("manual"); return this; } removeLayer(id) { const layer = this.layers.get(id); if (!layer) return this; layer.setSystem?.(null); layer.detach(); this.layers.delete(id); this.invalidate("manual"); return this; } clearLayers() { for (const layer of this.layers.values()) { layer.setSystem?.(null); layer.detach(); } 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 layer of this.layers.values()) { 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.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"); } }; // src/overlay/axisTriadLayer.ts var AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; var normalize2 = (x, y) => { const len = Math.hypot(x, y); if (len <= 1e-8) return [0, 0]; return [x / len, y / len]; }; var AxisTriadLayer = class { id; anchor; lengthWorld; sizePx; lineWidthPx; labels; colors; labelOffsetPx; font; root = null; container = null; lines = []; labelEls = []; _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.colors = desc.colors ?? ["#ff5f56", "#3fd77a", "#4ca7ff"]; this.labelOffsetPx = Math.max(0, desc.labelOffsetPx ?? 8); this.font = desc.font ?? "11px monospace"; } setSystem(system) { this._system = system; } attach(root) { this.root = 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.lines = []; this.labelEls = []; for (let i = 0; i < 3; i++) { const line = document.createElement("div"); line.style.position = "absolute"; line.style.transformOrigin = "0 50%"; line.style.background = this.colors[i]; container.appendChild(line); this.lines.push(line); const label = document.createElement("div"); label.style.position = "absolute"; label.style.font = this.font; label.style.color = this.colors[i]; label.style.whiteSpace = "nowrap"; label.textContent = this.labels[i]; container.appendChild(label); this.labelEls.push(label); } } detach() { this.container?.remove(); this.root = null; this.container = null; this.lines = []; this.labelEls = []; } update(ctx) { if (!this.container || !this.root) return; if (this.anchor?.kind === "world") { this.updateWorld(ctx, this.anchor); return; } this.updateScreen(ctx); } updateWorld(ctx, anchor) { const origin = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, anchor.position); if (!origin || !origin.inFront) { this.hideAll(); return; } for (let i = 0; i < 3; i++) { const axis = AXES[i]; const endpoint = projectWorldToScreen(ctx.camera, ctx.width, ctx.height, [ anchor.position[0] + axis[0] * this.lengthWorld, anchor.position[1] + axis[1] * this.lengthWorld, anchor.position[2] + axis[2] * this.lengthWorld ]); if (!endpoint || !endpoint.inFront) { this.lines[i].style.display = "none"; this.labelEls[i].style.display = "none"; continue; } this.drawLine(this.lines[i], origin.x, origin.y, endpoint.x, endpoint.y, this.colors[i], this.lineWidthPx); this.drawLabel(this.labelEls[i], endpoint.x, endpoint.y, this.colors[i]); } } updateScreen(ctx) { const [cx, cy] = resolveScreenAnchorPoint( this.anchor?.kind === "screen" ? this.anchor : { kind: "screen", corner: "bottom-left", offsetPx: [26, -26] }, ctx.width, ctx.height ); const m = ctx.camera.viewMatrix; for (let i = 0; i < 3; i++) { const axis = AXES[i]; const vx = m[0] * axis[0] + m[4] * axis[1] + m[8] * axis[2]; const vy = m[1] * axis[0] + m[5] * axis[1] + m[9] * axis[2]; const [dx, dy] = normalize2(vx, -vy); const ex = cx + dx * this.sizePx; const ey = cy + dy * this.sizePx; this.drawLine(this.lines[i], cx, cy, ex, ey, this.colors[i], this.lineWidthPx); this.drawLabel(this.labelEls[i], ex, ey, this.colors[i]); } } drawLine(node, x0, y0, x1, y1, color, widthPx) { 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; } const angle = Math.atan2(dy, dx); node.style.display = ""; node.style.background = color; 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(${angle}rad)`; } drawLabel(node, x, y, color) { node.style.display = ""; node.style.left = `${x + this.labelOffsetPx}px`; node.style.top = `${y - this.labelOffsetPx}px`; node.style.color = color; } hideAll() { for (let i = 0; i < this.lines.length; i++) this.lines[i].style.display = "none"; for (let i = 0; i < this.labelEls.length; i++) this.labelEls[i].style.display = "none"; } }; // src/overlay/pool.ts var DOMNodePool = class { constructor(parent, create, maxNodes) { this.parent = parent; this.create = create; this.maxNodes = 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; } }; // src/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 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) => { 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.display = ""; 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.background = color; 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; container = null; linePool = null; labelPool = 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"; } 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.linePool = new DOMNodePool(container, () => { const node = document.createElement("div"); node.style.position = "absolute"; node.style.transformOrigin = "0 50%"; 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"; return node; }, this.maxLabels); } detach() { this.linePool?.clear(true); this.labelPool?.clear(true); this.linePool = null; this.labelPool = null; this.container?.remove(); this.container = null; } 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); for (let i = 0; i < 32; i++) { const estLabelsU = majorCountU > 0 ? Math.ceil(majorCountU / labelStrideU) : 0; const estLabelsV = majorCountV > 0 ? Math.ceil(majorCountV / labelStrideV) : 0; if (estLabelsU + estLabelsV <= this.maxLabels) break; if (estLabelsU >= estLabelsV) labelStrideU++; else labelStrideV++; } this.linePool.beginFrame(); this.labelPool.beginFrame(); 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(); 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); if (major && uMajorIndex % labelStrideU === 0) { const label = this.labelPool.acquire(); label.textContent = formatTick(u); label.style.left = `${p0.x + 4}px`; label.style.top = `${p0.y + 2}px`; } 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(); 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); if (major && vMajorIndex % labelStrideV === 0) { const label = this.labelPool.acquire(); label.textContent = formatTick(v); label.style.left = `${p0.x + 4}px`; label.style.top = `${p0.y + 2}px`; } if (major) vMajorIndex++; } this.linePool.endFrame(); this.labelPool.endFrame(); } 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) }; } }; // src/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 sampleCustomStops = (tIn, stopsIn) => { return 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 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; anchor; _system = null; unsubscribeSource = null; sourceDirty = true; lastSignature = null; container = null; titleEl = 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(32, 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.bindSource(this.source); } setSystem(system) { this._system = system; } setSource(source) { this.source = source; this.bindSource(source); this.sourceDirty = true; this._system?.invalidate("scale"); } attach(root) { const container = document.createElement("div"); 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; root.appendChild(container); this.container = container; const titleEl = document.createElement("div"); titleEl.textContent = this.title; titleEl.style.marginBottom = "6px"; titleEl.style.font = this.font; container.appendChild(titleEl); this.titleEl = titleEl; const gradientWrap = document.createElement("div"); gradientWrap.style.position = "relative"; gradientWrap.style.width = `${this.widthPx + 64}px`; gradientWrap.style.height = `${this.heightPx}px`; container.appendChild(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"; gradientWrap.appendChild(canvas); this.gradientCanvas = canvas; this.gradientCtx = canvas.getContext("2d"); 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; this.tickMarkPool = new DOMNodePool(gradientWrap, () => { const el = document.createElement("div"); el.style.position = "absolute"; 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"; return el; }, this.tickCount); } 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.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(); } positionContainer(ctx) { if (!this.container) return; const [x, y] = resolveScreenAnchorPoint(this.anchor, ctx.width, ctx.height); const corner = this.anchor?.corner ?? "top-right"; const totalHeight = this.heightPx + 32; const totalWidth = this.widthPx + 80; let left = x; let top = y; if (this.anchor?.x === void 0 && corner.includes("right")) left -= totalWidth; if (this.anchor?.y === void 0 && corner.includes("bottom")) top -= totalHeight; this.container.style.left = `${left}px`; this.container.style.top = `${top}px`; } bindSource(source) { this.unsubscribeSource?.(); this.unsubscribeSource = subscribeSource(source, () => { this.sourceDirty = true; this._system?.invalidate("scale"); }); } renderLegend() { if (!this.gradientCanvas || !this.gradientCtx || !this.tickMarkPool || !this.tickLabelPool) return; try { const resolved = resolveSource(this.source, this.strictParity); if (resolved.signature !== this.lastSignature) { this.lastSignature = resolved.signature; 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); for (let y = 0; y < h; y++) { const t = 1 - y / Math.max(1, h - 1); const c = resolved.sample(t); 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))); for (let x = 0; x < w; x++) { 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; } } this.gradientCtx.putImageData(image, 0, 0); } renderTicks(resolved) { if (!this.tickMarkPool || !this.tickLabelPool || !this.gradientCanvas) return; this.tickMarkPool.beginFrame(); this.tickLabelPool.beginFrame(); const h = this.gradientCanvas.height; for (let i = 0; i < this.tickCount; i++) { const alpha = i / Math.max(1, this.tickCount - 1); const y = alpha * h; const t = 1 - alpha; const value = invertScaleTransformCPU(clamp01(t), resolved.transform); const mark = this.tickMarkPool.acquire(); mark.style.left = `${this.widthPx + 4}px`; mark.style.top = `${y}px`; mark.style.width = "8px"; mark.style.height = "1px"; mark.style.background = "#dce9ff"; const label = this.tickLabelPool.acquire(); label.style.left = `${this.widthPx + 16}px`; label.style.top = `${y - 6}px`; label.textContent = this.formatValue(value); } this.tickMarkPool.endFrame(); this.tickLabelPool.endFrame(); } }; // src/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})`; }; // src/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)}]`; }; // src/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; } }; // src/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; } }; // src/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 { } } }; // src/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; // src/python/index.ts var isPyProxyLike = (x) => { return typeof x === "object" && x !== null && typeof x.getBuffer === "function"; }; var isPyBufferLike = (x) => { return typeof x === "object" && x !== null && typeof x.data === "object" && Array.isArray(x.shape); }; var normalizeShape = (shape) => { const out = new Array(shape.length); for (let i = 0; i < shape.length; i++) { const dim = shape[i]; assert(Number.isFinite(dim), `shape[${i}] must be finite`); assert(Number.isInteger(dim), `shape[${i}] must be an integer`); assert(dim >= 0, `shape[${i}] must be >= 0`); out[i] = dim >>> 0; } return out; }; var numelOfShape = (shape) => { if (shape.length === 0) return 1; let n = 1; for (const d of shape) { const dim = d >>> 0; const next = n * dim; assert(Number.isSafeInteger(next), "shape is too large (numel overflow)"); n = next; } return n >>> 0; }; var dtypeOfTypedArray = (view) => { if (view instanceof Float32Array) return "f32"; if (view instanceof Float64Array) return "f64"; if (view instanceof Int8Array) return "i8"; if (view instanceof Uint8Array) return "u8"; if (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"; return null; }; var viewAsDType = (dtype, view, offsetElements = 0, lengthElements) => { const info = dtypeInfo(dtype); const ctor = info.ctor; const bpe = info.bytesPerElement >>> 0; const offEl = offsetElements >>> 0; const lenEl = lengthElements === void 0 ? (view.byteLength >>> 0) / bpe >>> 0 : lengthElements >>> 0; const byteOffset = (view.byteOffset >>> 0) + offEl * bpe >>> 0; return new ctor(view.buffer, byteOffset >>> 0, lenEl >>> 0); }; var allocBytes = (byteLength, align, allocator) => { const bytes = byteLength >>> 0; const a0 = align >>> 0; const a = a0 === 0 ? 16 : a0; if ((a & a - 1) !== 0) throw new Error(`allocBytes(${byteLength}, ${align}): align must be a power of two`); if (allocator === "frame") { const ptr2 = frameArena.alloc(bytes, a) >>> 0; return { kind: "frame", ptr: ptr2 }; } if (allocator && typeof allocator === "object") { const arena = allocator; const epoch = arena.epoch() >>> 0; const ptr2 = arena.alloc(bytes, a) >>> 0; return { kind: "arena", ptr: ptr2, epoch }; } const ptr = wasm.allocBytes(bytes) >>> 0; if (!ptr && bytes !== 0) throw new Error(`wasm.allocBytes(${bytes}) failed`); const heapAlign = Math.min(a, 8) >>> 0; if (heapAlign !== 0 && (ptr & heapAlign - 1) !== 0) throw new Error(`wasm.allocBytes(${bytes}) returned ptr 0x${ptr.toString(16)} which is not ${heapAlign}-byte aligned`); return { kind: "heap", ptr }; }; var typedViewFromPtr = (dtype, ptr, length) => { const info = dtypeInfo(dtype); const ctor = info.ctor; const buf = driver.buffer(); return new ctor(buf, ptr >>> 0, length >>> 0); }; var bytesViewFromPtr = (ptr, byteLength) => { const base = driver.bytes(); const start = ptr >>> 0; const end = start + (byteLength >>> 0) >>> 0; return base.subarray(start, end); }; var assertIsCContiguous = (buf) => { if (typeof buf.c_contiguous === "boolean") { assert(buf.c_contiguous, 'Python buffer must be C-contiguous (use numpy.ascontiguousarray(..., order="C"))'); return; } const shape = normalizeShape(buf.shape ?? []); const strides = Array.from(buf.strides ?? []); assert(strides.length === shape.length, "Python buffer strides/shape rank mismatch"); if (numelOfShape(shape) === 0) return; let expected = 1; for (let i = shape.length - 1; i >= 0; i--) { assert(strides[i] === expected, 'Python buffer must be C-contiguous (use numpy.ascontiguousarray(..., order="C"))'); expected *= shape[i]; } }; var resolveSource2 = (src, options) => { if (isPyProxyLike(src)) { const pybuf = src.getBuffer(); const resolved = resolveSource2(pybuf, options); const release = typeof pybuf.release === "function" ? () => pybuf.release?.() : void 0; return { ...resolved, release }; } if (isPyBufferLike(src)) { assertIsCContiguous(src); assert(ArrayBuffer.isView(src.data), "Python buffer .data must be an ArrayBufferView"); assert(typeof src.data.subarray === "function", "Python buffer .data must be a TypedArray (DataView is not supported)"); const inferred2 = dtypeOfTypedArray(src.data); assert(inferred2 !== null, "Unsupported Python buffer dtype (expected a numeric TypedArray)"); const dtype2 = options.dtype ?? inferred2; assert(dtype2 === inferred2, `dtype mismatch: expected ${dtype2}, got ${inferred2}`); const srcShape = normalizeShape(src.shape); const shape2 = normalizeShape(options.shape ?? srcShape); const numel2 = numelOfShape(shape2); const offset = (src.offset ?? 0) >>> 0; const data2 = viewAsDType(dtype2, src.data, offset, numel2); assert(data2.length >>> 0 === numel2 >>> 0, "Python buffer view length mismatch"); if (options.shape) assert(normalizeShape(options.shape).length === srcShape.length && normalizeShape(options.shape).every((d, i) => d === srcShape[i]), "shape mismatch"); return { dtype: dtype2, shape: shape2, data: data2, release: typeof src.release === "function" ? () => src.release?.() : void 0 }; } assert(ArrayBuffer.isView(src), "Expected a TypedArray / ArrayBufferView or a PyProxy supporting getBuffer()"); const inferred = dtypeOfTypedArray(src); assert(inferred !== null, "Unsupported TypedArray dtype"); const dtype = options.dtype ?? inferred; assert(dtype === inferred, `dtype mismatch: expected ${dtype}, got ${inferred}`); assert(options.shape, "shape is required when sending a plain TypedArray (no Python buffer metadata available)"); const shape = normalizeShape(options.shape); const numel = numelOfShape(shape); assert(src.byteLength >>> 0 >= numel * dtypeInfo(dtype).bytesPerElement, "source TypedArray is too small for the provided shape"); const data = viewAsDType(dtype, src, 0, numel); assert(data.length >>> 0 === numel >>> 0, "source TypedArray length mismatch"); return { dtype, shape, data }; }; var pythonInterop = { sendNdarray: (src, options = {}) => { const resolved = resolveSource2(src, options); const { dtype, shape, data } = resolved; const info = dtypeInfo(dtype); const numel = numelOfShape(shape); const byteLength = numel * info.bytesPerElement >>> 0; const alloc = allocBytes(byteLength, 16, options.allocator); const dst = typedViewFromPtr(dtype, alloc.ptr, numel); dst.set(data); try { resolved.release?.(); } catch { } const handle = { kind: alloc.kind, dtype, shape, ptr: alloc.ptr >>> 0, length: numel >>> 0, byteLength: byteLength >>> 0 }; if (alloc.epoch !== void 0) handle.epoch = alloc.epoch >>> 0; return handle; }, view: (handle) => { return typedViewFromPtr(handle.dtype, handle.ptr, handle.length); }, bytes: (handle) => { return bytesViewFromPtr(handle.ptr, handle.byteLength); }, copyInto: (handle, src, options = {}) => { const resolved = resolveSource2(src, { ...options, dtype: handle.dtype, shape: handle.shape }); const { data } = resolved; assert(data.length >>> 0 === handle.length >>> 0, "copyInto: source length mismatch"); const dst = typedViewFromPtr(handle.dtype, handle.ptr, handle.length); dst.set(data); try { resolved.release?.(); } catch { } }, receiveNdarray: (handle, options = {}) => { const view = typedViewFromPtr(handle.dtype, handle.ptr, handle.length); if (!options.copy) return { dtype: handle.dtype, shape: Array.from(handle.shape), data: view }; const info = dtypeInfo(handle.dtype); const ctor = info.ctor; const out = new ctor(handle.length >>> 0); out.set(view); return { dtype: handle.dtype, shape: Array.from(handle.shape), data: out }; }, free: (handle) => { if (handle.kind !== "heap") throw new Error(`pythonInterop.free(): cannot free a ${handle.kind} allocation. Use reset() for arena-like allocators (frameArena.reset() / WasmHeapArena.reset()).`); wasm.freeBytes(handle.ptr >>> 0, handle.byteLength >>> 0); } }; // src/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 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 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 }; _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]; _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 = 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; } this.domElement.style.touchAction = "none"; this.syncFromCamera(); 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); } dispose() { this.cancelTransition(); this.clearWheelInteractionTimer(); 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._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(); this._mode = mode; this.syncFromCamera(); return this; } syncFromCamera() { 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); this._theta = spherical.theta; this._phi = spherical.phi; this._thetaDelta = 0; this._phiDelta = 0; this._dollyDelta = 0; this._panOffset = [0, 0, 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() { this._savedTarget = vec3clone(this.target); this._savedPosition = vec3clone(this.camera.position); 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 this.updateTrackball(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(); 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.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._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.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(); }; 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; 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) }; } return { type: "perspective", near: lerp2(a.near, b.near, t), far: lerp2(a.far, b.far, t) }; } applyPose(position, target, up) { this.target = vec3clone(target); this.camera.transform.setPosition(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 }; } updateOrbit(dt) { const damping = this.enableDamping ? 1 - Math.pow(1 - clamp012(this.dampingFactor), dt * 60) : 1; 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 minPhi = Math.max(this.minPolarAngle, ORBIT_POLE_EPS); const maxPhi = 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.transform.setPosition(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.transform.setPosition(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); panX = 2 * deltaX * targetDistance / h * 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 = 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 = Math.max(minNear, camera.far - 0.01); const nextNear = clamp2(desiredNear, minNear, maxNear); if (Math.abs(nextNear - camera.near) > Math.max(minNear, camera.near) * 1e-3) camera.near = nextNear; 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; } 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 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" }); } }; // src/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); } } }; // src/core/engine.ts var WasmGPU = class _WasmGPU { renderer; compute; scale; _performanceStats = null; _isRunning = false; _lastTime = 0; _frameCallback = null; _animationFrameId = null; constructor(renderer, desc) { this.renderer = renderer; const gpu = renderer.gpu; this.compute = new Compute(gpu.device, gpu.queue, desc); 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 python() { return pythonInterop; } get python() { return pythonInterop; } static get math() { return { mat4, quat, vec3 }; } get math() { return { mat4, quat, vec3 }; } 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 === "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 }; } 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); } }; 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); } 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, GPUndarray, Geometry, GlyphField, GridLayer, LegendLayer, Light, Material, Mesh, NavigationControls, Ndarray, NodeLink, OrbitControls, OrthographicCamera, OverlaySystem, PerformanceStats, PerspectiveCamera, PointCloud, PointLight, ReadbackRing, Renderer, SCALE_UNIFORM_FLOAT_COUNT, ScaleService, Scene, SelectionStore, Skin, SkinInstance, 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, WasmGPU as default, defaultScaleTransform, driver, dtypeInfo, emptyBounds, expandBounds, formatAngleRadians, formatDistanceWorld, formatFiniteNumber, formatWorldVector, frameArena, frustumf, getBoundsCenter, getBoundsCorners, getBoundsSize, importGltf, initWebAssembly, invertScaleTransformCPU, loadGltf, makeWorkgroupCounts, makeWorkgroupSize, mapAnnotationProbeReadout, mat4, ndarrayf, normalizeBounds, normalizeScaleTransform, normalizeWorkgroups, packScaleTransform, parseGLB, pythonInterop, quat, readAccessor, readAccessorAsFloat32, readAccessorAsUint16, readIndicesAsUint32, resolveAnnotationUnits, resolveScaleTransformDomainCPU, scaleClampModeToId, scaleModeToId, scaleValueModeToId, transformBounds, unionBounds, vec3, wasm, webassemblyInterop, workgroups1D, workgroups2D, workgroups3D };