@chromatic-coherence/generative-engine 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,9 +135,22 @@ batch, GPU flocks included), clipped at the plane; the surface is a wave-displac
135
135
  sampling that reflection in screen space with a fresnel blend. The table fills wherever
136
136
  the ground dips below `level` — carve a basin and it becomes a lake that reflects the sky.
137
137
 
138
+ ## Skeletal figures (v1.6)
139
+
140
+ Bones, grown from code like everything else — no asset pipeline. `humanoid()` and
141
+ `quadruped()` build rigged figures (procedural segments bound to a small skeleton);
142
+ `w.figure(rig, opts)` uploads one and returns a handle; `fig.pose(walkPose(t))` bends it
143
+ in the vertex stage — scene, shadows and water reflections alike. `walkPose` / `trotPose`
144
+ ship as procedural cycles; `composePose` takes any per-joint rotations you write.
145
+
146
+ ## Fly controls (v1.7)
147
+
148
+ `controls: "fly"` — free flight: drag to look, wheel to fly along your gaze, W/A/S/D to
149
+ move (shift for speed), `w.flySpeed` to tune. The pitch clamp opens to near-vertical.
150
+
138
151
  ## Roadmap
139
152
 
140
- Skeletal figures. The chart editions stay lean.
153
+ The engine's founding frontiers are landed. What comes next, the scenes will ask for. The chart editions stay lean.
141
154
 
142
155
  ## Licence
143
156
 
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./math.js";
2
2
  export * from "./geometry.js";
3
3
  export * from "./world.js";
4
4
  export * from "./objectives.js";
5
+ export * from "./skeletal.js";
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export * from "./math.js";
5
5
  export * from "./geometry.js";
6
6
  export * from "./world.js";
7
7
  export * from "./objectives.js";
8
+ export * from "./skeletal.js";
package/dist/shaders.d.ts CHANGED
@@ -2,6 +2,8 @@ export declare const FACE_VS = "#version 300 es\nin vec3 aPos; in vec3 aNorm; in
2
2
  export declare const FACE_FS = "#version 300 es\nprecision highp float;\nin vec3 vPos; in vec3 vNorm; in vec4 vCol; in float vZ; in vec4 vSh;\nin float vGA; in float vBlood;\nuniform vec3 uLdir; uniform float uAmb; uniform float uDif; uniform float uSigned;\nuniform vec3 uLp[8]; uniform float uLi[8]; uniform float uLf[8]; uniform int uLn;\nuniform vec3 uCam; uniform float uFogK; uniform float uSpec; uniform vec3 uBg;\nuniform vec3 uSkyC; uniform vec3 uGroundC; uniform float uPulse;\nuniform float uShadowOn; uniform float uShadowSoft; uniform float uShadowBias;\nuniform lowp sampler2DShadow uShadow; uniform float uShadowPx;\nuniform float uClipY;\nout vec4 fragColor;\nfloat h3(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }\nfloat vnoise(vec3 p){ vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(h3(i), h3(i + vec3(1,0,0)), f.x), mix(h3(i + vec3(0,1,0)), h3(i + vec3(1,1,0)), f.x), f.y),\n mix(mix(h3(i + vec3(0,0,1)), h3(i + vec3(1,0,1)), f.x), mix(h3(i + vec3(0,1,1)), h3(i + vec3(1,1,1)), f.x), f.y), f.z); }\nfloat shadowFactor(){\n vec3 pr = vSh.xyz / vSh.w * 0.5 + 0.5;\n if (pr.x < 0.0 || pr.x > 1.0 || pr.y < 0.0 || pr.y > 1.0 || pr.z > 1.0) return 1.0;\n float ref = pr.z - uShadowBias;\n // 3x3 taps over a hardware-compared (LINEAR sampler2DShadow => free 2x2 PCF each) map\n float s = 0.0;\n for (int sy = -1; sy <= 1; sy++)\n for (int sx = -1; sx <= 1; sx++)\n s += texture(uShadow, vec3(pr.xy + vec2(float(sx), float(sy)) * uShadowPx * uShadowSoft, ref));\n s /= 9.0;\n return 0.35 + 0.65 * s;\n}\nvoid main(){\n if (vPos.y < uClipY) discard; // W11: the reflection pass clips at the water plane\n vec3 n = normalize(vNorm);\n vec3 V = normalize(uCam - vPos);\n float skin = clamp(vBlood * 5.0, 0.0, 1.0);\n { // procedural micro-relief: perturb the shading normal by a fine noise gradient\n float bs = 5.5, e = 0.12, n0 = vnoise(vPos * bs);\n vec3 g = vec3(vnoise(vPos * bs + vec3(e, 0.0, 0.0)) - n0,\n vnoise(vPos * bs + vec3(0.0, e, 0.0)) - n0,\n vnoise(vPos * bs + vec3(0.0, 0.0, e)) - n0) / e;\n n = normalize(n - g * (0.10 + 0.30 * skin));\n }\n float d = dot(n, normalize(uLdir));\n float shade = uAmb + uDif * mix(abs(d), max(d, 0.0), uSigned);\n float spec = 0.0;\n float sh = uShadowOn > 0.5 ? shadowFactor() : 1.0;\n for (int i = 0; i < 8; i++) { if (i >= uLn) break;\n vec3 v = uLp[i] - vPos; float d2 = dot(v, v); vec3 L = v * inversesqrt(d2 + 1e-4);\n float lam = dot(n, L); lam = mix(abs(lam), max(lam, 0.0), uSigned);\n float fall = 1.0 / (1.0 + d2 / (uLf[i] * uLf[i]));\n float k = (i == 0) ? sh : 1.0;\n shade += uLi[i] * lam * fall * k;\n // GGX microfacet + Fresnel \u2014 roughness from the face's gloss (colour alpha slot)\n vec3 H = normalize(L + V);\n float nh = max(dot(n, H), 0.0);\n float rough = clamp(1.0 - vCol.a, 0.08, 1.0), a2 = rough * rough; a2 = a2 * a2;\n float dnm = nh * nh * (a2 - 1.0) + 1.0;\n float ggx = a2 / (3.14159 * dnm * dnm);\n float fres = 0.04 + 0.96 * pow(1.0 - max(dot(H, V), 0.0), 5.0);\n spec += uLi[i] * min(ggx, 6.0) * fres * fall * k * 0.16;\n }\n // coloured hemispheric ambient: cool from above, warm from below\n vec3 tint = mix(uGroundC, uSkyC, n.y * 0.5 + 0.5);\n vec3 lit = vCol.rgb * (uAmb * tint + vec3(shade - uAmb));\n // the perfusion term: blood near the surface changes how it meets the light\n float b = clamp(vBlood + uPulse * vBlood * 6.0, 0.0, 1.0);\n lit *= mix(vec3(1.0), vec3(1.07, 0.95, 0.94), b);\n lit += vec3(0.62, 0.06, 0.08) * (b * 0.20 * (1.0 - abs(d)));\n // pore noise, only where there is blood (skin, not cloth)\n float pore = vnoise(vPos * 2.4) * 0.6 + vnoise(vPos * 6.0) * 0.4;\n lit *= 1.0 + (pore - 0.5) * 0.09 * skin;\n // subsurface rim: grazing light bleeds through thin tissue, warm\n float fres = pow(1.0 - clamp(dot(n, V), 0.0, 1.0), 3.0);\n lit += vec3(0.55, 0.09, 0.10) * fres * skin * (0.28 + 0.5 * b);\n float fog = clamp(uFogK / vZ, 0.08, 1.0);\n vec3 outC = lit + vec3(1.0) * spec * uSpec * vCol.a;\n // fog and fades resolve toward the REAL background colour (the engine owns its ground)\n fragColor = vec4(mix(uBg, outC, fog * vGA), 1.0);\n}";
3
3
  export declare const FACE_INST_VS = "#version 300 es\nin vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;\nin vec3 aPos2; in vec3 aNorm2;\nin vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3; in vec4 aIP;\nuniform mat4 uVP; uniform mat4 uSVP; uniform float uT; uniform vec3 uWindDir;\nuniform float uFlight;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;\nout float vGA; out float vBlood;\n// W10 FLIGHT: aIP = (a0, r0, ph, lobe). The flock's heart wanders, three lobes orbit\n// it breathing apart and back, each bird orbits its lobe with a ripple phase \u2014 the\n// murmuration, evaluated per vertex from uT alone. The instance matrix is the flock\n// frame (shared), so one batch = one flock you can place, scale and steer.\nvec3 flightPos(vec4 ip, float t){\n float a0 = ip.x, r0 = ip.y, ph = ip.z, lobe = ip.w;\n float cx = sin(t * 0.11) * 700.0, cz = -500.0 + cos(t * 0.07) * 380.0;\n float cy = 430.0 + sin(t * 0.16) * 110.0;\n float spread = 180.0 + 140.0 * sin(t * 0.21 + 1.3);\n float la = t * 0.34 + lobe * 2.0944;\n vec3 lc = vec3(cx + cos(la) * spread, cy + sin(la * 2.0 + lobe) * 60.0, cz + sin(la) * spread * 0.7);\n float aa = a0 + t * (0.5 + 0.25 * sin(ph));\n float wob = sin(t * 1.1 + ph) * 0.35;\n float rr = r0 * (0.75 + 0.25 * sin(t * 0.5 + ph + lobe));\n return lc + vec3(cos(aa) * rr, sin(aa * 1.7 + ph) * rr * 0.32 + wob * 24.0, sin(aa) * rr * (0.55 + wob * 0.2));\n}\nvec3 flightLocal(vec3 pl, vec4 ip, float t){\n float aa = ip.x + t * (0.5 + 0.25 * sin(ip.z));\n pl.y += sin(t * 11.0 + ip.z * 3.0) * abs(pl.z) * 0.9; // the flap, growing to the wingtips\n float ca = cos(aa + 1.5708), sa = sin(aa + 1.5708); // heading = the orbit tangent\n return vec3(pl.x * ca - pl.z * sa, pl.y, pl.x * sa + pl.z * ca);\n}\nvoid main(){\n int g = int(aGrp + 0.5);\n float w = uMW[g];\n mat4 inst = mat4(aI0, aI1, aI2, aI3);\n vec3 p = mix(aPos, aPos2, w), n = mix(aNorm, aNorm2, w);\n if (uFlight > 0.5) {\n p = flightPos(aIP, uT) + flightLocal(p, aIP, uT);\n } else {\n // W9 GPU WIND: per-instance (phase, freq, amp); displacement grows with height,\n // plus a second harmonic so the sway breathes instead of ticking\n float sw = sin(uT * aIP.y + aIP.x) + 0.5 * sin(uT * aIP.y * 2.3 + aIP.x * 1.7);\n p += uWindDir * (sw * aIP.z * max(p.y, 0.0));\n }\n vec4 wp = uGM[g] * (inst * vec4(p, 1.0));\n vPos = wp.xyz; vNorm = uNM[g] * (mat3(inst) * n); vCol = aCol; vGA = uGA[g]; vBlood = aBlood;\n vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;\n}";
4
4
  export declare const DEPTH_INST_VS = "#version 300 es\nin vec3 aPos; in float aGrp; in vec3 aPos2;\nin vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3; in vec4 aIP;\nuniform mat4 uVP; uniform float uT; uniform vec3 uWindDir;\nuniform float uFlight;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\n// W10 FLIGHT: aIP = (a0, r0, ph, lobe). The flock's heart wanders, three lobes orbit\n// it breathing apart and back, each bird orbits its lobe with a ripple phase \u2014 the\n// murmuration, evaluated per vertex from uT alone. The instance matrix is the flock\n// frame (shared), so one batch = one flock you can place, scale and steer.\nvec3 flightPos(vec4 ip, float t){\n float a0 = ip.x, r0 = ip.y, ph = ip.z, lobe = ip.w;\n float cx = sin(t * 0.11) * 700.0, cz = -500.0 + cos(t * 0.07) * 380.0;\n float cy = 430.0 + sin(t * 0.16) * 110.0;\n float spread = 180.0 + 140.0 * sin(t * 0.21 + 1.3);\n float la = t * 0.34 + lobe * 2.0944;\n vec3 lc = vec3(cx + cos(la) * spread, cy + sin(la * 2.0 + lobe) * 60.0, cz + sin(la) * spread * 0.7);\n float aa = a0 + t * (0.5 + 0.25 * sin(ph));\n float wob = sin(t * 1.1 + ph) * 0.35;\n float rr = r0 * (0.75 + 0.25 * sin(t * 0.5 + ph + lobe));\n return lc + vec3(cos(aa) * rr, sin(aa * 1.7 + ph) * rr * 0.32 + wob * 24.0, sin(aa) * rr * (0.55 + wob * 0.2));\n}\nvec3 flightLocal(vec3 pl, vec4 ip, float t){\n float aa = ip.x + t * (0.5 + 0.25 * sin(ip.z));\n pl.y += sin(t * 11.0 + ip.z * 3.0) * abs(pl.z) * 0.9; // the flap, growing to the wingtips\n float ca = cos(aa + 1.5708), sa = sin(aa + 1.5708); // heading = the orbit tangent\n return vec3(pl.x * ca - pl.z * sa, pl.y, pl.x * sa + pl.z * ca);\n}\nvoid main(){\n int g = int(aGrp + 0.5);\n vec3 p = mix(aPos, aPos2, uMW[g]);\n if (uFlight > 0.5) {\n p = flightPos(aIP, uT) + flightLocal(p, aIP, uT);\n } else {\n float sw = sin(uT * aIP.y + aIP.x) + 0.5 * sin(uT * aIP.y * 2.3 + aIP.x * 1.7);\n p += uWindDir * (sw * aIP.z * max(p.y, 0.0));\n }\n gl_Position = uVP * (uGM[g] * (mat4(aI0, aI1, aI2, aI3) * vec4(p, 1.0)));\n}";
5
+ export declare const FACE_SKIN_VS = "#version 300 es\nin vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;\nin vec3 aPos2; in vec3 aNorm2; in float aBone;\nuniform mat4 uVP; uniform mat4 uSVP; uniform mat4 uBones[16];\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;\nout float vGA; out float vBlood;\nvoid main(){\n int g = int(aGrp + 0.5);\n mat4 bm = uBones[int(aBone + 0.5)];\n vec3 p = (bm * vec4(aPos, 1.0)).xyz;\n vec3 n = mat3(bm) * aNorm;\n vec4 wp = uGM[g] * vec4(p, 1.0);\n vPos = wp.xyz; vNorm = uNM[g] * n; vCol = aCol; vGA = uGA[g]; vBlood = aBlood;\n vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;\n}";
6
+ export declare const DEPTH_SKIN_VS = "#version 300 es\nin vec3 aPos; in float aGrp; in float aBone;\nuniform mat4 uVP; uniform mat4 uBones[16];\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nvoid main(){\n int g = int(aGrp + 0.5);\n gl_Position = uVP * (uGM[g] * (uBones[int(aBone + 0.5)] * vec4(aPos, 1.0)));\n}";
5
7
  export declare const DEPTH_VS = "#version 300 es\nin vec3 aPos; in float aGrp; in vec3 aPos2;\nuniform mat4 uVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nvoid main(){\n int g = int(aGrp + 0.5);\n vec3 p = mix(aPos, aPos2, uMW[g]);\n gl_Position = uVP * (uGM[g] * vec4(p, 1.0));\n}";
6
8
  export declare const DEPTH_FS = "#version 300 es\nprecision mediump float;\nvoid main(){}";
7
9
  export declare const LINE_VS = "#version 300 es\nin vec3 aPos; in vec4 aCol; in float aGrp;\nuniform mat4 uVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec4 vCol; out float vZ; out float vGA;\nvoid main(){\n int g = int(aGrp + 0.5);\n vCol = aCol; vGA = uGA[g];\n vec4 p = uVP * (uGM[g] * vec4(aPos, 1.0)); vZ = p.w; gl_Position = p;\n}";
package/dist/shaders.js CHANGED
@@ -188,6 +188,30 @@ void main(){
188
188
  }
189
189
  gl_Position = uVP * (uGM[g] * (mat4(aI0, aI1, aI2, aI3) * vec4(p, 1.0)));
190
190
  }`;
191
+ // W12 SKELETAL: rigid skinning — one bone per vertex, 16 mat4s per figure,
192
+ // composed on the CPU (composePose) and applied here before the group transform.
193
+ export const FACE_SKIN_VS = `#version 300 es
194
+ in vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;
195
+ in vec3 aPos2; in vec3 aNorm2; in float aBone;
196
+ uniform mat4 uVP; uniform mat4 uSVP; uniform mat4 uBones[16];${GROUPS}
197
+ out vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;
198
+ out float vGA; out float vBlood;
199
+ void main(){
200
+ int g = int(aGrp + 0.5);
201
+ mat4 bm = uBones[int(aBone + 0.5)];
202
+ vec3 p = (bm * vec4(aPos, 1.0)).xyz;
203
+ vec3 n = mat3(bm) * aNorm;
204
+ vec4 wp = uGM[g] * vec4(p, 1.0);
205
+ vPos = wp.xyz; vNorm = uNM[g] * n; vCol = aCol; vGA = uGA[g]; vBlood = aBlood;
206
+ vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;
207
+ }`;
208
+ export const DEPTH_SKIN_VS = `#version 300 es
209
+ in vec3 aPos; in float aGrp; in float aBone;
210
+ uniform mat4 uVP; uniform mat4 uBones[16];${GROUPS}
211
+ void main(){
212
+ int g = int(aGrp + 0.5);
213
+ gl_Position = uVP * (uGM[g] * (uBones[int(aBone + 0.5)] * vec4(aPos, 1.0)));
214
+ }`;
191
215
  export const DEPTH_VS = `#version 300 es
192
216
  in vec3 aPos; in float aGrp; in vec3 aPos2;
193
217
  uniform mat4 uVP;${GROUPS}
@@ -0,0 +1,36 @@
1
+ import { V3 } from "./math.js";
2
+ import { Mesh } from "./geometry.js";
3
+ export type Bone = {
4
+ name: string;
5
+ /** parent bone index, -1 for the root */
6
+ parent: number;
7
+ /** joint position (rotations pivot here), rest pose, figure-local */
8
+ head: V3;
9
+ /** where the bone points, rest pose — children usually head here */
10
+ tail: V3;
11
+ };
12
+ /** A rigged figure: the mesh, one bone index per VERTEX, and the skeleton. */
13
+ export type Rig = {
14
+ mesh: Mesh;
15
+ boneOf: number[];
16
+ bones: Bone[];
17
+ };
18
+ export type PoseRot = {
19
+ x?: number;
20
+ y?: number;
21
+ z?: number;
22
+ };
23
+ /** A pose: local joint rotations by bone name (radians). Missing bones rest. */
24
+ export type Pose = Record<string, PoseRot>;
25
+ /** Compose a pose into per-bone world matrices (figure space).
26
+ * Each bone rotates about its HEAD, inside its parent's space. */
27
+ export declare function composePose(bones: Bone[], pose: Pose): Float32Array;
28
+ /** A humanoid: 11 bones, ~1.7 m tall by default, feet at y=0, facing +z. */
29
+ export declare function humanoid(h?: number): Rig;
30
+ /** A quadruped (dog-shaped): 10 bones, shoulder height h, facing +z. */
31
+ export declare function quadruped(h?: number): Rig;
32
+ /** The walk: phase in radians (t * cadence). Legs swing opposite, knees flex on
33
+ * the back-swing, arms counter-swing, the spine counter-rotates a touch. */
34
+ export declare function walkPose(phase: number, stride?: number): Pose;
35
+ /** The trot: diagonal pairs move together, the tail wags, the head bobs. */
36
+ export declare function trotPose(phase: number, stride?: number): Pose;
@@ -0,0 +1,115 @@
1
+ // generative-engine — skeletal.ts (W12): BONES. Procedural skeletons, rigid skinning,
2
+ // walk cycles — no asset pipeline, no files, figures grown from code like everything
3
+ // else here. Each vertex binds to ONE bone (rigid skinning): for procedural figures the
4
+ // segment boundaries are where joints live, so single-bone binding reads clean and the
5
+ // whole system stays small. Max 16 bones per figure; matrices composed on the CPU
6
+ // (16 tiny mat4 muls) and applied in the vertex stage.
7
+ import { v3, mMul, mCompose } from "./math.js";
8
+ import { tube, icosphere, transformMesh, mergeMesh } from "./geometry.js";
9
+ /** Compose a pose into per-bone world matrices (figure space).
10
+ * Each bone rotates about its HEAD, inside its parent's space. */
11
+ export function composePose(bones, pose) {
12
+ const out = new Float32Array(bones.length * 16);
13
+ const world = [];
14
+ for (let i = 0; i < bones.length; i++) {
15
+ const b = bones[i];
16
+ const r = pose[b.name] ?? {};
17
+ const local = mMul(mCompose({ translate: b.head }), mMul(mCompose({ rotateX: r.x ?? 0, rotateY: r.y ?? 0, rotateZ: r.z ?? 0 }), mCompose({ translate: v3(-b.head.x, -b.head.y, -b.head.z) })));
18
+ const m = b.parent >= 0 ? mMul(world[b.parent], local) : local;
19
+ world.push(m);
20
+ out.set(m, i * 16);
21
+ }
22
+ return out;
23
+ }
24
+ /** Build a limb segment (tapered tube along a bone) bound to that bone. */
25
+ function segment(rig, bone, a, b, r0, r1, seg = 7) {
26
+ const m = tube([a, b], (u) => r0 + (r1 - r0) * u, seg);
27
+ rig.parts.push(m);
28
+ for (let i = 0; i < m.pos.length / 3; i++)
29
+ rig.boneOf.push(bone);
30
+ }
31
+ function blob(rig, bone, at, r, detail = 1) {
32
+ const m = transformMesh(icosphere(r, detail), mCompose({ translate: at }));
33
+ rig.parts.push(m);
34
+ for (let i = 0; i < m.pos.length / 3; i++)
35
+ rig.boneOf.push(bone);
36
+ }
37
+ /** A humanoid: 11 bones, ~1.7 m tall by default, feet at y=0, facing +z. */
38
+ export function humanoid(h = 170) {
39
+ const s = h / 170;
40
+ const hip = 92 * s, knee = 48 * s, sho = 148 * s, elb = 118 * s, neck = 158 * s;
41
+ const bones = [
42
+ { name: "pelvis", parent: -1, head: v3(0, hip, 0), tail: v3(0, hip + 20 * s, 0) }, // 0
43
+ { name: "spine", parent: 0, head: v3(0, hip + 8 * s, 0), tail: v3(0, sho, 0) }, // 1
44
+ { name: "head", parent: 1, head: v3(0, neck, 0), tail: v3(0, h, 0) }, // 2
45
+ { name: "legL.up", parent: 0, head: v3(-9 * s, hip, 0), tail: v3(-9 * s, knee, 0) }, // 3
46
+ { name: "legL.lo", parent: 3, head: v3(-9 * s, knee, 0), tail: v3(-9 * s, 4 * s, 0) }, // 4
47
+ { name: "legR.up", parent: 0, head: v3(9 * s, hip, 0), tail: v3(9 * s, knee, 0) }, // 5
48
+ { name: "legR.lo", parent: 5, head: v3(9 * s, knee, 0), tail: v3(9 * s, 4 * s, 0) }, // 6
49
+ { name: "armL.up", parent: 1, head: v3(-15 * s, sho, 0), tail: v3(-17 * s, elb, 0) }, // 7
50
+ { name: "armL.lo", parent: 7, head: v3(-17 * s, elb, 0), tail: v3(-18 * s, 96 * s, 0) }, // 8
51
+ { name: "armR.up", parent: 1, head: v3(15 * s, sho, 0), tail: v3(17 * s, elb, 0) }, // 9
52
+ { name: "armR.lo", parent: 9, head: v3(17 * s, elb, 0), tail: v3(18 * s, 96 * s, 0) }, // 10
53
+ ];
54
+ const rig = { parts: [], boneOf: [] };
55
+ segment(rig, 1, v3(0, hip + 4 * s, 0), v3(0, sho, 0), 10 * s, 12 * s, 9); // torso
56
+ blob(rig, 0, v3(0, hip, 0), 10.5 * s); // pelvis
57
+ blob(rig, 2, v3(0, neck + 9 * s, 0), 7.5 * s); // head
58
+ segment(rig, 1, v3(0, sho - 3 * s, 0), v3(0, neck, 0), 4 * s, 3.4 * s, 6); // neck
59
+ for (const [up, lo, sx] of [[3, 4, -1], [5, 6, 1]]) {
60
+ segment(rig, up, bones[up].head, bones[up].tail, 6 * s, 4.6 * s);
61
+ segment(rig, lo, bones[lo].head, bones[lo].tail, 4.4 * s, 2.8 * s);
62
+ blob(rig, lo, v3(sx * 9 * s, 3 * s, 4 * s), 4.4 * s, 0); // foot
63
+ }
64
+ for (const [up, lo] of [[7, 8], [9, 10]]) {
65
+ segment(rig, up, bones[up].head, bones[up].tail, 3.8 * s, 3 * s);
66
+ segment(rig, lo, bones[lo].head, bones[lo].tail, 2.9 * s, 2.2 * s);
67
+ }
68
+ return { mesh: mergeMesh(...rig.parts), boneOf: rig.boneOf, bones };
69
+ }
70
+ /** A quadruped (dog-shaped): 10 bones, shoulder height h, facing +z. */
71
+ export function quadruped(h = 40) {
72
+ const s = h / 40;
73
+ const bones = [
74
+ { name: "body", parent: -1, head: v3(0, 30 * s, -14 * s), tail: v3(0, 32 * s, 14 * s) }, // 0
75
+ { name: "head", parent: 0, head: v3(0, 34 * s, 16 * s), tail: v3(0, 40 * s, 28 * s) }, // 1
76
+ { name: "tail", parent: 0, head: v3(0, 32 * s, -16 * s), tail: v3(0, 42 * s, -26 * s) }, // 2
77
+ { name: "legFL", parent: 0, head: v3(-6 * s, 28 * s, 10 * s), tail: v3(-6 * s, 2 * s, 10 * s) },
78
+ { name: "legFR", parent: 0, head: v3(6 * s, 28 * s, 10 * s), tail: v3(6 * s, 2 * s, 10 * s) },
79
+ { name: "legBL", parent: 0, head: v3(-6 * s, 28 * s, -10 * s), tail: v3(-6 * s, 2 * s, -10 * s) },
80
+ { name: "legBR", parent: 0, head: v3(6 * s, 28 * s, -10 * s), tail: v3(6 * s, 2 * s, -10 * s) },
81
+ ];
82
+ const rig = { parts: [], boneOf: [] };
83
+ segment(rig, 0, bones[0].head, bones[0].tail, 8 * s, 9 * s, 8);
84
+ blob(rig, 1, v3(0, 38 * s, 24 * s), 5.5 * s);
85
+ segment(rig, 1, v3(0, 36 * s, 24 * s), v3(0, 34 * s, 32 * s), 2.6 * s, 1.8 * s, 5); // snout
86
+ segment(rig, 2, bones[2].head, bones[2].tail, 2 * s, 1 * s, 5);
87
+ for (let i = 3; i <= 6; i++)
88
+ segment(rig, i, bones[i].head, bones[i].tail, 2.6 * s, 1.7 * s, 5);
89
+ return { mesh: mergeMesh(...rig.parts), boneOf: rig.boneOf, bones };
90
+ }
91
+ /** The walk: phase in radians (t * cadence). Legs swing opposite, knees flex on
92
+ * the back-swing, arms counter-swing, the spine counter-rotates a touch. */
93
+ export function walkPose(phase, stride = 0.6) {
94
+ const L = Math.sin(phase), R = Math.sin(phase + Math.PI);
95
+ const kneeL = Math.max(0, -Math.cos(phase)) * stride * 1.2;
96
+ const kneeR = Math.max(0, Math.cos(phase)) * stride * 1.2;
97
+ return {
98
+ "legL.up": { x: L * stride }, "legL.lo": { x: kneeL },
99
+ "legR.up": { x: R * stride }, "legR.lo": { x: kneeR },
100
+ "armL.up": { x: R * stride * 0.7 }, "armL.lo": { x: R * stride * 0.3 + 0.2 },
101
+ "armR.up": { x: L * stride * 0.7 }, "armR.lo": { x: L * stride * 0.3 + 0.2 },
102
+ spine: { y: Math.sin(phase) * 0.06 },
103
+ pelvis: { z: Math.sin(phase) * 0.05 },
104
+ };
105
+ }
106
+ /** The trot: diagonal pairs move together, the tail wags, the head bobs. */
107
+ export function trotPose(phase, stride = 0.55) {
108
+ const A = Math.sin(phase), B = Math.sin(phase + Math.PI);
109
+ return {
110
+ legFL: { x: A * stride }, legBR: { x: A * stride },
111
+ legFR: { x: B * stride }, legBL: { x: B * stride },
112
+ head: { x: Math.sin(phase * 2) * 0.08 },
113
+ tail: { z: Math.sin(phase * 2.6) * 0.4 },
114
+ };
115
+ }
package/dist/world.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { V3, M4, TransformSpec } from "./math.js";
2
2
  import { Mesh } from "./geometry.js";
3
+ import { Rig, Pose } from "./skeletal.js";
3
4
  export type LineOpts = {
4
5
  hue: number;
5
6
  sat?: number;
@@ -51,7 +52,9 @@ export type WorldOpts = {
51
52
  maxDpr?: number;
52
53
  drift?: number;
53
54
  parallax?: number;
54
- controls?: boolean;
55
+ /** true = orbit (drag + ctrl-wheel zoom). "fly" (W13) = free flight: drag to look,
56
+ * wheel to fly forward/back, W/A/S/D to move, shift for speed. */
57
+ controls?: boolean | "fly";
55
58
  /** shadow maps — lights[0] casts. true, or { size (px, default 2048), softness, bias } */
56
59
  shadows?: boolean | {
57
60
  size?: number;
@@ -83,6 +86,12 @@ export declare class World {
83
86
  private overlay;
84
87
  private og;
85
88
  private opts;
89
+ private flyMode;
90
+ private pitchMin;
91
+ private pitchMax;
92
+ private keys;
93
+ /** W13 fly speed, units/second (shift = 3x). */
94
+ flySpeed: number;
86
95
  private pFace;
87
96
  private pLine;
88
97
  private pPt;
@@ -120,6 +129,10 @@ export declare class World {
120
129
  private reflW;
121
130
  private reflH;
122
131
  private clipY;
132
+ /** W12 — skinned figures: rig buffers + live bone matrices */
133
+ private pFaceS;
134
+ private pDepthS;
135
+ private figures;
123
136
  private post;
124
137
  private ink;
125
138
  private bg;
@@ -313,6 +326,15 @@ export declare class World {
313
326
  private drawLines;
314
327
  private drawPts;
315
328
  private drawRibbons;
329
+ /** W12 — a skinned FIGURE: bones + rigid skinning + procedural poses.
330
+ * Returns a handle whose pose() re-composes the bone matrices (CPU, 16 tiny
331
+ * mat4 muls); the vertices bend in the vertex stage, scene and shadow alike.
332
+ * Place / turn the whole figure with its GROUP transform, like any geometry. */
333
+ figure(rig: Rig, o: FaceOpts): {
334
+ pose(p: Pose): void;
335
+ bones: typeof rig.bones;
336
+ };
337
+ private drawFigures;
316
338
  /** Instanced records under a given program (scene or depth pass). Divisors are reset
317
339
  * after each draw — attribute divisor is global-per-index state and would otherwise
318
340
  * poison the non-instanced draws that share those indices. */
package/dist/world.js CHANGED
@@ -4,7 +4,8 @@
4
4
  // hardware-PCF depth-texture shadows, per-group transforms + morph targets in the
5
5
  // vertex stage, mesh + ribbon primitives. Zero third-party imports, on purpose.
6
6
  import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, mInvert, normalMat3, } from "./math.js";
7
- import { FACE_VS, FACE_FS, DEPTH_VS, DEPTH_FS, LINE_VS, LINE_FS, PT_VS, PT_FS, RIBBON_VS, RIBBON_FS, FACE_INST_VS, DEPTH_INST_VS, WATER_VS, WATER_FS, } from "./shaders.js";
7
+ import { FACE_VS, FACE_FS, DEPTH_VS, DEPTH_FS, LINE_VS, LINE_FS, PT_VS, PT_FS, RIBBON_VS, RIBBON_FS, FACE_INST_VS, DEPTH_INST_VS, WATER_VS, WATER_FS, FACE_SKIN_VS, DEPTH_SKIN_VS, } from "./shaders.js";
8
+ import { composePose } from "./skeletal.js";
8
9
  import { PInfo, PostChain } from "./post.js";
9
10
  const FACE_STRIDE = 18; // pos3 norm3 rgb3 gloss1 blood1 grp1 pos2:3 norm2:3
10
11
  const LINE_STRIDE = 8; // pos3 col4 grp1
@@ -33,6 +34,12 @@ export class World {
33
34
  this.groupNM.set(normalMat3(mat), i * 9);
34
35
  }
35
36
  constructor(o) {
37
+ this.flyMode = false;
38
+ this.pitchMin = 0.18;
39
+ this.pitchMax = 0.95;
40
+ this.keys = new Set();
41
+ /** W13 fly speed, units/second (shift = 3x). */
42
+ this.flySpeed = 420;
36
43
  this.instanced = [];
37
44
  /** W9 — the one wind that blows through every swaying instanced batch */
38
45
  this.wind = { dir: v3(1, 0, 0.35), speed: 1 };
@@ -50,6 +57,10 @@ export class World {
50
57
  this.reflW = 0;
51
58
  this.reflH = 0;
52
59
  this.clipY = -1e9;
60
+ /** W12 — skinned figures: rig buffers + live bone matrices */
61
+ this.pFaceS = null;
62
+ this.pDepthS = null;
63
+ this.figures = [];
53
64
  this.ink = false;
54
65
  this.bg = [0.024, 0.016, 0.055];
55
66
  this.transparent = false;
@@ -154,8 +165,13 @@ export class World {
154
165
  maxDpr: o.maxDpr ?? 2,
155
166
  drift: o.drift ?? 0.045,
156
167
  parallax: o.parallax ?? 0.3,
157
- controls: o.controls ?? false,
168
+ controls: !!o.controls,
158
169
  };
170
+ this.flyMode = o.controls === "fly";
171
+ if (this.flyMode) {
172
+ this.pitchMin = -1.15;
173
+ this.pitchMax = 1.15;
174
+ }
159
175
  if (o.camera) {
160
176
  if (o.camera.target)
161
177
  this.cam.target = o.camera.target;
@@ -259,11 +275,26 @@ export class World {
259
275
  };
260
276
  const upFn = () => { dragging = false; cv.style.cursor = "grab"; };
261
277
  const wheel = (e) => {
278
+ if (this.flyMode) {
279
+ e.preventDefault();
280
+ // fly: the wheel moves the camera along its own gaze
281
+ const d = -e.deltaY * this.flySpeed * 0.004;
282
+ this.cam.target = add(this.cam.target, scale(this.fw, d));
283
+ return;
284
+ }
262
285
  if (!e.ctrlKey && !e.metaKey)
263
286
  return;
264
287
  e.preventDefault();
265
288
  this.cam.distT = clamp(this.cam.distT + e.deltaY * 0.6, 120, 2200);
266
289
  };
290
+ const kdown = (e) => { if (this.flyMode)
291
+ this.keys.add(e.key.toLowerCase()); };
292
+ const kup = (e) => { this.keys.delete(e.key.toLowerCase()); };
293
+ if (this.flyMode) {
294
+ window.addEventListener("keydown", kdown);
295
+ window.addEventListener("keyup", kup);
296
+ this.cleanup.push(() => { window.removeEventListener("keydown", kdown); window.removeEventListener("keyup", kup); });
297
+ }
267
298
  cv.addEventListener("pointerdown", down);
268
299
  cv.addEventListener("pointermove", move);
269
300
  cv.addEventListener("pointerup", upFn);
@@ -457,13 +488,26 @@ export class World {
457
488
  overlay2d(cb) { cb(this.og, this); }
458
489
  // ── camera + CPU projection for pick/labels ──
459
490
  setupCamera(t, dt) {
491
+ if (this.flyMode && this.keys.size && dt > 0) { // W13: keys fly the camera
492
+ const sp = this.flySpeed * (this.keys.has("shift") ? 3 : 1) * dt;
493
+ let m = v3();
494
+ if (this.keys.has("w"))
495
+ m = add(m, scale(this.fw, sp));
496
+ if (this.keys.has("s"))
497
+ m = add(m, scale(this.fw, -sp));
498
+ if (this.keys.has("a"))
499
+ m = add(m, scale(this.rt, -sp));
500
+ if (this.keys.has("d"))
501
+ m = add(m, scale(this.rt, sp));
502
+ this.cam.target = add(this.cam.target, m);
503
+ }
460
504
  this.mx += (this.mxT - this.mx) * Math.min(1, dt * 3);
461
505
  this.my += (this.myT - this.my) * Math.min(1, dt * 3);
462
506
  this.cam.yawUser += (this.yawUserT - this.cam.yawUser) * Math.min(1, dt * 5);
463
507
  this.cam.pitchUser += (this.pitchUserT - this.cam.pitchUser) * Math.min(1, dt * 5);
464
508
  this.cam.dist += (this.cam.distT - this.cam.dist) * Math.min(1, dt * 4);
465
509
  const yaw = this.cam.yawBase + t * this.opts.drift + this.cam.yawUser + this.mx * this.opts.parallax;
466
- const pitch = clamp(this.cam.pitch + this.cam.pitchUser + this.my * this.opts.parallax * 0.5, 0.18, 0.95);
510
+ const pitch = clamp(this.cam.pitch + this.cam.pitchUser + this.my * this.opts.parallax * 0.5, this.pitchMin, this.pitchMax);
467
511
  const cp = Math.cos(pitch), sp = Math.sin(pitch);
468
512
  this.cx = add(this.cam.target, scale(v3(cp * Math.cos(yaw), sp, cp * Math.sin(yaw)), this.cam.dist));
469
513
  this.fw = norm(sub(this.cam.target, this.cx));
@@ -627,6 +671,54 @@ export class World {
627
671
  this.attrib(this.pRib, "aGrp", 1, RIB_STRIDE, 15);
628
672
  gl.drawArrays(gl.TRIANGLES, 0, verts);
629
673
  }
674
+ /** W12 — a skinned FIGURE: bones + rigid skinning + procedural poses.
675
+ * Returns a handle whose pose() re-composes the bone matrices (CPU, 16 tiny
676
+ * mat4 muls); the vertices bend in the vertex stage, scene and shadow alike.
677
+ * Place / turn the whole figure with its GROUP transform, like any geometry. */
678
+ figure(rig, o) {
679
+ const gl = this.gl;
680
+ if (!this.pFaceS) {
681
+ this.pFaceS = new PInfo(gl, FACE_SKIN_VS, FACE_FS);
682
+ this.pDepthS = new PInfo(gl, DEPTH_SKIN_VS, DEPTH_FS);
683
+ }
684
+ const store = [];
685
+ const P = rig.mesh.pos, N = rig.mesh.nrm;
686
+ const base = [];
687
+ for (let i = 0; i < P.length; i += 9) {
688
+ this.pushFace(base, v3(P[i], P[i + 1], P[i + 2]), v3(P[i + 3], P[i + 4], P[i + 5]), v3(P[i + 6], P[i + 7], P[i + 8]), o, v3(N[i], N[i + 1], N[i + 2]), v3(N[i + 3], N[i + 4], N[i + 5]), v3(N[i + 6], N[i + 7], N[i + 8]));
689
+ }
690
+ // re-stride 18 -> 19 with the vertex's bone index appended
691
+ const nv = base.length / FACE_STRIDE;
692
+ for (let vtx = 0; vtx < nv; vtx++) {
693
+ for (let k = 0; k < FACE_STRIDE; k++)
694
+ store.push(base[vtx * FACE_STRIDE + k]);
695
+ store.push(rig.boneOf[vtx] ?? 0);
696
+ }
697
+ const vbuf = gl.createBuffer();
698
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
699
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(store), gl.STATIC_DRAW);
700
+ const rec = { vbuf, verts: nv, mats: composePose(rig.bones, {}) };
701
+ this.figures.push(rec);
702
+ return { pose: (pp) => { rec.mats = composePose(rig.bones, pp); }, bones: rig.bones };
703
+ }
704
+ drawFigures(p, depthOnly) {
705
+ if (!this.figures.length)
706
+ return;
707
+ const gl = this.gl, SK = FACE_STRIDE + 1;
708
+ for (const rec of this.figures) {
709
+ gl.uniformMatrix4fv(p.u("uBones[0]"), false, rec.mats);
710
+ gl.bindBuffer(gl.ARRAY_BUFFER, rec.vbuf);
711
+ this.attrib(p, "aPos", 3, SK, 0);
712
+ if (!depthOnly) {
713
+ this.attrib(p, "aNorm", 3, SK, 3);
714
+ this.attrib(p, "aCol", 4, SK, 6);
715
+ this.attrib(p, "aBlood", 1, SK, 10);
716
+ }
717
+ this.attrib(p, "aGrp", 1, SK, 11);
718
+ this.attrib(p, "aBone", 1, SK, 18);
719
+ gl.drawArrays(gl.TRIANGLES, 0, rec.verts);
720
+ }
721
+ }
630
722
  /** Instanced records under a given program (scene or depth pass). Divisors are reset
631
723
  * after each draw — attribute divisor is global-per-index state and would otherwise
632
724
  * poison the non-instanced draws that share those indices. */
@@ -723,6 +815,12 @@ export class World {
723
815
  gl.uniform3f(this.pFaceI.u("uWindDir"), this.wind.dir.x, this.wind.dir.y, this.wind.dir.z);
724
816
  this.drawInstancedAll(this.pFaceI, false);
725
817
  }
818
+ if (this.figures.length && this.pFaceS) {
819
+ this.pFaceS.use();
820
+ this.setVP(this.pFaceS);
821
+ this.setLights(this.pFaceS, fogK);
822
+ this.drawFigures(this.pFaceS, false);
823
+ }
726
824
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
727
825
  this.vp = vpSave;
728
826
  this.clipY = -1e9;
@@ -863,6 +961,12 @@ export class World {
863
961
  this.setGroups(this.pDepthI);
864
962
  this.drawInstancedAll(this.pDepthI, true);
865
963
  }
964
+ if (this.figures.length && this.pDepthS) {
965
+ this.pDepthS.use();
966
+ gl.uniformMatrix4fv(this.pDepthS.u("uVP"), false, this.svp);
967
+ this.setGroups(this.pDepthS);
968
+ this.drawFigures(this.pDepthS, true);
969
+ }
866
970
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
867
971
  }
868
972
  // W11: the reflection pass first — the world through the mirrored camera
@@ -900,6 +1004,12 @@ export class World {
900
1004
  gl.uniform3f(this.pFaceI.u("uWindDir"), this.wind.dir.x, this.wind.dir.y, this.wind.dir.z);
901
1005
  this.drawInstancedAll(this.pFaceI, false);
902
1006
  }
1007
+ if (this.figures.length && this.pFaceS) {
1008
+ this.pFaceS.use();
1009
+ this.setVP(this.pFaceS);
1010
+ this.setLights(this.pFaceS, fogK);
1011
+ this.drawFigures(this.pFaceS, false);
1012
+ }
903
1013
  // the light — additive, depth-TESTED but not written (ink: normal alpha blend)
904
1014
  gl.depthMask(false);
905
1015
  gl.enable(gl.BLEND);
@@ -1038,6 +1148,9 @@ export class World {
1038
1148
  this.gl.deleteFramebuffer(this.reflFBO);
1039
1149
  if (this.bWater)
1040
1150
  this.gl.deleteBuffer(this.bWater);
1151
+ for (const f of this.figures)
1152
+ this.gl.deleteBuffer(f.vbuf);
1153
+ this.figures = [];
1041
1154
  this.post?.dispose();
1042
1155
  if (releaseContext)
1043
1156
  this.gl.getExtension("WEBGL_lose_context")?.loseContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromatic-coherence/generative-engine",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "THE THIRD ENGINE — a zero-dependency WebGL2 graphics engine grown from generative-charts-3d: HDR + bloom + SSAO post chain, hardware-PCF shadow maps, a geometry stdlib, ribbon strokes, group transforms and morph targets. The charts API rides on top unchanged.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",