@chromatic-coherence/generative-engine 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/world.d.ts CHANGED
@@ -88,6 +88,14 @@ export declare class World {
88
88
  private pPt;
89
89
  private pRib;
90
90
  private pDepth;
91
+ private pFaceI;
92
+ private pDepthI;
93
+ private instanced;
94
+ /** W9 — the one wind that blows through every swaying instanced batch */
95
+ wind: {
96
+ dir: V3;
97
+ speed: number;
98
+ };
91
99
  private post;
92
100
  private ink;
93
101
  private bg;
@@ -171,6 +179,20 @@ export declare class World {
171
179
  };
172
180
  /** FXAA on the final composite */
173
181
  fxaa: boolean;
182
+ /** depth of field (W8) — blur grows with distance from the focus plane (world units) */
183
+ dof: {
184
+ on: boolean;
185
+ focus: number;
186
+ range: number;
187
+ maxBlur: number;
188
+ };
189
+ /** camera motion blur (W8) — depth reprojection into the previous frame's view */
190
+ motionBlur: {
191
+ on: boolean;
192
+ strength: number;
193
+ };
194
+ private prevVP;
195
+ private prevVPValid;
174
196
  private groupIdx;
175
197
  private groupAlpha;
176
198
  private groupTarget;
@@ -223,6 +245,17 @@ export declare class World {
223
245
  private pushRibbon;
224
246
  /** A static ribbon stroke — real pixel width, feathered edges, depth-tested. */
225
247
  ribbon(pts: V3[], o: RibbonOpts): void;
248
+ /** W8 — GPU instancing: upload a Mesh ONCE and draw it at every given transform in a
249
+ * single instanced call (scene + shadow pass alike). A forest is one draw. Instances
250
+ * ride their group's fade / morph / transform like any other geometry. */
251
+ meshInstanced(geo: Mesh, instances: (M4 | TransformSpec)[], o: FaceOpts & {
252
+ /** W9 GPU wind: per-instance sway computed in the vertex stage (shadows included).
253
+ * amp scales with vertex height; freq in rad/s; phases auto-scatter per instance. */
254
+ sway?: {
255
+ amp: number;
256
+ freq?: number;
257
+ };
258
+ }): void;
226
259
  grow(fn: GrowFn): void;
227
260
  drawLine(a: V3, b: V3, color: string, alpha: number, width?: number): void;
228
261
  drawPath(pts: {
@@ -250,6 +283,10 @@ export declare class World {
250
283
  private drawLines;
251
284
  private drawPts;
252
285
  private drawRibbons;
286
+ /** Instanced records under a given program (scene or depth pass). Divisors are reset
287
+ * after each draw — attribute divisor is global-per-index state and would otherwise
288
+ * poison the non-instanced draws that share those indices. */
289
+ private drawInstancedAll;
253
290
  private renderFrame;
254
291
  private frameFn;
255
292
  private pausedByUser;
package/dist/world.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // WebGL2 renderer: HDR scene target + post chain (bloom, SSAO, composite, FXAA),
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
- import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, 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, } from "./shaders.js";
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, } from "./shaders.js";
8
8
  import { PInfo, PostChain } from "./post.js";
9
9
  const FACE_STRIDE = 18; // pos3 norm3 rgb3 gloss1 blood1 grp1 pos2:3 norm2:3
10
10
  const LINE_STRIDE = 8; // pos3 col4 grp1
@@ -33,6 +33,9 @@ export class World {
33
33
  this.groupNM.set(normalMat3(mat), i * 9);
34
34
  }
35
35
  constructor(o) {
36
+ this.instanced = [];
37
+ /** W9 — the one wind that blows through every swaying instanced batch */
38
+ this.wind = { dir: v3(1, 0, 0.35), speed: 1 };
36
39
  this.ink = false;
37
40
  this.bg = [0.024, 0.016, 0.055];
38
41
  this.transparent = false;
@@ -84,6 +87,12 @@ export class World {
84
87
  this.ssao = { on: true, strength: 0.55, radius: 22 };
85
88
  /** FXAA on the final composite */
86
89
  this.fxaa = true;
90
+ /** depth of field (W8) — blur grows with distance from the focus plane (world units) */
91
+ this.dof = { on: false, focus: 700, range: 420, maxBlur: 10 };
92
+ /** camera motion blur (W8) — depth reprojection into the previous frame's view */
93
+ this.motionBlur = { on: false, strength: 0.6 };
94
+ this.prevVP = mIdent();
95
+ this.prevVPValid = false;
87
96
  // groups: fade alpha + morph weight + transform, resolved in the vertex stage
88
97
  this.groupIdx = new Map([["default", 0]]);
89
98
  this.groupAlpha = new Float32Array(8).fill(1);
@@ -156,6 +165,8 @@ export class World {
156
165
  this.pPt = new PInfo(gl, PT_VS, PT_FS);
157
166
  this.pRib = new PInfo(gl, RIBBON_VS, RIBBON_FS);
158
167
  this.pDepth = new PInfo(gl, DEPTH_VS, DEPTH_FS);
168
+ this.pFaceI = new PInfo(gl, FACE_INST_VS, FACE_FS);
169
+ this.pDepthI = new PInfo(gl, DEPTH_INST_VS, DEPTH_FS);
159
170
  this.post = (o.post ?? true) && !this.transparent ? new PostChain(gl) : null;
160
171
  this.shadowsOn = !!o.shadows;
161
172
  if (o.shadows && typeof o.shadows === "object") {
@@ -371,6 +382,35 @@ export class World {
371
382
  }
372
383
  /** A static ribbon stroke — real pixel width, feathered edges, depth-tested. */
373
384
  ribbon(pts, o) { this.pushRibbon(this.sRib, pts, o); this.staticDirty = true; }
385
+ /** W8 — GPU instancing: upload a Mesh ONCE and draw it at every given transform in a
386
+ * single instanced call (scene + shadow pass alike). A forest is one draw. Instances
387
+ * ride their group's fade / morph / transform like any other geometry. */
388
+ meshInstanced(geo, instances, o) {
389
+ if (!instances.length)
390
+ return;
391
+ const gl = this.gl;
392
+ const store = [];
393
+ const P = geo.pos, N = geo.nrm;
394
+ for (let i = 0; i < P.length; i += 9) {
395
+ this.pushFace(store, 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]));
396
+ }
397
+ const mats = new Float32Array(instances.length * 20); // mat4 + sway vec4, one stride
398
+ instances.forEach((m, i) => {
399
+ mats.set(m instanceof Float32Array ? m : mCompose(m), i * 20);
400
+ const ph = ((i * 2654435761) % 6283) / 1000; // scattered phase
401
+ mats[i * 20 + 16] = ph;
402
+ mats[i * 20 + 17] = (o.sway?.freq ?? 1.3) * (0.85 + ((i * 97) % 30) / 100);
403
+ mats[i * 20 + 18] = (o.sway?.amp ?? 0) * 0.01;
404
+ mats[i * 20 + 19] = 0;
405
+ });
406
+ const vbuf = gl.createBuffer();
407
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
408
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(store), gl.STATIC_DRAW);
409
+ const ibuf = gl.createBuffer();
410
+ gl.bindBuffer(gl.ARRAY_BUFFER, ibuf);
411
+ gl.bufferData(gl.ARRAY_BUFFER, mats, gl.STATIC_DRAW);
412
+ this.instanced.push({ vbuf, ibuf, verts: store.length / FACE_STRIDE, count: instances.length });
413
+ }
374
414
  grow(fn) { this.hooks.push(fn); }
375
415
  // ── the living verbs (gathered per frame) ──
376
416
  drawLine(a, b, color, alpha, width = 1) {
@@ -563,11 +603,53 @@ export class World {
563
603
  this.attrib(this.pRib, "aGrp", 1, RIB_STRIDE, 15);
564
604
  gl.drawArrays(gl.TRIANGLES, 0, verts);
565
605
  }
606
+ /** Instanced records under a given program (scene or depth pass). Divisors are reset
607
+ * after each draw — attribute divisor is global-per-index state and would otherwise
608
+ * poison the non-instanced draws that share those indices. */
609
+ drawInstancedAll(p, depthOnly) {
610
+ if (!this.instanced.length)
611
+ return;
612
+ const gl = this.gl;
613
+ for (const rec of this.instanced) {
614
+ gl.bindBuffer(gl.ARRAY_BUFFER, rec.vbuf);
615
+ this.attrib(p, "aPos", 3, FACE_STRIDE, 0);
616
+ if (!depthOnly) {
617
+ this.attrib(p, "aNorm", 3, FACE_STRIDE, 3);
618
+ this.attrib(p, "aCol", 4, FACE_STRIDE, 6);
619
+ this.attrib(p, "aBlood", 1, FACE_STRIDE, 10);
620
+ }
621
+ this.attrib(p, "aGrp", 1, FACE_STRIDE, 11);
622
+ this.attrib(p, "aPos2", 3, FACE_STRIDE, 12);
623
+ if (!depthOnly)
624
+ this.attrib(p, "aNorm2", 3, FACE_STRIDE, 15);
625
+ gl.bindBuffer(gl.ARRAY_BUFFER, rec.ibuf);
626
+ const locs = [];
627
+ for (let k = 0; k < 5; k++) { // aI0..3 = the matrix, aIP = the sway params
628
+ const loc = gl.getAttribLocation(p.prog, k < 4 ? "aI" + k : "aIP");
629
+ if (loc < 0)
630
+ continue;
631
+ gl.enableVertexAttribArray(loc);
632
+ gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, 80, k * 16);
633
+ gl.vertexAttribDivisor(loc, 1);
634
+ locs.push(loc);
635
+ }
636
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, rec.verts, rec.count);
637
+ for (const loc of locs)
638
+ gl.vertexAttribDivisor(loc, 0);
639
+ }
640
+ }
566
641
  // ── the frame ──
567
642
  renderFrame(t, dt) {
568
643
  const gl = this.gl;
569
644
  this.lastT = t;
645
+ if (this.motionBlur.on && this.prevVPValid)
646
+ this.prevVP = new Float32Array(this.vp);
570
647
  this.setupCamera(t, dt);
648
+ if (this.motionBlur.on && !this.prevVPValid) {
649
+ // first blurred frame: previous = current, so velocity is zero (no identity streak)
650
+ this.prevVP = new Float32Array(this.vp);
651
+ this.prevVPValid = true;
652
+ }
571
653
  this.fLight.length = 0;
572
654
  this.fRib.length = 0;
573
655
  this.dFaceA.length = 0;
@@ -640,6 +722,14 @@ export class World {
640
722
  gl.bindBuffer(gl.ARRAY_BUFFER, this.bFaceD);
641
723
  this.depthDraw(this.bFaceD, this.dFaceA.length / FACE_STRIDE);
642
724
  }
725
+ if (this.instanced.length) {
726
+ this.pDepthI.use();
727
+ gl.uniformMatrix4fv(this.pDepthI.u("uVP"), false, this.svp);
728
+ gl.uniform1f(this.pDepthI.u("uT"), t * this.wind.speed);
729
+ gl.uniform3f(this.pDepthI.u("uWindDir"), this.wind.dir.x, this.wind.dir.y, this.wind.dir.z);
730
+ this.setGroups(this.pDepthI);
731
+ this.drawInstancedAll(this.pDepthI, true);
732
+ }
643
733
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
644
734
  }
645
735
  // the scene pass — into the HDR target when the post chain is on
@@ -664,6 +754,14 @@ export class World {
664
754
  this.drawFaces(this.bFace, this.sFace.length / FACE_STRIDE, null);
665
755
  if (this.dFaceA.length)
666
756
  this.drawFaces(this.bFaceD, this.dFaceA.length / FACE_STRIDE, new Float32Array(this.dFaceA));
757
+ if (this.instanced.length) {
758
+ this.pFaceI.use();
759
+ this.setVP(this.pFaceI);
760
+ this.setLights(this.pFaceI, fogK);
761
+ gl.uniform1f(this.pFaceI.u("uT"), t * this.wind.speed);
762
+ gl.uniform3f(this.pFaceI.u("uWindDir"), this.wind.dir.x, this.wind.dir.y, this.wind.dir.z);
763
+ this.drawInstancedAll(this.pFaceI, false);
764
+ }
667
765
  // the light — additive, depth-TESTED but not written (ink: normal alpha blend)
668
766
  gl.depthMask(false);
669
767
  gl.enable(gl.BLEND);
@@ -701,6 +799,9 @@ export class World {
701
799
  ssaoOn: this.ssao.on, ssaoStrength: this.ssao.strength, ssaoRadius: this.ssao.radius,
702
800
  filmic: this.filmic && !this.ink, grade: this.grade, vignette: this.vignette, ink: this.ink,
703
801
  fxaa: this.fxaa, near: this.zNear, far: this.zFar,
802
+ dofOn: this.dof.on, dofFocus: this.dof.focus, dofRange: this.dof.range, dofMaxBlur: this.dof.maxBlur,
803
+ moOn: this.motionBlur.on, moStrength: this.motionBlur.strength,
804
+ invVP: this.motionBlur.on ? mInvert(this.vp) : undefined, prevVP: this.prevVP,
704
805
  });
705
806
  }
706
807
  // labels — 2D overlay, never occluded (the honest exception)
@@ -784,6 +885,11 @@ export class World {
784
885
  c();
785
886
  this.cleanup = [];
786
887
  this.overlay.remove();
888
+ for (const rec of this.instanced) {
889
+ this.gl.deleteBuffer(rec.vbuf);
890
+ this.gl.deleteBuffer(rec.ibuf);
891
+ }
892
+ this.instanced = [];
787
893
  this.post?.dispose();
788
894
  if (releaseContext)
789
895
  this.gl.getExtension("WEBGL_lose_context")?.loseContext();
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
- {
2
- "name": "@chromatic-coherence/generative-engine",
3
- "version": "1.1.0",
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
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "sideEffects": false,
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "scripts": {
25
- "build": "tsc -p tsconfig.json",
26
- "test": "node test/run.mjs",
27
- "prepublishOnly": "npm run build"
28
- },
29
- "keywords": [
30
- "3d",
31
- "webgl2",
32
- "engine",
33
- "graphics",
34
- "bloom",
35
- "ssao",
36
- "shadows",
37
- "zero-dependency"
38
- ],
39
- "author": "Chromatic Coherence",
40
- "license": "SEE LICENSE IN LICENSE",
41
- "devDependencies": {
42
- "typescript": "^5"
43
- }
44
- }
1
+ {
2
+ "name": "@chromatic-coherence/generative-engine",
3
+ "version": "1.3.0",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "sideEffects": false,
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "test": "node test/run.mjs",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "3d",
31
+ "webgl2",
32
+ "engine",
33
+ "graphics",
34
+ "bloom",
35
+ "ssao",
36
+ "shadows",
37
+ "zero-dependency"
38
+ ],
39
+ "author": "Chromatic Coherence",
40
+ "license": "SEE LICENSE IN LICENSE",
41
+ "devDependencies": {
42
+ "typescript": "^5"
43
+ }
44
+ }