pelican-engine 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,155 @@
1
+ import * as THREE from "three/webgpu";
2
+ import * as TSL from "three/tsl";
3
+ const fail = (code, detail) => { throw new Error(`TC_RENDER_TEXTURE_${code}: ${detail} Repair: Bind a live managed texture and use an initialized WebGPU backend.`); };
4
+ /** Three owns presentation textures; the compute executor owns the source versions. */
5
+ export function createRenderTextureBridge(renderer, source, resources) {
6
+ const backend = renderer.backend, device = backend.device;
7
+ const entries = new Map();
8
+ let disposed = false, allocations = 0, releases = 0, copies = 0, bytes = 0;
9
+ const active = () => { if (disposed)
10
+ fail("DISPOSED", "Texture bridge was disposed."); if (renderer.backend !== backend || backend.isWebGLBackend || !device || backend.device !== device)
11
+ fail("CAPABILITY", "Managed render textures require WebGPU."); };
12
+ const allocate = (width, height) => {
13
+ if (![width, height].every(value => Number.isSafeInteger(value) && value > 0 && value <= device.limits.maxTextureDimension2D))
14
+ fail("SIZE", "Texture dimensions exceed actual device limits.");
15
+ const texture = new THREE.StorageTexture(width, height);
16
+ texture.name = "Pelican managed render texture";
17
+ texture.format = THREE.RGBAFormat;
18
+ texture.type = THREE.FloatType;
19
+ texture.minFilter = THREE.NearestFilter;
20
+ texture.magFilter = THREE.NearestFilter;
21
+ texture.generateMipmaps = false;
22
+ texture.flipY = false;
23
+ texture.colorSpace = THREE.NoColorSpace;
24
+ try {
25
+ renderer.initTexture(texture);
26
+ }
27
+ catch (error) {
28
+ texture.dispose();
29
+ throw error;
30
+ }
31
+ allocations++;
32
+ return texture;
33
+ };
34
+ const get = (material, name) => {
35
+ active();
36
+ const descriptor = resources(material.name)[name] ?? fail("UNBOUND", material.name + ":" + name);
37
+ const shape = descriptor.texture;
38
+ if (!shape || descriptor.type !== "vec4" || descriptor.values.length !== shape.width * shape.height)
39
+ return fail("RESOURCE", "Invalid managed texture descriptor.");
40
+ let slots = entries.get(material);
41
+ if (!slots) {
42
+ slots = new Map();
43
+ entries.set(material, slots);
44
+ }
45
+ let entry = slots.get(name);
46
+ if (!entry) {
47
+ const texture = allocate(shape.width, shape.height);
48
+ entry = { material, name, texture, width: shape.width, height: shape.height, size: TSL.uniform(new THREE.Vector2(shape.width, shape.height)), nodes: [] };
49
+ slots.set(name, entry);
50
+ }
51
+ return entry;
52
+ };
53
+ return {
54
+ prepare(material, slots) { for (const name of Object.keys(slots))
55
+ get(material, name); },
56
+ node(material, name, uv, sampler) {
57
+ const entry = get(material, name), size = entry.size;
58
+ const finite = uv.x.equal(uv.x).and(uv.y.equal(uv.y)).and(TSL.abs(uv.x).lessThanEqual(3.402823e38)).and(TSL.abs(uv.y).lessThanEqual(3.402823e38));
59
+ const safe = TSL.select(finite, uv, TSL.vec2(0));
60
+ const normalize = (value, wrap = "clamp") => wrap === "repeat" ? value.sub(value.floor()) : wrap === "mirror" ? TSL.float(1).sub(TSL.abs(value.sub(value.mul(0.5).floor().mul(2)).sub(1))) : TSL.clamp(value, 0, 1);
61
+ const address = (value, length, wrap = "clamp") => {
62
+ if (wrap === "clamp")
63
+ return TSL.clamp(value, 0, length.sub(1));
64
+ const period = wrap === "mirror" ? length.mul(2) : length;
65
+ const cell = value.sub(value.div(period).floor().mul(period));
66
+ return wrap === "mirror" ? TSL.min(cell, length.mul(2).sub(1).sub(cell)) : cell;
67
+ };
68
+ const read = (x, y) => {
69
+ const coord = TSL.ivec2(address(x, size.x, sampler.wrapU), address(y, size.y, sampler.wrapV));
70
+ const node = TSL.textureLoad(entry.texture, coord);
71
+ node.updateMatrix = false;
72
+ node.mergeable = false;
73
+ entry.nodes.push(node);
74
+ return node.level(TSL.float(0));
75
+ };
76
+ const x = normalize(safe.x, sampler.wrapU).mul(size.x), y = normalize(safe.y, sampler.wrapV).mul(size.y);
77
+ let value;
78
+ if (sampler.filter === "nearest")
79
+ value = read(x.floor(), y.floor());
80
+ else {
81
+ const px = x.sub(0.5), py = y.sub(0.5), bx = px.floor(), by = py.floor(), wx = px.fract(), wy = py.fract();
82
+ value = TSL.mix(TSL.mix(read(bx, by), read(bx.add(1), by), wx), TSL.mix(read(bx, by.add(1)), read(bx.add(1), by.add(1)), wx), wy);
83
+ }
84
+ return TSL.select(finite, value, TSL.vec4(0));
85
+ },
86
+ update() {
87
+ if (!entries.size)
88
+ return;
89
+ active();
90
+ const owner = source() ?? fail("CAPABILITY", "Missing compute texture executor.");
91
+ const planned = [];
92
+ for (const [material, slots] of entries) {
93
+ const current = resources(material.name);
94
+ for (const entry of slots.values()) {
95
+ const descriptor = current[entry.name] ?? fail("UNBOUND", material.name + ":" + entry.name);
96
+ if (!descriptor.texture || descriptor.type !== "vec4" || descriptor.values.length !== descriptor.texture.width * descriptor.texture.height)
97
+ fail("RESOURCE", "Invalid managed texture descriptor.");
98
+ planned.push({ entry, descriptor });
99
+ }
100
+ }
101
+ const encoder = device.createCommandEncoder({ label: "Pelican managed render texture copies" }), staged = [];
102
+ try {
103
+ for (const { entry, descriptor } of planned) {
104
+ if (entry.key === descriptor.key && entry.revision === descriptor.revision)
105
+ continue;
106
+ const view = owner.textureResident(descriptor), shape = descriptor.texture;
107
+ if (view.width !== shape.width || view.height !== shape.height || view.revision !== descriptor.revision || view.texture.format !== "rgba32float" || !(view.texture.usage & GPUTextureUsage.COPY_SRC))
108
+ fail("RESOURCE", "Executor returned an incompatible texture.");
109
+ const replacement = entry.width !== view.width || entry.height !== view.height;
110
+ const texture = replacement ? allocate(view.width, view.height) : entry.texture;
111
+ staged.push({ entry, descriptor, texture, ...(replacement ? { previous: entry.texture } : {}) });
112
+ const target = backend.get(texture).texture ?? fail("RESOURCE", "Three did not allocate the render texture.");
113
+ if (target === view.texture || target.format !== "rgba32float" || !(target.usage & GPUTextureUsage.COPY_DST))
114
+ fail("RESOURCE", "Render destination is incompatible or aliases the compute source.");
115
+ encoder.copyTextureToTexture({ texture: view.texture }, { texture: target }, { width: view.width, height: view.height, depthOrArrayLayers: 1 });
116
+ }
117
+ if (staged.length)
118
+ device.queue.submit([encoder.finish()]);
119
+ }
120
+ catch (error) {
121
+ for (const item of staged)
122
+ if (item.previous) {
123
+ item.texture.dispose();
124
+ releases++;
125
+ }
126
+ throw error;
127
+ }
128
+ for (const item of staged) {
129
+ const shape = item.descriptor.texture;
130
+ if (item.previous) {
131
+ item.entry.texture = item.texture;
132
+ item.entry.width = shape.width;
133
+ item.entry.height = shape.height;
134
+ item.entry.size.value.set(shape.width, shape.height);
135
+ for (const node of item.entry.nodes)
136
+ node.value = item.texture;
137
+ item.entry.material.needsUpdate = true;
138
+ item.previous.dispose();
139
+ releases++;
140
+ }
141
+ item.entry.key = item.descriptor.key;
142
+ item.entry.revision = item.descriptor.revision;
143
+ copies++;
144
+ bytes += shape.width * shape.height * 16;
145
+ }
146
+ },
147
+ inspect() { return { disposed, slots: [...entries.values()].reduce((n, m) => n + m.size, 0), allocations, released: releases, liveTextures: allocations - releases, gpuCopies: copies, gpuCopyBytes: bytes, cpuResultUploads: 0, bindings: [...entries.values()].flatMap(slots => [...slots.values()].map(e => ({ target: e.material.name, name: e.name, width: e.width, height: e.height, key: e.key, revision: e.revision }))) }; },
148
+ dispose() { if (disposed)
149
+ return; disposed = true; for (const slots of entries.values())
150
+ for (const entry of slots.values()) {
151
+ entry.texture.dispose();
152
+ releases++;
153
+ } entries.clear(); },
154
+ };
155
+ }
@@ -0,0 +1,50 @@
1
+ export function startupPolicy(development, search) {
2
+ const automation = development && new URLSearchParams(search).get("automation") === "1";
3
+ return { automation, playing: !automation };
4
+ }
5
+ /** Diagnostic descriptions distinguish capability from lazy renderer allocation. */
6
+ export function inspectTraceCapabilities(data) {
7
+ const compute = data.backend === "webgpu";
8
+ const legacy = compute && data.legacyProbe;
9
+ const modern = compute && !data.legacyProbe;
10
+ return {
11
+ tracer: !compute ? false : legacy ? "legacy software-triangle-BVH" : data.surfaceInitialized ? "WebGPU surface triangle-BVH" : "WebGPU surface triangle-BVH (lazy initialization)",
12
+ tracerInitialized: compute && (legacy || data.surfaceInitialized),
13
+ hardwareRT: false,
14
+ denoiser: modern ? "motion-reprojected temporal history + spatial a-trous; complete quality matrix unaccepted" : false,
15
+ hybrid: !compute ? false : legacy ? "legacy raster + traced image compositing prototype" : "raster primary visibility + actual shadow and secondary rays",
16
+ pathBounces: data.maxBounces,
17
+ tracedLights: !compute ? false : legacy ? "legacy first authored light only" : "all captured point/directional/ambient/hemisphere lights",
18
+ textures: data.gameplay ? (modern ? "supported GLB PBR textures in raster and surface tracing; see documented limits" : "GLB PBR and base-color textures in raster") : "no authored texture assets",
19
+ experimentalProbes: "Four small CPU simulations and finite world probe are M0 debug sidecars, not M1 production modules",
20
+ };
21
+ }
22
+ /** Explicit progress deadline; startup and commands never require a user focus/key event. */
23
+ export async function boundedOperation(promise, label, timeoutMs = 30000) {
24
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
25
+ throw new Error("TC_GPU_TIMEOUT_CONFIG: Invalid timeout.");
26
+ let timer;
27
+ try {
28
+ return await Promise.race([
29
+ Promise.resolve(promise),
30
+ new Promise((_, reject) => {
31
+ timer = setTimeout(() => reject(new Error(`TC_GPU_TIMEOUT: ${label} did not complete within ${timeoutMs} ms; no click or key is required to continue.`)), timeoutMs);
32
+ }),
33
+ ]);
34
+ }
35
+ finally {
36
+ if (timer !== undefined)
37
+ clearTimeout(timer);
38
+ }
39
+ }
40
+ export function runtimeStatus(data) {
41
+ const state = data.faulted
42
+ ? "faulted"
43
+ : data.playing
44
+ ? "running"
45
+ : data.automation
46
+ ? "paused · automation"
47
+ : "paused";
48
+ const backend = data.backend === "webgl2" ? "WebGL2" : data.backend === "uninitialized" ? "Initializing" : "WebGPU";
49
+ return `Pelican Engine · ${data.mode}\n${backend} · ${data.triangles} triangles · tick ${data.tick}\n${state}`;
50
+ }
@@ -0,0 +1,199 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { extractSurfaceScene, createSurfaceTextureCache, createSurfaceMeshCache, sampleSurfaceTexture, } from "./surface.js";
3
+ import { createTexturePixelProvider } from "./texture-pixels.js";
4
+ import { createVertexDeformer } from "./vertex-graphs.js";
5
+ /** One owner per rendered content generation; no mutations to borrowed scene/materials. */
6
+ export function createSurfaceCapture() {
7
+ const pixels = createTexturePixelProvider(), textureCache = createSurfaceTextureCache(), meshCache = createSurfaceMeshCache(), adapters = new Map();
8
+ let disposed = false;
9
+ const adapter = (material, knownColorGraphs, graphs) => {
10
+ const candidate = material;
11
+ if (!candidate.isNodeMaterial)
12
+ return undefined;
13
+ const physical = candidate
14
+ .isMeshPhysicalNodeMaterial === true;
15
+ if ((!candidate.isMeshStandardNodeMaterial ||
16
+ candidate.type !== "MeshStandardNodeMaterial") &&
17
+ (!physical || candidate.type !== "MeshPhysicalNodeMaterial"))
18
+ throw new Error("TC_TRACE_MATERIAL: Node material requires an explicit engine surface adapter.");
19
+ for (const [key, value] of Object.entries(candidate))
20
+ if (key.endsWith("Node") &&
21
+ value &&
22
+ typeof value === "object" &&
23
+ value.isNode) {
24
+ const binding = graphs?.get(material);
25
+ const supported = binding &&
26
+ {
27
+ colorNode: "baseColor",
28
+ emissiveNode: "emissive",
29
+ roughnessNode: "roughness",
30
+ metalnessNode: "metalness",
31
+ opacityNode: "opacity",
32
+ normalNode: "normalWorld",
33
+ }[key];
34
+ if (!(supported && binding.graphs[supported]) &&
35
+ !(key === "positionNode" && binding?.vertex) &&
36
+ (key !== "colorNode" || !knownColorGraphs.has(material)))
37
+ throw new Error("TC_TRACE_NODE_GRAPH: Unsupported node override " + key);
38
+ }
39
+ let copy = adapters.get(material);
40
+ if (!copy) {
41
+ copy = physical
42
+ ? new THREE.MeshPhysicalMaterial()
43
+ : new THREE.MeshStandardMaterial();
44
+ adapters.set(material, copy);
45
+ }
46
+ if (copy instanceof THREE.MeshPhysicalMaterial)
47
+ copy.copy(candidate);
48
+ else
49
+ copy.copy(candidate);
50
+ // colorNode replaces materialColor; opacityNode replaces materialOpacity.
51
+ if (candidate.colorNode)
52
+ copy.map = null;
53
+ if (graphs?.get(material)?.graphs.opacity) {
54
+ copy.transparent = true;
55
+ copy.opacity = 1;
56
+ copy.alphaMap = null;
57
+ }
58
+ return copy;
59
+ };
60
+ return {
61
+ capture(scene, knownColorGraphs = new Set(), graphs, options = {}) {
62
+ if (disposed)
63
+ throw new Error("TC_TRACE_CAPTURE_DISPOSED");
64
+ scene.updateMatrixWorld(true);
65
+ const vertexDiagnostics = [];
66
+ const vertexTextures = new Map();
67
+ const hasVertex = graphs && [...graphs.values()].some(binding => binding.vertex);
68
+ const vertexDeformer = hasVertex ? createVertexDeformer(graphs, (binding, asset, uv) => {
69
+ const texture = binding.textures.get(asset);
70
+ if (!texture)
71
+ throw new Error("TC_VERTEX_TEXTURE: " + binding.id + ":" + asset);
72
+ let snapshot = vertexTextures.get(texture);
73
+ if (!snapshot) {
74
+ snapshot = textureCache.getSnapshot(texture, pixels.decode, vertexDiagnostics);
75
+ vertexTextures.set(texture, snapshot);
76
+ }
77
+ return sampleSurfaceTexture(snapshot, uv);
78
+ }) : undefined;
79
+ let snapshot = extractSurfaceScene(scene, {
80
+ textureCache,
81
+ meshCache,
82
+ decodedTextureProvider: pixels.decode,
83
+ materialAdapter: (material) => adapter(material, knownColorGraphs, graphs),
84
+ ...(vertexDeformer ? { vertexDeformer } : {}),
85
+ ...(options.geometryOnly ? { meshFilter: (mesh) => (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).some(material => graphs?.get(material)?.vertex) } : {}),
86
+ });
87
+ if (graphs?.size) {
88
+ const byUuid = new Map([...graphs].map(([material, binding]) => [material.uuid, binding]));
89
+ for (const triangle of snapshot.triangles) {
90
+ const binding = byUuid.get(triangle.materialUuid);
91
+ if (!binding)
92
+ continue;
93
+ for (const graph of [...Object.values(binding.graphs), ...Object.values(binding.vertex ?? {})])
94
+ for (const input of graph.requiredInputs ?? []) {
95
+ const attribute = input === "uv" ? "uv0" : input === "uv1" ? "uv1" : undefined;
96
+ if (attribute &&
97
+ triangle.vertices.some((vertex) => !vertex[attribute]))
98
+ throw Error("TC_GRAPH_UV: Material " +
99
+ binding.id +
100
+ " requires " +
101
+ input +
102
+ " on object " +
103
+ triangle.objectUuid +
104
+ " geometry " +
105
+ triangle.geometryUuid);
106
+ }
107
+ }
108
+ const diagnostics = [...snapshot.diagnostics, ...vertexDiagnostics], textures = [...snapshot.textures];
109
+ const present = new Set(textures.map((texture) => texture.uuid));
110
+ for (const binding of graphs.values())
111
+ for (const graph of [...Object.values(binding.graphs), ...Object.values(binding.vertex ?? {})])
112
+ for (const id of graph.textureIds ?? []) {
113
+ if (Object.hasOwn(graph.textureSlots ?? {}, id))
114
+ continue;
115
+ const texture = binding.textures.get(id);
116
+ if (!texture)
117
+ throw Error("TC_GRAPH_TEXTURE: " + binding.id + ":" + id);
118
+ if (present.has(texture.uuid))
119
+ continue;
120
+ textures.push(textureCache.getSnapshot(texture, pixels.decode, diagnostics));
121
+ present.add(texture.uuid);
122
+ }
123
+ snapshot = Object.freeze({
124
+ ...snapshot,
125
+ textures: Object.freeze(textures),
126
+ diagnostics: Object.freeze(diagnostics),
127
+ });
128
+ }
129
+ const lights = [];
130
+ const identities = [];
131
+ scene.traverseVisible((object) => {
132
+ if (options.geometryOnly)
133
+ return;
134
+ if (!(object instanceof THREE.Light))
135
+ return;
136
+ const color = object.color.clone().multiplyScalar(object.intensity), position = object.getWorldPosition(new THREE.Vector3());
137
+ if (object instanceof THREE.PointLight) {
138
+ lights.push(...position.toArray(), 0, ...color.toArray(), object.distance, 0, 0, 0, object.decay);
139
+ }
140
+ else if (object instanceof THREE.DirectionalLight) {
141
+ object.target.updateWorldMatrix(true, false);
142
+ const direction = position
143
+ .sub(object.target.getWorldPosition(new THREE.Vector3()))
144
+ .normalize();
145
+ lights.push(...direction.toArray(), 1, ...color.toArray(), 0, 0, 0, 0, 0);
146
+ }
147
+ else if (object instanceof THREE.HemisphereLight) {
148
+ const direction = position.normalize(), ground = object.groundColor
149
+ .clone()
150
+ .multiplyScalar(object.intensity);
151
+ lights.push(...direction.toArray(), 2, ...color.toArray(), 0, ...ground.toArray(), 0);
152
+ }
153
+ else if (object instanceof THREE.AmbientLight) {
154
+ lights.push(0, 1, 0, 3, ...color.toArray(), 0, 0, 0, 0, 0);
155
+ }
156
+ else
157
+ throw new Error("TC_TRACE_LIGHT: Unsupported light type " + object.type);
158
+ identities.push(object.uuid);
159
+ });
160
+ return {
161
+ surface: snapshot,
162
+ lights: new Float32Array(lights),
163
+ lightIds: identities,
164
+ environment: scene.background instanceof THREE.Color
165
+ ? scene.background.toArray()
166
+ : [0, 0, 0],
167
+ environmentTexture: scene.environment?.uuid ?? null,
168
+ };
169
+ },
170
+ inspect() {
171
+ return {
172
+ disposed,
173
+ adaptedMaterials: adapters.size,
174
+ pixels: pixels.inspect(),
175
+ textures: textureCache.inspect(),
176
+ meshes: meshCache.inspect(),
177
+ };
178
+ },
179
+ clear() {
180
+ for (const material of adapters.values())
181
+ material.dispose();
182
+ adapters.clear();
183
+ pixels.clear();
184
+ textureCache.clear();
185
+ meshCache.clear();
186
+ },
187
+ dispose() {
188
+ if (disposed)
189
+ return;
190
+ disposed = true;
191
+ for (const material of adapters.values())
192
+ material.dispose();
193
+ adapters.clear();
194
+ pixels.dispose();
195
+ textureCache.dispose();
196
+ meshCache.dispose();
197
+ },
198
+ };
199
+ }
@@ -0,0 +1,270 @@
1
+ import * as THREE from "three";
2
+ /** Private storage-buffer ABI, all fields occupy vec4-aligned float32 slots. */
3
+ export const SURFACE_GPU_ABI = 1;
4
+ export const SURFACE_TRIANGLE_FLOATS = 72;
5
+ export const SURFACE_MATERIAL_FLOATS = 32;
6
+ export const SURFACE_TEXTURE_FLOATS = 24;
7
+ const fail = (code, message) => {
8
+ throw new Error(code + ": " + message);
9
+ };
10
+ function finite(value, label) {
11
+ if (!Number.isFinite(value) || !Number.isFinite(Math.fround(value)))
12
+ fail("TC_SURFACE_GPU_NUMBER", label + " is not representable as float32.");
13
+ return value;
14
+ }
15
+ function indexMap(values) {
16
+ const result = new Map();
17
+ values.forEach((value, index) => {
18
+ if (result.has(value.uuid))
19
+ fail("TC_SURFACE_GPU_ID", "Duplicate content identity " + value.uuid);
20
+ result.set(value.uuid, index);
21
+ });
22
+ return result;
23
+ }
24
+ function reference(map, id) {
25
+ if (id === undefined)
26
+ return -1;
27
+ const index = map.get(id);
28
+ if (index === undefined)
29
+ fail("TC_SURFACE_GPU_REFERENCE", "Missing content " + id);
30
+ return index;
31
+ }
32
+ export function packSurfaceGeometry(snapshot, ordered = snapshot.triangles, volumeIds = new Map()) {
33
+ const materials = indexMap(snapshot.materials), textures = new Map(snapshot.textures.map((texture) => [texture.uuid, texture]));
34
+ const output = new Float32Array(ordered.length * SURFACE_TRIANGLE_FLOATS);
35
+ for (let index = 0; index < ordered.length; index++) {
36
+ const triangle = ordered[index], materialIndex = reference(materials, triangle.materialUuid), material = snapshot.materials[materialIndex], offset = index * SURFACE_TRIANGLE_FLOATS;
37
+ for (const textureId of Object.values(material.maps)) {
38
+ const texture = textures.get(textureId);
39
+ if (!texture)
40
+ fail("TC_SURFACE_GPU_REFERENCE", "Missing material texture " + textureId);
41
+ for (const vertex of triangle.vertices)
42
+ if ((texture.channel === 0 ? vertex.uv0 : vertex.uv1) === undefined)
43
+ fail("TC_SURFACE_GPU_UV", "Triangle is missing texture channel " + texture.channel);
44
+ }
45
+ const color = triangle.instanceColor ?? [1, 1, 1], tangent = triangle.vertices.every((vertex) => vertex.tangent !== undefined);
46
+ for (let vertex = 0; vertex < 3; vertex++) {
47
+ const data = triangle.vertices[vertex], positionW = vertex === 0 ? materialIndex : color[vertex - 1], normalW = vertex === 0
48
+ ? color[2]
49
+ : vertex === 1
50
+ ? material.vertexColors
51
+ ? 1
52
+ : 0
53
+ : tangent
54
+ ? 1
55
+ : 0;
56
+ output.set([...data.position, positionW], offset + vertex * 4);
57
+ output.set([...data.normal, normalW], offset + 12 + vertex * 4);
58
+ output.set([...(data.uv0 ?? [0, 0]), ...(data.uv1 ?? [0, 0])], offset + 24 + vertex * 4);
59
+ output.set(data.color ?? [1, 1, 1, 1], offset + 36 + vertex * 4);
60
+ output.set(data.tangent ?? [0, 0, 0, 0], offset + 48 + vertex * 4);
61
+ }
62
+ for (let column = 0; column < 3; column++)
63
+ output.set([...triangle.normalMatrix.slice(column * 3, column * 3 + 3), 0], offset + 60 + column * 4);
64
+ const m = triangle.normalMatrix;
65
+ const scales = [0, 3, 6].map((index) => Math.hypot(m[index], m[index + 1], m[index + 2]));
66
+ const normalized = m.map((value, index) => value / scales[Math.floor(index / 3)]);
67
+ const determinant = normalized[0] *
68
+ (normalized[4] * normalized[8] - normalized[5] * normalized[7]) -
69
+ normalized[3] *
70
+ (normalized[1] * normalized[8] - normalized[2] * normalized[7]) +
71
+ normalized[6] *
72
+ (normalized[1] * normalized[5] - normalized[2] * normalized[4]);
73
+ if (!Number.isFinite(determinant) || determinant === 0)
74
+ fail("TC_SURFACE_GPU_NORMAL_MATRIX", "Singular normal matrix.");
75
+ output[offset + 63] = determinant > 0 ? 1 : -1;
76
+ output[offset + 67] =
77
+ volumeIds.get(JSON.stringify([triangle.objectUuid, triangle.instanceId])) ?? 0;
78
+ }
79
+ for (const value of output)
80
+ finite(value, "Triangle data");
81
+ return output;
82
+ }
83
+ export function packSurfaceResources(snapshot) {
84
+ const materialIds = indexMap(snapshot.materials), textureIds = indexMap(snapshot.textures), materials = new Float32Array(snapshot.materials.length * SURFACE_MATERIAL_FLOATS), textures = new Float32Array(snapshot.textures.length * SURFACE_TEXTURE_FLOATS);
85
+ let texelWords = 0;
86
+ for (const texture of snapshot.textures) {
87
+ if (!texture.pixels ||
88
+ texture.pixels.length !== texture.width * texture.height * 4)
89
+ fail("TC_SURFACE_GPU_PIXELS", "Missing decoded pixels for " + texture.uuid);
90
+ if (!Number.isSafeInteger(texture.width) ||
91
+ !Number.isSafeInteger(texture.height) ||
92
+ texture.width < 1 ||
93
+ texture.height < 1)
94
+ fail("TC_SURFACE_GPU_TEXTURE", "Invalid dimensions for " + texture.uuid);
95
+ texelWords +=
96
+ texture.width *
97
+ texture.height *
98
+ (texture.pixelEncoding === "float32" ? 4 : 1);
99
+ }
100
+ if (!Number.isSafeInteger(texelWords) || texelWords > 0x3fffffff)
101
+ fail("TC_SURFACE_GPU_TEXTURE", "Texture storage exceeds addressable buffer layout.");
102
+ const texels = new Uint32Array(texelWords), floatBits = new Float32Array(texels.buffer), textureWords = new Uint32Array(textures.buffer);
103
+ let word = 0;
104
+ snapshot.textures.forEach((texture, index) => {
105
+ const float = texture.pixelEncoding === "float32", srgb = texture.colorSpace === THREE.SRGBColorSpace;
106
+ if (![
107
+ THREE.SRGBColorSpace,
108
+ THREE.LinearSRGBColorSpace,
109
+ THREE.NoColorSpace,
110
+ ].includes(texture.colorSpace))
111
+ fail("TC_SURFACE_GPU_COLOR_SPACE", "Unsupported color space " + texture.colorSpace);
112
+ const wrap = (mode) => mode === THREE.RepeatWrapping
113
+ ? 1
114
+ : mode === THREE.MirroredRepeatWrapping
115
+ ? 2
116
+ : mode === THREE.ClampToEdgeWrapping
117
+ ? 0
118
+ : fail("TC_SURFACE_GPU_WRAP", "Unsupported wrapping " + mode);
119
+ const nearest = [
120
+ THREE.NearestFilter,
121
+ THREE.NearestMipmapNearestFilter,
122
+ THREE.NearestMipmapLinearFilter,
123
+ ].includes(texture.magFilter), linear = [
124
+ THREE.LinearFilter,
125
+ THREE.LinearMipmapNearestFilter,
126
+ THREE.LinearMipmapLinearFilter,
127
+ ].includes(texture.magFilter);
128
+ if (!nearest && !linear)
129
+ fail("TC_SURFACE_GPU_FILTER", "Unsupported magnification filter " + texture.magFilter);
130
+ const offset = index * SURFACE_TEXTURE_FLOATS, m = texture.matrix;
131
+ // SurfaceTexture.pixels already uses uploaded texture row order. Never flip twice.
132
+ textures.set([
133
+ word,
134
+ texture.width,
135
+ texture.height,
136
+ texture.channel,
137
+ wrap(texture.wrapS),
138
+ wrap(texture.wrapT),
139
+ linear ? 1 : 0,
140
+ (float ? 1 : 0) | (srgb ? 2 : 0),
141
+ m[0],
142
+ m[1],
143
+ m[2],
144
+ 0,
145
+ m[3],
146
+ m[4],
147
+ m[5],
148
+ 0,
149
+ m[6],
150
+ m[7],
151
+ m[8],
152
+ 0,
153
+ texture.minFilter,
154
+ texture.anisotropy,
155
+ texture.generateMipmaps ? 1 : 0,
156
+ 0,
157
+ ], offset);
158
+ textureWords.set([
159
+ word,
160
+ texture.width,
161
+ texture.height,
162
+ texture.channel,
163
+ wrap(texture.wrapS),
164
+ wrap(texture.wrapT),
165
+ linear ? 1 : 0,
166
+ (float ? 1 : 0) | (srgb ? 2 : 0),
167
+ ], offset);
168
+ for (let pixel = 0; pixel < texture.width * texture.height; pixel++) {
169
+ if (float) {
170
+ for (let channel = 0; channel < 4; channel++)
171
+ floatBits[word++] = finite(texture.pixels[pixel * 4 + channel], "Float texture sample");
172
+ }
173
+ else {
174
+ let value = 0;
175
+ for (let channel = 0; channel < 4; channel++) {
176
+ const normalized = finite(texture.pixels[pixel * 4 + channel], "UNORM texture sample");
177
+ if (normalized < 0 || normalized > 1)
178
+ fail("TC_SURFACE_GPU_UNORM", "UNORM texture sample outside [0,1].");
179
+ value |= Math.round(normalized * 255) << (channel * 8);
180
+ }
181
+ texels[word++] = value >>> 0;
182
+ }
183
+ }
184
+ });
185
+ materials.set(packSurfaceMaterialTable(snapshot));
186
+ for (const value of [...materials, ...textures])
187
+ finite(value, "Resource data");
188
+ return {
189
+ abi: 1,
190
+ materials,
191
+ textures,
192
+ texels,
193
+ materialIds: [...materialIds.keys()],
194
+ textureIds: [...textureIds.keys()],
195
+ limitations: [
196
+ "Buffer ABI and exact source texels only; shader, BSDF and graphics acceptance require separate integration.",
197
+ "Texture metadata retains minification/mip parameters; this packer does not generate a mip chain.",
198
+ ],
199
+ };
200
+ }
201
+ /** Numeric reference for shader upload/readback tests; sRGB decoding precedes filtering. */
202
+ export function unpackSurfaceTexel(resources, textureIndex, x, y) {
203
+ if (!Number.isInteger(textureIndex) ||
204
+ textureIndex < 0 ||
205
+ textureIndex >= resources.textureIds.length)
206
+ return fail("TC_SURFACE_GPU_TEXEL", "Invalid texture index.");
207
+ const metadata = resources.textures.subarray(textureIndex * SURFACE_TEXTURE_FLOATS, (textureIndex + 1) * SURFACE_TEXTURE_FLOATS);
208
+ const words = new Uint32Array(resources.textures.buffer, resources.textures.byteOffset + textureIndex * SURFACE_TEXTURE_FLOATS * 4, metadata.length);
209
+ if (metadata.length !== SURFACE_TEXTURE_FLOATS ||
210
+ !Number.isInteger(x) ||
211
+ !Number.isInteger(y) ||
212
+ x < 0 ||
213
+ y < 0 ||
214
+ x >= words[1] ||
215
+ y >= words[2])
216
+ return fail("TC_SURFACE_GPU_TEXEL", "Invalid texture coordinates.");
217
+ const flags = words[7], offset = words[0] + (y * words[1] + x) * (flags & 1 ? 4 : 1);
218
+ const floats = new Float32Array(resources.texels.buffer, resources.texels.byteOffset, resources.texels.length);
219
+ const output = [0, 1, 2, 3].map((channel) => flags & 1
220
+ ? floats[offset + channel]
221
+ : ((resources.texels[offset] >>> (channel * 8)) & 255) / 255);
222
+ if (flags & 2)
223
+ for (let channel = 0; channel < 3; channel++) {
224
+ const value = output[channel];
225
+ output[channel] =
226
+ value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
227
+ }
228
+ return output;
229
+ }
230
+ export function packSurfaceMaterialTable(snapshot) {
231
+ const textureIds = indexMap(snapshot.textures), materials = new Float32Array(snapshot.materials.length * SURFACE_MATERIAL_FLOATS);
232
+ snapshot.materials.forEach((material, index) => {
233
+ const map = (slot) => reference(textureIds, material.maps[slot]);
234
+ materials.set([
235
+ ...material.baseColor,
236
+ material.roughness,
237
+ material.metalness,
238
+ material.alphaTest,
239
+ material.aoMapIntensity,
240
+ ...material.emissive,
241
+ material.emissiveIntensity,
242
+ map("map"),
243
+ map("alphaMap"),
244
+ map("normalMap"),
245
+ map("roughnessMap"),
246
+ map("metalnessMap"),
247
+ map("aoMap"),
248
+ map("emissiveMap"),
249
+ material.side === "double" ? 2 : material.side === "back" ? 1 : 0,
250
+ ...material.normalScale,
251
+ material.normalMapType === THREE.ObjectSpaceNormalMap ? 1 : 0,
252
+ material.alphaMode === "blend"
253
+ ? 2
254
+ : material.alphaMode === "mask"
255
+ ? 1
256
+ : 0,
257
+ material.transmission ?? 0,
258
+ material.ior ?? 1.5,
259
+ (material.transmission ?? 0) > 0
260
+ ? (material.attenuationDistance ?? 0)
261
+ : map("specularIntensityMap") + 1,
262
+ 1 - (material.specularIntensity ?? 1),
263
+ ...((material.transmission ?? 0) > 0 ? (material.attenuationColor ?? [1, 1, 1]) : (material.specularColor ?? [1, 1, 1])),
264
+ (material.transmission ?? 0) > 0 ? 0 : map("specularColorMap") + 1,
265
+ ], index * SURFACE_MATERIAL_FLOATS);
266
+ });
267
+ for (const value of materials)
268
+ finite(value, "Material data");
269
+ return materials;
270
+ }