@moood/render 0.1.3 → 0.1.5

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/engine/kit.js CHANGED
@@ -9,7 +9,11 @@
9
9
  // uses (spec.modules) and gets them as kit.<name>, as they are at the commit it is shown at.
10
10
  // the scene's lib — code its own elements and scripts share: spec.lib, the body of
11
11
  // function (anim, kit) that returns what it offers.
12
- // Painting code sees them as e.anim, e.kit, e.lib; scripts as scene.anim, scene.kit, scene.lib.
12
+ // the scene's land — spec.relief, the body of function (across, distance, anim, lib) that returns
13
+ // the ground's height there (metres; across to the right of the start's line of sight, distance
14
+ // ahead of it): what stands, walks, casts a shadow and looks on it, all on the one ground.
15
+ // Painting code sees them as e.anim, e.kit, e.lib, e.ground; scripts as scene.anim, scene.kit,
16
+ // scene.lib, scene.ground.
13
17
  // Where a spec's modules come from (the page says, with the spec):
14
18
  // {world, refs} — the world's id and the modules by their content's sha ({name: {code, data}},
15
19
  // as the scene's payload has them: the commit it shows); a module the refs don't name (a draft
@@ -120,9 +124,29 @@ const MooodKit = (() => {
120
124
  return built(refs);
121
125
  });
122
126
  }
123
- // what a spec's code shares, once its modules are here: { kit, lib } — or a promise of it (they
124
- // are still on their way); `source`: where its modules come from (above). The same modules are
125
- // the same kit, and a lib on them is made once.
127
+ // the ground's height (metres) at (across, distance) as `relief` says — said once if it throws or
128
+ // returns no number (it is flat there then); null without a relief
129
+ const grounds = new Map();
130
+ function groundOf(relief, lib, key) {
131
+ if (!relief) return null;
132
+ if (!grounds.has(key)) {
133
+ let fn = null, said = false;
134
+ try { fn = new Function('across', 'distance', 'anim', 'lib', relief); } catch (err) { report('relief', err); }
135
+ const say = msg => { if (!said) { said = true; report('relief', msg); } };
136
+ grounds.set(key, fn && ((across, distance) => {
137
+ try {
138
+ const h = fn(across, distance, anim, lib);
139
+ if (Number.isFinite(h)) return h;
140
+ say(`at across ${across}, distance ${distance} it returned ${h}, not a number of metres`);
141
+ } catch (err) { say(err.message || err); }
142
+ return 0;
143
+ }));
144
+ }
145
+ return grounds.get(key);
146
+ }
147
+ // what a spec's code shares, once its modules are here: { kit, lib, ground } — or a promise of it
148
+ // (they are still on their way); `source`: where its modules come from (above). The same modules
149
+ // are the same kit, and a lib on them is made once.
126
150
  const kits = new Map();
127
151
  function shared(spec, source) {
128
152
  const done = ({ mods, key }) => {
@@ -133,10 +157,30 @@ const MooodKit = (() => {
133
157
  try { value = new Function('anim', 'kit', lib)(anim, kit) || {}; } catch (err) { report('lib', err); }
134
158
  libs.set(`${key}\n\n${lib}`, value);
135
159
  }
136
- return { kit, lib: lib ? libs.get(`${key}\n\n${lib}`) : {} };
160
+ const made = lib ? libs.get(`${key}\n\n${lib}`) : {}, relief = typeof spec?.relief === 'string' && spec.relief.trim() ? spec.relief : null;
161
+ return { kit, lib: made, ground: groundOf(relief, made, `${key}\n\n${lib}\n\n${relief}`) };
137
162
  };
138
163
  const m = modulesOf(spec, source);
139
164
  return m instanceof Promise ? m.then(done) : done(m);
140
165
  }
141
- return { anim, shared };
166
+ // The state keys the engine itself reads for a spec — whatever the scene's own code does: the film's
167
+ // camera and fade (every scene's), an insert's key, a place's, a redraw's, the light's looks, a
168
+ // light's `on` and place — each with what it takes, as spec.states describes a key ({range} or
169
+ // {type}; {} — anything). A key set from outside that neither these nor the scene knows, or one of
170
+ // these set to what the engine can't take, is said (scenestate.js).
171
+ const NUM = { type: 'number' }, UNIT = { range: [0, 1] };
172
+ const CAMERA = { camera_target: {}, camera_shot: {}, camera_frame: {}, camera_glide: NUM, camera_x: NUM, camera_z: NUM, camera_look: NUM, camera_h: NUM, fade: UNIT, fade_color: { type: 'string' } };
173
+ function keys(spec) {
174
+ const out = new Map(Object.entries(CAMERA)), obj = v => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
175
+ const add = (d, ...ks) => { for (const k of ks) if (typeof k === 'string') out.set(k, d); };
176
+ for (const o of Array.isArray(spec?.objects) ? spec.objects : []) {
177
+ if (!o || typeof o !== 'object') continue;
178
+ add(UNIT, o.insert); add(NUM, ...Object.values(obj(o.place)));
179
+ add({}, ...(Array.isArray(o.redraw) ? o.redraw : Object.keys(obj(o.redraw))));
180
+ }
181
+ for (const l of Array.isArray(spec?.lights) ? spec.lights : []) if (l && typeof l === 'object') { add(UNIT, l.on); add(NUM, ...Object.values(obj(l.place))); }
182
+ add(UNIT, ...Object.keys(obj(obj(spec?.light).looks)));
183
+ return out;
184
+ }
185
+ return { anim, shared, keys };
142
186
  })();
@@ -0,0 +1,47 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>moood · motion</title>
6
+ <style>
7
+ html, body { margin: 0; height: 100%; overflow: hidden; background: #151a22; font: 13px system-ui, sans-serif; }
8
+ canvas { position: fixed; inset: 0; width: 100%; height: 100%; }
9
+ .label { position: fixed; padding: 3px 7px; color: #d6dae1; background: rgba(14, 17, 22, 0.6); border-bottom-right-radius: 6px; }
10
+ .label b { color: #e8c98a; font-weight: 600; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <!-- A made motion as frames on its body, for the render (services/render, @moood/render): what an
15
+ assistant looks at. Handed in: window.__mooodMotion — the motion ({n, fps, names, q, p}: the motion
16
+ service's body of it); window.__mooodMotionShow — {at: [seconds…], views: ["front" | "side" |
17
+ "back" | {yaw, pitch, dist}…], kit: its module clip or null (the module's skeleton over the body),
18
+ cell: [w, h]}: a row per view, a column per moment, each frame labelled. The page is as large as
19
+ the sheet; the title turns "ready" when it is drawn, errors in window.__mooodRenderErrors. -->
20
+ <canvas id="sheet"></canvas>
21
+ <script src="motion.js"></script>
22
+ <script>
23
+ window.__mooodRenderErrors = [];
24
+ (async () => {
25
+ try {
26
+ const motion = window.__mooodMotion, show = window.__mooodMotionShow;
27
+ const [w, h] = show.cell, canvas = document.getElementById('sheet');
28
+ const v = MooodMotion.view(canvas, await MooodMotion.body(), motion, show.kit);
29
+ const cells = [];
30
+ show.views.forEach((view, r) => show.at.forEach((s, c) => {
31
+ const t = Math.max(0, Math.min(motion.n - 1, s * motion.fps));
32
+ cells.push([c * w, r * h, w, h, t, typeof view === 'string' ? { facing: view } : view]);
33
+ const label = document.createElement('div');
34
+ label.className = 'label';
35
+ label.style.left = `${c * w}px`; label.style.top = `${r * h}px`;
36
+ label.innerHTML = `<b>${s.toFixed(2)} s</b> · ${typeof view === 'string' ? view : 'view'}`;
37
+ document.body.append(label);
38
+ }));
39
+ v.draw(0, {}, { kit: !!show.kit, cells });
40
+ } catch (e) {
41
+ window.__mooodRenderErrors.push(String(e && e.stack || e));
42
+ }
43
+ document.title = 'ready';
44
+ })();
45
+ </script>
46
+ </body>
47
+ </html>
@@ -0,0 +1,258 @@
1
+ // A person's motion made by a model (NVIDIA Kimodo: the motion service), drawn on a body — what the
2
+ // sandbox plays (the site) and what the render shows an assistant (motion.html: frames of it in a
3
+ // sheet). The body is Kimodo's own (soma.bin beside this file — its SOMA skin, Apache-2.0: soma.md):
4
+ // 18k vertices on 77 joints, 8 weights each, moved by every joint's rotation and place as Kimodo made
5
+ // them ({n, fps, names, q, p}: the motion service's /api/motions/<key>/body), skinned here, drawn
6
+ // with WebGL; the floor in metres, the pelvis's path on it. Over it, if asked, the skeleton a people
7
+ // module gets from the same motion (its data entry, {n, fps, v, p, c, r}: bone directions, the pelvis,
8
+ // foot contacts, the path over the floor — rebuilt the way dev/mocap/feat.py's rebuild does, on a body of usual proportions).
9
+ // Kimodo's axes: y up, metres; at frame 0 the pelvis over the origin, the person facing +z, their left +x.
10
+ // MooodMotion.body() → the body (fetched once)
11
+ // MooodMotion.view(canvas, body, motion, clip?) → {n, fps, draw(t, camera, {kit, cells})}
12
+ // t — the frame (fractional: between two, mixed); camera — {yaw, pitch, dist} around the pelvis
13
+ // at t (yaw 0: from +z), or {facing: 'front' | 'side' | 'back'} — from where the person faces at t;
14
+ // kit — the module's skeleton over the body; cells — [[x, y, w, h, t, camera]…] for several
15
+ // frames on one canvas (a sheet), instead of t and camera.
16
+ (function () {
17
+ const HERE = document.currentScript.src;
18
+ let soma = null;
19
+
20
+ const bytes = b64 => Uint8Array.from(atob(b64), ch => ch.charCodeAt(0));
21
+
22
+ // (a request, not fetch: the local render reads the engine from files, and fetch doesn't)
23
+ const read = url => new Promise((resolve, reject) => {
24
+ const x = new XMLHttpRequest();
25
+ x.open('GET', url); x.responseType = 'arraybuffer';
26
+ x.onload = () => (x.response && (x.status === 200 || x.status === 0) ? resolve(x.response) : reject(new Error(`${url}: ${x.status}`)));
27
+ x.onerror = () => reject(new Error(`${url}: not read`));
28
+ x.send();
29
+ });
30
+
31
+ function body() {
32
+ return (soma ??= read(new URL('soma.bin', HERE)).then(buf => {
33
+ const [V, F, J] = new Uint32Array(buf, 0, 3);
34
+ let at = 12;
35
+ const take = (Kind, n, size) => { const a = new Kind(buf.slice(at, at + n * size)); at += n * size; return a; };
36
+ return { V, F, J, bind: take(Float32Array, V * 3, 4), faces: take(Uint16Array, F * 3, 2), joint: take(Uint8Array, V * 8, 1),
37
+ weight: take(Uint8Array, V * 8, 1), inverse: take(Float32Array, J * 12, 4) };
38
+ }));
39
+ }
40
+
41
+ // ── the module's skeleton (the clip, as feat.py has it) ────────────────
42
+
43
+ const VEC = ['pF', 'pU', 'cF', 'cU', 'hF', 'hU', 'spine', 'neck', 'rUp', 'rFo', 'rHa', 'lUp', 'lFo', 'lHa', 'rTh', 'rSh', 'rFt', 'lTh', 'lSh', 'lFt'];
44
+ const D = { leg: 0.9, thigh: 0.43, shin: 0.43, spine: 0.5, neck: 0.1, head: 0.24, sw: 0.36, hw: 0.18, upper: 0.31, fore: 0.26, foot: 0.15 };
45
+ const BONES = [['pelvis', 'spine'], ['spine', 'chest'], ['chest', 'neck'], ['neck', 'headtop'], ['rhip', 'lhip'],
46
+ ...['r', 'l'].flatMap(s => [['chest', `${s}shoulder`], [`${s}shoulder`, `${s}elbow`], [`${s}elbow`, `${s}wrist`], [`${s}wrist`, `${s}hand`],
47
+ [`${s}hip`, `${s}knee`], [`${s}knee`, `${s}ankle`], [`${s}ankle`, `${s}ball`], [`${s}ball`, `${s}toe`]])];
48
+ const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
49
+ const mul = (v, k) => [v[0] * k, v[1] * k, v[2] * k];
50
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
51
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
52
+ const unit = v => mul(v, 1 / (Math.hypot(...v) || 1));
53
+ // a frame (its columns: right, up, forward) from forward and up; applied to v; one frame in another
54
+ const frame = (F, U) => { F = unit(F); U = unit(add(U, mul(F, -dot(U, F)))); return [cross(U, F), U, F]; };
55
+ const apply = (M, v) => add(add(mul(M[0], v[0]), mul(M[1], v[1])), mul(M[2], v[2]));
56
+ const within = (A, B) => B.map(c => apply(A, c));
57
+
58
+ // the clip's frames as the module's joints, the body going along its path (r) — in Kimodo's axes (the kit's x is
59
+ // the person's right: mirrored back)
60
+ function skeleton(clip) {
61
+ const v = new Int8Array(bytes(clip.v).buffer), p = new Int16Array(bytes(clip.p).buffer), r = clip.r ? new Int16Array(bytes(clip.r).buffer) : null;
62
+ return Array.from({ length: clip.n }, (_, i) => {
63
+ const V = Object.fromEntries(VEC.map((k, j) => [k, [v[i * 60 + j * 3] / 127, v[i * 60 + j * 3 + 1] / 127, v[i * 60 + j * 3 + 2] / 127]]));
64
+ const P = frame(V.pF, V.pU), C = within(P, frame(V.cF, V.cU)), H = within(C, frame(V.hF, V.hU));
65
+ const hip = [(p[i * 3] / 8000 + (r ? r[i * 2] / 100 : 0)) * D.leg, p[i * 3 + 1] / 8000 * D.leg, (p[i * 3 + 2] / 8000 + (r ? r[i * 2 + 1] / 100 : 0)) * D.leg];
66
+ const J = { pelvis: hip, rhip: add(hip, apply(P, [D.hw / 2, 0, 0])), lhip: add(hip, apply(P, [-D.hw / 2, 0, 0])) };
67
+ J.spine = add(hip, mul(apply(P, V.spine), D.spine / 2));
68
+ J.chest = add(hip, mul(apply(P, V.spine), D.spine));
69
+ J.neck = add(J.chest, mul(apply(C, V.neck), D.neck));
70
+ J.headtop = add(J.neck, apply(H, [0, D.head, 0]));
71
+ for (const [s, sg] of [['r', 1], ['l', -1]]) {
72
+ J[`${s}shoulder`] = add(J.chest, apply(C, [sg * D.sw / 2, -0.03, 0]));
73
+ J[`${s}elbow`] = add(J[`${s}shoulder`], mul(apply(C, V[`${s}Up`]), D.upper));
74
+ J[`${s}wrist`] = add(J[`${s}elbow`], mul(apply(C, V[`${s}Fo`]), D.fore));
75
+ J[`${s}hand`] = add(J[`${s}wrist`], mul(apply(C, V[`${s}Ha`]), 0.08));
76
+ J[`${s}knee`] = add(J[`${s}hip`], mul(apply(P, V[`${s}Th`]), D.thigh));
77
+ J[`${s}ankle`] = add(J[`${s}knee`], mul(apply(P, V[`${s}Sh`]), D.shin));
78
+ J[`${s}ball`] = add(J[`${s}ankle`], mul(apply(P, V[`${s}Ft`]), D.foot));
79
+ J[`${s}toe`] = add(J[`${s}ball`], apply(P, [0, 0, 0.05]));
80
+ }
81
+ for (const k in J) J[k] = [-J[k][0], J[k][1], J[k][2]];
82
+ return J;
83
+ });
84
+ }
85
+
86
+ // ── WebGL ─────────────────────────────────────────────────────────────
87
+
88
+ const MESH_VS = `#version 300 es
89
+ in vec3 at; in vec3 normal; uniform mat4 view; out vec3 n;
90
+ void main() { n = normal; gl_Position = view * vec4(at, 1.0); }`;
91
+ const MESH_FS = `#version 300 es
92
+ precision mediump float; in vec3 n; out vec4 color;
93
+ void main() {
94
+ vec3 m = normalize(n);
95
+ float light = 0.32 + 0.55 * max(dot(m, normalize(vec3(0.4, 0.8, 0.6))), 0.0) + 0.18 * max(dot(m, normalize(vec3(-0.6, 0.2, -0.7))), 0.0);
96
+ color = vec4(vec3(0.84, 0.80, 0.74) * light, 1.0);
97
+ }`;
98
+ const LINE_VS = `#version 300 es
99
+ in vec3 at; uniform mat4 view; void main() { gl_Position = view * vec4(at, 1.0); }`;
100
+ const LINE_FS = `#version 300 es
101
+ precision mediump float; uniform vec4 tint; out vec4 color; void main() { color = tint; }`;
102
+
103
+ function program(gl, vs, fs) {
104
+ const p = gl.createProgram();
105
+ for (const [kind, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
106
+ const s = gl.createShader(kind);
107
+ gl.shaderSource(s, src); gl.compileShader(s);
108
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
109
+ gl.attachShader(p, s);
110
+ }
111
+ gl.linkProgram(p);
112
+ return p;
113
+ }
114
+
115
+ // perspective × look-at, column-major
116
+ function viewOf(eye, target, aspect) {
117
+ const f = unit([eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]]);
118
+ const r = unit(cross([0, 1, 0], f)), u = cross(f, r);
119
+ const V = [r[0], u[0], f[0], 0, r[1], u[1], f[1], 0, r[2], u[2], f[2], 0, -dot(r, eye), -dot(u, eye), -dot(f, eye), 1];
120
+ const t = 1 / Math.tan(0.4), n = 0.05, far = 100;
121
+ const P = [t / aspect, 0, 0, 0, 0, t, 0, 0, 0, 0, (far + n) / (n - far), -1, 0, 0, 2 * far * n / (n - far), 0];
122
+ const M = new Float32Array(16);
123
+ for (let c = 0; c < 4; c++) for (let r2 = 0; r2 < 4; r2++) {
124
+ let s = 0;
125
+ for (let k = 0; k < 4; k++) s += P[k * 4 + r2] * V[c * 4 + k];
126
+ M[c * 4 + r2] = s;
127
+ }
128
+ return M;
129
+ }
130
+
131
+ function view(canvas, B, motion, clip) {
132
+ const gl = canvas.getContext('webgl2', { antialias: true, preserveDrawingBuffer: true });
133
+ const q = new Int16Array(bytes(motion.q).buffer), pos = new Int16Array(bytes(motion.p).buffer), J = motion.names.length, n = motion.n;
134
+ const kit = clip ? skeleton(clip) : null;
135
+ const mesh = program(gl, MESH_VS, MESH_FS), lines = program(gl, LINE_VS, LINE_FS);
136
+ const skinned = new Float32Array(B.V * 3), normals = new Float32Array(B.V * 3), M = new Float32Array(J * 12);
137
+ const vao = gl.createVertexArray();
138
+ gl.bindVertexArray(vao);
139
+ const atBuf = gl.createBuffer(), nBuf = gl.createBuffer(), iBuf = gl.createBuffer();
140
+ for (const [buf, name] of [[atBuf, 'at'], [nBuf, 'normal']]) {
141
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, skinned.byteLength, gl.DYNAMIC_DRAW);
142
+ const loc = gl.getAttribLocation(mesh, name); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, 0, 0);
143
+ }
144
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, B.faces, gl.STATIC_DRAW);
145
+ gl.bindVertexArray(null);
146
+ const lineBuf = gl.createBuffer(), lineAt = gl.getAttribLocation(lines, 'at');
147
+ const drawLines = (points, tint, V, mode = gl.LINES) => {
148
+ gl.useProgram(lines);
149
+ gl.uniformMatrix4fv(gl.getUniformLocation(lines, 'view'), false, V);
150
+ gl.uniform4fv(gl.getUniformLocation(lines, 'tint'), tint);
151
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STREAM_DRAW);
152
+ gl.enableVertexAttribArray(lineAt); gl.vertexAttribPointer(lineAt, 3, gl.FLOAT, false, 0, 0);
153
+ gl.drawArrays(mode, 0, points.length / 3);
154
+ };
155
+ const joint = (f, j) => [0, 1, 2].map(k => pos[(f * J + j) * 3 + k] / 1000);
156
+
157
+ // the joints' matrices at frame t (between two frames: positions mixed, rotations mixed and renormalised),
158
+ // each times its inverse bind
159
+ const pose = t => {
160
+ const a = Math.floor(t) % n, b = Math.min(n - 1, a + 1), u = t - Math.floor(t);
161
+ for (let j = 0; j < J; j++) {
162
+ const ia = (a * J + j) * 4, ib = (b * J + j) * 4;
163
+ const sign = q[ia] * q[ib] + q[ia + 1] * q[ib + 1] + q[ia + 2] * q[ib + 2] + q[ia + 3] * q[ib + 3] < 0 ? -1 : 1;
164
+ let x = q[ia] * (1 - u) + sign * q[ib] * u, y = q[ia + 1] * (1 - u) + sign * q[ib + 1] * u,
165
+ z = q[ia + 2] * (1 - u) + sign * q[ib + 2] * u, w = q[ia + 3] * (1 - u) + sign * q[ib + 3] * u;
166
+ const l = Math.hypot(x, y, z, w); x /= l; y /= l; z /= l; w /= l;
167
+ const R = [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
168
+ 2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
169
+ 2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)];
170
+ const pa = (a * J + j) * 3, pb = (b * J + j) * 3;
171
+ const T = [0, 1, 2].map(k => (pos[pa + k] * (1 - u) + pos[pb + k] * u) / 1000);
172
+ const I = B.inverse.subarray(j * 12, j * 12 + 12);
173
+ for (let r = 0; r < 3; r++) {
174
+ for (let c = 0; c < 4; c++) {
175
+ M[j * 12 + r * 4 + c] = R[r * 3] * I[c] + R[r * 3 + 1] * I[4 + c] + R[r * 3 + 2] * I[8 + c] + (c === 3 ? T[r] : 0);
176
+ }
177
+ }
178
+ }
179
+ };
180
+ const skin = () => {
181
+ const { bind, joint: jn, weight } = B;
182
+ for (let i = 0; i < B.V; i++) {
183
+ const x = bind[i * 3], y = bind[i * 3 + 1], z = bind[i * 3 + 2];
184
+ let sx = 0, sy = 0, sz = 0;
185
+ for (let k = 0; k < 8; k++) {
186
+ const w = weight[i * 8 + k];
187
+ if (!w) continue;
188
+ const o = jn[i * 8 + k] * 12, f = w / 255;
189
+ sx += f * (M[o] * x + M[o + 1] * y + M[o + 2] * z + M[o + 3]);
190
+ sy += f * (M[o + 4] * x + M[o + 5] * y + M[o + 6] * z + M[o + 7]);
191
+ sz += f * (M[o + 8] * x + M[o + 9] * y + M[o + 10] * z + M[o + 11]);
192
+ }
193
+ skinned[i * 3] = sx; skinned[i * 3 + 1] = sy; skinned[i * 3 + 2] = sz;
194
+ }
195
+ normals.fill(0);
196
+ const fc = B.faces;
197
+ for (let f = 0; f < B.F * 3; f += 3) {
198
+ const a = fc[f] * 3, b = fc[f + 1] * 3, c = fc[f + 2] * 3;
199
+ const ux = skinned[b] - skinned[a], uy = skinned[b + 1] - skinned[a + 1], uz = skinned[b + 2] - skinned[a + 2];
200
+ const vx = skinned[c] - skinned[a], vy = skinned[c + 1] - skinned[a + 1], vz = skinned[c + 2] - skinned[a + 2];
201
+ const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
202
+ for (const v of [a, b, c]) { normals[v] += nx; normals[v + 1] += ny; normals[v + 2] += nz; }
203
+ }
204
+ };
205
+
206
+ // the pelvis's path, and the floor around all of it
207
+ const hips = Array.from({ length: n }, (_, i) => joint(i, 0));
208
+ const mid = [hips.reduce((s, h) => s + h[0], 0) / n, hips.reduce((s, h) => s + h[2], 0) / n];
209
+ const reach = Math.max(...hips.map(h => Math.hypot(h[0] - mid[0], h[2] - mid[1])));
210
+ const R = Math.ceil(reach) + 5, x0 = Math.round(mid[0]), z0 = Math.round(mid[1]), grid = [];
211
+ for (let d = -R; d <= R; d++) grid.push(x0 + d, 0, z0 - R, x0 + d, 0, z0 + R, x0 - R, 0, z0 + d, x0 + R, 0, z0 + d);
212
+ const path = hips.flatMap(h => [h[0], 0.002, h[2]]);
213
+ // which way the person faces at frame f, as the camera's yaw from the front: the line from the right
214
+ // hip to the left one (+x while facing +z), turned a quarter
215
+ const L = motion.names.indexOf('LeftLeg'), Rl = motion.names.indexOf('RightLeg');
216
+ const facing = f => { const l = joint(f, L), r = joint(f, Rl); return Math.atan2(-(l[2] - r[2]), l[0] - r[0]); };
217
+
218
+ const cell = (x, y, w, h, t, camera, kitOn) => {
219
+ gl.viewport(x, y, w, h); gl.scissor(x, y, w, h);
220
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.DEPTH_TEST);
221
+ const f = Math.min(n - 1, Math.floor(t)), at = [hips[f][0], 0.85, hips[f][2]];
222
+ let { yaw = 0.6, pitch = 0.22, dist = 3.6 } = camera;
223
+ if (camera.facing) yaw = facing(f) + { front: 0, side: Math.PI / 2, back: Math.PI }[camera.facing];
224
+ const eye = [at[0] + dist * Math.cos(pitch) * Math.sin(yaw), at[1] + dist * Math.sin(pitch), at[2] + dist * Math.cos(pitch) * Math.cos(yaw)];
225
+ const V = viewOf(eye, at, w / h);
226
+ drawLines(grid, [0.2, 0.24, 0.3, 1], V);
227
+ drawLines(path, [0.5, 0.53, 0.58, 1], V, gl.LINE_STRIP);
228
+ pose(t); skin();
229
+ gl.useProgram(mesh);
230
+ gl.uniformMatrix4fv(gl.getUniformLocation(mesh, 'view'), false, V);
231
+ gl.bindVertexArray(vao);
232
+ gl.bindBuffer(gl.ARRAY_BUFFER, atBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, skinned);
233
+ gl.bindBuffer(gl.ARRAY_BUFFER, nBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, normals);
234
+ gl.drawElements(gl.TRIANGLES, B.F * 3, gl.UNSIGNED_SHORT, 0);
235
+ gl.bindVertexArray(null);
236
+ if (kitOn && kit) {
237
+ // (its clip may be a frame or two shorter or longer: the same moment)
238
+ const S = kit[Math.min(kit.length - 1, Math.round(t * kit.length / n))];
239
+ gl.disable(gl.DEPTH_TEST);
240
+ drawLines(BONES.flatMap(([a, b]) => [...S[a], ...S[b]]), [0.91, 0.79, 0.54, 1], V);
241
+ }
242
+ };
243
+ return {
244
+ n, fps: motion.fps,
245
+ draw(t, camera = {}, { kit: kitOn = false, cells = null } = {}) {
246
+ const k = devicePixelRatio || 1, w = canvas.clientWidth, h = canvas.clientHeight;
247
+ if (canvas.width !== Math.round(w * k) || canvas.height !== Math.round(h * k)) { canvas.width = Math.round(w * k); canvas.height = Math.round(h * k); }
248
+ gl.enable(gl.SCISSOR_TEST);
249
+ gl.clearColor(0.082, 0.102, 0.133, 1);
250
+ if (!cells) return cell(0, 0, canvas.width, canvas.height, t, camera, kitOn);
251
+ // cells in CSS pixels from the top left; WebGL's from the bottom left
252
+ for (const [x, y, cw, ch, ct, cc] of cells) cell(Math.round(x * k), Math.round(canvas.height - (y + ch) * k), Math.round(cw * k), Math.round(ch * k), ct, cc, kitOn);
253
+ },
254
+ };
255
+ }
256
+
257
+ window.MooodMotion = { body, view };
258
+ })();
@@ -12,7 +12,12 @@
12
12
  screenshots for the artist's look at its own work, the pictures of scenes and assistants (MCP).
13
13
  The service hands the
14
14
  spec in as window.__mooodSpec; errors the scene's code threw end up in
15
- window.__mooodRenderErrors; the title turns "ready" when it is time to take the picture. -->
15
+ window.__mooodRenderErrors, what it does wrong without failing (a name it does not know, a
16
+ mistyped key) in window.__mooodRenderWarnings, what its scripts told (scene.tell) in window.__mooodTells ({at:
17
+ seconds since it started, text, about}); the title turns "ready" when it is time to take the
18
+ picture. The stage is window.__mooodStage: a render steps the film unseen to the moments it
19
+ takes (draw(false)) and asks what is where (inspect()); it is checked as it is drawn (a painting cut
20
+ off by its box is said). ?debug — what is where, over the frame. -->
16
21
  <canvas id="scene"></canvas>
17
22
  <header class="titles"><h1 id="title"></h1><p id="caption"></p></header>
18
23
  <script src="kit.js"></script>
@@ -33,9 +38,11 @@
33
38
  document.getElementById('caption').textContent = spec.caption || '';
34
39
  }
35
40
  const modules = { texts: window.__mooodModules || {} }; // the modules the scene lists, given (kit.js)
36
- const canvas = document.getElementById('scene'), stage = Moood.mount(canvas);
41
+ const canvas = document.getElementById('scene'), stage = window.__mooodStage = Moood.mount(canvas, { check: true });
42
+ // the starting state, scripts running (their errors are reported) — before the scene is shown, so its first frame is as they set it
43
+ const t0 = performance.now(), told = window.__mooodTells = [];
44
+ MooodState.start(spec, {}, { stage, modules, tell: (text, about) => told.push({ at: (performance.now() - t0) / 1000, text, about }) });
37
45
  stage.show(spec, modules);
38
- MooodState.start(spec, {}, { stage, modules }); // the starting state, scripts running (their errors are reported)
39
46
  if (!bare) MooodWidgets.create({ canvas, stage }).show({ ...spec, widgets: (spec.widgets || []).map(w => ({ ...w, at: 0, for: 0 })) });
40
47
  // show every caption at once, so the reviewer reads them all
41
48
  if (!bare) MooodCaptions.show((spec.captions || []).map(c => ({ ...c, at: 0, for: 0 })));
@@ -43,6 +50,7 @@
43
50
  await new Promise(r => setTimeout(r, 1500));
44
51
  errors.push(...(window.__mooodErrors || []));
45
52
  window.__mooodRenderErrors = errors;
53
+ window.__mooodRenderWarnings = [...(window.__mooodWarnings || []), ...MooodState.unknown()];
46
54
  document.title = 'ready';
47
55
  })();
48
56
  </script>