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/gpu.js
CHANGED
|
@@ -3,15 +3,149 @@
|
|
|
3
3
|
// the cross marks and the framing all stay valid. The pure helpers (viewProjection,
|
|
4
4
|
// buildVertices) need no GPU and are unit tested; createGpu() needs a browser.
|
|
5
5
|
import { dot, norm } from './vec.js';
|
|
6
|
-
import { faceNormal } from './mesh.js';
|
|
6
|
+
import { bounds, faceNormal } from './mesh.js';
|
|
7
|
+
import { triangulate } from './triangulate.js';
|
|
7
8
|
import { makeCamera } from './camera.js';
|
|
8
9
|
import { surfaceOf } from './material.js';
|
|
9
|
-
|
|
10
|
+
/** Floats of one mark segment: a (xyz), b (xyz), colour (rgba), width in CSS pixels, one spare. */
|
|
11
|
+
export const MARK_FLOATS = 12;
|
|
12
|
+
const colorOf = (css) => {
|
|
13
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((css ?? '#0b7285').trim());
|
|
14
|
+
const h = m ? (m[1].length === 3 ? [...m[1]].map((c) => c + c).join('') : m[1]) : '0b7285';
|
|
15
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
16
|
+
};
|
|
17
|
+
/** One instance per segment of every polyline of every mark (a zero-length segment is a round point). */
|
|
18
|
+
export const buildMarks = (marks) => {
|
|
19
|
+
let n = 0;
|
|
20
|
+
for (const m of marks)
|
|
21
|
+
for (const l of m.lines)
|
|
22
|
+
n += Math.max(1, l.length - 1);
|
|
23
|
+
const out = new Float32Array(n * MARK_FLOATS);
|
|
24
|
+
let o = 0;
|
|
25
|
+
for (const m of marks) {
|
|
26
|
+
const [r, g, b] = colorOf(m.color);
|
|
27
|
+
for (const l of m.lines) {
|
|
28
|
+
const segs = l.length > 1 ? l.length - 1 : 1;
|
|
29
|
+
for (let i = 0; i < segs; i++) {
|
|
30
|
+
const a = l[i];
|
|
31
|
+
const c = l[Math.min(i + 1, l.length - 1)];
|
|
32
|
+
out.set([a[0], a[1], a[2], c[0], c[1], c[2], r, g, b, m.opacity ?? 1, m.width ?? 2], o);
|
|
33
|
+
o += MARK_FLOATS;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
/** Line-list endpoints (x y z, x y z) of every edge of every face, each pair of vertices once. */
|
|
40
|
+
export const buildEdges = (mesh) => {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const n = mesh.positions.length;
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const f of mesh.faces)
|
|
45
|
+
f.v.forEach((a, i) => {
|
|
46
|
+
const b = f.v[(i + 1) % f.v.length];
|
|
47
|
+
const k = a < b ? a * n + b : b * n + a;
|
|
48
|
+
if (seen.has(k))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(k);
|
|
51
|
+
out.push(...mesh.positions[a], ...mesh.positions[b]);
|
|
52
|
+
});
|
|
53
|
+
return new Float32Array(out);
|
|
54
|
+
};
|
|
55
|
+
const EDGE_LIMIT = 300000; // faces: a bigger scene (a galaxy of super-stars) is drawn without edge lines
|
|
56
|
+
const partEdges = new WeakMap();
|
|
57
|
+
/** 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. */
|
|
58
|
+
export const sceneEdges = (mesh, parts) => {
|
|
59
|
+
if (parts && parts.length) {
|
|
60
|
+
if (parts.reduce((n, m) => n + m.faces.length, 0) > EDGE_LIMIT)
|
|
61
|
+
return new Float32Array(0);
|
|
62
|
+
const built = parts.map((m) => {
|
|
63
|
+
let e = partEdges.get(m);
|
|
64
|
+
if (!e)
|
|
65
|
+
partEdges.set(m, (e = buildEdges(m)));
|
|
66
|
+
return e;
|
|
67
|
+
});
|
|
68
|
+
const all = new Float32Array(built.reduce((n, e) => n + e.length, 0));
|
|
69
|
+
let at = 0;
|
|
70
|
+
for (const e of built)
|
|
71
|
+
(all.set(e, at), (at += e.length));
|
|
72
|
+
return all;
|
|
73
|
+
}
|
|
74
|
+
return mesh.faces.length > EDGE_LIMIT ? new Float32Array(0) : buildEdges(mesh);
|
|
75
|
+
};
|
|
76
|
+
/** Floats of one glow light: position (3), reach (1), colour times intensity (3), spare (1). */
|
|
77
|
+
export const LIGHT_FLOATS = 8;
|
|
78
|
+
export const MAX_LIGHTS = 16;
|
|
79
|
+
const emitters = new WeakMap();
|
|
80
|
+
/** Every emitting face of a mesh: centre (3), area times emit (1), colour (3), area (1). */
|
|
81
|
+
const emittersOf = (mesh, palette) => {
|
|
82
|
+
const hit = emitters.get(mesh);
|
|
83
|
+
if (hit && hit.palette === palette)
|
|
84
|
+
return hit.data;
|
|
85
|
+
const rows = [];
|
|
86
|
+
for (const face of mesh.faces) {
|
|
87
|
+
const s = surfaceOf(face, palette);
|
|
88
|
+
if (s.emit < 0.05)
|
|
89
|
+
continue;
|
|
90
|
+
const pts = face.v.map((k) => mesh.positions[k]);
|
|
91
|
+
const area = Math.hypot(...(faceNormal(pts))) || 0;
|
|
92
|
+
if (!area)
|
|
93
|
+
continue;
|
|
94
|
+
const c = [0, 1, 2].map((k) => pts.reduce((a, p) => a + p[k], 0) / pts.length);
|
|
95
|
+
const [r, g, b] = colorOf(s.color);
|
|
96
|
+
rows.push(...c, area * s.emit, r, g, b, area);
|
|
97
|
+
}
|
|
98
|
+
const data = new Float32Array(rows);
|
|
99
|
+
emitters.set(mesh, { palette, data });
|
|
100
|
+
return data;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Glow: the emitting faces of a scene act as local light sources. Faces close to each other (one cell of a grid) merge into
|
|
104
|
+
* one light at their weighted centre; the brightest MAX_LIGHTS stay. Each light has a reach that grows with the emitting area.
|
|
105
|
+
* Float32Array, LIGHT_FLOATS per light.
|
|
106
|
+
*/
|
|
107
|
+
export const sceneLights = (mesh, parts, palette) => {
|
|
108
|
+
const list = (parts && parts.length ? parts : [mesh]).map((m) => emittersOf(m, palette)).filter((e) => e.length);
|
|
109
|
+
if (!list.length)
|
|
110
|
+
return new Float32Array(0);
|
|
111
|
+
const lo = [Infinity, Infinity, Infinity];
|
|
112
|
+
const hi = [-Infinity, -Infinity, -Infinity];
|
|
113
|
+
for (const e of list)
|
|
114
|
+
for (let i = 0; i < e.length; i += 8)
|
|
115
|
+
for (let k = 0; k < 3; k++)
|
|
116
|
+
(lo[k] = Math.min(lo[k], e[i + k]), (hi[k] = Math.max(hi[k], e[i + k])));
|
|
117
|
+
const cell = Math.max((Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) / 6) || 1, 1e-3);
|
|
118
|
+
const cells = new Map();
|
|
119
|
+
for (const e of list) {
|
|
120
|
+
for (let i = 0; i < e.length; i += 8) {
|
|
121
|
+
const key = [0, 1, 2].map((k) => Math.floor((e[i + k] - lo[k]) / cell)).join(',');
|
|
122
|
+
const c = cells.get(key) ?? { p: 0, x: 0, y: 0, z: 0, r: 0, g: 0, b: 0, a: 0 };
|
|
123
|
+
const w = e[i + 3];
|
|
124
|
+
c.p += w;
|
|
125
|
+
c.x += e[i] * w;
|
|
126
|
+
c.y += e[i + 1] * w;
|
|
127
|
+
c.z += e[i + 2] * w;
|
|
128
|
+
c.r += e[i + 4] * w;
|
|
129
|
+
c.g += e[i + 5] * w;
|
|
130
|
+
c.b += e[i + 6] * w;
|
|
131
|
+
c.a += e[i + 7];
|
|
132
|
+
cells.set(key, c);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const top = [...cells.values()].sort((a, b) => b.p - a.p).slice(0, MAX_LIGHTS);
|
|
136
|
+
const out = new Float32Array(top.length * LIGHT_FLOATS);
|
|
137
|
+
top.forEach((c, i) => {
|
|
138
|
+
const reach = Math.max(2.5 * Math.sqrt(c.p), 0.05); // a 1 m x 1 m lamp at emit 1 reaches 2.5 m
|
|
139
|
+
out.set([c.x / c.p, c.y / c.p, c.z / c.p, reach, c.r / c.p, c.g / c.p, c.b / c.p, 0], i * LIGHT_FLOATS);
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
143
|
+
const FLOATS = 14; // position(3) normal(3) color(3) material(4: metal, rough, alpha, emit) soft(1)
|
|
10
144
|
/**
|
|
11
145
|
* Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
|
|
12
146
|
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
13
147
|
*/
|
|
14
|
-
export const viewProjection = (cam, near = 0.03, far =
|
|
148
|
+
export const viewProjection = (cam, near = 0.03, far = 1e9) => {
|
|
15
149
|
const kk = cam.k / (cam.size / 2);
|
|
16
150
|
const row = (v, s) => [v[0] * s, v[1] * s, v[2] * s, -dot(v, cam.eye) * s];
|
|
17
151
|
const a = cam.ortho ? 1 / (far - near) : far / (far - near);
|
|
@@ -22,6 +156,21 @@ export const viewProjection = (cam, near = 0.03, far = 1000) => {
|
|
|
22
156
|
rows.forEach((r, i) => r.forEach((v, j) => (m[j * 4 + i] = v)));
|
|
23
157
|
return m;
|
|
24
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Near plane for a scene seen from `cam`: the usual 0.03, unless everything is very far (a galaxy seen from outside, the
|
|
161
|
+
* nearest point thousands of units away), where a near plane that close would leave no depth precision at all.
|
|
162
|
+
*/
|
|
163
|
+
export const nearPlane = (cam, mesh) => {
|
|
164
|
+
const [ex, ey, ez] = cam.eye;
|
|
165
|
+
const [fx, fy, fz] = cam.f;
|
|
166
|
+
let nearest = Infinity;
|
|
167
|
+
for (const p of mesh.positions) {
|
|
168
|
+
const d = (p[0] - ex) * fx + (p[1] - ey) * fy + (p[2] - ez) * fz;
|
|
169
|
+
if (d > 1e-6 && d < nearest)
|
|
170
|
+
nearest = d;
|
|
171
|
+
}
|
|
172
|
+
return nearest > 1000 ? nearest * 0.25 : 0.03;
|
|
173
|
+
};
|
|
25
174
|
const rgb = (color) => {
|
|
26
175
|
const h = String(color ?? '').replace('#', '');
|
|
27
176
|
const full = h.length === 3 ? [...h].map((c) => c + c).join('') : h;
|
|
@@ -29,26 +178,67 @@ const rgb = (color) => {
|
|
|
29
178
|
return [0.78, 0.78, 0.78];
|
|
30
179
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
31
180
|
};
|
|
32
|
-
/**
|
|
33
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Smooth shading: a normal for every corner of every face. The faces that meet at a point (welded by position, so a
|
|
183
|
+
* crack in the indices does not break it) are averaged, weighted by area, but only those within `crease` degrees of
|
|
184
|
+
* this face: a 90 degree box corner stays sharp, a faceted fuselage becomes round.
|
|
185
|
+
*/
|
|
186
|
+
export const cornerNormals = (mesh, crease, soft) => {
|
|
187
|
+
// the material's soft decides how far its edges may be smoothed: 0 keeps every edge hard, from 0.5 up the whole crease angle is allowed;
|
|
188
|
+
// between two faces the harder one rules
|
|
189
|
+
const cosAt = (i, j) => {
|
|
190
|
+
const k = soft ? Math.min(1, 2 * Math.min(soft[i], soft[j])) : 1;
|
|
191
|
+
return Math.cos((crease * k * Math.PI) / 180);
|
|
192
|
+
};
|
|
193
|
+
const area = mesh.faces.map((f) => faceNormal(f.v.map((k) => mesh.positions[k]))); // length = area
|
|
194
|
+
const unit = area.map((n) => (Math.hypot(...n) > 0 ? norm(n) : [0, 1, 0]));
|
|
195
|
+
const q = (bounds(mesh).radius || 1) * 1e-6;
|
|
196
|
+
const key = (p) => `${Math.round(p[0] / q)},${Math.round(p[1] / q)},${Math.round(p[2] / q)}`;
|
|
197
|
+
const at = new Map();
|
|
198
|
+
mesh.faces.forEach((f, i) => f.v.forEach((k) => {
|
|
199
|
+
const id = key(mesh.positions[k]);
|
|
200
|
+
const list = at.get(id) ?? [];
|
|
201
|
+
if (list[list.length - 1] !== i)
|
|
202
|
+
list.push(i);
|
|
203
|
+
at.set(id, list);
|
|
204
|
+
}));
|
|
205
|
+
return mesh.faces.map((f, i) => f.v.map((k) => {
|
|
206
|
+
const sum = [0, 0, 0];
|
|
207
|
+
for (const j of at.get(key(mesh.positions[k])) ?? []) {
|
|
208
|
+
if (i !== j && (soft ? Math.min(soft[i], soft[j]) <= 0 : false))
|
|
209
|
+
continue;
|
|
210
|
+
if (dot(unit[i], unit[j]) < cosAt(i, j) - 1e-12)
|
|
211
|
+
continue;
|
|
212
|
+
for (let c = 0; c < 3; c++)
|
|
213
|
+
sum[c] += area[j][c];
|
|
214
|
+
}
|
|
215
|
+
return Math.hypot(...sum) > 0 ? norm(sum) : unit[i];
|
|
216
|
+
}));
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Triangle list for the GPU: face normals (flat, or smoothed with `smooth` degrees), face colors and materials.
|
|
220
|
+
* Float32Array, 14 floats per vertex.
|
|
221
|
+
*/
|
|
222
|
+
export const buildVertices = (mesh, palette, { smooth = 0 } = {}) => {
|
|
34
223
|
let tris = 0;
|
|
35
224
|
for (const f of mesh.faces)
|
|
36
225
|
tris += Math.max(0, f.v.length - 2);
|
|
37
226
|
const out = new Float32Array(tris * 3 * FLOATS);
|
|
227
|
+
const corners = smooth > 0 ? cornerNormals(mesh, smooth, mesh.faces.map((f) => surfaceOf(f, palette).soft)) : null;
|
|
38
228
|
let o = 0;
|
|
39
|
-
|
|
229
|
+
mesh.faces.forEach((face, fi) => {
|
|
40
230
|
const pts = face.v.map((k) => mesh.positions[k]);
|
|
41
|
-
const
|
|
231
|
+
const flat = norm(faceNormal(pts));
|
|
42
232
|
const s = surfaceOf(face, palette);
|
|
43
233
|
const c = rgb(s.color);
|
|
44
234
|
const mat = [s.metal, s.rough, s.alpha, s.emit];
|
|
45
|
-
for (
|
|
46
|
-
for (const
|
|
47
|
-
out.set([...
|
|
235
|
+
for (const tri of triangulate(pts)) {
|
|
236
|
+
for (const k of tri) {
|
|
237
|
+
out.set([...pts[k], ...(corners ? corners[fi][k] : flat), ...c, ...mat, s.soft], o);
|
|
48
238
|
o += FLOATS;
|
|
49
239
|
}
|
|
50
240
|
}
|
|
51
|
-
}
|
|
241
|
+
});
|
|
52
242
|
return out;
|
|
53
243
|
};
|
|
54
244
|
const INSTANCE_FLOATS = 15; // at(3) ex(3) ey(3) ez(3) color(3)
|
|
@@ -71,17 +261,23 @@ struct U {
|
|
|
71
261
|
ground : vec4<f32>,
|
|
72
262
|
eye : vec4<f32>,
|
|
73
263
|
mat : vec4<f32>, // ambient, diffuse, specular, shininess
|
|
74
|
-
fx : vec4<f32>, // filmic (0 or 1), exposure, emission gain
|
|
264
|
+
fx : vec4<f32>, // filmic (0 or 1), exposure, emission gain, studio (0 or 1)
|
|
265
|
+
fill : vec4<f32>, // studio: xyz direction towards the fill light, w intensity
|
|
266
|
+
rim : vec4<f32>, // studio: xyz direction towards the rim light, w intensity
|
|
267
|
+
res : vec4<f32>, // marks: canvas pixels (x, y), device pixels per CSS pixel (z); w: number of glow lights
|
|
268
|
+
lpos : array<vec4<f32>, 16>, // glow lights: xyz position, w reach
|
|
269
|
+
lcol : array<vec4<f32>, 16>, // rgb colour times intensity
|
|
75
270
|
};
|
|
76
271
|
@group(0) @binding(0) var<uniform> u : U;
|
|
77
272
|
|
|
78
|
-
struct VIn { @location(0) pos : vec3<f32>, @location(1) nrm : vec3<f32>, @location(2) col : vec3<f32>, @location(8) mat : vec4<f32
|
|
273
|
+
struct VIn { @location(0) pos : vec3<f32>, @location(1) nrm : vec3<f32>, @location(2) col : vec3<f32>, @location(8) mat : vec4<f32>, @location(9) soft : f32 };
|
|
79
274
|
struct VOut {
|
|
80
275
|
@builtin(position) p : vec4<f32>,
|
|
81
276
|
@location(0) n : vec3<f32>,
|
|
82
277
|
@location(1) c : vec3<f32>,
|
|
83
278
|
@location(2) w : vec3<f32>,
|
|
84
279
|
@location(3) m : vec4<f32>, // metal, rough, alpha, emit
|
|
280
|
+
@location(4) s : f32, // soft
|
|
85
281
|
};
|
|
86
282
|
|
|
87
283
|
@vertex fn vs(i : VIn) -> VOut {
|
|
@@ -91,6 +287,7 @@ struct VOut {
|
|
|
91
287
|
o.c = i.col;
|
|
92
288
|
o.w = i.pos;
|
|
93
289
|
o.m = i.mat;
|
|
290
|
+
o.s = i.soft;
|
|
94
291
|
return o;
|
|
95
292
|
}
|
|
96
293
|
|
|
@@ -110,8 +307,61 @@ struct IIn {
|
|
|
110
307
|
o.c = i.col;
|
|
111
308
|
o.w = w;
|
|
112
309
|
o.m = vec4<f32>(0.0, 0.7, 1.0, 0.0);
|
|
310
|
+
o.s = 0.0;
|
|
311
|
+
return o;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// the edge lines: 1 pixel, nudged a hair towards the eye so they win against the faces they lie on
|
|
315
|
+
@vertex fn vsl(@location(0) pos : vec3<f32>) -> @builtin(position) vec4<f32> {
|
|
316
|
+
var p = u.vp * vec4<f32>(pos, 1.0);
|
|
317
|
+
p.z = p.z - 0.0002 * p.w;
|
|
318
|
+
return p;
|
|
319
|
+
}
|
|
320
|
+
@fragment fn fsl() -> @location(0) vec4<f32> {
|
|
321
|
+
// dark lines on the lit picture; with no light the picture is dark, so the lines turn light
|
|
322
|
+
let g = select(0.0, 0.85, u.light.w > 0.5);
|
|
323
|
+
return vec4<f32>(g, g, g, select(0.4, 0.55, u.light.w > 0.5));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// the marks: thick round-capped segments, expanded to screen-space quads, drawn over everything (no depth test)
|
|
327
|
+
struct MOut { @builtin(position) p : vec4<f32>, @location(0) q : vec2<f32>, @location(1) c : vec4<f32>, @location(2) h : vec3<f32> };
|
|
328
|
+
@vertex fn vsm(@builtin(vertex_index) vi : u32, @location(0) a : vec3<f32>, @location(1) b : vec3<f32>, @location(2) col : vec4<f32>, @location(3) wd : f32) -> MOut {
|
|
329
|
+
var o : MOut;
|
|
330
|
+
var ca = u.vp * vec4<f32>(a, 1.0);
|
|
331
|
+
var cb = u.vp * vec4<f32>(b, 1.0);
|
|
332
|
+
let e = 0.001;
|
|
333
|
+
o.p = vec4<f32>(2.0, 2.0, 2.0, 1.0);
|
|
334
|
+
if (ca.w < e && cb.w < e) { return o; }
|
|
335
|
+
if (ca.w < e) { ca = mix(ca, cb, (e - ca.w) / (cb.w - ca.w)); }
|
|
336
|
+
if (cb.w < e) { cb = mix(cb, ca, (e - cb.w) / (ca.w - cb.w)); }
|
|
337
|
+
let half = vec2<f32>(u.res.x, u.res.y) * 0.5;
|
|
338
|
+
let pa = ca.xy / ca.w * half;
|
|
339
|
+
let pb = cb.xy / cb.w * half;
|
|
340
|
+
let len = length(pb - pa);
|
|
341
|
+
var d = vec2<f32>(1.0, 0.0);
|
|
342
|
+
if (len > 0.0001) { d = (pb - pa) / len; }
|
|
343
|
+
let hw = wd * u.res.z * 0.5;
|
|
344
|
+
let n = vec2<f32>(-d.y, d.x);
|
|
345
|
+
let cx = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
|
|
346
|
+
let cy = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
|
347
|
+
let t = cx[vi];
|
|
348
|
+
let s = cy[vi];
|
|
349
|
+
let ext = hw + 1.0;
|
|
350
|
+
let along = mix(-ext, len + ext, t);
|
|
351
|
+
let across = s * ext;
|
|
352
|
+
let pos = pa + d * along + n * across;
|
|
353
|
+
o.p = vec4<f32>(pos / half, 0.0, 1.0);
|
|
354
|
+
o.q = vec2<f32>(along, across);
|
|
355
|
+
o.c = col;
|
|
356
|
+
o.h = vec3<f32>(len, hw, 0.0);
|
|
113
357
|
return o;
|
|
114
358
|
}
|
|
359
|
+
@fragment fn fsm(i : MOut) -> @location(0) vec4<f32> {
|
|
360
|
+
let dx = max(max(-i.q.x, i.q.x - i.h.x), 0.0);
|
|
361
|
+
let dist = length(vec2<f32>(dx, i.q.y));
|
|
362
|
+
let cov = clamp(i.h.y + 0.5 - dist, 0.0, 1.0);
|
|
363
|
+
return vec4<f32>(i.c.rgb, i.c.a * cov);
|
|
364
|
+
}
|
|
115
365
|
|
|
116
366
|
fn toLinear(c : vec3<f32>) -> vec3<f32> {
|
|
117
367
|
return select(pow((c + 0.055) / 1.055, vec3<f32>(2.4)), c / 12.92, c <= vec3<f32>(0.04045));
|
|
@@ -124,16 +374,57 @@ fn aces(x : vec3<f32>) -> vec3<f32> {
|
|
|
124
374
|
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
125
375
|
}
|
|
126
376
|
|
|
377
|
+
// Direct light, shared by both looks: x diffuse, y highlight, z scattered light (soft materials).
|
|
378
|
+
// Studio: the key light is u.light (placed by the camera), plus a softer fill and a rim light that only edges the shape.
|
|
379
|
+
fn lights(n : vec3<f32>, v : vec3<f32>, shin : f32, soft : f32) -> vec3<f32> {
|
|
380
|
+
let key = normalize(u.light.xyz);
|
|
381
|
+
let ndl = dot(n, key);
|
|
382
|
+
// soft: the light wraps round the form, and a warm band of scattered light sits where light turns to shade
|
|
383
|
+
var diff = max((ndl + soft) / (1.0 + soft), 0.0);
|
|
384
|
+
let h = normalize(key + v);
|
|
385
|
+
var spec = pow(max(dot(n, h), 0.0), shin) * step(0.0, ndl);
|
|
386
|
+
let scat = soft * exp(-ndl * ndl * 5.0);
|
|
387
|
+
if (u.fx.w > 0.5) {
|
|
388
|
+
diff = diff * 0.85; // three lights add up: the key takes a little less
|
|
389
|
+
let fl = normalize(u.fill.xyz);
|
|
390
|
+
diff = diff + max((dot(n, fl) + soft) / (1.0 + soft), 0.0) * u.fill.w;
|
|
391
|
+
let rl = normalize(u.rim.xyz);
|
|
392
|
+
let edge = pow(1.0 - max(dot(n, v), 0.0), 2.5);
|
|
393
|
+
diff = diff + max(dot(n, rl), 0.0) * edge * u.rim.w * 2.0;
|
|
394
|
+
let rh = normalize(rl + v);
|
|
395
|
+
spec = spec + pow(max(dot(n, rh), 0.0), shin) * step(0.0, dot(n, rl)) * u.rim.w * 0.6 * edge;
|
|
396
|
+
}
|
|
397
|
+
return vec3<f32>(diff, spec, scat);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Glow: every emitting cluster is a local light with a smooth falloff to zero at its reach, a little wrap so shade edges soften
|
|
401
|
+
fn glowAt(p : vec3<f32>, n : vec3<f32>) -> vec3<f32> {
|
|
402
|
+
var sum = vec3<f32>(0.0);
|
|
403
|
+
let cnt = u32(u.res.w);
|
|
404
|
+
for (var k = 0u; k < cnt; k = k + 1u) {
|
|
405
|
+
let d = u.lpos[k].xyz - p;
|
|
406
|
+
let dist = max(length(d), 0.001);
|
|
407
|
+
let reach = u.lpos[k].w;
|
|
408
|
+
let x = dist / reach;
|
|
409
|
+
var win = clamp(1.0 - x * x * x * x, 0.0, 1.0);
|
|
410
|
+
win = win * win;
|
|
411
|
+
let s = dist / (reach * 0.25);
|
|
412
|
+
let wrap = max((dot(n, d / dist) + 0.3) / 1.3, 0.0);
|
|
413
|
+
sum = sum + u.lcol[k].rgb * (win / (1.0 + s * s)) * wrap * 1.6;
|
|
414
|
+
}
|
|
415
|
+
return sum;
|
|
416
|
+
}
|
|
417
|
+
|
|
127
418
|
// The filmic path: the same light model, but in linear colour with HDR light and emission, then tone mapped.
|
|
128
419
|
fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
129
420
|
let metal = i.m.x;
|
|
130
421
|
let rough = i.m.y;
|
|
131
422
|
let albedo = toLinear(i.c);
|
|
132
|
-
let hemi = mix(toLinear(u.ground.rgb), toLinear(u.sky.rgb), n.y * 0.5 + 0.5) * u.mat.x;
|
|
133
|
-
let diff = max(dot(n, l), 0.0) * u.mat.y * 1.6;
|
|
134
|
-
let h = normalize(l + v);
|
|
423
|
+
let hemi = mix(toLinear(u.ground.rgb), toLinear(u.sky.rgb), n.y * 0.5 + 0.5) * u.mat.x * select(1.0, 0.7, u.fx.w > 0.5);
|
|
135
424
|
let shin = mix(u.mat.w * 7.0, u.mat.w * 0.15, rough);
|
|
136
|
-
let
|
|
425
|
+
let lt = lights(n, v, shin, i.s);
|
|
426
|
+
let diff = lt.x * u.mat.y * 1.6;
|
|
427
|
+
let spec = lt.y * mix(u.mat.z, 1.0, metal) * mix(1.0, 0.35, rough) * 2.5;
|
|
137
428
|
let r = reflect(-v, n);
|
|
138
429
|
// Schlick: edges mirror more, metal keeps its tint
|
|
139
430
|
let f0 = mix(vec3<f32>(0.04), albedo, metal);
|
|
@@ -144,6 +435,8 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
144
435
|
+ env * fres * (1.0 - rough) * 0.5
|
|
145
436
|
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), albedo, metal);
|
|
146
437
|
// emission adds light, so a lamp can go above 1.0 and still keep its colour after tone mapping
|
|
438
|
+
lit = lit + albedo * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.5 * (1.0 - metal); // soft: the warm scattered band
|
|
439
|
+
lit = lit + albedo * toLinear(vec3<f32>(1.0)) * glowAt(i.w, n) * (1.0 - metal);
|
|
147
440
|
lit = lit + albedo * i.m.w * u.fx.z;
|
|
148
441
|
return vec4<f32>(toSrgb(aces(lit * u.fx.y)), i.m.z);
|
|
149
442
|
}
|
|
@@ -153,21 +446,29 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
153
446
|
if (!front) { n = -n; }
|
|
154
447
|
let l = normalize(u.light.xyz);
|
|
155
448
|
let v = normalize(u.eye.xyz - i.w);
|
|
449
|
+
if (u.light.w > 0.5) {
|
|
450
|
+
// unlit: no light in the scene, only what glows
|
|
451
|
+
let g = glowAt(i.w, n) * (1.0 - i.m.x);
|
|
452
|
+
if (u.fx.x > 0.5) { return vec4<f32>(toSrgb(aces((toLinear(i.c) * (i.m.w * u.fx.z + g)) * u.fx.y)), i.m.z); }
|
|
453
|
+
return vec4<f32>(clamp(i.c * (min(i.m.w, 1.0) + g), vec3<f32>(0.0), vec3<f32>(1.0)), i.m.z);
|
|
454
|
+
}
|
|
156
455
|
if (u.fx.x > 0.5) { return filmic(i, n, l, v); }
|
|
157
456
|
let metal = i.m.x;
|
|
158
457
|
let rough = i.m.y;
|
|
159
|
-
let hemi = mix(u.ground.rgb, u.sky.rgb, n.y * 0.5 + 0.5) * u.mat.x;
|
|
160
|
-
let diff = max(dot(n, l), 0.0) * u.mat.y;
|
|
161
|
-
let h = normalize(l + v);
|
|
458
|
+
let hemi = mix(u.ground.rgb, u.sky.rgb, n.y * 0.5 + 0.5) * u.mat.x * select(1.0, 0.7, u.fx.w > 0.5);
|
|
162
459
|
// roughness widens the highlight; metal makes it strong and tinted by the color
|
|
163
460
|
let shin = mix(u.mat.w * 7.0, u.mat.w * 0.15, rough);
|
|
164
|
-
let
|
|
461
|
+
let lt = lights(n, v, shin, i.s);
|
|
462
|
+
let diff = lt.x * u.mat.y;
|
|
463
|
+
let spec = lt.y * mix(u.mat.z, 1.0, metal) * mix(1.0, 0.35, rough);
|
|
165
464
|
// metal mirrors the sky above and the ground below, blurred by roughness
|
|
166
465
|
let r = reflect(-v, n);
|
|
167
466
|
let env = mix(u.ground.rgb * 1.2, u.sky.rgb * 1.5, smoothstep(-0.25 - rough * 0.5, 0.25 + rough * 0.5, r.y));
|
|
168
467
|
var lit = i.c * (hemi + vec3<f32>(diff)) * (1.0 - metal)
|
|
169
468
|
+ i.c * env * metal * (1.0 - 0.6 * rough)
|
|
170
|
-
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), i.c, metal)
|
|
469
|
+
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), i.c, metal)
|
|
470
|
+
+ i.c * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.25 * (1.0 - metal);
|
|
471
|
+
lit = lit + i.c * glowAt(i.w, n) * (1.0 - metal);
|
|
171
472
|
lit = mix(lit, i.c * 1.15, min(i.m.w, 1.0)); // the plain look: emit 0..1 fades to flat colour, more does not add
|
|
172
473
|
return vec4<f32>(clamp(lit, vec3<f32>(0.0), vec3<f32>(1.0)), i.m.z);
|
|
173
474
|
}
|
|
@@ -179,14 +480,18 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
179
480
|
* prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
|
|
180
481
|
* light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
|
|
181
482
|
*/
|
|
182
|
-
export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
483
|
+
export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
183
484
|
if (!globalThis.navigator?.gpu)
|
|
184
485
|
throw new Error('WebGPU is not available in this browser');
|
|
185
486
|
const adapter = await navigator.gpu.requestAdapter();
|
|
186
487
|
if (!adapter)
|
|
187
488
|
throw new Error('No WebGPU adapter found');
|
|
188
489
|
const device = await adapter.requestDevice();
|
|
189
|
-
device.addEventListener('uncapturederror', (e) =>
|
|
490
|
+
device.addEventListener('uncapturederror', (e) => {
|
|
491
|
+
const message = String(e.error?.message ?? e);
|
|
492
|
+
console.error('WebGPU:', message);
|
|
493
|
+
onError?.(message);
|
|
494
|
+
});
|
|
190
495
|
device.lost.then((info) => console.error('WebGPU device lost:', info.reason, info.message));
|
|
191
496
|
const context = canvas.getContext('webgpu');
|
|
192
497
|
if (!context)
|
|
@@ -194,6 +499,12 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
194
499
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
195
500
|
context.configure({ device, format, alphaMode: 'opaque', });
|
|
196
501
|
const module = device.createShaderModule({ code: SHADER });
|
|
502
|
+
// shader compile errors are reported to onError too (the browser only logs them to the console)
|
|
503
|
+
module.getCompilationInfo?.().then((info) => {
|
|
504
|
+
for (const m of info.messages ?? [])
|
|
505
|
+
if (m.type === 'error')
|
|
506
|
+
onError?.(`shader line ${m.lineNum}: ${m.message}`);
|
|
507
|
+
});
|
|
197
508
|
const meshLayout = {
|
|
198
509
|
arrayStride: FLOATS * 4,
|
|
199
510
|
attributes: [
|
|
@@ -201,6 +512,7 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
201
512
|
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
202
513
|
{ shaderLocation: 2, offset: 24, format: 'float32x3' },
|
|
203
514
|
{ shaderLocation: 8, offset: 36, format: 'float32x4' },
|
|
515
|
+
{ shaderLocation: 9, offset: 52, format: 'float32' },
|
|
204
516
|
],
|
|
205
517
|
};
|
|
206
518
|
const instanceLayout = {
|
|
@@ -208,8 +520,14 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
208
520
|
stepMode: 'instance',
|
|
209
521
|
attributes: [3, 4, 5, 6, 7].map((loc, k) => ({ shaderLocation: loc, offset: k * 12, format: 'float32x3' })),
|
|
210
522
|
};
|
|
523
|
+
// ONE explicit layout for every pipeline: with 'auto' each pipeline gets its own, and a bind group made for one is refused by the others
|
|
524
|
+
const bindLayout = device.createBindGroupLayout({
|
|
525
|
+
// visibility 1 | 2 = GPUShaderStage.VERTEX | FRAGMENT
|
|
526
|
+
entries: [{ binding: 0, visibility: 1 | 2, buffer: { type: 'uniform' } }],
|
|
527
|
+
});
|
|
528
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindLayout] });
|
|
211
529
|
const make = (entry, buffers, glass = false) => device.createRenderPipeline({
|
|
212
|
-
layout:
|
|
530
|
+
layout: pipelineLayout,
|
|
213
531
|
vertex: { module, entryPoint: entry, buffers },
|
|
214
532
|
fragment: {
|
|
215
533
|
module,
|
|
@@ -235,12 +553,55 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
235
553
|
const glassPipeline = make('vs', [meshLayout], true);
|
|
236
554
|
// the instanced pipeline reads position and normal only from the prototype (locations 0, 1)
|
|
237
555
|
const instPipeline = make('vsi', [{ ...meshLayout, attributes: meshLayout.attributes.slice(0, 2) }, instanceLayout]);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
556
|
+
const linePipeline = device.createRenderPipeline({
|
|
557
|
+
layout: pipelineLayout,
|
|
558
|
+
vertex: { module, entryPoint: 'vsl', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
|
|
559
|
+
fragment: {
|
|
560
|
+
module,
|
|
561
|
+
entryPoint: 'fsl',
|
|
562
|
+
targets: [{ format, blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' } } }],
|
|
563
|
+
},
|
|
564
|
+
primitive: { topology: 'line-list' },
|
|
565
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'less-equal' },
|
|
566
|
+
multisample: { count: sampleCount },
|
|
567
|
+
});
|
|
568
|
+
const markPipeline = device.createRenderPipeline({
|
|
569
|
+
layout: pipelineLayout,
|
|
570
|
+
vertex: {
|
|
571
|
+
module,
|
|
572
|
+
entryPoint: 'vsm',
|
|
573
|
+
buffers: [
|
|
574
|
+
{
|
|
575
|
+
arrayStride: MARK_FLOATS * 4,
|
|
576
|
+
stepMode: 'instance',
|
|
577
|
+
attributes: [
|
|
578
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
579
|
+
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
580
|
+
{ shaderLocation: 2, offset: 24, format: 'float32x4' },
|
|
581
|
+
{ shaderLocation: 3, offset: 40, format: 'float32' },
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
fragment: {
|
|
587
|
+
module,
|
|
588
|
+
entryPoint: 'fsm',
|
|
589
|
+
targets: [{ format, blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' } } }],
|
|
590
|
+
},
|
|
591
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
592
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'always' },
|
|
593
|
+
multisample: { count: sampleCount },
|
|
594
|
+
});
|
|
595
|
+
const uniforms = device.createBuffer({ size: 720, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
596
|
+
const bind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
597
|
+
const instBind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
241
598
|
// Geometry that did not change since the last frame is not rebuilt or uploaded again.
|
|
242
599
|
let uploaded = null; // the mesh now in vertexBuffer
|
|
243
600
|
let uploadedPalette; // and the palette it was built with
|
|
601
|
+
let uploadedSmooth = 0; // and the smoothing
|
|
602
|
+
let partCache = new WeakMap(); // vertices per part mesh (see `parts`)
|
|
603
|
+
let partPalette;
|
|
604
|
+
let partSmooth = 0;
|
|
244
605
|
let vertexCount = 0;
|
|
245
606
|
let protoMesh = null;
|
|
246
607
|
let protoBuffer = null;
|
|
@@ -251,6 +612,15 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
251
612
|
let instCount = 0;
|
|
252
613
|
let vertexBuffer = null;
|
|
253
614
|
let capacity = 0;
|
|
615
|
+
let markBuffer = null;
|
|
616
|
+
let markCapacity = 0;
|
|
617
|
+
let markCount = 0;
|
|
618
|
+
let edgeBuffer = null;
|
|
619
|
+
let edgeCapacity = 0;
|
|
620
|
+
let edgeCount = 0; // vertices (two per line)
|
|
621
|
+
let edgeMesh = null;
|
|
622
|
+
let edgeParts;
|
|
623
|
+
let edgeOn = false;
|
|
254
624
|
let glassBuffer = null; // the see-through triangles, sorted far to near for the eye
|
|
255
625
|
let glassCapacity = 0;
|
|
256
626
|
let glassTris = []; // centroid and offset (in floats) in glassSource
|
|
@@ -271,30 +641,55 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
271
641
|
depth = device.createTexture({ size: [n, n], format: 'depth24plus', sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
272
642
|
color = sampleCount > 1 ? device.createTexture({ size: [n, n], format, sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT }) : null;
|
|
273
643
|
};
|
|
274
|
-
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, tonemap = false } = {}, readback = false) => {
|
|
644
|
+
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 } = {}, readback = false) => {
|
|
275
645
|
const L = { ...defaultLight, ...light };
|
|
276
646
|
const filmic = tonemap ? { exposure: 1, emit: 2, ...(tonemap === true ? {} : tonemap) } : null;
|
|
277
647
|
const M = { ...defaultMaterial, ...material };
|
|
278
648
|
const cam = makeCamera(view, mesh, size, fit);
|
|
279
649
|
targets(Math.max(1, Math.round(size * dpr)));
|
|
280
|
-
|
|
281
|
-
|
|
650
|
+
const crease = smooth === true ? 40 : smooth || 0;
|
|
651
|
+
if (mesh !== uploaded || palette !== uploadedPalette || crease !== uploadedSmooth) {
|
|
652
|
+
uploadedSmooth = crease;
|
|
653
|
+
let all;
|
|
654
|
+
if (parts && parts.length) {
|
|
655
|
+
if (palette !== partPalette || crease !== partSmooth) {
|
|
656
|
+
partCache = new WeakMap();
|
|
657
|
+
partPalette = palette;
|
|
658
|
+
partSmooth = crease;
|
|
659
|
+
}
|
|
660
|
+
const built = parts.map((m) => {
|
|
661
|
+
let v = partCache.get(m);
|
|
662
|
+
if (!v)
|
|
663
|
+
partCache.set(m, (v = buildVertices(m, palette, { smooth: crease })));
|
|
664
|
+
return v;
|
|
665
|
+
});
|
|
666
|
+
all = new Float32Array(built.reduce((n, v) => n + v.length, 0));
|
|
667
|
+
let at = 0;
|
|
668
|
+
for (const v of built) {
|
|
669
|
+
all.set(v, at);
|
|
670
|
+
at += v.length;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else
|
|
674
|
+
all = buildVertices(mesh, palette, { smooth: crease });
|
|
282
675
|
// split: solid triangles are drawn first; triangles with alpha < 1 are kept apart and blended afterwards
|
|
283
676
|
const tri = 3 * FLOATS;
|
|
284
|
-
const solid =
|
|
677
|
+
const solid = new Float32Array(all.length);
|
|
678
|
+
let solidLen = 0;
|
|
285
679
|
glassTris = [];
|
|
286
680
|
for (let o = 0; o < all.length; o += tri) {
|
|
287
681
|
if (all[o + 11] < 1) {
|
|
288
682
|
const c = [0, 1, 2].map((k) => (all[o + k] + all[o + FLOATS + k] + all[o + 2 * FLOATS + k]) / 3);
|
|
289
683
|
glassTris.push({ c, at: o });
|
|
290
684
|
}
|
|
291
|
-
else
|
|
292
|
-
|
|
293
|
-
|
|
685
|
+
else {
|
|
686
|
+
solid.set(all.subarray(o, o + tri), solidLen);
|
|
687
|
+
solidLen += tri;
|
|
688
|
+
}
|
|
294
689
|
}
|
|
295
690
|
glassSource = all;
|
|
296
691
|
glassEye = '';
|
|
297
|
-
const verts =
|
|
692
|
+
const verts = solid.subarray(0, solidLen);
|
|
298
693
|
if (verts.byteLength > capacity) {
|
|
299
694
|
vertexBuffer?.destroy();
|
|
300
695
|
capacity = Math.max(verts.byteLength, 4096);
|
|
@@ -306,6 +701,22 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
306
701
|
uploaded = mesh;
|
|
307
702
|
uploadedPalette = palette;
|
|
308
703
|
}
|
|
704
|
+
if (!edges)
|
|
705
|
+
edgeCount = 0;
|
|
706
|
+
else if (mesh !== edgeMesh || parts !== edgeParts || !edgeOn) {
|
|
707
|
+
const e = sceneEdges(mesh, parts);
|
|
708
|
+
if (e.byteLength > edgeCapacity) {
|
|
709
|
+
edgeBuffer?.destroy();
|
|
710
|
+
edgeCapacity = Math.max(e.byteLength, 4096);
|
|
711
|
+
edgeBuffer = device.createBuffer({ size: edgeCapacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
712
|
+
}
|
|
713
|
+
if (e.byteLength)
|
|
714
|
+
device.queue.writeBuffer(edgeBuffer, 0, e);
|
|
715
|
+
edgeCount = e.length / 3;
|
|
716
|
+
edgeMesh = mesh;
|
|
717
|
+
edgeParts = parts;
|
|
718
|
+
}
|
|
719
|
+
edgeOn = edges;
|
|
309
720
|
if (instances?.mesh && instances.mesh !== protoMesh) {
|
|
310
721
|
const v = buildVertices(instances.mesh);
|
|
311
722
|
protoBuffer?.destroy();
|
|
@@ -346,15 +757,36 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
346
757
|
}
|
|
347
758
|
if (!glassTris.length)
|
|
348
759
|
glassCount = 0;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
760
|
+
// studio rig, relative to the camera: key upper left in front, fill from the right, rim from behind and above
|
|
761
|
+
const rig = (f, r, up) => norm([0, 1, 2].map((k) => -cam.f[k] * f + cam.r[k] * r + cam.u[k] * up));
|
|
762
|
+
const S = studio ? { fill: 0.4, rim: 1, ...(studio === true ? {} : studio) } : null;
|
|
763
|
+
const u = new Float32Array(52 + MAX_LIGHTS * 8);
|
|
764
|
+
u.set(viewProjection(cam, nearPlane(cam, mesh)), 0);
|
|
765
|
+
u.set([...(S ? rig(0.7, -0.5, 0.6) : norm(L.direction)), unlit ? 1 : 0], 16);
|
|
352
766
|
u.set([...L.sky, 0], 20);
|
|
353
767
|
u.set([...L.ground, 0], 24);
|
|
354
768
|
u.set([...cam.eye, 0], 28);
|
|
355
769
|
u.set([M.ambient, M.diffuse, M.specular, M.shininess], 32);
|
|
356
|
-
u.set([filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, 0], 36);
|
|
770
|
+
u.set([filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, S ? 1 : 0], 36);
|
|
771
|
+
u.set([...rig(0.6, 0.8, 0.1), S ? S.fill : 0], 40);
|
|
772
|
+
u.set([...rig(-0.8, 0.3, 0.5), S ? S.rim : 0], 44);
|
|
773
|
+
const lights = glow ? sceneLights(mesh, parts, palette) : new Float32Array(0);
|
|
774
|
+
u.set([px, px, px / size, lights.length / LIGHT_FLOATS], 48);
|
|
775
|
+
for (let k = 0; k < lights.length / LIGHT_FLOATS; k++) {
|
|
776
|
+
u.set(lights.subarray(k * LIGHT_FLOATS, k * LIGHT_FLOATS + 4), 52 + k * 4);
|
|
777
|
+
u.set([...lights.subarray(k * LIGHT_FLOATS + 4, k * LIGHT_FLOATS + 7), 0], 52 + MAX_LIGHTS * 4 + k * 4);
|
|
778
|
+
}
|
|
357
779
|
device.queue.writeBuffer(uniforms, 0, u);
|
|
780
|
+
const markData = marks?.length ? buildMarks(marks) : null;
|
|
781
|
+
markCount = markData ? markData.length / MARK_FLOATS : 0;
|
|
782
|
+
if (markData && markData.byteLength) {
|
|
783
|
+
if (markData.byteLength > markCapacity) {
|
|
784
|
+
markBuffer?.destroy();
|
|
785
|
+
markCapacity = Math.ceil(markData.byteLength * 1.5);
|
|
786
|
+
markBuffer = device.createBuffer({ size: markCapacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
787
|
+
}
|
|
788
|
+
device.queue.writeBuffer(markBuffer, 0, markData);
|
|
789
|
+
}
|
|
358
790
|
const encoder = device.createCommandEncoder();
|
|
359
791
|
const offscreen = readback
|
|
360
792
|
? device.createTexture({ size: [px, px], format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC })
|
|
@@ -385,6 +817,12 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
385
817
|
pass.setVertexBuffer(1, instBuffer);
|
|
386
818
|
pass.draw(protoCount, instCount);
|
|
387
819
|
}
|
|
820
|
+
if (edgeCount) {
|
|
821
|
+
pass.setPipeline(linePipeline);
|
|
822
|
+
pass.setBindGroup(0, bind);
|
|
823
|
+
pass.setVertexBuffer(0, edgeBuffer);
|
|
824
|
+
pass.draw(edgeCount);
|
|
825
|
+
}
|
|
388
826
|
if (glassCount) {
|
|
389
827
|
// after everything solid (and the instances): see-through faces, far to near
|
|
390
828
|
pass.setPipeline(glassPipeline);
|
|
@@ -392,6 +830,12 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
392
830
|
pass.setVertexBuffer(0, glassBuffer);
|
|
393
831
|
pass.draw(glassCount);
|
|
394
832
|
}
|
|
833
|
+
if (markCount) {
|
|
834
|
+
pass.setPipeline(markPipeline);
|
|
835
|
+
pass.setBindGroup(0, bind);
|
|
836
|
+
pass.setVertexBuffer(0, markBuffer);
|
|
837
|
+
pass.draw(6, markCount);
|
|
838
|
+
}
|
|
395
839
|
pass.end();
|
|
396
840
|
if (!offscreen) {
|
|
397
841
|
device.queue.submit([encoder.finish()]);
|
|
@@ -430,6 +874,7 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
430
874
|
const destroy = () => {
|
|
431
875
|
vertexBuffer?.destroy();
|
|
432
876
|
glassBuffer?.destroy();
|
|
877
|
+
markBuffer?.destroy();
|
|
433
878
|
protoBuffer?.destroy();
|
|
434
879
|
instBuffer?.destroy();
|
|
435
880
|
depth?.destroy();
|