@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.
Files changed (41) hide show
  1. package/README.md +48 -3
  2. package/dist/camera-rig.d.ts +2 -0
  3. package/dist/camera-rig.js +4 -0
  4. package/dist/components/animator-blends.d.ts +54 -0
  5. package/dist/components/animator-blends.js +86 -0
  6. package/dist/components/animator-mix.d.ts +53 -0
  7. package/dist/components/animator-mix.js +92 -0
  8. package/dist/components/animator-params.d.ts +21 -0
  9. package/dist/components/animator-params.js +50 -0
  10. package/dist/components/animator-sources.d.ts +59 -0
  11. package/dist/components/animator-sources.js +125 -0
  12. package/dist/components/animator.d.ts +84 -10
  13. package/dist/components/animator.js +163 -47
  14. package/dist/components/model.d.ts +17 -1
  15. package/dist/components/model.js +49 -3
  16. package/dist/components/point-light.d.ts +38 -3
  17. package/dist/components/point-light.js +6 -3
  18. package/dist/components/shadow-light.d.ts +63 -0
  19. package/dist/components/shadow-light.js +71 -0
  20. package/dist/components/sun.d.ts +38 -3
  21. package/dist/components/sun.js +6 -3
  22. package/dist/entity.js +3 -0
  23. package/dist/game-shadows.d.ts +54 -0
  24. package/dist/game-shadows.js +260 -0
  25. package/dist/game.d.ts +3 -0
  26. package/dist/game.js +15 -2
  27. package/dist/index.d.ts +7 -4
  28. package/dist/index.js +3 -1
  29. package/dist/point-shadow.d.ts +14 -0
  30. package/dist/point-shadow.js +54 -0
  31. package/dist/runtime-lighting-snapshot.d.ts +9 -1
  32. package/dist/runtime-lighting-snapshot.js +24 -4
  33. package/dist/scene-shadows.d.ts +82 -0
  34. package/dist/scene-shadows.js +151 -0
  35. package/dist/scene.d.ts +11 -1
  36. package/dist/scene.js +2 -1
  37. package/dist/shadow-renderer.d.ts +8 -0
  38. package/dist/shadow-renderer.js +37 -0
  39. package/dist/sun-shadow-camera.d.ts +17 -0
  40. package/dist/sun-shadow-camera.js +67 -0
  41. 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
- return this.physics.whenReady(this.readiness.promise);
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 Point Lights; a 2D scene's snapshot has neither key.
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
- /** The Suns and Point Lights of a 3D scene's entities, in spawn order. */
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({ entity: entity.name, id: idFor(entity), direction: unit(direction), color: hexOf(color), intensity });
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({ entity: entity.name, id: idFor(entity), position, color: hexOf(color), intensity, distance });
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
- return { sun, pointLights };
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 {