trid3d 0.3.0 → 0.4.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.
@@ -3,10 +3,12 @@ import { normalMatrix, determinantMat4 } from "../math/matrix.js";
3
3
  import { worldObjects } from "../sceneGraph.js";
4
4
  import { clear, edgeSegments, wireEdges } from "./draw.js";
5
5
  import { parseColor } from "./color.js";
6
- import { DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./canvasRenderer.js";
6
+ import { materialsOf } from "./canvasRenderer.js";
7
+ import { sceneLights, MAX_LIGHTS } from "./lights.js";
8
+ import { FRAME_GLSL, postPasses, activePost, runCanvasPost } from "./post.js";
7
9
  import { uvsFor, uvTransform, mappingKey } from "./uvMapping.js";
8
10
  import { newell, cornerNormals, creaseOf } from "./normals.js";
9
- import { SURFACE_GLSL, buildEffects, effectsKey } from "./effects.js";
11
+ import { SURFACE_GLSL, buildEffects, effectsKey, uniformType } from "./effects.js";
10
12
  import { textureReady, textureSize, textureIsLive, textureVersion } from "../loaders/texture.js";
11
13
 
12
14
  /** Farthest distance drawn by the WebGL renderer, unless the camera sets `far`. */
@@ -61,8 +63,14 @@ precision highp float;
61
63
  precision mediump float;
62
64
  #endif
63
65
  uniform vec4 uColor;
64
- uniform vec3 uLight;
66
+ uniform vec3 uAmbientColor;
65
67
  uniform float uAmbient;
68
+ uniform vec3 uLightDir; // the first directional light, for effects
69
+ // Directional and point lights: xyz = direction light comes from (w = 0) or position (w = 1)
70
+ uniform vec4 uLightVec[${MAX_LIGHTS}];
71
+ // rgb = color × intensity, a = range (0: no fading)
72
+ uniform vec4 uLightColor[${MAX_LIGHTS}];
73
+ uniform int uLightCount;
66
74
  uniform float uShade;
67
75
  uniform float uTime;
68
76
  uniform vec4 uBackground;
@@ -74,12 +82,37 @@ varying vec3 vWorld;
74
82
  varying vec3 vLocal;
75
83
  varying vec2 vUV;
76
84
  ${SURFACE_GLSL}
85
+ // The light reaching a point: the same math as lightAt in trid/render/lights.js
86
+ vec3 tridLighting(vec3 n, vec3 p) {
87
+ vec3 c = uAmbientColor;
88
+ for (int i = 0; i < ${MAX_LIGHTS}; i++) {
89
+ if (i >= uLightCount) break;
90
+ vec4 l = uLightVec[i];
91
+ vec3 d = l.xyz;
92
+ float fade = 1.0;
93
+ if (l.w > 0.5) {
94
+ d = l.xyz - p;
95
+ float dist = max(length(d), 1e-6);
96
+ d /= dist;
97
+ float range = uLightColor[i].a;
98
+ if (range > 0.0) {
99
+ float k = clamp(1.0 - (dist * dist) / (range * range), 0.0, 1.0);
100
+ fade = k * k;
101
+ }
102
+ }
103
+ c += uLightColor[i].rgb * max(0.0, dot(n, d)) * fade;
104
+ }
105
+ return c;
106
+ }
77
107
  ${fx.code}
78
108
  void main() {
79
109
  vec3 n = normalize(vNormal);
80
110
  if (!gl_FrontFacing) n = -n;
81
- float diffuse = max(0.0, dot(n, uLight));
82
- float light = mix(1.0, uAmbient + (1.0 - uAmbient) * diffuse, uShade);
111
+ vec3 reach = tridLighting(n, vWorld);
112
+ float level = max(reach.r, max(reach.g, reach.b));
113
+ float diffuse = clamp((level - uAmbient) / max(1.0 - uAmbient, 1e-4), 0.0, 1.0);
114
+ float light = mix(1.0, level, uShade);
115
+ vec3 lightColor = mix(vec3(1.0), reach, uShade);
83
116
 
84
117
  // Fill color, times the texture's color when there is one
85
118
  vec4 base = uColor * mix(vec4(1.0), texture2D(uTexture, vUV), uUseTexture);
@@ -91,15 +124,16 @@ void main() {
91
124
  s.position = vWorld;
92
125
  s.local = vLocal;
93
126
  s.view = normalize(uCamWorld - vWorld);
94
- s.lightDir = uLight;
127
+ s.lightDir = uLightDir;
95
128
  s.diffuse = diffuse;
96
129
  s.light = light;
130
+ s.lightColor = lightColor;
97
131
  s.ambient = uAmbient;
98
132
  s.depth = length(uCamWorld - vWorld);
99
133
  s.time = uTime;
100
134
  s.background = uBackground;
101
135
 
102
- vec4 color = vec4(base.rgb * light, base.a);
136
+ vec4 color = vec4(base.rgb * lightColor, base.a);
103
137
  ${fx.calls}
104
138
  color = clamp(color, 0.0, 1.0);
105
139
  if (color.a < 0.004) discard; // fully see-through pixels (e.g. a PNG's empty parts) don't hide what's behind
@@ -109,7 +143,7 @@ ${fx.calls}
109
143
  return { source, uniforms: fx.uniforms };
110
144
  }
111
145
 
112
- const FACE_UNIFORMS = ["uUVTransform", "uNormalMatrix", "uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
146
+ const FACE_UNIFORMS = ["uUVTransform", "uNormalMatrix", "uAmbientColor", "uAmbient", "uLightDir", "uLightVec", "uLightColor", "uLightCount", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
113
147
 
114
148
  // Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
115
149
  const LINE_VS = `
@@ -147,6 +181,48 @@ void main() {
147
181
 
148
182
  const UNIFORMS = ["uModel", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFar", "uColor"];
149
183
 
184
+ // Post effects: one big triangle covering the screen
185
+ const POST_VS = `
186
+ attribute vec2 aPos;
187
+ varying vec2 vUV;
188
+ void main() {
189
+ vUV = aPos * 0.5 + 0.5;
190
+ gl_Position = vec4(aPos, 0.0, 1.0);
191
+ }
192
+ `;
193
+
194
+ /** Fragment shader for one pass of a post effect (see trid/render/post.js). */
195
+ function postFS(glsl, uniforms) {
196
+ const decls = uniforms.map((u) => `uniform ${u.type} ${u.name};`).join("\n");
197
+ return `
198
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
199
+ precision highp float;
200
+ #else
201
+ precision mediump float;
202
+ #endif
203
+ uniform vec2 uSize;
204
+ uniform vec2 uTexel;
205
+ uniform float uTime;
206
+ uniform vec4 uBackground;
207
+ varying vec2 vUV;
208
+ ${FRAME_GLSL}
209
+ ${decls}
210
+ ${glsl}
211
+ void main() {
212
+ Frame f;
213
+ f.uv = vUV;
214
+ f.size = uSize;
215
+ f.texel = uTexel;
216
+ f.time = uTime;
217
+ f.background = uBackground;
218
+ gl_FragColor = clamp(post(frame(vUV), f), 0.0, 1.0);
219
+ }
220
+ `;
221
+ }
222
+
223
+ const POST_UNIFORMS = ["uFrame", "uSource", "uSize", "uTexel", "uTime", "uBackground"];
224
+ const COPY_GLSL = "vec4 post(vec4 color, Frame f) { return color; }";
225
+
150
226
  /**
151
227
  * Draws a scene with WebGL, on the graphics card.
152
228
  *
@@ -164,6 +240,10 @@ const UNIFORMS = ["uModel", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFa
164
240
  * Textures are sent once too (videos every frame): after redrawing a canvas,
165
241
  * call `updateTexture(canvas)`.
166
242
  *
243
+ * Post effects (`scene.post`, see trid/render/post.js) run on the graphics
244
+ * card too: the scene is drawn into a texture, then each pass draws the
245
+ * next picture from it.
246
+ *
167
247
  * Meshes and textures that haven't been drawn for `releaseAfter` frames
168
248
  * are freed from the graphics card, so replacing them (`obj.mesh = newMesh`)
169
249
  * doesn't fill its memory. They're sent again if they come back.
@@ -229,6 +309,8 @@ export class WebGLRenderer {
229
309
 
230
310
  this._faces = new Map(); // face programs, one per combination of effects
231
311
  this._line = program(gl, LINE_VS, LINE_FS, ["aA", "aB", "aCorner"], [...UNIFORMS, "uRes", "uWidth"]);
312
+ this._posts = new Map(); // post effect pass programs, one per pass code
313
+ this._post = null; // render targets for post effects, made when first needed
232
314
 
233
315
  gl.enable(gl.DEPTH_TEST);
234
316
  gl.depthFunc(gl.LEQUAL);
@@ -257,14 +339,27 @@ export class WebGLRenderer {
257
339
  this.canvas.width = w;
258
340
  this.canvas.height = h;
259
341
  }
342
+
343
+ // Post effects: the ones with GLSL run here, canvas-only ones after the copy
344
+ const post = activePost(scene.post);
345
+ const gpuPost = post.filter((e) => postPasses(e).length);
346
+ const background = this._color(ctx, scene.background);
347
+ if (gpuPost.length) {
348
+ // Into a texture, on the scene's background so effects see the whole picture
349
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._sceneTarget(w, h));
350
+ const [r, g, b, a] = background;
351
+ gl.clearColor(r * a, g * a, b * a, a);
352
+ } else {
353
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
354
+ gl.clearColor(0, 0, 0, 0);
355
+ }
260
356
  gl.viewport(0, 0, w, h);
261
- gl.clearColor(0, 0, 0, 0);
262
357
  gl.depthMask(true);
263
358
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
264
359
 
265
360
  const view = {
266
361
  ctx,
267
- background: this._color(ctx, scene.background),
362
+ background,
268
363
  time: (performance.now() - this._start) / 1000,
269
364
  camPos: [camera.x ?? 0, camera.y ?? 0, camera.z ?? 0],
270
365
  camRot: [camera.yaw ?? 0, camera.pitch ?? 0],
@@ -273,10 +368,7 @@ export class WebGLRenderer {
273
368
  near: camera.near ?? DEFAULT_NEAR,
274
369
  far: camera.far ?? DEFAULT_FAR,
275
370
  };
276
- const l = scene.light ?? DEFAULT_LIGHT;
277
- const len = Math.hypot(l.x, l.y, l.z) || 1;
278
- const light = [l.x / len, l.y / len, l.z / len];
279
- const ambient = scene.ambient ?? DEFAULT_AMBIENT;
371
+ const lights = this._lights(scene, ctx);
280
372
 
281
373
  // Opaque things first; see-through ones after, without writing depth
282
374
  const opaque = [], clear2 = [];
@@ -311,11 +403,14 @@ export class WebGLRenderer {
311
403
  }
312
404
  }
313
405
 
314
- for (const d of opaque) this._draw(d, view, light, ambient, w, h);
406
+ for (const d of opaque) this._draw(d, view, lights, w, h);
315
407
  gl.depthMask(false);
316
- for (const d of clear2) this._draw(d, view, light, ambient, w, h);
408
+ for (const d of clear2) this._draw(d, view, lights, w, h);
409
+
410
+ if (gpuPost.length) this._runPost(gpuPost, view, w, h);
317
411
 
318
412
  ctx.drawImage(this.canvas, 0, 0);
413
+ runCanvasPost(scene, post.filter((e) => !postPasses(e).length), this._start);
319
414
 
320
415
  // Meshes marked as changed have been sent again
321
416
  for (const obj of drawn) if (obj.mesh.dirty) obj.mesh.dirty = false;
@@ -361,6 +456,211 @@ export class WebGLRenderer {
361
456
  const oldest = this._frame - this.releaseAfter;
362
457
  for (const [item, frame] of this._meshUse) if (frame < oldest) this.release(item);
363
458
  for (const [item, frame] of this._textureUse) if (frame < oldest) this.release(item);
459
+ // Post effect pictures of a size no longer used (after a resize, or effects removed)
460
+ if (this._post) {
461
+ const keep = [];
462
+ for (const t of this._post.pool) {
463
+ if (t.used < this._frame - 60) this._freeTarget(t);
464
+ else keep.push(t);
465
+ }
466
+ this._post.pool = keep;
467
+ if (this._post.used < this._frame - 60) this._freePost();
468
+ }
469
+ }
470
+
471
+ // --- Post effects ---
472
+
473
+ /**
474
+ * The framebuffer the scene is drawn into when there are post effects:
475
+ * multisampled on WebGL 2 (copied into a texture by `_runPost`), else
476
+ * straight into a texture.
477
+ */
478
+ _sceneTarget(w, h) {
479
+ const gl = this.gl;
480
+ let P = this._post;
481
+ if (P && (P.w !== w || P.h !== h)) {
482
+ this._freePost();
483
+ P = null;
484
+ }
485
+ if (!P) {
486
+ P = this._post = { w, h, pool: [], used: this._frame };
487
+ P.scene = this._makeTarget(w, h, true);
488
+ if (this._webgl2 && this._options.antialias) {
489
+ const samples = Math.min(4, gl.getParameter(gl.MAX_SAMPLES) || 0);
490
+ if (samples > 1) {
491
+ const fb = gl.createFramebuffer();
492
+ const color = gl.createRenderbuffer();
493
+ gl.bindRenderbuffer(gl.RENDERBUFFER, color);
494
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, w, h);
495
+ const depth = gl.createRenderbuffer();
496
+ gl.bindRenderbuffer(gl.RENDERBUFFER, depth);
497
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.DEPTH_COMPONENT24, w, h);
498
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
499
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color);
500
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depth);
501
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) P.msaa = { fb, color, depth };
502
+ else {
503
+ gl.deleteFramebuffer(fb);
504
+ gl.deleteRenderbuffer(color);
505
+ gl.deleteRenderbuffer(depth);
506
+ }
507
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
508
+ }
509
+ }
510
+ const quad = gl.createBuffer();
511
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
512
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
513
+ P.quad = quad;
514
+ }
515
+ P.used = this._frame;
516
+ return P.msaa ? P.msaa.fb : P.scene.fb;
517
+ }
518
+
519
+ /** A texture to draw into, `w` × `h`, with a depth buffer when `depth`. */
520
+ _makeTarget(w, h, depth = false) {
521
+ const gl = this.gl;
522
+ const tex = gl.createTexture();
523
+ gl.bindTexture(gl.TEXTURE_2D, tex);
524
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
525
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
526
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
527
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
528
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
529
+ const fb = gl.createFramebuffer();
530
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
531
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
532
+ let rb = null;
533
+ if (depth) {
534
+ rb = gl.createRenderbuffer();
535
+ gl.bindRenderbuffer(gl.RENDERBUFFER, rb);
536
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h);
537
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rb);
538
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
539
+ }
540
+ gl.bindTexture(gl.TEXTURE_2D, null);
541
+ return { fb, tex, depth: rb, w, h, used: this._frame };
542
+ }
543
+
544
+ _freeTarget(t) {
545
+ const gl = this.gl;
546
+ gl.deleteFramebuffer(t.fb);
547
+ gl.deleteTexture(t.tex);
548
+ if (t.depth) gl.deleteRenderbuffer(t.depth);
549
+ }
550
+
551
+ /** Frees every post effect render target. */
552
+ _freePost() {
553
+ const P = this._post;
554
+ if (!P) return;
555
+ const gl = this.gl;
556
+ this._freeTarget(P.scene);
557
+ for (const t of P.pool) this._freeTarget(t);
558
+ if (P.msaa) {
559
+ gl.deleteFramebuffer(P.msaa.fb);
560
+ gl.deleteRenderbuffer(P.msaa.color);
561
+ gl.deleteRenderbuffer(P.msaa.depth);
562
+ }
563
+ gl.deleteBuffer(P.quad);
564
+ this._post = null;
565
+ }
566
+
567
+ /** A pool picture of a size that isn't one of `busy`. */
568
+ _poolTarget(w, h, busy) {
569
+ const P = this._post;
570
+ let t = P.pool.find((x) => x.w === w && x.h === h && !busy.includes(x));
571
+ if (!t) {
572
+ t = this._makeTarget(w, h);
573
+ P.pool.push(t);
574
+ }
575
+ t.used = this._frame;
576
+ return t;
577
+ }
578
+
579
+ /** The program for one pass's code, compiled once. Null if it doesn't compile (reported once). */
580
+ _postProgram(glsl, uniforms) {
581
+ const list = Object.entries(uniforms ?? {})
582
+ .map(([name, value]) => ({ name, type: uniformType(value) }))
583
+ .filter((u) => u.type);
584
+ const key = glsl + "|" + list.map((u) => u.name + ":" + u.type).join(",");
585
+ if (this._posts.has(key)) return this._posts.get(key);
586
+ let p = null;
587
+ try {
588
+ p = program(this.gl, POST_VS, postFS(glsl, list), ["aPos"], [...POST_UNIFORMS, ...list.map((u) => u.name)]);
589
+ p.list = list;
590
+ } catch (err) {
591
+ console.error("Trid: a post effect's shader failed to compile, skipping it.\n" + err.message);
592
+ }
593
+ this._posts.set(key, p);
594
+ return p;
595
+ }
596
+
597
+ /**
598
+ * Runs the post effects on the scene's picture, then draws the result on
599
+ * the WebGL canvas.
600
+ */
601
+ _runPost(effects, view, w, h) {
602
+ const gl = this.gl;
603
+ const P = this._post;
604
+ // Multisampled drawing: resolve it into the scene's texture
605
+ if (P.msaa) {
606
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, P.msaa.fb);
607
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, P.scene.fb);
608
+ gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, gl.COLOR_BUFFER_BIT, gl.NEAREST);
609
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
610
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
611
+ }
612
+
613
+ gl.disable(gl.DEPTH_TEST);
614
+ gl.disable(gl.BLEND);
615
+ gl.disable(gl.CULL_FACE);
616
+ gl.disable(gl.POLYGON_OFFSET_FILL);
617
+ gl.bindBuffer(gl.ARRAY_BUFFER, P.quad);
618
+
619
+ const pass = (prog, input, source, target, tw, th, uniforms) => {
620
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fb : null);
621
+ gl.viewport(0, 0, tw, th);
622
+ gl.useProgram(prog.program);
623
+ const u = prog.uniforms;
624
+ gl.activeTexture(gl.TEXTURE0);
625
+ gl.bindTexture(gl.TEXTURE_2D, input.tex);
626
+ gl.uniform1i(u.uFrame, 0);
627
+ gl.activeTexture(gl.TEXTURE1);
628
+ gl.bindTexture(gl.TEXTURE_2D, source.tex);
629
+ gl.uniform1i(u.uSource, 1);
630
+ gl.uniform2f(u.uSize, tw, th);
631
+ gl.uniform2f(u.uTexel, 1 / input.w, 1 / input.h);
632
+ gl.uniform1f(u.uTime, view.time);
633
+ gl.uniform4fv(u.uBackground, view.background);
634
+ for (const x of prog.list) this._setUniform(view.ctx, u[x.name], x.type, uniforms?.[x.name]);
635
+ const a = prog.attributes.aPos;
636
+ gl.enableVertexAttribArray(a);
637
+ gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0);
638
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
639
+ gl.disableVertexAttribArray(a);
640
+ };
641
+
642
+ let source = P.scene; // the picture the current effect starts from
643
+ for (const e of effects) {
644
+ let prev = source;
645
+ for (const ps of postPasses(e)) {
646
+ const prog = this._postProgram(ps.glsl, e.uniforms);
647
+ if (!prog) continue;
648
+ const tw = Math.max(1, Math.round(w * ps.scale)), th = Math.max(1, Math.round(h * ps.scale));
649
+ const target = this._poolTarget(tw, th, [source, prev]);
650
+ pass(prog, prev, source, target, tw, th, e.uniforms);
651
+ prev = target;
652
+ }
653
+ source = prev;
654
+ }
655
+
656
+ // Onto the canvas
657
+ pass(this._postProgram(COPY_GLSL, {}), source, source, null, w, h, {});
658
+
659
+ gl.activeTexture(gl.TEXTURE1);
660
+ gl.bindTexture(gl.TEXTURE_2D, null);
661
+ gl.activeTexture(gl.TEXTURE0);
662
+ gl.enable(gl.DEPTH_TEST);
663
+ gl.enable(gl.BLEND);
364
664
  }
365
665
 
366
666
  /** Frees the graphics card memory. The renderer can't be used afterwards. */
@@ -375,11 +675,29 @@ export class WebGLRenderer {
375
675
  this._meshUse.clear();
376
676
  this._textureUse.clear();
377
677
  for (const p of new Set(this._faces.values())) gl.deleteProgram(p.program);
678
+ for (const p of this._posts.values()) if (p) gl.deleteProgram(p.program);
378
679
  gl.deleteProgram(this._line.program);
680
+ this._freePost();
379
681
  gl.getExtension("WEBGL_lose_context")?.loseContext();
380
682
  }
381
683
 
382
- _draw(d, view, light, ambient, w, h) {
684
+ /** The scene's lights, packed for the face shader's uniforms. */
685
+ _lights(scene, ctx) {
686
+ const L = sceneLights(scene, (css) => this._color(ctx, css));
687
+ const vec = new Float32Array(MAX_LIGHTS * 4), color = new Float32Array(MAX_LIGHTS * 4);
688
+ L.lights.forEach((l, i) => {
689
+ const p = l.point ? l.pos : l.dir;
690
+ vec.set([p.x, p.y, p.z, l.point ? 1 : 0], i * 4);
691
+ color.set([l.color[0], l.color[1], l.color[2], l.range], i * 4);
692
+ });
693
+ return {
694
+ vec, color, count: L.lights.length,
695
+ ambient: L.ambient, ambientLevel: L.ambientLevel,
696
+ dir: [L.mainDir.x, L.mainDir.y, L.mainDir.z],
697
+ };
698
+ }
699
+
700
+ _draw(d, view, lights, w, h) {
383
701
  const gl = this.gl;
384
702
  const isFace = d.kind === "face";
385
703
  const effects = isFace ? d.mat.effects ?? [] : null;
@@ -409,8 +727,12 @@ export class WebGLRenderer {
409
727
  gl.uniformMatrix3fv(u.uUVTransform, false, [m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
410
728
  gl.uniformMatrix3fv(u.uNormalMatrix, false, d.place.normals);
411
729
  gl.frontFace(d.place.mirrored ? gl.CCW : gl.CW);
412
- gl.uniform3fv(u.uLight, light);
413
- gl.uniform1f(u.uAmbient, ambient);
730
+ gl.uniform3fv(u.uAmbientColor, lights.ambient);
731
+ gl.uniform1f(u.uAmbient, lights.ambientLevel);
732
+ gl.uniform3fv(u.uLightDir, lights.dir);
733
+ gl.uniform4fv(u.uLightVec, lights.vec);
734
+ gl.uniform4fv(u.uLightColor, lights.color);
735
+ gl.uniform1i(u.uLightCount, lights.count);
414
736
  gl.uniform1f(u.uShade, d.mat.shade === false ? 0 : 1);
415
737
  gl.uniform1f(u.uTime, view.time);
416
738
  gl.uniform4fv(u.uBackground, view.background);
package/trid/scene.js CHANGED
@@ -1,4 +1,5 @@
1
- import { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
1
+ import { CanvasRenderer } from "./render/canvasRenderer.js";
2
+ import { DEFAULT_LIGHT, DEFAULT_AMBIENT, defaultLights } from "./render/lights.js";
2
3
  import { loop } from "./render/loop.js";
3
4
  import { ToolManager } from "./tools/manager.js";
4
5
  import { linkTree } from "./sceneGraph.js";
@@ -18,6 +19,10 @@ import { screenshot, record } from "./export/capture.js";
18
19
  * `material` draws its mesh's faces (filled and shaded) instead of
19
20
  * only lines: `{ mesh, material: { fill: "#E9806E" } }`.
20
21
  *
22
+ * Lights are a list, `scene.lights` (see `trid/render/lights.js`): by
23
+ * default an ambient light and one directional light. Post effects
24
+ * (bloom, vignette...) are a list too, `scene.post` (see `trid/render/post.js`).
25
+ *
21
26
  * Tools (camera controls, dragging, overlays...) are added with
22
27
  * `use`. See `trid/tools/`.
23
28
  *
@@ -37,8 +42,11 @@ export class Scene {
37
42
  * @param {number} [opts.lineWidth=3] - Default wireframe width
38
43
  * @param {import("./math/transform.js").Camera} [opts.camera={ z: -1 }] - Camera
39
44
  * @param {{x:number,y:number,z:number}} [opts.light] - Direction light comes from
40
- * (default: above, left and in front)
41
- * @param {number} [opts.ambient=0.25] - Brightness of faces turned away from the light (0..1)
45
+ * (default: above, left and in front). Ignored when `lights` is given
46
+ * @param {number} [opts.ambient=0.25] - Brightness of faces turned away from the light (0..1).
47
+ * The directional light gets `1 - ambient`. Ignored when `lights` is given
48
+ * @param {Object[]} [opts.lights] - Every light (see trid/render/lights.js), instead of `light` and `ambient`
49
+ * @param {Object[]} [opts.post=[]] - Post effects run on the picture (see trid/render/post.js)
42
50
  * @param {{ render(scene:Scene): void, dispose?(): void }} [opts.renderer] - What draws the scene
43
51
  * (default: a new `CanvasRenderer`; or a `WebGLRenderer`)
44
52
  */
@@ -49,8 +57,10 @@ export class Scene {
49
57
  color = "#ffffff",
50
58
  lineWidth = 3,
51
59
  camera = { z: -1 },
52
- light = { ...DEFAULT_LIGHT },
60
+ light = DEFAULT_LIGHT,
53
61
  ambient = DEFAULT_AMBIENT,
62
+ lights,
63
+ post = [],
54
64
  renderer = new CanvasRenderer(),
55
65
  } = {}) {
56
66
  this.canvas = canvas;
@@ -62,8 +72,10 @@ export class Scene {
62
72
  this.color = color;
63
73
  this.lineWidth = lineWidth;
64
74
  this.camera = camera;
65
- this.light = light;
66
- this.ambient = ambient;
75
+ /** Every light. Edit it freely: push, splice, change fields; it's read every frame. */
76
+ this.lights = lights ?? defaultLights(light, ambient);
77
+ /** Post effects, run in order on the picture before tools draw. */
78
+ this.post = post;
67
79
  this.renderer = renderer;
68
80
  this.objects = [];
69
81
  this.tools = new ToolManager(this);
@@ -194,6 +206,36 @@ export class Scene {
194
206
  this.renderer.dispose?.();
195
207
  }
196
208
 
209
+ /**
210
+ * Shortcut for the direction of the first directional light in `lights`
211
+ * (null if there's none). Change its fields, or set a new direction;
212
+ * setting one when there's no directional light adds one.
213
+ */
214
+ get light() {
215
+ return this.lights.find((l) => l?.type === "directional")?.direction ?? null;
216
+ }
217
+
218
+ set light(direction) {
219
+ const l = this.lights.find((x) => x?.type === "directional");
220
+ if (l) l.direction = direction;
221
+ else if (direction) this.lights.push({ type: "directional", direction, color: "#ffffff", intensity: 1 - this.ambient });
222
+ }
223
+
224
+ /**
225
+ * Shortcut for the intensity of the first ambient light in `lights`
226
+ * (0 if there's none). Setting it when there's none adds one. It only
227
+ * changes the ambient light: the directional light keeps its intensity.
228
+ */
229
+ get ambient() {
230
+ return this.lights.find((l) => l?.type === "ambient")?.intensity ?? 0;
231
+ }
232
+
233
+ set ambient(value) {
234
+ const l = this.lights.find((x) => x?.type === "ambient");
235
+ if (l) l.intensity = value;
236
+ else this.lights.unshift({ type: "ambient", color: "#ffffff", intensity: value });
237
+ }
238
+
197
239
  /** Whether the frame loop is running. */
198
240
  get running() {
199
241
  return this._stop !== null;