@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,125 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/** A clip of the file as one sound of the mix: its own action, advanced by the mixer at its `speed`. */
|
|
3
|
+
export class ClipSource {
|
|
4
|
+
name;
|
|
5
|
+
action;
|
|
6
|
+
constructor(name, action) {
|
|
7
|
+
this.name = name;
|
|
8
|
+
this.action = action;
|
|
9
|
+
}
|
|
10
|
+
/** A `loop: false` clip clamps when it ends, which three marks by pausing it: nothing else pauses an action here. */
|
|
11
|
+
get finished() {
|
|
12
|
+
return this.action.loop === THREE.LoopOnce && this.action.paused;
|
|
13
|
+
}
|
|
14
|
+
get time() {
|
|
15
|
+
return this.action.time;
|
|
16
|
+
}
|
|
17
|
+
get progress() {
|
|
18
|
+
const { duration } = this.action.getClip();
|
|
19
|
+
return duration > 0 ? Math.min(this.action.time / duration, 1) : 1;
|
|
20
|
+
}
|
|
21
|
+
get playing() {
|
|
22
|
+
return this.action.isRunning();
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
this.action.reset().play();
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
this.action.stop();
|
|
29
|
+
}
|
|
30
|
+
apply(weight) {
|
|
31
|
+
this.action.setEffectiveWeight(weight);
|
|
32
|
+
}
|
|
33
|
+
addWeights(weight, into) {
|
|
34
|
+
into[this.name] = (into[this.name] ?? 0) + weight;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A blend as one sound of the mix (issue #205, ADR 0033): its points share
|
|
39
|
+
* one normalized phase, advanced by the step over the cycle interpolated by
|
|
40
|
+
* the points' shares, so walk and run put their feet down together; each
|
|
41
|
+
* point's action sits at that phase of its own clip, weighted by its share.
|
|
42
|
+
*/
|
|
43
|
+
export class BlendSource {
|
|
44
|
+
name;
|
|
45
|
+
points;
|
|
46
|
+
value;
|
|
47
|
+
phase = 0;
|
|
48
|
+
constructor(name, points, value) {
|
|
49
|
+
this.name = name;
|
|
50
|
+
this.points = points;
|
|
51
|
+
this.value = value;
|
|
52
|
+
}
|
|
53
|
+
/** A blend loops: it never finishes. */
|
|
54
|
+
get finished() {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/** The phase as seconds of the interpolated cycle. */
|
|
58
|
+
get time() {
|
|
59
|
+
return this.phase * this.cycle(blendShares(this.points, this.value()));
|
|
60
|
+
}
|
|
61
|
+
get progress() {
|
|
62
|
+
return this.phase;
|
|
63
|
+
}
|
|
64
|
+
get playing() {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
start() {
|
|
68
|
+
this.phase = 0;
|
|
69
|
+
for (const point of this.points)
|
|
70
|
+
point.action.reset().play();
|
|
71
|
+
}
|
|
72
|
+
stop() {
|
|
73
|
+
for (const point of this.points)
|
|
74
|
+
point.action.stop();
|
|
75
|
+
}
|
|
76
|
+
apply(weight, dt) {
|
|
77
|
+
const shares = blendShares(this.points, this.value());
|
|
78
|
+
const cycle = this.cycle(shares);
|
|
79
|
+
if (cycle > 0) {
|
|
80
|
+
this.phase += dt / cycle;
|
|
81
|
+
this.phase -= Math.floor(this.phase);
|
|
82
|
+
}
|
|
83
|
+
for (const [index, point] of this.points.entries()) {
|
|
84
|
+
point.action.time = this.phase * point.duration;
|
|
85
|
+
point.action.setEffectiveWeight(weight * (shares[index] ?? 0));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
addWeights(weight, into) {
|
|
89
|
+
const shares = blendShares(this.points, this.value());
|
|
90
|
+
for (const [index, point] of this.points.entries()) {
|
|
91
|
+
const share = shares[index] ?? 0;
|
|
92
|
+
if (share > 0)
|
|
93
|
+
into[point.clip] = (into[point.clip] ?? 0) + weight * share;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
cycle(shares) {
|
|
97
|
+
return this.points.reduce((sum, point, index) => sum + (shares[index] ?? 0) * point.cycle, 0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Each point's share of a 1D blend at `value`: the two points around it,
|
|
102
|
+
* weighted linearly by where it falls between their thresholds; past either
|
|
103
|
+
* end, that end alone. `points` are in threshold order.
|
|
104
|
+
*/
|
|
105
|
+
export function blendShares(points, value) {
|
|
106
|
+
const shares = points.map(() => 0);
|
|
107
|
+
const last = points.length - 1;
|
|
108
|
+
const first = points[0];
|
|
109
|
+
const final = points[last];
|
|
110
|
+
if (!first || !final)
|
|
111
|
+
return shares;
|
|
112
|
+
if (value <= first.at)
|
|
113
|
+
shares[0] = 1;
|
|
114
|
+
else if (value >= final.at)
|
|
115
|
+
shares[last] = 1;
|
|
116
|
+
else {
|
|
117
|
+
const index = points.findIndex((point, i) => i < last && value < (points[i + 1]?.at ?? point.at));
|
|
118
|
+
const low = points[index]?.at ?? first.at;
|
|
119
|
+
const high = points[index + 1]?.at ?? low;
|
|
120
|
+
const t = high > low ? (value - low) / (high - low) : 1;
|
|
121
|
+
shares[index] = 1 - t;
|
|
122
|
+
shares[index + 1] = t;
|
|
123
|
+
}
|
|
124
|
+
return shares;
|
|
125
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { ClipTarget } from '../animation/clip-target.js';
|
|
2
2
|
import { Component, type ComponentSpace } from '../component.js';
|
|
3
|
-
|
|
3
|
+
import { type AnimatorBlend } from './animator-blends.js';
|
|
4
|
+
export type { AnimatorBlend, AnimatorBlendPoint } from './animator-blends.js';
|
|
5
|
+
export { DEFAULT_BLEND_SMOOTHING } from './animator-blends.js';
|
|
6
|
+
/** Per-clip overrides, keyed by the glTF's clip name: a one-shot (`loop: false`), a playback rate and a fade-in. */
|
|
4
7
|
export interface AnimatorClipOverride {
|
|
5
8
|
/** Defaults to true; false plays the clip once and holds its last pose. */
|
|
6
9
|
loop?: boolean;
|
|
7
10
|
/** Playback rate; 1 by default, 2 finishes in half the time. */
|
|
8
11
|
speed?: number;
|
|
12
|
+
/** Seconds of fade into this clip (issue #205); without it, the Animator's `fade`. 0 cuts. */
|
|
13
|
+
fade?: number;
|
|
9
14
|
}
|
|
10
15
|
/** What `inspect_runtime` shows of an Animator. */
|
|
11
16
|
export interface AnimatorState {
|
|
@@ -18,16 +23,23 @@ export interface AnimatorState {
|
|
|
18
23
|
playing: boolean;
|
|
19
24
|
/** The pose fingerprint of the rig (see poseHash); empty with no skeleton. */
|
|
20
25
|
poseHash: string;
|
|
26
|
+
/** Each glb clip's effective weight, only those above 0, the ones fading out included (issue #205). */
|
|
27
|
+
weights: Record<string, number>;
|
|
28
|
+
/** The smoothed value of every parameter the blends read or `setParam` wrote. */
|
|
29
|
+
params: Record<string, number>;
|
|
21
30
|
}
|
|
22
31
|
/**
|
|
23
|
-
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029
|
|
24
|
-
* 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
32
|
+
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029; mixing,
|
|
33
|
+
* issue #205, ADR 0033): the 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
25
34
|
* per clip, advanced by the Simulation Step in onUpdate — nothing runs per
|
|
26
35
|
* render frame, so the pose is deterministic and visible step by step. A
|
|
27
36
|
* StateMachine finds it like it finds an AnimatedSprite and plays the clip of
|
|
28
37
|
* each state; `clip:end` fires when a `loop: false` clip finishes. A `play`
|
|
29
38
|
* before the model loaded is remembered and applied when it settles;
|
|
30
|
-
* otherwise `initialClip` plays then.
|
|
39
|
+
* otherwise `initialClip` plays then. A fade into a clip lasts its own
|
|
40
|
+
* `clips[name].fade` or `fade`, every sounding clip leaving from the weight
|
|
41
|
+
* it has; `blends` are virtual clips mixing several by a `setParam` value.
|
|
42
|
+
* In a 2D scene it creates nothing.
|
|
31
43
|
*/
|
|
32
44
|
export declare class Animator extends Component implements ClipTarget {
|
|
33
45
|
static componentName: string;
|
|
@@ -51,18 +63,30 @@ export declare class Animator extends Component implements ClipTarget {
|
|
|
51
63
|
fade: number;
|
|
52
64
|
/**
|
|
53
65
|
* Per-clip overrides by glTF clip name (authored in JSON, like
|
|
54
|
-
* AnimatedSprite.clips).
|
|
55
|
-
* settles
|
|
66
|
+
* AnimatedSprite.clips). `loop` and `speed` are read when the actions are
|
|
67
|
+
* built, as the model settles, so a change after that applies on the next
|
|
68
|
+
* build (a Model rebuild); `fade` is read at every `play`.
|
|
56
69
|
*/
|
|
57
70
|
clips: Record<string, AnimatorClipOverride>;
|
|
71
|
+
/**
|
|
72
|
+
* 1D blends by name (`AnimatorBlend`, authored in JSON like `clips`): each
|
|
73
|
+
* is a clip `play` accepts. Its `param` and `points` are read when the
|
|
74
|
+
* actions are built, as the model settles; its `smoothing` every step.
|
|
75
|
+
*/
|
|
76
|
+
blends: Record<string, AnimatorBlend>;
|
|
58
77
|
/** The clip playing now; undefined before the first play. */
|
|
59
78
|
current: string | undefined;
|
|
60
79
|
private mixer;
|
|
61
|
-
|
|
62
|
-
private
|
|
80
|
+
/** What `play` accepts, by name, for the current build: the file's clips, then the blends. */
|
|
81
|
+
private readonly sources;
|
|
82
|
+
/** The fades between them. */
|
|
83
|
+
private readonly mix;
|
|
84
|
+
/** The named parameters the blends read; they outlive a rebuild. */
|
|
85
|
+
private readonly params;
|
|
86
|
+
/** Each blend the current build made playable, by name, and the parameter it reads; null before a build. */
|
|
87
|
+
private built;
|
|
63
88
|
/** A play asked for before the model loaded. */
|
|
64
89
|
private requested;
|
|
65
|
-
private finished;
|
|
66
90
|
private root;
|
|
67
91
|
private readonly warned;
|
|
68
92
|
private attached;
|
|
@@ -82,11 +106,61 @@ export declare class Animator extends Component implements ClipTarget {
|
|
|
82
106
|
*/
|
|
83
107
|
get clipProgress(): number;
|
|
84
108
|
onReady(): void;
|
|
85
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Plays a clip: a no-op for the running one, a restart for a finished one,
|
|
111
|
+
* and from any other a fade over the clip's own `clips[name].fade`, else
|
|
112
|
+
* `fade` seconds, every sounding clip leaving from the weight it has (ADR 0033).
|
|
113
|
+
*/
|
|
86
114
|
play(name: string): void;
|
|
115
|
+
/**
|
|
116
|
+
* Aims a named parameter at `value`; the blends reading it follow, smoothed
|
|
117
|
+
* by the first blend that names it (once built, the first one the build made
|
|
118
|
+
* playable). Kept when made before the model loads;
|
|
119
|
+
* a value that is not a finite number is ignored with one warning.
|
|
120
|
+
*/
|
|
121
|
+
setParam(name: string, value: number): void;
|
|
87
122
|
onUpdate(dt: number): void;
|
|
123
|
+
/**
|
|
124
|
+
* One step of the parameters, the fades and the mixer while the Game does
|
|
125
|
+
* not simulate (issue #205): the editor's edit-mode preview of a blend
|
|
126
|
+
* drives the selected entity's Animator alone through it, once per frame,
|
|
127
|
+
* following its Model as onUpdate does. In Play it does nothing: onUpdate
|
|
128
|
+
* steps the Animator then.
|
|
129
|
+
*/
|
|
130
|
+
previewStep(dt: number): void;
|
|
131
|
+
/**
|
|
132
|
+
* Ends an edit-mode preview (issue #205) as a freshly built stage starts:
|
|
133
|
+
* no parameter written, every clip stopped (three puts the rig back in the
|
|
134
|
+
* pose its file was built in) and `initialClip`, if any, asked for again
|
|
135
|
+
* from its start. In Play, or before the model loads, it does nothing.
|
|
136
|
+
*/
|
|
137
|
+
previewReset(): void;
|
|
88
138
|
onDestroy(): void;
|
|
89
139
|
inspectState(): AnimatorState;
|
|
140
|
+
/** One step of the parameters, then of the fades, then of the mixer. */
|
|
141
|
+
private advance;
|
|
142
|
+
/** The smoothing of the first blend that reads `param`; none without one. */
|
|
143
|
+
private smoothingOf;
|
|
144
|
+
/** The parameters the blends read, in declaration order. */
|
|
145
|
+
private blendParams;
|
|
146
|
+
/**
|
|
147
|
+
* Each blend's name and the parameter it reads: once built, only the blends
|
|
148
|
+
* the build made playable (one it dropped decides nothing); before, every
|
|
149
|
+
* authored blend that names a parameter.
|
|
150
|
+
*/
|
|
151
|
+
private blendReaders;
|
|
152
|
+
/** The authored blends by name; a `blends` that is not an object (JSON props arrive unchecked) warns once and counts as none. */
|
|
153
|
+
private authoredBlends;
|
|
154
|
+
/** The fade into `name`: its own `clips[name].fade` when that is a finite number ≥ 0, else `fade`. */
|
|
155
|
+
private fadeInto;
|
|
156
|
+
/**
|
|
157
|
+
* Keeps the mixer on the Model's current clone. A rebuilt Model (a new src
|
|
158
|
+
* or shape) draws a new clone: the mixer over the old one would animate a
|
|
159
|
+
* graph nobody draws, so it is torn down and the clip it was playing is
|
|
160
|
+
* asked for again on the new one. A Model listed after the Animator, which
|
|
161
|
+
* onReady could not see, is attached here.
|
|
162
|
+
*/
|
|
163
|
+
private syncModel;
|
|
90
164
|
/**
|
|
91
165
|
* Takes the sibling Model's handle and builds the mixer when it settles. A
|
|
92
166
|
* Model later in the component order is not there at onReady, so the first
|
|
@@ -2,16 +2,24 @@ import * as THREE from 'three/webgpu';
|
|
|
2
2
|
import { poseHash } from '../animation/pose-hash.js';
|
|
3
3
|
import { Component } from '../component.js';
|
|
4
4
|
import { reportRejection } from '../report-rejection.js';
|
|
5
|
+
import { blendReaders, blendSmoothing, blendsRecord, buildBlendSources } from './animator-blends.js';
|
|
6
|
+
export { DEFAULT_BLEND_SMOOTHING } from './animator-blends.js';
|
|
7
|
+
import { AnimatorMix } from './animator-mix.js';
|
|
8
|
+
import { AnimatorParams } from './animator-params.js';
|
|
9
|
+
import { ClipSource } from './animator-sources.js';
|
|
5
10
|
import { Model } from './model.js';
|
|
6
11
|
/**
|
|
7
|
-
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029
|
|
8
|
-
* 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
12
|
+
* Plays the glTF clips of the Model beside it (issue #166, ADR 0029; mixing,
|
|
13
|
+
* issue #205, ADR 0033): the 3D Clip Target. One AnimationMixer over the Model's own clone, one action
|
|
9
14
|
* per clip, advanced by the Simulation Step in onUpdate — nothing runs per
|
|
10
15
|
* render frame, so the pose is deterministic and visible step by step. A
|
|
11
16
|
* StateMachine finds it like it finds an AnimatedSprite and plays the clip of
|
|
12
17
|
* each state; `clip:end` fires when a `loop: false` clip finishes. A `play`
|
|
13
18
|
* before the model loaded is remembered and applied when it settles;
|
|
14
|
-
* otherwise `initialClip` plays then.
|
|
19
|
+
* otherwise `initialClip` plays then. A fade into a clip lasts its own
|
|
20
|
+
* `clips[name].fade` or `fade`, every sounding clip leaving from the weight
|
|
21
|
+
* it has; `blends` are virtual clips mixing several by a `setParam` value.
|
|
22
|
+
* In a 2D scene it creates nothing.
|
|
15
23
|
*/
|
|
16
24
|
export class Animator extends Component {
|
|
17
25
|
static componentName = 'Animator';
|
|
@@ -21,25 +29,37 @@ export class Animator extends Component {
|
|
|
21
29
|
initialClip: { label: 'Initial clip' },
|
|
22
30
|
fade: { label: 'Fade (s)', min: 0, max: 1, step: 0.05 },
|
|
23
31
|
};
|
|
24
|
-
static transient = ['current', 'mixer', '
|
|
32
|
+
static transient = ['current', 'mixer', 'sources', 'mix', 'params', 'built', 'requested', 'root', 'warned', 'attached', 'handle', 'names', 'pending'];
|
|
25
33
|
/** The clip that plays once the model settles, when nothing asked for one before. */
|
|
26
34
|
initialClip = '';
|
|
27
35
|
/** Crossfade between clips, in seconds; 0 cuts. */
|
|
28
36
|
fade = 0.15;
|
|
29
37
|
/**
|
|
30
38
|
* Per-clip overrides by glTF clip name (authored in JSON, like
|
|
31
|
-
* AnimatedSprite.clips).
|
|
32
|
-
* settles
|
|
39
|
+
* AnimatedSprite.clips). `loop` and `speed` are read when the actions are
|
|
40
|
+
* built, as the model settles, so a change after that applies on the next
|
|
41
|
+
* build (a Model rebuild); `fade` is read at every `play`.
|
|
33
42
|
*/
|
|
34
43
|
clips = {};
|
|
44
|
+
/**
|
|
45
|
+
* 1D blends by name (`AnimatorBlend`, authored in JSON like `clips`): each
|
|
46
|
+
* is a clip `play` accepts. Its `param` and `points` are read when the
|
|
47
|
+
* actions are built, as the model settles; its `smoothing` every step.
|
|
48
|
+
*/
|
|
49
|
+
blends = {};
|
|
35
50
|
/** The clip playing now; undefined before the first play. */
|
|
36
51
|
current;
|
|
37
52
|
mixer = null;
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
/** What `play` accepts, by name, for the current build: the file's clips, then the blends. */
|
|
54
|
+
sources = new Map();
|
|
55
|
+
/** The fades between them. */
|
|
56
|
+
mix = new AnimatorMix();
|
|
57
|
+
/** The named parameters the blends read; they outlive a rebuild. */
|
|
58
|
+
params = new AnimatorParams();
|
|
59
|
+
/** Each blend the current build made playable, by name, and the parameter it reads; null before a build. */
|
|
60
|
+
built = null;
|
|
40
61
|
/** A play asked for before the model loaded. */
|
|
41
62
|
requested;
|
|
42
|
-
finished = false;
|
|
43
63
|
root = null;
|
|
44
64
|
warned = new Set();
|
|
45
65
|
attached = false;
|
|
@@ -53,7 +73,7 @@ export class Animator extends Component {
|
|
|
53
73
|
return this.names;
|
|
54
74
|
}
|
|
55
75
|
get clipFinished() {
|
|
56
|
-
return this.finished;
|
|
76
|
+
return this.mix.target?.finished ?? false;
|
|
57
77
|
}
|
|
58
78
|
get clipsPending() {
|
|
59
79
|
return this.pending;
|
|
@@ -64,11 +84,7 @@ export class Animator extends Component {
|
|
|
64
84
|
* loop starts over at 0. 0 while nothing plays.
|
|
65
85
|
*/
|
|
66
86
|
get clipProgress() {
|
|
67
|
-
|
|
68
|
-
if (!action)
|
|
69
|
-
return 0;
|
|
70
|
-
const { duration } = action.getClip();
|
|
71
|
-
return duration > 0 ? Math.min(action.time / duration, 1) : 1;
|
|
87
|
+
return this.mix.target?.progress ?? 0;
|
|
72
88
|
}
|
|
73
89
|
onReady() {
|
|
74
90
|
if (this.game.space !== '3d')
|
|
@@ -76,7 +92,11 @@ export class Animator extends Component {
|
|
|
76
92
|
this.pending = true;
|
|
77
93
|
this.attach();
|
|
78
94
|
}
|
|
79
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Plays a clip: a no-op for the running one, a restart for a finished one,
|
|
97
|
+
* and from any other a fade over the clip's own `clips[name].fade`, else
|
|
98
|
+
* `fade` seconds, every sounding clip leaving from the weight it has (ADR 0033).
|
|
99
|
+
*/
|
|
80
100
|
play(name) {
|
|
81
101
|
if (this.game.space !== '3d')
|
|
82
102
|
return;
|
|
@@ -84,50 +104,137 @@ export class Animator extends Component {
|
|
|
84
104
|
this.requested = name;
|
|
85
105
|
return;
|
|
86
106
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
107
|
+
const source = this.sources.get(name);
|
|
108
|
+
if (!source) {
|
|
89
109
|
this.warnOnce(name, `[waica] "${this.entity.name}": no clip "${name}"`);
|
|
90
110
|
return;
|
|
91
111
|
}
|
|
92
|
-
if (
|
|
112
|
+
if (source === this.mix.target && !source.finished)
|
|
93
113
|
return;
|
|
94
|
-
const previous = this.active;
|
|
95
|
-
this.active = action;
|
|
96
114
|
this.current = name;
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
this.mix.enter(source, this.fadeInto(name));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Aims a named parameter at `value`; the blends reading it follow, smoothed
|
|
119
|
+
* by the first blend that names it (once built, the first one the build made
|
|
120
|
+
* playable). Kept when made before the model loads;
|
|
121
|
+
* a value that is not a finite number is ignored with one warning.
|
|
122
|
+
*/
|
|
123
|
+
setParam(name, value) {
|
|
124
|
+
if (this.game.space !== '3d')
|
|
125
|
+
return;
|
|
126
|
+
if (!Number.isFinite(value)) {
|
|
127
|
+
this.warnOnce(`param:${name}`, `[waica] "${this.entity.name}": setParam("${name}") needs a finite number; ignored`);
|
|
100
128
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
previous.crossFadeTo(action, this.fade, false);
|
|
129
|
+
}
|
|
130
|
+
this.params.set(name, value, this.smoothingOf(name));
|
|
105
131
|
}
|
|
106
132
|
onUpdate(dt) {
|
|
107
133
|
if (this.game.space !== '3d')
|
|
108
134
|
return;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
this.syncModel();
|
|
136
|
+
this.advance(dt);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* One step of the parameters, the fades and the mixer while the Game does
|
|
140
|
+
* not simulate (issue #205): the editor's edit-mode preview of a blend
|
|
141
|
+
* drives the selected entity's Animator alone through it, once per frame,
|
|
142
|
+
* following its Model as onUpdate does. In Play it does nothing: onUpdate
|
|
143
|
+
* steps the Animator then.
|
|
144
|
+
*/
|
|
145
|
+
previewStep(dt) {
|
|
146
|
+
if (this.game.simulate || this.game.space !== '3d')
|
|
147
|
+
return;
|
|
148
|
+
this.syncModel();
|
|
149
|
+
this.advance(dt);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Ends an edit-mode preview (issue #205) as a freshly built stage starts:
|
|
153
|
+
* no parameter written, every clip stopped (three puts the rig back in the
|
|
154
|
+
* pose its file was built in) and `initialClip`, if any, asked for again
|
|
155
|
+
* from its start. In Play, or before the model loads, it does nothing.
|
|
156
|
+
*/
|
|
157
|
+
previewReset() {
|
|
158
|
+
if (this.game.simulate || this.game.space !== '3d' || !this.mixer)
|
|
159
|
+
return;
|
|
160
|
+
this.params.clear();
|
|
161
|
+
this.mix.clear();
|
|
162
|
+
this.current = undefined;
|
|
163
|
+
if (this.initialClip)
|
|
164
|
+
this.play(this.initialClip);
|
|
117
165
|
}
|
|
118
166
|
onDestroy() {
|
|
119
167
|
this.release();
|
|
120
168
|
this.pending = false;
|
|
121
169
|
}
|
|
122
170
|
inspectState() {
|
|
171
|
+
const target = this.mix.target;
|
|
123
172
|
return {
|
|
124
173
|
loaded: this.mixer !== null,
|
|
125
174
|
current: this.current ?? null,
|
|
126
|
-
time:
|
|
127
|
-
playing:
|
|
175
|
+
time: target?.time ?? 0,
|
|
176
|
+
playing: target?.playing ?? false,
|
|
128
177
|
poseHash: this.root ? poseHash(this.root) : '',
|
|
178
|
+
weights: this.mix.weights(),
|
|
179
|
+
params: this.params.snapshot(this.blendParams()),
|
|
129
180
|
};
|
|
130
181
|
}
|
|
182
|
+
/** One step of the parameters, then of the fades, then of the mixer. */
|
|
183
|
+
advance(dt) {
|
|
184
|
+
this.params.step(dt, (name) => this.smoothingOf(name));
|
|
185
|
+
if (!this.mixer)
|
|
186
|
+
return;
|
|
187
|
+
this.mix.step(dt);
|
|
188
|
+
this.mixer.update(dt);
|
|
189
|
+
}
|
|
190
|
+
/** The smoothing of the first blend that reads `param`; none without one. */
|
|
191
|
+
smoothingOf(param) {
|
|
192
|
+
const reader = this.blendReaders().find(([, reads]) => reads === param);
|
|
193
|
+
return reader === undefined ? 0 : blendSmoothing(this.authoredBlends()[reader[0]]);
|
|
194
|
+
}
|
|
195
|
+
/** The parameters the blends read, in declaration order. */
|
|
196
|
+
blendParams() {
|
|
197
|
+
return [...new Set(this.blendReaders().map(([, param]) => param))];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Each blend's name and the parameter it reads: once built, only the blends
|
|
201
|
+
* the build made playable (one it dropped decides nothing); before, every
|
|
202
|
+
* authored blend that names a parameter.
|
|
203
|
+
*/
|
|
204
|
+
blendReaders() {
|
|
205
|
+
return this.built ? [...this.built] : blendReaders(this.authoredBlends());
|
|
206
|
+
}
|
|
207
|
+
/** The authored blends by name; a `blends` that is not an object (JSON props arrive unchecked) warns once and counts as none. */
|
|
208
|
+
authoredBlends() {
|
|
209
|
+
const blends = blendsRecord(this.blends);
|
|
210
|
+
if (blends)
|
|
211
|
+
return blends;
|
|
212
|
+
this.warnOnce('blends', `[waica] "${this.entity.name}": blends must be an object of blends by name; none plays`);
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
/** The fade into `name`: its own `clips[name].fade` when that is a finite number ≥ 0, else `fade`. */
|
|
216
|
+
fadeInto(name) {
|
|
217
|
+
const own = this.clips[name]?.fade;
|
|
218
|
+
if (own === undefined)
|
|
219
|
+
return this.fade;
|
|
220
|
+
if (typeof own === 'number' && Number.isFinite(own) && own >= 0)
|
|
221
|
+
return own;
|
|
222
|
+
this.warnOnce(`fade:${name}`, `[waica] "${this.entity.name}": clips.${name}.fade must be a number of seconds ≥ 0; fading over ${this.fade}`);
|
|
223
|
+
return this.fade;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Keeps the mixer on the Model's current clone. A rebuilt Model (a new src
|
|
227
|
+
* or shape) draws a new clone: the mixer over the old one would animate a
|
|
228
|
+
* graph nobody draws, so it is torn down and the clip it was playing is
|
|
229
|
+
* asked for again on the new one. A Model listed after the Animator, which
|
|
230
|
+
* onReady could not see, is attached here.
|
|
231
|
+
*/
|
|
232
|
+
syncModel() {
|
|
233
|
+
if (this.attached && (this.entity.get(Model)?.handle ?? null) !== this.handle)
|
|
234
|
+
this.detach();
|
|
235
|
+
if (!this.attached)
|
|
236
|
+
this.attach(true);
|
|
237
|
+
}
|
|
131
238
|
/**
|
|
132
239
|
* Takes the sibling Model's handle and builds the mixer when it settles. A
|
|
133
240
|
* Model later in the component order is not there at onReady, so the first
|
|
@@ -162,14 +269,14 @@ export class Animator extends Component {
|
|
|
162
269
|
}
|
|
163
270
|
/** Stops and drops the mixer, its actions and the clone they ran over. */
|
|
164
271
|
release() {
|
|
272
|
+
this.mix.clear();
|
|
165
273
|
this.mixer?.stopAllAction();
|
|
166
274
|
if (this.mixer && this.root)
|
|
167
275
|
this.mixer.uncacheRoot(this.root);
|
|
168
276
|
this.mixer = null;
|
|
169
|
-
this.active = null;
|
|
170
277
|
this.current = undefined;
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
278
|
+
this.sources.clear();
|
|
279
|
+
this.built = null;
|
|
173
280
|
this.names = [];
|
|
174
281
|
this.root = null;
|
|
175
282
|
this.handle = null;
|
|
@@ -179,13 +286,22 @@ export class Animator extends Component {
|
|
|
179
286
|
this.mixer = mixer;
|
|
180
287
|
this.root = handle.root;
|
|
181
288
|
for (const clip of handle.animations)
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
289
|
+
this.sources.set(clip.name, new ClipSource(clip.name, this.configure(mixer.clipAction(clip), clip.name)));
|
|
290
|
+
const authored = this.authoredBlends();
|
|
291
|
+
const blends = buildBlendSources({
|
|
292
|
+
blends: authored,
|
|
293
|
+
clips: this.clips,
|
|
294
|
+
mixer,
|
|
295
|
+
animations: handle.animations,
|
|
296
|
+
value: (param) => this.params.value(param),
|
|
297
|
+
warn: (key, message) => this.warnOnce(key, `[waica] "${this.entity.name}": ${message}`),
|
|
188
298
|
});
|
|
299
|
+
for (const blend of blends)
|
|
300
|
+
this.sources.set(blend.name, blend);
|
|
301
|
+
const playable = new Set(blends.map((blend) => blend.name));
|
|
302
|
+
this.built = new Map(blendReaders(authored).filter(([name]) => playable.has(name)));
|
|
303
|
+
this.names = [...this.sources.keys()];
|
|
304
|
+
this.pending = false;
|
|
189
305
|
const first = this.requested ?? this.initialClip;
|
|
190
306
|
this.requested = undefined;
|
|
191
307
|
if (first)
|
|
@@ -34,6 +34,12 @@ export declare class Model extends Component {
|
|
|
34
34
|
min: number;
|
|
35
35
|
step: number;
|
|
36
36
|
};
|
|
37
|
+
castShadows: {
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
40
|
+
receiveShadows: {
|
|
41
|
+
label: string;
|
|
42
|
+
};
|
|
37
43
|
};
|
|
38
44
|
static transient: string[];
|
|
39
45
|
/** What this Model draws, under the entity's node; null until it is ready. The Pointer ray-casts against it. */
|
|
@@ -49,6 +55,8 @@ export declare class Model extends Component {
|
|
|
49
55
|
private _shape;
|
|
50
56
|
private _color;
|
|
51
57
|
private _size;
|
|
58
|
+
private _castShadows;
|
|
59
|
+
private _receiveShadows;
|
|
52
60
|
private _primitive;
|
|
53
61
|
/** A `waica:` uri or project path to a `.glb` / `.gltf`; empty draws the primitive `shape`. */
|
|
54
62
|
get src(): string;
|
|
@@ -61,11 +69,19 @@ export declare class Model extends Component {
|
|
|
61
69
|
/** Uniform scale of a primitive: the box side, sphere diameter or plane side in world units. */
|
|
62
70
|
get size(): number;
|
|
63
71
|
set size(value: number);
|
|
72
|
+
/** Whether this Model's meshes cast shadows; only false opts out. */
|
|
73
|
+
get castShadows(): boolean;
|
|
74
|
+
set castShadows(value: unknown);
|
|
75
|
+
/** Whether this Model's meshes receive shadows; only false opts out. */
|
|
76
|
+
get receiveShadows(): boolean;
|
|
77
|
+
set receiveShadows(value: unknown);
|
|
64
78
|
onReady(): void;
|
|
65
79
|
onDestroy(): void;
|
|
66
80
|
private build;
|
|
67
81
|
private buildPrimitive;
|
|
68
82
|
private rebuild;
|
|
69
|
-
/**
|
|
83
|
+
/** Participation belongs to this Model's meshes, never their shared glTF resources. */
|
|
84
|
+
private applyShadowFlags;
|
|
85
|
+
/** Retire clone-local render state; shared glTF geometry/materials stay cache-owned. */
|
|
70
86
|
private clear;
|
|
71
87
|
}
|