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.
- package/README.md +43 -15
- package/package.json +1 -1
- package/trid/index.d.ts +247 -10
- package/trid/index.js +8 -1
- package/trid/render/canvasRenderer.js +99 -48
- package/trid/render/effects.js +16 -9
- package/trid/render/grid.js +177 -0
- package/trid/render/lights.js +162 -0
- package/trid/render/post.js +325 -0
- package/trid/render/webglRenderer.js +341 -19
- package/trid/scene.js +48 -6
- package/trid/tools/helpers.js +107 -9
- package/trid/tools/index.js +1 -1
- package/trid/tools/transformGizmo.js +40 -6
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
82
|
-
float
|
|
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 =
|
|
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 *
|
|
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", "
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
413
|
-
gl.uniform1f(u.uAmbient,
|
|
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
|
|
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 =
|
|
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
|
-
|
|
66
|
-
this.
|
|
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;
|