@waica/engine 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,5 +1,7 @@
1
1
  import * as THREE from 'three/webgpu';
2
2
  import { Component } from '../component.js';
3
+ import { reportRejection } from '../report-rejection.js';
4
+ import { normalizeShadowBoolean } from '../scene-shadows.js';
3
5
  const SHAPES = ['box', 'sphere', 'plane'];
4
6
  /** One unit of the shape: a box of side 1, a sphere of diameter 1, a plane of side 1 lying flat on the ground (normal up). */
5
7
  function primitiveGeometry(shape) {
@@ -31,6 +33,8 @@ export class Model extends Component {
31
33
  shape: { label: 'Shape', options: [...SHAPES] },
32
34
  color: { label: 'Color', kind: 'color' },
33
35
  size: { label: 'Size', min: 0.01, step: 0.1 },
36
+ castShadows: { label: 'Cast shadows' },
37
+ receiveShadows: { label: 'Receive shadows' },
34
38
  };
35
39
  static transient = ['root', 'handle'];
36
40
  /** What this Model draws, under the entity's node; null until it is ready. The Pointer ray-casts against it. */
@@ -46,6 +50,8 @@ export class Model extends Component {
46
50
  _shape = 'box';
47
51
  _color = 0xffffff;
48
52
  _size = 1;
53
+ _castShadows = true;
54
+ _receiveShadows = true;
49
55
  _primitive = null;
50
56
  /** A `waica:` uri or project path to a `.glb` / `.gltf`; empty draws the primitive `shape`. */
51
57
  get src() {
@@ -78,6 +84,22 @@ export class Model extends Component {
78
84
  this._size = value;
79
85
  this._primitive?.mesh.scale.setScalar(value);
80
86
  }
87
+ /** Whether this Model's meshes cast shadows; only false opts out. */
88
+ get castShadows() {
89
+ return this._castShadows;
90
+ }
91
+ set castShadows(value) {
92
+ this._castShadows = normalizeShadowBoolean(value);
93
+ this.applyShadowFlags();
94
+ }
95
+ /** Whether this Model's meshes receive shadows; only false opts out. */
96
+ get receiveShadows() {
97
+ return this._receiveShadows;
98
+ }
99
+ set receiveShadows(value) {
100
+ this._receiveShadows = normalizeShadowBoolean(value);
101
+ this.applyShadowFlags();
102
+ }
81
103
  onReady() {
82
104
  // Draws only in a 3D scene, like Sun and PointLight: 2.5D is a later block, and a lit-less mesh would come out black.
83
105
  if (this.game.space !== '3d')
@@ -95,8 +117,19 @@ export class Model extends Component {
95
117
  if (!this.root)
96
118
  return;
97
119
  if (this._src) {
98
- this.handle = this.game.assets.model(this._src);
99
- this.root.add(this.handle.root);
120
+ const handle = this.game.assets.model(this._src);
121
+ this.handle = handle;
122
+ this.root.add(handle.root);
123
+ // A cache hit inserts its clone one microtask before settled reactions;
124
+ // apply on insertion so Assets Ready already exposes the latest flags.
125
+ const applyFlags = () => {
126
+ if (this.handle === handle)
127
+ this.applyShadowFlags();
128
+ };
129
+ handle.root.addEventListener('childadded', applyFlags);
130
+ reportRejection(handle.settled.finally(() => {
131
+ handle.root.removeEventListener('childadded', applyFlags);
132
+ }), 'model shadows settle');
100
133
  }
101
134
  else {
102
135
  this.buildPrimitive(this.root);
@@ -106,6 +139,8 @@ export class Model extends Component {
106
139
  const material = new THREE.MeshStandardNodeMaterial({ color: this._color });
107
140
  const mesh = new THREE.Mesh(primitiveGeometry(this._shape), material);
108
141
  mesh.scale.setScalar(this._size);
142
+ mesh.castShadow = this.castShadows;
143
+ mesh.receiveShadow = this.receiveShadows;
109
144
  root.add(mesh);
110
145
  this._primitive = { mesh };
111
146
  }
@@ -115,8 +150,19 @@ export class Model extends Component {
115
150
  this.clear();
116
151
  this.build();
117
152
  }
118
- /** Empties the root and disposes the primitive this Model created; a glTF's clone only leaves the graph. */
153
+ /** Participation belongs to this Model's meshes, never their shared glTF resources. */
154
+ applyShadowFlags() {
155
+ this.root?.traverse((object) => {
156
+ if (!(object instanceof THREE.Mesh))
157
+ return;
158
+ object.castShadow = this._castShadows;
159
+ object.receiveShadow = this._receiveShadows;
160
+ });
161
+ }
162
+ /** Retire clone-local render state; shared glTF geometry/materials stay cache-owned. */
119
163
  clear() {
164
+ for (const child of this.root?.children ?? [])
165
+ child.traverse((object) => object.dispose());
120
166
  this.root?.clear();
121
167
  this.handle = null;
122
168
  if (!this._primitive)
@@ -1,20 +1,55 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { Component, type ComponentSpace } from '../component.js';
2
+ import type { ComponentSpace } from '../component.js';
3
+ import { ShadowLight } from './shadow-light.js';
3
4
  /**
4
5
  * A point light in a 3D scene (ADR 0027, `CONTEXT.md` Point Light): it shines
5
6
  * in every direction from the entity's position plus an offset (in the
6
7
  * entity's local frame: it turns and scales with the entity, so a lantern's
7
8
  * offset follows the object it hangs on), reaching
8
9
  * `distance` world units (0 = without limit) and fading with `decay` (2 is
9
- * physical). Not the 2D `Light`, which paints a light-map. No shadows. In a
10
+ * physical). Not the 2D `Light`, which paints a light-map. Its shadows have an
11
+ * independent finite range without shortening illumination. In a
10
12
  * 2D scene it creates nothing; the three light lives under the entity's node
11
13
  * and leaves with it.
12
14
  */
13
- export declare class PointLight extends Component {
15
+ export declare class PointLight extends ShadowLight {
14
16
  static componentName: string;
15
17
  static space: ComponentSpace;
16
18
  static displayName: string;
17
19
  static params: {
20
+ shadowMapSize: {
21
+ label: string;
22
+ min: number;
23
+ max: number;
24
+ step: number;
25
+ };
26
+ shadowMaxDistance: {
27
+ label: string;
28
+ min: number;
29
+ max: number;
30
+ step: number;
31
+ };
32
+ shadowBias: {
33
+ label: string;
34
+ min: number;
35
+ max: number;
36
+ step: number;
37
+ };
38
+ shadowNormalBias: {
39
+ label: string;
40
+ min: number;
41
+ max: number;
42
+ step: number;
43
+ };
44
+ shadowRadius: {
45
+ label: string;
46
+ min: number;
47
+ max: number;
48
+ step: number;
49
+ };
50
+ castShadows: {
51
+ label: string;
52
+ };
18
53
  color: {
19
54
  label: string;
20
55
  kind: "color";
@@ -1,16 +1,17 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { Component } from '../component.js';
2
+ import { ShadowLight } from './shadow-light.js';
3
3
  /**
4
4
  * A point light in a 3D scene (ADR 0027, `CONTEXT.md` Point Light): it shines
5
5
  * in every direction from the entity's position plus an offset (in the
6
6
  * entity's local frame: it turns and scales with the entity, so a lantern's
7
7
  * offset follows the object it hangs on), reaching
8
8
  * `distance` world units (0 = without limit) and fading with `decay` (2 is
9
- * physical). Not the 2D `Light`, which paints a light-map. No shadows. In a
9
+ * physical). Not the 2D `Light`, which paints a light-map. Its shadows have an
10
+ * independent finite range without shortening illumination. In a
10
11
  * 2D scene it creates nothing; the three light lives under the entity's node
11
12
  * and leaves with it.
12
13
  */
13
- export class PointLight extends Component {
14
+ export class PointLight extends ShadowLight {
14
15
  static componentName = 'PointLight';
15
16
  static space = '3d';
16
17
  static displayName = 'Point light (3D)';
@@ -22,6 +23,7 @@ export class PointLight extends Component {
22
23
  offsetX: { label: 'x offset' },
23
24
  offsetY: { label: 'y offset' },
24
25
  offsetZ: { label: 'z offset' },
26
+ ...ShadowLight.params,
25
27
  };
26
28
  static transient = ['light'];
27
29
  /** The three light, null in a 2D scene or before the component is ready. */
@@ -94,6 +96,7 @@ export class PointLight extends Component {
94
96
  this.place();
95
97
  }
96
98
  onDestroy() {
99
+ this.game.shadows.remove(this);
97
100
  this.light?.removeFromParent();
98
101
  }
99
102
  place() {
@@ -0,0 +1,63 @@
1
+ import { Component } from '../component.js';
2
+ /** Shared, normalized authoring properties for the two 3D lights. */
3
+ export declare abstract class ShadowLight extends Component {
4
+ static params: {
5
+ shadowMapSize: {
6
+ label: string;
7
+ min: number;
8
+ max: number;
9
+ step: number;
10
+ };
11
+ shadowMaxDistance: {
12
+ label: string;
13
+ min: number;
14
+ max: number;
15
+ step: number;
16
+ };
17
+ shadowBias: {
18
+ label: string;
19
+ min: number;
20
+ max: number;
21
+ step: number;
22
+ };
23
+ shadowNormalBias: {
24
+ label: string;
25
+ min: number;
26
+ max: number;
27
+ step: number;
28
+ };
29
+ shadowRadius: {
30
+ label: string;
31
+ min: number;
32
+ max: number;
33
+ step: number;
34
+ };
35
+ castShadows: {
36
+ label: string;
37
+ };
38
+ };
39
+ private _castShadows;
40
+ private _shadowMapSize;
41
+ private _shadowMaxDistance;
42
+ private _shadowBias;
43
+ private _shadowNormalBias;
44
+ private _shadowRadius;
45
+ get castShadows(): boolean;
46
+ set castShadows(value: unknown);
47
+ /** Null inherits the scene's resolution for this kind of light. */
48
+ get shadowMapSize(): number | null;
49
+ set shadowMapSize(value: unknown);
50
+ /** Null inherits the scene's maximum shadow distance, in world units. */
51
+ get shadowMaxDistance(): number | null;
52
+ set shadowMaxDistance(value: unknown);
53
+ /** Null inherits the engine default shadow bias (SHADOW_DEFAULTS.bias). */
54
+ get shadowBias(): number | null;
55
+ set shadowBias(value: unknown);
56
+ /** Null inherits the engine default normal offset (SHADOW_DEFAULTS.normalBias). */
57
+ get shadowNormalBias(): number | null;
58
+ set shadowNormalBias(value: unknown);
59
+ /** Null inherits the engine default PCF radius (SHADOW_DEFAULTS.radius). */
60
+ get shadowRadius(): number | null;
61
+ set shadowRadius(value: unknown);
62
+ private validateChange;
63
+ }
@@ -0,0 +1,71 @@
1
+ import { Component } from '../component.js';
2
+ import { normalizeShadowBoolean, normalizeShadowOverride, SHADOW_OVERRIDE_PARAMS } from '../scene-shadows.js';
3
+ /** Shared, normalized authoring properties for the two 3D lights. */
4
+ export class ShadowLight extends Component {
5
+ static params = {
6
+ castShadows: { label: 'Cast shadows' },
7
+ ...SHADOW_OVERRIDE_PARAMS,
8
+ };
9
+ _castShadows = true;
10
+ _shadowMapSize = null;
11
+ _shadowMaxDistance = null;
12
+ _shadowBias = null;
13
+ _shadowNormalBias = null;
14
+ _shadowRadius = null;
15
+ get castShadows() {
16
+ return this._castShadows;
17
+ }
18
+ set castShadows(value) {
19
+ const normalized = normalizeShadowBoolean(value);
20
+ this.validateChange({ castShadows: normalized });
21
+ this._castShadows = normalized;
22
+ }
23
+ /** Null inherits the scene's resolution for this kind of light. */
24
+ get shadowMapSize() {
25
+ return this._shadowMapSize;
26
+ }
27
+ set shadowMapSize(value) {
28
+ const normalized = normalizeShadowOverride('shadowMapSize', value);
29
+ this.validateChange({ shadowMapSize: normalized });
30
+ this._shadowMapSize = normalized;
31
+ }
32
+ /** Null inherits the scene's maximum shadow distance, in world units. */
33
+ get shadowMaxDistance() {
34
+ return this._shadowMaxDistance;
35
+ }
36
+ set shadowMaxDistance(value) {
37
+ const normalized = normalizeShadowOverride('shadowMaxDistance', value);
38
+ this.validateChange({ shadowMaxDistance: normalized });
39
+ this._shadowMaxDistance = normalized;
40
+ }
41
+ /** Null inherits the engine default shadow bias (SHADOW_DEFAULTS.bias). */
42
+ get shadowBias() {
43
+ return this._shadowBias;
44
+ }
45
+ set shadowBias(value) {
46
+ const normalized = normalizeShadowOverride('shadowBias', value);
47
+ this.validateChange({ shadowBias: normalized });
48
+ this._shadowBias = normalized;
49
+ }
50
+ /** Null inherits the engine default normal offset (SHADOW_DEFAULTS.normalBias). */
51
+ get shadowNormalBias() {
52
+ return this._shadowNormalBias;
53
+ }
54
+ set shadowNormalBias(value) {
55
+ const normalized = normalizeShadowOverride('shadowNormalBias', value);
56
+ this.validateChange({ shadowNormalBias: normalized });
57
+ this._shadowNormalBias = normalized;
58
+ }
59
+ /** Null inherits the engine default PCF radius (SHADOW_DEFAULTS.radius). */
60
+ get shadowRadius() {
61
+ return this._shadowRadius;
62
+ }
63
+ set shadowRadius(value) {
64
+ const normalized = normalizeShadowOverride('shadowRadius', value);
65
+ this.validateChange({ shadowRadius: normalized });
66
+ this._shadowRadius = normalized;
67
+ }
68
+ validateChange(change) {
69
+ this.game?.shadows.validateChange(this, change);
70
+ }
71
+ }
@@ -1,17 +1,52 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { Component, type ComponentSpace } from '../component.js';
2
+ import type { ComponentSpace } from '../component.js';
3
3
  import type { Vec3Json } from '../scene-camera-3d.js';
4
+ import { ShadowLight } from './shadow-light.js';
4
5
  /**
5
6
  * The 3D scene's sunlight (ADR 0027, `CONTEXT.md` Sun): a directional light
6
7
  * that shines along `direction`, in world space — where its entity stands, or
7
- * how it is rotated, does not matter. Color is `0xrrggbb`; no shadows. A Sun
8
+ * how it is rotated, does not matter. Color is `0xrrggbb`; shadows follow the
9
+ * active view through the scene's shadow policy. A Sun
8
10
  * in a 2D scene creates nothing. The three light lives in the Game's scene,
9
11
  * moves with its props, and leaves with the entity.
10
12
  */
11
- export declare class Sun extends Component {
13
+ export declare class Sun extends ShadowLight {
12
14
  static componentName: string;
13
15
  static space: ComponentSpace;
14
16
  static params: {
17
+ shadowMapSize: {
18
+ label: string;
19
+ min: number;
20
+ max: number;
21
+ step: number;
22
+ };
23
+ shadowMaxDistance: {
24
+ label: string;
25
+ min: number;
26
+ max: number;
27
+ step: number;
28
+ };
29
+ shadowBias: {
30
+ label: string;
31
+ min: number;
32
+ max: number;
33
+ step: number;
34
+ };
35
+ shadowNormalBias: {
36
+ label: string;
37
+ min: number;
38
+ max: number;
39
+ step: number;
40
+ };
41
+ shadowRadius: {
42
+ label: string;
43
+ min: number;
44
+ max: number;
45
+ step: number;
46
+ };
47
+ castShadows: {
48
+ label: string;
49
+ };
15
50
  direction: {
16
51
  label: string;
17
52
  kind: "vector3";
@@ -1,22 +1,24 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { Component } from '../component.js';
2
+ import { ShadowLight } from './shadow-light.js';
3
3
  const DEFAULT_DIRECTION = [-1, -2, -1];
4
4
  /** Scene JSON is untyped at runtime: only an array of three finite numbers, not all zero, aims a light. */
5
5
  const finiteDirection = (value) => Array.isArray(value) && value.length === 3 && value.every(Number.isFinite) && value.some((component) => component !== 0);
6
6
  /**
7
7
  * The 3D scene's sunlight (ADR 0027, `CONTEXT.md` Sun): a directional light
8
8
  * that shines along `direction`, in world space — where its entity stands, or
9
- * how it is rotated, does not matter. Color is `0xrrggbb`; no shadows. A Sun
9
+ * how it is rotated, does not matter. Color is `0xrrggbb`; shadows follow the
10
+ * active view through the scene's shadow policy. A Sun
10
11
  * in a 2D scene creates nothing. The three light lives in the Game's scene,
11
12
  * moves with its props, and leaves with the entity.
12
13
  */
13
- export class Sun extends Component {
14
+ export class Sun extends ShadowLight {
14
15
  static componentName = 'Sun';
15
16
  static space = '3d';
16
17
  static params = {
17
18
  direction: { label: 'Direction', kind: 'vector3' },
18
19
  color: { label: 'Color', kind: 'color' },
19
20
  intensity: { label: 'Intensity', min: 0, step: 0.1 },
21
+ ...ShadowLight.params,
20
22
  };
21
23
  static transient = ['light'];
22
24
  /** The three light, null in a 2D scene or before the component is ready. */
@@ -60,6 +62,7 @@ export class Sun extends Component {
60
62
  this.aim();
61
63
  }
62
64
  onDestroy() {
65
+ this.game.shadows.remove(this);
63
66
  this.light?.removeFromParent();
64
67
  this.light?.target.removeFromParent();
65
68
  }
package/dist/entity.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as THREE from 'three/webgpu';
2
+ import { ShadowLight } from './components/shadow-light.js';
2
3
  /**
3
4
  * A live scene node: a transform (three Group) + components.
4
5
  * Created with `game.spawn(name)`.
@@ -43,6 +44,8 @@ export class Entity {
43
44
  if (props)
44
45
  Object.assign(component, props);
45
46
  this.game.applyParamOverrides(this, component);
47
+ if (component instanceof ShadowLight)
48
+ this.game.shadows.validateAddition(component);
46
49
  this.components.push(component);
47
50
  component.onReady?.();
48
51
  return component;
@@ -0,0 +1,54 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { PointLight } from './components/point-light.js';
3
+ import { Sun } from './components/sun.js';
4
+ import type { ShadowLight } from './components/shadow-light.js';
5
+ import type { Game } from './game.js';
6
+ import { type LightShadowOverrides, type ResolvedLightShadow, type ResolvedSceneShadows, type ShadowQuality } from './scene-shadows.js';
7
+ import { type SceneJson, type SceneRegistry } from './scene.js';
8
+ /** The live scene's shadow policy; resolving settings never allocates a shadow map. */
9
+ export declare class GameShadows {
10
+ private readonly game;
11
+ private state;
12
+ private renderer;
13
+ private readonly pointShadows;
14
+ private readonly composing;
15
+ private readonly lastValid;
16
+ private readonly capacityErrors;
17
+ private lastRendererFrame;
18
+ constructor(game: Game);
19
+ connect(renderer: THREE.WebGPURenderer): void;
20
+ get policy(): ResolvedSceneShadows;
21
+ get quality(): ShadowQuality;
22
+ set quality(value: unknown);
23
+ get maxDistance(): number;
24
+ set maxDistance(value: unknown);
25
+ loadScene(value: unknown): void;
26
+ unloadScene(): void;
27
+ /** Validate final composed props, before Entity.add publishes or readies a light. */
28
+ validateAddition(component: ShadowLight): void;
29
+ /** Check the ready device, even for authored 3D settings in a 2D preview. */
30
+ assertCapacity(settings: ResolvedLightShadow): void;
31
+ /** Persisted params are one composed request, not a sequence of active setters. */
32
+ applyParams(component: ShadowLight, props: Record<string, unknown>): void;
33
+ /** Check an incoming scene before unloadScene destroys the last valid one. */
34
+ validateScene(scene: SceneJson, registry: SceneRegistry): void;
35
+ /** A failing request leaves both the authored live value and its active map unchanged. */
36
+ validateChange(component: ShadowLight, change: LightShadowOverrides | Record<string, unknown>): void;
37
+ validate(): void;
38
+ resolve(light: Sun | PointLight): ResolvedLightShadow;
39
+ private requested;
40
+ private effectiveSettings;
41
+ /** Called at the render boundary, including paused and edit-mode frames. */
42
+ sync(renderer: THREE.WebGPURenderer): void;
43
+ private syncLight;
44
+ /** Prepare host overlays from the same current fit, without allocating or rendering. */
45
+ prepareSunCamera(component: Sun): THREE.OrthographicCamera | null;
46
+ private fitSun;
47
+ private syncPoint;
48
+ private withCoverage;
49
+ /** Notify Three's analytic light node as well as disposing the exposed target. */
50
+ remove(component: Sun | PointLight): void;
51
+ private adopt;
52
+ private validatePolicy;
53
+ private lights;
54
+ }