three-cad-viewer 4.1.2 → 4.3.0
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/Readme.md +12 -5
- package/dist/camera/camera.d.ts +14 -2
- package/dist/core/studio-manager.d.ts +90 -0
- package/dist/core/types.d.ts +239 -9
- package/dist/core/viewer-state.d.ts +28 -2
- package/dist/core/viewer.d.ts +200 -6
- package/dist/index.d.ts +7 -2
- package/dist/rendering/environment.d.ts +239 -0
- package/dist/rendering/light-detection.d.ts +44 -0
- package/dist/rendering/material-factory.d.ts +77 -2
- package/dist/rendering/material-presets.d.ts +32 -0
- package/dist/rendering/room-environment.d.ts +13 -0
- package/dist/rendering/studio-composer.d.ts +130 -0
- package/dist/rendering/studio-floor.d.ts +53 -0
- package/dist/rendering/texture-cache.d.ts +100 -0
- package/dist/rendering/triplanar.d.ts +37 -0
- package/dist/scene/animation.d.ts +1 -1
- package/dist/scene/clipping.d.ts +31 -0
- package/dist/scene/nestedgroup.d.ts +63 -27
- package/dist/scene/objectgroup.d.ts +47 -0
- package/dist/three-cad-viewer.css +339 -29
- package/dist/three-cad-viewer.esm.js +26944 -11874
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +10 -4
- package/dist/three-cad-viewer.js +26863 -11787
- package/dist/three-cad-viewer.min.js +10 -4
- package/dist/ui/display.d.ts +147 -0
- package/dist/utils/decode-instances.d.ts +60 -0
- package/dist/utils/utils.d.ts +10 -0
- package/package.json +4 -2
- package/src/_version.ts +1 -1
- package/src/camera/camera.ts +27 -10
- package/src/core/studio-manager.ts +652 -0
- package/src/core/types.ts +302 -9
- package/src/core/viewer-state.ts +84 -4
- package/src/core/viewer.ts +453 -22
- package/src/index.ts +24 -1
- package/src/rendering/environment.ts +840 -0
- package/src/rendering/light-detection.ts +327 -0
- package/src/rendering/material-factory.ts +458 -2
- package/src/rendering/material-presets.ts +289 -0
- package/src/rendering/raycast.ts +2 -2
- package/src/rendering/room-environment.ts +192 -0
- package/src/rendering/studio-composer.ts +577 -0
- package/src/rendering/studio-floor.ts +108 -0
- package/src/rendering/texture-cache.ts +319 -0
- package/src/rendering/triplanar.ts +329 -0
- package/src/scene/animation.ts +3 -2
- package/src/scene/clipping.ts +59 -0
- package/src/scene/nestedgroup.ts +392 -0
- package/src/scene/objectgroup.ts +186 -11
- package/src/scene/orientation.ts +12 -0
- package/src/scene/render-shape.ts +55 -21
- package/src/types/n8ao.d.ts +28 -0
- package/src/ui/display.ts +1032 -27
- package/src/ui/index.html +181 -44
- package/src/utils/decode-instances.ts +233 -0
- package/src/utils/utils.ts +33 -20
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { gpuTracker } from "../utils/gpu-tracker.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Constants
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Texture fields that carry sRGB color data.
|
|
11
|
+
*
|
|
12
|
+
* When a texture is used for one of these roles, its `colorSpace` must be set
|
|
13
|
+
* to `SRGBColorSpace` so Three.js applies the sRGB-to-linear decode on
|
|
14
|
+
* sampling. All other texture roles (normal, metallic-roughness, occlusion,
|
|
15
|
+
* thickness, transmission, roughness maps) remain `LinearSRGBColorSpace`.
|
|
16
|
+
*/
|
|
17
|
+
const SRGB_TEXTURE_ROLES = new Set([
|
|
18
|
+
"baseColorTexture",
|
|
19
|
+
"emissiveTexture",
|
|
20
|
+
"sheenColorTexture",
|
|
21
|
+
"specularColorTexture",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Three.js MeshPhysicalMaterial map property names that carry sRGB color data.
|
|
26
|
+
*
|
|
27
|
+
* Used by threejs-materials integration where texture params use Three.js property
|
|
28
|
+
* names directly (e.g., "map", "emissiveMap") instead of MaterialAppearance
|
|
29
|
+
* role names (e.g., "baseColorTexture", "emissiveTexture").
|
|
30
|
+
*/
|
|
31
|
+
const THREEJS_SRGB_MAPS = new Set([
|
|
32
|
+
"map",
|
|
33
|
+
"emissiveMap",
|
|
34
|
+
"sheenColorMap",
|
|
35
|
+
"specularColorMap",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// TextureCache
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Manages loading, caching, and lifecycle of all Studio mode textures.
|
|
44
|
+
*
|
|
45
|
+
* The TextureCache is the **sole owner** of all THREE.Texture objects used by
|
|
46
|
+
* Studio mode. Materials reference textures but never dispose them directly.
|
|
47
|
+
* Only TextureCache.dispose() / disposeFull() disposes GPU texture resources.
|
|
48
|
+
*
|
|
49
|
+
* Resolution order for texture reference strings:
|
|
50
|
+
* 1. `data:` prefix -- treat as data URI, load directly
|
|
51
|
+
* 2. Otherwise -- treat as URL, resolve relative to HTML page
|
|
52
|
+
*
|
|
53
|
+
* Features:
|
|
54
|
+
* - In-flight promise deduplication (no duplicate loads for the same key)
|
|
55
|
+
* - Correct colorSpace assignment per texture semantic role
|
|
56
|
+
* - GPU resource tracking via gpuTracker
|
|
57
|
+
*/
|
|
58
|
+
class TextureCache {
|
|
59
|
+
/** Textures cache (disposed on clear/dispose, rebuilt per shape data) */
|
|
60
|
+
private _cache: Map<string, THREE.Texture> = new Map();
|
|
61
|
+
|
|
62
|
+
/** In-flight load promises keyed by cache key */
|
|
63
|
+
private _inflight: Map<string, Promise<THREE.Texture>> = new Map();
|
|
64
|
+
|
|
65
|
+
/** THREE.TextureLoader instance (created lazily) */
|
|
66
|
+
private _textureLoader: THREE.TextureLoader | null = null;
|
|
67
|
+
|
|
68
|
+
/** Whether this cache has been fully disposed */
|
|
69
|
+
private _disposed = false;
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Public API
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a texture reference string and return a cached or newly loaded
|
|
77
|
+
* THREE.Texture with the correct colorSpace set.
|
|
78
|
+
*
|
|
79
|
+
* @param ref - Texture reference string (table key, data URI, or URL)
|
|
80
|
+
* @param textureRole - The texture role name (MaterialAppearance field name or proxy role)
|
|
81
|
+
* (e.g. "baseColorTexture", "normalTexture"). Used to determine colorSpace.
|
|
82
|
+
* @returns The resolved THREE.Texture, or null if the reference is invalid
|
|
83
|
+
* or loading fails
|
|
84
|
+
*/
|
|
85
|
+
async get(
|
|
86
|
+
ref: string,
|
|
87
|
+
textureRole: string,
|
|
88
|
+
): Promise<THREE.Texture | null> {
|
|
89
|
+
if (this._disposed) {
|
|
90
|
+
logger.warn("TextureCache.get() called after dispose");
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!ref) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Determine the target color space based on the texture role
|
|
99
|
+
const colorSpace = SRGB_TEXTURE_ROLES.has(textureRole)
|
|
100
|
+
? THREE.SRGBColorSpace
|
|
101
|
+
: THREE.LinearSRGBColorSpace;
|
|
102
|
+
|
|
103
|
+
// 1. data: prefix (data URI)
|
|
104
|
+
if (ref.startsWith("data:")) {
|
|
105
|
+
return this._getFromDataUri(ref, colorSpace);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2. URL (relative to HTML page)
|
|
109
|
+
return this._getFromUrl(ref, colorSpace);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Dispose textures (called on viewer.clear() when shape data is replaced).
|
|
114
|
+
*
|
|
115
|
+
* Disposes all textures in the cache and clears in-flight promises.
|
|
116
|
+
*/
|
|
117
|
+
dispose(): void {
|
|
118
|
+
for (const [key, texture] of this._cache) {
|
|
119
|
+
gpuTracker.untrack("texture", texture);
|
|
120
|
+
texture.dispose();
|
|
121
|
+
logger.debug(`Disposed texture: ${key}`);
|
|
122
|
+
}
|
|
123
|
+
this._cache.clear();
|
|
124
|
+
|
|
125
|
+
// Clear in-flight promises (they may resolve but won't be used)
|
|
126
|
+
this._inflight.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Dispose all textures.
|
|
131
|
+
*
|
|
132
|
+
* Called on viewer.dispose() when the viewer is fully torn down.
|
|
133
|
+
* After this call, the TextureCache cannot be used again.
|
|
134
|
+
*/
|
|
135
|
+
disposeFull(): void {
|
|
136
|
+
this._disposed = true;
|
|
137
|
+
this.dispose();
|
|
138
|
+
this._textureLoader = null;
|
|
139
|
+
logger.debug("TextureCache fully disposed");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Private: Data URI loading
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Load a texture from a data URI string.
|
|
148
|
+
*/
|
|
149
|
+
private async _getFromDataUri(
|
|
150
|
+
dataUri: string,
|
|
151
|
+
colorSpace: THREE.ColorSpace,
|
|
152
|
+
): Promise<THREE.Texture | null> {
|
|
153
|
+
// Use the data URI itself as the cache key
|
|
154
|
+
const cacheKey = dataUri;
|
|
155
|
+
|
|
156
|
+
const cached = this._cache.get(cacheKey);
|
|
157
|
+
if (cached) {
|
|
158
|
+
cached.colorSpace = colorSpace;
|
|
159
|
+
return cached;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inflight = this._inflight.get(cacheKey);
|
|
163
|
+
if (inflight) {
|
|
164
|
+
const texture = await inflight;
|
|
165
|
+
texture.colorSpace = colorSpace;
|
|
166
|
+
return texture;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return this._loadAndCache(cacheKey, dataUri, colorSpace);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Private: URL loading
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Load a texture from a URL (resolved relative to the HTML page).
|
|
178
|
+
*/
|
|
179
|
+
private async _getFromUrl(
|
|
180
|
+
url: string,
|
|
181
|
+
colorSpace: THREE.ColorSpace,
|
|
182
|
+
): Promise<THREE.Texture | null> {
|
|
183
|
+
const cached = this._cache.get(url);
|
|
184
|
+
if (cached) {
|
|
185
|
+
cached.colorSpace = colorSpace;
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const inflight = this._inflight.get(url);
|
|
190
|
+
if (inflight) {
|
|
191
|
+
const texture = await inflight;
|
|
192
|
+
texture.colorSpace = colorSpace;
|
|
193
|
+
return texture;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return this._loadAndCache(url, url, colorSpace);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Private: Core loading
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load a texture from a source (URL or data URI), cache it, and return it.
|
|
205
|
+
*
|
|
206
|
+
* Deduplicates in-flight loads for the same cache key.
|
|
207
|
+
*
|
|
208
|
+
* @param cacheKey - Key for the user cache
|
|
209
|
+
* @param source - URL or data URI to load from
|
|
210
|
+
* @param colorSpace - Color space to assign to the loaded texture
|
|
211
|
+
* @returns The loaded texture, or null on failure
|
|
212
|
+
*/
|
|
213
|
+
private async _loadAndCache(
|
|
214
|
+
cacheKey: string,
|
|
215
|
+
source: string,
|
|
216
|
+
colorSpace: THREE.ColorSpace,
|
|
217
|
+
): Promise<THREE.Texture | null> {
|
|
218
|
+
const promise = this._doLoad(source, colorSpace);
|
|
219
|
+
this._inflight.set(cacheKey, promise);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const texture = await promise;
|
|
223
|
+
|
|
224
|
+
// Check if disposed while loading:
|
|
225
|
+
// - disposeFull() sets _disposed
|
|
226
|
+
// - dispose() clears _inflight (so our entry is gone)
|
|
227
|
+
if (this._disposed || !this._inflight.has(cacheKey)) {
|
|
228
|
+
texture.dispose();
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Cache and track
|
|
233
|
+
this._cache.set(cacheKey, texture);
|
|
234
|
+
const label = cacheKey.startsWith("data:")
|
|
235
|
+
? `Texture (data URI, ${cacheKey.length} chars)`
|
|
236
|
+
: `Texture: ${cacheKey}`;
|
|
237
|
+
gpuTracker.trackTexture(texture, label);
|
|
238
|
+
logger.debug(`Loaded and cached texture: ${label}`);
|
|
239
|
+
|
|
240
|
+
return texture;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (this._disposed) return null;
|
|
243
|
+
|
|
244
|
+
const displayKey = cacheKey.startsWith("data:")
|
|
245
|
+
? `data URI (${cacheKey.length} chars)`
|
|
246
|
+
: cacheKey;
|
|
247
|
+
logger.warn(
|
|
248
|
+
`Failed to load texture "${displayKey}":`,
|
|
249
|
+
error instanceof Error ? error.message : error,
|
|
250
|
+
);
|
|
251
|
+
return null;
|
|
252
|
+
} finally {
|
|
253
|
+
this._inflight.delete(cacheKey);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Perform the actual texture load via THREE.TextureLoader.
|
|
259
|
+
*
|
|
260
|
+
* THREE.TextureLoader handles both URLs and data URIs.
|
|
261
|
+
*/
|
|
262
|
+
private _doLoad(
|
|
263
|
+
source: string,
|
|
264
|
+
colorSpace: THREE.ColorSpace,
|
|
265
|
+
): Promise<THREE.Texture> {
|
|
266
|
+
const loader = this._ensureTextureLoader();
|
|
267
|
+
|
|
268
|
+
return new Promise<THREE.Texture>((resolve, reject) => {
|
|
269
|
+
loader.load(
|
|
270
|
+
source,
|
|
271
|
+
(texture) => {
|
|
272
|
+
texture.colorSpace = colorSpace;
|
|
273
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
274
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
275
|
+
resolve(texture);
|
|
276
|
+
},
|
|
277
|
+
undefined, // onProgress (not used)
|
|
278
|
+
(error) => {
|
|
279
|
+
reject(
|
|
280
|
+
error instanceof Error
|
|
281
|
+
? error
|
|
282
|
+
: new Error(`Texture load failed: ${source.substring(0, 100)}`),
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Private: Utilities
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get or create the THREE.TextureLoader (lazy initialization).
|
|
295
|
+
*/
|
|
296
|
+
private _ensureTextureLoader(): THREE.TextureLoader {
|
|
297
|
+
if (!this._textureLoader) {
|
|
298
|
+
this._textureLoader = new THREE.TextureLoader();
|
|
299
|
+
logger.debug("Created TextureLoader");
|
|
300
|
+
}
|
|
301
|
+
return this._textureLoader;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get the correct color space for a Three.js material map property name.
|
|
308
|
+
*
|
|
309
|
+
* sRGB maps (color data): map, emissiveMap, sheenColorMap, specularColorMap
|
|
310
|
+
* Linear maps (non-color data): everything else (normalMap, roughnessMap, etc.)
|
|
311
|
+
*
|
|
312
|
+
* @param mapName - Three.js material property name (e.g., "map", "normalMap")
|
|
313
|
+
* @returns THREE.SRGBColorSpace or THREE.LinearSRGBColorSpace
|
|
314
|
+
*/
|
|
315
|
+
function getColorSpaceForMap(mapName: string): THREE.ColorSpace {
|
|
316
|
+
return THREEJS_SRGB_MAPS.has(mapName) ? THREE.SRGBColorSpace : THREE.LinearSRGBColorSpace;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export { TextureCache, SRGB_TEXTURE_ROLES, getColorSpaceForMap };
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triplanar texture mapping for MeshPhysicalMaterial.
|
|
3
|
+
*
|
|
4
|
+
* Replaces standard UV-based texture sampling with model-space triplanar
|
|
5
|
+
* projection via `material.onBeforeCompile`. Eliminates seams on curved
|
|
6
|
+
* surfaces (cylinders, cones, etc.) and maintains uniform texture scale
|
|
7
|
+
* regardless of object proportions.
|
|
8
|
+
*
|
|
9
|
+
* All coordinates are in **model space** to match the geometry's bounding box.
|
|
10
|
+
* `transformed` (model-space position) and `objectNormal` (model-space normal)
|
|
11
|
+
* are used — NOT the view-space `transformedNormal`.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: `onBeforeCompile` receives shaders BEFORE `#include` resolution.
|
|
14
|
+
* Therefore we replace `#include <chunk_name>` directives with inline GLSL
|
|
15
|
+
* that uses triplanar sampling, rather than replacing expanded texture2D calls.
|
|
16
|
+
*
|
|
17
|
+
* Handles: map, normalMap, roughnessMap, metalnessMap, emissiveMap, aoMap,
|
|
18
|
+
* clearcoatNormalMap, alphaMap.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// GLSL: Varyings (shared between vertex & fragment)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const TRIPLANAR_VARYINGS = /* glsl */ `
|
|
28
|
+
varying vec3 vTriplanarPos;
|
|
29
|
+
varying vec3 vTriplanarNormal;
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// GLSL: Fragment header (uniforms + helper functions)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const TRIPLANAR_FRAGMENT_HEADER = /* glsl */ `
|
|
37
|
+
varying vec3 vTriplanarPos;
|
|
38
|
+
varying vec3 vTriplanarNormal;
|
|
39
|
+
uniform vec3 triplanarOffset;
|
|
40
|
+
uniform float triplanarScale;
|
|
41
|
+
uniform vec2 triplanarRepeat;
|
|
42
|
+
|
|
43
|
+
// normalMatrix is only declared in the fragment shader for object-space
|
|
44
|
+
// normal maps. We need it for triplanar tangent-space normal mapping too.
|
|
45
|
+
#ifndef USE_NORMALMAP_OBJECTSPACE
|
|
46
|
+
uniform mat3 normalMatrix;
|
|
47
|
+
#endif
|
|
48
|
+
|
|
49
|
+
// --- Global triplanar state (computed once, reused by all samples) ---
|
|
50
|
+
vec2 tri_uvX, tri_uvY, tri_uvZ;
|
|
51
|
+
vec3 tri_blend;
|
|
52
|
+
|
|
53
|
+
// Initialize global triplanar UVs and blend weights from varyings.
|
|
54
|
+
// Must be called before any texture sampling.
|
|
55
|
+
void initTriplanarUVs() {
|
|
56
|
+
tri_blend = abs(vTriplanarNormal);
|
|
57
|
+
tri_blend = pow(tri_blend, vec3(3.0));
|
|
58
|
+
tri_blend /= (tri_blend.x + tri_blend.y + tri_blend.z);
|
|
59
|
+
vec3 p = (vTriplanarPos - triplanarOffset) * triplanarScale;
|
|
60
|
+
vec2 r = triplanarRepeat;
|
|
61
|
+
tri_uvX = p.yz * r;
|
|
62
|
+
tri_uvY = p.xz * r;
|
|
63
|
+
tri_uvZ = p.xy * r;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sample a texture using the global triplanar UVs and blend weights.
|
|
67
|
+
vec4 triplanarSample(sampler2D tex) {
|
|
68
|
+
return texture2D(tex, tri_uvX) * tri_blend.x
|
|
69
|
+
+ texture2D(tex, tri_uvY) * tri_blend.y
|
|
70
|
+
+ texture2D(tex, tri_uvZ) * tri_blend.z;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Surface-gradient triplanar normal mapping.
|
|
74
|
+
// Instead of constructing full 3D normals per axis (which creates faceted
|
|
75
|
+
// shading at axis boundaries), we extract the tangent-space perturbation (xy)
|
|
76
|
+
// from each axis sample, project it into model space as a gradient, blend
|
|
77
|
+
// the gradients, and add to the geometric normal. Gradients blend smoothly.
|
|
78
|
+
// Ref: Morten Mikkelsen, "Surface Gradient Based Bump Mapping Framework"
|
|
79
|
+
//
|
|
80
|
+
// normalScale is declared in normalmap_pars_fragment (after this header),
|
|
81
|
+
// so we accept it as a parameter to avoid forward-reference errors.
|
|
82
|
+
vec3 triplanarNormal(sampler2D normalTex, vec2 nScale) {
|
|
83
|
+
vec3 N = vTriplanarNormal;
|
|
84
|
+
|
|
85
|
+
// Sample tangent-space normals for each projection axis
|
|
86
|
+
vec3 tnX = texture2D(normalTex, tri_uvX).xyz * 2.0 - 1.0;
|
|
87
|
+
vec3 tnY = texture2D(normalTex, tri_uvY).xyz * 2.0 - 1.0;
|
|
88
|
+
vec3 tnZ = texture2D(normalTex, tri_uvZ).xyz * 2.0 - 1.0;
|
|
89
|
+
|
|
90
|
+
// Apply normal scale to perturbation components
|
|
91
|
+
tnX.xy *= nScale;
|
|
92
|
+
tnY.xy *= nScale;
|
|
93
|
+
tnZ.xy *= nScale;
|
|
94
|
+
|
|
95
|
+
// Project each tangent-space perturbation (xy) into model space.
|
|
96
|
+
// X proj: UV=(y,z) -> tangent=(0,1,0), bitangent=(0,0,1) -> grad=(0, tx, ty)
|
|
97
|
+
// Y proj: UV=(x,z) -> tangent=(1,0,0), bitangent=(0,0,1) -> grad=(tx, 0, ty)
|
|
98
|
+
// Z proj: UV=(x,y) -> tangent=(1,0,0), bitangent=(0,1,0) -> grad=(tx, ty, 0)
|
|
99
|
+
vec3 surfGrad =
|
|
100
|
+
vec3(0.0, tnX.x, tnX.y) * tri_blend.x +
|
|
101
|
+
vec3(tnY.x, 0.0, tnY.y) * tri_blend.y +
|
|
102
|
+
vec3(tnZ.x, tnZ.y, 0.0) * tri_blend.z;
|
|
103
|
+
|
|
104
|
+
return normalize(N + surfGrad);
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// GLSL: UV initialization injection
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Injected after #include <logdepthbuf_fragment>.
|
|
114
|
+
* This is the earliest reliable point in the fragment shader before any
|
|
115
|
+
* texture sampling. Initializes global triplanar UVs.
|
|
116
|
+
*/
|
|
117
|
+
const UV_INIT = /* glsl */ `
|
|
118
|
+
#include <logdepthbuf_fragment>
|
|
119
|
+
initTriplanarUVs();
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// GLSL: Replacement chunks (inline GLSL replacing #include directives)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/** Replaces #include <map_fragment> */
|
|
127
|
+
const MAP_FRAGMENT = /* glsl */ `
|
|
128
|
+
#ifdef USE_MAP
|
|
129
|
+
vec4 sampledDiffuseColor = triplanarSample( map );
|
|
130
|
+
#ifdef DECODE_VIDEO_TEXTURE
|
|
131
|
+
sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );
|
|
132
|
+
#endif
|
|
133
|
+
diffuseColor *= sampledDiffuseColor;
|
|
134
|
+
#endif
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
/** Replaces #include <roughnessmap_fragment> */
|
|
138
|
+
const ROUGHNESSMAP_FRAGMENT = /* glsl */ `
|
|
139
|
+
float roughnessFactor = roughness;
|
|
140
|
+
#ifdef USE_ROUGHNESSMAP
|
|
141
|
+
vec4 texelRoughness = triplanarSample( roughnessMap );
|
|
142
|
+
roughnessFactor *= texelRoughness.g;
|
|
143
|
+
#endif
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
/** Replaces #include <metalnessmap_fragment> */
|
|
147
|
+
const METALNESSMAP_FRAGMENT = /* glsl */ `
|
|
148
|
+
float metalnessFactor = metalness;
|
|
149
|
+
#ifdef USE_METALNESSMAP
|
|
150
|
+
vec4 texelMetalness = triplanarSample( metalnessMap );
|
|
151
|
+
metalnessFactor *= texelMetalness.b;
|
|
152
|
+
#endif
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
/** Replaces #include <emissivemap_fragment> */
|
|
156
|
+
const EMISSIVEMAP_FRAGMENT = /* glsl */ `
|
|
157
|
+
#ifdef USE_EMISSIVEMAP
|
|
158
|
+
vec4 emissiveColor = triplanarSample( emissiveMap );
|
|
159
|
+
#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE
|
|
160
|
+
emissiveColor = sRGBTransferEOTF( emissiveColor );
|
|
161
|
+
#endif
|
|
162
|
+
totalEmissiveRadiance *= emissiveColor.rgb;
|
|
163
|
+
#endif
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
/** Replaces #include <aomap_fragment> */
|
|
167
|
+
const AOMAP_FRAGMENT = /* glsl */ `
|
|
168
|
+
#ifdef USE_AOMAP
|
|
169
|
+
float ambientOcclusion = ( triplanarSample( aoMap ).r - 1.0 ) * aoMapIntensity + 1.0;
|
|
170
|
+
reflectedLight.indirectDiffuse *= ambientOcclusion;
|
|
171
|
+
#if defined( USE_CLEARCOAT )
|
|
172
|
+
clearcoatSpecularIndirect *= ambientOcclusion;
|
|
173
|
+
#endif
|
|
174
|
+
#if defined( USE_SHEEN )
|
|
175
|
+
sheenSpecularIndirect *= ambientOcclusion;
|
|
176
|
+
#endif
|
|
177
|
+
#if defined( USE_ENVMAP ) && defined( STANDARD )
|
|
178
|
+
float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
|
|
179
|
+
reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
|
|
180
|
+
#endif
|
|
181
|
+
#endif
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replaces #include <normal_fragment_maps>.
|
|
186
|
+
* Object-space and bumpmap paths kept as-is.
|
|
187
|
+
* Tangent-space path: triplanar normal mapping samples the normal map 3x
|
|
188
|
+
* (one per projection axis), swizzles each to model space, blends, and
|
|
189
|
+
* transforms to view space via normalMatrix.
|
|
190
|
+
*/
|
|
191
|
+
const NORMAL_FRAGMENT_MAPS = /* glsl */ `
|
|
192
|
+
#ifdef USE_NORMALMAP_OBJECTSPACE
|
|
193
|
+
normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;
|
|
194
|
+
#ifdef FLIP_SIDED
|
|
195
|
+
normal = - normal;
|
|
196
|
+
#endif
|
|
197
|
+
#ifdef DOUBLE_SIDED
|
|
198
|
+
normal = normal * faceDirection;
|
|
199
|
+
#endif
|
|
200
|
+
normal = normalize( normalMatrix * normal );
|
|
201
|
+
|
|
202
|
+
#elif defined( USE_NORMALMAP_TANGENTSPACE )
|
|
203
|
+
// Triplanar normal mapping: sample normal map 3x (one per projection axis),
|
|
204
|
+
// swizzle each to model space, blend, and transform to view space.
|
|
205
|
+
normal = normalize(normalMatrix * triplanarNormal(normalMap, normalScale));
|
|
206
|
+
|
|
207
|
+
#elif defined( USE_BUMPMAP )
|
|
208
|
+
normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );
|
|
209
|
+
|
|
210
|
+
#endif
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Replaces #include <clearcoat_normal_fragment_maps>.
|
|
215
|
+
* Triplanar clearcoat normal mapping using surface gradient blending.
|
|
216
|
+
*/
|
|
217
|
+
const CLEARCOAT_NORMAL_FRAGMENT_MAPS = /* glsl */ `
|
|
218
|
+
#ifdef USE_CLEARCOAT_NORMALMAP
|
|
219
|
+
// Reuse triplanarNormal with clearcoat normal map and scale
|
|
220
|
+
vec3 clearcoatModelNormal = triplanarNormal(clearcoatNormalMap, clearcoatNormalScale);
|
|
221
|
+
clearcoatNormal = normalize(normalMatrix * clearcoatModelNormal);
|
|
222
|
+
#endif
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
/** Replaces #include <alphamap_fragment> */
|
|
226
|
+
const ALPHAMAP_FRAGMENT = /* glsl */ `
|
|
227
|
+
#ifdef USE_ALPHAMAP
|
|
228
|
+
diffuseColor.a *= triplanarSample( alphaMap ).g;
|
|
229
|
+
#endif
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Chunk replacement table
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/** Fragment shader chunk replacements */
|
|
237
|
+
const FRAGMENT_CHUNK_REPLACEMENTS: [string, string][] = [
|
|
238
|
+
["#include <logdepthbuf_fragment>", UV_INIT],
|
|
239
|
+
["#include <map_fragment>", MAP_FRAGMENT],
|
|
240
|
+
["#include <roughnessmap_fragment>", ROUGHNESSMAP_FRAGMENT],
|
|
241
|
+
["#include <metalnessmap_fragment>", METALNESSMAP_FRAGMENT],
|
|
242
|
+
["#include <emissivemap_fragment>", EMISSIVEMAP_FRAGMENT],
|
|
243
|
+
["#include <aomap_fragment>", AOMAP_FRAGMENT],
|
|
244
|
+
["#include <normal_fragment_maps>", NORMAL_FRAGMENT_MAPS],
|
|
245
|
+
["#include <clearcoat_normal_fragment_maps>", CLEARCOAT_NORMAL_FRAGMENT_MAPS],
|
|
246
|
+
["#include <alphamap_fragment>", ALPHAMAP_FRAGMENT],
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Public API
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Apply triplanar texture mapping to a MeshPhysicalMaterial.
|
|
255
|
+
*
|
|
256
|
+
* Modifies the material's shader via `onBeforeCompile` so that all texture
|
|
257
|
+
* lookups use model-space triplanar projection instead of UV coordinates.
|
|
258
|
+
* The material should be a **clone** (not shared) because `onBeforeCompile`
|
|
259
|
+
* and `customProgramCacheKey` are set on it.
|
|
260
|
+
*
|
|
261
|
+
* Bounding box of the geometry determines coordinate normalization: the
|
|
262
|
+
* texture tiles once across the largest dimension with uniform scale,
|
|
263
|
+
* preserving aspect ratio. `textureRepeat` on the material's textures
|
|
264
|
+
* (if set) is respected via the triplanarRepeat uniform.
|
|
265
|
+
*
|
|
266
|
+
* @param material - Material clone to modify
|
|
267
|
+
* @param geometry - Geometry for bounding box computation
|
|
268
|
+
*/
|
|
269
|
+
export function applyTriplanarMapping(
|
|
270
|
+
material: THREE.MeshPhysicalMaterial,
|
|
271
|
+
geometry: THREE.BufferGeometry,
|
|
272
|
+
): void {
|
|
273
|
+
// Compute bounding box for coordinate normalization (model space)
|
|
274
|
+
geometry.computeBoundingBox();
|
|
275
|
+
const bb = geometry.boundingBox!;
|
|
276
|
+
const size = new THREE.Vector3();
|
|
277
|
+
bb.getSize(size);
|
|
278
|
+
const maxDim = Math.max(size.x, size.y, size.z, 1e-6);
|
|
279
|
+
|
|
280
|
+
// Uniform values (captured by closure, per-object)
|
|
281
|
+
const offset = bb.min.clone();
|
|
282
|
+
const scale = 1.0 / maxDim;
|
|
283
|
+
|
|
284
|
+
// Read texture repeat from the material's map (all textures share same repeat)
|
|
285
|
+
const repeat = material.map?.repeat?.clone() ?? new THREE.Vector2(1, 1);
|
|
286
|
+
|
|
287
|
+
material.onBeforeCompile = (shader) => {
|
|
288
|
+
// Custom uniforms
|
|
289
|
+
shader.uniforms.triplanarOffset = { value: offset };
|
|
290
|
+
shader.uniforms.triplanarScale = { value: scale };
|
|
291
|
+
shader.uniforms.triplanarRepeat = { value: repeat };
|
|
292
|
+
|
|
293
|
+
// --- Vertex shader ---
|
|
294
|
+
// Declare varyings
|
|
295
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
296
|
+
"#include <common>",
|
|
297
|
+
`#include <common>\n${TRIPLANAR_VARYINGS}`,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Pass model-space position and normal to fragment shader.
|
|
301
|
+
// `transformed` = model-space position (after morphing, before view transform)
|
|
302
|
+
// `objectNormal` = model-space normal (before normalMatrix)
|
|
303
|
+
// Both match the geometry's bounding box coordinate space.
|
|
304
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
305
|
+
"#include <worldpos_vertex>",
|
|
306
|
+
`#include <worldpos_vertex>
|
|
307
|
+
vTriplanarPos = transformed;
|
|
308
|
+
vTriplanarNormal = normalize(objectNormal);`,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// --- Fragment shader ---
|
|
312
|
+
// Inject varyings, uniforms, and the triplanarSample helper
|
|
313
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
314
|
+
"#include <common>",
|
|
315
|
+
`#include <common>\n${TRIPLANAR_FRAGMENT_HEADER}`,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Replace texture-sampling #include chunks with triplanar versions
|
|
319
|
+
for (const [from, to] of FRAGMENT_CHUNK_REPLACEMENTS) {
|
|
320
|
+
shader.fragmentShader = shader.fragmentShader.replace(from, to);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// All triplanar materials share the same compiled WebGL program
|
|
325
|
+
// (uniform values differ per instance). Appended to Three.js's standard
|
|
326
|
+
// program cache key, so materials with different maps/flags still get
|
|
327
|
+
// separate programs.
|
|
328
|
+
material.customProgramCacheKey = () => "triplanar";
|
|
329
|
+
}
|
package/src/scene/animation.ts
CHANGED
|
@@ -30,7 +30,7 @@ class Animation {
|
|
|
30
30
|
mixer: THREE.AnimationMixer | null;
|
|
31
31
|
clip: THREE.AnimationClip | null;
|
|
32
32
|
clipAction: THREE.AnimationAction | null;
|
|
33
|
-
clock: THREE.
|
|
33
|
+
clock: THREE.Timer;
|
|
34
34
|
duration: number | null;
|
|
35
35
|
speed: number | null;
|
|
36
36
|
repeat: boolean | null;
|
|
@@ -47,7 +47,7 @@ class Animation {
|
|
|
47
47
|
this.mixer = null;
|
|
48
48
|
this.clip = null;
|
|
49
49
|
this.clipAction = null;
|
|
50
|
-
this.clock = new THREE.
|
|
50
|
+
this.clock = new THREE.Timer();
|
|
51
51
|
this.duration = null;
|
|
52
52
|
this._backup = null;
|
|
53
53
|
this.root = null;
|
|
@@ -334,6 +334,7 @@ class Animation {
|
|
|
334
334
|
*/
|
|
335
335
|
update(): void {
|
|
336
336
|
if (this.mixer) {
|
|
337
|
+
this.clock.update();
|
|
337
338
|
this.mixer.update(this.clock.getDelta());
|
|
338
339
|
}
|
|
339
340
|
}
|