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.
- package/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Content-owned cache; clear on unload, dispose when the content owner ends. */
|
|
3
|
+
export class SurfaceTextureCache {
|
|
4
|
+
entries = new Map();
|
|
5
|
+
disposed = false;
|
|
6
|
+
hits = 0;
|
|
7
|
+
/** @internal Extraction calls this after the material/texture validation. */
|
|
8
|
+
getSnapshot(texture, provider, diagnostics) {
|
|
9
|
+
if (this.disposed)
|
|
10
|
+
return fail("SURFACE_TEXTURE_CACHE_DISPOSED", "Create a new surface texture cache after dispose().", texture.uuid);
|
|
11
|
+
rejectExtensions(texture.userData, texture.uuid);
|
|
12
|
+
const image = texture.image;
|
|
13
|
+
const flags = texture;
|
|
14
|
+
const signature = [
|
|
15
|
+
texture.uuid,
|
|
16
|
+
texture.version,
|
|
17
|
+
texture.source,
|
|
18
|
+
texture.source.uuid,
|
|
19
|
+
texture.source.version,
|
|
20
|
+
image,
|
|
21
|
+
image?.width,
|
|
22
|
+
image?.height,
|
|
23
|
+
image?.naturalWidth,
|
|
24
|
+
image?.naturalHeight,
|
|
25
|
+
image?.currentSrc,
|
|
26
|
+
image?.src,
|
|
27
|
+
image?.complete,
|
|
28
|
+
texture.format,
|
|
29
|
+
texture.type,
|
|
30
|
+
texture.mapping,
|
|
31
|
+
texture.channel,
|
|
32
|
+
texture.colorSpace,
|
|
33
|
+
texture.flipY,
|
|
34
|
+
texture.wrapS,
|
|
35
|
+
texture.wrapT,
|
|
36
|
+
texture.magFilter,
|
|
37
|
+
texture.minFilter,
|
|
38
|
+
texture.anisotropy,
|
|
39
|
+
texture.generateMipmaps,
|
|
40
|
+
texture.premultiplyAlpha,
|
|
41
|
+
texture.mipmaps.length,
|
|
42
|
+
texture.isRenderTargetTexture,
|
|
43
|
+
texture.rotation,
|
|
44
|
+
...texture.offset.toArray(),
|
|
45
|
+
...texture.repeat.toArray(),
|
|
46
|
+
...texture.center.toArray(),
|
|
47
|
+
...textureMatrix(texture),
|
|
48
|
+
provider,
|
|
49
|
+
...[
|
|
50
|
+
"isDataTexture",
|
|
51
|
+
"isCompressedTexture",
|
|
52
|
+
"isDataArrayTexture",
|
|
53
|
+
"isData3DTexture",
|
|
54
|
+
"isVideoTexture",
|
|
55
|
+
"isFramebufferTexture",
|
|
56
|
+
].map((key) => flags[key]),
|
|
57
|
+
];
|
|
58
|
+
const cached = this.entries.get(texture);
|
|
59
|
+
if (cached &&
|
|
60
|
+
signature.length === cached.signature.length &&
|
|
61
|
+
signature.every((value, i) => Object.is(value, cached.signature[i]))) {
|
|
62
|
+
this.hits++;
|
|
63
|
+
diagnostics.push(...cached.diagnostics);
|
|
64
|
+
return cached.snapshot;
|
|
65
|
+
}
|
|
66
|
+
const local = [];
|
|
67
|
+
const snapshot = freezeDeep(snapshotTexture(texture, provider, local));
|
|
68
|
+
diagnostics.push(...local);
|
|
69
|
+
// Missing pixels may become available asynchronously without needsUpdate.
|
|
70
|
+
if (snapshot.pixels)
|
|
71
|
+
this.entries.set(texture, {
|
|
72
|
+
signature,
|
|
73
|
+
snapshot,
|
|
74
|
+
diagnostics: freezeDeep(local),
|
|
75
|
+
});
|
|
76
|
+
else
|
|
77
|
+
this.entries.delete(texture);
|
|
78
|
+
return snapshot;
|
|
79
|
+
}
|
|
80
|
+
inspect() {
|
|
81
|
+
return {
|
|
82
|
+
disposed: this.disposed,
|
|
83
|
+
entries: this.entries.size,
|
|
84
|
+
hits: this.hits,
|
|
85
|
+
texels: Array.from(this.entries.values()).reduce((n, entry) => n + entry.snapshot.width * entry.snapshot.height, 0),
|
|
86
|
+
capacity: "host; no fixed resource limit",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
clear() {
|
|
90
|
+
this.entries.clear();
|
|
91
|
+
this.hits = 0;
|
|
92
|
+
}
|
|
93
|
+
dispose() {
|
|
94
|
+
this.clear();
|
|
95
|
+
this.disposed = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function createSurfaceTextureCache() {
|
|
99
|
+
return new SurfaceTextureCache();
|
|
100
|
+
}
|
|
101
|
+
/** Explicit GPU upload packing: R in bits 0..7, G 8..15, B 16..23, A 24..31.
|
|
102
|
+
* Own the returned buffer alongside the frozen snapshot; no float32 staging copy.
|
|
103
|
+
*/
|
|
104
|
+
export function packSurfaceTextureRGBA8(texture) {
|
|
105
|
+
if (!texture.pixels || texture.pixelEncoding !== "unorm8")
|
|
106
|
+
return fail("SURFACE_TEXTURE_PACK_UNSUPPORTED", "RGBA8 packing requires decoded unorm8 pixels; HDR needs a separate packing format.", texture.uuid);
|
|
107
|
+
const packed = new Uint32Array(texture.width * texture.height);
|
|
108
|
+
for (let i = 0; i < packed.length; i++) {
|
|
109
|
+
const p = i * 4;
|
|
110
|
+
packed[i] =
|
|
111
|
+
Math.round(texture.pixels[p] * 255) |
|
|
112
|
+
(Math.round(texture.pixels[p + 1] * 255) << 8) |
|
|
113
|
+
(Math.round(texture.pixels[p + 2] * 255) << 16) |
|
|
114
|
+
(Math.round(texture.pixels[p + 3] * 255) << 24);
|
|
115
|
+
}
|
|
116
|
+
return packed;
|
|
117
|
+
}
|
|
118
|
+
/** Content-owned world-space triangle cache. Material records are never cached. */
|
|
119
|
+
export class SurfaceMeshCache {
|
|
120
|
+
entries = new Map();
|
|
121
|
+
disposed = false;
|
|
122
|
+
hits = 0;
|
|
123
|
+
misses = 0;
|
|
124
|
+
/** @internal Also called for empty scenes so disposed options fail early. */
|
|
125
|
+
assertActive() {
|
|
126
|
+
if (this.disposed)
|
|
127
|
+
fail("SURFACE_MESH_CACHE_DISPOSED", "Create a new surface mesh cache after dispose().");
|
|
128
|
+
}
|
|
129
|
+
/** @internal The signature owns copied scalar state, not borrowed arrays. */
|
|
130
|
+
get(mesh, signature) {
|
|
131
|
+
this.assertActive();
|
|
132
|
+
const entry = this.entries.get(mesh);
|
|
133
|
+
if (entry &&
|
|
134
|
+
signature.length === entry.signature.length &&
|
|
135
|
+
signature.every((v, i) => Object.is(v, entry.signature[i]))) {
|
|
136
|
+
this.hits++;
|
|
137
|
+
return entry.triangles;
|
|
138
|
+
}
|
|
139
|
+
this.misses++;
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
/** @internal Publish only successfully extracted, deeply frozen data. */
|
|
143
|
+
set(mesh, signature, triangles) {
|
|
144
|
+
this.assertActive();
|
|
145
|
+
this.entries.set(mesh, { signature, triangles: freezeDeep(triangles) });
|
|
146
|
+
}
|
|
147
|
+
inspect() {
|
|
148
|
+
return {
|
|
149
|
+
disposed: this.disposed,
|
|
150
|
+
meshes: this.entries.size,
|
|
151
|
+
triangles: Array.from(this.entries.values()).reduce((n, e) => n + e.triangles.length, 0),
|
|
152
|
+
hits: this.hits,
|
|
153
|
+
misses: this.misses,
|
|
154
|
+
capacity: "host; no fixed resource limit",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
clear() {
|
|
158
|
+
this.entries.clear();
|
|
159
|
+
this.hits = this.misses = 0;
|
|
160
|
+
}
|
|
161
|
+
dispose() {
|
|
162
|
+
this.clear();
|
|
163
|
+
this.disposed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function createSurfaceMeshCache() {
|
|
167
|
+
return new SurfaceMeshCache();
|
|
168
|
+
}
|
|
169
|
+
export class SurfaceExtractionError extends Error {
|
|
170
|
+
code;
|
|
171
|
+
identity;
|
|
172
|
+
constructor(code, message, identity) {
|
|
173
|
+
super(`${code}${identity ? ` [${identity}]` : ""}: ${message}`);
|
|
174
|
+
this.code = code;
|
|
175
|
+
this.identity = identity;
|
|
176
|
+
this.name = "SurfaceExtractionError";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const fail = (code, message, identity) => {
|
|
180
|
+
throw new SurfaceExtractionError(code, message, identity);
|
|
181
|
+
};
|
|
182
|
+
const finite = (n, id, field) => Number.isFinite(n)
|
|
183
|
+
? n
|
|
184
|
+
: fail("SURFACE_NON_FINITE", `${field} is not finite`, id);
|
|
185
|
+
const vec = (v) => [v.x, v.y, v.z];
|
|
186
|
+
const freezeDeep = (value) => {
|
|
187
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
188
|
+
for (const child of Object.values(value))
|
|
189
|
+
freezeDeep(child);
|
|
190
|
+
Object.freeze(value);
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
};
|
|
194
|
+
const srgbToLinear = (v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
195
|
+
const finiteVector = (v, id, field) => {
|
|
196
|
+
finite(v.x, id, `${field}.x`);
|
|
197
|
+
finite(v.y, id, `${field}.y`);
|
|
198
|
+
finite(v.z, id, `${field}.z`);
|
|
199
|
+
};
|
|
200
|
+
const isNode = (v) => !!v && typeof v === "object" && v.isNode === true;
|
|
201
|
+
function localMatrix(o) {
|
|
202
|
+
return o.matrixAutoUpdate
|
|
203
|
+
? new THREE.Matrix4().compose(o.position, o.quaternion, o.scale)
|
|
204
|
+
: o.matrix.clone();
|
|
205
|
+
}
|
|
206
|
+
function validateAffine(matrix, identity) {
|
|
207
|
+
const e = matrix.elements;
|
|
208
|
+
for (const n of e)
|
|
209
|
+
finite(n, identity, "transform matrix");
|
|
210
|
+
if (e[3] !== 0 || e[7] !== 0 || e[11] !== 0 || e[15] !== 1)
|
|
211
|
+
fail("SURFACE_TRANSFORM_UNSUPPORTED", "Surface transforms must be affine matrices", identity);
|
|
212
|
+
}
|
|
213
|
+
function worldMatrix(o, cache) {
|
|
214
|
+
const old = cache.get(o);
|
|
215
|
+
if (old)
|
|
216
|
+
return old;
|
|
217
|
+
const result = o.matrixWorldAutoUpdate
|
|
218
|
+
? o.parent
|
|
219
|
+
? worldMatrix(o.parent, cache).clone().multiply(localMatrix(o))
|
|
220
|
+
: localMatrix(o)
|
|
221
|
+
: o.matrixWorld.clone();
|
|
222
|
+
validateAffine(result, o.uuid);
|
|
223
|
+
cache.set(o, result);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function rejectExtensions(userData, identity) {
|
|
227
|
+
const extensions = userData.gltfExtensions;
|
|
228
|
+
if (extensions && typeof extensions === "object")
|
|
229
|
+
for (const key of Object.keys(extensions)) {
|
|
230
|
+
if (key !== "KHR_texture_transform" &&
|
|
231
|
+
key !== "KHR_materials_emissive_strength")
|
|
232
|
+
fail("SURFACE_EXTENSION_UNSUPPORTED", "Extension " +
|
|
233
|
+
key +
|
|
234
|
+
" has no surface representation; remove it or implement an adapter", identity);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function textureMatrix(t) {
|
|
238
|
+
const result = (t.matrixAutoUpdate
|
|
239
|
+
? new THREE.Matrix3().setUvTransform(t.offset.x, t.offset.y, t.repeat.x, t.repeat.y, t.rotation, t.center.x, t.center.y)
|
|
240
|
+
: t.matrix).elements.slice();
|
|
241
|
+
for (const value of result)
|
|
242
|
+
finite(value, t.uuid, "texture matrix");
|
|
243
|
+
if (result[2] !== 0 || result[5] !== 0 || result[8] !== 1)
|
|
244
|
+
fail("SURFACE_UV_TRANSFORM_INVALID", "Use an affine UV matrix; Three samples matrix * vec3(uv, 1) without projective division", t.uuid);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
function channelsFor(format) {
|
|
248
|
+
if (format === THREE.RedFormat)
|
|
249
|
+
return 1;
|
|
250
|
+
if (format === THREE.RGFormat)
|
|
251
|
+
return 2;
|
|
252
|
+
if (format === THREE.RGBFormat)
|
|
253
|
+
return 3;
|
|
254
|
+
if (format === THREE.RGBAFormat)
|
|
255
|
+
return 4;
|
|
256
|
+
return fail("SURFACE_TEXTURE_FORMAT_UNSUPPORTED", `Texture format ${format} is not CPU-sampleable`);
|
|
257
|
+
}
|
|
258
|
+
const capturedPixelArrays = new WeakSet();
|
|
259
|
+
/** Internal provenance: only dense numeric arrays validated by copyPixels qualify. */
|
|
260
|
+
export function isFrozenCapturedPixels(value) {
|
|
261
|
+
return (Array.isArray(value) &&
|
|
262
|
+
Object.isFrozen(value) &&
|
|
263
|
+
capturedPixelArrays.has(value));
|
|
264
|
+
}
|
|
265
|
+
function copyPixels(width, height, channels, data, encoding, origin, flipY, id) {
|
|
266
|
+
if (![1, 2, 3, 4].includes(channels) ||
|
|
267
|
+
(encoding !== "unorm8" && encoding !== "float32") ||
|
|
268
|
+
(origin !== "texture" && origin !== "image"))
|
|
269
|
+
fail("SURFACE_TEXTURE_DATA_INVALID", "Decoded pixels require channels 1..4, an explicit encoding and row origin", id);
|
|
270
|
+
if (!Number.isInteger(width) ||
|
|
271
|
+
width < 1 ||
|
|
272
|
+
!Number.isInteger(height) ||
|
|
273
|
+
height < 1)
|
|
274
|
+
fail("SURFACE_TEXTURE_SIZE_INVALID", "Texture dimensions must be positive integers", id);
|
|
275
|
+
if (data.length !== width * height * channels)
|
|
276
|
+
fail("SURFACE_TEXTURE_DATA_INVALID", "Pixel length differs from dimensions and channel count", id);
|
|
277
|
+
const out = new Array(width * height * 4);
|
|
278
|
+
for (let y = 0; y < height; y++)
|
|
279
|
+
for (let x = 0; x < width; x++) {
|
|
280
|
+
const sy = origin === "image" && flipY ? height - 1 - y : y, si = (sy * width + x) * channels, di = (y * width + x) * 4;
|
|
281
|
+
for (let c = 0; c < 4; c++) {
|
|
282
|
+
const raw = c < channels
|
|
283
|
+
? data[si + c]
|
|
284
|
+
: c === 3
|
|
285
|
+
? encoding === "unorm8"
|
|
286
|
+
? 255
|
|
287
|
+
: 1
|
|
288
|
+
: 0;
|
|
289
|
+
out[di + c] = finite(encoding === "unorm8" ? raw / 255 : raw, id, "texture pixel");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
capturedPixelArrays.add(out);
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function snapshotTexture(t, provider, diagnostics) {
|
|
296
|
+
rejectExtensions(t.userData, t.uuid);
|
|
297
|
+
const image = t.image;
|
|
298
|
+
let width = image?.width ?? 0, height = image?.height ?? 0, pixels, pixelEncoding, pixelOrigin;
|
|
299
|
+
try {
|
|
300
|
+
const channels = channelsFor(t.format);
|
|
301
|
+
if (t.mipmaps.length > 0)
|
|
302
|
+
fail("SURFACE_TEXTURE_TYPE_UNSUPPORTED", "Custom mip chains require a future adapter; the CPU reference cannot substitute image for supplied mip zero", t.uuid);
|
|
303
|
+
if (t.type !== THREE.UnsignedByteType && t.type !== THREE.FloatType)
|
|
304
|
+
fail("SURFACE_TEXTURE_TYPE_UNSUPPORTED", "Only unsigned-byte and float32 texture types have a CPU reference", t.uuid);
|
|
305
|
+
const textureFlags = t;
|
|
306
|
+
if (textureFlags.isCompressedTexture ||
|
|
307
|
+
textureFlags.isDataArrayTexture ||
|
|
308
|
+
textureFlags.isData3DTexture ||
|
|
309
|
+
textureFlags.isVideoTexture ||
|
|
310
|
+
textureFlags.isFramebufferTexture ||
|
|
311
|
+
t.isRenderTargetTexture ||
|
|
312
|
+
t.premultiplyAlpha)
|
|
313
|
+
fail("SURFACE_TEXTURE_TYPE_UNSUPPORTED", "Dynamic/array/compressed/render-target/premultiplied textures require an explicit future adapter", t.uuid);
|
|
314
|
+
if (t.isDataTexture === true &&
|
|
315
|
+
ArrayBuffer.isView(image?.data)) {
|
|
316
|
+
const source = image.data, bytes = image.data instanceof Uint8Array ||
|
|
317
|
+
image.data instanceof Uint8ClampedArray;
|
|
318
|
+
if (!bytes && !(image.data instanceof Float32Array))
|
|
319
|
+
fail("SURFACE_TEXTURE_TYPE_UNSUPPORTED", "Only unsigned-byte and float32 DataTexture pixels are decoded", t.uuid);
|
|
320
|
+
if ((bytes && t.type !== THREE.UnsignedByteType) ||
|
|
321
|
+
(!bytes && t.type !== THREE.FloatType))
|
|
322
|
+
fail("SURFACE_TEXTURE_DATA_INVALID", "DataTexture typed array does not match its pixel type", t.uuid);
|
|
323
|
+
pixelEncoding = bytes ? "unorm8" : "float32";
|
|
324
|
+
pixelOrigin = "image";
|
|
325
|
+
pixels = copyPixels(width, height, channels, source, pixelEncoding, pixelOrigin, t.flipY, t.uuid);
|
|
326
|
+
}
|
|
327
|
+
else if (provider) {
|
|
328
|
+
const decoded = provider(t);
|
|
329
|
+
if (decoded) {
|
|
330
|
+
if ((decoded.encoding === "unorm8" &&
|
|
331
|
+
!(decoded.data instanceof Uint8Array ||
|
|
332
|
+
decoded.data instanceof Uint8ClampedArray)) ||
|
|
333
|
+
(decoded.encoding === "float32" &&
|
|
334
|
+
!(decoded.data instanceof Float32Array)))
|
|
335
|
+
fail("SURFACE_TEXTURE_DATA_INVALID", "Provider pixel array does not match encoding", t.uuid);
|
|
336
|
+
width = decoded.width;
|
|
337
|
+
height = decoded.height;
|
|
338
|
+
pixelEncoding = decoded.encoding;
|
|
339
|
+
pixelOrigin = decoded.origin;
|
|
340
|
+
pixels = copyPixels(width, height, decoded.channels, decoded.data, pixelEncoding, pixelOrigin, t.flipY, t.uuid);
|
|
341
|
+
}
|
|
342
|
+
else
|
|
343
|
+
diagnostics.push({
|
|
344
|
+
code: "SURFACE_TEXTURE_PIXELS_UNAVAILABLE",
|
|
345
|
+
message: "Decoded provider returned no pixels; texture identity is retained.",
|
|
346
|
+
textureUuid: t.uuid,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
else
|
|
350
|
+
diagnostics.push({
|
|
351
|
+
code: "SURFACE_TEXTURE_PIXELS_UNAVAILABLE",
|
|
352
|
+
message: "CPU pixels unavailable; texture identity is retained.",
|
|
353
|
+
textureUuid: t.uuid,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error instanceof SurfaceExtractionError &&
|
|
358
|
+
(error.code === "SURFACE_TEXTURE_TYPE_UNSUPPORTED" ||
|
|
359
|
+
error.code === "SURFACE_TEXTURE_FORMAT_UNSUPPORTED"))
|
|
360
|
+
diagnostics.push({
|
|
361
|
+
code: error.code,
|
|
362
|
+
message: error.message,
|
|
363
|
+
textureUuid: t.uuid,
|
|
364
|
+
});
|
|
365
|
+
else
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
for (const n of [
|
|
369
|
+
...textureMatrix(t),
|
|
370
|
+
t.rotation,
|
|
371
|
+
...t.offset.toArray(),
|
|
372
|
+
...t.repeat.toArray(),
|
|
373
|
+
...t.center.toArray(),
|
|
374
|
+
t.anisotropy,
|
|
375
|
+
width,
|
|
376
|
+
height,
|
|
377
|
+
])
|
|
378
|
+
finite(n, t.uuid, "texture metadata");
|
|
379
|
+
return {
|
|
380
|
+
uuid: t.uuid,
|
|
381
|
+
sourceUuid: t.source.uuid,
|
|
382
|
+
width,
|
|
383
|
+
height,
|
|
384
|
+
format: t.format,
|
|
385
|
+
type: t.type,
|
|
386
|
+
mapping: t.mapping,
|
|
387
|
+
channel: t.channel,
|
|
388
|
+
colorSpace: t.colorSpace,
|
|
389
|
+
flipY: t.flipY,
|
|
390
|
+
wrapS: t.wrapS,
|
|
391
|
+
wrapT: t.wrapT,
|
|
392
|
+
magFilter: t.magFilter,
|
|
393
|
+
minFilter: t.minFilter,
|
|
394
|
+
anisotropy: t.anisotropy,
|
|
395
|
+
generateMipmaps: t.generateMipmaps,
|
|
396
|
+
offset: [t.offset.x, t.offset.y],
|
|
397
|
+
repeat: [t.repeat.x, t.repeat.y],
|
|
398
|
+
center: [t.center.x, t.center.y],
|
|
399
|
+
rotation: t.rotation,
|
|
400
|
+
matrix: textureMatrix(t),
|
|
401
|
+
// Freeze numeric storage directly: freezeDeep must not allocate Object.values
|
|
402
|
+
// for millions of texels on the first capture either.
|
|
403
|
+
...(pixels
|
|
404
|
+
? { pixels: Object.freeze(pixels), pixelEncoding, pixelOrigin }
|
|
405
|
+
: {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const unsupportedMaps = [
|
|
409
|
+
"lightMap",
|
|
410
|
+
"bumpMap",
|
|
411
|
+
"displacementMap",
|
|
412
|
+
"envMap",
|
|
413
|
+
"specularMap",
|
|
414
|
+
"gradientMap",
|
|
415
|
+
];
|
|
416
|
+
const physicalExtensions = [
|
|
417
|
+
"clearcoat",
|
|
418
|
+
"iridescence",
|
|
419
|
+
"sheen",
|
|
420
|
+
"thickness",
|
|
421
|
+
"anisotropy",
|
|
422
|
+
"dispersion",
|
|
423
|
+
"diffuseRoughness",
|
|
424
|
+
"retroreflectivity",
|
|
425
|
+
];
|
|
426
|
+
function snapshotMaterial(m, materials, byMaterial, textures, byTexture, options, diagnostics, original) {
|
|
427
|
+
const cached = byMaterial.get(original);
|
|
428
|
+
if (cached)
|
|
429
|
+
return cached;
|
|
430
|
+
const flags = m;
|
|
431
|
+
if (m.isNodeMaterial)
|
|
432
|
+
fail("SURFACE_NODE_GRAPH_UNSUPPORTED", "Node materials require a graph-aware surface adapter", m.uuid);
|
|
433
|
+
if (!flags.isMeshStandardMaterial && !flags.isMeshPhysicalMaterial)
|
|
434
|
+
return fail("SURFACE_MATERIAL_UNSUPPORTED", `Material ${m.type} is not standard/physical PBR`, m.uuid);
|
|
435
|
+
if (m.type !== "MeshStandardMaterial" && m.type !== "MeshPhysicalMaterial")
|
|
436
|
+
return fail("SURFACE_MATERIAL_CUSTOM_SUBCLASS", `Custom material ${m.type} is unsupported`, m.uuid);
|
|
437
|
+
if (m.onBeforeCompile.toString() !==
|
|
438
|
+
THREE.Material.prototype.onBeforeCompile.toString() ||
|
|
439
|
+
m.customProgramCacheKey() !==
|
|
440
|
+
THREE.Material.prototype.customProgramCacheKey.call(m))
|
|
441
|
+
fail("SURFACE_CUSTOM_SHADER_UNSUPPORTED", "Custom shader hooks are unsupported", m.uuid);
|
|
442
|
+
for (const [key, value] of Object.entries(m))
|
|
443
|
+
if (key.endsWith("Node") && isNode(value))
|
|
444
|
+
fail("SURFACE_NODE_GRAPH_UNSUPPORTED", `Node property ${key} is unsupported`, m.uuid);
|
|
445
|
+
rejectExtensions(m.userData, m.uuid);
|
|
446
|
+
if (m.constructor !== THREE.MeshStandardMaterial &&
|
|
447
|
+
m.constructor !== THREE.MeshPhysicalMaterial)
|
|
448
|
+
fail("SURFACE_MATERIAL_CUSTOM_SUBCLASS", "Custom material subclasses require an explicit surface adapter", m.uuid);
|
|
449
|
+
const s = m;
|
|
450
|
+
if (s.wireframe)
|
|
451
|
+
fail("SURFACE_MATERIAL_MODE_UNSUPPORTED", "Wireframe materials are not triangle surfaces", m.uuid);
|
|
452
|
+
if (m.side !== THREE.FrontSide &&
|
|
453
|
+
m.side !== THREE.BackSide &&
|
|
454
|
+
m.side !== THREE.DoubleSide)
|
|
455
|
+
fail("SURFACE_MATERIAL_SIDE_INVALID", "Unknown side value", m.uuid);
|
|
456
|
+
for (const slot of unsupportedMaps)
|
|
457
|
+
if (s[slot])
|
|
458
|
+
fail("SURFACE_MATERIAL_MAP_UNSUPPORTED", `${slot} is not sampled`, m.uuid);
|
|
459
|
+
const physicalMaterial = s
|
|
460
|
+
.isMeshPhysicalMaterial === true;
|
|
461
|
+
if (physicalMaterial) {
|
|
462
|
+
const p = s;
|
|
463
|
+
if (!Number.isFinite(p.transmission) ||
|
|
464
|
+
p.transmission < 0 ||
|
|
465
|
+
p.transmission > 1 ||
|
|
466
|
+
!Number.isFinite(p.ior) ||
|
|
467
|
+
p.ior <= 0)
|
|
468
|
+
fail("SURFACE_PHYSICAL_VALUE_INVALID", "Transmission and IOR must be finite and in their supported domains.", m.uuid);
|
|
469
|
+
if (p.transmission > 0 && p.metalness !== 0)
|
|
470
|
+
fail("SURFACE_PHYSICAL_VALUE_INVALID", "Dielectric volume transmission requires metalness 0.", m.uuid);
|
|
471
|
+
if (p.transmission > 0 &&
|
|
472
|
+
((!Number.isFinite(p.attenuationDistance) &&
|
|
473
|
+
p.attenuationDistance !== Infinity) ||
|
|
474
|
+
p.attenuationDistance <= 0))
|
|
475
|
+
fail("SURFACE_PHYSICAL_VALUE_INVALID", "Attenuation distance must be positive or Infinity.", m.uuid);
|
|
476
|
+
for (const key of physicalExtensions)
|
|
477
|
+
if (p[key] !== 0)
|
|
478
|
+
fail("SURFACE_PHYSICAL_EXTENSION_UNSUPPORTED", `Active ${key} extension is unsupported`, m.uuid);
|
|
479
|
+
if (!Number.isFinite(p.specularIntensity) || p.specularIntensity < 0 || p.specularIntensity > 1 ||
|
|
480
|
+
[p.specularColor.r, p.specularColor.g, p.specularColor.b].some(value => !Number.isFinite(value) || value < 0 || value > 1))
|
|
481
|
+
fail("SURFACE_PHYSICAL_VALUE_INVALID", "Specular intensity and linear RGB must be in [0,1].", m.uuid);
|
|
482
|
+
if (p.transmission > 0 && (p.specularIntensity !== 1 || p.specularColor.r !== 1 || p.specularColor.g !== 1 || p.specularColor.b !== 1))
|
|
483
|
+
fail("SURFACE_PHYSICAL_EXTENSION_UNSUPPORTED", "Non-default specular on transmissive media is unsupported", m.uuid);
|
|
484
|
+
if (p.transmission > 0 && (p.specularIntensityMap || p.specularColorMap))
|
|
485
|
+
fail("SURFACE_PHYSICAL_EXTENSION_UNSUPPORTED", "Specular texture maps on transmissive media are unsupported", m.uuid);
|
|
486
|
+
for (const key of [
|
|
487
|
+
"diffuseRoughnessMap",
|
|
488
|
+
"clearcoatMap",
|
|
489
|
+
"clearcoatRoughnessMap",
|
|
490
|
+
"clearcoatNormalMap",
|
|
491
|
+
"iridescenceMap",
|
|
492
|
+
"iridescenceThicknessMap",
|
|
493
|
+
"sheenColorMap",
|
|
494
|
+
"sheenRoughnessMap",
|
|
495
|
+
"transmissionMap",
|
|
496
|
+
"thicknessMap",
|
|
497
|
+
"anisotropyMap",
|
|
498
|
+
])
|
|
499
|
+
if (p[key])
|
|
500
|
+
fail("SURFACE_PHYSICAL_EXTENSION_UNSUPPORTED", `Active extension map ${key} is unsupported`, m.uuid);
|
|
501
|
+
}
|
|
502
|
+
if (!m.visible)
|
|
503
|
+
fail("SURFACE_MATERIAL_INVISIBLE", "Invisible material should be omitted before snapshotting", m.uuid);
|
|
504
|
+
if (m.blending !== THREE.NormalBlending ||
|
|
505
|
+
m.clipIntersection ||
|
|
506
|
+
m.clipShadows ||
|
|
507
|
+
m.clippingPlanes?.length ||
|
|
508
|
+
m
|
|
509
|
+
.alphaHash ||
|
|
510
|
+
m.alphaToCoverage ||
|
|
511
|
+
m.premultipliedAlpha)
|
|
512
|
+
fail("SURFACE_ALPHA_MODE_UNSUPPORTED", "Custom blending, clipping, alpha hash/coverage and premultiplied alpha are unsupported", m.uuid);
|
|
513
|
+
const maps = {};
|
|
514
|
+
for (const slot of [
|
|
515
|
+
"map",
|
|
516
|
+
"alphaMap",
|
|
517
|
+
"normalMap",
|
|
518
|
+
"metalnessMap",
|
|
519
|
+
"roughnessMap",
|
|
520
|
+
"aoMap",
|
|
521
|
+
"emissiveMap",
|
|
522
|
+
"specularIntensityMap",
|
|
523
|
+
"specularColorMap",
|
|
524
|
+
]) {
|
|
525
|
+
const t = s[slot];
|
|
526
|
+
if (!t)
|
|
527
|
+
continue;
|
|
528
|
+
if (t.mapping !== THREE.UVMapping)
|
|
529
|
+
fail("SURFACE_TEXTURE_MAPPING_UNSUPPORTED", "Only UVMapping is supported", t.uuid);
|
|
530
|
+
if (t.channel !== 0 && t.channel !== 1)
|
|
531
|
+
fail("SURFACE_UV_CHANNEL_UNSUPPORTED", `UV channel ${t.channel} is unsupported`, t.uuid);
|
|
532
|
+
if (slot === "specularColorMap") {
|
|
533
|
+
if (t.colorSpace !== THREE.SRGBColorSpace)
|
|
534
|
+
fail("SURFACE_COLOR_SPACE_UNSUPPORTED", `${slot} must use SRGBColorSpace`, t.uuid);
|
|
535
|
+
}
|
|
536
|
+
else if (slot === "map" || slot === "emissiveMap") {
|
|
537
|
+
if (![
|
|
538
|
+
THREE.NoColorSpace,
|
|
539
|
+
THREE.SRGBColorSpace,
|
|
540
|
+
THREE.LinearSRGBColorSpace,
|
|
541
|
+
].includes(t.colorSpace))
|
|
542
|
+
fail("SURFACE_COLOR_SPACE_UNSUPPORTED", `Unsupported color space ${t.colorSpace}`, t.uuid);
|
|
543
|
+
}
|
|
544
|
+
else if (t.colorSpace !== THREE.NoColorSpace)
|
|
545
|
+
fail("SURFACE_COLOR_SPACE_UNSUPPORTED", `${slot} must use NoColorSpace`, t.uuid);
|
|
546
|
+
let st = byTexture.get(t);
|
|
547
|
+
if (!st) {
|
|
548
|
+
st = options.textureCache
|
|
549
|
+
? options.textureCache.getSnapshot(t, options.decodedTextureProvider, diagnostics)
|
|
550
|
+
: snapshotTexture(t, options.decodedTextureProvider, diagnostics);
|
|
551
|
+
byTexture.set(t, st);
|
|
552
|
+
textures.push(st);
|
|
553
|
+
}
|
|
554
|
+
maps[slot] = st.uuid;
|
|
555
|
+
}
|
|
556
|
+
const c = s.color, material = {
|
|
557
|
+
// Preserve source identity without mutating/cloning the adapted material.
|
|
558
|
+
uuid: original.uuid,
|
|
559
|
+
name: m.name,
|
|
560
|
+
kind: physicalMaterial ? "physical" : "standard",
|
|
561
|
+
side: m.side === THREE.DoubleSide
|
|
562
|
+
? "double"
|
|
563
|
+
: m.side === THREE.BackSide
|
|
564
|
+
? "back"
|
|
565
|
+
: "front",
|
|
566
|
+
vertexColors: s.vertexColors,
|
|
567
|
+
flatShading: s.flatShading,
|
|
568
|
+
baseColor: [c.r, c.g, c.b, s.opacity],
|
|
569
|
+
alphaMode: m.transparent ? "blend" : m.alphaTest > 0 ? "mask" : "opaque",
|
|
570
|
+
alphaTest: m.alphaTest,
|
|
571
|
+
roughness: s.roughness,
|
|
572
|
+
metalness: s.metalness,
|
|
573
|
+
normalScale: [s.normalScale.x, s.normalScale.y],
|
|
574
|
+
normalMapType: s.normalMapType,
|
|
575
|
+
aoMapIntensity: s.aoMapIntensity,
|
|
576
|
+
emissive: [s.emissive.r, s.emissive.g, s.emissive.b],
|
|
577
|
+
emissiveIntensity: s.emissiveIntensity,
|
|
578
|
+
...(physicalMaterial
|
|
579
|
+
? {
|
|
580
|
+
transmission: s.transmission,
|
|
581
|
+
ior: s.ior,
|
|
582
|
+
specularIntensity: s.specularIntensity,
|
|
583
|
+
specularColor: s.specularColor.toArray(),
|
|
584
|
+
attenuationColor: s.attenuationColor.toArray(),
|
|
585
|
+
attenuationDistance: s.attenuationDistance === Infinity
|
|
586
|
+
? 0
|
|
587
|
+
: s.attenuationDistance,
|
|
588
|
+
}
|
|
589
|
+
: {}),
|
|
590
|
+
maps,
|
|
591
|
+
};
|
|
592
|
+
for (const n of [
|
|
593
|
+
...material.baseColor,
|
|
594
|
+
...material.normalScale,
|
|
595
|
+
material.roughness,
|
|
596
|
+
material.metalness,
|
|
597
|
+
material.alphaTest,
|
|
598
|
+
material.aoMapIntensity,
|
|
599
|
+
...material.emissive,
|
|
600
|
+
material.emissiveIntensity,
|
|
601
|
+
...(physicalMaterial
|
|
602
|
+
? [
|
|
603
|
+
material.transmission,
|
|
604
|
+
material.ior,
|
|
605
|
+
material.specularIntensity,
|
|
606
|
+
...material.specularColor,
|
|
607
|
+
...material.attenuationColor,
|
|
608
|
+
material.attenuationDistance,
|
|
609
|
+
]
|
|
610
|
+
: []),
|
|
611
|
+
])
|
|
612
|
+
finite(n, m.uuid, "material factor");
|
|
613
|
+
if (material.attenuationColor?.some((value) => value < 0 || value > 1))
|
|
614
|
+
fail("SURFACE_PHYSICAL_VALUE_INVALID", "Attenuation color must be linear RGB in [0,1].", m.uuid);
|
|
615
|
+
byMaterial.set(original, material);
|
|
616
|
+
materials.push(material);
|
|
617
|
+
return material;
|
|
618
|
+
}
|
|
619
|
+
function morphed(geometry, name, index, weights, identity) {
|
|
620
|
+
const base = geometry.getAttribute(name);
|
|
621
|
+
if (index >= base.count)
|
|
622
|
+
fail("SURFACE_ATTRIBUTE_COUNT_INVALID", `${name} has fewer vertices than position`, identity);
|
|
623
|
+
const size = Math.min(base.itemSize, 4);
|
|
624
|
+
const baseValue = new THREE.Vector4(base.getX(index), size > 1 ? base.getY(index) : 0, size > 2 ? base.getZ(index) : 0, size > 3 ? base.getW(index) : 1);
|
|
625
|
+
const targets = geometry.morphAttributes[name];
|
|
626
|
+
if (!targets?.length)
|
|
627
|
+
return baseValue;
|
|
628
|
+
if (targets.length !== weights.length)
|
|
629
|
+
fail("SURFACE_MORPH_INFLUENCE_INVALID", `Morph ${name} target count does not match influences`, identity);
|
|
630
|
+
const baseInfluence = geometry.morphTargetsRelative
|
|
631
|
+
? 1
|
|
632
|
+
: 1 - weights.reduce((sum, w) => sum + w, 0);
|
|
633
|
+
const result = baseValue.clone().multiplyScalar(baseInfluence);
|
|
634
|
+
for (let i = 0; i < targets.length; i++) {
|
|
635
|
+
const weight = finite(weights[i], identity, "morph influence");
|
|
636
|
+
if (weight === 0)
|
|
637
|
+
continue;
|
|
638
|
+
const target = targets[i];
|
|
639
|
+
if (target.count !== base.count || target.itemSize !== base.itemSize)
|
|
640
|
+
fail("SURFACE_MORPH_ATTRIBUTE_INVALID", `Morph ${name} count/itemSize differs from base attribute`, identity);
|
|
641
|
+
const v = new THREE.Vector4(target.getX(index), target.itemSize > 1 ? target.getY(index) : 0, target.itemSize > 2 ? target.getZ(index) : 0, target.itemSize > 3
|
|
642
|
+
? target.getW(index)
|
|
643
|
+
: name === "color" && size > 3
|
|
644
|
+
? 0
|
|
645
|
+
: 1);
|
|
646
|
+
result.addScaledVector(v, weight);
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function getVertex(mesh, index, instance, normalMatrix, world, direction, material, cache, instanceIndex, vertexDeformer) {
|
|
651
|
+
const g = mesh.geometry, id = `${mesh.uuid}/${g.uuid}/${index}`;
|
|
652
|
+
if (mesh.isInstancedMesh &&
|
|
653
|
+
mesh.morphTexture)
|
|
654
|
+
fail("SURFACE_INSTANCE_MORPH_UNSUPPORTED", "Per-instance morph weights are unsupported", mesh.uuid);
|
|
655
|
+
const weights = mesh.morphTargetInfluences ?? [];
|
|
656
|
+
for (const weight of weights)
|
|
657
|
+
finite(weight, id, "morph influence");
|
|
658
|
+
const p4 = morphed(g, "position", index, weights, id), position = new THREE.Vector3(p4.x, p4.y, p4.z);
|
|
659
|
+
const normalAttribute = g.getAttribute("normal");
|
|
660
|
+
let normal = normalAttribute
|
|
661
|
+
? new THREE.Vector3(...morphed(g, "normal", index, weights, id).toArray().slice(0, 3))
|
|
662
|
+
: new THREE.Vector3();
|
|
663
|
+
const ta = g.getAttribute("tangent");
|
|
664
|
+
let tangent = ta
|
|
665
|
+
? new THREE.Vector4(ta.getX(index), ta.getY(index), ta.getZ(index), ta.itemSize > 3 ? ta.getW(index) : 1)
|
|
666
|
+
: undefined;
|
|
667
|
+
if (mesh.isSkinnedMesh === true) {
|
|
668
|
+
const skinned = mesh, si = g.getAttribute("skinIndex"), sw = g.getAttribute("skinWeight");
|
|
669
|
+
if (!si || !sw)
|
|
670
|
+
fail("SURFACE_SKIN_ATTRIBUTES_MISSING", "skinIndex and skinWeight are required", id);
|
|
671
|
+
validateAffine(skinned.bindMatrix, mesh.uuid);
|
|
672
|
+
if (skinned.bindMatrix.determinant() === 0)
|
|
673
|
+
fail("SURFACE_SINGULAR_TRANSFORM", "Skin bind matrix is singular", mesh.uuid);
|
|
674
|
+
let weightSum = 0;
|
|
675
|
+
for (let j = 0; j < 4; j++) {
|
|
676
|
+
const weight = finite(sw.getComponent(index, j), id, "skin weight");
|
|
677
|
+
if (weight < 0)
|
|
678
|
+
fail("SURFACE_SKIN_WEIGHT_INVALID", "Skin weights must be nonnegative and normalized", id);
|
|
679
|
+
weightSum += weight;
|
|
680
|
+
}
|
|
681
|
+
if (Math.abs(weightSum - 1) > 1e-5)
|
|
682
|
+
fail("SURFACE_SKIN_WEIGHT_INVALID", "Normalize skin weights before extraction", id);
|
|
683
|
+
const bindInverse = skinned.bindMode === "attached"
|
|
684
|
+
? world.clone().invert()
|
|
685
|
+
: skinned.bindMode === "detached"
|
|
686
|
+
? skinned.bindMatrix.clone().invert()
|
|
687
|
+
: fail("SURFACE_SKIN_BIND_MODE_UNSUPPORTED", `Unknown bind mode ${skinned.bindMode}`, mesh.uuid);
|
|
688
|
+
const weighted = new THREE.Matrix4().set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), weightedPosition = new THREE.Vector3(), bindPosition = position.clone().applyMatrix4(skinned.bindMatrix);
|
|
689
|
+
for (let j = 0; j < si.itemSize; j++) {
|
|
690
|
+
const weight = finite(sw.getComponent(index, j), id, "skin weight");
|
|
691
|
+
if (!weight)
|
|
692
|
+
continue;
|
|
693
|
+
const boneIndex = si.getComponent(index, j), bone = skinned.skeleton.bones[boneIndex], inverse = skinned.skeleton.boneInverses[boneIndex];
|
|
694
|
+
if (!bone || !inverse)
|
|
695
|
+
fail("SURFACE_SKIN_BONE_INVALID", `Invalid bone index ${boneIndex}`, id);
|
|
696
|
+
const boneMatrix = worldMatrix(bone, cache).clone().multiply(inverse);
|
|
697
|
+
validateAffine(boneMatrix, bone.uuid);
|
|
698
|
+
for (let k = 0; k < 16; k++)
|
|
699
|
+
weighted.elements[k] += boneMatrix.elements[k] * weight;
|
|
700
|
+
weightedPosition.addScaledVector(bindPosition.clone().applyMatrix4(boneMatrix), weight);
|
|
701
|
+
}
|
|
702
|
+
position.copy(weightedPosition.applyMatrix4(bindInverse));
|
|
703
|
+
const skin = new THREE.Matrix3()
|
|
704
|
+
.setFromMatrix4(bindInverse)
|
|
705
|
+
.multiply(new THREE.Matrix3().setFromMatrix4(weighted))
|
|
706
|
+
.multiply(new THREE.Matrix3().setFromMatrix4(skinned.bindMatrix));
|
|
707
|
+
if (normalAttribute)
|
|
708
|
+
normal.applyMatrix3(skin).normalize();
|
|
709
|
+
if (tangent) {
|
|
710
|
+
const t = new THREE.Vector3(tangent.x, tangent.y, tangent.z)
|
|
711
|
+
.applyMatrix3(skin)
|
|
712
|
+
.normalize();
|
|
713
|
+
tangent.set(t.x, t.y, t.z, tangent.w);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const read = (name, minimum) => {
|
|
717
|
+
const a = g.getAttribute(name);
|
|
718
|
+
if (!a)
|
|
719
|
+
return undefined;
|
|
720
|
+
if (index >= a.count)
|
|
721
|
+
fail("SURFACE_ATTRIBUTE_COUNT_INVALID", `${name} has fewer vertices than position`, id);
|
|
722
|
+
if (a.itemSize < minimum)
|
|
723
|
+
fail("SURFACE_ATTRIBUTE_SIZE_INVALID", `${name} itemSize < ${minimum}`, id);
|
|
724
|
+
return Array.from({ length: a.itemSize > 4 ? 4 : Math.max(minimum, a.itemSize) }, (_, j) => finite(a.getComponent(index, j), id, name));
|
|
725
|
+
};
|
|
726
|
+
let uv0, uv1;
|
|
727
|
+
position.applyMatrix4(instance);
|
|
728
|
+
let normalOverridden = false;
|
|
729
|
+
if (vertexDeformer) {
|
|
730
|
+
uv0 = read("uv", 2);
|
|
731
|
+
uv1 = read("uv1", 2);
|
|
732
|
+
const normalLocal = normal.clone();
|
|
733
|
+
if (normalAttribute) {
|
|
734
|
+
normalLocal
|
|
735
|
+
.applyMatrix3(new THREE.Matrix3().getNormalMatrix(instance))
|
|
736
|
+
.normalize();
|
|
737
|
+
finiteVector(normalLocal, id, "vertex input normal");
|
|
738
|
+
if (normalLocal.lengthSq() === 0)
|
|
739
|
+
fail("SURFACE_NORMAL_INVALID", "A vertex input normal must have nonzero length", id);
|
|
740
|
+
}
|
|
741
|
+
finiteVector(position, id, "vertex input position");
|
|
742
|
+
const result = vertexDeformer.evaluate(mesh, material.uuid, {
|
|
743
|
+
positionLocal: vec(position),
|
|
744
|
+
normalLocal: vec(normalLocal),
|
|
745
|
+
vertexIndex: index,
|
|
746
|
+
instanceIndex,
|
|
747
|
+
...(uv0 ? { uv: [uv0[0], uv0[1]] } : {}),
|
|
748
|
+
...(uv1 ? { uv1: [uv1[0], uv1[1]] } : {}),
|
|
749
|
+
});
|
|
750
|
+
if (result) {
|
|
751
|
+
position.fromArray(result.position);
|
|
752
|
+
finiteVector(position, id, "vertex output position");
|
|
753
|
+
if (result.normal !== undefined) {
|
|
754
|
+
if (!normalAttribute)
|
|
755
|
+
fail("TC_VERTEX_NORMAL_ATTRIBUTE", "A vertex normal graph requires a normal attribute; supply one before extraction", id);
|
|
756
|
+
normal.fromArray(result.normal);
|
|
757
|
+
finiteVector(normal, id, "vertex output normal");
|
|
758
|
+
if (normal.lengthSq() === 0)
|
|
759
|
+
fail("SURFACE_NORMAL_INVALID", "A vertex output normal must have nonzero length; retain the input normal for zero graph outputs", id);
|
|
760
|
+
normalOverridden = true;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
position.applyMatrix4(world);
|
|
765
|
+
finiteVector(position, id, "world position");
|
|
766
|
+
if (normalAttribute)
|
|
767
|
+
normal
|
|
768
|
+
.applyMatrix3(normalOverridden
|
|
769
|
+
? new THREE.Matrix3().getNormalMatrix(world)
|
|
770
|
+
: normalMatrix)
|
|
771
|
+
.normalize();
|
|
772
|
+
if (tangent) {
|
|
773
|
+
const t = new THREE.Vector3(tangent.x, tangent.y, tangent.z)
|
|
774
|
+
.applyMatrix3(direction)
|
|
775
|
+
.normalize();
|
|
776
|
+
tangent.set(t.x, t.y, t.z, tangent.w);
|
|
777
|
+
}
|
|
778
|
+
if (!vertexDeformer) {
|
|
779
|
+
uv0 = read("uv", 2);
|
|
780
|
+
uv1 = read("uv1", 2);
|
|
781
|
+
}
|
|
782
|
+
const colorAttribute = g.getAttribute("color");
|
|
783
|
+
let color = read("color", 3);
|
|
784
|
+
if (color && material.vertexColors && g.morphAttributes.color?.length) {
|
|
785
|
+
const c = morphed(g, "color", index, weights, id);
|
|
786
|
+
color = [c.x, c.y, c.z, colorAttribute?.itemSize === 4 ? c.w : 1];
|
|
787
|
+
}
|
|
788
|
+
else if (color && color.length === 3)
|
|
789
|
+
color.push(1);
|
|
790
|
+
if (!normalAttribute)
|
|
791
|
+
normal.set(0, 0, 0);
|
|
792
|
+
else if (normal.lengthSq() === 0)
|
|
793
|
+
fail("SURFACE_NORMAL_INVALID", "A normal attribute must have nonzero length", id);
|
|
794
|
+
if (tangent &&
|
|
795
|
+
(new THREE.Vector3(tangent.x, tangent.y, tangent.z).lengthSq() === 0 ||
|
|
796
|
+
Math.abs(tangent.w) !== 1))
|
|
797
|
+
fail("SURFACE_TANGENT_INVALID", "Tangent must be nonzero and handedness must be +/-1", id);
|
|
798
|
+
finiteVector(normal, id, "world normal");
|
|
799
|
+
if (tangent)
|
|
800
|
+
for (const n of tangent.toArray())
|
|
801
|
+
finite(n, id, "world tangent");
|
|
802
|
+
return {
|
|
803
|
+
position: vec(position),
|
|
804
|
+
normal: vec(normal),
|
|
805
|
+
...(uv0 ? { uv0: uv0.slice(0, 2) } : {}),
|
|
806
|
+
...(uv1 ? { uv1: uv1.slice(0, 2) } : {}),
|
|
807
|
+
...(tangent ? { tangent: tangent.toArray() } : {}),
|
|
808
|
+
...(color ? { color: color } : {}),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function drawSegments(g, assigned) {
|
|
812
|
+
const count = g.index?.count ?? g.getAttribute("position")?.count;
|
|
813
|
+
if (count === undefined)
|
|
814
|
+
fail("SURFACE_POSITION_MISSING", "Mesh has no position attribute", g.uuid);
|
|
815
|
+
const rs = g.drawRange.start, re = Number.isFinite(g.drawRange.count) ? rs + g.drawRange.count : count;
|
|
816
|
+
if (!Number.isInteger(rs) ||
|
|
817
|
+
rs < 0 ||
|
|
818
|
+
!(Number.isInteger(g.drawRange.count) || g.drawRange.count === Infinity) ||
|
|
819
|
+
g.drawRange.count < 0)
|
|
820
|
+
fail("SURFACE_DRAW_RANGE_INVALID", "drawRange is invalid", g.uuid);
|
|
821
|
+
const intersect = (start, end, materialIndex, groupIndex) => {
|
|
822
|
+
const a = Math.max(0, start, rs), b = Math.min(count, end, re);
|
|
823
|
+
if (b <= a)
|
|
824
|
+
return [];
|
|
825
|
+
if (a % 3 !== 0 || (b - a) % 3 !== 0)
|
|
826
|
+
fail("SURFACE_TRIANGLE_BOUNDARY_INVALID", "drawRange/group intersection is not triangle aligned", g.uuid);
|
|
827
|
+
return [{ start: a, end: b, materialIndex, groupIndex }];
|
|
828
|
+
};
|
|
829
|
+
if (!Array.isArray(assigned))
|
|
830
|
+
return intersect(0, count, 0, -1);
|
|
831
|
+
const out = [];
|
|
832
|
+
g.groups.forEach((group, groupIndex) => {
|
|
833
|
+
const materialIndex = group.materialIndex ?? 0;
|
|
834
|
+
if (!Number.isInteger(group.start) ||
|
|
835
|
+
group.start < 0 ||
|
|
836
|
+
!(Number.isInteger(group.count) || group.count === Infinity) ||
|
|
837
|
+
group.count < 0 ||
|
|
838
|
+
!Number.isInteger(materialIndex) ||
|
|
839
|
+
materialIndex < 0)
|
|
840
|
+
fail("SURFACE_GROUP_INVALID", "Geometry group is invalid", g.uuid);
|
|
841
|
+
out.push(...intersect(group.start, group.start + group.count, materialIndex, groupIndex));
|
|
842
|
+
});
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
function validateGeometry(g, identity) {
|
|
846
|
+
for (const [name, expectedSizes] of Object.entries({
|
|
847
|
+
position: [3],
|
|
848
|
+
normal: [3],
|
|
849
|
+
tangent: [4],
|
|
850
|
+
color: [3, 4],
|
|
851
|
+
uv: [2],
|
|
852
|
+
uv1: [2],
|
|
853
|
+
skinIndex: [4],
|
|
854
|
+
skinWeight: [4],
|
|
855
|
+
})) {
|
|
856
|
+
const attribute = g.getAttribute(name);
|
|
857
|
+
if (!attribute)
|
|
858
|
+
continue;
|
|
859
|
+
if (!(attribute.isBufferAttribute ||
|
|
860
|
+
attribute
|
|
861
|
+
.isInterleavedBufferAttribute) ||
|
|
862
|
+
!expectedSizes.includes(attribute.itemSize))
|
|
863
|
+
fail("SURFACE_ATTRIBUTE_UNSUPPORTED", `${name} must be a readable CPU attribute with itemSize ${expectedSizes.join("/")}`, identity);
|
|
864
|
+
if (attribute
|
|
865
|
+
.isInstancedBufferAttribute ||
|
|
866
|
+
attribute.data?.isInstancedInterleavedBuffer)
|
|
867
|
+
fail("SURFACE_DEFORMATION_UNSUPPORTED", `Instanced attribute ${name} requires an explicit deformation adapter`, identity);
|
|
868
|
+
if (attribute.count !== g.getAttribute("position").count)
|
|
869
|
+
fail("SURFACE_ATTRIBUTE_COUNT_INVALID", `${name} vertex count differs from position`, identity);
|
|
870
|
+
}
|
|
871
|
+
if (g.index && g.index.itemSize !== 1)
|
|
872
|
+
fail("SURFACE_INDEX_INVALID", "Index itemSize must be one", identity);
|
|
873
|
+
}
|
|
874
|
+
function meshSignature(mesh, world, worldCache, resolved, vertexDeformer) {
|
|
875
|
+
const g = mesh.geometry, signature = [
|
|
876
|
+
mesh.uuid,
|
|
877
|
+
g,
|
|
878
|
+
g.uuid,
|
|
879
|
+
g.type,
|
|
880
|
+
g.drawRange.start,
|
|
881
|
+
g.drawRange.count,
|
|
882
|
+
g.morphTargetsRelative,
|
|
883
|
+
];
|
|
884
|
+
const attribute = (a) => {
|
|
885
|
+
if (!a) {
|
|
886
|
+
signature.push(a);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const ordinary = a, interleaved = a, data = interleaved.data;
|
|
890
|
+
signature.push(a, a.constructor, a.count, a.itemSize, a.normalized, ordinary.version, ordinary.isBufferAttribute, ordinary.gpuType, ordinary.isFloat16BufferAttribute, interleaved.isInterleavedBufferAttribute, interleaved.offset, data, data?.version, data?.stride, data?.count, a.array, a.array?.constructor, a.array?.length, a.getX, a.getY, a.getZ, a.getW, a.getComponent);
|
|
891
|
+
};
|
|
892
|
+
attribute(g.index);
|
|
893
|
+
const attributes = Object.entries(g.attributes);
|
|
894
|
+
signature.push(attributes.length);
|
|
895
|
+
for (const [name, a] of attributes) {
|
|
896
|
+
signature.push(name);
|
|
897
|
+
attribute(a);
|
|
898
|
+
}
|
|
899
|
+
const morphs = Object.entries(g.morphAttributes);
|
|
900
|
+
signature.push(morphs.length);
|
|
901
|
+
for (const [name, targets] of morphs) {
|
|
902
|
+
signature.push(name, targets?.length);
|
|
903
|
+
for (const a of targets ?? [])
|
|
904
|
+
attribute(a);
|
|
905
|
+
}
|
|
906
|
+
signature.push(g.groups.length);
|
|
907
|
+
for (const group of g.groups)
|
|
908
|
+
signature.push(group.start, group.count, group.materialIndex);
|
|
909
|
+
const assigned = Array.isArray(mesh.material)
|
|
910
|
+
? mesh.material
|
|
911
|
+
: [mesh.material];
|
|
912
|
+
signature.push(Array.isArray(mesh.material), assigned.length);
|
|
913
|
+
for (const m of assigned)
|
|
914
|
+
signature.push(m, m?.uuid, m?.visible);
|
|
915
|
+
for (const [segment, m] of resolved)
|
|
916
|
+
signature.push(segment.start, segment.end, segment.materialIndex, segment.groupIndex, m?.uuid, m?.side, m?.flatShading, m?.vertexColors);
|
|
917
|
+
const chain = (object) => {
|
|
918
|
+
const seen = new Set();
|
|
919
|
+
for (let current = object; current; current = current.parent) {
|
|
920
|
+
if (seen.has(current))
|
|
921
|
+
fail("SURFACE_TRANSFORM_UNSUPPORTED", "Cyclic ancestry", object.uuid);
|
|
922
|
+
seen.add(current);
|
|
923
|
+
signature.push(current, current.uuid, current.visible, current.layers.mask, current.matrixAutoUpdate, current.matrixWorldAutoUpdate, ...localMatrix(current).elements, ...current.matrix.elements, ...current.matrixWorld.elements);
|
|
924
|
+
}
|
|
925
|
+
signature.push(null);
|
|
926
|
+
};
|
|
927
|
+
chain(mesh);
|
|
928
|
+
signature.push(...world.elements, mesh.morphTargetInfluences?.length, ...(mesh.morphTargetInfluences ?? []));
|
|
929
|
+
if (mesh.isInstancedMesh) {
|
|
930
|
+
const instanced = mesh;
|
|
931
|
+
signature.push(instanced.count);
|
|
932
|
+
attribute(instanced.instanceMatrix);
|
|
933
|
+
attribute(instanced.instanceColor);
|
|
934
|
+
// setMatrixAt/setColorAt do not bump attribute versions. Transforms and
|
|
935
|
+
// instance colors are cheap scalar state and must be observed immediately.
|
|
936
|
+
for (let i = 0; i < instanced.count * 16; i++)
|
|
937
|
+
signature.push(instanced.instanceMatrix.array[i]);
|
|
938
|
+
if (instanced.instanceColor)
|
|
939
|
+
for (let i = 0; i < instanced.count * instanced.instanceColor.itemSize; i++)
|
|
940
|
+
signature.push(instanced.instanceColor.array[i]);
|
|
941
|
+
}
|
|
942
|
+
if (mesh.isSkinnedMesh) {
|
|
943
|
+
const skin = mesh, skeleton = skin.skeleton;
|
|
944
|
+
signature.push(skeleton, skin.bindMode, ...skin.bindMatrix.elements, ...skin.bindMatrixInverse.elements, skeleton?.bones.length, skeleton?.boneInverses.length);
|
|
945
|
+
for (const bone of skeleton?.bones ?? []) {
|
|
946
|
+
signature.push(bone);
|
|
947
|
+
if (bone) {
|
|
948
|
+
chain(bone);
|
|
949
|
+
signature.push(...worldMatrix(bone, worldCache).elements);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
for (const inverse of skeleton?.boneInverses ?? [])
|
|
953
|
+
signature.push(inverse, ...(inverse?.elements ?? []));
|
|
954
|
+
}
|
|
955
|
+
signature.push(vertexDeformer !== undefined);
|
|
956
|
+
if (vertexDeformer) {
|
|
957
|
+
const state = vertexDeformer.signature(mesh);
|
|
958
|
+
signature.push(state.length);
|
|
959
|
+
for (const value of state)
|
|
960
|
+
signature.push(value);
|
|
961
|
+
}
|
|
962
|
+
return signature;
|
|
963
|
+
}
|
|
964
|
+
/** Private CPU-to-GPU preparation interface. Captures current world-space scene surfaces without mutating Three objects. */
|
|
965
|
+
export function extractSurfaceScene(scene, options = {}) {
|
|
966
|
+
options.meshCache?.assertActive();
|
|
967
|
+
if (THREE.ColorManagement.workingColorSpace !== THREE.LinearSRGBColorSpace)
|
|
968
|
+
fail("SURFACE_WORKING_COLOR_SPACE_UNSUPPORTED", "CPU reference requires Three working LinearSRGBColorSpace", scene.uuid);
|
|
969
|
+
rejectExtensions(scene.userData, scene.uuid);
|
|
970
|
+
if (scene.overrideMaterial)
|
|
971
|
+
fail("SURFACE_SCENE_OVERRIDE_UNSUPPORTED", "scene.overrideMaterial requires an explicit surface adapter", scene.uuid);
|
|
972
|
+
const triangles = [], materials = [], textures = [], diagnostics = [];
|
|
973
|
+
const byMaterial = new Map(), adaptedMaterials = new Map(), byTexture = new Map(), worldCache = new Map(), layerMask = options.layersMask ?? 0xffffffff;
|
|
974
|
+
if (scene.environment || scene.background || scene.fog)
|
|
975
|
+
diagnostics.push({
|
|
976
|
+
code: "SURFACE_SCENE_EFFECTS_NOT_CAPTURED",
|
|
977
|
+
message: "Scene environment/background/fog are outside the surface contract",
|
|
978
|
+
objectUuid: scene.uuid,
|
|
979
|
+
});
|
|
980
|
+
const visit = (object, visible) => {
|
|
981
|
+
if (!visible || !object.visible)
|
|
982
|
+
return;
|
|
983
|
+
if (!object.userData.noTrace &&
|
|
984
|
+
object.isMesh === true &&
|
|
985
|
+
object.layers.mask & layerMask &&
|
|
986
|
+
(options.meshFilter?.(object) ?? true)) {
|
|
987
|
+
const mesh = object, g = mesh.geometry;
|
|
988
|
+
if (!g)
|
|
989
|
+
fail("SURFACE_GEOMETRY_MISSING", "Mesh requires BufferGeometry", mesh.uuid);
|
|
990
|
+
if (g.isBufferGeometry !==
|
|
991
|
+
true ||
|
|
992
|
+
g.type === "InstancedBufferGeometry" ||
|
|
993
|
+
mesh.isBatchedMesh)
|
|
994
|
+
fail("SURFACE_GEOMETRY_UNSUPPORTED", "Only ordinary BufferGeometry is supported", mesh.uuid);
|
|
995
|
+
if (mesh.constructor !== THREE.Mesh &&
|
|
996
|
+
mesh.constructor !== THREE.SkinnedMesh &&
|
|
997
|
+
mesh.constructor !== THREE.InstancedMesh)
|
|
998
|
+
fail("SURFACE_DEFORMATION_UNSUPPORTED", `Custom mesh/deformation ${mesh.constructor.name} is unsupported`, mesh.uuid);
|
|
999
|
+
if (mesh.onBeforeRender.toString() !==
|
|
1000
|
+
THREE.Object3D.prototype.onBeforeRender.toString())
|
|
1001
|
+
fail("SURFACE_CUSTOM_RENDER_HOOK_UNSUPPORTED", "Custom onBeforeRender is unsupported", mesh.uuid);
|
|
1002
|
+
const nativeVertex = mesh
|
|
1003
|
+
.isSkinnedMesh
|
|
1004
|
+
? THREE.SkinnedMesh.prototype.getVertexPosition
|
|
1005
|
+
: THREE.Mesh.prototype.getVertexPosition;
|
|
1006
|
+
if (mesh.getVertexPosition !== nativeVertex)
|
|
1007
|
+
fail("SURFACE_DEFORMATION_UNSUPPORTED", "Overridden getVertexPosition is unsupported; use ordinary mesh/morph/skin state", mesh.uuid);
|
|
1008
|
+
rejectExtensions(mesh.userData, mesh.uuid);
|
|
1009
|
+
rejectExtensions(g.userData, g.uuid);
|
|
1010
|
+
const position = g.getAttribute("position");
|
|
1011
|
+
if (!position || position.itemSize < 3)
|
|
1012
|
+
fail("SURFACE_POSITION_MISSING", "position itemSize >= 3 is required", mesh.uuid);
|
|
1013
|
+
validateGeometry(g, `${mesh.uuid}/${g.uuid}`);
|
|
1014
|
+
for (const name of Object.keys(g.morphAttributes))
|
|
1015
|
+
if (!["position", "normal", "color"].includes(name))
|
|
1016
|
+
fail("SURFACE_MORPH_ATTRIBUTE_UNSUPPORTED", `Morph attribute ${name} is unsupported`, mesh.uuid);
|
|
1017
|
+
if (mesh.isInstancedMesh &&
|
|
1018
|
+
mesh.morphTexture)
|
|
1019
|
+
fail("SURFACE_INSTANCE_MORPH_UNSUPPORTED", "Instanced per-instance morph weights are unsupported", mesh.uuid);
|
|
1020
|
+
const assigned = mesh.material, segments = drawSegments(g, assigned), world = worldMatrix(mesh, worldCache), isInstanced = mesh.isInstancedMesh ===
|
|
1021
|
+
true, count = isInstanced ? mesh.count : 1;
|
|
1022
|
+
if (!Number.isInteger(count) ||
|
|
1023
|
+
count < 0 ||
|
|
1024
|
+
(isInstanced &&
|
|
1025
|
+
count > mesh.instanceMatrix.count))
|
|
1026
|
+
fail("SURFACE_INSTANCE_COUNT_INVALID", "InstancedMesh.count exceeds matrix attributes", mesh.uuid);
|
|
1027
|
+
const resolved = new Map();
|
|
1028
|
+
const materialFor = (segment) => {
|
|
1029
|
+
const m = Array.isArray(assigned)
|
|
1030
|
+
? assigned[segment.materialIndex]
|
|
1031
|
+
: assigned;
|
|
1032
|
+
if (!m)
|
|
1033
|
+
fail("SURFACE_MATERIAL_INDEX_MISSING", "Group references missing material " + segment.materialIndex, mesh.uuid + "/" + g.uuid);
|
|
1034
|
+
if (!m.visible)
|
|
1035
|
+
return undefined;
|
|
1036
|
+
let adapted = adaptedMaterials.get(m);
|
|
1037
|
+
if (!adapted) {
|
|
1038
|
+
adapted = options.materialAdapter?.(m) ?? m;
|
|
1039
|
+
adaptedMaterials.set(m, adapted);
|
|
1040
|
+
}
|
|
1041
|
+
const material = snapshotMaterial(adapted, materials, byMaterial, textures, byTexture, options, diagnostics, m);
|
|
1042
|
+
if (material.maps.normalMap &&
|
|
1043
|
+
!g.hasAttribute("tangent") &&
|
|
1044
|
+
!diagnostics.some((d) => d.code === "SURFACE_NORMAL_FRAME_REFERENCE_ONLY" &&
|
|
1045
|
+
d.objectUuid === mesh.uuid))
|
|
1046
|
+
diagnostics.push({
|
|
1047
|
+
code: "SURFACE_NORMAL_FRAME_REFERENCE_ONLY",
|
|
1048
|
+
message: "Missing tangent: CPU reference derives an orthonormal triangle frame from this normal map's transformed UVs. This is not a pixel-identical implementation of Three WebGPU derivative TBN.",
|
|
1049
|
+
objectUuid: mesh.uuid,
|
|
1050
|
+
materialUuid: material.uuid,
|
|
1051
|
+
});
|
|
1052
|
+
return material;
|
|
1053
|
+
};
|
|
1054
|
+
if (options.meshCache && count > 0)
|
|
1055
|
+
for (const segment of segments)
|
|
1056
|
+
resolved.set(segment, materialFor(segment));
|
|
1057
|
+
const signature = options.meshCache
|
|
1058
|
+
? meshSignature(mesh, world, worldCache, resolved, options.vertexDeformer)
|
|
1059
|
+
: undefined, cached = signature
|
|
1060
|
+
? options.meshCache.get(mesh, signature)
|
|
1061
|
+
: undefined, firstTriangle = triangles.length;
|
|
1062
|
+
if (cached) {
|
|
1063
|
+
// Do not spread: a content-owned mesh can contain more than the argument limit.
|
|
1064
|
+
for (const triangle of cached)
|
|
1065
|
+
triangles.push(triangle);
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
for (let instanceId = 0; instanceId < count; instanceId++) {
|
|
1069
|
+
const instance = new THREE.Matrix4();
|
|
1070
|
+
let instanceColor;
|
|
1071
|
+
if (isInstanced) {
|
|
1072
|
+
const instanced = mesh;
|
|
1073
|
+
instanced.getMatrixAt(instanceId, instance);
|
|
1074
|
+
if (instanced.instanceColor) {
|
|
1075
|
+
const color = new THREE.Color();
|
|
1076
|
+
instanced.getColorAt(instanceId, color);
|
|
1077
|
+
instanceColor = [color.r, color.g, color.b];
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const combined = world.clone().multiply(instance), determinant = finite(combined.determinant(), mesh.uuid, "transform determinant");
|
|
1081
|
+
validateAffine(instance, `${mesh.uuid}/instance/${instanceId}`);
|
|
1082
|
+
if (instanceColor)
|
|
1083
|
+
for (const n of instanceColor)
|
|
1084
|
+
finite(n, mesh.uuid, "instance color");
|
|
1085
|
+
if (determinant === 0)
|
|
1086
|
+
fail("SURFACE_SINGULAR_TRANSFORM", "Combined world/instance transform is singular", mesh.uuid);
|
|
1087
|
+
const normalMatrix = new THREE.Matrix3().getNormalMatrix(combined), direction = new THREE.Matrix3().setFromMatrix4(combined);
|
|
1088
|
+
for (const n of normalMatrix.elements)
|
|
1089
|
+
finite(n, mesh.uuid, "inverse-transpose normal matrix");
|
|
1090
|
+
for (const segment of segments) {
|
|
1091
|
+
const material = options.meshCache
|
|
1092
|
+
? resolved.get(segment)
|
|
1093
|
+
: materialFor(segment);
|
|
1094
|
+
if (!material)
|
|
1095
|
+
continue;
|
|
1096
|
+
for (let offset = segment.start; offset < segment.end; offset += 3) {
|
|
1097
|
+
const indices = [0, 1, 2].map((k) => g.index?.getX(offset + k) ?? offset + k);
|
|
1098
|
+
for (const i of indices)
|
|
1099
|
+
if (!Number.isInteger(i) || i < 0 || i >= position.count)
|
|
1100
|
+
fail("SURFACE_INDEX_INVALID", `Vertex index ${i} is out of range`, g.uuid);
|
|
1101
|
+
const vertices = indices.map((i) => getVertex(mesh, i, instance, normalMatrix, world, direction, material, worldCache, instanceId, options.vertexDeformer));
|
|
1102
|
+
const p = vertices.map((v) => new THREE.Vector3(...v.position)), face = p[1].sub(p[0]).cross(p[2].sub(p[0]));
|
|
1103
|
+
finiteVector(face, g.uuid, "triangle cross product");
|
|
1104
|
+
if (face.lengthSq() === 0)
|
|
1105
|
+
fail("SURFACE_DEGENERATE_TRIANGLE", "Cannot generate a face normal for a degenerate triangle", g.uuid);
|
|
1106
|
+
if (!g.hasAttribute("normal") || material.flatShading) {
|
|
1107
|
+
face.normalize();
|
|
1108
|
+
for (const v of vertices)
|
|
1109
|
+
v.normal = vec(face);
|
|
1110
|
+
}
|
|
1111
|
+
for (const v of vertices)
|
|
1112
|
+
for (const n of [
|
|
1113
|
+
...v.position,
|
|
1114
|
+
...v.normal,
|
|
1115
|
+
...(v.uv0 ?? []),
|
|
1116
|
+
...(v.uv1 ?? []),
|
|
1117
|
+
...(v.tangent ?? []),
|
|
1118
|
+
...(v.color ?? []),
|
|
1119
|
+
])
|
|
1120
|
+
finite(n, mesh.uuid, "vertex attribute");
|
|
1121
|
+
triangles.push({
|
|
1122
|
+
objectUuid: mesh.uuid,
|
|
1123
|
+
geometryUuid: g.uuid,
|
|
1124
|
+
instanceId,
|
|
1125
|
+
...(instanceColor ? { instanceColor } : {}),
|
|
1126
|
+
materialUuid: material.uuid,
|
|
1127
|
+
materialIndex: segment.materialIndex,
|
|
1128
|
+
groupIndex: segment.groupIndex,
|
|
1129
|
+
elementOffset: offset,
|
|
1130
|
+
vertexIndices: indices,
|
|
1131
|
+
vertices,
|
|
1132
|
+
normalMatrix: normalMatrix.elements.slice(),
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (signature)
|
|
1138
|
+
options.meshCache.set(mesh, signature, triangles.slice(firstTriangle));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
else if (!object.userData.noTrace &&
|
|
1142
|
+
(object.isPoints ||
|
|
1143
|
+
object.isLine ||
|
|
1144
|
+
object.isSprite)) {
|
|
1145
|
+
diagnostics.push({
|
|
1146
|
+
code: "SURFACE_RENDERABLE_UNSUPPORTED",
|
|
1147
|
+
message: `${object.type} is outside triangle surface extraction`,
|
|
1148
|
+
objectUuid: object.uuid,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (object.isLight &&
|
|
1152
|
+
object.layers.mask & layerMask)
|
|
1153
|
+
diagnostics.push({
|
|
1154
|
+
code: "SURFACE_LIGHT_NOT_CAPTURED",
|
|
1155
|
+
message: "Lights are outside the material surface contract",
|
|
1156
|
+
objectUuid: object.uuid,
|
|
1157
|
+
});
|
|
1158
|
+
for (const child of object.children)
|
|
1159
|
+
visit(child, true);
|
|
1160
|
+
};
|
|
1161
|
+
visit(scene, true);
|
|
1162
|
+
return freezeDeep({
|
|
1163
|
+
version: 1,
|
|
1164
|
+
triangles,
|
|
1165
|
+
materials,
|
|
1166
|
+
textures,
|
|
1167
|
+
diagnostics,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
function textureById(snapshot, id, material) {
|
|
1171
|
+
const texture = snapshot.textures.find((t) => t.uuid === id);
|
|
1172
|
+
if (!texture)
|
|
1173
|
+
return fail("SURFACE_TEXTURE_ID_MISSING", `Missing texture ${id}`, material.uuid);
|
|
1174
|
+
if (!texture.pixels)
|
|
1175
|
+
return fail("SURFACE_TEXTURE_PIXELS_UNAVAILABLE", "CPU sampling requires decoded pixels", texture.uuid);
|
|
1176
|
+
if (texture.mapping !== THREE.UVMapping || texture.channel > 1)
|
|
1177
|
+
return fail("SURFACE_TEXTURE_SAMPLING_UNSUPPORTED", "Mapping/channel is unsupported", texture.uuid);
|
|
1178
|
+
return texture;
|
|
1179
|
+
}
|
|
1180
|
+
function wrapIndex(i, size, mode) {
|
|
1181
|
+
if (mode === THREE.RepeatWrapping)
|
|
1182
|
+
return ((i % size) + size) % size;
|
|
1183
|
+
if (mode === THREE.MirroredRepeatWrapping) {
|
|
1184
|
+
const t = ((i % (2 * size)) + 2 * size) % (2 * size);
|
|
1185
|
+
return t < size ? t : 2 * size - 1 - t;
|
|
1186
|
+
}
|
|
1187
|
+
if (mode === THREE.ClampToEdgeWrapping)
|
|
1188
|
+
return Math.max(0, Math.min(size - 1, i));
|
|
1189
|
+
return fail("SURFACE_WRAP_UNSUPPORTED", `Unknown wrap mode ${mode}`);
|
|
1190
|
+
}
|
|
1191
|
+
function sampleTexture(t, uv, footprint) {
|
|
1192
|
+
if (!t.pixels || t.width < 1 || t.height < 1)
|
|
1193
|
+
return fail("SURFACE_TEXTURE_PIXELS_UNAVAILABLE", "CPU pixels are unavailable", t.uuid);
|
|
1194
|
+
const m = t.matrix, q = m[2] * uv[0] + m[5] * uv[1] + m[8];
|
|
1195
|
+
if (!q)
|
|
1196
|
+
return fail("SURFACE_UV_TRANSFORM_INVALID", "UV transform has homogeneous w=0", t.uuid);
|
|
1197
|
+
let u = (m[0] * uv[0] + m[3] * uv[1] + m[6]) / q, v = (m[1] * uv[0] + m[4] * uv[1] + m[7]) / q;
|
|
1198
|
+
const wrap = (x, mode) => {
|
|
1199
|
+
if (mode === THREE.ClampToEdgeWrapping)
|
|
1200
|
+
return Math.max(0, Math.min(1, x));
|
|
1201
|
+
if (mode === THREE.RepeatWrapping)
|
|
1202
|
+
return ((x % 1) + 1) % 1;
|
|
1203
|
+
if (mode === THREE.MirroredRepeatWrapping) {
|
|
1204
|
+
const r = ((x % 2) + 2) % 2;
|
|
1205
|
+
return r <= 1 ? r : 2 - r;
|
|
1206
|
+
}
|
|
1207
|
+
return fail("SURFACE_WRAP_UNSUPPORTED", `Unknown wrap mode ${mode}`, t.uuid);
|
|
1208
|
+
};
|
|
1209
|
+
u = wrap(u, t.wrapS);
|
|
1210
|
+
v = wrap(v, t.wrapT);
|
|
1211
|
+
const filter = footprint === "magnification" ? t.magFilter : t.minFilter;
|
|
1212
|
+
const nearest = [
|
|
1213
|
+
THREE.NearestFilter,
|
|
1214
|
+
THREE.NearestMipmapNearestFilter,
|
|
1215
|
+
THREE.NearestMipmapLinearFilter,
|
|
1216
|
+
].includes(filter);
|
|
1217
|
+
const linear = [
|
|
1218
|
+
THREE.LinearFilter,
|
|
1219
|
+
THREE.LinearMipmapNearestFilter,
|
|
1220
|
+
THREE.LinearMipmapLinearFilter,
|
|
1221
|
+
].includes(filter);
|
|
1222
|
+
if (!nearest && !linear)
|
|
1223
|
+
return fail("SURFACE_FILTER_UNSUPPORTED", `Filter ${filter} is unsupported`, t.uuid);
|
|
1224
|
+
const read = (x, y) => {
|
|
1225
|
+
const i = (wrapIndex(y, t.height, t.wrapT) * t.width +
|
|
1226
|
+
wrapIndex(x, t.width, t.wrapS)) *
|
|
1227
|
+
4;
|
|
1228
|
+
const rgb = [t.pixels[i], t.pixels[i + 1], t.pixels[i + 2]];
|
|
1229
|
+
if (t.colorSpace === THREE.SRGBColorSpace)
|
|
1230
|
+
for (let c = 0; c < 3; c++)
|
|
1231
|
+
rgb[c] = srgbToLinear(rgb[c]);
|
|
1232
|
+
else if (t.colorSpace !== THREE.NoColorSpace &&
|
|
1233
|
+
t.colorSpace !== THREE.LinearSRGBColorSpace)
|
|
1234
|
+
fail("SURFACE_COLOR_SPACE_UNSUPPORTED", `Cannot sample ${t.colorSpace}`, t.uuid);
|
|
1235
|
+
return [rgb[0], rgb[1], rgb[2], t.pixels[i + 3]];
|
|
1236
|
+
};
|
|
1237
|
+
if (nearest)
|
|
1238
|
+
return read(Math.floor(u * t.width), Math.floor(v * t.height));
|
|
1239
|
+
const x = u * t.width - 0.5, y = v * t.height - 0.5, x0 = Math.floor(x), y0 = Math.floor(y), tx = x - x0, ty = y - y0;
|
|
1240
|
+
const a = read(x0, y0), b = read(x0 + 1, y0), c = read(x0, y0 + 1), d = read(x0 + 1, y0 + 1);
|
|
1241
|
+
return [0, 1, 2, 3].map((i) => THREE.MathUtils.lerp(THREE.MathUtils.lerp(a[i], b[i], tx), THREE.MathUtils.lerp(c[i], d[i], tx), ty));
|
|
1242
|
+
}
|
|
1243
|
+
/** Engine-private base-level sampler shared with material surface sampling. */
|
|
1244
|
+
export function sampleSurfaceTexture(texture, uv) {
|
|
1245
|
+
for (const value of uv)
|
|
1246
|
+
finite(value, texture.uuid, "sample UV");
|
|
1247
|
+
return sampleTexture(texture, uv, "magnification");
|
|
1248
|
+
}
|
|
1249
|
+
function interpolateUV(triangle, channel, bary) {
|
|
1250
|
+
const key = channel === 0 ? "uv0" : "uv1", values = triangle.vertices.map((v) => v[key]);
|
|
1251
|
+
if (values.some((v) => !v))
|
|
1252
|
+
return undefined;
|
|
1253
|
+
return [0, 1].map((axis) => values.reduce((sum, uv, i) => sum + uv[axis] * bary[i], 0));
|
|
1254
|
+
}
|
|
1255
|
+
/** CPU reference for declared solid/DataTexture/provider-backed maps; this is not a shader, BSDF, tracer, or lighting implementation. */
|
|
1256
|
+
export function sampleSurfaceMaterial(snapshot, triangleOrIndex, barycentric, options = {}) {
|
|
1257
|
+
const tri = typeof triangleOrIndex === "number"
|
|
1258
|
+
? snapshot.triangles[triangleOrIndex]
|
|
1259
|
+
: triangleOrIndex;
|
|
1260
|
+
if (!tri)
|
|
1261
|
+
return fail("SURFACE_TRIANGLE_INDEX_INVALID", "Triangle is absent from snapshot");
|
|
1262
|
+
const material = snapshot.materials.find((m) => m.uuid === tri.materialUuid);
|
|
1263
|
+
if (!material)
|
|
1264
|
+
return fail("SURFACE_MATERIAL_ID_MISSING", `Missing material ${tri.materialUuid}`);
|
|
1265
|
+
if (barycentric.some((v) => !Number.isFinite(v) || v < 0 || v > 1) ||
|
|
1266
|
+
Math.abs(barycentric[0] + barycentric[1] + barycentric[2] - 1) > 1e-6)
|
|
1267
|
+
fail("SURFACE_BARYCENTRIC_INVALID", "Coordinates must be finite and sum to one", tri.geometryUuid);
|
|
1268
|
+
const frontFacing = options.frontFacing ?? true, discarded = (material.transmission ?? 0) === 0 &&
|
|
1269
|
+
((material.side === "front" && !frontFacing) ||
|
|
1270
|
+
(material.side === "back" && frontFacing));
|
|
1271
|
+
const blend = (key) => [0, 1, 2, 3].map((axis) => tri.vertices.reduce((sum, v, i) => sum +
|
|
1272
|
+
(v[key]?.[axis] ??
|
|
1273
|
+
(axis === 3 ? 1 : key === "color" ? 1 : 0)) *
|
|
1274
|
+
barycentric[i], 0));
|
|
1275
|
+
let normal = new THREE.Vector3(...blend("normal").slice(0, 3));
|
|
1276
|
+
if (!normal.lengthSq()) {
|
|
1277
|
+
const p = tri.vertices.map((v) => new THREE.Vector3(...v.position));
|
|
1278
|
+
normal = p[1].sub(p[0]).cross(p[2].sub(p[0])).normalize();
|
|
1279
|
+
}
|
|
1280
|
+
if ((material.transmission ?? 0) > 0
|
|
1281
|
+
? !frontFacing
|
|
1282
|
+
: material.side === "back" || (material.side === "double" && !frontFacing))
|
|
1283
|
+
normal.negate();
|
|
1284
|
+
normal.normalize();
|
|
1285
|
+
const color = [...material.baseColor], vertexColor = blend("color");
|
|
1286
|
+
if (material.vertexColors && tri.vertices.some((v) => v.color))
|
|
1287
|
+
for (let i = 0; i < 4; i++)
|
|
1288
|
+
color[i] *= vertexColor[i];
|
|
1289
|
+
if (tri.instanceColor)
|
|
1290
|
+
for (let i = 0; i < 3; i++)
|
|
1291
|
+
color[i] *= tri.instanceColor[i];
|
|
1292
|
+
const uvFor = (t) => {
|
|
1293
|
+
const uv = (t.channel === 0 ? options.uv0 : options.uv1) ??
|
|
1294
|
+
interpolateUV(tri, t.channel, barycentric);
|
|
1295
|
+
if (!uv)
|
|
1296
|
+
return fail("SURFACE_UV_MISSING", `Texture ${t.uuid} requires uv${t.channel}`, t.uuid);
|
|
1297
|
+
for (const value of uv)
|
|
1298
|
+
finite(value, t.uuid, "sample UV");
|
|
1299
|
+
return uv;
|
|
1300
|
+
};
|
|
1301
|
+
const map = (slot) => {
|
|
1302
|
+
const id = material.maps[slot];
|
|
1303
|
+
if (!id)
|
|
1304
|
+
return undefined;
|
|
1305
|
+
const texture = textureById(snapshot, id, material);
|
|
1306
|
+
return {
|
|
1307
|
+
texture,
|
|
1308
|
+
value: sampleTexture(texture, uvFor(texture), options.footprint ?? "magnification"),
|
|
1309
|
+
};
|
|
1310
|
+
};
|
|
1311
|
+
const baseMap = map("map"), alphaMap = map("alphaMap");
|
|
1312
|
+
if (baseMap)
|
|
1313
|
+
for (let i = 0; i < 4; i++)
|
|
1314
|
+
color[i] *= baseMap.value[i];
|
|
1315
|
+
if (alphaMap)
|
|
1316
|
+
color[3] *= alphaMap.value[1];
|
|
1317
|
+
const roughnessMap = map("roughnessMap"), metalnessMap = map("metalnessMap"), aoMap = map("aoMap"), emissiveMap = map("emissiveMap"), normalMap = map("normalMap"), specularIntensityMap = map("specularIntensityMap"), specularColorMap = map("specularColorMap");
|
|
1318
|
+
const roughness = material.roughness * (roughnessMap?.value[1] ?? 1), metalness = material.metalness * (metalnessMap?.value[2] ?? 1), specularIntensity = (material.specularIntensity ?? 1) * (specularIntensityMap?.value[3] ?? 1), specularColor = (material.specularColor ?? [1, 1, 1]).map((v, i) => v * (specularColorMap?.value[i] ?? 1));
|
|
1319
|
+
const occlusion = 1 + ((aoMap?.value[0] ?? 1) - 1) * material.aoMapIntensity;
|
|
1320
|
+
const emissive = material.emissive.map((v, i) => v * material.emissiveIntensity * (emissiveMap?.value[i] ?? 1));
|
|
1321
|
+
if (normalMap) {
|
|
1322
|
+
const sampled = new THREE.Vector3(normalMap.value[0] * 2 - 1, normalMap.value[1] * 2 - 1, normalMap.value[2] * 2 - 1);
|
|
1323
|
+
sampled.x *= material.normalScale[0];
|
|
1324
|
+
sampled.y *= material.normalScale[1];
|
|
1325
|
+
const sideSign = ((material.transmission ?? 0) > 0
|
|
1326
|
+
? !frontFacing
|
|
1327
|
+
: material.side === "back" ||
|
|
1328
|
+
(material.side === "double" && !frontFacing))
|
|
1329
|
+
? -1
|
|
1330
|
+
: 1;
|
|
1331
|
+
if (material.normalMapType === THREE.ObjectSpaceNormalMap)
|
|
1332
|
+
normal = sampled
|
|
1333
|
+
.applyMatrix3(new THREE.Matrix3().fromArray(tri.normalMatrix))
|
|
1334
|
+
.multiplyScalar(sideSign)
|
|
1335
|
+
.normalize();
|
|
1336
|
+
else if (material.normalMapType === THREE.TangentSpaceNormalMap) {
|
|
1337
|
+
let tangent, handedness;
|
|
1338
|
+
if (tri.vertices.every((v) => v.tangent)) {
|
|
1339
|
+
tangent = tri.vertices
|
|
1340
|
+
.reduce((sum, v, i) => sum.addScaledVector(new THREE.Vector3(...v.tangent.slice(0, 3)), barycentric[i]), new THREE.Vector3())
|
|
1341
|
+
.normalize();
|
|
1342
|
+
handedness =
|
|
1343
|
+
Math.sign(tri.vertices.reduce((sum, v, i) => sum + v.tangent[3] * barycentric[i], 0)) || 1;
|
|
1344
|
+
tangent.multiplyScalar(sideSign);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
const key = normalMap.texture.channel === 0 ? "uv0" : "uv1", uvs = tri.vertices.map((v) => v[key]);
|
|
1348
|
+
if (uvs.some((v) => !v))
|
|
1349
|
+
fail("SURFACE_UV_MISSING", `Normal map ${normalMap.texture.uuid} requires UVs`, normalMap.texture.uuid);
|
|
1350
|
+
const m = normalMap.texture.matrix, transform = (uv) => [
|
|
1351
|
+
m[0] * uv[0] + m[3] * uv[1] + m[6],
|
|
1352
|
+
m[1] * uv[0] + m[4] * uv[1] + m[7],
|
|
1353
|
+
];
|
|
1354
|
+
const a = transform(uvs[0]), b = transform(uvs[1]), c = transform(uvs[2]), du1 = b[0] - a[0], dv1 = b[1] - a[1], du2 = c[0] - a[0], dv2 = c[1] - a[1], det = du1 * dv2 - du2 * dv1;
|
|
1355
|
+
if (det === 0)
|
|
1356
|
+
fail("SURFACE_NORMAL_TANGENT_DEGENERATE", "UVs do not define a tangent frame", tri.geometryUuid);
|
|
1357
|
+
const p = tri.vertices.map((v) => new THREE.Vector3(...v.position));
|
|
1358
|
+
const edge1 = p[1].sub(p[0]), edge2 = p[2].sub(p[0]);
|
|
1359
|
+
tangent = edge1
|
|
1360
|
+
.clone()
|
|
1361
|
+
.multiplyScalar(dv2)
|
|
1362
|
+
.addScaledVector(edge2, -dv1)
|
|
1363
|
+
.multiplyScalar(1 / det);
|
|
1364
|
+
const derivativeB = edge2
|
|
1365
|
+
.clone()
|
|
1366
|
+
.multiplyScalar(du1)
|
|
1367
|
+
.addScaledVector(edge1, -du2)
|
|
1368
|
+
.multiplyScalar(1 / det);
|
|
1369
|
+
tangent
|
|
1370
|
+
.addScaledVector(normal, -tangent.dot(normal))
|
|
1371
|
+
.normalize()
|
|
1372
|
+
.multiplyScalar(sideSign);
|
|
1373
|
+
handedness = Math.sign(new THREE.Vector3().crossVectors(normal, tangent).dot(derivativeB));
|
|
1374
|
+
if (!handedness || tangent.lengthSq() === 0)
|
|
1375
|
+
fail("SURFACE_NORMAL_TANGENT_DEGENERATE", "UV derivatives and shading normal cannot define an orthonormal tangent frame", tri.geometryUuid);
|
|
1376
|
+
}
|
|
1377
|
+
const bitangent = new THREE.Vector3()
|
|
1378
|
+
.crossVectors(normal, tangent)
|
|
1379
|
+
.multiplyScalar(handedness * sideSign)
|
|
1380
|
+
.normalize();
|
|
1381
|
+
normal = tangent
|
|
1382
|
+
.multiplyScalar(sampled.x)
|
|
1383
|
+
.addScaledVector(bitangent, sampled.y)
|
|
1384
|
+
.addScaledVector(normal, sampled.z)
|
|
1385
|
+
.normalize();
|
|
1386
|
+
}
|
|
1387
|
+
else
|
|
1388
|
+
fail("SURFACE_NORMAL_MAP_TYPE_UNSUPPORTED", `Unknown normal map type ${material.normalMapType}`, material.uuid);
|
|
1389
|
+
}
|
|
1390
|
+
const alpha = color[3];
|
|
1391
|
+
for (const n of [
|
|
1392
|
+
...color,
|
|
1393
|
+
roughness,
|
|
1394
|
+
metalness,
|
|
1395
|
+
specularIntensity,
|
|
1396
|
+
...specularColor,
|
|
1397
|
+
occlusion,
|
|
1398
|
+
...emissive,
|
|
1399
|
+
...normal.toArray(),
|
|
1400
|
+
])
|
|
1401
|
+
finite(n, material.uuid, "sample result");
|
|
1402
|
+
if (normal.lengthSq() === 0)
|
|
1403
|
+
fail("SURFACE_NORMAL_INVALID", "Sampled normal has zero length", material.uuid);
|
|
1404
|
+
const alphaDiscarded = material.alphaTest > 0 && alpha <= material.alphaTest;
|
|
1405
|
+
const coverage = discarded || alphaDiscarded
|
|
1406
|
+
? 0
|
|
1407
|
+
: material.alphaMode === "blend"
|
|
1408
|
+
? alpha
|
|
1409
|
+
: 1;
|
|
1410
|
+
return {
|
|
1411
|
+
materialUuid: material.uuid,
|
|
1412
|
+
discarded: discarded || alphaDiscarded,
|
|
1413
|
+
alphaMode: material.alphaMode,
|
|
1414
|
+
alpha,
|
|
1415
|
+
coverage,
|
|
1416
|
+
baseColor: color.slice(0, 3),
|
|
1417
|
+
roughness,
|
|
1418
|
+
metalness,
|
|
1419
|
+
specularIntensity,
|
|
1420
|
+
specularColor,
|
|
1421
|
+
occlusion,
|
|
1422
|
+
emissive,
|
|
1423
|
+
normal: vec(normal.normalize()),
|
|
1424
|
+
};
|
|
1425
|
+
}
|