@genex-ai/cli-demo 0.87.0-dev.218 → 0.90.0-dev.227
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/index.js +1664 -45
- package/package.json +1 -1
- package/templates/motion/anim-runtime.js +265 -0
- package/templates/motion/ik.js +75 -0
- package/templates/motion/motion.config.json +11 -0
- package/templates/motion/rigs.js +563 -0
- package/templates/motion/sets/jumps.json +1 -0
- package/templates/motion/sets/rifle.json +1 -0
- package/templates/skills/genex-ai-character/SKILL.md +24 -0
- package/templates/skills/genex-ai-character/references/motion.md +145 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +4 -0
- package/templates/skills/genex-threejs-creatures/SKILL.md +5 -0
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0-dev.227",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// anim-runtime.js — compiled-clip playback for the AG-803 scenario pages.
|
|
2
|
+
// Precomputes rig-corrected local quats for every clip (same precompute as
|
|
3
|
+
// loco.html), then layers at runtime:
|
|
4
|
+
// - a LOOP layer: weighted progressive-slerp blend of looping clips, one
|
|
5
|
+
// shared phase for the directional gaits (they're phase-aligned at compile
|
|
6
|
+
// time) + an independent phase per free-running loop (idle/stance/push);
|
|
7
|
+
// - a ONE-SHOT layer: a non-looping clip crossfaded over the loop pose with
|
|
8
|
+
// a smooth envelope; its hips-Y can be capped when the physics capsule owns
|
|
9
|
+
// the jump (otherwise capsule + clip would both rise).
|
|
10
|
+
// The page owns world transform: hips are driven with the clip's LOCAL values
|
|
11
|
+
// only (residual x/z + absolute y), and the character root carries position/yaw.
|
|
12
|
+
import * as THREE from "three";
|
|
13
|
+
|
|
14
|
+
export class ClipSet {
|
|
15
|
+
/** data = compiled json; clips = data.gaits or data.clips */
|
|
16
|
+
constructor(rig, data, clips) {
|
|
17
|
+
this.rig = rig;
|
|
18
|
+
this.base = { joints: data.joints, parents: data.parents,
|
|
19
|
+
restPositions: data.restPositions, hipsRestY: data.hipsRestY };
|
|
20
|
+
this.tracks = {};
|
|
21
|
+
for (const [name, c] of Object.entries(clips)) {
|
|
22
|
+
const F = c.frames;
|
|
23
|
+
const t = { F, fps: c.fps, dur: F / c.fps, speed: c.speed, dir: c.dir,
|
|
24
|
+
loop: c.loop !== false, quat: {}, hips: new Float32Array(F * 3),
|
|
25
|
+
contacts: c.contacts };
|
|
26
|
+
for (const j of rig.mapped) t.quat[j] = new Float32Array(F * 4);
|
|
27
|
+
for (let f = 0; f < F; f++) {
|
|
28
|
+
rig.retargetFrame((k, out) => out.fromArray(c.localQuat[f][k]), true,
|
|
29
|
+
(j, q) => q.toArray(t.quat[j], f * 4));
|
|
30
|
+
for (let k = 0; k < 3; k++) t.hips[f * 3 + k] = c.hipsPos[f][k];
|
|
31
|
+
}
|
|
32
|
+
this.tracks[name] = t;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Upper-body mask (layered blend per bone): feathered weights up the spine so
|
|
38
|
+
// the locomotion's weight-shift still leaks into the lower torso while the aim
|
|
39
|
+
// pose owns the chest, arms and head. Hips + legs stay 0 — the gait owns them.
|
|
40
|
+
const MASK_FEATHER = { Spine: 0.3, Spine1: 0.55, Spine2: 0.8, Spine3: 1, Neck: 1, Head: 0.85 };
|
|
41
|
+
function maskFeather(name) {
|
|
42
|
+
if (name in MASK_FEATHER) return MASK_FEATHER[name];
|
|
43
|
+
return /^(Right|Left)(Shoulder|Arm|ForeArm|Hand)/.test(name) ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const _s0 = new THREE.Quaternion(), _s1 = new THREE.Quaternion();
|
|
47
|
+
|
|
48
|
+
function sampleQuat(t, j, ph, out) {
|
|
49
|
+
const f = ph * t.F;
|
|
50
|
+
let i0 = Math.floor(f), u = f - i0;
|
|
51
|
+
let i1;
|
|
52
|
+
if (t.loop) { i0 %= t.F; i1 = (i0 + 1) % t.F; }
|
|
53
|
+
else { i0 = Math.min(i0, t.F - 1); i1 = Math.min(i0 + 1, t.F - 1); }
|
|
54
|
+
_s0.fromArray(t.quat[j], i0 * 4);
|
|
55
|
+
_s1.fromArray(t.quat[j], i1 * 4);
|
|
56
|
+
return out.copy(_s0).slerp(_s1, u);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sampleHips(t, ph, out3) {
|
|
60
|
+
const f = ph * t.F;
|
|
61
|
+
let i0 = Math.floor(f), u = f - i0;
|
|
62
|
+
let i1;
|
|
63
|
+
if (t.loop) { i0 %= t.F; i1 = (i0 + 1) % t.F; }
|
|
64
|
+
else { i0 = Math.min(i0, t.F - 1); i1 = Math.min(i0 + 1, t.F - 1); }
|
|
65
|
+
for (let k = 0; k < 3; k++) out3[k] = t.hips[i0 * 3 + k] * (1 - u) + t.hips[i1 * 3 + k] * u;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sampleContact(t, ph, foot) {
|
|
69
|
+
const i = Math.min(Math.floor(ph * t.F), t.F - 1);
|
|
70
|
+
return t.contacts[t.loop ? i % t.F : i][foot];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class Animator {
|
|
74
|
+
constructor(clipSet) {
|
|
75
|
+
this.set = clipSet;
|
|
76
|
+
this.rig = clipSet.rig;
|
|
77
|
+
this.parts = []; // rebuilt each frame: [track, phase, weight]
|
|
78
|
+
this.phase = 0; // shared phase of the directional gaits
|
|
79
|
+
this.freePhase = {}; // name -> independent phase (idle/stance/…)
|
|
80
|
+
this.shot = null; // { t: track, time, fade, yCap, onDone }
|
|
81
|
+
this.mask = null; // { t: track (1-frame pose), w } — upper-body override
|
|
82
|
+
this.maskFeather = clipSet.base.joints.map(maskFeather);
|
|
83
|
+
this._acc = new THREE.Quaternion();
|
|
84
|
+
this._qT = new THREE.Quaternion();
|
|
85
|
+
this._hp = [0, 0, 0];
|
|
86
|
+
this._hA = [0, 0, 0];
|
|
87
|
+
this._shotQ = new THREE.Quaternion();
|
|
88
|
+
this._qV = new THREE.Quaternion();
|
|
89
|
+
this._shotH = [0, 0, 0];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** weights: {clipName: w}. Directional gaits share this.phase (advanced by
|
|
93
|
+
* the weight-blended duration); every loop listed in `free` runs its own. */
|
|
94
|
+
setLoops(weights, dt, free = ["idle", "stance", "push"]) {
|
|
95
|
+
const tr = this.set.tracks;
|
|
96
|
+
let dur = 0, wDir = 0;
|
|
97
|
+
for (const [n, w] of Object.entries(weights)) {
|
|
98
|
+
if (w <= 1e-3 || free.includes(n)) continue;
|
|
99
|
+
dur += (w * tr[n].dur);
|
|
100
|
+
wDir += w;
|
|
101
|
+
}
|
|
102
|
+
if (wDir > 1e-4) this.phase = (this.phase + dt / (dur / wDir)) % 1;
|
|
103
|
+
this.parts.length = 0;
|
|
104
|
+
for (const [n, w] of Object.entries(weights)) {
|
|
105
|
+
if (w <= 1e-3) continue;
|
|
106
|
+
let ph;
|
|
107
|
+
if (free.includes(n)) {
|
|
108
|
+
this.freePhase[n] = ((this.freePhase[n] ?? 0) + dt / tr[n].dur) % 1;
|
|
109
|
+
ph = this.freePhase[n];
|
|
110
|
+
} else ph = this.phase;
|
|
111
|
+
this.parts.push([tr[n], ph, w]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
playOneShot(name, { fadeIn = 0.1, fadeOut = 0.18, yCap = null, rate = 1, onDone = null } = {}) {
|
|
116
|
+
const t = this.set.tracks[name];
|
|
117
|
+
if (!t || this.shot) return false;
|
|
118
|
+
this.shot = { t, time: 0, fadeIn, fadeOut, yCap, rate, onDone, name };
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get oneShotActive() { return this.shot !== null; }
|
|
123
|
+
|
|
124
|
+
/** Upper-body mask layer: override joints above the hips with a pose track
|
|
125
|
+
* (typically 1 frame), feathered up the spine. weight 0..1; ≤0 clears.
|
|
126
|
+
* pitch01 ∈ [-1, 1] slerps toward the compiled `${name}Up` / `${name}Dn`
|
|
127
|
+
* aim-offset variants (aim up = positive) when they exist. */
|
|
128
|
+
setMask(name, weight, pitch01 = 0) {
|
|
129
|
+
const t = this.set.tracks[name];
|
|
130
|
+
this.mask = t && weight > 1e-3
|
|
131
|
+
? { t, w: Math.min(weight, 1),
|
|
132
|
+
up: this.set.tracks[name + "Up"], dn: this.set.tracks[name + "Dn"],
|
|
133
|
+
k: Math.max(-1, Math.min(1, pitch01)) }
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Compute + apply the frame to the rig. Call scene.updateMatrixWorld after. */
|
|
138
|
+
update(dt) {
|
|
139
|
+
const rig = this.rig;
|
|
140
|
+
let shotW = 0, shotPh = 0;
|
|
141
|
+
if (this.shot) {
|
|
142
|
+
const s = this.shot;
|
|
143
|
+
s.time += dt * s.rate;
|
|
144
|
+
const remain = s.t.dur - s.time;
|
|
145
|
+
if (remain <= 0) {
|
|
146
|
+
const done = s.onDone;
|
|
147
|
+
this.shot = null;
|
|
148
|
+
done?.();
|
|
149
|
+
} else {
|
|
150
|
+
shotW = Math.min(1, s.time / s.fadeIn, Math.max(0, remain / s.fadeOut));
|
|
151
|
+
shotW = shotW * shotW * (3 - 2 * shotW); // smoothstep
|
|
152
|
+
shotPh = Math.min(s.time / s.t.dur, 0.9999);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.parts.length === 0 && shotW === 0 && !this.mask) {
|
|
157
|
+
rig.update(dt); // nothing to blend this frame — hold the last pose
|
|
158
|
+
return { shotW: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const j of rig.mapped) {
|
|
162
|
+
let accW = 0;
|
|
163
|
+
for (const [t, ph, wt] of this.parts) {
|
|
164
|
+
sampleQuat(t, j, ph, this._qT);
|
|
165
|
+
if (accW === 0) this._acc.copy(this._qT);
|
|
166
|
+
else this._acc.slerp(this._qT, wt / (accW + wt));
|
|
167
|
+
accW += wt;
|
|
168
|
+
}
|
|
169
|
+
// one-shot over the locomotion, then the aim mask WINS on masked
|
|
170
|
+
// bones: a jump drives the legs/hips/spine while the arms + chest
|
|
171
|
+
// KEEP AIMING (a weapon game must not drop the aim mid-jump)
|
|
172
|
+
if (shotW > 0 && this.shot) {
|
|
173
|
+
sampleQuat(this.shot.t, j, shotPh, this._shotQ);
|
|
174
|
+
if (accW === 0) { this._acc.copy(this._shotQ); accW = 1; }
|
|
175
|
+
else this._acc.slerp(this._shotQ, shotW);
|
|
176
|
+
}
|
|
177
|
+
if (this.mask) {
|
|
178
|
+
const mw = this.maskFeather[j] * this.mask.w;
|
|
179
|
+
if (mw > 1e-3) {
|
|
180
|
+
sampleQuat(this.mask.t, j, 0, this._qT);
|
|
181
|
+
const variant = this.mask.k > 0 ? this.mask.up : this.mask.dn;
|
|
182
|
+
if (variant && Math.abs(this.mask.k) > 1e-3) {
|
|
183
|
+
sampleQuat(variant, j, 0, this._qV);
|
|
184
|
+
this._qT.slerp(this._qV, Math.abs(this.mask.k));
|
|
185
|
+
}
|
|
186
|
+
if (accW === 0) { this._acc.copy(this._qT); accW = 1; }
|
|
187
|
+
else this._acc.slerp(this._qT, mw);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
rig.animNode(j).quaternion.copy(this._acc);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hA = this._hA;
|
|
194
|
+
hA[0] = hA[1] = hA[2] = 0;
|
|
195
|
+
let accW = 0;
|
|
196
|
+
for (const [t, ph, wt] of this.parts) {
|
|
197
|
+
sampleHips(t, ph, this._hp);
|
|
198
|
+
for (let k = 0; k < 3; k++) hA[k] += this._hp[k] * wt;
|
|
199
|
+
accW += wt;
|
|
200
|
+
}
|
|
201
|
+
for (let k = 0; k < 3; k++) hA[k] /= accW || 1;
|
|
202
|
+
if (shotW > 0 && this.shot) {
|
|
203
|
+
sampleHips(this.shot.t, shotPh, this._shotH);
|
|
204
|
+
let y = this._shotH[1];
|
|
205
|
+
if (this.shot.yCap !== null) y = Math.min(y, this.set.base.hipsRestY + this.shot.yCap);
|
|
206
|
+
for (let k = 0; k < 3; k++) {
|
|
207
|
+
const v = k === 1 ? y : this._shotH[k];
|
|
208
|
+
hA[k] = hA[k] * (1 - shotW) + v * shotW;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
rig.setHipsFromArdy(hA[0], hA[1], hA[2]);
|
|
212
|
+
rig.update(dt);
|
|
213
|
+
return { shotW };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Directional blend weights from a character-local command (ARDY frame:
|
|
219
|
+
* +z forward, +x LEFT), magnitude 0..1. Weights are split between the two
|
|
220
|
+
* angularly-nearest AVAILABLE directions (pass `available` to restrict to the
|
|
221
|
+
* gaits a data set actually has — e.g. the 4-cardinal sword fallback), so a
|
|
222
|
+
* missing diagonal folds onto its neighbors instead of dropping the command.
|
|
223
|
+
*/
|
|
224
|
+
const DIR8 = [
|
|
225
|
+
["forward", 0], ["forwardLeft", Math.PI / 4], ["strafeLeft", Math.PI / 2],
|
|
226
|
+
["backLeft", (3 * Math.PI) / 4], ["back", Math.PI], ["backRight", -(3 * Math.PI) / 4],
|
|
227
|
+
["strafeRight", -Math.PI / 2], ["forwardRight", -Math.PI / 4],
|
|
228
|
+
];
|
|
229
|
+
export function dir8Weights(x, z, mag, available = null) {
|
|
230
|
+
const w = {};
|
|
231
|
+
if (mag < 1e-3) return w;
|
|
232
|
+
const dirs = available ? DIR8.filter(([n]) => available.includes(n)) : DIR8;
|
|
233
|
+
if (dirs.length === 0) return w;
|
|
234
|
+
const a = Math.atan2(x, z);
|
|
235
|
+
const ds = dirs.map(([n, da]) => {
|
|
236
|
+
let d = a - da;
|
|
237
|
+
while (d > Math.PI) d -= 2 * Math.PI;
|
|
238
|
+
while (d < -Math.PI) d += 2 * Math.PI;
|
|
239
|
+
return [n, Math.abs(d)];
|
|
240
|
+
}).sort((p, q) => p[1] - q[1]);
|
|
241
|
+
const [n0, d0] = ds[0];
|
|
242
|
+
if (ds.length === 1 || d0 < 1e-4) { w[n0] = mag; return w; }
|
|
243
|
+
const [n1, d1] = ds[1];
|
|
244
|
+
const span = d0 + d1;
|
|
245
|
+
w[n0] = (d1 / span) * mag;
|
|
246
|
+
w[n1] = (d0 / span) * mag;
|
|
247
|
+
return w;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* PROPORTIONAL AIM-MASK HAND-OFF (AG-823 §7.3 #4). The mask target follows
|
|
252
|
+
* the smoothed COMMAND MAGNITUDE (`mag`) instead of a binary threshold+lag:
|
|
253
|
+
* the chest's spine-twist leaves exactly as the bladed idle's root yaw
|
|
254
|
+
* arrives, so a stop never visibly twists right then settles back (the old
|
|
255
|
+
* binary fade did, on every stop). Airborne or pitched aim keeps the mask
|
|
256
|
+
* fully on (the standing idle can't carry a pitched hold).
|
|
257
|
+
*
|
|
258
|
+
* maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });
|
|
259
|
+
* anim.setMask(pose, maskW, pitch / 0.6);
|
|
260
|
+
*/
|
|
261
|
+
export function aimMaskStep(maskW, { mag, grounded, pitch, dt, aiming = true, tau = 0.06 }) {
|
|
262
|
+
const airOrPitch = !grounded || Math.abs(pitch) > 0.12 ? 1 : 0;
|
|
263
|
+
const target = aiming ? Math.max(Math.min(mag, 1), airOrPitch) : 0;
|
|
264
|
+
return maskW + (target - maskW) * (1 - Math.exp(-dt / tau));
|
|
265
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ik.js — minimal analytic two-bone IK (AG-803 Step C). Pins a hand to a
|
|
2
|
+
// weapon grip: law-of-cosines elbow, pole-vector bend plane, applied as
|
|
3
|
+
// world-space quaternion deltas onto the ANIMATED nodes (rig-agnostic — works
|
|
4
|
+
// on the VRM normalized rig and raw Mixamo/Meshy bones alike, because a world
|
|
5
|
+
// delta premultiplied through the node's own parent frame is rig-independent).
|
|
6
|
+
// weight scales the correction (slerp from identity) so IK can fade with the
|
|
7
|
+
// aim mask.
|
|
8
|
+
import * as THREE from "three";
|
|
9
|
+
|
|
10
|
+
const _A = new THREE.Vector3(), _E = new THREE.Vector3(), _W = new THREE.Vector3();
|
|
11
|
+
const _n = new THREE.Vector3(), _h = new THREE.Vector3(), _bend = new THREE.Vector3();
|
|
12
|
+
const _e = new THREE.Vector3(), _v0 = new THREE.Vector3(), _v1 = new THREE.Vector3();
|
|
13
|
+
const _delta = new THREE.Quaternion(), _pw = new THREE.Quaternion();
|
|
14
|
+
const _w0 = new THREE.Quaternion(), _id = new THREE.Quaternion();
|
|
15
|
+
const _dw = new THREE.Quaternion(); // scaled delta — MUST be distinct from _delta
|
|
16
|
+
// (callers pass _delta in; aliasing zeroed the IK once)
|
|
17
|
+
|
|
18
|
+
/** Premultiply a WORLD-space rotation delta (scaled by w) onto a bone. */
|
|
19
|
+
function applyWorldDelta(bone, delta, w) {
|
|
20
|
+
_dw.copy(_id).slerp(delta, w);
|
|
21
|
+
bone.getWorldQuaternion(_w0);
|
|
22
|
+
_w0.premultiply(_dw); // new world orientation
|
|
23
|
+
bone.parent.getWorldQuaternion(_pw).invert();
|
|
24
|
+
bone.quaternion.copy(_pw.multiply(_w0));
|
|
25
|
+
bone.updateMatrixWorld(true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Blend a bone's WORLD orientation toward worldQuat (rotates in place —
|
|
29
|
+
* wrist position is untouched, so it composes with the position IK). */
|
|
30
|
+
export function setWorldQuat(bone, worldQuat, w = 1) {
|
|
31
|
+
if (!bone || w <= 1e-3) return;
|
|
32
|
+
bone.getWorldQuaternion(_w0).slerp(worldQuat, w);
|
|
33
|
+
bone.parent.getWorldQuaternion(_pw).invert();
|
|
34
|
+
bone.quaternion.copy(_pw.multiply(_w0));
|
|
35
|
+
bone.updateMatrixWorld(true);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* upper/fore/hand: THREE nodes of the arm chain (shoulder->elbow->wrist).
|
|
40
|
+
* target: world-space Vector3 the wrist should reach.
|
|
41
|
+
* hintDir: world-space direction the elbow should bend toward (pole vector).
|
|
42
|
+
* weight: 0..1 correction strength.
|
|
43
|
+
*/
|
|
44
|
+
export function twoBoneIK(upper, fore, hand, target, hintDir, weight = 1) {
|
|
45
|
+
if (!upper || !fore || !hand || weight <= 1e-3) return;
|
|
46
|
+
upper.getWorldPosition(_A);
|
|
47
|
+
fore.getWorldPosition(_E);
|
|
48
|
+
hand.getWorldPosition(_W);
|
|
49
|
+
const L1 = _A.distanceTo(_E), L2 = _E.distanceTo(_W);
|
|
50
|
+
if (L1 < 1e-5 || L2 < 1e-5) return;
|
|
51
|
+
|
|
52
|
+
_n.copy(target).sub(_A);
|
|
53
|
+
const d = Math.min(Math.max(_n.length(), Math.abs(L1 - L2) + 1e-4), L1 + L2 - 1e-4);
|
|
54
|
+
_n.normalize();
|
|
55
|
+
_h.copy(hintDir).normalize();
|
|
56
|
+
_bend.copy(_h).addScaledVector(_n, -_h.dot(_n));
|
|
57
|
+
if (_bend.lengthSq() < 1e-8) _bend.set(0, -1, 0).addScaledVector(_n, _n.y); // degenerate hint
|
|
58
|
+
_bend.normalize();
|
|
59
|
+
|
|
60
|
+
const cosA = Math.min(1, Math.max(-1, (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d)));
|
|
61
|
+
const sinA = Math.sqrt(1 - cosA * cosA);
|
|
62
|
+
_e.copy(_A).addScaledVector(_n, L1 * cosA).addScaledVector(_bend, L1 * sinA);
|
|
63
|
+
|
|
64
|
+
// 1) upper arm: current elbow dir -> desired elbow dir
|
|
65
|
+
_v0.copy(_E).sub(_A).normalize();
|
|
66
|
+
_v1.copy(_e).sub(_A).normalize();
|
|
67
|
+
applyWorldDelta(upper, _delta.setFromUnitVectors(_v0, _v1), weight);
|
|
68
|
+
|
|
69
|
+
// 2) forearm: fresh positions after step 1, then wrist -> target
|
|
70
|
+
fore.getWorldPosition(_E);
|
|
71
|
+
hand.getWorldPosition(_W);
|
|
72
|
+
_v0.copy(_W).sub(_E).normalize();
|
|
73
|
+
_v1.copy(target).sub(_E).normalize();
|
|
74
|
+
applyWorldDelta(fore, _delta.setFromUnitVectors(_v0, _v1), weight);
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "genex motion compile tunables — the 'change the formula' surface. Pass with --config; every field overrides the built-in default (values here ARE those defaults). rampFrames/tailSkip: frames skipped at a take's head (constraint ramp) and tail (post-processing wobble). idleLoop: the idle seam-search window in frames. headGazeTargetDeg: mean head pitch is LOWERED to this (a natural aiming gaze), never raised. aimPitchDeg: the Up/Dn aim-variant magnitude, spine-distributed. aimCandidates: how many aim mask poses are plucked (one per take). hipsClampM: tall-stance clamp threshold in meters above ARDY rest. minDirDot: minimum travel-direction agreement before a gait cycle is rejected.",
|
|
3
|
+
"rampFrames": 50,
|
|
4
|
+
"tailSkip": 10,
|
|
5
|
+
"idleLoop": { "minFrames": 40, "maxFrames": 70, "step": 2 },
|
|
6
|
+
"headGazeTargetDeg": 6,
|
|
7
|
+
"aimPitchDeg": 40,
|
|
8
|
+
"aimCandidates": 3,
|
|
9
|
+
"hipsClampM": 0.005,
|
|
10
|
+
"minDirDot": 0.9
|
|
11
|
+
}
|