3draft 26.11.4010 → 26.11.11010
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/APP.md +130 -31
- package/README.md +64 -2
- package/dist/camera.js +3 -2
- package/dist/csg.d.ts +2 -0
- package/dist/csg.js +13 -1
- package/dist/gl.d.ts +8 -0
- package/dist/gl.js +517 -0
- package/dist/gpu.d.ts +67 -4
- package/dist/gpu.js +485 -40
- package/dist/index.d.ts +12 -4
- package/dist/index.js +9 -4
- package/dist/instances.d.ts +19 -0
- package/dist/instances.js +41 -0
- package/dist/kernel.js +6 -5
- package/dist/material.d.ts +4 -0
- package/dist/material.js +2 -1
- package/dist/pick.js +3 -2
- package/dist/saw.d.ts +29 -0
- package/dist/saw.js +121 -0
- package/dist/svg.js +1 -1
- package/dist/triangulate.d.ts +4 -0
- package/dist/triangulate.js +72 -0
- package/dist/types.d.ts +4 -0
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +111 -9
- package/package.json +1 -1
package/dist/gl.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// WebGL2 renderer: the same picture as the WebGPU one (gpu.ts), for browsers and devices that have no WebGPU.
|
|
2
|
+
// Same options, same vertices (buildVertices), same light model: the shader below is a line-by-line port of the WGSL.
|
|
3
|
+
// It also runs on a software GL (SwiftShader), so a frame can be checked on a machine without a GPU.
|
|
4
|
+
import { norm } from './vec.js';
|
|
5
|
+
import { makeCamera } from './camera.js';
|
|
6
|
+
import { buildVertices, buildMarks, MARK_FLOATS, sceneEdges, sceneLights, LIGHT_FLOATS, MAX_LIGHTS, viewProjection, nearPlane, defaultLight, defaultMaterial } from './gpu.js';
|
|
7
|
+
const GL_FLOATS = 14; // position(3) normal(3) color(3) material(4: metal, rough, alpha, emit) soft(1)
|
|
8
|
+
const GL_INSTANCE_FLOATS = 15; // at(3) ex(3) ey(3) ez(3) color(3)
|
|
9
|
+
const GL_COMMON = /* glsl */ `#version 300 es
|
|
10
|
+
precision highp float;
|
|
11
|
+
uniform mat4 uVp;
|
|
12
|
+
out vec3 vN; out vec3 vC; out vec3 vW; out vec4 vM; out float vS;
|
|
13
|
+
`;
|
|
14
|
+
const GL_VERT = `${GL_COMMON}
|
|
15
|
+
layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNrm; layout(location = 2) in vec3 aCol;
|
|
16
|
+
layout(location = 3) in vec4 aMat; layout(location = 4) in float aSoft;
|
|
17
|
+
void main() {
|
|
18
|
+
vec4 p = uVp * vec4(aPos, 1.0);
|
|
19
|
+
gl_Position = vec4(p.xy, p.z * 2.0 - p.w, p.w); // WebGPU depth is 0..1, GL wants -1..1
|
|
20
|
+
vN = aNrm; vC = aCol; vW = aPos; vM = aMat; vS = aSoft;
|
|
21
|
+
}`;
|
|
22
|
+
// the same prototype drawn many times: its frame (ex, ey, ez; scaled axes scale it) and position come per instance
|
|
23
|
+
const GL_VERT_INST = `${GL_COMMON}
|
|
24
|
+
layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNrm;
|
|
25
|
+
layout(location = 5) in vec3 aAt; layout(location = 6) in vec3 aEx; layout(location = 7) in vec3 aEy; layout(location = 8) in vec3 aEz;
|
|
26
|
+
layout(location = 9) in vec3 aCol;
|
|
27
|
+
void main() {
|
|
28
|
+
vec3 w = aAt + aEx * aPos.x + aEy * aPos.y + aEz * aPos.z;
|
|
29
|
+
vec4 p = uVp * vec4(w, 1.0);
|
|
30
|
+
gl_Position = vec4(p.xy, p.z * 2.0 - p.w, p.w);
|
|
31
|
+
vN = normalize(aEx) * aNrm.x + normalize(aEy) * aNrm.y + normalize(aEz) * aNrm.z;
|
|
32
|
+
vC = aCol; vW = w; vM = vec4(0.0, 0.7, 1.0, 0.0); vS = 0.0;
|
|
33
|
+
}`;
|
|
34
|
+
// the edge lines: 1 pixel, nudged a hair towards the eye so they win against the faces they lie on
|
|
35
|
+
const GL_VERT_LINE = `#version 300 es
|
|
36
|
+
precision highp float;
|
|
37
|
+
uniform mat4 uVp;
|
|
38
|
+
layout(location = 0) in vec3 aPos;
|
|
39
|
+
void main() {
|
|
40
|
+
vec4 p = uVp * vec4(aPos, 1.0);
|
|
41
|
+
gl_Position = vec4(p.xy, p.z * 2.0 - p.w - 0.0004 * p.w, p.w);
|
|
42
|
+
}`;
|
|
43
|
+
const GL_FRAG_LINE = `#version 300 es
|
|
44
|
+
precision mediump float;
|
|
45
|
+
out vec4 outColor;
|
|
46
|
+
uniform float uUnlit;
|
|
47
|
+
void main() { outColor = uUnlit > 0.5 ? vec4(0.85, 0.85, 0.85, 0.55) : vec4(0.0, 0.0, 0.0, 0.4); }`;
|
|
48
|
+
// the marks: thick round-capped segments expanded to screen-space quads, over everything (see the WGSL vsm/fsm)
|
|
49
|
+
const GL_VERT_MARK = `#version 300 es
|
|
50
|
+
precision highp float;
|
|
51
|
+
uniform mat4 uVp; uniform vec3 uRes;
|
|
52
|
+
layout(location = 0) in vec3 aA; layout(location = 1) in vec3 aB; layout(location = 2) in vec4 aCol; layout(location = 3) in float aW;
|
|
53
|
+
out vec2 vQ; out vec4 vCol; out vec2 vH;
|
|
54
|
+
void main() {
|
|
55
|
+
vec4 ca = uVp * vec4(aA, 1.0);
|
|
56
|
+
vec4 cb = uVp * vec4(aB, 1.0);
|
|
57
|
+
float e = 0.001;
|
|
58
|
+
gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
|
|
59
|
+
if (ca.w < e && cb.w < e) return;
|
|
60
|
+
if (ca.w < e) ca = mix(ca, cb, (e - ca.w) / (cb.w - ca.w));
|
|
61
|
+
if (cb.w < e) cb = mix(cb, ca, (e - cb.w) / (ca.w - cb.w));
|
|
62
|
+
vec2 half_ = uRes.xy * 0.5;
|
|
63
|
+
vec2 pa = ca.xy / ca.w * half_;
|
|
64
|
+
vec2 pb = cb.xy / cb.w * half_;
|
|
65
|
+
float len = length(pb - pa);
|
|
66
|
+
vec2 d = len > 0.0001 ? (pb - pa) / len : vec2(1.0, 0.0);
|
|
67
|
+
float hw = aW * uRes.z * 0.5;
|
|
68
|
+
vec2 n = vec2(-d.y, d.x);
|
|
69
|
+
float cx[6] = float[6](0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
|
|
70
|
+
float cy[6] = float[6](-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
|
71
|
+
float ext = hw + 1.0;
|
|
72
|
+
float along = mix(-ext, len + ext, cx[gl_VertexID]);
|
|
73
|
+
float across = cy[gl_VertexID] * ext;
|
|
74
|
+
vec2 pos = pa + d * along + n * across;
|
|
75
|
+
gl_Position = vec4(pos / half_, 0.0, 1.0);
|
|
76
|
+
vQ = vec2(along, across); vCol = aCol; vH = vec2(len, hw);
|
|
77
|
+
}`;
|
|
78
|
+
const GL_FRAG_MARK = `#version 300 es
|
|
79
|
+
precision highp float;
|
|
80
|
+
in vec2 vQ; in vec4 vCol; in vec2 vH;
|
|
81
|
+
out vec4 outColor;
|
|
82
|
+
void main() {
|
|
83
|
+
float dx = max(max(-vQ.x, vQ.x - vH.x), 0.0);
|
|
84
|
+
float dist = length(vec2(dx, vQ.y));
|
|
85
|
+
float cov = clamp(vH.y + 0.5 - dist, 0.0, 1.0);
|
|
86
|
+
outColor = vec4(vCol.rgb, vCol.a * cov);
|
|
87
|
+
}`;
|
|
88
|
+
const GL_FRAG = /* glsl */ `#version 300 es
|
|
89
|
+
precision highp float;
|
|
90
|
+
uniform vec4 uLight; uniform vec4 uSky; uniform vec4 uGround; uniform vec4 uEye; uniform vec4 uMat;
|
|
91
|
+
uniform vec4 uFx; uniform vec4 uFill; uniform vec4 uRim;
|
|
92
|
+
uniform vec4 uLP[16]; uniform vec4 uLC[16]; uniform float uLN;
|
|
93
|
+
in vec3 vN; in vec3 vC; in vec3 vW; in vec4 vM; in float vS;
|
|
94
|
+
out vec4 outColor;
|
|
95
|
+
|
|
96
|
+
vec3 toLinear(vec3 c) { return mix(pow((c + 0.055) / 1.055, vec3(2.4)), c / 12.92, vec3(lessThanEqual(c, vec3(0.04045)))); }
|
|
97
|
+
vec3 toSrgb(vec3 c) { return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, c * 12.92, vec3(lessThanEqual(c, vec3(0.0031308)))); }
|
|
98
|
+
// ACES filmic curve (Narkowicz fit): highlights roll off softly instead of clipping
|
|
99
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3(0.0), vec3(1.0)); }
|
|
100
|
+
// Glow: every emitting cluster is a local light with a smooth falloff to zero at its reach
|
|
101
|
+
vec3 glowAt(vec3 p, vec3 n) {
|
|
102
|
+
vec3 sum = vec3(0.0);
|
|
103
|
+
for (int k = 0; k < 16; k++) {
|
|
104
|
+
if (float(k) >= uLN) break;
|
|
105
|
+
vec3 d = uLP[k].xyz - p;
|
|
106
|
+
float dist = max(length(d), 0.001);
|
|
107
|
+
float x = dist / uLP[k].w;
|
|
108
|
+
float win = clamp(1.0 - x * x * x * x, 0.0, 1.0);
|
|
109
|
+
win = win * win;
|
|
110
|
+
float s = dist / (uLP[k].w * 0.25);
|
|
111
|
+
float wrap = max((dot(n, d / dist) + 0.3) / 1.3, 0.0);
|
|
112
|
+
sum += uLC[k].rgb * (win / (1.0 + s * s)) * wrap * 1.6;
|
|
113
|
+
}
|
|
114
|
+
return sum;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Direct light, shared by both looks: x diffuse, y highlight, z scattered light (soft materials).
|
|
118
|
+
vec3 lights(vec3 n, vec3 v, float shin, float soft) {
|
|
119
|
+
vec3 key = normalize(uLight.xyz);
|
|
120
|
+
float ndl = dot(n, key);
|
|
121
|
+
float diff = max((ndl + soft) / (1.0 + soft), 0.0);
|
|
122
|
+
vec3 h = normalize(key + v);
|
|
123
|
+
float spec = pow(max(dot(n, h), 0.0), shin) * step(0.0, ndl);
|
|
124
|
+
float scat = soft * exp(-ndl * ndl * 5.0);
|
|
125
|
+
if (uFx.w > 0.5) {
|
|
126
|
+
diff = diff * 0.85;
|
|
127
|
+
vec3 fl = normalize(uFill.xyz);
|
|
128
|
+
diff = diff + max((dot(n, fl) + soft) / (1.0 + soft), 0.0) * uFill.w;
|
|
129
|
+
vec3 rl = normalize(uRim.xyz);
|
|
130
|
+
float edge = pow(1.0 - max(dot(n, v), 0.0), 2.5);
|
|
131
|
+
diff = diff + max(dot(n, rl), 0.0) * edge * uRim.w * 2.0;
|
|
132
|
+
vec3 rh = normalize(rl + v);
|
|
133
|
+
spec = spec + pow(max(dot(n, rh), 0.0), shin) * step(0.0, dot(n, rl)) * uRim.w * 0.6 * edge;
|
|
134
|
+
}
|
|
135
|
+
return vec3(diff, spec, scat);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
vec4 filmic(vec3 n, vec3 v) {
|
|
139
|
+
float metal = vM.x;
|
|
140
|
+
float rough = vM.y;
|
|
141
|
+
vec3 albedo = toLinear(vC);
|
|
142
|
+
vec3 hemi = mix(toLinear(uGround.rgb), toLinear(uSky.rgb), n.y * 0.5 + 0.5) * uMat.x * (uFx.w > 0.5 ? 0.7 : 1.0);
|
|
143
|
+
float shin = mix(uMat.w * 7.0, uMat.w * 0.15, rough);
|
|
144
|
+
vec3 lt = lights(n, v, shin, vS);
|
|
145
|
+
float diff = lt.x * uMat.y * 1.6;
|
|
146
|
+
float spec = lt.y * mix(uMat.z, 1.0, metal) * mix(1.0, 0.35, rough) * 2.5;
|
|
147
|
+
vec3 r = reflect(-v, n);
|
|
148
|
+
vec3 f0 = mix(vec3(0.04), albedo, metal);
|
|
149
|
+
vec3 fres = f0 + (vec3(1.0) - f0) * pow(1.0 - max(dot(n, v), 0.0), 5.0) * (1.0 - rough);
|
|
150
|
+
vec3 env = mix(toLinear(uGround.rgb) * 1.2, toLinear(uSky.rgb) * 1.5, smoothstep(-0.25 - rough * 0.5, 0.25 + rough * 0.5, r.y));
|
|
151
|
+
vec3 lit = albedo * (hemi + vec3(diff)) * (1.0 - metal)
|
|
152
|
+
+ albedo * env * metal * (1.0 - 0.6 * rough)
|
|
153
|
+
+ env * fres * (1.0 - rough) * 0.5
|
|
154
|
+
+ vec3(spec) * mix(vec3(1.0), albedo, metal);
|
|
155
|
+
lit = lit + albedo * vec3(1.0, 0.38, 0.26) * lt.z * 0.5 * (1.0 - metal);
|
|
156
|
+
lit = lit + albedo * glowAt(vW, n) * (1.0 - metal);
|
|
157
|
+
lit = lit + albedo * vM.w * uFx.z;
|
|
158
|
+
return vec4(toSrgb(aces(lit * uFx.y)), vM.z);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
void main() {
|
|
162
|
+
vec3 n = normalize(vN);
|
|
163
|
+
if (!gl_FrontFacing) n = -n;
|
|
164
|
+
vec3 v = normalize(uEye.xyz - vW);
|
|
165
|
+
if (uLight.w > 0.5) {
|
|
166
|
+
vec3 g = glowAt(vW, n) * (1.0 - vM.x);
|
|
167
|
+
if (uFx.x > 0.5) { outColor = vec4(toSrgb(aces(toLinear(vC) * (vM.w * uFx.z + g) * uFx.y)), vM.z); return; }
|
|
168
|
+
outColor = vec4(clamp(vC * (min(vM.w, 1.0) + g), vec3(0.0), vec3(1.0)), vM.z); return;
|
|
169
|
+
}
|
|
170
|
+
if (uFx.x > 0.5) { outColor = filmic(n, v); return; }
|
|
171
|
+
float metal = vM.x;
|
|
172
|
+
float rough = vM.y;
|
|
173
|
+
vec3 hemi = mix(uGround.rgb, uSky.rgb, n.y * 0.5 + 0.5) * uMat.x * (uFx.w > 0.5 ? 0.7 : 1.0);
|
|
174
|
+
float shin = mix(uMat.w * 7.0, uMat.w * 0.15, rough);
|
|
175
|
+
vec3 lt = lights(n, v, shin, vS);
|
|
176
|
+
float diff = lt.x * uMat.y;
|
|
177
|
+
float spec = lt.y * mix(uMat.z, 1.0, metal) * mix(1.0, 0.35, rough);
|
|
178
|
+
vec3 r = reflect(-v, n);
|
|
179
|
+
vec3 env = mix(uGround.rgb * 1.2, uSky.rgb * 1.5, smoothstep(-0.25 - rough * 0.5, 0.25 + rough * 0.5, r.y));
|
|
180
|
+
vec3 lit = vC * (hemi + vec3(diff)) * (1.0 - metal)
|
|
181
|
+
+ vC * env * metal * (1.0 - 0.6 * rough)
|
|
182
|
+
+ vec3(spec) * mix(vec3(1.0), vC, metal)
|
|
183
|
+
+ vC * vec3(1.0, 0.38, 0.26) * lt.z * 0.25 * (1.0 - metal);
|
|
184
|
+
lit = lit + vC * glowAt(vW, n) * (1.0 - metal);
|
|
185
|
+
lit = mix(lit, vC * 1.15, min(vM.w, 1.0));
|
|
186
|
+
outColor = vec4(clamp(lit, vec3(0.0), vec3(1.0)), vM.z);
|
|
187
|
+
}`;
|
|
188
|
+
/**
|
|
189
|
+
* Sets up WebGL2 on a canvas. Rejects when WebGL2 is not available. Same call and options as createGpu:
|
|
190
|
+
* returns { render(mesh, view, options), pixels(...), destroy() }. `device.lost` resolves when the context is lost.
|
|
191
|
+
*/
|
|
192
|
+
export const createGl = async (canvas, { onError } = {}) => {
|
|
193
|
+
const gl = canvas.getContext('webgl2', { antialias: true, alpha: false, preserveDrawingBuffer: false });
|
|
194
|
+
if (!gl)
|
|
195
|
+
throw new Error('WebGL2 is not available in this browser');
|
|
196
|
+
const lost = new Promise((resolve) => canvas.addEventListener('webglcontextlost', (e) => (e.preventDefault(), resolve({ reason: 'lost', message: 'WebGL context lost' }))));
|
|
197
|
+
const program = (vs, fs) => {
|
|
198
|
+
const sh = (type, src) => {
|
|
199
|
+
const s = gl.createShader(type);
|
|
200
|
+
gl.shaderSource(s, src);
|
|
201
|
+
gl.compileShader(s);
|
|
202
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
203
|
+
const message = `shader: ${gl.getShaderInfoLog(s)}`;
|
|
204
|
+
onError?.(message);
|
|
205
|
+
throw new Error(message);
|
|
206
|
+
}
|
|
207
|
+
return s;
|
|
208
|
+
};
|
|
209
|
+
const p = gl.createProgram();
|
|
210
|
+
gl.attachShader(p, sh(gl.VERTEX_SHADER, vs));
|
|
211
|
+
gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs));
|
|
212
|
+
gl.linkProgram(p);
|
|
213
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
|
214
|
+
const message = `program: ${gl.getProgramInfoLog(p)}`;
|
|
215
|
+
onError?.(message);
|
|
216
|
+
throw new Error(message);
|
|
217
|
+
}
|
|
218
|
+
return p;
|
|
219
|
+
};
|
|
220
|
+
const meshProg = program(GL_VERT, GL_FRAG);
|
|
221
|
+
const instProg = program(GL_VERT_INST, GL_FRAG);
|
|
222
|
+
const uni = (p) => {
|
|
223
|
+
const g = (n) => gl.getUniformLocation(p, n);
|
|
224
|
+
return { vp: g('uVp'), light: g('uLight'), sky: g('uSky'), ground: g('uGround'), eye: g('uEye'), mat: g('uMat'), fx: g('uFx'), fill: g('uFill'), rim: g('uRim'), lp: g('uLP'), lc: g('uLC'), ln: g('uLN') };
|
|
225
|
+
};
|
|
226
|
+
const lineProg = program(GL_VERT_LINE, GL_FRAG_LINE);
|
|
227
|
+
const lineVp = gl.getUniformLocation(lineProg, 'uVp');
|
|
228
|
+
const lineUnlit = gl.getUniformLocation(lineProg, 'uUnlit');
|
|
229
|
+
const markProg = program(GL_VERT_MARK, GL_FRAG_MARK);
|
|
230
|
+
const markVp = gl.getUniformLocation(markProg, 'uVp');
|
|
231
|
+
const markRes = gl.getUniformLocation(markProg, 'uRes');
|
|
232
|
+
const meshU = uni(meshProg);
|
|
233
|
+
const instU = uni(instProg);
|
|
234
|
+
const stride = GL_FLOATS * 4;
|
|
235
|
+
const buffer = () => gl.createBuffer();
|
|
236
|
+
const vaoWith = (setup) => {
|
|
237
|
+
const v = gl.createVertexArray();
|
|
238
|
+
gl.bindVertexArray(v);
|
|
239
|
+
setup();
|
|
240
|
+
gl.bindVertexArray(null);
|
|
241
|
+
return v;
|
|
242
|
+
};
|
|
243
|
+
const attr = (loc, size, stride, offset, divisor = 0) => {
|
|
244
|
+
gl.enableVertexAttribArray(loc);
|
|
245
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, stride, offset);
|
|
246
|
+
gl.vertexAttribDivisor(loc, divisor);
|
|
247
|
+
};
|
|
248
|
+
const solidBuf = buffer();
|
|
249
|
+
const glassBuf = buffer();
|
|
250
|
+
const protoBuf = buffer();
|
|
251
|
+
const instBuf = buffer();
|
|
252
|
+
const edgeBuf = buffer();
|
|
253
|
+
const edgeVao = vaoWith(() => {
|
|
254
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, edgeBuf);
|
|
255
|
+
attr(0, 3, 12, 0);
|
|
256
|
+
});
|
|
257
|
+
const markBuf = buffer();
|
|
258
|
+
const markVao = vaoWith(() => {
|
|
259
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, markBuf);
|
|
260
|
+
const s = MARK_FLOATS * 4;
|
|
261
|
+
attr(0, 3, s, 0, 1);
|
|
262
|
+
attr(1, 3, s, 12, 1);
|
|
263
|
+
attr(2, 4, s, 24, 1);
|
|
264
|
+
attr(3, 1, s, 40, 1);
|
|
265
|
+
});
|
|
266
|
+
let markCount = 0;
|
|
267
|
+
let edgeCount = 0;
|
|
268
|
+
let edgeMesh = null;
|
|
269
|
+
let edgeParts;
|
|
270
|
+
let edgeOn = false;
|
|
271
|
+
const meshAttrs = (b) => () => {
|
|
272
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, b);
|
|
273
|
+
attr(0, 3, stride, 0);
|
|
274
|
+
attr(1, 3, stride, 12);
|
|
275
|
+
attr(2, 3, stride, 24);
|
|
276
|
+
attr(3, 4, stride, 36);
|
|
277
|
+
attr(4, 1, stride, 52);
|
|
278
|
+
};
|
|
279
|
+
const solidVao = vaoWith(meshAttrs(solidBuf));
|
|
280
|
+
const glassVao = vaoWith(meshAttrs(glassBuf));
|
|
281
|
+
const instVao = vaoWith(() => {
|
|
282
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, protoBuf);
|
|
283
|
+
attr(0, 3, stride, 0);
|
|
284
|
+
attr(1, 3, stride, 12);
|
|
285
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
286
|
+
[5, 6, 7, 8, 9].forEach((loc, k) => attr(loc, 3, GL_INSTANCE_FLOATS * 4, k * 12, 1));
|
|
287
|
+
});
|
|
288
|
+
// Geometry that did not change since the last frame is not rebuilt or uploaded again.
|
|
289
|
+
let uploaded = null;
|
|
290
|
+
let uploadedPalette;
|
|
291
|
+
let uploadedSmooth = 0;
|
|
292
|
+
let partCache = new WeakMap();
|
|
293
|
+
let partPalette;
|
|
294
|
+
let partSmooth = 0;
|
|
295
|
+
let vertexCount = 0;
|
|
296
|
+
let protoMesh = null;
|
|
297
|
+
let protoCount = 0;
|
|
298
|
+
let instData = null;
|
|
299
|
+
let instCount = 0;
|
|
300
|
+
let glassTris = [];
|
|
301
|
+
let glassSource = new Float32Array(0);
|
|
302
|
+
let glassCount = 0;
|
|
303
|
+
let glassEye = '';
|
|
304
|
+
let px = 0;
|
|
305
|
+
const upload = (b, data) => {
|
|
306
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, b);
|
|
307
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
|
|
308
|
+
};
|
|
309
|
+
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, parts, tonemap = false, smooth = false, studio = false, edges = false, marks, unlit = false, glow = false } = {}) => {
|
|
310
|
+
const L = { ...defaultLight, ...light };
|
|
311
|
+
const filmic = tonemap ? { exposure: 1, emit: 2, ...(tonemap === true ? {} : tonemap) } : null;
|
|
312
|
+
const M = { ...defaultMaterial, ...material };
|
|
313
|
+
const cam = makeCamera(view, mesh, size, fit);
|
|
314
|
+
const n = Math.max(1, Math.round(size * dpr));
|
|
315
|
+
if (n !== px) {
|
|
316
|
+
px = n;
|
|
317
|
+
canvas.width = n;
|
|
318
|
+
canvas.height = n;
|
|
319
|
+
}
|
|
320
|
+
const crease = smooth === true ? 40 : smooth || 0;
|
|
321
|
+
if (mesh !== uploaded || palette !== uploadedPalette || crease !== uploadedSmooth) {
|
|
322
|
+
uploadedSmooth = crease;
|
|
323
|
+
let all;
|
|
324
|
+
if (parts && parts.length) {
|
|
325
|
+
if (palette !== partPalette || crease !== partSmooth) {
|
|
326
|
+
partCache = new WeakMap();
|
|
327
|
+
partPalette = palette;
|
|
328
|
+
partSmooth = crease;
|
|
329
|
+
}
|
|
330
|
+
const built = parts.map((m) => {
|
|
331
|
+
let v = partCache.get(m);
|
|
332
|
+
if (!v)
|
|
333
|
+
partCache.set(m, (v = buildVertices(m, palette, { smooth: crease })));
|
|
334
|
+
return v;
|
|
335
|
+
});
|
|
336
|
+
all = new Float32Array(built.reduce((s, v) => s + v.length, 0));
|
|
337
|
+
let at = 0;
|
|
338
|
+
for (const v of built) {
|
|
339
|
+
all.set(v, at);
|
|
340
|
+
at += v.length;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else
|
|
344
|
+
all = buildVertices(mesh, palette, { smooth: crease });
|
|
345
|
+
// solid triangles first; triangles with alpha < 1 are kept apart and blended afterwards
|
|
346
|
+
const tri = 3 * GL_FLOATS;
|
|
347
|
+
const solid = new Float32Array(all.length);
|
|
348
|
+
let solidLen = 0;
|
|
349
|
+
glassTris = [];
|
|
350
|
+
for (let o = 0; o < all.length; o += tri) {
|
|
351
|
+
if (all[o + 11] < 1) {
|
|
352
|
+
const c = [0, 1, 2].map((k) => (all[o + k] + all[o + GL_FLOATS + k] + all[o + 2 * GL_FLOATS + k]) / 3);
|
|
353
|
+
glassTris.push({ c, at: o });
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
solid.set(all.subarray(o, o + tri), solidLen);
|
|
357
|
+
solidLen += tri;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
glassSource = all;
|
|
361
|
+
glassEye = '';
|
|
362
|
+
upload(solidBuf, solid.subarray(0, solidLen));
|
|
363
|
+
vertexCount = solidLen / GL_FLOATS;
|
|
364
|
+
uploaded = mesh;
|
|
365
|
+
uploadedPalette = palette;
|
|
366
|
+
}
|
|
367
|
+
if (!edges)
|
|
368
|
+
edgeCount = 0;
|
|
369
|
+
else if (mesh !== edgeMesh || parts !== edgeParts || !edgeOn) {
|
|
370
|
+
const e = sceneEdges(mesh, parts);
|
|
371
|
+
upload(edgeBuf, e);
|
|
372
|
+
edgeCount = e.length / 3;
|
|
373
|
+
edgeMesh = mesh;
|
|
374
|
+
edgeParts = parts;
|
|
375
|
+
}
|
|
376
|
+
edgeOn = edges;
|
|
377
|
+
if (instances?.mesh && instances.mesh !== protoMesh) {
|
|
378
|
+
const v = buildVertices(instances.mesh);
|
|
379
|
+
upload(protoBuf, v);
|
|
380
|
+
protoCount = v.length / GL_FLOATS;
|
|
381
|
+
protoMesh = instances.mesh;
|
|
382
|
+
}
|
|
383
|
+
if (instances?.data && instances.data !== instData) {
|
|
384
|
+
upload(instBuf, instances.data);
|
|
385
|
+
instData = instances.data;
|
|
386
|
+
instCount = instances.data.length / GL_INSTANCE_FLOATS;
|
|
387
|
+
}
|
|
388
|
+
if (!instances)
|
|
389
|
+
instCount = 0;
|
|
390
|
+
const eyeKey = cam.eye.join(',');
|
|
391
|
+
if (glassTris.length && eyeKey !== glassEye) {
|
|
392
|
+
glassEye = eyeKey;
|
|
393
|
+
const [ex, ey, ez] = cam.eye;
|
|
394
|
+
const far = (t) => (t.c[0] - ex) ** 2 + (t.c[1] - ey) ** 2 + (t.c[2] - ez) ** 2;
|
|
395
|
+
const sorted = [...glassTris].sort((a, b) => far(b) - far(a)); // far first
|
|
396
|
+
const out = new Float32Array(sorted.length * 3 * GL_FLOATS);
|
|
397
|
+
sorted.forEach((t, k) => out.set(glassSource.subarray(t.at, t.at + 3 * GL_FLOATS), k * 3 * GL_FLOATS));
|
|
398
|
+
upload(glassBuf, out);
|
|
399
|
+
glassCount = sorted.length * 3;
|
|
400
|
+
}
|
|
401
|
+
if (!glassTris.length)
|
|
402
|
+
glassCount = 0;
|
|
403
|
+
// studio rig, relative to the camera: key upper left in front, fill from the right, rim from behind and above
|
|
404
|
+
const rig = (f, r, up) => norm([0, 1, 2].map((k) => -cam.f[k] * f + cam.r[k] * r + cam.u[k] * up));
|
|
405
|
+
const S = studio ? { fill: 0.4, rim: 1, ...(studio === true ? {} : studio) } : null;
|
|
406
|
+
const vp = viewProjection(cam, nearPlane(cam, mesh));
|
|
407
|
+
const key = S ? rig(0.7, -0.5, 0.6) : norm(L.direction);
|
|
408
|
+
const fill = rig(0.6, 0.8, 0.1);
|
|
409
|
+
const rim = rig(-0.8, 0.3, 0.5);
|
|
410
|
+
const glowLights = glow ? sceneLights(mesh, parts, palette) : new Float32Array(0);
|
|
411
|
+
const glowN = glowLights.length / LIGHT_FLOATS;
|
|
412
|
+
const glowPos = new Float32Array(MAX_LIGHTS * 4);
|
|
413
|
+
const glowCol = new Float32Array(MAX_LIGHTS * 4);
|
|
414
|
+
for (let k = 0; k < glowN; k++) {
|
|
415
|
+
glowPos.set(glowLights.subarray(k * LIGHT_FLOATS, k * LIGHT_FLOATS + 4), k * 4);
|
|
416
|
+
glowCol.set(glowLights.subarray(k * LIGHT_FLOATS + 4, k * LIGHT_FLOATS + 7), k * 4);
|
|
417
|
+
}
|
|
418
|
+
const setU = (u) => {
|
|
419
|
+
gl.uniformMatrix4fv(u.vp, false, vp);
|
|
420
|
+
gl.uniform4f(u.light, key[0], key[1], key[2], unlit ? 1 : 0);
|
|
421
|
+
gl.uniform4f(u.sky, L.sky[0], L.sky[1], L.sky[2], 0);
|
|
422
|
+
gl.uniform4f(u.ground, L.ground[0], L.ground[1], L.ground[2], 0);
|
|
423
|
+
gl.uniform4f(u.eye, cam.eye[0], cam.eye[1], cam.eye[2], 0);
|
|
424
|
+
gl.uniform4f(u.mat, M.ambient, M.diffuse, M.specular, M.shininess);
|
|
425
|
+
gl.uniform4f(u.fx, filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, S ? 1 : 0);
|
|
426
|
+
gl.uniform4f(u.fill, fill[0], fill[1], fill[2], S ? S.fill : 0);
|
|
427
|
+
gl.uniform4f(u.rim, rim[0], rim[1], rim[2], S ? S.rim : 0);
|
|
428
|
+
gl.uniform1f(u.ln, glowN);
|
|
429
|
+
if (glowN) {
|
|
430
|
+
gl.uniform4fv(u.lp, glowPos);
|
|
431
|
+
gl.uniform4fv(u.lc, glowCol);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
gl.viewport(0, 0, px, px);
|
|
435
|
+
gl.clearColor(background[0], background[1], background[2], 1);
|
|
436
|
+
gl.clearDepth(1);
|
|
437
|
+
gl.depthMask(true);
|
|
438
|
+
gl.disable(gl.BLEND);
|
|
439
|
+
gl.enable(gl.DEPTH_TEST);
|
|
440
|
+
gl.depthFunc(gl.LESS);
|
|
441
|
+
gl.disable(gl.CULL_FACE);
|
|
442
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
443
|
+
if (vertexCount) {
|
|
444
|
+
gl.useProgram(meshProg);
|
|
445
|
+
setU(meshU);
|
|
446
|
+
gl.bindVertexArray(solidVao);
|
|
447
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
|
|
448
|
+
}
|
|
449
|
+
if (instCount && protoCount) {
|
|
450
|
+
gl.useProgram(instProg);
|
|
451
|
+
setU(instU);
|
|
452
|
+
gl.bindVertexArray(instVao);
|
|
453
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, protoCount, instCount);
|
|
454
|
+
}
|
|
455
|
+
if (edgeCount) {
|
|
456
|
+
gl.useProgram(lineProg);
|
|
457
|
+
gl.uniformMatrix4fv(lineVp, false, vp);
|
|
458
|
+
gl.uniform1f(lineUnlit, unlit ? 1 : 0);
|
|
459
|
+
gl.enable(gl.BLEND);
|
|
460
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
461
|
+
gl.depthFunc(gl.LEQUAL);
|
|
462
|
+
gl.depthMask(false);
|
|
463
|
+
gl.bindVertexArray(edgeVao);
|
|
464
|
+
gl.drawArrays(gl.LINES, 0, edgeCount);
|
|
465
|
+
gl.depthMask(true);
|
|
466
|
+
gl.depthFunc(gl.LESS);
|
|
467
|
+
gl.disable(gl.BLEND);
|
|
468
|
+
}
|
|
469
|
+
if (glassCount) {
|
|
470
|
+
// after everything solid: see-through faces, far to near; they test depth but do not write it
|
|
471
|
+
gl.useProgram(meshProg);
|
|
472
|
+
setU(meshU);
|
|
473
|
+
gl.enable(gl.BLEND);
|
|
474
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
475
|
+
gl.depthMask(false);
|
|
476
|
+
gl.bindVertexArray(glassVao);
|
|
477
|
+
gl.drawArrays(gl.TRIANGLES, 0, glassCount);
|
|
478
|
+
gl.depthMask(true);
|
|
479
|
+
gl.disable(gl.BLEND);
|
|
480
|
+
}
|
|
481
|
+
const markData = marks?.length ? buildMarks(marks) : null;
|
|
482
|
+
markCount = markData ? markData.length / MARK_FLOATS : 0;
|
|
483
|
+
if (markCount && markData) {
|
|
484
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, markBuf);
|
|
485
|
+
gl.bufferData(gl.ARRAY_BUFFER, markData, gl.DYNAMIC_DRAW);
|
|
486
|
+
gl.useProgram(markProg);
|
|
487
|
+
gl.uniformMatrix4fv(markVp, false, vp);
|
|
488
|
+
gl.uniform3f(markRes, px, px, px / size);
|
|
489
|
+
gl.disable(gl.DEPTH_TEST);
|
|
490
|
+
gl.depthMask(false);
|
|
491
|
+
gl.enable(gl.BLEND);
|
|
492
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
493
|
+
gl.bindVertexArray(markVao);
|
|
494
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, markCount);
|
|
495
|
+
gl.disable(gl.BLEND);
|
|
496
|
+
gl.depthMask(true);
|
|
497
|
+
gl.enable(gl.DEPTH_TEST);
|
|
498
|
+
}
|
|
499
|
+
gl.bindVertexArray(null);
|
|
500
|
+
};
|
|
501
|
+
const render = (mesh, view, options) => draw(mesh, view, options);
|
|
502
|
+
const pixels = (mesh, view, options) => {
|
|
503
|
+
draw(mesh, view, options);
|
|
504
|
+
const raw = new Uint8Array(px * px * 4);
|
|
505
|
+
gl.readPixels(0, 0, px, px, gl.RGBA, gl.UNSIGNED_BYTE, raw); // read in the same task as the draw, before the buffer is presented
|
|
506
|
+
const data = new Uint8ClampedArray(px * px * 4);
|
|
507
|
+
for (let y = 0; y < px; y++)
|
|
508
|
+
data.set(raw.subarray((px - 1 - y) * px * 4, (px - y) * px * 4), y * px * 4); // GL rows go bottom up
|
|
509
|
+
for (let i = 3; i < data.length; i += 4)
|
|
510
|
+
data[i] = 255;
|
|
511
|
+
return Promise.resolve({ width: px, height: px, data });
|
|
512
|
+
};
|
|
513
|
+
const destroy = () => {
|
|
514
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
515
|
+
};
|
|
516
|
+
return { render, pixels, destroy, device: { lost }, adapter: null };
|
|
517
|
+
};
|
package/dist/gpu.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Palette } from './material.js';
|
|
2
|
-
import type { Axes, Camera, Fit, Mesh, Vec3, ViewName, ViewSpec } from './types.js';
|
|
2
|
+
import type { Axes, Camera, Fit, Mark, Mesh, Vec3, ViewName, ViewSpec } from './types.js';
|
|
3
3
|
type Handle = any;
|
|
4
4
|
export interface GpuLight {
|
|
5
5
|
/** Direction towards the light, world space. */
|
|
@@ -25,6 +25,12 @@ export interface GpuRenderOptions {
|
|
|
25
25
|
/** Named materials the faces point at (`face.mat`); pass a new object when an entry changes. */
|
|
26
26
|
palette?: Palette;
|
|
27
27
|
dpr?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The separate meshes `mesh` was merged from (a scene made of objects). When given, their vertices are built once per part
|
|
30
|
+
* and kept while the part (the same Mesh object), the palette and the smoothing stay the same, so adding or changing one
|
|
31
|
+
* object does not rebuild the others. `mesh` must still be their merge (it is used for the camera).
|
|
32
|
+
*/
|
|
33
|
+
parts?: Mesh[];
|
|
28
34
|
/** Many copies of one prototype mesh (modelled around the origin), drawn after `mesh`. */
|
|
29
35
|
instances?: {
|
|
30
36
|
mesh: Mesh;
|
|
@@ -39,6 +45,27 @@ export interface GpuRenderOptions {
|
|
|
39
45
|
exposure?: number;
|
|
40
46
|
emit?: number;
|
|
41
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Smooth shading: faces that meet at an angle smaller than this (degrees; `true` = 40) share their normals, so a curved
|
|
50
|
+
* surface made of flat faces looks round and sharper corners stay crisp. Off by default (every face flat).
|
|
51
|
+
*/
|
|
52
|
+
smooth?: boolean | number;
|
|
53
|
+
/**
|
|
54
|
+
* Studio light: three lights that follow the camera instead of one fixed sun: a key light (upper left), a fill light
|
|
55
|
+
* (right, softer) and a rim light from behind that edges the shape. Off by default.
|
|
56
|
+
*/
|
|
57
|
+
studio?: boolean | {
|
|
58
|
+
fill?: number;
|
|
59
|
+
rim?: number;
|
|
60
|
+
};
|
|
61
|
+
/** Thin (1 pixel) dark lines along the edges of every face, hidden where the surface is. Off by default. */
|
|
62
|
+
edges?: boolean;
|
|
63
|
+
/** No lighting at all: a surface shows only its emission (flat colour times emit); lines and marks still draw. Off by default. */
|
|
64
|
+
unlit?: boolean;
|
|
65
|
+
/** Glow: emitting materials light their surroundings as local lights (no shadows), up to 16 at a time. Off by default. */
|
|
66
|
+
glow?: boolean;
|
|
67
|
+
/** Thick lines and points on top of everything (selections, the cursor, handles): world-space polylines drawn by the GPU with round caps. */
|
|
68
|
+
marks?: Mark[];
|
|
42
69
|
}
|
|
43
70
|
/** One copy of a prototype: its position, orthonormal frame and color. */
|
|
44
71
|
export interface Instance {
|
|
@@ -59,13 +86,48 @@ export interface Gpu {
|
|
|
59
86
|
device: Handle;
|
|
60
87
|
adapter: Handle;
|
|
61
88
|
}
|
|
89
|
+
/** Floats of one mark segment: a (xyz), b (xyz), colour (rgba), width in CSS pixels, one spare. */
|
|
90
|
+
export declare const MARK_FLOATS = 12;
|
|
91
|
+
/** One instance per segment of every polyline of every mark (a zero-length segment is a round point). */
|
|
92
|
+
export declare const buildMarks: (marks: Mark[]) => Float32Array;
|
|
93
|
+
/** Line-list endpoints (x y z, x y z) of every edge of every face, each pair of vertices once. */
|
|
94
|
+
export declare const buildEdges: (mesh: Mesh) => Float32Array;
|
|
95
|
+
/** The edge lines of a scene: of its parts (kept while a part stays the same Mesh) or of the whole mesh. Empty when the scene is huge. */
|
|
96
|
+
export declare const sceneEdges: (mesh: Mesh, parts?: Mesh[]) => Float32Array;
|
|
97
|
+
/** Floats of one glow light: position (3), reach (1), colour times intensity (3), spare (1). */
|
|
98
|
+
export declare const LIGHT_FLOATS = 8;
|
|
99
|
+
export declare const MAX_LIGHTS = 16;
|
|
100
|
+
/**
|
|
101
|
+
* Glow: the emitting faces of a scene act as local light sources. Faces close to each other (one cell of a grid) merge into
|
|
102
|
+
* one light at their weighted centre; the brightest MAX_LIGHTS stay. Each light has a reach that grows with the emitting area.
|
|
103
|
+
* Float32Array, LIGHT_FLOATS per light.
|
|
104
|
+
*/
|
|
105
|
+
export declare const sceneLights: (mesh: Mesh, parts?: Mesh[], palette?: Palette) => Float32Array;
|
|
62
106
|
/**
|
|
63
107
|
* Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
|
|
64
108
|
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
65
109
|
*/
|
|
66
110
|
export declare const viewProjection: (cam: Camera, near?: number, far?: number) => Float32Array;
|
|
67
|
-
/**
|
|
68
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Near plane for a scene seen from `cam`: the usual 0.03, unless everything is very far (a galaxy seen from outside, the
|
|
113
|
+
* nearest point thousands of units away), where a near plane that close would leave no depth precision at all.
|
|
114
|
+
*/
|
|
115
|
+
export declare const nearPlane: (cam: Camera, mesh: {
|
|
116
|
+
positions: Vec3[];
|
|
117
|
+
}) => number;
|
|
118
|
+
/**
|
|
119
|
+
* Smooth shading: a normal for every corner of every face. The faces that meet at a point (welded by position, so a
|
|
120
|
+
* crack in the indices does not break it) are averaged, weighted by area, but only those within `crease` degrees of
|
|
121
|
+
* this face: a 90 degree box corner stays sharp, a faceted fuselage becomes round.
|
|
122
|
+
*/
|
|
123
|
+
export declare const cornerNormals: (mesh: Mesh, crease: number, soft?: number[]) => Vec3[][];
|
|
124
|
+
/**
|
|
125
|
+
* Triangle list for the GPU: face normals (flat, or smoothed with `smooth` degrees), face colors and materials.
|
|
126
|
+
* Float32Array, 14 floats per vertex.
|
|
127
|
+
*/
|
|
128
|
+
export declare const buildVertices: (mesh: Mesh, palette?: Palette, { smooth }?: {
|
|
129
|
+
smooth?: number;
|
|
130
|
+
}) => Float32Array;
|
|
69
131
|
/**
|
|
70
132
|
* Per-instance data for drawInstances: one entry { at, axes: [ex, ey, ez], color } per copy of a
|
|
71
133
|
* prototype mesh (modelled around the origin). Float32Array, 15 floats per instance.
|
|
@@ -80,7 +142,8 @@ export declare const defaultMaterial: Required<GpuMaterial>;
|
|
|
80
142
|
* prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
|
|
81
143
|
* light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
|
|
82
144
|
*/
|
|
83
|
-
export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount }?: {
|
|
145
|
+
export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount, onError }?: {
|
|
84
146
|
sampleCount?: number;
|
|
147
|
+
onError?: (message: string) => void;
|
|
85
148
|
}) => Promise<Gpu>;
|
|
86
149
|
export {};
|