@fps-games/editor 0.1.3-beta.1 → 0.1.3-beta.2
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/node_modules/@fps-games/babylon-renderer/dist/index.d.ts +5 -0
- package/node_modules/@fps-games/babylon-renderer/dist/index.d.ts.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/index.js +5 -0
- package/node_modules/@fps-games/babylon-renderer/dist/index.js.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.d.ts +17 -0
- package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.d.ts.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.js +64 -0
- package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.js.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.d.ts +7 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.d.ts.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.js +176 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.js.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.d.ts +5 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.d.ts.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.js +837 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.js.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.d.ts +96 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.d.ts.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.js +45 -0
- package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.js.map +1 -0
- package/node_modules/@fps-games/babylon-renderer/package.json +20 -0
- package/node_modules/@fps-games/editor-babylon/package.json +5 -5
- package/node_modules/@fps-games/editor-browser/package.json +2 -2
- package/node_modules/@fps-games/editor-core/package.json +2 -2
- package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
- package/node_modules/@fps-games/editor-protocol/package.json +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { Constants } from '@babylonjs/core/Engines/constants';
|
|
2
|
+
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight';
|
|
3
|
+
import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
|
|
4
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
5
|
+
import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
|
|
6
|
+
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
|
|
7
|
+
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
|
8
|
+
import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
|
|
9
|
+
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
10
|
+
import { VertexData } from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
11
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
12
|
+
import { VertexBuffer } from '@babylonjs/core/Buffers/buffer';
|
|
13
|
+
import { getPlanarShadowShaderAttributes, getPlanarShadowShaderSamplers, getPlanarShadowShaderUniforms, getPlanarShadowSkeletonDefines, PLANAR_SHADOW_SHADER_NAME, registerPlanarShadowShaders, } from './planar-shadow-shaders';
|
|
14
|
+
import { DEFAULT_PLANAR_SHADOW_OPTIONS, } from './types';
|
|
15
|
+
const PLANAR_SHADOW_ALPHA_INDEX = 4;
|
|
16
|
+
const DEFAULT_SHADOW_DIRECTION = new Vector3(-0.3, -1, -0.2).normalize();
|
|
17
|
+
export function createPlanarShadowSystem(scene, directionalLight, options = {}) {
|
|
18
|
+
return new BabylonPlanarShadowSystem(scene, directionalLight, options);
|
|
19
|
+
}
|
|
20
|
+
class BabylonPlanarShadowSystem {
|
|
21
|
+
constructor(scene, directionalLight, options) {
|
|
22
|
+
this.scene = scene;
|
|
23
|
+
this.directionalLight = directionalLight;
|
|
24
|
+
this.baseMaterial = null;
|
|
25
|
+
this.flatMaterial = null;
|
|
26
|
+
this.materials = new Set();
|
|
27
|
+
this.casters = new Map();
|
|
28
|
+
this.receivers = new Map();
|
|
29
|
+
this.syncObservers = new Map();
|
|
30
|
+
this.renderObserver = null;
|
|
31
|
+
this.meshAddedObserver = null;
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
this.casterPatterns = [];
|
|
34
|
+
this.resolvedPlaneNormal = Vector3.Up();
|
|
35
|
+
this.resolvedPlaneHeight = 0;
|
|
36
|
+
this.options = resolvePlanarShadowOptions(options);
|
|
37
|
+
this.casterPatterns = [...this.options.casters.includePatterns];
|
|
38
|
+
}
|
|
39
|
+
initialize() {
|
|
40
|
+
if (this.initialized || !this.options.enabled)
|
|
41
|
+
return;
|
|
42
|
+
registerPlanarShadowShaders();
|
|
43
|
+
if (this.options.stencil.enabled)
|
|
44
|
+
this.setupStencil();
|
|
45
|
+
this.baseMaterial = this.createShadowMaterial();
|
|
46
|
+
this.materials.add(this.baseMaterial);
|
|
47
|
+
this.renderObserver = this.scene.onBeforeRenderObservable.add(() => this.updateUniforms());
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
this.refresh();
|
|
50
|
+
}
|
|
51
|
+
addCaster(mesh) {
|
|
52
|
+
const root = this.findCasterRoot(mesh);
|
|
53
|
+
if (!this.initialized || this.casters.has(root))
|
|
54
|
+
return;
|
|
55
|
+
if (!this.isValidShadowSource(mesh))
|
|
56
|
+
return;
|
|
57
|
+
const shadow = this.createShadowForNode(root);
|
|
58
|
+
if (shadow)
|
|
59
|
+
this.casters.set(shadow.source, shadow);
|
|
60
|
+
}
|
|
61
|
+
removeCaster(mesh) {
|
|
62
|
+
const root = this.findCasterRoot(mesh);
|
|
63
|
+
this.disposeCaster(root);
|
|
64
|
+
}
|
|
65
|
+
addReceiver(mesh) {
|
|
66
|
+
if (!this.options.stencil.enabled || this.receivers.has(mesh))
|
|
67
|
+
return;
|
|
68
|
+
const originalRenderingGroup = mesh instanceof Mesh ? mesh.renderingGroupId : 0;
|
|
69
|
+
if (mesh instanceof Mesh)
|
|
70
|
+
mesh.renderingGroupId = this.options.stencil.receiverRenderingGroup;
|
|
71
|
+
const material = readMeshMaterial(mesh);
|
|
72
|
+
if (material?.stencil) {
|
|
73
|
+
const stencil = material.stencil;
|
|
74
|
+
stencil.enabled = true;
|
|
75
|
+
stencil.func = Constants.ALWAYS;
|
|
76
|
+
stencil.funcRef = 1;
|
|
77
|
+
stencil.funcMask = 0xff;
|
|
78
|
+
stencil.mask = 0xff;
|
|
79
|
+
stencil.opStencilDepthPass = Constants.REPLACE;
|
|
80
|
+
stencil.opStencilFail = Constants.KEEP;
|
|
81
|
+
stencil.opDepthFail = Constants.KEEP;
|
|
82
|
+
}
|
|
83
|
+
this.receivers.set(mesh, { mesh, originalRenderingGroup, material });
|
|
84
|
+
}
|
|
85
|
+
removeReceiver(mesh) {
|
|
86
|
+
const info = this.receivers.get(mesh);
|
|
87
|
+
if (!info)
|
|
88
|
+
return;
|
|
89
|
+
if (mesh instanceof Mesh)
|
|
90
|
+
mesh.renderingGroupId = info.originalRenderingGroup;
|
|
91
|
+
if (info.material?.stencil)
|
|
92
|
+
info.material.stencil.enabled = false;
|
|
93
|
+
this.receivers.delete(mesh);
|
|
94
|
+
}
|
|
95
|
+
enableCasterAutoDetection(patterns) {
|
|
96
|
+
this.options = {
|
|
97
|
+
...this.options,
|
|
98
|
+
casters: {
|
|
99
|
+
...this.options.casters,
|
|
100
|
+
autoDetectAll: false,
|
|
101
|
+
includePatterns: [...patterns],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
this.casterPatterns = [...patterns];
|
|
105
|
+
this.refresh();
|
|
106
|
+
this.ensureMeshObserver();
|
|
107
|
+
}
|
|
108
|
+
enableAutoDetectionForAll() {
|
|
109
|
+
this.options = {
|
|
110
|
+
...this.options,
|
|
111
|
+
casters: {
|
|
112
|
+
...this.options.casters,
|
|
113
|
+
autoDetectAll: true,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
this.casterPatterns = [];
|
|
117
|
+
this.refresh();
|
|
118
|
+
this.ensureMeshObserver();
|
|
119
|
+
}
|
|
120
|
+
refresh() {
|
|
121
|
+
if (!this.initialized)
|
|
122
|
+
return;
|
|
123
|
+
this.pruneDisposedCasters();
|
|
124
|
+
this.refreshReceivers();
|
|
125
|
+
for (const mesh of this.scene.meshes)
|
|
126
|
+
this.tryAutoAddCaster(mesh);
|
|
127
|
+
if (this.options.casters.autoDetectAll || this.casterPatterns.length > 0 || this.options.receivers.patterns.length > 0) {
|
|
128
|
+
this.ensureMeshObserver();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
setOptions(options) {
|
|
132
|
+
const wasEnabled = this.options.enabled;
|
|
133
|
+
const requiredUniqueMaterials = this.requiresPerShadowMaterial();
|
|
134
|
+
this.options = resolvePlanarShadowOptions(options, this.options);
|
|
135
|
+
this.casterPatterns = [...this.options.casters.includePatterns];
|
|
136
|
+
const requiresUniqueMaterials = this.requiresPerShadowMaterial();
|
|
137
|
+
if (!wasEnabled && this.options.enabled)
|
|
138
|
+
this.initialize();
|
|
139
|
+
else if (wasEnabled && !this.options.enabled)
|
|
140
|
+
this.dispose();
|
|
141
|
+
else {
|
|
142
|
+
if (requiredUniqueMaterials !== requiresUniqueMaterials)
|
|
143
|
+
this.rebuildCasters();
|
|
144
|
+
this.refresh();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
getOptions() {
|
|
148
|
+
return cloneResolvedOptions(this.options);
|
|
149
|
+
}
|
|
150
|
+
getCasterCount() {
|
|
151
|
+
return this.casters.size;
|
|
152
|
+
}
|
|
153
|
+
getReceiverCount() {
|
|
154
|
+
return this.receivers.size;
|
|
155
|
+
}
|
|
156
|
+
dispose() {
|
|
157
|
+
if (this.renderObserver) {
|
|
158
|
+
this.scene.onBeforeRenderObservable.remove(this.renderObserver);
|
|
159
|
+
this.renderObserver = null;
|
|
160
|
+
}
|
|
161
|
+
if (this.meshAddedObserver) {
|
|
162
|
+
this.scene.onNewMeshAddedObservable.remove(this.meshAddedObserver);
|
|
163
|
+
this.meshAddedObserver = null;
|
|
164
|
+
}
|
|
165
|
+
for (const [mesh, observer] of [...this.syncObservers]) {
|
|
166
|
+
this.scene.onBeforeRenderObservable.remove(observer);
|
|
167
|
+
this.syncObservers.delete(mesh);
|
|
168
|
+
}
|
|
169
|
+
for (const mesh of [...this.casters.keys()])
|
|
170
|
+
this.disposeCaster(mesh);
|
|
171
|
+
for (const mesh of [...this.receivers.keys()])
|
|
172
|
+
this.removeReceiver(mesh);
|
|
173
|
+
for (const material of this.materials)
|
|
174
|
+
material.dispose();
|
|
175
|
+
this.materials.clear();
|
|
176
|
+
this.baseMaterial = null;
|
|
177
|
+
this.flatMaterial?.dispose();
|
|
178
|
+
this.flatMaterial = null;
|
|
179
|
+
this.initialized = false;
|
|
180
|
+
}
|
|
181
|
+
setupStencil() {
|
|
182
|
+
this.scene.getEngine().setStencilBuffer(true);
|
|
183
|
+
this.scene.setRenderingAutoClearDepthStencil(this.options.stencil.receiverRenderingGroup, true, false, true);
|
|
184
|
+
this.scene.setRenderingAutoClearDepthStencil(this.options.stencil.shadowRenderingGroup, false, false, false);
|
|
185
|
+
}
|
|
186
|
+
createShadowMaterial(defines = []) {
|
|
187
|
+
const usesBoneTexture = defines.includes('BONETEXTURE');
|
|
188
|
+
const usesBones = defines.includes('BONE');
|
|
189
|
+
const material = new ShaderMaterial(`fps.planarShadow.${this.materials.size}`, this.scene, { vertex: PLANAR_SHADOW_SHADER_NAME, fragment: PLANAR_SHADOW_SHADER_NAME }, {
|
|
190
|
+
attributes: getPlanarShadowShaderAttributes(),
|
|
191
|
+
uniforms: getPlanarShadowShaderUniforms(),
|
|
192
|
+
samplers: getPlanarShadowShaderSamplers(),
|
|
193
|
+
defines,
|
|
194
|
+
});
|
|
195
|
+
material.backFaceCulling = false;
|
|
196
|
+
material.disableDepthWrite = true;
|
|
197
|
+
material.depthFunction = Constants.LEQUAL;
|
|
198
|
+
material.needAlphaBlending = () => true;
|
|
199
|
+
material.alphaMode = Constants.ALPHA_COMBINE;
|
|
200
|
+
material.onBindObservable.add((mesh) => {
|
|
201
|
+
if (mesh instanceof AbstractMesh)
|
|
202
|
+
this.bindShadowProjectionUniforms(material, mesh);
|
|
203
|
+
});
|
|
204
|
+
if (usesBones) {
|
|
205
|
+
let lastBoundTexture = null;
|
|
206
|
+
let lastTextureWidth = 0;
|
|
207
|
+
material.onBindObservable.add((mesh) => {
|
|
208
|
+
if (!mesh?.skeleton)
|
|
209
|
+
return;
|
|
210
|
+
const skeleton = mesh.skeleton;
|
|
211
|
+
skeleton.prepare();
|
|
212
|
+
if (usesBoneTexture) {
|
|
213
|
+
const boneTexture = skeleton.getTransformMatrixTexture(mesh);
|
|
214
|
+
if (!boneTexture)
|
|
215
|
+
return;
|
|
216
|
+
const textureWidth = boneTexture.getSize().width;
|
|
217
|
+
if (boneTexture !== lastBoundTexture || textureWidth !== lastTextureWidth) {
|
|
218
|
+
material.setTexture('boneSampler', boneTexture);
|
|
219
|
+
material.setFloat('boneTextureWidth', textureWidth);
|
|
220
|
+
lastBoundTexture = boneTexture;
|
|
221
|
+
lastTextureWidth = textureWidth;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const matrices = skeleton.getTransformMatrices(mesh);
|
|
226
|
+
const effect = material.getEffect();
|
|
227
|
+
if (matrices && effect)
|
|
228
|
+
effect.setMatrices('mBones', matrices);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (this.options.stencil.enabled && material.stencil) {
|
|
233
|
+
const stencil = material.stencil;
|
|
234
|
+
stencil.enabled = true;
|
|
235
|
+
stencil.func = Constants.EQUAL;
|
|
236
|
+
stencil.funcRef = 1;
|
|
237
|
+
stencil.funcMask = 0xff;
|
|
238
|
+
stencil.mask = 0xff;
|
|
239
|
+
stencil.opStencilDepthPass = Constants.INCR;
|
|
240
|
+
stencil.opStencilFail = Constants.KEEP;
|
|
241
|
+
stencil.opDepthFail = Constants.KEEP;
|
|
242
|
+
stencil.backFunc = stencil.func;
|
|
243
|
+
stencil.backOpStencilDepthPass = stencil.opStencilDepthPass;
|
|
244
|
+
stencil.backOpStencilFail = stencil.opStencilFail;
|
|
245
|
+
stencil.backOpDepthFail = stencil.opDepthFail;
|
|
246
|
+
}
|
|
247
|
+
return material;
|
|
248
|
+
}
|
|
249
|
+
getFlatShadowMaterial() {
|
|
250
|
+
if (this.flatMaterial)
|
|
251
|
+
return this.flatMaterial;
|
|
252
|
+
const material = new StandardMaterial(`fps.planarShadow.flat`, this.scene);
|
|
253
|
+
material.backFaceCulling = false;
|
|
254
|
+
material.disableLighting = true;
|
|
255
|
+
material.disableDepthWrite = true;
|
|
256
|
+
material.diffuseColor = Color3.Black();
|
|
257
|
+
material.emissiveColor = Color3.Black();
|
|
258
|
+
material.alpha = clamp01(this.options.appearance.color.a);
|
|
259
|
+
material.alphaMode = Constants.ALPHA_COMBINE;
|
|
260
|
+
if (this.options.stencil.enabled && material.stencil) {
|
|
261
|
+
const stencil = material.stencil;
|
|
262
|
+
stencil.enabled = true;
|
|
263
|
+
stencil.func = Constants.EQUAL;
|
|
264
|
+
stencil.funcRef = 1;
|
|
265
|
+
stencil.funcMask = 0xff;
|
|
266
|
+
stencil.mask = 0xff;
|
|
267
|
+
stencil.opStencilDepthPass = Constants.INCR;
|
|
268
|
+
stencil.opStencilFail = Constants.KEEP;
|
|
269
|
+
stencil.opDepthFail = Constants.KEEP;
|
|
270
|
+
stencil.backFunc = stencil.func;
|
|
271
|
+
stencil.backOpStencilDepthPass = stencil.opStencilDepthPass;
|
|
272
|
+
stencil.backOpStencilFail = stencil.opStencilFail;
|
|
273
|
+
stencil.backOpDepthFail = stencil.opDepthFail;
|
|
274
|
+
}
|
|
275
|
+
this.flatMaterial = material;
|
|
276
|
+
return material;
|
|
277
|
+
}
|
|
278
|
+
updateUniforms() {
|
|
279
|
+
if (this.materials.size === 0)
|
|
280
|
+
return;
|
|
281
|
+
const planeNormal = new Vector3(this.options.plane.normal.x, this.options.plane.normal.y, this.options.plane.normal.z).normalize();
|
|
282
|
+
const planeHeight = this.resolvePlaneHeight(planeNormal);
|
|
283
|
+
this.resolvedPlaneNormal = planeNormal;
|
|
284
|
+
this.resolvedPlaneHeight = planeHeight;
|
|
285
|
+
const lightDir = this.resolveShadowDirection();
|
|
286
|
+
const shadowColor = new Color4(clamp01(this.options.appearance.color.r), clamp01(this.options.appearance.color.g), clamp01(this.options.appearance.color.b), clamp01(this.options.appearance.color.a));
|
|
287
|
+
for (const material of this.materials) {
|
|
288
|
+
material.setVector3('u_lightDir', lightDir);
|
|
289
|
+
material.setVector3('u_planeNormal', planeNormal);
|
|
290
|
+
material.setVector3('u_shadowCenter', new Vector3(0, planeHeight, 0));
|
|
291
|
+
material.setFloat('u_planeHeight', planeHeight);
|
|
292
|
+
material.setFloat('u_planeBias', this.options.plane.bias);
|
|
293
|
+
material.setFloat('u_footprintScale', this.options.projection.footprintScale);
|
|
294
|
+
material.setColor4('u_shadowColor', shadowColor);
|
|
295
|
+
}
|
|
296
|
+
if (this.flatMaterial) {
|
|
297
|
+
const color = new Color3(shadowColor.r, shadowColor.g, shadowColor.b);
|
|
298
|
+
this.flatMaterial.diffuseColor = color;
|
|
299
|
+
this.flatMaterial.emissiveColor = color;
|
|
300
|
+
this.flatMaterial.alpha = shadowColor.a;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
bindShadowProjectionUniforms(material, mesh) {
|
|
304
|
+
mesh.computeWorldMatrix(true);
|
|
305
|
+
const center = mesh.getBoundingInfo()?.boundingBox.centerWorld.clone() ?? Vector3.Zero();
|
|
306
|
+
const offset = Vector3.Dot(this.resolvedPlaneNormal, center) - this.resolvedPlaneHeight;
|
|
307
|
+
const projectedCenter = center.subtract(this.resolvedPlaneNormal.scale(offset));
|
|
308
|
+
material.setVector3('u_shadowCenter', projectedCenter);
|
|
309
|
+
}
|
|
310
|
+
requiresPerShadowMaterial() {
|
|
311
|
+
return Math.abs(this.options.projection.footprintScale - 1) > 1e-6;
|
|
312
|
+
}
|
|
313
|
+
resolvePlaneHeight(planeNormal) {
|
|
314
|
+
const fallbackHeight = this.options.plane.height;
|
|
315
|
+
if (this.options.plane.heightMode !== 'receiver')
|
|
316
|
+
return fallbackHeight;
|
|
317
|
+
let totalHeight = 0;
|
|
318
|
+
let receiverCount = 0;
|
|
319
|
+
for (const mesh of this.receivers.keys()) {
|
|
320
|
+
if (isDisposedNode(mesh))
|
|
321
|
+
continue;
|
|
322
|
+
mesh.computeWorldMatrix(true);
|
|
323
|
+
const vectorsWorld = mesh.getBoundingInfo()?.boundingBox?.vectorsWorld;
|
|
324
|
+
if (!vectorsWorld || vectorsWorld.length === 0)
|
|
325
|
+
continue;
|
|
326
|
+
let receiverSurfaceHeight = -Infinity;
|
|
327
|
+
for (const point of vectorsWorld) {
|
|
328
|
+
const height = Vector3.Dot(planeNormal, point);
|
|
329
|
+
if (Number.isFinite(height))
|
|
330
|
+
receiverSurfaceHeight = Math.max(receiverSurfaceHeight, height);
|
|
331
|
+
}
|
|
332
|
+
if (!Number.isFinite(receiverSurfaceHeight))
|
|
333
|
+
continue;
|
|
334
|
+
totalHeight += receiverSurfaceHeight;
|
|
335
|
+
receiverCount += 1;
|
|
336
|
+
}
|
|
337
|
+
return receiverCount > 0 ? totalHeight / receiverCount : fallbackHeight;
|
|
338
|
+
}
|
|
339
|
+
resolveShadowDirection() {
|
|
340
|
+
const direction = this.directionalLight.direction;
|
|
341
|
+
if (direction?.lengthSquared?.() > 1e-6)
|
|
342
|
+
return direction.clone().normalize();
|
|
343
|
+
return DEFAULT_SHADOW_DIRECTION.clone();
|
|
344
|
+
}
|
|
345
|
+
refreshReceivers() {
|
|
346
|
+
if (!this.options.stencil.enabled)
|
|
347
|
+
return;
|
|
348
|
+
for (const mesh of [...this.receivers.keys()]) {
|
|
349
|
+
if (!this.matchesReceiver(mesh))
|
|
350
|
+
this.removeReceiver(mesh);
|
|
351
|
+
}
|
|
352
|
+
for (const mesh of this.scene.meshes) {
|
|
353
|
+
if (this.matchesReceiver(mesh))
|
|
354
|
+
this.addReceiver(mesh);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
pruneDisposedCasters() {
|
|
358
|
+
for (const [root, info] of [...this.casters]) {
|
|
359
|
+
if (isDisposedNode(root) || isDisposedNode(info.shadow)) {
|
|
360
|
+
this.disposeCaster(root);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
disposeCaster(root) {
|
|
365
|
+
const info = this.casters.get(root);
|
|
366
|
+
if (!info)
|
|
367
|
+
return;
|
|
368
|
+
const observer = this.syncObservers.get(root);
|
|
369
|
+
if (observer) {
|
|
370
|
+
this.scene.onBeforeRenderObservable.remove(observer);
|
|
371
|
+
this.syncObservers.delete(root);
|
|
372
|
+
}
|
|
373
|
+
for (const shadowMesh of info.shadow.getChildMeshes(false)) {
|
|
374
|
+
if (shadowMesh instanceof Mesh
|
|
375
|
+
&& shadowMesh.material
|
|
376
|
+
&& shadowMesh.material !== this.baseMaterial
|
|
377
|
+
&& shadowMesh.material !== this.flatMaterial) {
|
|
378
|
+
this.materials.delete(shadowMesh.material);
|
|
379
|
+
shadowMesh.material.dispose();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
info.shadow.dispose();
|
|
383
|
+
this.casters.delete(root);
|
|
384
|
+
}
|
|
385
|
+
rebuildCasters() {
|
|
386
|
+
const roots = [...this.casters.keys()].filter((root) => !isDisposedNode(root));
|
|
387
|
+
for (const root of roots)
|
|
388
|
+
this.disposeCaster(root);
|
|
389
|
+
for (const root of roots)
|
|
390
|
+
this.addCaster(root);
|
|
391
|
+
}
|
|
392
|
+
tryAutoAddCaster(mesh) {
|
|
393
|
+
if (!this.initialized || this.casters.has(mesh))
|
|
394
|
+
return;
|
|
395
|
+
if (!this.isValidShadowSource(mesh))
|
|
396
|
+
return;
|
|
397
|
+
if (!this.options.casters.autoDetectAll && !this.matchesCasterPattern(mesh))
|
|
398
|
+
return;
|
|
399
|
+
const root = this.findCasterRoot(mesh);
|
|
400
|
+
if (this.isAlreadyCoveredByCaster(root))
|
|
401
|
+
return;
|
|
402
|
+
const shadow = this.createShadowForNode(root);
|
|
403
|
+
if (shadow)
|
|
404
|
+
this.casters.set(shadow.source, shadow);
|
|
405
|
+
}
|
|
406
|
+
ensureMeshObserver() {
|
|
407
|
+
if (this.meshAddedObserver)
|
|
408
|
+
return;
|
|
409
|
+
this.meshAddedObserver = this.scene.onNewMeshAddedObservable.add((mesh) => {
|
|
410
|
+
queueMicrotask(() => {
|
|
411
|
+
if (!this.initialized || mesh.isDisposed())
|
|
412
|
+
return;
|
|
413
|
+
if (this.matchesReceiver(mesh))
|
|
414
|
+
this.addReceiver(mesh);
|
|
415
|
+
this.tryAutoAddCaster(mesh);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
createShadowForNode(root) {
|
|
420
|
+
const validMeshes = collectNodeMeshes(root).filter((mesh) => this.isValidShadowSource(mesh));
|
|
421
|
+
if (validMeshes.length === 0)
|
|
422
|
+
return null;
|
|
423
|
+
const shadowRoot = new TransformNode(`${root.name}_planarShadowRoot`, this.scene);
|
|
424
|
+
let hasSkeleton = false;
|
|
425
|
+
const pairs = [];
|
|
426
|
+
let sharedSkeleton = null;
|
|
427
|
+
let sharedMaterial = null;
|
|
428
|
+
for (const mesh of validMeshes) {
|
|
429
|
+
if (!(mesh instanceof Mesh) && !(mesh instanceof InstancedMesh))
|
|
430
|
+
continue;
|
|
431
|
+
const geometrySource = mesh instanceof InstancedMesh ? mesh.sourceMesh : mesh;
|
|
432
|
+
if (geometrySource.skeleton) {
|
|
433
|
+
const material = this.resolveShadowMaterial(geometrySource, sharedSkeleton, sharedMaterial);
|
|
434
|
+
hasSkeleton = true;
|
|
435
|
+
sharedSkeleton = geometrySource.skeleton;
|
|
436
|
+
sharedMaterial = material;
|
|
437
|
+
if (!material)
|
|
438
|
+
continue;
|
|
439
|
+
const shadowMesh = this.createProjectedShadowMesh(mesh, material);
|
|
440
|
+
if (shadowMesh) {
|
|
441
|
+
shadowMesh.parent = shadowRoot;
|
|
442
|
+
pairs.push({ mode: 'projected-mesh', source: mesh, shadow: shadowMesh });
|
|
443
|
+
}
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const shadowMesh = this.createHullShadowMesh(mesh);
|
|
447
|
+
if (shadowMesh) {
|
|
448
|
+
shadowMesh.parent = shadowRoot;
|
|
449
|
+
pairs.push({ mode: 'flat-hull', source: mesh, shadow: shadowMesh });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (pairs.length === 0) {
|
|
453
|
+
shadowRoot.dispose();
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const sync = () => {
|
|
457
|
+
if (shadowRoot.isDisposed())
|
|
458
|
+
return;
|
|
459
|
+
for (const pair of pairs) {
|
|
460
|
+
if (pair.source.isDisposed() || pair.shadow.isDisposed())
|
|
461
|
+
continue;
|
|
462
|
+
if (pair.mode === 'flat-hull') {
|
|
463
|
+
this.updateHullShadowMesh(pair.source, pair.shadow);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
pair.source.computeWorldMatrix(true).decompose(pair.shadow.scaling, pair.shadow.rotationQuaternion, pair.shadow.position);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
sync();
|
|
471
|
+
const observer = this.scene.onBeforeRenderObservable.add(sync);
|
|
472
|
+
this.syncObservers.set(root, observer);
|
|
473
|
+
return {
|
|
474
|
+
source: root,
|
|
475
|
+
shadow: shadowRoot,
|
|
476
|
+
hasSkeleton,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
resolveSkeletonShadowMaterial(geometrySource, sharedSkeleton, sharedMaterial) {
|
|
480
|
+
if (geometrySource.skeleton === sharedSkeleton && sharedMaterial)
|
|
481
|
+
return sharedMaterial;
|
|
482
|
+
const numBones = geometrySource.skeleton?.bones.length ?? 0;
|
|
483
|
+
const useTexture = !!this.scene.getEngine().getCaps().textureFloat && numBones > 4;
|
|
484
|
+
const hasExtra = geometrySource.isVerticesDataPresent('matricesIndicesExtra');
|
|
485
|
+
const material = this.createShadowMaterial(getPlanarShadowSkeletonDefines(numBones, useTexture, hasExtra));
|
|
486
|
+
this.materials.add(material);
|
|
487
|
+
return material;
|
|
488
|
+
}
|
|
489
|
+
resolveShadowMaterial(geometrySource, sharedSkeleton, sharedMaterial) {
|
|
490
|
+
if (this.requiresPerShadowMaterial()) {
|
|
491
|
+
const material = geometrySource.skeleton
|
|
492
|
+
? this.createShadowMaterial(getPlanarShadowSkeletonDefines(geometrySource.skeleton?.bones.length ?? 0, !!this.scene.getEngine().getCaps().textureFloat && (geometrySource.skeleton?.bones.length ?? 0) > 4, geometrySource.isVerticesDataPresent('matricesIndicesExtra')))
|
|
493
|
+
: this.createShadowMaterial();
|
|
494
|
+
this.materials.add(material);
|
|
495
|
+
return material;
|
|
496
|
+
}
|
|
497
|
+
return geometrySource.skeleton
|
|
498
|
+
? this.resolveSkeletonShadowMaterial(geometrySource, sharedSkeleton, sharedMaterial)
|
|
499
|
+
: this.baseMaterial;
|
|
500
|
+
}
|
|
501
|
+
createProjectedShadowMesh(source, material) {
|
|
502
|
+
const geometrySource = source instanceof InstancedMesh ? source.sourceMesh : source;
|
|
503
|
+
const shadowMesh = geometrySource.clone(`${source.name}_planarShadow`, null, true, false);
|
|
504
|
+
if (!shadowMesh)
|
|
505
|
+
return null;
|
|
506
|
+
if (geometrySource.skeleton) {
|
|
507
|
+
shadowMesh.skeleton = geometrySource.skeleton;
|
|
508
|
+
geometrySource.skeleton.prepare();
|
|
509
|
+
}
|
|
510
|
+
shadowMesh.material = material;
|
|
511
|
+
shadowMesh.isPickable = false;
|
|
512
|
+
shadowMesh.receiveShadows = false;
|
|
513
|
+
shadowMesh.renderingGroupId = this.options.stencil.shadowRenderingGroup;
|
|
514
|
+
shadowMesh.alphaIndex = PLANAR_SHADOW_ALPHA_INDEX;
|
|
515
|
+
shadowMesh.renderOutline = false;
|
|
516
|
+
shadowMesh.renderOverlay = false;
|
|
517
|
+
shadowMesh.metadata = {
|
|
518
|
+
...(shadowMesh.metadata && typeof shadowMesh.metadata === 'object' ? shadowMesh.metadata : {}),
|
|
519
|
+
disablePlanarShadow: true,
|
|
520
|
+
planarShadowInternal: true,
|
|
521
|
+
};
|
|
522
|
+
if (!shadowMesh.rotationQuaternion)
|
|
523
|
+
shadowMesh.rotationQuaternion = shadowMesh.rotation.toQuaternion();
|
|
524
|
+
return shadowMesh;
|
|
525
|
+
}
|
|
526
|
+
createHullShadowMesh(source) {
|
|
527
|
+
const shadowMesh = new Mesh(`${source.name}_planarShadow`, this.scene);
|
|
528
|
+
shadowMesh.material = this.getFlatShadowMaterial();
|
|
529
|
+
shadowMesh.isPickable = false;
|
|
530
|
+
shadowMesh.receiveShadows = false;
|
|
531
|
+
shadowMesh.renderingGroupId = this.options.stencil.shadowRenderingGroup;
|
|
532
|
+
shadowMesh.alphaIndex = PLANAR_SHADOW_ALPHA_INDEX;
|
|
533
|
+
shadowMesh.renderOutline = false;
|
|
534
|
+
shadowMesh.renderOverlay = false;
|
|
535
|
+
shadowMesh.metadata = {
|
|
536
|
+
...(shadowMesh.metadata && typeof shadowMesh.metadata === 'object' ? shadowMesh.metadata : {}),
|
|
537
|
+
disablePlanarShadow: true,
|
|
538
|
+
planarShadowInternal: true,
|
|
539
|
+
};
|
|
540
|
+
this.updateHullShadowMesh(source, shadowMesh);
|
|
541
|
+
return shadowMesh;
|
|
542
|
+
}
|
|
543
|
+
updateHullShadowMesh(source, shadowMesh) {
|
|
544
|
+
const geometry = buildPlanarShadowHullGeometry(source, {
|
|
545
|
+
lightDir: this.resolveShadowDirection(),
|
|
546
|
+
planeNormal: this.resolvedPlaneNormal,
|
|
547
|
+
planeHeight: this.resolvedPlaneHeight,
|
|
548
|
+
planeBias: this.options.plane.bias,
|
|
549
|
+
footprintScale: this.options.projection.footprintScale,
|
|
550
|
+
});
|
|
551
|
+
if (!geometry) {
|
|
552
|
+
shadowMesh.isVisible = false;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const vertexData = new VertexData();
|
|
556
|
+
vertexData.positions = geometry.positions;
|
|
557
|
+
vertexData.indices = geometry.indices;
|
|
558
|
+
vertexData.normals = geometry.normals;
|
|
559
|
+
vertexData.applyToMesh(shadowMesh, true);
|
|
560
|
+
shadowMesh.isVisible = true;
|
|
561
|
+
}
|
|
562
|
+
isValidShadowSource(mesh) {
|
|
563
|
+
if (!(mesh instanceof Mesh) && !(mesh instanceof InstancedMesh))
|
|
564
|
+
return false;
|
|
565
|
+
if (mesh instanceof Mesh && !mesh.geometry)
|
|
566
|
+
return false;
|
|
567
|
+
if (mesh instanceof InstancedMesh && !mesh.sourceMesh.geometry)
|
|
568
|
+
return false;
|
|
569
|
+
if (!mesh.isVisible || !mesh.isEnabled())
|
|
570
|
+
return false;
|
|
571
|
+
if (hasDisablePlanarShadowMetadata(mesh))
|
|
572
|
+
return false;
|
|
573
|
+
if (this.matchesExcludePattern(mesh) || this.hasExcludedAncestor(mesh))
|
|
574
|
+
return false;
|
|
575
|
+
if (this.matchesReceiverPattern(mesh))
|
|
576
|
+
return false;
|
|
577
|
+
mesh.computeWorldMatrix(true);
|
|
578
|
+
const size = mesh.getBoundingInfo()?.boundingBox.extendSizeWorld;
|
|
579
|
+
if (!size)
|
|
580
|
+
return true;
|
|
581
|
+
return size.x * size.y * size.z >= this.options.casters.minVolume;
|
|
582
|
+
}
|
|
583
|
+
findCasterRoot(mesh) {
|
|
584
|
+
let topmost = mesh;
|
|
585
|
+
let current = mesh.parent;
|
|
586
|
+
while (current) {
|
|
587
|
+
if (this.matchesRootBoundaryPattern(current) || hasDisablePlanarShadowMetadata(current))
|
|
588
|
+
break;
|
|
589
|
+
if (!(current instanceof AbstractMesh)) {
|
|
590
|
+
current = current.parent;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (this.matchesExcludePattern(current))
|
|
594
|
+
break;
|
|
595
|
+
topmost = current;
|
|
596
|
+
current = current.parent;
|
|
597
|
+
}
|
|
598
|
+
return topmost;
|
|
599
|
+
}
|
|
600
|
+
matchesCasterPattern(mesh) {
|
|
601
|
+
return this.casterPatterns.some((pattern) => nodeOrAncestorNameIncludes(mesh, pattern));
|
|
602
|
+
}
|
|
603
|
+
matchesReceiver(mesh) {
|
|
604
|
+
return this.options.stencil.enabled
|
|
605
|
+
&& !hasDisablePlanarShadowMetadata(mesh)
|
|
606
|
+
&& !this.matchesExcludePattern(mesh)
|
|
607
|
+
&& !this.hasExcludedAncestor(mesh)
|
|
608
|
+
&& this.matchesReceiverPattern(mesh);
|
|
609
|
+
}
|
|
610
|
+
matchesReceiverPattern(mesh) {
|
|
611
|
+
return this.options.receivers.patterns.some((pattern) => nodeOrAncestorNameIncludes(mesh, pattern));
|
|
612
|
+
}
|
|
613
|
+
matchesExcludePattern(node) {
|
|
614
|
+
const name = node.name?.toLowerCase() ?? '';
|
|
615
|
+
return this.options.casters.excludePatterns.some((pattern) => pattern && name.includes(pattern.toLowerCase()));
|
|
616
|
+
}
|
|
617
|
+
matchesRootBoundaryPattern(node) {
|
|
618
|
+
const name = node.name?.toLowerCase() ?? '';
|
|
619
|
+
return this.options.casters.rootBoundaryPatterns.some((pattern) => pattern && name.includes(pattern.toLowerCase()));
|
|
620
|
+
}
|
|
621
|
+
hasExcludedAncestor(mesh) {
|
|
622
|
+
let current = mesh.parent;
|
|
623
|
+
while (current) {
|
|
624
|
+
if (hasDisablePlanarShadowMetadata(current) || this.matchesExcludePattern(current))
|
|
625
|
+
return true;
|
|
626
|
+
current = current.parent;
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
isAlreadyCoveredByCaster(node) {
|
|
631
|
+
for (const caster of this.casters.keys()) {
|
|
632
|
+
if (caster === node || isDescendantOf(node, caster) || isDescendantOf(caster, node))
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function buildPlanarShadowHullGeometry(source, options) {
|
|
639
|
+
const geometrySource = source instanceof InstancedMesh ? source.sourceMesh : source;
|
|
640
|
+
const positions = geometrySource.getVerticesData(VertexBuffer.PositionKind);
|
|
641
|
+
if (!positions || positions.length < 9)
|
|
642
|
+
return null;
|
|
643
|
+
const planeNormal = options.planeNormal.clone().normalize();
|
|
644
|
+
const lightDir = options.lightDir.clone().normalize();
|
|
645
|
+
const denom = Vector3.Dot(planeNormal, lightDir);
|
|
646
|
+
if (Math.abs(denom) < 1e-5)
|
|
647
|
+
return null;
|
|
648
|
+
source.computeWorldMatrix(true);
|
|
649
|
+
const worldMatrix = source.getWorldMatrix();
|
|
650
|
+
const projectedPoints = [];
|
|
651
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
652
|
+
const localPoint = new Vector3(Number(positions[index]), Number(positions[index + 1]), Number(positions[index + 2]));
|
|
653
|
+
const worldPoint = Vector3.TransformCoordinates(localPoint, worldMatrix);
|
|
654
|
+
const t = (options.planeHeight - Vector3.Dot(planeNormal, worldPoint)) / denom;
|
|
655
|
+
if (t < -1e-4)
|
|
656
|
+
continue;
|
|
657
|
+
const projectedPoint = worldPoint.add(lightDir.scale(Math.max(0, t)));
|
|
658
|
+
projectedPoints.push(projectedPoint);
|
|
659
|
+
}
|
|
660
|
+
if (projectedPoints.length < 3)
|
|
661
|
+
return null;
|
|
662
|
+
const scaleCenter = averageVector3(projectedPoints);
|
|
663
|
+
const footprintScale = Number.isFinite(options.footprintScale) ? Math.max(0, options.footprintScale) : 1;
|
|
664
|
+
const biasedPoints = projectedPoints.map((point) => {
|
|
665
|
+
const scaled = footprintScale === 1
|
|
666
|
+
? point
|
|
667
|
+
: scaleCenter.add(point.subtract(scaleCenter).scale(footprintScale));
|
|
668
|
+
return scaled.add(planeNormal.scale(options.planeBias * 0.002));
|
|
669
|
+
});
|
|
670
|
+
const basis = buildPlaneBasis(planeNormal);
|
|
671
|
+
const points2d = dedupeHullPoints(biasedPoints.map((point) => ({
|
|
672
|
+
x: Vector3.Dot(point, basis.tangent),
|
|
673
|
+
y: Vector3.Dot(point, basis.bitangent),
|
|
674
|
+
point,
|
|
675
|
+
})));
|
|
676
|
+
if (points2d.length < 3)
|
|
677
|
+
return null;
|
|
678
|
+
const hull = buildConvexHull(points2d);
|
|
679
|
+
if (hull.length < 3)
|
|
680
|
+
return null;
|
|
681
|
+
const center = averageVector3(hull.map((point) => point.point));
|
|
682
|
+
const positionsOut = [center.x, center.y, center.z];
|
|
683
|
+
const normalsOut = [planeNormal.x, planeNormal.y, planeNormal.z];
|
|
684
|
+
for (const hullPoint of hull) {
|
|
685
|
+
positionsOut.push(hullPoint.point.x, hullPoint.point.y, hullPoint.point.z);
|
|
686
|
+
normalsOut.push(planeNormal.x, planeNormal.y, planeNormal.z);
|
|
687
|
+
}
|
|
688
|
+
const indicesOut = [];
|
|
689
|
+
for (let index = 1; index <= hull.length; index += 1) {
|
|
690
|
+
indicesOut.push(0, index, index === hull.length ? 1 : index + 1);
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
positions: positionsOut,
|
|
694
|
+
indices: indicesOut,
|
|
695
|
+
normals: normalsOut,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function buildPlaneBasis(normal) {
|
|
699
|
+
const seed = Math.abs(Vector3.Dot(normal, Vector3.Up())) > 0.95
|
|
700
|
+
? Vector3.Right()
|
|
701
|
+
: Vector3.Up();
|
|
702
|
+
const tangent = Vector3.Cross(seed, normal).normalize();
|
|
703
|
+
const bitangent = Vector3.Cross(normal, tangent).normalize();
|
|
704
|
+
return { tangent, bitangent };
|
|
705
|
+
}
|
|
706
|
+
function dedupeHullPoints(points) {
|
|
707
|
+
const seen = new Map();
|
|
708
|
+
for (const point of points) {
|
|
709
|
+
const key = `${Math.round(point.x * 100000)}:${Math.round(point.y * 100000)}`;
|
|
710
|
+
if (!seen.has(key))
|
|
711
|
+
seen.set(key, point);
|
|
712
|
+
}
|
|
713
|
+
return [...seen.values()];
|
|
714
|
+
}
|
|
715
|
+
function buildConvexHull(points) {
|
|
716
|
+
const sorted = [...points].sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x));
|
|
717
|
+
const lower = [];
|
|
718
|
+
for (const point of sorted) {
|
|
719
|
+
while (lower.length >= 2 && hullCross(lower[lower.length - 2], lower[lower.length - 1], point) <= 1e-7) {
|
|
720
|
+
lower.pop();
|
|
721
|
+
}
|
|
722
|
+
lower.push(point);
|
|
723
|
+
}
|
|
724
|
+
const upper = [];
|
|
725
|
+
for (let index = sorted.length - 1; index >= 0; index -= 1) {
|
|
726
|
+
const point = sorted[index];
|
|
727
|
+
while (upper.length >= 2 && hullCross(upper[upper.length - 2], upper[upper.length - 1], point) <= 1e-7) {
|
|
728
|
+
upper.pop();
|
|
729
|
+
}
|
|
730
|
+
upper.push(point);
|
|
731
|
+
}
|
|
732
|
+
lower.pop();
|
|
733
|
+
upper.pop();
|
|
734
|
+
return lower.concat(upper);
|
|
735
|
+
}
|
|
736
|
+
function hullCross(origin, a, b) {
|
|
737
|
+
return (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x);
|
|
738
|
+
}
|
|
739
|
+
function averageVector3(points) {
|
|
740
|
+
const total = points.reduce((sum, point) => sum.addInPlace(point), Vector3.Zero());
|
|
741
|
+
return total.scale(1 / points.length);
|
|
742
|
+
}
|
|
743
|
+
function resolvePlanarShadowOptions(override, base = DEFAULT_PLANAR_SHADOW_OPTIONS) {
|
|
744
|
+
return {
|
|
745
|
+
enabled: override.enabled ?? base.enabled,
|
|
746
|
+
plane: {
|
|
747
|
+
...base.plane,
|
|
748
|
+
...override.plane,
|
|
749
|
+
heightMode: readPlaneHeightMode(override.plane?.heightMode, base.plane.heightMode),
|
|
750
|
+
},
|
|
751
|
+
appearance: {
|
|
752
|
+
color: { ...base.appearance.color, ...override.appearance?.color },
|
|
753
|
+
},
|
|
754
|
+
direction: { mode: 'follow-light' },
|
|
755
|
+
projection: {
|
|
756
|
+
footprintScale: readNonNegativeFiniteNumber(override.projection?.footprintScale, base.projection.footprintScale),
|
|
757
|
+
},
|
|
758
|
+
stencil: {
|
|
759
|
+
enabled: override.stencil?.enabled ?? base.stencil.enabled,
|
|
760
|
+
receiverRenderingGroup: override.stencil?.receiverRenderingGroup ?? base.stencil.receiverRenderingGroup,
|
|
761
|
+
shadowRenderingGroup: override.stencil?.shadowRenderingGroup ?? base.stencil.shadowRenderingGroup,
|
|
762
|
+
},
|
|
763
|
+
casters: {
|
|
764
|
+
autoDetectAll: override.casters?.autoDetectAll ?? base.casters.autoDetectAll,
|
|
765
|
+
includePatterns: override.casters?.includePatterns ? [...override.casters.includePatterns] : [...base.casters.includePatterns],
|
|
766
|
+
excludePatterns: override.casters?.excludePatterns ? [...override.casters.excludePatterns] : [...base.casters.excludePatterns],
|
|
767
|
+
rootBoundaryPatterns: override.casters?.rootBoundaryPatterns
|
|
768
|
+
? [...override.casters.rootBoundaryPatterns]
|
|
769
|
+
: [...base.casters.rootBoundaryPatterns],
|
|
770
|
+
minVolume: override.casters?.minVolume ?? base.casters.minVolume,
|
|
771
|
+
},
|
|
772
|
+
receivers: {
|
|
773
|
+
patterns: override.receivers?.patterns ? [...override.receivers.patterns] : [...base.receivers.patterns],
|
|
774
|
+
},
|
|
775
|
+
debug: override.debug ?? base.debug,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function cloneResolvedOptions(options) {
|
|
779
|
+
return resolvePlanarShadowOptions(options);
|
|
780
|
+
}
|
|
781
|
+
function readPlaneHeightMode(value, fallback) {
|
|
782
|
+
return value === 'fixed' || value === 'receiver'
|
|
783
|
+
? value
|
|
784
|
+
: fallback === 'fixed' || fallback === 'receiver'
|
|
785
|
+
? fallback
|
|
786
|
+
: 'receiver';
|
|
787
|
+
}
|
|
788
|
+
function collectNodeMeshes(root) {
|
|
789
|
+
const meshes = root.getChildMeshes(false);
|
|
790
|
+
if (root instanceof Mesh || root instanceof InstancedMesh)
|
|
791
|
+
meshes.unshift(root);
|
|
792
|
+
return meshes;
|
|
793
|
+
}
|
|
794
|
+
function nodeOrAncestorNameIncludes(mesh, pattern) {
|
|
795
|
+
const normalized = pattern.toLowerCase();
|
|
796
|
+
if (mesh.name.toLowerCase().includes(normalized))
|
|
797
|
+
return true;
|
|
798
|
+
let current = mesh.parent;
|
|
799
|
+
while (current) {
|
|
800
|
+
if (current.name?.toLowerCase().includes(normalized))
|
|
801
|
+
return true;
|
|
802
|
+
current = current.parent;
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
function isDescendantOf(node, ancestor) {
|
|
807
|
+
let current = node.parent;
|
|
808
|
+
while (current) {
|
|
809
|
+
if (current === ancestor)
|
|
810
|
+
return true;
|
|
811
|
+
current = current.parent;
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
function hasDisablePlanarShadowMetadata(node) {
|
|
816
|
+
const metadata = node.metadata;
|
|
817
|
+
return !!metadata
|
|
818
|
+
&& typeof metadata === 'object'
|
|
819
|
+
&& (metadata.disablePlanarShadow === true
|
|
820
|
+
|| metadata.planarShadowInternal === true);
|
|
821
|
+
}
|
|
822
|
+
function isDisposedNode(node) {
|
|
823
|
+
return typeof node.isDisposed === 'function' && node.isDisposed();
|
|
824
|
+
}
|
|
825
|
+
function readNonNegativeFiniteNumber(value, fallback) {
|
|
826
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
827
|
+
}
|
|
828
|
+
function readMeshMaterial(mesh) {
|
|
829
|
+
const material = mesh.material;
|
|
830
|
+
return material && typeof material === 'object'
|
|
831
|
+
? material
|
|
832
|
+
: null;
|
|
833
|
+
}
|
|
834
|
+
function clamp01(value) {
|
|
835
|
+
return Math.min(1, Math.max(0, value));
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=planar-shadow-system.js.map
|