trid3d 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +102 -2
- package/package.json +41 -4
- package/trid/index.js +53 -0
- package/trid/loaders/obj.js +206 -0
- package/trid/loaders/texture.js +56 -0
- package/trid/math/projection.js +49 -0
- package/trid/math/transform.js +302 -0
- package/trid/render/canvasRenderer.js +634 -0
- package/trid/render/color.js +22 -0
- package/trid/render/draw.js +146 -0
- package/trid/render/effects.js +265 -0
- package/trid/render/loop.js +42 -0
- package/trid/render/normals.js +90 -0
- package/trid/render/shapes.js +230 -0
- package/trid/render/webglRenderer.js +613 -0
- package/trid/scene.js +167 -0
- package/trid/tools/dragObject.js +89 -0
- package/trid/tools/flyCamera.js +111 -0
- package/trid/tools/fpsMeter.js +51 -0
- package/trid/tools/helpers.js +85 -0
- package/trid/tools/hoverHighlight.js +62 -0
- package/trid/tools/index.js +8 -0
- package/trid/tools/manager.js +241 -0
- package/trid/tools/objUploader.js +153 -0
- package/trid/tools/orbitCamera.js +132 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
|
|
2
|
+
import { clear, edgeSegments } from "./draw.js";
|
|
3
|
+
import { parseColor } from "./color.js";
|
|
4
|
+
import { DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./canvasRenderer.js";
|
|
5
|
+
import { newell, cornerNormals, creaseOf } from "./normals.js";
|
|
6
|
+
import { SURFACE_GLSL, buildEffects, effectsKey } from "./effects.js";
|
|
7
|
+
import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
|
|
8
|
+
|
|
9
|
+
/** Farthest distance drawn by the WebGL renderer, unless the camera sets `far`. */
|
|
10
|
+
export const DEFAULT_FAR = 1000;
|
|
11
|
+
|
|
12
|
+
// Trid's transform pipeline, written in GLSL. It matches localToWorld,
|
|
13
|
+
// worldToCamera and project() exactly, so tools and picking line up.
|
|
14
|
+
const COMMON = `
|
|
15
|
+
uniform vec3 uPos, uRot, uScale;
|
|
16
|
+
uniform vec3 uCamPos;
|
|
17
|
+
uniform vec2 uCamRot; // yaw, pitch
|
|
18
|
+
uniform float uF, uAspect, uNear, uFar;
|
|
19
|
+
|
|
20
|
+
vec3 rotX(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x, p.y * c - p.z * s, p.y * s + p.z * c); }
|
|
21
|
+
vec3 rotY(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x * c - p.z * s, p.y, p.x * s + p.z * c); }
|
|
22
|
+
vec3 rotZ(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x * c - p.y * s, p.x * s + p.y * c, p.z); }
|
|
23
|
+
vec3 rotXYZ(vec3 p) { return rotZ(rotY(rotX(p, uRot.x), uRot.y), uRot.z); }
|
|
24
|
+
vec3 toWorld(vec3 v) { return rotXYZ(v * uScale) + uPos; }
|
|
25
|
+
vec3 toCamera(vec3 p) { return rotX(rotY(p - uCamPos, uCamRot.x), uCamRot.y); }
|
|
26
|
+
vec4 toClip(vec3 p) {
|
|
27
|
+
float a = (uFar + uNear) / (uFar - uNear);
|
|
28
|
+
float b = -2.0 * uFar * uNear / (uFar - uNear);
|
|
29
|
+
return vec4(p.x * uF / uAspect, p.y * uF, a * p.z + b, p.z);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const FACE_VS = `
|
|
34
|
+
attribute vec3 aPos;
|
|
35
|
+
attribute vec3 aNormal;
|
|
36
|
+
attribute vec2 aUV;
|
|
37
|
+
varying vec3 vNormal;
|
|
38
|
+
varying vec3 vWorld;
|
|
39
|
+
varying vec3 vLocal;
|
|
40
|
+
varying vec2 vUV;
|
|
41
|
+
${COMMON}
|
|
42
|
+
void main() {
|
|
43
|
+
vNormal = rotXYZ(aNormal / uScale); // inverse scale keeps normals right on stretched objects
|
|
44
|
+
vLocal = aPos;
|
|
45
|
+
vUV = aUV;
|
|
46
|
+
vWorld = toWorld(aPos);
|
|
47
|
+
gl_Position = toClip(toCamera(vWorld));
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/** Fragment shader for faces: Trid's lighting, then the material's effects in order. */
|
|
52
|
+
function faceFS(effects) {
|
|
53
|
+
const fx = buildEffects(effects);
|
|
54
|
+
const source = `
|
|
55
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
56
|
+
precision highp float;
|
|
57
|
+
#else
|
|
58
|
+
precision mediump float;
|
|
59
|
+
#endif
|
|
60
|
+
uniform vec4 uColor;
|
|
61
|
+
uniform vec3 uLight;
|
|
62
|
+
uniform float uAmbient;
|
|
63
|
+
uniform float uShade;
|
|
64
|
+
uniform float uTime;
|
|
65
|
+
uniform vec4 uBackground;
|
|
66
|
+
uniform vec3 uCamWorld;
|
|
67
|
+
uniform sampler2D uTexture;
|
|
68
|
+
uniform float uUseTexture;
|
|
69
|
+
varying vec3 vNormal;
|
|
70
|
+
varying vec3 vWorld;
|
|
71
|
+
varying vec3 vLocal;
|
|
72
|
+
varying vec2 vUV;
|
|
73
|
+
${SURFACE_GLSL}
|
|
74
|
+
${fx.code}
|
|
75
|
+
void main() {
|
|
76
|
+
vec3 n = normalize(vNormal);
|
|
77
|
+
if (!gl_FrontFacing) n = -n;
|
|
78
|
+
float diffuse = max(0.0, dot(n, uLight));
|
|
79
|
+
float light = mix(1.0, uAmbient + (1.0 - uAmbient) * diffuse, uShade);
|
|
80
|
+
|
|
81
|
+
// Fill color, times the texture's color when there is one
|
|
82
|
+
vec4 base = uColor * mix(vec4(1.0), texture2D(uTexture, vUV), uUseTexture);
|
|
83
|
+
|
|
84
|
+
Surface s;
|
|
85
|
+
s.base = base;
|
|
86
|
+
s.uv = vUV;
|
|
87
|
+
s.normal = n;
|
|
88
|
+
s.position = vWorld;
|
|
89
|
+
s.local = vLocal;
|
|
90
|
+
s.view = normalize(uCamWorld - vWorld);
|
|
91
|
+
s.lightDir = uLight;
|
|
92
|
+
s.diffuse = diffuse;
|
|
93
|
+
s.light = light;
|
|
94
|
+
s.ambient = uAmbient;
|
|
95
|
+
s.depth = length(uCamWorld - vWorld);
|
|
96
|
+
s.time = uTime;
|
|
97
|
+
s.background = uBackground;
|
|
98
|
+
|
|
99
|
+
vec4 color = vec4(base.rgb * light, base.a);
|
|
100
|
+
${fx.calls}
|
|
101
|
+
color = clamp(color, 0.0, 1.0);
|
|
102
|
+
if (color.a < 0.004) discard; // fully see-through pixels (e.g. a PNG's empty parts) don't hide what's behind
|
|
103
|
+
gl_FragColor = vec4(color.rgb * color.a, color.a);
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
return { source, uniforms: fx.uniforms };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const FACE_UNIFORMS = ["uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
|
|
110
|
+
|
|
111
|
+
// Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
|
|
112
|
+
const LINE_VS = `
|
|
113
|
+
attribute vec3 aA;
|
|
114
|
+
attribute vec3 aB;
|
|
115
|
+
attribute vec2 aCorner; // x: 0 = at A, 1 = at B; y: -1 / +1 = which side
|
|
116
|
+
uniform vec2 uRes;
|
|
117
|
+
uniform float uWidth;
|
|
118
|
+
${COMMON}
|
|
119
|
+
void main() {
|
|
120
|
+
vec3 a = toCamera(toWorld(aA));
|
|
121
|
+
vec3 b = toCamera(toWorld(aB));
|
|
122
|
+
float near = uNear * 1.001;
|
|
123
|
+
if (a.z < near && b.z < near) { gl_Position = vec4(0.0, 0.0, -2.0, 1.0); return; }
|
|
124
|
+
if (a.z < near) a = mix(a, b, (near - a.z) / (b.z - a.z));
|
|
125
|
+
if (b.z < near) b = mix(b, a, (near - b.z) / (a.z - b.z));
|
|
126
|
+
vec4 ca = toClip(a), cb = toClip(b);
|
|
127
|
+
vec2 sa = ca.xy / ca.w * uRes * 0.5, sb = cb.xy / cb.w * uRes * 0.5;
|
|
128
|
+
vec2 d = sb - sa;
|
|
129
|
+
float len = length(d);
|
|
130
|
+
vec2 side = len > 1e-6 ? vec2(-d.y, d.x) / len : vec2(0.0, 1.0);
|
|
131
|
+
vec4 c = aCorner.x < 0.5 ? ca : cb;
|
|
132
|
+
c.xy += side * aCorner.y * uWidth / uRes * c.w;
|
|
133
|
+
gl_Position = c;
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const LINE_FS = `
|
|
138
|
+
precision mediump float;
|
|
139
|
+
uniform vec4 uColor;
|
|
140
|
+
void main() {
|
|
141
|
+
gl_FragColor = vec4(uColor.rgb * uColor.a, uColor.a);
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const UNIFORMS = ["uPos", "uRot", "uScale", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFar", "uColor"];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Draws a scene with WebGL, on the graphics card.
|
|
149
|
+
*
|
|
150
|
+
* Same objects, materials and light as `CanvasRenderer`, but:
|
|
151
|
+
* - a depth buffer decides what's in front, per pixel, so faces always
|
|
152
|
+
* overlap correctly (no sorting mistakes)
|
|
153
|
+
* - much faster on big models
|
|
154
|
+
*
|
|
155
|
+
* The scene is drawn on a hidden WebGL canvas, then copied onto the
|
|
156
|
+
* scene's 2D canvas. Tools draw on top as usual, the page keeps a
|
|
157
|
+
* single canvas, and `canvas.toDataURL()` includes everything.
|
|
158
|
+
*
|
|
159
|
+
* Meshes are sent to the graphics card once and reused. After changing
|
|
160
|
+
* a mesh's vertices, edges or faces in place, set `mesh.dirty = true`.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* const scene = new Scene(canvas, { renderer: new WebGLRenderer() });
|
|
164
|
+
*/
|
|
165
|
+
export class WebGLRenderer {
|
|
166
|
+
/** Whether this browser can create a WebGL context. */
|
|
167
|
+
static isSupported() {
|
|
168
|
+
try {
|
|
169
|
+
const c = document.createElement("canvas");
|
|
170
|
+
return !!(c.getContext("webgl2") || c.getContext("webgl"));
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {Object} [opts]
|
|
178
|
+
* @param {boolean} [opts.antialias=true] - Smooth edges
|
|
179
|
+
* @throws {Error} If WebGL isn't available (check `WebGLRenderer.isSupported()` first)
|
|
180
|
+
*/
|
|
181
|
+
constructor({ antialias = true } = {}) {
|
|
182
|
+
/** The hidden canvas WebGL draws on. */
|
|
183
|
+
this.canvas = document.createElement("canvas");
|
|
184
|
+
this._options = { antialias, alpha: true, premultipliedAlpha: true, depth: true };
|
|
185
|
+
this._colors = new Map();
|
|
186
|
+
this._lost = false;
|
|
187
|
+
this._start = performance.now();
|
|
188
|
+
this._failed = new Set(); // effect lists whose shader didn't compile (reported once)
|
|
189
|
+
|
|
190
|
+
this.canvas.addEventListener("webglcontextlost", (e) => {
|
|
191
|
+
e.preventDefault(); // allows the context to come back
|
|
192
|
+
this._lost = true;
|
|
193
|
+
});
|
|
194
|
+
this.canvas.addEventListener("webglcontextrestored", () => {
|
|
195
|
+
this._init();
|
|
196
|
+
this._lost = false;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this._init();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_init() {
|
|
203
|
+
const gl = this.canvas.getContext("webgl2", this._options) || this.canvas.getContext("webgl", this._options);
|
|
204
|
+
if (!gl) throw new Error("WebGL is not available in this browser");
|
|
205
|
+
this.gl = gl;
|
|
206
|
+
this._webgl2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
|
|
207
|
+
this._meshes = new WeakMap();
|
|
208
|
+
this._buffers = new Set();
|
|
209
|
+
this._textures = new WeakMap(); // image / canvas / video → GL texture
|
|
210
|
+
this._glTextures = new Set();
|
|
211
|
+
this._warned = new WeakSet();
|
|
212
|
+
this._frame = 0;
|
|
213
|
+
|
|
214
|
+
this._faces = new Map(); // face programs, one per combination of effects
|
|
215
|
+
this._line = program(gl, LINE_VS, LINE_FS, ["aA", "aB", "aCorner"], [...UNIFORMS, "uRes", "uWidth"]);
|
|
216
|
+
|
|
217
|
+
gl.enable(gl.DEPTH_TEST);
|
|
218
|
+
gl.depthFunc(gl.LEQUAL);
|
|
219
|
+
gl.frontFace(gl.CW); // Trid faces are clockwise from the front
|
|
220
|
+
gl.cullFace(gl.BACK);
|
|
221
|
+
gl.enable(gl.BLEND);
|
|
222
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
223
|
+
// Push faces back a little so outlines on them win the depth test
|
|
224
|
+
gl.polygonOffset(1, 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Draws every object of the scene, then copies the image onto the scene's canvas.
|
|
229
|
+
*
|
|
230
|
+
* @param {import("../scene.js").Scene} scene
|
|
231
|
+
*/
|
|
232
|
+
render(scene) {
|
|
233
|
+
const { ctx, canvas, camera } = scene;
|
|
234
|
+
const w = canvas.width, h = canvas.height;
|
|
235
|
+
clear(ctx, w, h, scene.background);
|
|
236
|
+
if (this._lost || this._disposed) return;
|
|
237
|
+
|
|
238
|
+
const gl = this.gl;
|
|
239
|
+
this._frame++;
|
|
240
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
241
|
+
this.canvas.width = w;
|
|
242
|
+
this.canvas.height = h;
|
|
243
|
+
}
|
|
244
|
+
gl.viewport(0, 0, w, h);
|
|
245
|
+
gl.clearColor(0, 0, 0, 0);
|
|
246
|
+
gl.depthMask(true);
|
|
247
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
248
|
+
|
|
249
|
+
const view = {
|
|
250
|
+
ctx,
|
|
251
|
+
background: this._color(ctx, scene.background),
|
|
252
|
+
time: (performance.now() - this._start) / 1000,
|
|
253
|
+
camPos: [camera.x ?? 0, camera.y ?? 0, camera.z ?? 0],
|
|
254
|
+
camRot: [camera.yaw ?? 0, camera.pitch ?? 0],
|
|
255
|
+
f: 1 / Math.tan((camera.fov ?? DEFAULT_FOV) / 2),
|
|
256
|
+
aspect: w / h,
|
|
257
|
+
near: camera.near ?? DEFAULT_NEAR,
|
|
258
|
+
far: camera.far ?? DEFAULT_FAR,
|
|
259
|
+
};
|
|
260
|
+
const l = scene.light ?? DEFAULT_LIGHT;
|
|
261
|
+
const len = Math.hypot(l.x, l.y, l.z) || 1;
|
|
262
|
+
const light = [l.x / len, l.y / len, l.z / len];
|
|
263
|
+
const ambient = scene.ambient ?? DEFAULT_AMBIENT;
|
|
264
|
+
|
|
265
|
+
// Opaque things first; see-through ones after, without writing depth
|
|
266
|
+
const opaque = [], clear2 = [];
|
|
267
|
+
for (const obj of scene.objects) {
|
|
268
|
+
const geo = this._geometry(obj.mesh);
|
|
269
|
+
const lineColor = this._color(ctx, obj.color ?? scene.color);
|
|
270
|
+
const lineWidth = obj.lineWidth ?? scene.lineWidth;
|
|
271
|
+
const mat = obj.material;
|
|
272
|
+
|
|
273
|
+
if (mat) {
|
|
274
|
+
const fill = this._color(ctx, mat.fill ?? (mat.texture ? "#ffffff" : scene.color));
|
|
275
|
+
(fill[3] < 1 ? clear2 : opaque).push({ kind: "face", obj, geo, color: fill, mat });
|
|
276
|
+
if (mat.wireframe) {
|
|
277
|
+
(lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf: geo.outlines, color: lineColor, width: lineWidth });
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
(lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf: geo.edges, color: lineColor, width: lineWidth });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const d of opaque) this._draw(d, view, light, ambient, w, h);
|
|
285
|
+
gl.depthMask(false);
|
|
286
|
+
for (const d of clear2) this._draw(d, view, light, ambient, w, h);
|
|
287
|
+
|
|
288
|
+
ctx.drawImage(this.canvas, 0, 0);
|
|
289
|
+
|
|
290
|
+
// Meshes marked as changed have been sent again
|
|
291
|
+
for (const obj of scene.objects) if (obj.mesh.dirty) obj.mesh.dirty = false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Frees the graphics card memory. The renderer can't be used afterwards. */
|
|
295
|
+
dispose() {
|
|
296
|
+
if (this._disposed) return;
|
|
297
|
+
this._disposed = true;
|
|
298
|
+
const gl = this.gl;
|
|
299
|
+
for (const b of this._buffers) gl.deleteBuffer(b);
|
|
300
|
+
for (const t of this._glTextures) gl.deleteTexture(t);
|
|
301
|
+
this._glTextures.clear();
|
|
302
|
+
this._buffers.clear();
|
|
303
|
+
for (const p of new Set(this._faces.values())) gl.deleteProgram(p.program);
|
|
304
|
+
gl.deleteProgram(this._line.program);
|
|
305
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_draw(d, view, light, ambient, w, h) {
|
|
309
|
+
const gl = this.gl;
|
|
310
|
+
const isFace = d.kind === "face";
|
|
311
|
+
const effects = isFace ? d.mat.effects ?? [] : null;
|
|
312
|
+
const prog = isFace ? this._faceProgram(effects) : this._line;
|
|
313
|
+
const buf = isFace ? this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth)) : d.buf;
|
|
314
|
+
if (!buf.count) return;
|
|
315
|
+
|
|
316
|
+
gl.useProgram(prog.program);
|
|
317
|
+
const u = prog.uniforms;
|
|
318
|
+
const { pos, rot, scale } = d.obj;
|
|
319
|
+
gl.uniform3f(u.uPos, pos.x, pos.y, pos.z);
|
|
320
|
+
gl.uniform3f(u.uRot, rot.x, rot.y, rot.z);
|
|
321
|
+
gl.uniform3f(u.uScale, scale.x, scale.y, scale.z);
|
|
322
|
+
gl.uniform3fv(u.uCamPos, view.camPos);
|
|
323
|
+
gl.uniform2fv(u.uCamRot, view.camRot);
|
|
324
|
+
gl.uniform1f(u.uF, view.f);
|
|
325
|
+
gl.uniform1f(u.uAspect, view.aspect);
|
|
326
|
+
gl.uniform1f(u.uNear, view.near);
|
|
327
|
+
gl.uniform1f(u.uFar, view.far);
|
|
328
|
+
gl.uniform4fv(u.uColor, d.color);
|
|
329
|
+
|
|
330
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
|
|
331
|
+
const a = prog.attributes;
|
|
332
|
+
|
|
333
|
+
if (isFace) {
|
|
334
|
+
gl.uniform3fv(u.uLight, light);
|
|
335
|
+
gl.uniform1f(u.uAmbient, ambient);
|
|
336
|
+
gl.uniform1f(u.uShade, d.mat.shade === false ? 0 : 1);
|
|
337
|
+
gl.uniform1f(u.uTime, view.time);
|
|
338
|
+
gl.uniform4fv(u.uBackground, view.background);
|
|
339
|
+
gl.uniform3fv(u.uCamWorld, view.camPos);
|
|
340
|
+
for (const e of prog.effectUniforms) {
|
|
341
|
+
this._setUniform(view.ctx, u[e.glslName], e.type, effects[e.effect]?.uniforms?.[e.name]);
|
|
342
|
+
}
|
|
343
|
+
if (d.mat.doubleSided) gl.disable(gl.CULL_FACE);
|
|
344
|
+
else gl.enable(gl.CULL_FACE);
|
|
345
|
+
gl.enable(gl.POLYGON_OFFSET_FILL);
|
|
346
|
+
gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
|
|
347
|
+
if (a.aNormal >= 0) gl.vertexAttribPointer(a.aNormal, 3, gl.FLOAT, false, 32, 12);
|
|
348
|
+
if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
|
|
349
|
+
const tex = d.mat.texture ? this._texture(d.mat.texture, !!d.mat.pixelated, d.obj.mesh) : null;
|
|
350
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
351
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
352
|
+
gl.uniform1i(u.uTexture, 0);
|
|
353
|
+
gl.uniform1f(u.uUseTexture, tex ? 1 : 0);
|
|
354
|
+
} else {
|
|
355
|
+
if (a.aA < 0) return;
|
|
356
|
+
gl.uniform2f(u.uRes, w, h);
|
|
357
|
+
gl.uniform1f(u.uWidth, d.width);
|
|
358
|
+
gl.disable(gl.CULL_FACE);
|
|
359
|
+
gl.disable(gl.POLYGON_OFFSET_FILL);
|
|
360
|
+
gl.vertexAttribPointer(a.aA, 3, gl.FLOAT, false, 32, 0);
|
|
361
|
+
gl.vertexAttribPointer(a.aB, 3, gl.FLOAT, false, 32, 12);
|
|
362
|
+
gl.vertexAttribPointer(a.aCorner, 2, gl.FLOAT, false, 32, 24);
|
|
363
|
+
}
|
|
364
|
+
const locs = Object.values(a).filter((loc) => loc >= 0);
|
|
365
|
+
for (const loc of locs) gl.enableVertexAttribArray(loc);
|
|
366
|
+
gl.drawArrays(gl.TRIANGLES, 0, buf.count);
|
|
367
|
+
for (const loc of locs) gl.disableVertexAttribArray(loc);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The face program for a list of effects, compiled once per combination.
|
|
372
|
+
* A shader that doesn't compile is reported once, and the object is
|
|
373
|
+
* drawn without its effects.
|
|
374
|
+
*/
|
|
375
|
+
_faceProgram(effects) {
|
|
376
|
+
const key = effectsKey(effects);
|
|
377
|
+
let p = this._faces.get(key);
|
|
378
|
+
if (p) return p;
|
|
379
|
+
try {
|
|
380
|
+
const fs = faceFS(effects);
|
|
381
|
+
p = program(this.gl, FACE_VS, fs.source, ["aPos", "aNormal", "aUV"],
|
|
382
|
+
[...UNIFORMS, ...FACE_UNIFORMS, ...fs.uniforms.map((u) => u.glslName)]);
|
|
383
|
+
p.effectUniforms = fs.uniforms;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (!this._failed.has(key)) {
|
|
386
|
+
this._failed.add(key);
|
|
387
|
+
console.error("Trid: an effect's shader failed to compile, drawing without effects.\n" + err.message);
|
|
388
|
+
}
|
|
389
|
+
p = effects.length ? this._faceProgram([]) : null;
|
|
390
|
+
if (!p) throw err;
|
|
391
|
+
}
|
|
392
|
+
this._faces.set(key, p);
|
|
393
|
+
return p;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The GL texture for an image, canvas or video, uploaded once
|
|
398
|
+
* (canvases and videos: every frame). Null while it isn't ready,
|
|
399
|
+
* or if the mesh has no texture coordinates.
|
|
400
|
+
*/
|
|
401
|
+
_texture(src, pixelated, mesh) {
|
|
402
|
+
const gl = this.gl;
|
|
403
|
+
if (!textureReady(src)) return null;
|
|
404
|
+
if (!mesh.faceUVs) {
|
|
405
|
+
if (!this._warned.has(mesh)) {
|
|
406
|
+
this._warned.add(mesh);
|
|
407
|
+
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let t = this._textures.get(src);
|
|
413
|
+
const live = textureIsLive(src);
|
|
414
|
+
if (!t || live || t.failed) {
|
|
415
|
+
if (t?.failed) return null;
|
|
416
|
+
const { w, h } = textureSize(src);
|
|
417
|
+
if (!t) {
|
|
418
|
+
t = { tex: gl.createTexture(), frame: -1 };
|
|
419
|
+
this._textures.set(src, t);
|
|
420
|
+
this._glTextures.add(t.tex);
|
|
421
|
+
}
|
|
422
|
+
if (t.frame !== this._frame) {
|
|
423
|
+
t.frame = this._frame;
|
|
424
|
+
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
425
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // images go top-down, v goes up
|
|
426
|
+
try {
|
|
427
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
// Usually an image from another site without permission (CORS)
|
|
430
|
+
console.warn("Trid: can't use this image as a texture: " + err.message);
|
|
431
|
+
t.failed = true;
|
|
432
|
+
return null;
|
|
433
|
+
} finally {
|
|
434
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
435
|
+
}
|
|
436
|
+
// WebGL 1 can only repeat and mipmap power-of-two sizes
|
|
437
|
+
const pot = (w & (w - 1)) === 0 && (h & (h - 1)) === 0;
|
|
438
|
+
const full = this._webgl2 || pot;
|
|
439
|
+
t.mipmaps = full && !live;
|
|
440
|
+
if (t.mipmaps) gl.generateMipmap(gl.TEXTURE_2D);
|
|
441
|
+
const wrap = full ? gl.REPEAT : gl.CLAMP_TO_EDGE;
|
|
442
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
|
|
443
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
448
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, pixelated ? gl.NEAREST : gl.LINEAR);
|
|
449
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
|
|
450
|
+
pixelated ? (t.mipmaps ? gl.NEAREST_MIPMAP_NEAREST : gl.NEAREST) : (t.mipmaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR));
|
|
451
|
+
return t.tex;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Sends one effect setting to the shader, converting it to its GLSL type. */
|
|
455
|
+
_setUniform(ctx, loc, type, value) {
|
|
456
|
+
const gl = this.gl;
|
|
457
|
+
if (loc == null || value == null) return;
|
|
458
|
+
if (type === "float") gl.uniform1f(loc, Number(value));
|
|
459
|
+
else if (typeof value === "string") gl.uniform4fv(loc, this._color(ctx, value));
|
|
460
|
+
else if (Array.isArray(value)) gl[`uniform${value.length}fv`](loc, value);
|
|
461
|
+
else gl.uniform3f(loc, value.x, value.y, value.z);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Graphics card buffers for a mesh, built once and rebuilt when it changes. */
|
|
465
|
+
_geometry(mesh) {
|
|
466
|
+
let geo = this._meshes.get(mesh);
|
|
467
|
+
const stale = !geo || mesh.dirty || geo.vertices !== mesh.vertices || geo.faceList !== mesh.faces
|
|
468
|
+
|| geo.edgeList !== mesh.edges || geo.counts !== countsOf(mesh);
|
|
469
|
+
if (!stale) return geo;
|
|
470
|
+
|
|
471
|
+
if (geo) {
|
|
472
|
+
for (const b of [geo.outlines, geo.edges, ...geo.faceBuffers.values()]) this._free(b);
|
|
473
|
+
}
|
|
474
|
+
const v = mesh.vertices;
|
|
475
|
+
const faces = mesh.faces ?? [];
|
|
476
|
+
|
|
477
|
+
// Outlines of the faces, each edge once
|
|
478
|
+
const outline = new Set();
|
|
479
|
+
const outlinePairs = [];
|
|
480
|
+
for (const f of faces) {
|
|
481
|
+
for (let k = 0; k < f.length; k++) {
|
|
482
|
+
const a = f[k], b = f[(k + 1) % f.length];
|
|
483
|
+
const key = a < b ? a + "," + b : b + "," + a;
|
|
484
|
+
if (!outline.has(key)) {
|
|
485
|
+
outline.add(key);
|
|
486
|
+
outlinePairs.push([a, b]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
geo = {
|
|
492
|
+
vertices: mesh.vertices,
|
|
493
|
+
faceList: mesh.faces,
|
|
494
|
+
edgeList: mesh.edges,
|
|
495
|
+
counts: countsOf(mesh),
|
|
496
|
+
faceBuffers: new Map(), // one per shading mode, built when first needed
|
|
497
|
+
outlines: this._upload(lineData(v, outlinePairs), 8),
|
|
498
|
+
edges: this._upload(lineData(v, edgeSegments(mesh.edges ?? [])), 8),
|
|
499
|
+
};
|
|
500
|
+
this._meshes.set(mesh, geo);
|
|
501
|
+
return geo;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Triangles of a mesh's faces, each corner with its own position and
|
|
506
|
+
* normal. Flat shading uses the face's normal at every corner; smooth
|
|
507
|
+
* shading uses the corner normals.
|
|
508
|
+
*/
|
|
509
|
+
_faceBuffer(geo, mesh, crease) {
|
|
510
|
+
const key = crease ?? "flat";
|
|
511
|
+
let buf = geo.faceBuffers.get(key);
|
|
512
|
+
if (buf) return buf;
|
|
513
|
+
|
|
514
|
+
const v = mesh.vertices;
|
|
515
|
+
const faces = mesh.faces ?? [];
|
|
516
|
+
const corners = crease ? cornerNormals(mesh, crease) : null;
|
|
517
|
+
const uvs = mesh.uvs, faceUVs = mesh.faceUVs;
|
|
518
|
+
const tri = [];
|
|
519
|
+
faces.forEach((f, fi) => {
|
|
520
|
+
if (f.length < 3) return;
|
|
521
|
+
let ns;
|
|
522
|
+
if (corners) {
|
|
523
|
+
ns = corners[fi];
|
|
524
|
+
} else {
|
|
525
|
+
const n = newell(f.map((i) => v[i]));
|
|
526
|
+
const len = Math.hypot(n.x, n.y, n.z) || 1;
|
|
527
|
+
const flat = { x: n.x / len, y: n.y / len, z: n.z / len };
|
|
528
|
+
ns = f.map(() => flat);
|
|
529
|
+
}
|
|
530
|
+
const ts = uvs && faceUVs?.[fi];
|
|
531
|
+
for (let k = 1; k < f.length - 1; k++) {
|
|
532
|
+
for (const c of [0, k, k + 1]) {
|
|
533
|
+
const p = v[f[c]], n = ns[c];
|
|
534
|
+
const t = ts ? uvs[ts[c]] : null;
|
|
535
|
+
tri.push(p.x, p.y, p.z, n.x, n.y, n.z, t ? t.u : 0, t ? t.v : 0);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
buf = this._upload(tri, 8);
|
|
541
|
+
geo.faceBuffers.set(key, buf);
|
|
542
|
+
return buf;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_upload(data, floatsPerVertex) {
|
|
546
|
+
const gl = this.gl;
|
|
547
|
+
const buffer = gl.createBuffer();
|
|
548
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
549
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
|
|
550
|
+
this._buffers.add(buffer);
|
|
551
|
+
return { buffer, count: data.length / floatsPerVertex };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_free(b) {
|
|
555
|
+
this.gl.deleteBuffer(b.buffer);
|
|
556
|
+
this._buffers.delete(b.buffer);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** [r, g, b, a] in 0..1 for a CSS color, cached. */
|
|
560
|
+
_color(ctx, css) {
|
|
561
|
+
let c = this._colors.get(css);
|
|
562
|
+
if (!c) {
|
|
563
|
+
const [r, g, b, a] = parseColor(ctx, css);
|
|
564
|
+
c = [r / 255, g / 255, b / 255, a];
|
|
565
|
+
this._colors.set(css, c);
|
|
566
|
+
}
|
|
567
|
+
return c;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** A short string that changes when a mesh's arrays grow or shrink. */
|
|
572
|
+
function countsOf(mesh) {
|
|
573
|
+
return `${mesh.vertices.length}/${mesh.edges?.length ?? 0}/${mesh.faces?.length ?? 0}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Six corners (two triangles) per segment: A, B, then which corner. */
|
|
577
|
+
function lineData(v, pairs) {
|
|
578
|
+
const out = [];
|
|
579
|
+
const corners = [[0, -1], [1, -1], [1, 1], [0, -1], [1, 1], [0, 1]];
|
|
580
|
+
for (const [i, j] of pairs) {
|
|
581
|
+
const a = v[i], b = v[j];
|
|
582
|
+
for (const [cx, cy] of corners) out.push(a.x, a.y, a.z, b.x, b.y, b.z, cx, cy);
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function program(gl, vsSource, fsSource, attributes, uniforms) {
|
|
588
|
+
const compile = (type, src) => {
|
|
589
|
+
const s = gl.createShader(type);
|
|
590
|
+
gl.shaderSource(s, src);
|
|
591
|
+
gl.compileShader(s);
|
|
592
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS) && !gl.isContextLost()) {
|
|
593
|
+
throw new Error("Trid shader error: " + gl.getShaderInfoLog(s));
|
|
594
|
+
}
|
|
595
|
+
return s;
|
|
596
|
+
};
|
|
597
|
+
const p = gl.createProgram();
|
|
598
|
+
const vs = compile(gl.VERTEX_SHADER, vsSource);
|
|
599
|
+
const fs = compile(gl.FRAGMENT_SHADER, fsSource);
|
|
600
|
+
gl.attachShader(p, vs);
|
|
601
|
+
gl.attachShader(p, fs);
|
|
602
|
+
gl.linkProgram(p);
|
|
603
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS) && !gl.isContextLost()) {
|
|
604
|
+
throw new Error("Trid shader error: " + gl.getProgramInfoLog(p));
|
|
605
|
+
}
|
|
606
|
+
gl.deleteShader(vs);
|
|
607
|
+
gl.deleteShader(fs);
|
|
608
|
+
|
|
609
|
+
const out = { program: p, attributes: {}, uniforms: {} };
|
|
610
|
+
for (const name of attributes) out.attributes[name] = gl.getAttribLocation(p, name);
|
|
611
|
+
for (const name of uniforms) out.uniforms[name] = gl.getUniformLocation(p, name);
|
|
612
|
+
return out;
|
|
613
|
+
}
|