@waica/engine 0.30.1 → 0.31.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/README.md +48 -3
- package/dist/camera-rig.d.ts +2 -0
- package/dist/camera-rig.js +4 -0
- package/dist/components/animator-blends.d.ts +54 -0
- package/dist/components/animator-blends.js +86 -0
- package/dist/components/animator-mix.d.ts +53 -0
- package/dist/components/animator-mix.js +92 -0
- package/dist/components/animator-params.d.ts +21 -0
- package/dist/components/animator-params.js +50 -0
- package/dist/components/animator-sources.d.ts +59 -0
- package/dist/components/animator-sources.js +125 -0
- package/dist/components/animator.d.ts +84 -10
- package/dist/components/animator.js +163 -47
- package/dist/components/model.d.ts +17 -1
- package/dist/components/model.js +49 -3
- package/dist/components/point-light.d.ts +38 -3
- package/dist/components/point-light.js +6 -3
- package/dist/components/shadow-light.d.ts +63 -0
- package/dist/components/shadow-light.js +71 -0
- package/dist/components/sun.d.ts +38 -3
- package/dist/components/sun.js +6 -3
- package/dist/entity.js +3 -0
- package/dist/game-shadows.d.ts +54 -0
- package/dist/game-shadows.js +260 -0
- package/dist/game.d.ts +3 -0
- package/dist/game.js +15 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +3 -1
- package/dist/point-shadow.d.ts +14 -0
- package/dist/point-shadow.js +54 -0
- package/dist/runtime-lighting-snapshot.d.ts +9 -1
- package/dist/runtime-lighting-snapshot.js +24 -4
- package/dist/scene-shadows.d.ts +82 -0
- package/dist/scene-shadows.js +151 -0
- package/dist/scene.d.ts +11 -1
- package/dist/scene.js +2 -1
- package/dist/shadow-renderer.d.ts +8 -0
- package/dist/shadow-renderer.js +37 -0
- package/dist/sun-shadow-camera.d.ts +17 -0
- package/dist/sun-shadow-camera.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { PointLight } from './components/point-light.js';
|
|
3
|
+
import { Sun } from './components/sun.js';
|
|
4
|
+
import { assertShadowMapCapacity, resolveLightShadow, resolveSceneShadows } from './scene-shadows.js';
|
|
5
|
+
import { advanceRendererFrame, shadowTextureLimit } from './shadow-renderer.js';
|
|
6
|
+
import { createRangedPointShadow } from './point-shadow.js';
|
|
7
|
+
import { fitSunShadow, sunCoverageDepth } from './sun-shadow-camera.js';
|
|
8
|
+
import { registryEntry, resolveEntityComponents, resolveProps, sceneProps } from './scene.js';
|
|
9
|
+
import { resolvePerspectiveCamera } from './scene-camera-3d.js';
|
|
10
|
+
function isShadowClass(Class) {
|
|
11
|
+
return Class !== undefined && (Class === Sun || Class === PointLight || Class.prototype instanceof Sun || Class.prototype instanceof PointLight);
|
|
12
|
+
}
|
|
13
|
+
function hasCoverage(light, camera, maxDistance) {
|
|
14
|
+
return !(light instanceof Sun) || (camera !== null && sunCoverageDepth(camera, maxDistance) !== null);
|
|
15
|
+
}
|
|
16
|
+
/** Detached lights compose normalized authored defaults/props/params without intermediate capacity checks. */
|
|
17
|
+
function* sceneLights(scene, registry, params) {
|
|
18
|
+
for (const entity of scene.entities) {
|
|
19
|
+
for (const json of resolveEntityComponents(entity, registry.prefabs)) {
|
|
20
|
+
const Class = registryEntry(registry.components, json.type);
|
|
21
|
+
if (!isShadowClass(Class))
|
|
22
|
+
continue;
|
|
23
|
+
let light;
|
|
24
|
+
try {
|
|
25
|
+
light = new Class();
|
|
26
|
+
Object.assign(light, sceneProps(resolveProps(json.props, registry), json.type, entity.name), params[entity.name]?.[Class.componentName]);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Preserve spawnFromJson's isolation of throwing project-owned components.
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
yield light;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The live scene's shadow policy; resolving settings never allocates a shadow map. */
|
|
37
|
+
export class GameShadows {
|
|
38
|
+
game;
|
|
39
|
+
state = resolveSceneShadows(undefined);
|
|
40
|
+
renderer = null;
|
|
41
|
+
pointShadows = new Map();
|
|
42
|
+
composing = new WeakSet();
|
|
43
|
+
lastValid = new WeakMap();
|
|
44
|
+
capacityErrors = new WeakMap();
|
|
45
|
+
lastRendererFrame = null;
|
|
46
|
+
constructor(game) {
|
|
47
|
+
this.game = game;
|
|
48
|
+
}
|
|
49
|
+
connect(renderer) {
|
|
50
|
+
this.renderer = renderer;
|
|
51
|
+
}
|
|
52
|
+
get policy() {
|
|
53
|
+
return { ...this.state };
|
|
54
|
+
}
|
|
55
|
+
get quality() {
|
|
56
|
+
return this.state.quality;
|
|
57
|
+
}
|
|
58
|
+
set quality(value) {
|
|
59
|
+
this.adopt(resolveSceneShadows({ ...this.state, quality: value }));
|
|
60
|
+
}
|
|
61
|
+
get maxDistance() {
|
|
62
|
+
return this.state.maxDistance;
|
|
63
|
+
}
|
|
64
|
+
set maxDistance(value) {
|
|
65
|
+
this.adopt(resolveSceneShadows({ ...this.state, maxDistance: value }));
|
|
66
|
+
}
|
|
67
|
+
loadScene(value) {
|
|
68
|
+
this.adopt(resolveSceneShadows(value));
|
|
69
|
+
if (this.game.space !== '3d')
|
|
70
|
+
for (const light of this.lights())
|
|
71
|
+
this.remove(light);
|
|
72
|
+
}
|
|
73
|
+
unloadScene() {
|
|
74
|
+
this.state = resolveSceneShadows(undefined);
|
|
75
|
+
if (this.renderer)
|
|
76
|
+
this.renderer.shadowMap.enabled = false;
|
|
77
|
+
}
|
|
78
|
+
/** Validate final composed props, before Entity.add publishes or readies a light. */
|
|
79
|
+
validateAddition(component) {
|
|
80
|
+
if (component instanceof Sun || component instanceof PointLight)
|
|
81
|
+
this.assertCapacity(this.requested(component));
|
|
82
|
+
}
|
|
83
|
+
/** Check the ready device, even for authored 3D settings in a 2D preview. */
|
|
84
|
+
assertCapacity(settings) {
|
|
85
|
+
if (!this.renderer || !this.game.backend)
|
|
86
|
+
return;
|
|
87
|
+
assertShadowMapCapacity(settings, shadowTextureLimit(this.renderer.backend));
|
|
88
|
+
}
|
|
89
|
+
/** Persisted params are one composed request, not a sequence of active setters. */
|
|
90
|
+
applyParams(component, props) {
|
|
91
|
+
this.validateChange(component, props);
|
|
92
|
+
this.composing.add(component);
|
|
93
|
+
try {
|
|
94
|
+
Object.assign(component, props);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.composing.delete(component);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Check an incoming scene before unloadScene destroys the last valid one. */
|
|
101
|
+
validateScene(scene, registry) {
|
|
102
|
+
if (scene.render?.space !== '3d' || !this.game.backend)
|
|
103
|
+
return;
|
|
104
|
+
const policy = resolveSceneShadows(scene.render.shadows);
|
|
105
|
+
const camera = this.game.cameraRig.viewCamera ?? resolvePerspectiveCamera(scene.camera?.kind === 'perspective' ? scene.camera : undefined);
|
|
106
|
+
for (const light of sceneLights(scene, registry, this.game.paramOverrides)) {
|
|
107
|
+
const settings = resolveLightShadow(light instanceof Sun ? 'sun' : 'point', policy, light);
|
|
108
|
+
settings.enabled = settings.enabled && hasCoverage(light, camera, settings.maxDistance);
|
|
109
|
+
this.assertCapacity(settings);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** A failing request leaves both the authored live value and its active map unchanged. */
|
|
113
|
+
validateChange(component, change) {
|
|
114
|
+
if (this.composing.has(component))
|
|
115
|
+
return;
|
|
116
|
+
const light = this.lights().find((candidate) => candidate === component);
|
|
117
|
+
if (!light || !this.renderer || !this.game.backend || this.game.space !== '3d')
|
|
118
|
+
return;
|
|
119
|
+
const inputs = {
|
|
120
|
+
castShadows: light.castShadows, shadowMapSize: light.shadowMapSize,
|
|
121
|
+
shadowMaxDistance: light.shadowMaxDistance, shadowBias: light.shadowBias,
|
|
122
|
+
shadowNormalBias: light.shadowNormalBias, shadowRadius: light.shadowRadius,
|
|
123
|
+
distance: light instanceof PointLight ? light.distance : undefined,
|
|
124
|
+
...change,
|
|
125
|
+
};
|
|
126
|
+
const settings = this.withCoverage(light, resolveLightShadow(light instanceof Sun ? 'sun' : 'point', this.state, inputs));
|
|
127
|
+
assertShadowMapCapacity(settings, shadowTextureLimit(this.renderer.backend));
|
|
128
|
+
}
|
|
129
|
+
validate() {
|
|
130
|
+
this.validatePolicy(this.state);
|
|
131
|
+
}
|
|
132
|
+
resolve(light) {
|
|
133
|
+
const settings = this.effectiveSettings(light, this.requested(light));
|
|
134
|
+
if (settings.enabled)
|
|
135
|
+
this.lastValid.set(light, settings);
|
|
136
|
+
return settings;
|
|
137
|
+
}
|
|
138
|
+
requested(light) {
|
|
139
|
+
const settings = resolveLightShadow(light instanceof Sun ? 'sun' : 'point', this.state, light);
|
|
140
|
+
return this.withCoverage(light, settings);
|
|
141
|
+
}
|
|
142
|
+
effectiveSettings(component, requested) {
|
|
143
|
+
try {
|
|
144
|
+
this.assertCapacity(requested);
|
|
145
|
+
this.capacityErrors.delete(component);
|
|
146
|
+
return requested;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!(error instanceof RangeError))
|
|
150
|
+
throw error;
|
|
151
|
+
if (this.capacityErrors.get(component) !== error.message) {
|
|
152
|
+
console.error(`[waica] shadow request rejected for "${component.entity.name}": ${error.message} Keeping the last supported shadow configuration, or no map if none exists.`);
|
|
153
|
+
this.capacityErrors.set(component, error.message);
|
|
154
|
+
}
|
|
155
|
+
const previous = this.lastValid.get(component);
|
|
156
|
+
return previous && this.renderer && previous.mapSize <= shadowTextureLimit(this.renderer.backend)
|
|
157
|
+
? this.withCoverage(component, previous)
|
|
158
|
+
: { ...requested, enabled: false };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Called at the render boundary, including paused and edit-mode frames. */
|
|
162
|
+
sync(renderer) {
|
|
163
|
+
if (this.game.space !== '3d') {
|
|
164
|
+
renderer.shadowMap.enabled = false;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.lastRendererFrame = advanceRendererFrame(renderer, this.lastRendererFrame);
|
|
168
|
+
this.game.scene.updateWorldMatrix(true, false);
|
|
169
|
+
this.game.camera.updateWorldMatrix(true, false);
|
|
170
|
+
renderer.shadowMap.enabled = this.game.space === '3d' && this.state.quality !== 'off';
|
|
171
|
+
renderer.shadowMap.type = THREE.PCFShadowMap;
|
|
172
|
+
const lights = this.lights();
|
|
173
|
+
for (const component of this.pointShadows.keys()) {
|
|
174
|
+
if (!lights.includes(component))
|
|
175
|
+
this.remove(component);
|
|
176
|
+
}
|
|
177
|
+
for (const component of lights)
|
|
178
|
+
this.syncLight(component);
|
|
179
|
+
}
|
|
180
|
+
syncLight(component) {
|
|
181
|
+
const light = component.light;
|
|
182
|
+
if (!light)
|
|
183
|
+
return;
|
|
184
|
+
const settings = this.resolve(component);
|
|
185
|
+
if ((light.castShadow || light.shadow.map) && !settings.enabled) {
|
|
186
|
+
this.remove(component);
|
|
187
|
+
}
|
|
188
|
+
light.castShadow = settings.enabled;
|
|
189
|
+
light.shadow.mapSize.set(settings.mapSize, settings.mapSize);
|
|
190
|
+
// setSize retires GPU storage while preserving nodes held by cached materials.
|
|
191
|
+
light.shadow.map?.setSize(settings.mapSize, settings.mapSize);
|
|
192
|
+
light.shadow.bias = settings.bias;
|
|
193
|
+
light.shadow.normalBias = settings.normalBias;
|
|
194
|
+
light.shadow.radius = settings.radius;
|
|
195
|
+
if (component instanceof Sun)
|
|
196
|
+
this.fitSun(component, settings);
|
|
197
|
+
if (component instanceof PointLight && settings.enabled)
|
|
198
|
+
this.syncPoint(component, settings);
|
|
199
|
+
}
|
|
200
|
+
/** Prepare host overlays from the same current fit, without allocating or rendering. */
|
|
201
|
+
prepareSunCamera(component) {
|
|
202
|
+
const light = component.light;
|
|
203
|
+
if (!light)
|
|
204
|
+
return null;
|
|
205
|
+
this.game.scene.updateWorldMatrix(true, false);
|
|
206
|
+
this.game.camera.updateWorldMatrix(true, false);
|
|
207
|
+
return this.fitSun(component, this.resolve(component)) ? light.shadow.camera : null;
|
|
208
|
+
}
|
|
209
|
+
fitSun(component, settings) {
|
|
210
|
+
const camera = this.game.camera;
|
|
211
|
+
if (!component.light || !(camera instanceof THREE.PerspectiveCamera))
|
|
212
|
+
return false;
|
|
213
|
+
return fitSunShadow({ light: component.light, camera, settings, direction: component.direction });
|
|
214
|
+
}
|
|
215
|
+
syncPoint(component, settings) {
|
|
216
|
+
if (!component.light)
|
|
217
|
+
return;
|
|
218
|
+
let adapter = this.pointShadows.get(component);
|
|
219
|
+
if (!adapter) {
|
|
220
|
+
adapter = createRangedPointShadow(component.light);
|
|
221
|
+
this.pointShadows.set(component, adapter);
|
|
222
|
+
}
|
|
223
|
+
adapter.sync(settings.maxDistance);
|
|
224
|
+
}
|
|
225
|
+
withCoverage(light, settings) {
|
|
226
|
+
const camera = this.game.camera;
|
|
227
|
+
const enabled = this.game.space === '3d' && settings.enabled
|
|
228
|
+
&& hasCoverage(light, camera instanceof THREE.PerspectiveCamera ? camera : null, settings.maxDistance);
|
|
229
|
+
return { ...settings, enabled };
|
|
230
|
+
}
|
|
231
|
+
/** Notify Three's analytic light node as well as disposing the exposed target. */
|
|
232
|
+
remove(component) {
|
|
233
|
+
if (component instanceof PointLight) {
|
|
234
|
+
this.pointShadows.get(component)?.dispose();
|
|
235
|
+
this.pointShadows.delete(component);
|
|
236
|
+
}
|
|
237
|
+
const light = component.light;
|
|
238
|
+
if (!light)
|
|
239
|
+
return;
|
|
240
|
+
light.dispose();
|
|
241
|
+
light.shadow.map = null;
|
|
242
|
+
light.castShadow = false;
|
|
243
|
+
}
|
|
244
|
+
adopt(policy) {
|
|
245
|
+
this.validatePolicy(policy);
|
|
246
|
+
this.state = policy;
|
|
247
|
+
}
|
|
248
|
+
validatePolicy(policy) {
|
|
249
|
+
if (!this.renderer || !this.game.backend || this.game.space !== '3d')
|
|
250
|
+
return;
|
|
251
|
+
const limit = shadowTextureLimit(this.renderer.backend);
|
|
252
|
+
for (const light of this.lights()) {
|
|
253
|
+
const settings = resolveLightShadow(light instanceof Sun ? 'sun' : 'point', policy, light);
|
|
254
|
+
assertShadowMapCapacity(this.withCoverage(light, settings), limit);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lights() {
|
|
258
|
+
return this.game.entities.flatMap((entity) => entity.components.filter((component) => component instanceof Sun || component instanceof PointLight));
|
|
259
|
+
}
|
|
260
|
+
}
|
package/dist/game.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Entity } from './entity.js';
|
|
|
13
13
|
import type { PositionJson } from './entity-transform.js';
|
|
14
14
|
import { Emitter } from './events.js';
|
|
15
15
|
import { GameTime } from './game-time.js';
|
|
16
|
+
import { GameShadows } from './game-shadows.js';
|
|
16
17
|
import { Input, type InputBindings } from './input.js';
|
|
17
18
|
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
18
19
|
import type { PhysicsBackend } from './physics-3d/rapier-module.js';
|
|
@@ -122,6 +123,8 @@ export declare class Game {
|
|
|
122
123
|
readonly time: GameTime;
|
|
123
124
|
/** The live scene's Ambient Light and Lights (ADR 0026); it dies with its scene. */
|
|
124
125
|
readonly lighting: GameLighting;
|
|
126
|
+
/** Scene-scoped 3D shadow policy, independent of Ambient Light. */
|
|
127
|
+
readonly shadows: GameShadows;
|
|
125
128
|
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
126
129
|
readonly post: GamePost;
|
|
127
130
|
/** The live 3D scene's Rapier world and bodies (ADR 0028); package-internal, read by the physics components and the Runtime Snapshot. */
|
package/dist/game.js
CHANGED
|
@@ -10,8 +10,10 @@ import { Entity } from './entity.js';
|
|
|
10
10
|
import { EntityUpdateSchedules } from './entity-update-schedules.js';
|
|
11
11
|
import { Emitter } from './events.js';
|
|
12
12
|
import { RigidBody } from './components/rigid-body.js';
|
|
13
|
+
import { ShadowLight } from './components/shadow-light.js';
|
|
13
14
|
import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
|
|
14
15
|
import { advanceGameTime, GameTime } from './game-time.js';
|
|
16
|
+
import { GameShadows } from './game-shadows.js';
|
|
15
17
|
import { Input } from './input.js';
|
|
16
18
|
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
17
19
|
import { defaultPlatformBackend } from './platform/desktop-platform-backend.js';
|
|
@@ -67,6 +69,8 @@ export class Game {
|
|
|
67
69
|
time = new GameTime();
|
|
68
70
|
/** The live scene's Ambient Light and Lights (ADR 0026); it dies with its scene. */
|
|
69
71
|
lighting = new GameLighting();
|
|
72
|
+
/** Scene-scoped 3D shadow policy, independent of Ambient Light. */
|
|
73
|
+
shadows = new GameShadows(this);
|
|
70
74
|
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
71
75
|
post = new GamePost();
|
|
72
76
|
/** The live 3D scene's Rapier world and bodies (ADR 0028); package-internal, read by the physics components and the Runtime Snapshot. */
|
|
@@ -178,6 +182,7 @@ export class Game {
|
|
|
178
182
|
pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
|
|
179
183
|
});
|
|
180
184
|
this.renderer = new THREE.WebGPURenderer({ canvas, antialias: true });
|
|
185
|
+
this.shadows.connect(this.renderer);
|
|
181
186
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
182
187
|
// The model view on the CPU, as three's WebGL renderer computed it: a Sprite
|
|
183
188
|
// Batch instance carries that same matrix (ADR 0024), so batched and
|
|
@@ -211,7 +216,8 @@ export class Game {
|
|
|
211
216
|
* its first frame (ADR 0025).
|
|
212
217
|
*/
|
|
213
218
|
ready() {
|
|
214
|
-
|
|
219
|
+
const ready = this.physics.whenReady(this.readiness.promise);
|
|
220
|
+
return this.sceneSpace === '3d' ? ready.then(() => this.shadows.validate()) : ready;
|
|
215
221
|
}
|
|
216
222
|
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
217
223
|
get backend() {
|
|
@@ -285,6 +291,7 @@ export class Game {
|
|
|
285
291
|
this.time.cancelSceneScoped();
|
|
286
292
|
this.cameraEffects.unloadScene();
|
|
287
293
|
this.lighting.unloadScene();
|
|
294
|
+
this.shadows.unloadScene();
|
|
288
295
|
this.post.unloadScene();
|
|
289
296
|
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
290
297
|
// would otherwise flush next frame and resurrect one.
|
|
@@ -376,7 +383,11 @@ export class Game {
|
|
|
376
383
|
applyParamOverrides(entity, component) {
|
|
377
384
|
const Class = componentClassOf(component);
|
|
378
385
|
const override = this.paramOverrides[entity.name]?.[Class.componentName];
|
|
379
|
-
if (override)
|
|
386
|
+
if (!override)
|
|
387
|
+
return;
|
|
388
|
+
if (component instanceof ShadowLight)
|
|
389
|
+
this.shadows.applyParams(component, override);
|
|
390
|
+
else
|
|
380
391
|
Object.assign(component, override);
|
|
381
392
|
}
|
|
382
393
|
/**
|
|
@@ -399,6 +410,7 @@ export class Game {
|
|
|
399
410
|
this.renderSort = sort;
|
|
400
411
|
this.spriteBatches.enabled = json?.batch !== false;
|
|
401
412
|
this.lighting.loadScene(json);
|
|
413
|
+
this.shadows.loadScene(json?.shadows);
|
|
402
414
|
this.post.loadScene(json);
|
|
403
415
|
this.physics.enterScene(space);
|
|
404
416
|
if (projection === this.sceneProjection)
|
|
@@ -727,6 +739,7 @@ export class Game {
|
|
|
727
739
|
// Before ready() the frame simulated, but three cannot draw yet.
|
|
728
740
|
if (!this.readiness.isReady)
|
|
729
741
|
return;
|
|
742
|
+
this.shadows.sync(this.renderer);
|
|
730
743
|
this.frame.draw();
|
|
731
744
|
}
|
|
732
745
|
finally {
|
package/dist/index.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_B
|
|
|
62
62
|
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
63
63
|
export type { RenderBackend } from './render-readiness.js';
|
|
64
64
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
65
|
-
export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
65
|
+
export type { RuntimeSnapshotLight, RuntimeSnapshotLightShadows, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
66
66
|
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
67
67
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, SceneArchetypeNeeds, } from './archetype.js';
|
|
68
68
|
export { sceneArchetypeNeeds } from './archetype.js';
|
|
@@ -74,8 +74,8 @@ export { Sprite } from './components/sprite.js';
|
|
|
74
74
|
export { Light } from './components/light.js';
|
|
75
75
|
export { Model } from './components/model.js';
|
|
76
76
|
export type { ModelShape } from './components/model.js';
|
|
77
|
-
export { Animator } from './components/animator.js';
|
|
78
|
-
export type { AnimatorClipOverride, AnimatorState } from './components/animator.js';
|
|
77
|
+
export { Animator, DEFAULT_BLEND_SMOOTHING } from './components/animator.js';
|
|
78
|
+
export type { AnimatorBlend, AnimatorBlendPoint, AnimatorClipOverride, AnimatorState } from './components/animator.js';
|
|
79
79
|
export { poseHash } from './animation/pose-hash.js';
|
|
80
80
|
export { Sun } from './components/sun.js';
|
|
81
81
|
export { Collider, COLLIDER_SHAPES } from './components/collider.js';
|
|
@@ -88,12 +88,15 @@ export type { PhysicsBackend, RapierModule } from './physics-3d/rapier-module.js
|
|
|
88
88
|
export type { RuntimeSnapshotBody, RuntimeSnapshotPhysics } from './runtime-physics-snapshot.js';
|
|
89
89
|
export { PointLight } from './components/point-light.js';
|
|
90
90
|
export { GameLighting } from './scene-lighting.js';
|
|
91
|
+
export { GameShadows } from './game-shadows.js';
|
|
91
92
|
export type { AmbientLightInput } from './scene-lighting.js';
|
|
92
93
|
export { GamePost } from './post-effects.js';
|
|
93
94
|
export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
|
|
94
95
|
export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
|
|
95
96
|
export type { LightField, OccluderGrid, Rgb } from './light-field.js';
|
|
96
97
|
export { sceneRenderIssues } from './scene-render-options.js';
|
|
98
|
+
export { assertShadowMapCapacity, normalizeShadowBoolean, normalizeShadowOverride, resolveLightShadow, resolveSceneShadows, sceneShadowIssues, shadowComponentIssues, SHADOW_DEFAULTS, SHADOW_MAP_SIZES, SHADOW_OVERRIDE_PARAMS, SHADOW_QUALITIES, } from './scene-shadows.js';
|
|
99
|
+
export type { LightShadowOverrides, ResolvedLightShadow, ResolvedSceneShadows, ShadowOverrideName, } from './scene-shadows.js';
|
|
97
100
|
export type { AmbientLight, ColorGradeEffect, PostEffectsState, SceneRenderIssue, VignetteEffect, } from './scene-render-options.js';
|
|
98
101
|
export { ParticleEmitter } from './components/particle-emitter.js';
|
|
99
102
|
export type { ParticleBlend, ParticleDestroyMode, ParticleOverflow, ParticleSpace, ParticleVector, } from './components/particle-emitter.js';
|
|
@@ -115,7 +118,7 @@ export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit
|
|
|
115
118
|
export type { Point3d, QueryColliders3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
|
|
116
119
|
export { Emitter } from './events.js';
|
|
117
120
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
118
|
-
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
|
|
121
|
+
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, SceneShadowsJson, ShadowQuality, PrefabJson, } from './scene.js';
|
|
119
122
|
export { ClipPlayer } from './animation/clip-player.js';
|
|
120
123
|
export type { ClipDef } from './animation/clip-player.js';
|
|
121
124
|
export { sheetCell, sheetFrameCount, locateFrame } from './animation/sheet.js';
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export { GameUi } from './ui.js';
|
|
|
35
35
|
export { Sprite } from './components/sprite.js';
|
|
36
36
|
export { Light } from './components/light.js';
|
|
37
37
|
export { Model } from './components/model.js';
|
|
38
|
-
export { Animator } from './components/animator.js';
|
|
38
|
+
export { Animator, DEFAULT_BLEND_SMOOTHING } from './components/animator.js';
|
|
39
39
|
export { poseHash } from './animation/pose-hash.js';
|
|
40
40
|
export { Sun } from './components/sun.js';
|
|
41
41
|
export { Collider, COLLIDER_SHAPES } from './components/collider.js';
|
|
@@ -43,10 +43,12 @@ export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
|
|
|
43
43
|
export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
|
|
44
44
|
export { PointLight } from './components/point-light.js';
|
|
45
45
|
export { GameLighting } from './scene-lighting.js';
|
|
46
|
+
export { GameShadows } from './game-shadows.js';
|
|
46
47
|
export { GamePost } from './post-effects.js';
|
|
47
48
|
export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
|
|
48
49
|
export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
|
|
49
50
|
export { sceneRenderIssues } from './scene-render-options.js';
|
|
51
|
+
export { assertShadowMapCapacity, normalizeShadowBoolean, normalizeShadowOverride, resolveLightShadow, resolveSceneShadows, sceneShadowIssues, shadowComponentIssues, SHADOW_DEFAULTS, SHADOW_MAP_SIZES, SHADOW_OVERRIDE_PARAMS, SHADOW_QUALITIES, } from './scene-shadows.js';
|
|
50
52
|
export { ParticleEmitter } from './components/particle-emitter.js';
|
|
51
53
|
export { Solid } from './components/solid.js';
|
|
52
54
|
export { Hitbox } from './components/hitbox.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
export interface RangedPointShadow {
|
|
3
|
+
/** Call before rendering, with the resolved finite, positive shadow range. */
|
|
4
|
+
sync(maxDistance: number): void;
|
|
5
|
+
/** The owner must also notify the renderer's light node with light.dispose(). */
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the shadow camera independent of the real light's attenuation.
|
|
10
|
+
* The unparented proxy is never added to the scene and cannot illuminate it.
|
|
11
|
+
* Three's stock node still renders all six faces and its stock PCF does the
|
|
12
|
+
* filtering; the extra TSL gate clips receivers to an Euclidean world radius.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRangedPointShadow(light: THREE.PointLight): RangedPointShadow;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import { float, Fn, PointShadowFilter, positionWorld, uniform } from 'three/tsl';
|
|
3
|
+
function installShadowHooks(shadow, node, filterNode) {
|
|
4
|
+
const previousNode = shadow.shadowNode;
|
|
5
|
+
const previousFilter = 'filterNode' in shadow ? shadow.filterNode : undefined;
|
|
6
|
+
// Public WebGPURenderer filterNode hook, missing in @types/three 0.186.
|
|
7
|
+
const hooks = Object.assign(shadow, { shadowNode: node, filterNode });
|
|
8
|
+
return () => {
|
|
9
|
+
if (hooks.shadowNode === node)
|
|
10
|
+
shadow.shadowNode = previousNode;
|
|
11
|
+
if (hooks.filterNode === filterNode)
|
|
12
|
+
Object.assign(hooks, { filterNode: previousFilter });
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Keeps the shadow camera independent of the real light's attenuation.
|
|
17
|
+
* The unparented proxy is never added to the scene and cannot illuminate it.
|
|
18
|
+
* Three's stock node still renders all six faces and its stock PCF does the
|
|
19
|
+
* filtering; the extra TSL gate clips receivers to an Euclidean world radius.
|
|
20
|
+
*/
|
|
21
|
+
export function createRangedPointShadow(light) {
|
|
22
|
+
const shadow = light.shadow;
|
|
23
|
+
const proxy = new THREE.PointLight();
|
|
24
|
+
// lightShadowMatrix must use PointShadowNode's cubemap matrix, never LightShadow's target path.
|
|
25
|
+
proxy.castShadow = true;
|
|
26
|
+
proxy.shadow = shadow;
|
|
27
|
+
const node = new THREE.PointShadowNode(proxy, shadow);
|
|
28
|
+
const center = uniform(proxy.position);
|
|
29
|
+
const radius = uniform(0);
|
|
30
|
+
// Fn parameters are proxy arrays: nested functions need a fresh named-argument object.
|
|
31
|
+
const filterNode = Fn(({ depthTexture, bd3D, dp, shadow: filterShadow }) => positionWorld.sub(center).length().lessThanEqual(radius).select(PointShadowFilter({ depthTexture, bd3D, dp, shadow: filterShadow }), float(1)));
|
|
32
|
+
const unlinkHooks = installShadowHooks(shadow, node, filterNode);
|
|
33
|
+
let disposed = false;
|
|
34
|
+
return {
|
|
35
|
+
sync(maxDistance) {
|
|
36
|
+
if (disposed)
|
|
37
|
+
return;
|
|
38
|
+
const range = light.distance > 0 ? Math.min(light.distance, maxDistance) : maxDistance;
|
|
39
|
+
light.getWorldPosition(proxy.position);
|
|
40
|
+
proxy.updateMatrixWorld();
|
|
41
|
+
radius.value = range;
|
|
42
|
+
shadow.camera.near = Math.min(0.1, range / 100);
|
|
43
|
+
shadow.camera.far = range;
|
|
44
|
+
shadow.camera.updateProjectionMatrix();
|
|
45
|
+
},
|
|
46
|
+
dispose() {
|
|
47
|
+
if (disposed)
|
|
48
|
+
return;
|
|
49
|
+
disposed = true;
|
|
50
|
+
node.dispose();
|
|
51
|
+
unlinkHooks();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { Game } from './game.js';
|
|
|
3
3
|
import type { ProjectionIssue } from './runtime-inspection.js';
|
|
4
4
|
import type { Vec3Json } from './scene-camera-3d.js';
|
|
5
5
|
import type { AmbientLight, PostEffectsState } from './scene-render-options.js';
|
|
6
|
+
import type { LightShadowOverrides, ResolvedLightShadow, ResolvedSceneShadows } from './scene-shadows.js';
|
|
6
7
|
/** One live Light as a Runtime Snapshot reports it: its entity's name, logical position and params. */
|
|
7
8
|
export interface RuntimeSnapshotLight {
|
|
8
9
|
/** The entity's name, as `ui.anchored[].entity` reports it. */
|
|
@@ -19,6 +20,10 @@ export interface RuntimeSnapshotLight {
|
|
|
19
20
|
softness: number;
|
|
20
21
|
castShadows: boolean;
|
|
21
22
|
}
|
|
23
|
+
/** Effective shadow configuration, independent of GPU allocation, with the light's nullable authoring overrides. */
|
|
24
|
+
export interface RuntimeSnapshotLightShadows extends ResolvedLightShadow {
|
|
25
|
+
requested: Required<LightShadowOverrides>;
|
|
26
|
+
}
|
|
22
27
|
/** One live Sun in a 3D scene (issue #154 CA-16): its entity's name and id, the unit direction it shines along, color and intensity. */
|
|
23
28
|
export interface RuntimeSnapshotSun {
|
|
24
29
|
entity: string;
|
|
@@ -27,6 +32,7 @@ export interface RuntimeSnapshotSun {
|
|
|
27
32
|
/** `#rrggbb`. */
|
|
28
33
|
color: string;
|
|
29
34
|
intensity: number;
|
|
35
|
+
shadows: RuntimeSnapshotLightShadows;
|
|
30
36
|
}
|
|
31
37
|
/** One live Point Light in a 3D scene (issue #154 CA-16): its world position and look; `distance` 0 is unlimited. */
|
|
32
38
|
export interface RuntimeSnapshotPointLight {
|
|
@@ -37,17 +43,19 @@ export interface RuntimeSnapshotPointLight {
|
|
|
37
43
|
color: string;
|
|
38
44
|
intensity: number;
|
|
39
45
|
distance: number;
|
|
46
|
+
shadows: RuntimeSnapshotLightShadows;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
49
|
* `game.lighting` (issue #78 CA-12): the Ambient Light now and every live
|
|
43
50
|
* Light, in logical coordinates. A 3D scene (issue #154 CA-16) adds its
|
|
44
|
-
* Suns and
|
|
51
|
+
* Suns, Point Lights and shadow policy; a 2D scene's snapshot has none of these keys.
|
|
45
52
|
*/
|
|
46
53
|
export interface RuntimeSnapshotLighting {
|
|
47
54
|
ambient: AmbientLight;
|
|
48
55
|
lights: RuntimeSnapshotLight[];
|
|
49
56
|
sun?: RuntimeSnapshotSun[];
|
|
50
57
|
pointLights?: RuntimeSnapshotPointLight[];
|
|
58
|
+
shadows?: ResolvedSceneShadows;
|
|
51
59
|
}
|
|
52
60
|
/** `game.post` (issue #78 CA-12): each Post Effect, null when off. */
|
|
53
61
|
export type RuntimeSnapshotPost = PostEffectsState;
|
|
@@ -8,7 +8,20 @@ function unit(direction) {
|
|
|
8
8
|
const [x, y, z] = new THREE.Vector3(...direction).normalize().toArray();
|
|
9
9
|
return [rounded(x), rounded(y), rounded(z)];
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
function lightShadowsSnapshot(game, light) {
|
|
12
|
+
return {
|
|
13
|
+
...game.shadows.resolve(light),
|
|
14
|
+
requested: {
|
|
15
|
+
castShadows: light.castShadows,
|
|
16
|
+
shadowMapSize: light.shadowMapSize,
|
|
17
|
+
shadowMaxDistance: light.shadowMaxDistance,
|
|
18
|
+
shadowBias: light.shadowBias,
|
|
19
|
+
shadowNormalBias: light.shadowNormalBias,
|
|
20
|
+
shadowRadius: light.shadowRadius,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** The Suns and Point Lights of a 3D scene's entities, in spawn order, and its shadow policy. */
|
|
12
25
|
function lights3dSnapshot(game, idFor) {
|
|
13
26
|
const sun = [];
|
|
14
27
|
const pointLights = [];
|
|
@@ -16,18 +29,25 @@ function lights3dSnapshot(game, idFor) {
|
|
|
16
29
|
for (const component of entity.components) {
|
|
17
30
|
if (component instanceof Sun) {
|
|
18
31
|
const { direction, color, intensity } = component;
|
|
19
|
-
sun.push({
|
|
32
|
+
sun.push({
|
|
33
|
+
entity: entity.name, id: idFor(entity), direction: unit(direction), color: hexOf(color), intensity,
|
|
34
|
+
shadows: lightShadowsSnapshot(game, component),
|
|
35
|
+
});
|
|
20
36
|
}
|
|
21
37
|
else if (component instanceof PointLight && component.light) {
|
|
22
38
|
const { color, intensity, distance, light } = component;
|
|
23
39
|
light.updateWorldMatrix(true, false);
|
|
24
40
|
const at = light.getWorldPosition(new THREE.Vector3());
|
|
25
41
|
const position = [rounded(at.x), rounded(at.y), rounded(at.z)];
|
|
26
|
-
pointLights.push({
|
|
42
|
+
pointLights.push({
|
|
43
|
+
entity: entity.name, id: idFor(entity), position, color: hexOf(color), intensity, distance,
|
|
44
|
+
shadows: lightShadowsSnapshot(game, component),
|
|
45
|
+
});
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
}
|
|
30
|
-
|
|
49
|
+
const { quality, maxDistance } = game.shadows.policy;
|
|
50
|
+
return { sun, pointLights, shadows: { quality, maxDistance } };
|
|
31
51
|
}
|
|
32
52
|
export function lightingSnapshot(game, idFor) {
|
|
33
53
|
return {
|