@drawcall/acta 0.0.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.
- package/dist/animations/Jog_Bwd.d.ts +2 -0
- package/dist/animations/Jog_Bwd.js +1 -0
- package/dist/animations/Jog_Bwd_L.d.ts +2 -0
- package/dist/animations/Jog_Bwd_L.js +1 -0
- package/dist/animations/Jog_Bwd_R.d.ts +2 -0
- package/dist/animations/Jog_Bwd_R.js +1 -0
- package/dist/animations/Jog_Fwd.d.ts +2 -0
- package/dist/animations/Jog_Fwd.js +1 -0
- package/dist/animations/Jog_Fwd_L.d.ts +2 -0
- package/dist/animations/Jog_Fwd_L.js +1 -0
- package/dist/animations/Jog_Fwd_R.d.ts +2 -0
- package/dist/animations/Jog_Fwd_R.js +1 -0
- package/dist/animations/Jog_Left.d.ts +2 -0
- package/dist/animations/Jog_Left.js +1 -0
- package/dist/animations/Jog_Right.d.ts +2 -0
- package/dist/animations/Jog_Right.js +1 -0
- package/dist/animations/Pistol_Aim_Down.d.ts +2 -0
- package/dist/animations/Pistol_Aim_Down.js +1 -0
- package/dist/animations/Pistol_Aim_Neutral.d.ts +2 -0
- package/dist/animations/Pistol_Aim_Neutral.js +1 -0
- package/dist/animations/Pistol_Aim_Up.d.ts +2 -0
- package/dist/animations/Pistol_Aim_Up.js +1 -0
- package/dist/behavior.d.ts +67 -0
- package/dist/behavior.js +458 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +1 -0
- package/package.json +26 -0
package/dist/behavior.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { AdditiveAnimationBlendMode, Euler, EventDispatcher, LoopOnce, Quaternion, Vector2, Vector3, } from 'three';
|
|
2
|
+
import { action, GraphTimeline, parallel, timePassed, animationFinished, runTimeline, scope, switch_, } from '@pmndrs/timeline';
|
|
3
|
+
import { applyMask, fixModelAnimationClip, lowerBody, startAnimation, upperBody, loadCharacterAnimation, WalkAnimationUrl, RunAnimationUrl, } from '@pmndrs/viverse';
|
|
4
|
+
import { makeClipAdditive } from 'three/src/animation/AnimationUtils.js';
|
|
5
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
6
|
+
import { VRM } from '@pixiv/three-vrm';
|
|
7
|
+
import { clamp } from 'three/src/math/MathUtils.js';
|
|
8
|
+
const ZeroVector = new Vector3();
|
|
9
|
+
const ForwardVector = new Vector3(0, 0, 1);
|
|
10
|
+
const UpVector = new Vector3(0, 1, 0);
|
|
11
|
+
const SpineAimYawOffset = -0.5;
|
|
12
|
+
const quaternionHelper = new Quaternion();
|
|
13
|
+
const parentQuaternionInvHelper = new Quaternion();
|
|
14
|
+
const localQuaternionHelper = new Quaternion();
|
|
15
|
+
const eulerHelper = new Euler();
|
|
16
|
+
const directionHelper = new Vector3();
|
|
17
|
+
const vectorHelper = new Vector3();
|
|
18
|
+
//TODO: support direction AND position targets for head and aim
|
|
19
|
+
//TODO: lerp everything (spine rotation etc)
|
|
20
|
+
//TODO: jumping support
|
|
21
|
+
export class CharacterBehavior extends EventDispatcher {
|
|
22
|
+
model;
|
|
23
|
+
applyMove;
|
|
24
|
+
properties = {};
|
|
25
|
+
computedProperties = {
|
|
26
|
+
isMoving: () => this.worldMoveVelocity.length() > 0,
|
|
27
|
+
};
|
|
28
|
+
// Direction and movement properties
|
|
29
|
+
worldHeadDirection;
|
|
30
|
+
worldAimDirection;
|
|
31
|
+
worldMoveVelocity = new Vector3(0, 0, 0);
|
|
32
|
+
abortController = new AbortController();
|
|
33
|
+
updateTimeline;
|
|
34
|
+
aimUpAnimation;
|
|
35
|
+
aimNeutralAnimation;
|
|
36
|
+
aimDownAnimation;
|
|
37
|
+
walkAnimation;
|
|
38
|
+
runAnimation;
|
|
39
|
+
jogRightAnimation;
|
|
40
|
+
jogLeftAnimation;
|
|
41
|
+
jogFwdRightAnimation;
|
|
42
|
+
jogFwdAnimation;
|
|
43
|
+
jogFwdLeftAnimation;
|
|
44
|
+
jogBwdRightAnimation;
|
|
45
|
+
jogBwdAnimation;
|
|
46
|
+
jogBwdLeftAnimation;
|
|
47
|
+
animations = {};
|
|
48
|
+
audios = {};
|
|
49
|
+
gltfLoader;
|
|
50
|
+
moveStatus = 'none';
|
|
51
|
+
isAiming = false;
|
|
52
|
+
constructor(behavior, animationUrls, audioUrls, model, applyMove, loadingManager) {
|
|
53
|
+
super();
|
|
54
|
+
this.model = model;
|
|
55
|
+
this.applyMove = applyMove;
|
|
56
|
+
if (model instanceof VRM) {
|
|
57
|
+
throw new Error(`not supported yet`);
|
|
58
|
+
}
|
|
59
|
+
this.gltfLoader = new GLTFLoader(loadingManager);
|
|
60
|
+
this.init(behavior, animationUrls, audioUrls).catch(console.error);
|
|
61
|
+
}
|
|
62
|
+
async init(behavior, animationUrls, audioUrls) {
|
|
63
|
+
await Promise.all([
|
|
64
|
+
this.loadMoveAnimations(),
|
|
65
|
+
this.loadBaseAimAnimation(),
|
|
66
|
+
...Object.entries(animationUrls).map(([name, url]) => this.loadAnimation(name, url)),
|
|
67
|
+
...Object.entries(audioUrls).map(([name, url]) => this.loadAudio(name, url)),
|
|
68
|
+
]);
|
|
69
|
+
const timeline = this.convertToTimeline(behavior);
|
|
70
|
+
this.updateTimeline = runTimeline(timeline, this.abortController.signal);
|
|
71
|
+
}
|
|
72
|
+
async loadMoveAnimations() {
|
|
73
|
+
const [walkAnimation, runAnimation, jogRightAnimation, jogLeftAnimation, jogFwdRightAnimation, jogFwdAnimation, jogFwdLeftAnimation, jogBwdRightAnimation, jogBwdAnimation, jogBwdLeftAnimation,] = await Promise.all([
|
|
74
|
+
await loadCharacterAnimation(this.model, WalkAnimationUrl),
|
|
75
|
+
await loadCharacterAnimation(this.model, RunAnimationUrl),
|
|
76
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Right.js')).default, 'gltf'),
|
|
77
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Left.js')).default, 'gltf'),
|
|
78
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd_R.js')).default, 'gltf'),
|
|
79
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd.js')).default, 'gltf'),
|
|
80
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd_L.js')).default, 'gltf'),
|
|
81
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd_R.js')).default, 'gltf'),
|
|
82
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd.js')).default, 'gltf'),
|
|
83
|
+
await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd_L.js')).default, 'gltf'),
|
|
84
|
+
]);
|
|
85
|
+
this.walkAnimation = walkAnimation;
|
|
86
|
+
this.runAnimation = runAnimation;
|
|
87
|
+
//apply masks because jogging is only used for the lower body
|
|
88
|
+
applyMask(jogRightAnimation, lowerBody);
|
|
89
|
+
applyMask(jogLeftAnimation, lowerBody);
|
|
90
|
+
applyMask(jogFwdRightAnimation, lowerBody);
|
|
91
|
+
applyMask(jogFwdAnimation, lowerBody);
|
|
92
|
+
applyMask(jogFwdLeftAnimation, lowerBody);
|
|
93
|
+
applyMask(jogBwdRightAnimation, lowerBody);
|
|
94
|
+
applyMask(jogBwdAnimation, lowerBody);
|
|
95
|
+
applyMask(jogBwdLeftAnimation, lowerBody);
|
|
96
|
+
this.jogRightAnimation = jogRightAnimation;
|
|
97
|
+
this.jogLeftAnimation = jogLeftAnimation;
|
|
98
|
+
this.jogFwdRightAnimation = jogFwdRightAnimation;
|
|
99
|
+
this.jogFwdAnimation = jogFwdAnimation;
|
|
100
|
+
this.jogFwdLeftAnimation = jogFwdLeftAnimation;
|
|
101
|
+
this.jogBwdRightAnimation = jogBwdRightAnimation;
|
|
102
|
+
this.jogBwdAnimation = jogBwdAnimation;
|
|
103
|
+
this.jogBwdLeftAnimation = jogBwdLeftAnimation;
|
|
104
|
+
}
|
|
105
|
+
async loadBaseAimAnimation() {
|
|
106
|
+
//TODO: load only if necassary
|
|
107
|
+
const [aimUpAnimation, aimNeutralAnimation, aimDownAnimation] = await Promise.all([
|
|
108
|
+
loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Up.js')).default, 'gltf'),
|
|
109
|
+
loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Neutral.js')).default, 'gltf'),
|
|
110
|
+
loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Down.js')).default, 'gltf'),
|
|
111
|
+
]);
|
|
112
|
+
this.aimUpAnimation = aimUpAnimation;
|
|
113
|
+
this.aimNeutralAnimation = aimNeutralAnimation;
|
|
114
|
+
this.aimDownAnimation = aimDownAnimation;
|
|
115
|
+
}
|
|
116
|
+
async loadAnimation(name, url) {
|
|
117
|
+
const gltf = await this.gltfLoader.loadAsync(url);
|
|
118
|
+
if (gltf.animations.length != 1) {
|
|
119
|
+
throw new Error(`gltf contains has more than 1 animation`);
|
|
120
|
+
}
|
|
121
|
+
fixModelAnimationClip(this.model, gltf.animations[0], gltf.scene, false);
|
|
122
|
+
this.animations[name] = gltf.animations[0];
|
|
123
|
+
}
|
|
124
|
+
async loadAudio(name, url) { }
|
|
125
|
+
update(delta) {
|
|
126
|
+
if (this.moveStatus === 'fullbody' && this.worldMoveVelocity.lengthSq() > 0) {
|
|
127
|
+
// Signed angle from forward (0,0,1) to move direction on XZ plane
|
|
128
|
+
this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldMoveVelocity.x, this.worldMoveVelocity.z);
|
|
129
|
+
}
|
|
130
|
+
else if (this.worldAimDirection != null && this.isAiming) {
|
|
131
|
+
this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldAimDirection.x, this.worldAimDirection.z);
|
|
132
|
+
}
|
|
133
|
+
else if (this.worldHeadDirection != null) {
|
|
134
|
+
this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldHeadDirection.x, this.worldHeadDirection.z);
|
|
135
|
+
}
|
|
136
|
+
this.updateTimeline?.(undefined, delta);
|
|
137
|
+
this.model.mixer.update(delta);
|
|
138
|
+
// Stabilize spine during aiming to face the aim direction
|
|
139
|
+
if (this.isAiming) {
|
|
140
|
+
const spineBone = this.model.scene.getObjectByName('spine');
|
|
141
|
+
// Extract yaw from aim direction and add PI offset for model facing
|
|
142
|
+
const aimYaw = this.model.scene.rotation.y + Math.PI + SpineAimYawOffset;
|
|
143
|
+
// Create quaternion with just the yaw (no pitch/roll)
|
|
144
|
+
eulerHelper.set(0, aimYaw, 0, 'YXZ');
|
|
145
|
+
quaternionHelper.setFromEuler(eulerHelper);
|
|
146
|
+
// Transform to spine's local space
|
|
147
|
+
spineBone.parent.getWorldQuaternion(parentQuaternionInvHelper).invert();
|
|
148
|
+
localQuaternionHelper.copy(parentQuaternionInvHelper).multiply(quaternionHelper);
|
|
149
|
+
spineBone.quaternion.copy(localQuaternionHelper);
|
|
150
|
+
spineBone.updateMatrixWorld();
|
|
151
|
+
}
|
|
152
|
+
if (this.worldHeadDirection != null) {
|
|
153
|
+
const neckBone = this.model.scene.getObjectByName('neck');
|
|
154
|
+
// Transform worldHeadDirection to neck's parent local space
|
|
155
|
+
neckBone.parent.getWorldQuaternion(quaternionHelper).invert();
|
|
156
|
+
directionHelper.copy(this.worldHeadDirection).applyQuaternion(quaternionHelper);
|
|
157
|
+
// Compute the local rotation that aligns (0,0,1) with the local target direction
|
|
158
|
+
quaternionHelper.setFromUnitVectors(ForwardVector, directionHelper);
|
|
159
|
+
neckBone.rotation.setFromQuaternion(quaternionHelper, 'YXZ');
|
|
160
|
+
neckBone.rotation.z = 0;
|
|
161
|
+
neckBone.rotation.y = clamp(neckBone.rotation.y, -Math.PI / 2, Math.PI / 2);
|
|
162
|
+
neckBone.rotation.x = clamp(neckBone.rotation.x, -Math.PI / 2, Math.PI / 2);
|
|
163
|
+
}
|
|
164
|
+
this.applyMove?.(this.moveStatus === 'none' ? ZeroVector : this.worldMoveVelocity, delta);
|
|
165
|
+
}
|
|
166
|
+
destroy() {
|
|
167
|
+
this.abortController.abort();
|
|
168
|
+
}
|
|
169
|
+
convertToTimeline(behavior, mask) {
|
|
170
|
+
if (behavior.type === 'animation' && behavior.name === 'Move') {
|
|
171
|
+
return this.createMoveTimeline(mask);
|
|
172
|
+
}
|
|
173
|
+
switch (behavior.type) {
|
|
174
|
+
case 'animation':
|
|
175
|
+
return this.createAnimationTimeline(behavior.name, mask);
|
|
176
|
+
case 'audio':
|
|
177
|
+
throw new Error('not implemented');
|
|
178
|
+
case 'graph':
|
|
179
|
+
return this.createGraphTimeline(behavior, mask);
|
|
180
|
+
case 'loop':
|
|
181
|
+
return this.createLoopTimeline(behavior.behavior, mask);
|
|
182
|
+
case 'parallel':
|
|
183
|
+
return () => parallel('race', ...behavior.behaviors.map((b) => this.convertToTimeline(b, mask)));
|
|
184
|
+
case 'sequential':
|
|
185
|
+
return this.createSequentialTimeline(behavior.behaviors, mask);
|
|
186
|
+
case 'split':
|
|
187
|
+
return () => parallel('race', this.convertToTimeline(behavior.lowerBodyBehavior, 'lowerbody'), this.convertToTimeline(behavior.upperBodyBehavior, 'upperbody'));
|
|
188
|
+
case 'wait':
|
|
189
|
+
return () => action({ until: timePassed(behavior.seconds, 'seconds') });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
createMoveTimeline(mask) {
|
|
193
|
+
const layer = computeAnimationLayers(mask);
|
|
194
|
+
if (mask == null) {
|
|
195
|
+
//fullbody (just animate between walk and run)
|
|
196
|
+
const walkAction = this.model.mixer.clipAction(this.walkAnimation);
|
|
197
|
+
const runAction = this.model.mixer.clipAction(this.runAnimation);
|
|
198
|
+
const self = this;
|
|
199
|
+
const vectorHelper = new Vector2();
|
|
200
|
+
return () => parallel('all', action({
|
|
201
|
+
update: () => {
|
|
202
|
+
vectorHelper.set(this.worldMoveVelocity.x, -this.worldMoveVelocity.z);
|
|
203
|
+
const speed = vectorHelper.length();
|
|
204
|
+
walkAction.timeScale = 0.667 * speed;
|
|
205
|
+
runAction.timeScale = 0.208 * speed;
|
|
206
|
+
},
|
|
207
|
+
}), scope(async function* (abortSignal) {
|
|
208
|
+
self.moveStatus = 'fullbody';
|
|
209
|
+
self.dispatchEvent({ type: 'animationStarted', name: 'Move' });
|
|
210
|
+
abortSignal.addEventListener('abort', () => {
|
|
211
|
+
self.moveStatus = 'none';
|
|
212
|
+
self.dispatchEvent({ type: 'animationFinished', name: 'Move' });
|
|
213
|
+
}, { once: true });
|
|
214
|
+
yield* switch_([
|
|
215
|
+
{
|
|
216
|
+
timeline: () => action({ init: () => startAnimation(walkAction, self.model.currentAnimations, { layer }) }),
|
|
217
|
+
condition: () => vectorHelper.length() < 4.5,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
timeline: () => action({ init: () => startAnimation(runAction, self.model.currentAnimations, { layer }) }),
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
//only lower body (computing angle between world torso direction and world velocity)
|
|
226
|
+
const jogRightAction = this.model.mixer.clipAction(this.jogRightAnimation);
|
|
227
|
+
const jogLeftAction = this.model.mixer.clipAction(this.jogLeftAnimation);
|
|
228
|
+
const jogFwdRightAction = this.model.mixer.clipAction(this.jogFwdRightAnimation);
|
|
229
|
+
const jogFwdAction = this.model.mixer.clipAction(this.jogFwdAnimation);
|
|
230
|
+
const jogFwdLeftAction = this.model.mixer.clipAction(this.jogFwdLeftAnimation);
|
|
231
|
+
const jogBwdRightAction = this.model.mixer.clipAction(this.jogBwdRightAnimation);
|
|
232
|
+
const jogBwdAction = this.model.mixer.clipAction(this.jogBwdAnimation);
|
|
233
|
+
const jogBwdLeftAction = this.model.mixer.clipAction(this.jogBwdLeftAnimation);
|
|
234
|
+
const normalizedLocalMoveDirection = new Vector2();
|
|
235
|
+
const self = this;
|
|
236
|
+
return () => parallel('all', action({
|
|
237
|
+
update: () => {
|
|
238
|
+
// Transform world velocity to model's local space by rotating by inverse of model's Y rotation
|
|
239
|
+
vectorHelper.copy(this.worldMoveVelocity).applyAxisAngle(UpVector, -this.model.scene.rotation.y);
|
|
240
|
+
const speed = normalizedLocalMoveDirection.set(vectorHelper.x, -vectorHelper.z).length();
|
|
241
|
+
normalizedLocalMoveDirection.divideScalar(speed);
|
|
242
|
+
jogFwdAction.timeScale = 0.222 * speed;
|
|
243
|
+
jogFwdRightAction.timeScale = 0.222 * speed;
|
|
244
|
+
jogRightAction.timeScale = 0.37 * speed;
|
|
245
|
+
jogBwdRightAction.timeScale = 0.256 * speed;
|
|
246
|
+
jogBwdAction.timeScale = 0.238 * speed;
|
|
247
|
+
jogBwdLeftAction.timeScale = 0.256 * speed;
|
|
248
|
+
jogLeftAction.timeScale = 0.37 * speed;
|
|
249
|
+
jogFwdLeftAction.timeScale = 0.222 * speed;
|
|
250
|
+
},
|
|
251
|
+
}), scope(async function* (abortSignal) {
|
|
252
|
+
self.moveStatus = 'lowerbody';
|
|
253
|
+
self.dispatchEvent({ type: 'animationStarted', name: 'Move' });
|
|
254
|
+
abortSignal.addEventListener('abort', () => {
|
|
255
|
+
self.moveStatus = 'none';
|
|
256
|
+
self.dispatchEvent({ type: 'animationFinished', name: 'Move' });
|
|
257
|
+
}, { once: true });
|
|
258
|
+
yield* switch_([
|
|
259
|
+
{
|
|
260
|
+
timeline: () => action({
|
|
261
|
+
init: () => startAnimation(jogFwdAction, self.model.currentAnimations, { layer, sync: true }),
|
|
262
|
+
}),
|
|
263
|
+
condition: () => Math.abs(normalizedLocalMoveDirection.x) < 0.5 && normalizedLocalMoveDirection.y > 0.5,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
timeline: () => action({
|
|
267
|
+
init: () => startAnimation(jogFwdRightAction, self.model.currentAnimations, { layer, sync: true }),
|
|
268
|
+
}),
|
|
269
|
+
condition: () => normalizedLocalMoveDirection.x > 0.5 && normalizedLocalMoveDirection.y > 0.5,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
timeline: () => action({
|
|
273
|
+
init: () => startAnimation(jogRightAction, self.model.currentAnimations, { layer, sync: true }),
|
|
274
|
+
}),
|
|
275
|
+
condition: () => normalizedLocalMoveDirection.x > 0.5 && Math.abs(normalizedLocalMoveDirection.y) < 0.5,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
timeline: () => action({
|
|
279
|
+
init: () => startAnimation(jogBwdRightAction, self.model.currentAnimations, { layer, sync: true }),
|
|
280
|
+
}),
|
|
281
|
+
condition: () => normalizedLocalMoveDirection.x > 0.5 && normalizedLocalMoveDirection.y < -0.5,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
timeline: () => action({
|
|
285
|
+
init: () => startAnimation(jogBwdAction, self.model.currentAnimations, { layer, sync: true }),
|
|
286
|
+
}),
|
|
287
|
+
condition: () => Math.abs(normalizedLocalMoveDirection.x) < 0.5 && normalizedLocalMoveDirection.y < -0.5,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
timeline: () => action({
|
|
291
|
+
init: () => startAnimation(jogBwdLeftAction, self.model.currentAnimations, { layer, sync: true }),
|
|
292
|
+
}),
|
|
293
|
+
condition: () => normalizedLocalMoveDirection.x < -0.5 && normalizedLocalMoveDirection.y < -0.5,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
timeline: () => action({
|
|
297
|
+
init: () => startAnimation(jogLeftAction, self.model.currentAnimations, { layer, sync: true }),
|
|
298
|
+
}),
|
|
299
|
+
condition: () => normalizedLocalMoveDirection.x < -0.5 && Math.abs(normalizedLocalMoveDirection.y) < 0.5,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
timeline: () => action({
|
|
303
|
+
init: () => startAnimation(jogFwdLeftAction, self.model.currentAnimations, { layer, sync: true }),
|
|
304
|
+
}),
|
|
305
|
+
},
|
|
306
|
+
]);
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
createAnimationTimeline(name, mask) {
|
|
310
|
+
let animationClip = this.animations[name];
|
|
311
|
+
if (mask != null) {
|
|
312
|
+
animationClip = animationClip.clone();
|
|
313
|
+
applyMask(animationClip, mask === 'lowerbody' ? lowerBody : upperBody);
|
|
314
|
+
}
|
|
315
|
+
if (name.includes('Pistol_')) {
|
|
316
|
+
return this.createAdditiveAimingTimeline(animationClip, mask, name);
|
|
317
|
+
}
|
|
318
|
+
return this.createStandardAnimationTimeline(animationClip, mask, name);
|
|
319
|
+
}
|
|
320
|
+
createStandardAnimationTimeline(clip, mask, name) {
|
|
321
|
+
const animationAction = this.model.mixer.clipAction(clip);
|
|
322
|
+
animationAction.loop = LoopOnce;
|
|
323
|
+
animationAction.clampWhenFinished = true;
|
|
324
|
+
const layer = computeAnimationLayers(mask);
|
|
325
|
+
const self = this;
|
|
326
|
+
return async function* () {
|
|
327
|
+
if (name)
|
|
328
|
+
self.dispatchEvent({ type: 'animationStarted', name });
|
|
329
|
+
try {
|
|
330
|
+
yield* action({
|
|
331
|
+
init: () => startAnimation(animationAction, self.model.currentAnimations, { layer }),
|
|
332
|
+
until: animationFinished(animationAction),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
if (name)
|
|
337
|
+
self.dispatchEvent({ type: 'animationFinished', name });
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
createAdditiveAimingTimeline(clip, mask, name) {
|
|
342
|
+
let aimUpAnimation = this.aimUpAnimation;
|
|
343
|
+
let aimNeutralAnimation = this.aimNeutralAnimation;
|
|
344
|
+
let aimDownAnimation = this.aimDownAnimation;
|
|
345
|
+
if (mask != null) {
|
|
346
|
+
aimUpAnimation = aimUpAnimation.clone();
|
|
347
|
+
aimNeutralAnimation = aimNeutralAnimation.clone();
|
|
348
|
+
aimDownAnimation = aimDownAnimation.clone();
|
|
349
|
+
const maskFn = mask === 'lowerbody' ? lowerBody : upperBody;
|
|
350
|
+
applyMask(aimUpAnimation, maskFn);
|
|
351
|
+
applyMask(aimNeutralAnimation, maskFn);
|
|
352
|
+
applyMask(aimDownAnimation, maskFn);
|
|
353
|
+
}
|
|
354
|
+
clip = clip.clone();
|
|
355
|
+
makeClipAdditive(clip, undefined, aimNeutralAnimation);
|
|
356
|
+
const additiveAction = this.model.mixer.clipAction(clip);
|
|
357
|
+
additiveAction.loop = LoopOnce;
|
|
358
|
+
additiveAction.blendMode = AdditiveAnimationBlendMode;
|
|
359
|
+
additiveAction.clampWhenFinished = true;
|
|
360
|
+
const aimUpAcion = this.model.mixer.clipAction(aimUpAnimation);
|
|
361
|
+
const aimNeutralAction = this.model.mixer.clipAction(aimNeutralAnimation);
|
|
362
|
+
const aimDownAction = this.model.mixer.clipAction(aimDownAnimation);
|
|
363
|
+
const layer = computeAnimationLayers(mask, false);
|
|
364
|
+
const self = this;
|
|
365
|
+
return async function* () {
|
|
366
|
+
if (name)
|
|
367
|
+
self.dispatchEvent({ type: 'animationStarted', name });
|
|
368
|
+
self.isAiming = true;
|
|
369
|
+
try {
|
|
370
|
+
yield* action({
|
|
371
|
+
init: () => {
|
|
372
|
+
startAnimation(aimUpAcion, self.model.currentAnimations, { layer: 'aim-up' });
|
|
373
|
+
startAnimation(aimNeutralAction, self.model.currentAnimations, { layer });
|
|
374
|
+
startAnimation(aimDownAction, self.model.currentAnimations, { layer: 'adim-down' });
|
|
375
|
+
startAnimation(additiveAction, self.model.currentAnimations, { layer: 'aim' });
|
|
376
|
+
},
|
|
377
|
+
update: () => {
|
|
378
|
+
const pitch = self.worldAimDirection ? -Math.asin(self.worldAimDirection.y) : 0;
|
|
379
|
+
if (pitch <= 0) {
|
|
380
|
+
// looking up
|
|
381
|
+
aimUpAcion.weight = Math.min(1, Math.max(0, -pitch / (Math.PI / 2)));
|
|
382
|
+
aimNeutralAction.weight = 1 - aimUpAcion.weight;
|
|
383
|
+
aimDownAction.weight = 0;
|
|
384
|
+
}
|
|
385
|
+
else if (pitch > 0) {
|
|
386
|
+
// looking down
|
|
387
|
+
aimDownAction.weight = Math.min(1, Math.max(0, pitch / (Math.PI / 2)));
|
|
388
|
+
aimNeutralAction.weight = 1 - aimDownAction.weight;
|
|
389
|
+
aimUpAcion.weight = 0;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
until: animationFinished(additiveAction),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
self.isAiming = false;
|
|
397
|
+
if (name)
|
|
398
|
+
self.dispatchEvent({ type: 'animationFinished', name });
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
createGraphTimeline(behavior, mask) {
|
|
403
|
+
const graph = new GraphTimeline(behavior.enterState, undefined, behavior.exitState, undefined, true);
|
|
404
|
+
for (const name in behavior.states) {
|
|
405
|
+
const state = behavior.states[name];
|
|
406
|
+
const transitionsTo = {};
|
|
407
|
+
if (state.finally != null) {
|
|
408
|
+
transitionsTo.finally = state.finally;
|
|
409
|
+
}
|
|
410
|
+
for (const targetState in state.transitionsTo) {
|
|
411
|
+
const condition = state.transitionsTo[targetState];
|
|
412
|
+
transitionsTo[targetState] = {
|
|
413
|
+
whenUpdate: () => evaluateCondition(condition, this.properties, this.computedProperties),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
graph.attach(name, this.convertToTimeline(state.behavior, mask), transitionsTo);
|
|
417
|
+
}
|
|
418
|
+
return () => graph.run();
|
|
419
|
+
}
|
|
420
|
+
createLoopTimeline(behavior, mask) {
|
|
421
|
+
const subTimeline = this.convertToTimeline(behavior, mask);
|
|
422
|
+
return async function* () {
|
|
423
|
+
while (true) {
|
|
424
|
+
yield* subTimeline();
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
createSequentialTimeline(behaviors, mask) {
|
|
429
|
+
const subTimelines = behaviors.map((b) => this.convertToTimeline(b, mask));
|
|
430
|
+
return async function* () {
|
|
431
|
+
for (const timeline of subTimelines) {
|
|
432
|
+
yield* timeline();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function computeAnimationLayers(mask, includeAim = true) {
|
|
438
|
+
const result = [];
|
|
439
|
+
if (mask !== 'lowerbody')
|
|
440
|
+
result.push('upperbody');
|
|
441
|
+
if (mask !== 'lowerbody' && includeAim)
|
|
442
|
+
result.push('aim', 'aim-up', 'aim-down');
|
|
443
|
+
if (mask !== 'upperbody')
|
|
444
|
+
result.push('lowerbody');
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
function evaluateCondition(condition, properties, computedProperties) {
|
|
448
|
+
switch (condition.type) {
|
|
449
|
+
case 'isTrue':
|
|
450
|
+
return computedProperties[condition.property]?.() ?? properties[condition.property] ?? false;
|
|
451
|
+
case 'and':
|
|
452
|
+
return condition.conditions.every((c) => evaluateCondition(c, properties, computedProperties));
|
|
453
|
+
case 'or':
|
|
454
|
+
return condition.conditions.some((c) => evaluateCondition(c, properties, computedProperties));
|
|
455
|
+
case 'not':
|
|
456
|
+
return !evaluateCondition(condition.condition, properties, computedProperties);
|
|
457
|
+
}
|
|
458
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type Condition = {
|
|
2
|
+
type: 'isTrue';
|
|
3
|
+
property: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'not';
|
|
6
|
+
condition: Condition;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'or';
|
|
9
|
+
conditions: Condition[];
|
|
10
|
+
} | {
|
|
11
|
+
type: 'and';
|
|
12
|
+
conditions: Condition[];
|
|
13
|
+
};
|
|
14
|
+
export type Behavior = {
|
|
15
|
+
type: 'animation';
|
|
16
|
+
name: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'audio';
|
|
19
|
+
name: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'wait';
|
|
22
|
+
seconds: number;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'split';
|
|
25
|
+
upperBodyBehavior: Behavior;
|
|
26
|
+
lowerBodyBehavior: Behavior;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'sequential';
|
|
29
|
+
behaviors: Behavior[];
|
|
30
|
+
} | {
|
|
31
|
+
type: 'parallel';
|
|
32
|
+
behaviors: Behavior[];
|
|
33
|
+
} | {
|
|
34
|
+
type: 'loop';
|
|
35
|
+
behavior: Behavior;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'graph';
|
|
38
|
+
enterState: string;
|
|
39
|
+
exitState?: string;
|
|
40
|
+
states: Record<string, {
|
|
41
|
+
behavior: Behavior;
|
|
42
|
+
/**
|
|
43
|
+
* name of the state to transition to when the behavior finishes.
|
|
44
|
+
*/
|
|
45
|
+
finally?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Transitions are evaluated continuously during the state's behavior execution.
|
|
48
|
+
* If a condition is met, the current behavior is cancelled immediately and the transition occurs.
|
|
49
|
+
*/
|
|
50
|
+
transitionsTo?: Record<string, Condition>;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drawcall/acta",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"author": "Bela Bohlender",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"homepage": "https://drawcall.ai",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"drawcall.ai"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@pixiv/three-vrm": "3.4.4",
|
|
17
|
+
"@pmndrs/timeline": "^0.3.7",
|
|
18
|
+
"@pmndrs/viverse": "^0.2.10"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"three": "*"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc"
|
|
25
|
+
}
|
|
26
|
+
}
|