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.
- package/README.md +43 -20
- package/package.json +1 -1
- package/trid/index.d.ts +210 -7
- package/trid/index.js +10 -3
- package/trid/math/projection.js +8 -2
- package/trid/math/transform.js +4 -5
- package/trid/modeling/query.js +706 -0
- package/trid/render/canvasRenderer.js +268 -5
- package/trid/render/draw.js +12 -2
- package/trid/render/lights.js +14 -1
- package/trid/render/post.js +155 -1
- package/trid/render/shadows.js +184 -0
- package/trid/render/shapes.js +42 -23
- package/trid/render/webglRenderer.js +348 -17
- package/trid/scene.js +1 -1
- package/trid/tools/index.js +1 -0
- package/trid/tools/selectionOutline.js +244 -0
- package/trid/tools/transformGizmo.js +9 -1
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
package/trid/tools/index.js
CHANGED