@moood/render 0.1.2 → 0.1.4
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 +19 -1
- package/cli.js +24 -8
- package/engine/engine.js +73 -50
- package/engine/engine.json +1 -1
- package/engine/gl.js +8 -12
- package/engine/motion.html +47 -0
- package/engine/motion.js +258 -0
- package/engine/render.html +6 -2
- package/engine/scenestate.js +12 -7
- package/engine/select.js +5 -5
- package/engine/soma.bin +0 -0
- package/engine/soma.md +4 -0
- package/index.js +47 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ network): a PNG and the errors the scene's code threw.
|
|
|
15
15
|
same every run. The same moment and state give the same picture — pixel-close, not bit-equal (the
|
|
16
16
|
GPU emulation's rounding): compare frames with a small tolerance.
|
|
17
17
|
- **`--state '{"dawn": 1}'`** — values over the scene's starting state.
|
|
18
|
+
- **`--set '[[4, "beat", "cover"], [6, "dawn", 1, 3]]'`** — values set as the scene goes, as a game sets
|
|
19
|
+
them: `[second, key, value, over?, ease?]` (the clock stops at each one's second). A film's beat at
|
|
20
|
+
its second; a branch begun mid-beat.
|
|
21
|
+
- **What the scene told** (`scene.tell` — a film's `beat:<name>` as a beat ends) is printed with its
|
|
22
|
+
second: `told at 4.50s: beat:cover`.
|
|
18
23
|
- **`--device`** — `16:9` (1600×900), `4:3`, `21:9` (the same height), `tile` (640×360), `big` (1920×1080).
|
|
19
24
|
- **`--bare`** — without the title and captions over it.
|
|
20
25
|
- **The browser**: `--chromium <path>`, or `MOOOD_CHROMIUM`, or Playwright's own (`PLAYWRIGHT_BROWSERS_PATH`).
|
|
@@ -24,6 +29,19 @@ From code:
|
|
|
24
29
|
import { render, readScene, readModules } from '@moood/render';
|
|
25
30
|
const spec = readScene('world/scenes/kukhnya');
|
|
26
31
|
const [frame] = await render({ spec, modules: readModules('world', spec), at: [3] });
|
|
27
|
-
// frame.png (a Buffer), frame.errors
|
|
32
|
+
// frame.png (a Buffer), frame.errors, frame.tells ([{at, text, about}] so far)
|
|
28
33
|
|
|
29
34
|
Exits 1 if the scene's code threw.
|
|
35
|
+
|
|
36
|
+
## A person's motion
|
|
37
|
+
|
|
38
|
+
A motion made by moood's `make_motion` (NVIDIA Kimodo), as frames on its body — the same picture as the
|
|
39
|
+
MCP's `look_at_motion`. Save `list_motions(key, body=true)` as a file, then:
|
|
40
|
+
|
|
41
|
+
moood-render motion walk.json --at 0,1,2,3 --views front,side --out walk.png
|
|
42
|
+
|
|
43
|
+
- **`--at`** — moments in seconds (by default eight across the motion); **`--views`** — rows: `front`, `side`
|
|
44
|
+
(from the person's left), `back` — each from where the person faces then; **`--kit`** — over the body, the
|
|
45
|
+
skeleton a people module gets from it (the file's `clip`).
|
|
46
|
+
|
|
47
|
+
From code: `renderMotion({motion, at, views, kit})` → `{png, errors}`.
|
package/cli.js
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// moood-render <scene folder | spec.json> [--world <its world's repository>] [--device 16:9] [--at 0,1.5,3]
|
|
3
|
-
// [--state '{"dawn":1}'] [--bare] [--out frame.png] [--chromium <path>]
|
|
3
|
+
// [--state '{"dawn":1}'] [--set '[[4,"beat","cover"]]'] [--bare] [--out frame.png] [--chromium <path>]
|
|
4
4
|
// A frame for each moment (--out frame.png → frame.png, or frame-1.5s.png … for several); the
|
|
5
|
-
// errors the scene's code threw, on stderr. The world defaults to the folder two levels up
|
|
5
|
+
// errors the scene's code threw, on stderr; what its scripts told (scene.tell), on stdout. The world defaults to the folder two levels up
|
|
6
6
|
// (<world>/scenes/<folder>): where its modules are.
|
|
7
|
+
// moood-render motion <motion.json> [--at 0,1.5,3] [--views front,side] [--kit] [--out motion.png]
|
|
8
|
+
// A person's motion made by a model (list_motions(key, body=true) saved as a file) as frames on its body.
|
|
7
9
|
import { statSync, writeFileSync, readFileSync } from 'node:fs';
|
|
8
10
|
import { dirname, resolve } from 'node:path';
|
|
9
11
|
import { parseArgs } from 'node:util';
|
|
10
|
-
import { ENGINE, readModules, readScene, render } from './index.js';
|
|
12
|
+
import { ENGINE, readModules, readScene, render, renderMotion } from './index.js';
|
|
11
13
|
|
|
12
|
-
const { values: o, positionals: [what] } = parseArgs({
|
|
14
|
+
const { values: o, positionals: [what, file] } = parseArgs({
|
|
13
15
|
allowPositionals: true,
|
|
14
16
|
options: { world: { type: 'string' }, device: { type: 'string', default: '16:9' }, at: { type: 'string', default: '0' },
|
|
15
|
-
state: { type: 'string' }, bare: { type: 'boolean' }, out: { type: 'string', default: 'frame.png' },
|
|
16
|
-
chromium: { type: 'string' }, version: { type: 'boolean' } },
|
|
17
|
+
state: { type: 'string' }, set: { type: 'string' }, bare: { type: 'boolean' }, out: { type: 'string', default: 'frame.png' },
|
|
18
|
+
chromium: { type: 'string' }, version: { type: 'boolean' }, views: { type: 'string', default: 'front,side' },
|
|
19
|
+
kit: { type: 'boolean' } },
|
|
17
20
|
});
|
|
18
21
|
if (o.version) { console.log(`moood engine ${ENGINE}`); process.exit(0); }
|
|
19
|
-
if (!what) { console.error('moood-render <scene folder | spec.json> [--world …] [--device 16:9] [--at 0,1.5] [--state {…}] [--bare] [--out frame.png]'); process.exit(2); }
|
|
22
|
+
if (!what) { console.error('moood-render <scene folder | spec.json> [--world …] [--device 16:9] [--at 0,1.5] [--state {…}] [--set [[s,k,v],…]] [--bare] [--out frame.png]'); process.exit(2); }
|
|
23
|
+
|
|
24
|
+
if (what === 'motion') {
|
|
25
|
+
if (!file) { console.error('moood-render motion <motion.json> [--at 0,1.5] [--views front,side] [--kit] [--out motion.png]'); process.exit(2); }
|
|
26
|
+
const made = JSON.parse(readFileSync(file, 'utf8'));
|
|
27
|
+
const { png, errors } = await renderMotion({ motion: made.body || made, kit: o.kit ? made.clip : null, chromium: o.chromium,
|
|
28
|
+
at: process.argv.includes('--at') ? o.at.split(',').map(Number) : null, views: o.views.split(',') });
|
|
29
|
+
const out = o.out === 'frame.png' ? 'motion.png' : o.out;
|
|
30
|
+
writeFileSync(out, png);
|
|
31
|
+
console.log(out);
|
|
32
|
+
for (const e of errors) console.error(e);
|
|
33
|
+
process.exit(errors.length ? 1 : 0);
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
const folder = statSync(what).isDirectory();
|
|
22
37
|
const spec = folder ? readScene(what) : JSON.parse(readFileSync(what, 'utf8'));
|
|
23
38
|
const world = o.world || (folder ? resolve(what, '..', '..') : dirname(resolve(what)));
|
|
24
39
|
const at = o.at.split(',').map(Number);
|
|
25
40
|
const frames = await render({ spec, modules: readModules(world, spec), device: o.device, at, state: o.state ? JSON.parse(o.state) : {},
|
|
26
|
-
bare: o.bare, chromium: o.chromium });
|
|
41
|
+
set: o.set ? JSON.parse(o.set) : [], bare: o.bare, chromium: o.chromium });
|
|
27
42
|
let failed = false;
|
|
28
43
|
for (const f of frames) {
|
|
29
44
|
const out = frames.length > 1 ? o.out.replace(/(\.png)?$/, `-${f.at}s.png`) : o.out;
|
|
@@ -31,4 +46,5 @@ for (const f of frames) {
|
|
|
31
46
|
console.log(out);
|
|
32
47
|
for (const e of f.errors) { console.error(`${f.at}s: ${e}`); failed = true; }
|
|
33
48
|
}
|
|
49
|
+
for (const t of frames.at(-1)?.tells || []) console.log(`told at ${t.at.toFixed(2)}s: ${t.text}${t.about ? ` (${t.about})` : ''}`);
|
|
34
50
|
process.exit(failed ? 1 : 0);
|
package/engine/engine.js
CHANGED
|
@@ -352,8 +352,9 @@ const Moood = (() => {
|
|
|
352
352
|
const STATE = { values: {}, ver: 0, was: key => STATE.values[key], sample: null };
|
|
353
353
|
// what the shown scene's code shares (kit.js): the world's modules it lists and the scene's lib
|
|
354
354
|
let SHARED = { kit: {}, lib: {} };
|
|
355
|
-
// Where an element is now, as its painting code says (e.at):
|
|
356
|
-
//
|
|
355
|
+
// Where an element is now, as its painting code says (e.at): the point it marks, in metres from
|
|
356
|
+
// its anchor as painted — speech bubbles and widgets follow it (stage.screenOf) wherever the
|
|
357
|
+
// element is put (its `place`)
|
|
357
358
|
const FOCUS = new Map();
|
|
358
359
|
// A platformer's hero as it moves now (camera.mode platform): its painting code reads e.hero —
|
|
359
360
|
// { vx, vy (m/s), ground } — to move its legs; the engine moves and turns the painting itself.
|
|
@@ -376,22 +377,19 @@ const Moood = (() => {
|
|
|
376
377
|
compiled.set(code, fn);
|
|
377
378
|
}
|
|
378
379
|
if (!fn) return;
|
|
379
|
-
|
|
380
|
-
// seen from the start (x 0…W/H, y 0…1, with margins); others in metres from their anchor
|
|
381
|
-
const flat = o.sky || o.ground;
|
|
382
|
-
const ppm = flat ? H : o.size * H / (o.height || 2), x = flat ? 0 : o.x * W, y = flat ? 0 : o.y * H;
|
|
380
|
+
const [x, y, ppm] = anchorOf(o, W, H);
|
|
383
381
|
ctx.save();
|
|
384
382
|
ctx.translate(x, y);
|
|
385
383
|
ctx.scale(ppm, ppm);
|
|
386
384
|
ctx.fillStyle = ctx.strokeStyle = o.color || '#000000';
|
|
387
385
|
ctx.lineCap = ctx.lineJoin = 'round';
|
|
388
386
|
const period = o.period || 2;
|
|
389
|
-
// a point the code names, as painted — through whatever it did to
|
|
390
|
-
// scale, rotate): [x, y, how much it is scaled]
|
|
387
|
+
// a point the code names, in its units from the anchor as painted — through whatever it did to
|
|
388
|
+
// the canvas since (translate, scale, rotate): [x, y, how much it is scaled]
|
|
391
389
|
const base = ctx.getTransform(), unbase = base.inverse();
|
|
392
390
|
const painted = (lx, ly) => {
|
|
393
391
|
const now = ctx.getTransform(), q = unbase.transformPoint(now.transformPoint({ x: lx, y: ly }));
|
|
394
|
-
return [
|
|
392
|
+
return [q.x, q.y, Math.hypot(now.a, now.b) / Math.hypot(base.a, base.b)];
|
|
395
393
|
};
|
|
396
394
|
const read = { time: false, state: false };
|
|
397
395
|
READS.set(o, read);
|
|
@@ -404,11 +402,11 @@ const Moood = (() => {
|
|
|
404
402
|
light(lx, ly, r, color = '#ffd08a', strength = 0.6) {
|
|
405
403
|
try {
|
|
406
404
|
const [px, py, k] = painted(lx, ly);
|
|
407
|
-
lightsOut.push({ x: px, y: py, r: r * k * ppm, col: rgb(color), a: strength, flicker: o.flicker ?? 0.3 });
|
|
405
|
+
lightsOut.push({ x: x + px * ppm, y: y + py * ppm, r: r * k * ppm, col: rgb(color), a: strength, flicker: o.flicker ?? 0.3 });
|
|
408
406
|
} catch (err) { reportError(o, err); }
|
|
409
407
|
},
|
|
410
408
|
// where this element is now (its head, say): what speaks or hovers over it follows this point
|
|
411
|
-
at(lx, ly) { if (o.id) { const [px, py] = painted(lx, ly); FOCUS.set(o.id, { x: px
|
|
409
|
+
at(lx, ly) { if (o.id) { const [px, py] = painted(lx, ly); FOCUS.set(o.id, { x: px, y: py }); } },
|
|
412
410
|
anim: MooodKit.anim, kit: SHARED.kit, lib: SHARED.lib,
|
|
413
411
|
was: (key, ago = 0) => { read.state = true; return STATE.was(key, ago); },
|
|
414
412
|
speed: key => { read.state = true; const a = STATE.was(key, 0), b = STATE.was(key, 0.05); return typeof a === 'number' && typeof b === 'number' ? (a - b) / 0.05 : 0; },
|
|
@@ -417,6 +415,12 @@ const Moood = (() => {
|
|
|
417
415
|
try { fn(ctx, e); } catch (err) { reportError(o, err); }
|
|
418
416
|
ctx.restore();
|
|
419
417
|
}
|
|
418
|
+
// Where a custom element's painting has its origin on the W × H stage, and its scale: [x, y, px a
|
|
419
|
+
// unit]. Sky and ground elements are painted in stage heights from the stage's top-left corner, as
|
|
420
|
+
// seen from the start (x 0…W/H, y 0…1, with margins); others in metres from their anchor.
|
|
421
|
+
function anchorOf(o, W, H) {
|
|
422
|
+
return o.sky || o.ground ? [0, 0, H] : [o.x * W, o.y * H, o.size * H / (o.height || 2)];
|
|
423
|
+
}
|
|
420
424
|
// The box an object's painting fits in (pixels of the W × H stage): built-in kinds a fixed frame
|
|
421
425
|
// around the base, custom ones their `box` in metres (default: a little wider than tall).
|
|
422
426
|
function objectBox(o, W, H) {
|
|
@@ -545,6 +549,15 @@ const Moood = (() => {
|
|
|
545
549
|
const land = (spec.layers || []).filter(L => LAND.has(L.kind));
|
|
546
550
|
return land.length ? Math.min(...land.map(L => L.y)) : 0.6;
|
|
547
551
|
}
|
|
552
|
+
// An element that moves through the world has `place`: its across, distance, elevation or x are the
|
|
553
|
+
// names of numbers in the state (metres) — it is where they say now, the rest as the spec has it.
|
|
554
|
+
const PLACE = ['across', 'distance', 'elevation', 'x'];
|
|
555
|
+
function placed(o) {
|
|
556
|
+
if (!o.place) return o;
|
|
557
|
+
const n = { ...o };
|
|
558
|
+
for (const f of PLACE) { const k = o.place[f]; if (typeof k === 'string' && typeof STATE.values[k] === 'number') n[f] = STATE.values[k]; }
|
|
559
|
+
return n;
|
|
560
|
+
}
|
|
548
561
|
function depths(spec) {
|
|
549
562
|
const layers = spec.layers || [];
|
|
550
563
|
// An object stands where its base is — unless it is attached to a bigger one (a window on
|
|
@@ -589,21 +602,27 @@ const Moood = (() => {
|
|
|
589
602
|
const paintZ = o => (o.plane ? planeZ(o) : custom(o) ? sightZ(o) : own(host(o) || o));
|
|
590
603
|
const objectX = o => (bearing(o) ? 0.5 : typeof o.across === 'number' && !o.sky ? 0.5 + o.across / EYE / STAGE / paintZ(o) : o.x ?? 0.5);
|
|
591
604
|
const objectY = o => (o.sky || o.ground ? 0 : custom(o) ? yh + (1 - (o.elevation || 0) / EYE) / sightZ(o) : o.kind === 'bird' ? o.y : o.y + shift(o));
|
|
605
|
+
const objectSize = o => (o.sky || o.ground ? 1 : custom(o) ? (o.height || 2) / EYE / sightZ(o) : o.size);
|
|
606
|
+
// where it stands in the world (eye heights: X to the right, Z ahead, Y its anchor's height) and
|
|
607
|
+
// the distance Zp it is painted for
|
|
608
|
+
const objectAt = o => {
|
|
609
|
+
const Zp = paintZ(o), Y = 1 - (objectY(o) - yh) * Zp;
|
|
610
|
+
if (!bearing(o)) return { X: (objectX(o) - 0.5) * STAGE * Zp, Y, Z: Zp, Zp };
|
|
611
|
+
const b = o.bearing * Math.PI / 180, d = o.distance / EYE;
|
|
612
|
+
return { X: d * Math.sin(b), Y, Z: d * Math.cos(b), Zp };
|
|
613
|
+
};
|
|
614
|
+
// ground planes are drawn right after the sky, under everything standing on them
|
|
615
|
+
// a horizontal plane (gl.js) is ordered by its far edge, before what stands on it
|
|
616
|
+
const objectZ = o => (o.sky ? Infinity : o.ground ? Z_FAR * 4 : o.plane ? clamp(planeZ(o) + (o.depth || 4) / 2 / EYE, Z_NEAR, Z_FAR) : custom(o) ? customZ(o) : o.kind === 'bird' ? 30 : own(host(o) || o));
|
|
592
617
|
return {
|
|
593
|
-
yh, Z, objectX,
|
|
594
|
-
// where it
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
return { X: d * Math.sin(b), Y, Z: d * Math.cos(b), Zp };
|
|
618
|
+
yh, Z, objectX, objectAt, objectZ,
|
|
619
|
+
// an element where it is now (its `place`), the one truth the renderers, the picking and
|
|
620
|
+
// whatever follows it go by: { o: as painted on the stage (x, y, size: fractions of it),
|
|
621
|
+
// Z: the depth it is drawn at, at: where it stands in the world }
|
|
622
|
+
now: o => {
|
|
623
|
+
const n = placed(o);
|
|
624
|
+
return { o: { ...o, x: objectX(n), y: objectY(n), size: objectSize(n), _yh: yh }, Z: objectZ(n), at: objectAt(n) };
|
|
601
625
|
},
|
|
602
|
-
// ground planes are drawn right after the sky, under everything standing on them
|
|
603
|
-
// a horizontal plane (gl.js) is ordered by its far edge, before what stands on it
|
|
604
|
-
objectZ: o => (o.sky ? Infinity : o.ground ? Z_FAR * 4 : o.plane ? clamp(planeZ(o) + (o.depth || 4) / 2 / EYE, Z_NEAR, Z_FAR) : custom(o) ? customZ(o) : o.kind === 'bird' ? 30 : own(host(o) || o)),
|
|
605
|
-
objectY,
|
|
606
|
-
objectSize: o => (o.sky || o.ground ? 1 : custom(o) ? (o.height || 2) / EYE / sightZ(o) : o.size),
|
|
607
626
|
};
|
|
608
627
|
}
|
|
609
628
|
// Screen transform [a b c d e f] for a plane at distance Z (Infinity = the sky): maps the
|
|
@@ -635,10 +654,11 @@ const Moood = (() => {
|
|
|
635
654
|
return { m: [s, 0, 0, s, q[0] - s * ax, q[1] - s * ay], D: q[2] };
|
|
636
655
|
}
|
|
637
656
|
// Where an element's painting (as depths() places it on the SW-wide stage) goes on the frame from
|
|
638
|
-
// camera cam: { m: the transform, D: its distance along the gaze }, or null behind the eye
|
|
657
|
+
// camera cam, where it is now: { m: the transform, D: its distance along the gaze }, or null behind the eye
|
|
639
658
|
function placeOf(o, cam, W, H, dz, round) {
|
|
640
|
-
|
|
641
|
-
|
|
659
|
+
const n = dz.now(o);
|
|
660
|
+
if (!round) return { m: view(cam, n.Z, W, H, dz.yh), D: n.Z - cam.z };
|
|
661
|
+
return billboard(cam, n.at, n.o.x * STAGE * H, n.o.y * H, W, H, dz.yh);
|
|
642
662
|
}
|
|
643
663
|
// The hero of a platformer h = { dx, dy (metres from where it is painted), flip } as a transform
|
|
644
664
|
// of its painting: its anchor painted at x = ax, `ppm` painted px per metre
|
|
@@ -779,9 +799,12 @@ const Moood = (() => {
|
|
|
779
799
|
if (L.kind === 'water') return { L, Z };
|
|
780
800
|
return { L, Z, ...staticLayer(L, SW, H, seed + i * 101) };
|
|
781
801
|
}).sort((a, b) => b.Z - a.Z);
|
|
782
|
-
// looking around, each also stands somewhere in the world (p, depths().objectAt)
|
|
783
|
-
|
|
784
|
-
const
|
|
802
|
+
// looking around, each also stands somewhere in the world (p, depths().objectAt); what moves
|
|
803
|
+
// through the world (`place`) is put where the state has it as each frame starts (move)
|
|
804
|
+
const round = camSetup(spec).round, farFirst = (a, b) => (b.Z - a.Z) || 0;
|
|
805
|
+
const at = o => { const n = dz.now(o); return { o: n.o, Z: n.Z, p: round && !o.sky && !o.ground ? n.at : null }; };
|
|
806
|
+
const objects = (spec.objects || []).map(o => ({ from: o, ...at(o) })).sort(farFirst), moving = objects.filter(ob => ob.from.place);
|
|
807
|
+
const move = () => { if (!moving.length) return; for (const ob of moving) Object.assign(ob, at(ob.from)); objects.sort(farFirst); };
|
|
785
808
|
const sky = !spec.sky ? '#000000' : (() => {
|
|
786
809
|
const c = document.createElement('canvas').getContext('2d');
|
|
787
810
|
const g = c.createLinearGradient(0, 0, 0, H);
|
|
@@ -795,7 +818,7 @@ const Moood = (() => {
|
|
|
795
818
|
v.addColorStop(0, 'rgba(0,0,0,0)');
|
|
796
819
|
v.addColorStop(1, `rgba(0,0,0,${0.75 * clamp(spec.light?.vignette ?? 0.5, 0, 1)})`);
|
|
797
820
|
vg.fillStyle = v; vg.fillRect(0, 0, W, H);
|
|
798
|
-
return { W, H, SW, u, x0, span, yh: dz.yh, round, lim: limits(spec, W, H), sky, stars, layers, particles, objects, vignette, scratch: layer(W, H) };
|
|
821
|
+
return { W, H, SW, u, x0, span, yh: dz.yh, round, lim: limits(spec, W, H), sky, stars, layers, particles, objects, move, vignette, scratch: layer(W, H) };
|
|
799
822
|
}
|
|
800
823
|
|
|
801
824
|
// Looking around, with Canvas 2D (the fallback): the sky turned as a flat band (not bent into a
|
|
@@ -852,6 +875,7 @@ const Moood = (() => {
|
|
|
852
875
|
// S.out: where the frame lies in the canvas (pixels); the scene goes on around it as painted
|
|
853
876
|
function frame(ctx, S, spec, cam, t, dt) {
|
|
854
877
|
const { W, H, SW, u } = S, out = S.out;
|
|
878
|
+
S.move();
|
|
855
879
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
856
880
|
ctx.fillStyle = '#000000';
|
|
857
881
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -1048,7 +1072,7 @@ const Moood = (() => {
|
|
|
1048
1072
|
const pick = (clientX, clientY) => {
|
|
1049
1073
|
const [cx, cy] = local(clientX, clientY), Hc = box.h, Wc = box.w, yh = S?.yh ?? 0.6, dz = spec ? depths(spec) : null;
|
|
1050
1074
|
const at = D => ({ Z: target.z + D, x: target.x + (cx - Wc / 2) / Hc * D });
|
|
1051
|
-
const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o =>
|
|
1075
|
+
const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o => dz.now(o))
|
|
1052
1076
|
.filter(({ Z }) => fadeNear(Z, target) > 0).sort((a, b) => a.Z - b.Z);
|
|
1053
1077
|
for (const { o, Z } of near) {
|
|
1054
1078
|
const m = view(target, Z, Wc, Hc, yh), b = objectBox(o, STAGE * Hc, Hc);
|
|
@@ -1114,32 +1138,31 @@ const Moood = (() => {
|
|
|
1114
1138
|
glide.vy += ((target.y - y0) / dt - glide.vy) * k;
|
|
1115
1139
|
lastMove = now;
|
|
1116
1140
|
};
|
|
1117
|
-
// where in the canvas (CSS px) an element is from camera c —
|
|
1118
|
-
// else its anchor (its top, with over) — lifted by `lift` metres:
|
|
1119
|
-
// when out of sight
|
|
1141
|
+
// where in the canvas (CSS px) an element is from camera c — where its `place` has it now: the
|
|
1142
|
+
// point its code marks (e.at), else its anchor (its top, with over) — lifted by `lift` metres:
|
|
1143
|
+
// [x, y, its distance]; null when out of sight
|
|
1120
1144
|
function seenFrom(c, id, lift = 0, over = false) {
|
|
1121
1145
|
const o = (spec?.objects || []).find(o => o.id === id);
|
|
1122
1146
|
if (!o || o.sky) return null;
|
|
1123
1147
|
const W = box.w, H = box.h, dz = depths(spec), yh = dz.yh;
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const q = project(c, p.X, p.Y + (lift + (over && !o.plane ? o.height ?? EYE : 0)) / EYE, p.Z, W, H, yh);
|
|
1148
|
+
if (o.plane) {
|
|
1149
|
+
const p = dz.objectAt(o), Z = p.Zp, up = lift / EYE;
|
|
1150
|
+
if (round()) {
|
|
1151
|
+
const q = project(c, p.X, p.Y + up, p.Z, W, H, yh);
|
|
1129
1152
|
return q && q[2] > 0.3 ? [box.x + q[0], box.y + q[1], q[2]] : null;
|
|
1130
1153
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
return [box.x +
|
|
1154
|
+
if (fadeNear(Z, c) < 0.3) return null;
|
|
1155
|
+
const m = view(c, Z, W, H, yh), px = dz.objectX(o) * STAGE * H, py = (yh + (1 - (o.elevation || 0) / EYE) / Z - up / Z) * H;
|
|
1156
|
+
return [box.x + m[0] * px + m[4], box.y + m[3] * py + m[5], Z - c.z];
|
|
1134
1157
|
}
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1158
|
+
const at = placeOf(o, c, W, H, dz, round());
|
|
1159
|
+
// behind the eye, or faded out as it is passed (fadeNear)
|
|
1160
|
+
if (!at || at.D < 0.48) return null;
|
|
1161
|
+
const n = dz.now(o), [ax, ay, ppm] = anchorOf(n.o, STAGE * H, H), f = FOCUS.get(id) || { x: 0, y: 0 };
|
|
1138
1162
|
if (over && !FOCUS.has(id)) lift += o.height ?? EYE;
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return [box.x + m[0] * px + m[4], box.y + m[3] * py + m[5], Z - c.z];
|
|
1163
|
+
let px = ax + f.x * ppm, py = ay + f.y * ppm - lift / EYE * H / n.at.Zp;
|
|
1164
|
+
if (c.hero && id === c.hero.id) { const hm = heroMatrix(c.hero, ax, ppm); px = hm[0] * px + hm[4]; py += hm[5]; }
|
|
1165
|
+
return [box.x + at.m[0] * px + at.m[4], box.y + at.m[3] * py + at.m[5], at.D];
|
|
1143
1166
|
}
|
|
1144
1167
|
// the camera keeps the elements it follows in the visible part of the screen: together when
|
|
1145
1168
|
// they fit, else the first of them
|
package/engine/engine.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"engine":"
|
|
1
|
+
{"engine":"ff145eec9cf9"}
|
package/engine/gl.js
CHANGED
|
@@ -453,7 +453,7 @@ const MooodGL = (() => {
|
|
|
453
453
|
const objects = (spec.objects || []).map(o0 => {
|
|
454
454
|
if (o0.plane) return planeOf(o0, dz, tex);
|
|
455
455
|
// looking around, the sky is the full circle, 2π stage heights wide (engine.js)
|
|
456
|
-
const
|
|
456
|
+
const n = dz.now(o0), o = { ...n.o, _sw: ROUND && o0.sky ? I.TAU * H : SW }, Z = n.Z;
|
|
457
457
|
const x = o.x * SW, y = o.y * H, s = Math.max(2, o.size * H);
|
|
458
458
|
if (o.kind === 'bird') {
|
|
459
459
|
const canvas = paint(s * 1.2 * SS, s * 0.6 * SS, (g) => {
|
|
@@ -472,7 +472,7 @@ const MooodGL = (() => {
|
|
|
472
472
|
const live = (o.kind === 'custom' || !o.kind) ? liveOf(o, o.frames, tex) : null;
|
|
473
473
|
return {
|
|
474
474
|
o, Z, x, y, s, bx, by, bw, bh, tex: frames[0], frames, period: o.period || 2, detail: null,
|
|
475
|
-
p: ROUND && !o.sky ?
|
|
475
|
+
p: ROUND && !o.sky ? n.at : null,
|
|
476
476
|
live, ground: !!o.ground, from: o0.place ? o0 : null,
|
|
477
477
|
reflect: o.reflect ?? (o.kind === 'boat' || o.kind === 'bridge'),
|
|
478
478
|
lights: lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, fire: o.kind === 'campfire', flicker: l.flicker })),
|
|
@@ -873,22 +873,18 @@ const MooodGL = (() => {
|
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
-
// An element that moves through the world (its `place
|
|
877
|
-
//
|
|
878
|
-
// its haze and what it is drawn in front of or behind follow.
|
|
876
|
+
// An element that moves through the world (its `place`): put where the state has it now
|
|
877
|
+
// (depths().now) — its size, its depth, its haze and what it is drawn in front of or behind follow.
|
|
879
878
|
function placeMoving(S) {
|
|
880
|
-
const v = I.STATE.values;
|
|
881
879
|
let moved = false;
|
|
882
880
|
for (const ob of S.objects) {
|
|
883
881
|
if (!ob.from) continue;
|
|
884
|
-
const
|
|
885
|
-
for (const [f, k] of Object.entries(ob.from.place)) if (typeof k === 'string' && typeof v[k] === 'number') o1[f] = v[k];
|
|
886
|
-
const key = `${o1.x}|${o1.across}|${o1.distance}|${o1.elevation}`;
|
|
882
|
+
const n = S.dz.now(ob.from), key = `${n.o.x}|${n.o.y}|${n.o.size}`;
|
|
887
883
|
if (key === ob.placed) continue;
|
|
888
884
|
ob.placed = key; moved = true;
|
|
889
|
-
const o = { ...
|
|
890
|
-
Object.assign(ob, { o, Z:
|
|
891
|
-
if (ROUND && !o.sky) ob.p =
|
|
885
|
+
const o = { ...n.o, _sw: ob.o._sw };
|
|
886
|
+
Object.assign(ob, { o, Z: n.Z, x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H) }, I.objectBox(o, SW, H));
|
|
887
|
+
if (ROUND && !o.sky) ob.p = n.at;
|
|
892
888
|
}
|
|
893
889
|
if (!moved) return;
|
|
894
890
|
for (const item of S.order) if (item.ob) item.Z = item.ob.Z;
|
|
@@ -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>
|
package/engine/motion.js
ADDED
|
@@ -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
|
+
})();
|
package/engine/render.html
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
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
|
|
15
|
+
window.__mooodRenderErrors, what its scripts told (scene.tell) in window.__mooodTells ({at:
|
|
16
|
+
seconds since it started, text, about}); the title turns "ready" when it is time to take the
|
|
17
|
+
picture. -->
|
|
16
18
|
<canvas id="scene"></canvas>
|
|
17
19
|
<header class="titles"><h1 id="title"></h1><p id="caption"></p></header>
|
|
18
20
|
<script src="kit.js"></script>
|
|
@@ -34,8 +36,10 @@
|
|
|
34
36
|
}
|
|
35
37
|
const modules = { texts: window.__mooodModules || {} }; // the modules the scene lists, given (kit.js)
|
|
36
38
|
const canvas = document.getElementById('scene'), stage = Moood.mount(canvas);
|
|
39
|
+
// the starting state, scripts running (their errors are reported) — before the scene is shown, so its first frame is as they set it
|
|
40
|
+
const t0 = performance.now(), told = window.__mooodTells = [];
|
|
41
|
+
MooodState.start(spec, {}, { stage, modules, tell: (text, about) => told.push({ at: (performance.now() - t0) / 1000, text, about }) });
|
|
37
42
|
stage.show(spec, modules);
|
|
38
|
-
MooodState.start(spec, {}, { stage, modules }); // the starting state, scripts running (their errors are reported)
|
|
39
43
|
if (!bare) MooodWidgets.create({ canvas, stage }).show({ ...spec, widgets: (spec.widgets || []).map(w => ({ ...w, at: 0, for: 0 })) });
|
|
40
44
|
// show every caption at once, so the reviewer reads them all
|
|
41
45
|
if (!bare) MooodCaptions.show((spec.captions || []).map(c => ({ ...c, at: 0, for: 0 })));
|
package/engine/scenestate.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// widgets — MooodState is global: get / set / on.
|
|
8
8
|
// The painting code reads the current values as e.state and is repainted when they change. A
|
|
9
9
|
// number may be set to move there over some seconds, along an easing curve (kit.js: smooth by
|
|
10
|
-
// default — it starts and stops gently; linear, in, out, back, spring, anticipate…).
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// default — it starts and stops gently; linear, in, out, back, spring, anticipate…). A value set
|
|
11
|
+
// is heard once, where it is set to (on('change')); a value moving there is heard each frame as it
|
|
12
|
+
// moves (on('move')). What a value was a moment ago stays known for a while (was): what lags
|
|
13
|
+
// behind — a coat's tail, hair, a head that follows — reads it. A script may tell that something
|
|
14
|
+
// happened (scene.tell): the game decides what it means.
|
|
14
15
|
const MooodState = (() => {
|
|
15
16
|
let stage = null, tell = null, save = null, t0 = performance.now();
|
|
16
17
|
let values = {}, cur = {}, tweens = new Map(), listeners = [], timers = [], raf = 0, pending = {}, flushTimer = 0;
|
|
@@ -44,12 +45,14 @@ const MooodState = (() => {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function publish() { cur = { ...values }; for (const [k, tw] of tweens) cur[k] = along(tw, performance.now()); stage?.setState(cur, was, sample); }
|
|
47
|
-
// the moving values, where they are now (the stage asks as it paints a frame)
|
|
48
|
+
// the moving values, where they are now (the stage asks as it paints a frame): each heard moving
|
|
49
|
+
// from where it was last seen
|
|
48
50
|
function sample() {
|
|
49
51
|
if (!tweens.size) return;
|
|
50
|
-
const now = performance.now();
|
|
52
|
+
const now = performance.now(), before = cur, moving = [...tweens.keys()];
|
|
51
53
|
for (const [k, tw] of tweens) if (now >= tw.end) tweens.delete(k);
|
|
52
54
|
publish();
|
|
55
|
+
for (const k of moving) if (cur[k] !== before[k]) emit('move', k, cur[k], before[k], k);
|
|
53
56
|
}
|
|
54
57
|
function tick() {
|
|
55
58
|
raf = 0;
|
|
@@ -167,7 +170,9 @@ const MooodState = (() => {
|
|
|
167
170
|
}
|
|
168
171
|
return end;
|
|
169
172
|
},
|
|
170
|
-
// on('change', fn(value, old, key)) · on('change', key, fn)
|
|
173
|
+
// on('change', fn(value, old, key)) · on('change', key, fn) — a value set (with `over`: where it
|
|
174
|
+
// goes, once); on('move', [key,] fn(value, last, key)) — a value moving, each frame (set with
|
|
175
|
+
// `over`, path), till it stops; on('load', fn(state))
|
|
171
176
|
on(ev, a, b) {
|
|
172
177
|
const fn = typeof a === 'function' ? a : b, key = typeof a === 'function' ? null : String(a);
|
|
173
178
|
listeners.push({ ev, key, fn, who });
|
package/engine/select.js
CHANGED
|
@@ -51,12 +51,12 @@ const MooodSelect = (() => {
|
|
|
51
51
|
// so a plane's view() maps painted coordinates straight to where they are in the frame;
|
|
52
52
|
// the frame lies at (x, y) in the canvas.
|
|
53
53
|
const size = () => { const f = stage.frame(); return { W: f.w, H: f.h, SW: I.STAGE * f.h, x: f.x, y: f.y }; };
|
|
54
|
-
// objects stand where the engine puts them (
|
|
55
|
-
const placed = (dz, o) =>
|
|
54
|
+
// objects stand where the engine puts them now (its depths().now: their real distance, their `place`)
|
|
55
|
+
const placed = (dz, o) => dz.now(o).o;
|
|
56
56
|
// a platformer's hero is drawn moved and turned (engine.js heroMatrix): so is its outline
|
|
57
57
|
function heroed(m, o, cam, SW, H) {
|
|
58
58
|
if (!o || !cam.hero || o.id !== cam.hero.id) return m;
|
|
59
|
-
const
|
|
59
|
+
const p = placed(I.depths(spec), o), h = I.heroMatrix(cam.hero, p.x * SW, p.size * H / (o.height || 2));
|
|
60
60
|
return [m[0] * h[0], 0, 0, m[3], m[0] * h[4] + m[4], m[3] * h[5] + m[5]];
|
|
61
61
|
}
|
|
62
62
|
const layerSeed = i => I.hash(JSON.stringify(spec.layers || []) + (spec.title || '')) + i * 101;
|
|
@@ -125,7 +125,7 @@ const MooodSelect = (() => {
|
|
|
125
125
|
const c = I.layer(b.bw * k, b.bh * k), g = c.getContext('2d');
|
|
126
126
|
g.setTransform(k, 0, 0, k, -b.bx * k, -b.by * k);
|
|
127
127
|
I.drawObject(g, o, SW, H, 0, []);
|
|
128
|
-
return { c, x: b.bx, y: b.by, k, Z: dz.
|
|
128
|
+
return { c, x: b.bx, y: b.by, k, Z: dz.now(it).Z, obj: it };
|
|
129
129
|
}
|
|
130
130
|
if (sel.startsWith('layers') && LAND.has(it.kind)) {
|
|
131
131
|
if (it.kind === 'water') {
|
|
@@ -239,7 +239,7 @@ const MooodSelect = (() => {
|
|
|
239
239
|
if (!coverage.has(path)) {
|
|
240
240
|
const { W, H, SW } = size(), dz = I.depths(spec), it = item(spec, path), n = 64, c = I.layer(n, n * H / W), g = c.getContext('2d');
|
|
241
241
|
// from the start — looking around, turned toward it
|
|
242
|
-
const round = I.camSetup(spec).round, p = round && !it.sky ? dz.
|
|
242
|
+
const round = I.camSetup(spec).round, p = round && !it.sky ? dz.now(it).at : null;
|
|
243
243
|
const at = I.placeOf(it, { x: 0, y: 0, z: 0, h: 1, yaw: p ? Math.atan2(p.X, p.Z) : 0 }, W, H, dz, round && !it.sky), k = n / W;
|
|
244
244
|
const m = at ? at.m : [0, 0, 0, 0, 0, 0];
|
|
245
245
|
g.setTransform(m[0] * k, 0, 0, m[3] * k, m[4] * k, m[5] * k);
|
package/engine/soma.bin
ADDED
|
Binary file
|
package/engine/soma.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
`soma.bin` — the SOMA body: NVIDIA Kimodo's skin of its 77-joint skeleton
|
|
2
|
+
(https://github.com/nv-tlabs/kimodo, `kimodo/assets/skeletons/somaskel77/skin_standard.npz`; Copyright (c)
|
|
3
|
+
2026 NVIDIA CORPORATION & AFFILIATES, Apache License 2.0). Made by `dev/mocap/soma_body.py`; `motion.js`
|
|
4
|
+
draws the motions Kimodo made on it (the sandbox, the render).
|
package/index.js
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
// engine/engine.json) in a Chromium already installed (playwright-core: nothing is downloaded) — no
|
|
3
3
|
// network, no site. Time is a virtual clock, stepped from the scene's start: a frame at a moment is
|
|
4
4
|
// the same every time, with the same state.
|
|
5
|
-
// render({spec, modules, device, at, state, bare, chromium, date}) → [{at, png, errors}]
|
|
5
|
+
// render({spec, modules, device, at, state, set, bare, chromium, date}) → [{at, png, errors, tells}]
|
|
6
6
|
// spec — the scene (readScene(folder) from a world's repository, or any spec);
|
|
7
7
|
// modules — the modules it lists, themselves: {name: {code, data}} (readModules(world, spec));
|
|
8
8
|
// device — the frame: 16:9 (1600×900), 4:3, 21:9 (the same height), tile (640×360), big (1920×1080);
|
|
9
9
|
// at — the moments, seconds from the scene's start (default [0]): a frame each;
|
|
10
10
|
// state — values over the spec's starting state ({"dawn": 1, "girl": "gone"});
|
|
11
|
+
// set — values set as the scene goes, as a game sets them: [[second, key, value, over?, ease?], …]
|
|
12
|
+
// (a film's beat at its second: [[4, "beat", "cover"]]);
|
|
11
13
|
// bare — without the title and the captions over it;
|
|
12
14
|
// chromium — the browser's executable (else MOOOD_CHROMIUM, else Playwright's own);
|
|
13
15
|
// date — what the clock says at the start (default 2026-01-01T12:00:00Z).
|
|
16
|
+
// Each frame: its errors, and what the scene's scripts told so far (scene.tell): [{at, text, about}].
|
|
17
|
+
// renderMotion({motion, at, views, kit, cell, chromium}) → {png, errors}
|
|
18
|
+
// a person's motion made by a model (moood's make_motion: list_motions(key, body=true) — its `body` is
|
|
19
|
+
// the motion, its `clip` the people module's entry) as frames on its body (engine/motion.html): a row
|
|
20
|
+
// per view (front, side, back, or {yaw, pitch, dist}), a column per moment (at, seconds; default eight
|
|
21
|
+
// across it); kit — the module's clip, its skeleton drawn over the body; cell — [w, h] of a frame.
|
|
14
22
|
import { readFileSync } from 'node:fs';
|
|
15
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
24
|
import { chromium as browserType } from 'playwright-core';
|
|
@@ -18,20 +26,21 @@ import { chromium as browserType } from 'playwright-core';
|
|
|
18
26
|
export { readModules, readScene } from './scene.js';
|
|
19
27
|
|
|
20
28
|
const PAGE = pathToFileURL(fileURLToPath(new URL('./engine/render.html', import.meta.url))).href;
|
|
29
|
+
const MOTION = pathToFileURL(fileURLToPath(new URL('./engine/motion.html', import.meta.url))).href;
|
|
21
30
|
export const ENGINE = JSON.parse(readFileSync(new URL('./engine/engine.json', import.meta.url), 'utf8')).engine;
|
|
22
31
|
export const DEVICES = { '16:9': [1600, 900], '4:3': [1200, 900], '21:9': [2100, 900], tile: [640, 360], big: [1920, 1080] };
|
|
23
32
|
const STEP = 100; // ms: timers fire as they come due, not all at the end of one jump
|
|
33
|
+
// WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
|
|
34
|
+
const ARGS = ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
|
|
35
|
+
'--allow-file-access-from-files'];
|
|
24
36
|
|
|
25
|
-
export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, bare = false, chromium, date } = {}) {
|
|
37
|
+
export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, set = [], bare = false, chromium, date } = {}) {
|
|
26
38
|
const size = DEVICES[device];
|
|
27
39
|
if (!size) throw new Error(`device: one of ${Object.keys(DEVICES).join(', ')}`);
|
|
28
40
|
if (!spec || typeof spec !== 'object') throw new Error('spec: a scene expected');
|
|
29
41
|
const shown = { ...spec, state: { ...(spec.state && typeof spec.state === 'object' ? spec.state : {}), ...state } };
|
|
30
42
|
const browser = await browserType.launch({
|
|
31
|
-
executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined,
|
|
32
|
-
// WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
|
|
33
|
-
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
|
|
34
|
-
'--allow-file-access-from-files'],
|
|
43
|
+
executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined, args: ARGS,
|
|
35
44
|
});
|
|
36
45
|
try {
|
|
37
46
|
const page = await (await browser.newContext({ viewport: { width: size[0], height: size[1] } })).newPage();
|
|
@@ -51,20 +60,48 @@ export async function render({ spec, modules = {}, device = '16:9', at = [0], st
|
|
|
51
60
|
// what loads by itself (fonts, the scene's modules) has loaded before the clock moves at all
|
|
52
61
|
await page.evaluate(() => document.fonts.ready);
|
|
53
62
|
await page.waitForTimeout(1000);
|
|
54
|
-
const frames = [];
|
|
63
|
+
const frames = [], sets = [...set].map(([s, ...rest]) => [Number(s) * 1000, ...rest]).sort((a, b) => a[0] - b[0]);
|
|
55
64
|
let now = 0;
|
|
65
|
+
// the values due by now, set (the clock stops at each one's second: it is set on time)
|
|
66
|
+
const due = async () => {
|
|
67
|
+
while (sets.length && sets[0][0] <= now) {
|
|
68
|
+
const [, key, value, over, ease] = sets.shift();
|
|
69
|
+
await page.evaluate(([k, v, o, e]) => MooodState.set(k, v, o, e), [key, value, over, ease]);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
await due();
|
|
56
73
|
for (const t of [...at].map(Number).sort((a, b) => a - b)) {
|
|
57
74
|
while (now < t * 1000) {
|
|
58
|
-
const step = Math.min(STEP, t * 1000 - now);
|
|
75
|
+
const step = Math.min(STEP, t * 1000 - now, sets.length ? sets[0][0] - now : STEP); // (what is due by now is set)
|
|
59
76
|
await page.clock.runFor(step);
|
|
60
77
|
now += step;
|
|
78
|
+
await due();
|
|
61
79
|
}
|
|
62
80
|
await page.clock.runFor(0);
|
|
63
|
-
const said = await page.evaluate(() => window.__mooodErrors || []);
|
|
64
|
-
frames.push({ at: t, png: await page.screenshot({ type: 'png' }), errors: [...new Set([...errors, ...said])].slice(0, 100) });
|
|
81
|
+
const [said, tells] = await page.evaluate(() => [window.__mooodErrors || [], window.__mooodTells || []]);
|
|
82
|
+
frames.push({ at: t, png: await page.screenshot({ type: 'png' }), errors: [...new Set([...errors, ...said])].slice(0, 100), tells });
|
|
65
83
|
}
|
|
66
84
|
return frames;
|
|
67
85
|
} finally {
|
|
68
86
|
await browser.close();
|
|
69
87
|
}
|
|
70
88
|
}
|
|
89
|
+
|
|
90
|
+
export async function renderMotion({ motion, at, views = ['front', 'side'], kit = null, cell = [320, 400], chromium } = {}) {
|
|
91
|
+
if (!motion || !Array.isArray(motion.names)) throw new Error('motion: the body of a made motion expected (list_motions(key, body=true).body)');
|
|
92
|
+
const length = (motion.n - 1) / motion.fps;
|
|
93
|
+
const moments = at && at.length ? at.map(s => Math.min(Math.max(0, Number(s)), length)) : Array.from({ length: 8 }, (_, i) => +(length * i / 7).toFixed(2));
|
|
94
|
+
const browser = await browserType.launch({ executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined, args: ARGS });
|
|
95
|
+
try {
|
|
96
|
+
const page = await (await browser.newContext({ viewport: { width: cell[0] * moments.length, height: cell[1] * views.length } })).newPage();
|
|
97
|
+
const errors = [];
|
|
98
|
+
page.on('pageerror', e => errors.push(String(e.message)));
|
|
99
|
+
await page.addInitScript(([m, s]) => { window.__mooodMotion = m; window.__mooodMotionShow = s; }, [motion, { at: moments, views, kit, cell }]);
|
|
100
|
+
await page.goto(MOTION);
|
|
101
|
+
await page.waitForFunction(() => document.title === 'ready', null, { timeout: 120_000 });
|
|
102
|
+
errors.push(...await page.evaluate(() => window.__mooodRenderErrors || []));
|
|
103
|
+
return { png: await page.screenshot({ type: 'png' }), errors: [...new Set(errors)] };
|
|
104
|
+
} finally {
|
|
105
|
+
await browser.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moood/render",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A moood scene rendered locally — the site's own engine, in a browser already installed — at a moment, with a state: a PNG and the errors its code threw.",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "A moood scene rendered locally — the site's own engine, in a browser already installed — at a moment, with a state: a PNG and the errors its code threw; and a person's motion made by a model, as frames on its body.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|