trid3d 0.5.0 → 0.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.
@@ -5,7 +5,8 @@ import { clear, edgeSegments, wireEdges } from "./draw.js";
5
5
  import { parseColor } from "./color.js";
6
6
  import { materialsOf } from "./canvasRenderer.js";
7
7
  import { sceneLights, MAX_LIGHTS } from "./lights.js";
8
- import { FRAME_GLSL, postPasses, activePost, runCanvasPost } from "./post.js";
8
+ import { shadowFrame, shadowFlag } from "./shadows.js";
9
+ import { FRAME_GLSL, GEOMETRY_GLSL, postPasses, activePost, runCanvasPost, wantsGeometry, maskedObjects } from "./post.js";
9
10
  import { uvsFor, uvTransform, mappingKey } from "./uvMapping.js";
10
11
  import { newell, cornerNormals, creaseOf } from "./normals.js";
11
12
  import { SURFACE_GLSL, buildEffects, effectsKey, uniformType } from "./effects.js";
@@ -77,12 +78,40 @@ uniform vec4 uBackground;
77
78
  uniform vec3 uCamWorld;
78
79
  uniform sampler2D uTexture;
79
80
  uniform float uUseTexture;
81
+ // Shadows (trid/render/shadows.js): which light casts them (-1: none), and its shadow map
82
+ uniform int uShadowLight;
83
+ uniform sampler2D uShadowMap;
84
+ uniform mat4 uShadowMatrix;
85
+ uniform float uShadowTexel; // one pixel of the map, in its 0..1 units
86
+ uniform float uShadowSoft; // edge blur, in map pixels
87
+ uniform float uShadowOffset; // how far the test point moves along the normal, in world units
88
+ uniform float uShadowBias;
89
+ uniform float uReceive;
80
90
  varying vec3 vNormal;
81
91
  varying vec3 vWorld;
82
92
  varying vec3 vLocal;
83
93
  varying vec2 vUV;
84
94
  ${SURFACE_GLSL}
85
- // The light reaching a point: the same math as lightAt in trid/render/lights.js
95
+ float tridShadowDepth(vec2 uv) {
96
+ return dot(texture2D(uShadowMap, uv), vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0));
97
+ }
98
+ // How much of the shadow-casting light reaches a point: 1 = all, 0 = in shadow
99
+ float tridShadow(vec3 n, vec3 p) {
100
+ vec4 q = uShadowMatrix * vec4(p + n * uShadowOffset, 1.0);
101
+ vec3 c = q.xyz * 0.5 + 0.5;
102
+ if (c.x < 0.0 || c.x > 1.0 || c.y < 0.0 || c.y > 1.0 || c.z < 0.0) return 1.0;
103
+ float z = min(c.z, 0.999) - uShadowBias; // past the casters: compared as just behind them
104
+ float lit = 0.0;
105
+ // 4 × 4 samples spread over the blur: soft edges
106
+ for (int i = 0; i < 4; i++) {
107
+ for (int j = 0; j < 4; j++) {
108
+ vec2 o = (vec2(float(i), float(j)) - 1.5) / 1.5 * uShadowSoft * uShadowTexel;
109
+ lit += z <= tridShadowDepth(c.xy + o) ? 1.0 : 0.0;
110
+ }
111
+ }
112
+ return lit / 16.0;
113
+ }
114
+ // The light reaching a point: the same math as lightAt in trid/render/lights.js, with shadows
86
115
  vec3 tridLighting(vec3 n, vec3 p) {
87
116
  vec3 c = uAmbientColor;
88
117
  for (int i = 0; i < ${MAX_LIGHTS}; i++) {
@@ -100,7 +129,9 @@ vec3 tridLighting(vec3 n, vec3 p) {
100
129
  fade = k * k;
101
130
  }
102
131
  }
103
- c += uLightColor[i].rgb * max(0.0, dot(n, d)) * fade;
132
+ float k = max(0.0, dot(n, d)) * fade;
133
+ if (i == uShadowLight && uReceive > 0.5 && k > 0.0) k *= tridShadow(n, p);
134
+ c += uLightColor[i].rgb * k;
104
135
  }
105
136
  return c;
106
137
  }
@@ -143,7 +174,8 @@ ${fx.calls}
143
174
  return { source, uniforms: fx.uniforms };
144
175
  }
145
176
 
146
- const FACE_UNIFORMS = ["uUVTransform", "uNormalMatrix", "uAmbientColor", "uAmbient", "uLightDir", "uLightVec", "uLightColor", "uLightCount", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
177
+ const FACE_UNIFORMS = ["uUVTransform", "uNormalMatrix", "uAmbientColor", "uAmbient", "uLightDir", "uLightVec", "uLightColor", "uLightCount", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture",
178
+ "uShadowLight", "uShadowMap", "uShadowMatrix", "uShadowTexel", "uShadowSoft", "uShadowOffset", "uShadowBias", "uReceive"];
147
179
 
148
180
  // Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
149
181
  const LINE_VS = `
@@ -181,6 +213,94 @@ void main() {
181
213
 
182
214
  const UNIFORMS = ["uModel", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFar", "uColor"];
183
215
 
216
+ // The shadow map: each pixel keeps the distance to the nearest face, seen from the light
217
+ const SHADOW_VS = `
218
+ attribute vec3 aPos;
219
+ attribute vec2 aUV;
220
+ uniform mat4 uModel;
221
+ uniform mat4 uShadowMatrix;
222
+ uniform mat3 uUVTransform;
223
+ varying vec2 vUV;
224
+ void main() {
225
+ vUV = (uUVTransform * vec3(aUV, 1.0)).xy;
226
+ gl_Position = uShadowMatrix * (uModel * vec4(aPos, 1.0));
227
+ }
228
+ `;
229
+
230
+ const SHADOW_FS = `
231
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
232
+ precision highp float;
233
+ #else
234
+ precision mediump float;
235
+ #endif
236
+ uniform vec4 uColor;
237
+ uniform sampler2D uTexture;
238
+ uniform float uUseTexture;
239
+ varying vec2 vUV;
240
+ void main() {
241
+ // See-through parts of a texture cast no shadow
242
+ if (uColor.a * mix(1.0, texture2D(uTexture, vUV).a, uUseTexture) < 0.5) discard;
243
+ // The distance (0..1) spread over four bytes
244
+ vec4 e = fract(gl_FragCoord.z * vec4(1.0, 255.0, 65025.0, 16581375.0));
245
+ gl_FragColor = e - e.yzww * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0);
246
+ }
247
+ `;
248
+
249
+ // Depth and normals of the faces, for post effects with `geometry: true`
250
+ // (the packing is read back by GEOMETRY_GLSL in trid/render/post.js)
251
+ const GEO_VS = `
252
+ attribute vec3 aPos;
253
+ attribute vec3 aNormal;
254
+ attribute vec2 aUV;
255
+ varying vec3 vNormal; // camera space
256
+ varying vec2 vUV;
257
+ varying float vDepth;
258
+ uniform mat3 uUVTransform;
259
+ uniform mat3 uNormalMatrix;
260
+ ${COMMON}
261
+ void main() {
262
+ vec3 n = uNormalMatrix * aNormal;
263
+ vNormal = rotX(rotY(n, uCamRot.x), uCamRot.y);
264
+ vUV = (uUVTransform * vec3(aUV, 1.0)).xy;
265
+ vec3 c = toCamera(toWorld(aPos));
266
+ vDepth = c.z;
267
+ gl_Position = toClip(c);
268
+ }
269
+ `;
270
+
271
+ const GEO_FS = `
272
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
273
+ precision highp float;
274
+ #else
275
+ precision mediump float;
276
+ #endif
277
+ uniform vec4 uColor;
278
+ uniform sampler2D uTexture;
279
+ uniform float uUseTexture;
280
+ uniform float uGeoNear;
281
+ uniform float uGeoRange;
282
+ uniform float uMask;
283
+ varying vec3 vNormal;
284
+ varying vec2 vUV;
285
+ varying float vDepth;
286
+ void main() {
287
+ // See-through parts of a texture don't count, like in the face shader
288
+ if (uColor.a * mix(1.0, texture2D(uTexture, vUV).a, uUseTexture) < 0.004) discard;
289
+ vec3 n = normalize(vNormal);
290
+ if (!gl_FrontFacing) n = -n;
291
+ // Octahedral packing: the unit normal folded onto a square
292
+ n /= abs(n.x) + abs(n.y) + abs(n.z);
293
+ vec2 p = n.z >= 0.0 ? n.xy : (1.0 - abs(n.yx)) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);
294
+ vec2 rg = floor((p * 0.5 + 0.5) * 255.0 + 0.5);
295
+ // Distance on a log scale, 15 bits: 0 = near, 32766 = far (32767: nothing drawn)
296
+ float d = clamp(log(max(vDepth, uGeoNear) / uGeoNear) / uGeoRange, 0.0, 1.0);
297
+ float q = floor(d * 32766.0 + 0.5);
298
+ float hi = floor(q / 128.0);
299
+ float lo = q - hi * 128.0;
300
+ gl_FragColor = vec4(rg, hi, lo * 2.0 + uMask) / 255.0;
301
+ }
302
+ `;
303
+
184
304
  // Post effects: one big triangle covering the screen
185
305
  const POST_VS = `
186
306
  attribute vec2 aPos;
@@ -192,7 +312,7 @@ void main() {
192
312
  `;
193
313
 
194
314
  /** Fragment shader for one pass of a post effect (see trid/render/post.js). */
195
- function postFS(glsl, uniforms) {
315
+ function postFS(glsl, uniforms, geometry = false) {
196
316
  const decls = uniforms.map((u) => `uniform ${u.type} ${u.name};`).join("\n");
197
317
  return `
198
318
  #ifdef GL_FRAGMENT_PRECISION_HIGH
@@ -204,8 +324,10 @@ uniform vec2 uSize;
204
324
  uniform vec2 uTexel;
205
325
  uniform float uTime;
206
326
  uniform vec4 uBackground;
327
+ uniform float uNear, uFar;
207
328
  varying vec2 vUV;
208
329
  ${FRAME_GLSL}
330
+ ${geometry ? GEOMETRY_GLSL : ""}
209
331
  ${decls}
210
332
  ${glsl}
211
333
  void main() {
@@ -215,12 +337,14 @@ void main() {
215
337
  f.texel = uTexel;
216
338
  f.time = uTime;
217
339
  f.background = uBackground;
340
+ f.near = uNear;
341
+ f.far = uFar;
218
342
  gl_FragColor = clamp(post(frame(vUV), f), 0.0, 1.0);
219
343
  }
220
344
  `;
221
345
  }
222
346
 
223
- const POST_UNIFORMS = ["uFrame", "uSource", "uSize", "uTexel", "uTime", "uBackground"];
347
+ const POST_UNIFORMS = ["uFrame", "uSource", "uSize", "uTexel", "uTime", "uBackground", "uNear", "uFar", "uGeometry", "uGeoNear", "uGeoRange"];
224
348
  const COPY_GLSL = "vec4 post(vec4 color, Frame f) { return color; }";
225
349
 
226
350
  /**
@@ -242,7 +366,8 @@ const COPY_GLSL = "vec4 post(vec4 color, Frame f) { return color; }";
242
366
  *
243
367
  * Post effects (`scene.post`, see trid/render/post.js) run on the graphics
244
368
  * card too: the scene is drawn into a texture, then each pass draws the
245
- * next picture from it.
369
+ * next picture from it. Effects with `geometry: true` (like `outline()`) also
370
+ * get the faces' depth and normals, drawn in one extra pass.
246
371
  *
247
372
  * Meshes and textures that haven't been drawn for `releaseAfter` frames
248
373
  * are freed from the graphics card, so replacing them (`obj.mesh = newMesh`)
@@ -311,6 +436,8 @@ export class WebGLRenderer {
311
436
  this._line = program(gl, LINE_VS, LINE_FS, ["aA", "aB", "aCorner"], [...UNIFORMS, "uRes", "uWidth"]);
312
437
  this._posts = new Map(); // post effect pass programs, one per pass code
313
438
  this._post = null; // render targets for post effects, made when first needed
439
+ this._geo = null; // depth and normals program, for post effects with `geometry`
440
+ this._shadow = null; // shadow map and its program, made when a light first casts shadows
314
441
 
315
442
  gl.enable(gl.DEPTH_TEST);
316
443
  gl.depthFunc(gl.LEQUAL);
@@ -343,7 +470,7 @@ export class WebGLRenderer {
343
470
  // Post effects: the ones with GLSL run here, canvas-only ones after the copy
344
471
  const post = activePost(scene.post);
345
472
  const gpuPost = post.filter((e) => postPasses(e).length);
346
- const background = this._color(ctx, scene.background);
473
+ const background = this._color(ctx, scene.background ?? "transparent");
347
474
  if (gpuPost.length) {
348
475
  // Into a texture, on the scene's background so effects see the whole picture
349
476
  gl.bindFramebuffer(gl.FRAMEBUFFER, this._sceneTarget(w, h));
@@ -403,11 +530,27 @@ export class WebGLRenderer {
403
530
  }
404
531
  }
405
532
 
533
+ // Shadows: drawn from the light first, then the main framebuffer again
534
+ if (lights.shadow) {
535
+ lights.shadow.map = this._shadowPass(lights.shadow, opaque);
536
+ gl.bindFramebuffer(gl.FRAMEBUFFER, gpuPost.length ? this._sceneTarget(w, h) : null);
537
+ gl.viewport(0, 0, w, h);
538
+ }
539
+
406
540
  for (const d of opaque) this._draw(d, view, lights, w, h);
407
541
  gl.depthMask(false);
408
542
  for (const d of clear2) this._draw(d, view, lights, w, h);
409
543
 
410
- if (gpuPost.length) this._runPost(gpuPost, view, w, h);
544
+ if (gpuPost.length) {
545
+ // Depth and normals for the effects that read them: one picture per `objects` list
546
+ const geometry = new Map();
547
+ for (const e of gpuPost) {
548
+ if (!wantsGeometry(e)) continue;
549
+ const key = Array.isArray(e.objects) ? e.objects : null;
550
+ if (!geometry.has(key)) geometry.set(key, this._geometryPass(opaque, view, w, h, maskedObjects(e), key));
551
+ }
552
+ this._runPost(gpuPost, view, w, h, geometry);
553
+ }
411
554
 
412
555
  ctx.drawImage(this.canvas, 0, 0);
413
556
  runCanvasPost(scene, post.filter((e) => !postPasses(e).length), this._start);
@@ -456,6 +599,7 @@ export class WebGLRenderer {
456
599
  const oldest = this._frame - this.releaseAfter;
457
600
  for (const [item, frame] of this._meshUse) if (frame < oldest) this.release(item);
458
601
  for (const [item, frame] of this._textureUse) if (frame < oldest) this.release(item);
602
+ if (this._shadow && this._shadow.used < this._frame - 60) this._freeShadow();
459
603
  // Post effect pictures of a size no longer used (after a resize, or effects removed)
460
604
  if (this._post) {
461
605
  const keep = [];
@@ -464,6 +608,12 @@ export class WebGLRenderer {
464
608
  else keep.push(t);
465
609
  }
466
610
  this._post.pool = keep;
611
+ for (const [key, t] of this._post.geometry) {
612
+ if (t.used < this._frame - 60) {
613
+ this._freeTarget(t);
614
+ this._post.geometry.delete(key);
615
+ }
616
+ }
467
617
  if (this._post.used < this._frame - 60) this._freePost();
468
618
  }
469
619
  }
@@ -483,7 +633,7 @@ export class WebGLRenderer {
483
633
  P = null;
484
634
  }
485
635
  if (!P) {
486
- P = this._post = { w, h, pool: [], used: this._frame };
636
+ P = this._post = { w, h, pool: [], geometry: new Map(), used: this._frame };
487
637
  P.scene = this._makeTarget(w, h, true);
488
638
  if (this._webgl2 && this._options.antialias) {
489
639
  const samples = Math.min(4, gl.getParameter(gl.MAX_SAMPLES) || 0);
@@ -555,6 +705,7 @@ export class WebGLRenderer {
555
705
  const gl = this.gl;
556
706
  this._freeTarget(P.scene);
557
707
  for (const t of P.pool) this._freeTarget(t);
708
+ for (const t of P.geometry.values()) this._freeTarget(t);
558
709
  if (P.msaa) {
559
710
  gl.deleteFramebuffer(P.msaa.fb);
560
711
  gl.deleteRenderbuffer(P.msaa.color);
@@ -576,16 +727,166 @@ export class WebGLRenderer {
576
727
  return t;
577
728
  }
578
729
 
730
+ /**
731
+ * Draws the shadow map for the light casting shadows: the opaque faces of
732
+ * objects that cast them, seen from the light, in a box fitted around them.
733
+ *
734
+ * @returns {{ tex:WebGLTexture, matrix:Float32Array, size:number, radius:number } | null}
735
+ */
736
+ _shadowPass(shadow, draws) {
737
+ const gl = this.gl;
738
+ const casters = draws.filter((d) => d.kind === "face" && shadowFlag(d.obj, "castShadow"));
739
+ const frame = shadowFrame([...new Set(casters.map((d) => d.obj))], shadow.dir);
740
+ if (!frame) return null;
741
+ const size = Math.min(shadow.size, gl.getParameter(gl.MAX_TEXTURE_SIZE) || shadow.size);
742
+
743
+ let S = this._shadow;
744
+ if (S && S.size !== size) {
745
+ this._freeShadow();
746
+ S = null;
747
+ }
748
+ if (!S) {
749
+ S = this._shadow = { size, target: this._makeTarget(size, size, true), used: this._frame };
750
+ gl.bindTexture(gl.TEXTURE_2D, S.target.tex);
751
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); // packed distances: never blended
752
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
753
+ gl.bindTexture(gl.TEXTURE_2D, null);
754
+ }
755
+ S.used = this._frame;
756
+ S.prog ??= program(gl, SHADOW_VS, SHADOW_FS, ["aPos", "aUV"], ["uModel", "uShadowMatrix", "uUVTransform", "uColor", "uTexture", "uUseTexture"]);
757
+ const prog = S.prog, u = prog.uniforms, a = prog.attributes;
758
+
759
+ gl.bindFramebuffer(gl.FRAMEBUFFER, S.target.fb);
760
+ gl.viewport(0, 0, size, size);
761
+ gl.clearColor(1, 1, 1, 1); // the farthest distance: nothing there
762
+ gl.depthMask(true);
763
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
764
+ gl.disable(gl.BLEND);
765
+ gl.disable(gl.CULL_FACE); // both sides cast: open meshes don't leak light
766
+ gl.disable(gl.POLYGON_OFFSET_FILL);
767
+ gl.useProgram(prog.program);
768
+ gl.uniformMatrix4fv(u.uShadowMatrix, false, frame.matrix);
769
+ gl.uniform1i(u.uTexture, 0);
770
+ const locs = [a.aPos, a.aUV].filter((loc) => loc >= 0);
771
+ for (const loc of locs) gl.enableVertexAttribArray(loc);
772
+
773
+ for (const d of casters) {
774
+ if (!d.count) continue;
775
+ const buf = this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth), d.mat.mapping);
776
+ gl.uniformMatrix4fv(u.uModel, false, d.obj.world);
777
+ gl.uniform4fv(u.uColor, d.color);
778
+ const m = uvTransform(d.mat) ?? [1, 0, 0, 1, 0, 0];
779
+ gl.uniformMatrix3fv(u.uUVTransform, false, [m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
780
+ const tex = d.mat.texture
781
+ ? this._texture(d.mat.texture, d.mat, d.obj.mesh, !!uvsFor(d.obj.mesh, d.mat.mapping).faceUVs)
782
+ : null;
783
+ gl.activeTexture(gl.TEXTURE0);
784
+ gl.bindTexture(gl.TEXTURE_2D, tex);
785
+ gl.uniform1f(u.uUseTexture, tex ? 1 : 0);
786
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
787
+ gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
788
+ if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
789
+ gl.drawArrays(gl.TRIANGLES, d.first, d.count);
790
+ }
791
+
792
+ for (const loc of locs) gl.disableVertexAttribArray(loc);
793
+ gl.enable(gl.BLEND);
794
+ return { tex: S.target.tex, matrix: frame.matrix, size, radius: frame.radius };
795
+ }
796
+
797
+ /** Frees the shadow map and its program. */
798
+ _freeShadow() {
799
+ const S = this._shadow;
800
+ if (!S) return;
801
+ this._freeTarget(S.target);
802
+ if (S.prog) this.gl.deleteProgram(S.prog.program);
803
+ this._shadow = null;
804
+ }
805
+
806
+ /**
807
+ * Draws the opaque faces' depth and normals (see GEO_FS) into a picture
808
+ * kept for this `objects` list. `mask`: the objects marked in it (null: all).
809
+ */
810
+ _geometryPass(draws, view, w, h, mask, key) {
811
+ const gl = this.gl;
812
+ const P = this._post;
813
+ let t = P.geometry.get(key);
814
+ if (!t) {
815
+ t = this._makeTarget(w, h, true);
816
+ // Packed numbers: never blend neighboring pixels
817
+ gl.bindTexture(gl.TEXTURE_2D, t.tex);
818
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
819
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
820
+ gl.bindTexture(gl.TEXTURE_2D, null);
821
+ P.geometry.set(key, t);
822
+ }
823
+ t.used = this._frame;
824
+ this._geo ??= program(gl, GEO_VS, GEO_FS, ["aPos", "aNormal", "aUV"],
825
+ [...UNIFORMS, "uUVTransform", "uNormalMatrix", "uTexture", "uUseTexture", "uGeoNear", "uGeoRange", "uMask"]);
826
+ const prog = this._geo;
827
+ const u = prog.uniforms, a = prog.attributes;
828
+
829
+ gl.bindFramebuffer(gl.FRAMEBUFFER, t.fb);
830
+ gl.viewport(0, 0, w, h);
831
+ gl.clearColor(0.5, 0.5, 1, 254 / 255); // "nothing drawn": the farthest distance, mask 0
832
+ gl.depthMask(true);
833
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
834
+ gl.disable(gl.BLEND);
835
+ gl.disable(gl.POLYGON_OFFSET_FILL);
836
+ gl.useProgram(prog.program);
837
+ gl.uniform3fv(u.uCamPos, view.camPos);
838
+ gl.uniform2fv(u.uCamRot, view.camRot);
839
+ gl.uniform1f(u.uF, view.f);
840
+ gl.uniform1f(u.uAspect, view.aspect);
841
+ gl.uniform1f(u.uNear, view.near);
842
+ gl.uniform1f(u.uFar, view.far);
843
+ gl.uniform1f(u.uGeoNear, view.near);
844
+ gl.uniform1f(u.uGeoRange, Math.log(view.far / view.near));
845
+ gl.uniform1i(u.uTexture, 0);
846
+ const locs = [a.aPos, a.aNormal, a.aUV].filter((loc) => loc >= 0);
847
+ for (const loc of locs) gl.enableVertexAttribArray(loc);
848
+
849
+ for (const d of draws) {
850
+ if (d.kind !== "face") continue;
851
+ const buf = this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth), d.mat.mapping);
852
+ if (!d.count) continue;
853
+ gl.uniformMatrix4fv(u.uModel, false, d.obj.world);
854
+ gl.uniform4fv(u.uColor, d.color);
855
+ gl.uniform1f(u.uMask, !mask || mask.has(d.obj) ? 1 : 0);
856
+ const m = uvTransform(d.mat) ?? [1, 0, 0, 1, 0, 0];
857
+ gl.uniformMatrix3fv(u.uUVTransform, false, [m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
858
+ gl.uniformMatrix3fv(u.uNormalMatrix, false, d.place.normals);
859
+ gl.frontFace(d.place.mirrored ? gl.CCW : gl.CW);
860
+ if (d.mat.doubleSided) gl.disable(gl.CULL_FACE);
861
+ else gl.enable(gl.CULL_FACE);
862
+ const tex = d.mat.texture
863
+ ? this._texture(d.mat.texture, d.mat, d.obj.mesh, !!uvsFor(d.obj.mesh, d.mat.mapping).faceUVs)
864
+ : null;
865
+ gl.activeTexture(gl.TEXTURE0);
866
+ gl.bindTexture(gl.TEXTURE_2D, tex);
867
+ gl.uniform1f(u.uUseTexture, tex ? 1 : 0);
868
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
869
+ gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
870
+ if (a.aNormal >= 0) gl.vertexAttribPointer(a.aNormal, 3, gl.FLOAT, false, 32, 12);
871
+ if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
872
+ gl.drawArrays(gl.TRIANGLES, d.first, d.count);
873
+ }
874
+
875
+ for (const loc of locs) gl.disableVertexAttribArray(loc);
876
+ gl.enable(gl.BLEND);
877
+ return t;
878
+ }
879
+
579
880
  /** The program for one pass's code, compiled once. Null if it doesn't compile (reported once). */
580
- _postProgram(glsl, uniforms) {
881
+ _postProgram(glsl, uniforms, geometry = false) {
581
882
  const list = Object.entries(uniforms ?? {})
582
883
  .map(([name, value]) => ({ name, type: uniformType(value) }))
583
884
  .filter((u) => u.type);
584
- const key = glsl + "|" + list.map((u) => u.name + ":" + u.type).join(",");
885
+ const key = (geometry ? "geo|" : "") + glsl + "|" + list.map((u) => u.name + ":" + u.type).join(",");
585
886
  if (this._posts.has(key)) return this._posts.get(key);
586
887
  let p = null;
587
888
  try {
588
- p = program(this.gl, POST_VS, postFS(glsl, list), ["aPos"], [...POST_UNIFORMS, ...list.map((u) => u.name)]);
889
+ p = program(this.gl, POST_VS, postFS(glsl, list, geometry), ["aPos"], [...POST_UNIFORMS, ...list.map((u) => u.name)]);
589
890
  p.list = list;
590
891
  } catch (err) {
591
892
  console.error("Trid: a post effect's shader failed to compile, skipping it.\n" + err.message);
@@ -598,7 +899,7 @@ export class WebGLRenderer {
598
899
  * Runs the post effects on the scene's picture, then draws the result on
599
900
  * the WebGL canvas.
600
901
  */
601
- _runPost(effects, view, w, h) {
902
+ _runPost(effects, view, w, h, geometry = new Map()) {
602
903
  const gl = this.gl;
603
904
  const P = this._post;
604
905
  // Multisampled drawing: resolve it into the scene's texture
@@ -616,7 +917,7 @@ export class WebGLRenderer {
616
917
  gl.disable(gl.POLYGON_OFFSET_FILL);
617
918
  gl.bindBuffer(gl.ARRAY_BUFFER, P.quad);
618
919
 
619
- const pass = (prog, input, source, target, tw, th, uniforms) => {
920
+ const pass = (prog, input, source, target, tw, th, uniforms, geo = null) => {
620
921
  gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fb : null);
621
922
  gl.viewport(0, 0, tw, th);
622
923
  gl.useProgram(prog.program);
@@ -631,6 +932,15 @@ export class WebGLRenderer {
631
932
  gl.uniform2f(u.uTexel, 1 / input.w, 1 / input.h);
632
933
  gl.uniform1f(u.uTime, view.time);
633
934
  gl.uniform4fv(u.uBackground, view.background);
935
+ gl.uniform1f(u.uNear, view.near);
936
+ gl.uniform1f(u.uFar, view.far);
937
+ if (geo) {
938
+ gl.activeTexture(gl.TEXTURE2);
939
+ gl.bindTexture(gl.TEXTURE_2D, geo.tex);
940
+ gl.uniform1i(u.uGeometry, 2);
941
+ gl.uniform1f(u.uGeoNear, view.near);
942
+ gl.uniform1f(u.uGeoRange, Math.log(view.far / view.near));
943
+ }
634
944
  for (const x of prog.list) this._setUniform(view.ctx, u[x.name], x.type, uniforms?.[x.name]);
635
945
  const a = prog.attributes.aPos;
636
946
  gl.enableVertexAttribArray(a);
@@ -642,12 +952,13 @@ export class WebGLRenderer {
642
952
  let source = P.scene; // the picture the current effect starts from
643
953
  for (const e of effects) {
644
954
  let prev = source;
955
+ const geo = wantsGeometry(e) ? geometry.get(Array.isArray(e.objects) ? e.objects : null) ?? null : null;
645
956
  for (const ps of postPasses(e)) {
646
- const prog = this._postProgram(ps.glsl, e.uniforms);
957
+ const prog = this._postProgram(ps.glsl, e.uniforms, !!geo);
647
958
  if (!prog) continue;
648
959
  const tw = Math.max(1, Math.round(w * ps.scale)), th = Math.max(1, Math.round(h * ps.scale));
649
960
  const target = this._poolTarget(tw, th, [source, prev]);
650
- pass(prog, prev, source, target, tw, th, e.uniforms);
961
+ pass(prog, prev, source, target, tw, th, e.uniforms, geo);
651
962
  prev = target;
652
963
  }
653
964
  source = prev;
@@ -656,6 +967,8 @@ export class WebGLRenderer {
656
967
  // Onto the canvas
657
968
  pass(this._postProgram(COPY_GLSL, {}), source, source, null, w, h, {});
658
969
 
970
+ gl.activeTexture(gl.TEXTURE2);
971
+ gl.bindTexture(gl.TEXTURE_2D, null);
659
972
  gl.activeTexture(gl.TEXTURE1);
660
973
  gl.bindTexture(gl.TEXTURE_2D, null);
661
974
  gl.activeTexture(gl.TEXTURE0);
@@ -677,6 +990,8 @@ export class WebGLRenderer {
677
990
  for (const p of new Set(this._faces.values())) gl.deleteProgram(p.program);
678
991
  for (const p of this._posts.values()) if (p) gl.deleteProgram(p.program);
679
992
  gl.deleteProgram(this._line.program);
993
+ if (this._geo) gl.deleteProgram(this._geo.program);
994
+ this._freeShadow();
680
995
  this._freePost();
681
996
  gl.getExtension("WEBGL_lose_context")?.loseContext();
682
997
  }
@@ -694,6 +1009,7 @@ export class WebGLRenderer {
694
1009
  vec, color, count: L.lights.length,
695
1010
  ambient: L.ambient, ambientLevel: L.ambientLevel,
696
1011
  dir: [L.mainDir.x, L.mainDir.y, L.mainDir.z],
1012
+ shadow: L.shadow, // the light casting shadows; `map` is added by _shadowPass
697
1013
  };
698
1014
  }
699
1015
 
@@ -737,6 +1053,21 @@ export class WebGLRenderer {
737
1053
  gl.uniform1f(u.uTime, view.time);
738
1054
  gl.uniform4fv(u.uBackground, view.background);
739
1055
  gl.uniform3fv(u.uCamWorld, view.camPos);
1056
+ const sh = lights.shadow?.map;
1057
+ gl.uniform1i(u.uShadowLight, sh ? lights.shadow.index : -1);
1058
+ if (sh) {
1059
+ gl.activeTexture(gl.TEXTURE1);
1060
+ gl.bindTexture(gl.TEXTURE_2D, sh.tex);
1061
+ gl.uniform1i(u.uShadowMap, 1);
1062
+ gl.uniformMatrix4fv(u.uShadowMatrix, false, sh.matrix);
1063
+ gl.uniform1f(u.uShadowTexel, 1 / sh.size);
1064
+ gl.uniform1f(u.uShadowSoft, lights.shadow.softness);
1065
+ // About two map pixels along the normal, and one pixel of distance: no speckles on lit faces
1066
+ const pixel = (2 * sh.radius) / sh.size;
1067
+ gl.uniform1f(u.uShadowOffset, pixel * 1.5 * lights.shadow.bias);
1068
+ gl.uniform1f(u.uShadowBias, (1 / sh.size) * lights.shadow.bias);
1069
+ gl.uniform1f(u.uReceive, shadowFlag(d.obj, "receiveShadow") ? 1 : 0);
1070
+ }
740
1071
  for (const e of prog.effectUniforms) {
741
1072
  this._setUniform(view.ctx, u[e.glslName], e.type, effects[e.effect]?.uniforms?.[e.name]);
742
1073
  }
package/trid/scene.js CHANGED
@@ -37,7 +37,7 @@ export class Scene {
37
37
  * @param {Object} [opts]
38
38
  * @param {number} [opts.width] - Sets canvas.width (keeps the current one if omitted)
39
39
  * @param {number} [opts.height] - Sets canvas.height (keeps the current one if omitted)
40
- * @param {string} [opts.background="#141414"] - Clear color
40
+ * @param {string|null} [opts.background="#141414"] - Clear color; "transparent" or null: see-through, the page shows behind
41
41
  * @param {string} [opts.color="#ffffff"] - Default wireframe color
42
42
  * @param {number} [opts.lineWidth=3] - Default wireframe width
43
43
  * @param {import("./math/transform.js").Camera} [opts.camera={ z: -1 }] - Camera
@@ -8,3 +8,4 @@ export { FpsMeter } from "./fpsMeter.js";
8
8
  export { ObjUploader } from "./objUploader.js";
9
9
  export { ViewGizmo } from "./viewGizmo.js";
10
10
  export { TransformGizmo } from "./transformGizmo.js";
11
+ export { SelectionOutline } from "./selectionOutline.js";