@driftengine/core 4.13.1 → 4.13.3
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/CHANGELOG.json +42 -0
- package/README.md +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/render/backend/api.d.ts +63 -8
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
- package/dist/render/backend/webgl2/renderer.d.ts +33 -0
- package/dist/render/backend/webgl2/renderer.js +88 -4
- package/dist/render/backend/webgpu/buffers.d.ts +10 -2
- package/dist/render/backend/webgpu/buffers.js +66 -16
- package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
- package/dist/render/backend/webgpu/clothTextures.js +14 -11
- package/dist/render/backend/webgpu/drawCommand.js +3 -3
- package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
- package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
- package/dist/render/backend/webgpu/groupShelf.js +54 -0
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
- package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
- package/dist/render/backend/webgpu/renderer.d.ts +40 -7
- package/dist/render/backend/webgpu/renderer.js +151 -48
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
- package/dist/render/backend/webgpu/vertexFormats.js +1 -1
- package/dist/render/lightBudget.d.ts +6 -0
- package/dist/render/lightBudget.js +6 -0
- package/dist/render/mesh.d.ts +19 -5
- package/dist/render/mesh.js +39 -8
- package/dist/render/pointSkin.d.ts +55 -0
- package/dist/render/pointSkin.js +121 -0
- package/dist/render/rendererHarness.js +6 -0
- package/dist/render/shader.d.ts +3 -1
- package/dist/render/shader.js +6 -1
- package/dist/render/shaders/flat/detail.d.ts +21 -0
- package/dist/render/shaders/flat/detail.js +74 -0
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +1 -0
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +3 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +2 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
- package/dist/render/shaders/generated/flat.wgsl.js +183 -55
- package/dist/render/surfaceDetail.d.ts +79 -0
- package/dist/render/surfaceDetail.js +86 -0
- package/dist/render/surfaceTexture.d.ts +9 -0
- package/dist/render/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +4 -0
- package/src/render/backend/api.ts +70 -7
- package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
- package/src/render/backend/webgl2/renderer.ts +121 -4
- package/src/render/backend/webgpu/buffers.ts +90 -16
- package/src/render/backend/webgpu/clothTextures.ts +14 -11
- package/src/render/backend/webgpu/drawCommand.ts +3 -3
- package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
- package/src/render/backend/webgpu/groupShelf.ts +74 -0
- package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
- package/src/render/backend/webgpu/renderer.ts +192 -83
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
- package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
- package/src/render/backend/webgpu/vertexFormats.ts +1 -1
- package/src/render/lightBudget.ts +7 -0
- package/src/render/mesh.ts +49 -7
- package/src/render/pointSkin.ts +178 -0
- package/src/render/rendererHarness.ts +6 -0
- package/src/render/shader.ts +5 -0
- package/src/render/shaders/flat/detail.ts +74 -0
- package/src/render/shaders/flat/glassTint.ts +3 -1
- package/src/render/shaders/flat/index.ts +8 -0
- package/src/render/shaders/flat/main.ts +2 -0
- package/src/render/shaders/generated/flat.wgsl.ts +183 -55
- package/src/render/surfaceDetail.ts +136 -0
- package/src/render/surfaceTexture.ts +9 -0
- package/src/render/vertexPacking.ts +25 -5
package/src/render/mesh.ts
CHANGED
|
@@ -115,6 +115,12 @@ export class Mesh {
|
|
|
115
115
|
* this — two references, and only where a consumer asked.
|
|
116
116
|
*/
|
|
117
117
|
private readonly deformable: { positions: WebGLBuffer; normals: WebGLBuffer } | null = null;
|
|
118
|
+
/** A dynamic mesh's position and normal buffers, for a pose written on the device; else null. */
|
|
119
|
+
get streams(): { readonly positions: WebGLBuffer; readonly normals: WebGLBuffer } | null {
|
|
120
|
+
return this.deformable;
|
|
121
|
+
}
|
|
122
|
+
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
123
|
+
private readonly recolourable: WebGLBuffer | null = null;
|
|
118
124
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
119
125
|
private readonly positionFloats: number;
|
|
120
126
|
/**
|
|
@@ -222,8 +228,15 @@ export class Mesh {
|
|
|
222
228
|
* and the default is the fast path: filling a buffer with `bufferData` in one call skips the
|
|
223
229
|
* zero-fill that `bufferData(size)` then `bufferSubData` pays for, and every mesh in the
|
|
224
230
|
* engine that does not need to be spread should keep skipping it.
|
|
231
|
+
* @param dynamicColors Colours as floats in a buffer kept for `updateColors`.
|
|
225
232
|
*/
|
|
226
|
-
constructor(
|
|
233
|
+
constructor(
|
|
234
|
+
gl: WebGL2RenderingContext,
|
|
235
|
+
data: MeshData,
|
|
236
|
+
dynamic = false,
|
|
237
|
+
spread = false,
|
|
238
|
+
dynamicColors = false,
|
|
239
|
+
) {
|
|
227
240
|
validateMeshData(data);
|
|
228
241
|
this.spread = spread;
|
|
229
242
|
this.hasUvs = data.uvs !== undefined;
|
|
@@ -243,11 +256,15 @@ export class Mesh {
|
|
|
243
256
|
*/
|
|
244
257
|
const usage = dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
|
|
245
258
|
/* How each attribute travels: sixteen-bit fixed point, or one value as a constant. */
|
|
246
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
259
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
247
260
|
const positions = this.attachAttribute(gl, ATTR_POSITION, data.positions, 3, usage);
|
|
248
261
|
const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage, packing.normals);
|
|
249
262
|
if (dynamic) this.deformable = { positions, normals };
|
|
250
|
-
|
|
263
|
+
if (packing.colors === 'stream') {
|
|
264
|
+
this.recolourable = this.attachAttribute(gl, ATTR_COLOR, data.colors, 3, gl.DYNAMIC_DRAW);
|
|
265
|
+
} else {
|
|
266
|
+
this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
|
|
267
|
+
}
|
|
251
268
|
this.attachShared(gl, ATTR_EMISSIVE, data.emissive, 1, packing.emissive);
|
|
252
269
|
/*
|
|
253
270
|
* Absent means "not shiny", supplied as a constant rather than as a buffer of
|
|
@@ -463,9 +480,9 @@ export class Mesh {
|
|
|
463
480
|
/**
|
|
464
481
|
* Rewrite this mesh's positions, and its normals where the caller has them.
|
|
465
482
|
*
|
|
466
|
-
* **Only the two attributes that move.**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
483
|
+
* **Only the two attributes that move.** Emissive and the rest describe what a surface *is* and
|
|
484
|
+
* do not change because it bent; rewriting them would be uploading unchanged bytes every frame.
|
|
485
|
+
* A caller whose colours change has `updateColors`, on a mesh made `dynamicColors`.
|
|
469
486
|
*
|
|
470
487
|
* **The bounds are recomputed**, because everything that decides whether this is on screen
|
|
471
488
|
* starts from them — a cloth that blew sideways out of its original box would be culled while
|
|
@@ -495,6 +512,30 @@ export class Mesh {
|
|
|
495
512
|
}
|
|
496
513
|
}
|
|
497
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Rewrite this mesh's colours, three floats a vertex, into the buffer they have to themselves.
|
|
517
|
+
*
|
|
518
|
+
* Nothing else moves, the bounds included: a colour does not change where a vertex is.
|
|
519
|
+
*/
|
|
520
|
+
updateColors(gl: WebGL2RenderingContext, colors: Float32Array): void {
|
|
521
|
+
if (this.recolourable === null) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
'this mesh was not created with { dynamicColors: true }, so its colours are packed or are ' +
|
|
524
|
+
'one value with no buffer at all, and cannot be rewritten. Say so at `createMesh`: the ' +
|
|
525
|
+
'flag gives the colours a buffer of their own.',
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (colors.length !== this.positionFloats) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`this mesh has ${this.positionFloats / 3} vertices and the colours have ` +
|
|
531
|
+
`${colors.length / 3}. A mesh's vertex count is fixed at creation: the index buffer, ` +
|
|
532
|
+
'the pipeline and every other attribute are sized against it.',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.recolourable);
|
|
536
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);
|
|
537
|
+
}
|
|
538
|
+
|
|
498
539
|
/**
|
|
499
540
|
* One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
|
|
500
541
|
* WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
|
|
@@ -638,8 +679,9 @@ export function createMeshIncremental(
|
|
|
638
679
|
gl: WebGL2RenderingContext,
|
|
639
680
|
data: MeshData,
|
|
640
681
|
dynamic = false,
|
|
682
|
+
dynamicColors = false,
|
|
641
683
|
): IncrementalMesh {
|
|
642
|
-
const mesh = new Mesh(gl, data, dynamic, true);
|
|
684
|
+
const mesh = new Mesh(gl, data, dynamic, true, dynamicColors);
|
|
643
685
|
return { mesh, upload: mesh.uploads(gl) };
|
|
644
686
|
}
|
|
645
687
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mesh posed on the device from weighted points in its joints' own spaces: the data, its checks,
|
|
3
|
+
* and the bounds a pose is culled by. Each backend's binding poses the vertices.
|
|
4
|
+
*
|
|
5
|
+
* **What it is for.** Some formats store a vertex as up to four points, each in the space of a
|
|
6
|
+
* joint, with a weight each and a normal in the space of the first: posed, the vertex is the
|
|
7
|
+
* weighted sum of each point moved by its joint. That is not ordinary skinning, which moves one
|
|
8
|
+
* bind-pose position by a blend of joints, so a rig of that kind had to be posed on the main thread
|
|
9
|
+
* and its positions uploaded every frame. Reported at 38% of the main thread's drawing time.
|
|
10
|
+
* Posed on the device into a dynamic mesh's own buffers, the main thread hands over the joints.
|
|
11
|
+
*
|
|
12
|
+
* **Into a dynamic mesh rather than in the vertex stage**, because a skinned vertex stage has no
|
|
13
|
+
* attribute left for four more points, and every pass that draws a mesh would need a variant that
|
|
14
|
+
* reads them: lit, cast, glass, motion. Posed once into the mesh's positions and normals, it is an
|
|
15
|
+
* ordinary dynamic mesh to every one of them, motion vectors included. What it costs is the mesh's
|
|
16
|
+
* two buffers, written a pose, and a pass that runs before the draws.
|
|
17
|
+
*
|
|
18
|
+
* **The bounds are a box of spheres, not the posed vertices**, which the CPU never sees. Each joint
|
|
19
|
+
* reaches as far as the farthest point a weighted influence holds in its space, scaled by its
|
|
20
|
+
* largest axis; a weighted sum of points inside those spheres lies inside their box, scaled by the
|
|
21
|
+
* least and greatest sums of a vertex's weights. Conservative, so it culls nothing visible; loose
|
|
22
|
+
* by the joints' reach where a mesh is long and thin, which culls a little less than it could.
|
|
23
|
+
*/
|
|
24
|
+
import type { Bounds } from '../math/bounds.ts';
|
|
25
|
+
import { MAX_JOINTS } from './skinPalette.ts';
|
|
26
|
+
|
|
27
|
+
/** A point skin, one entry a vertex unless said otherwise. */
|
|
28
|
+
export interface PointSkinData {
|
|
29
|
+
/** Four joint indices a vertex, as floats, as `MeshData.joints` is. */
|
|
30
|
+
readonly joints: Float32Array;
|
|
31
|
+
/** Four weights a vertex. An influence of weight 0 is skipped, whatever its joint and point. */
|
|
32
|
+
readonly weights: Float32Array;
|
|
33
|
+
/** Four points a vertex, three floats each: influence `k`'s point in joint `k`'s own space. */
|
|
34
|
+
readonly points: Float32Array;
|
|
35
|
+
/** One normal a vertex, in the space of its first joint. */
|
|
36
|
+
readonly normals: Float32Array;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How far a skin reaches from each joint, and what its weights sum to: what a pose's bounds need. */
|
|
40
|
+
export interface PointSkinReach {
|
|
41
|
+
/** Joints a pose must hand over: one past the highest any vertex names and uses. */
|
|
42
|
+
readonly joints: number;
|
|
43
|
+
/** For each joint, the farthest point a weighted influence holds in its space; -1 where none. */
|
|
44
|
+
readonly reach: Float32Array;
|
|
45
|
+
/** The least and the greatest sum of one vertex's weights. */
|
|
46
|
+
readonly sums: readonly [number, number];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
|
|
50
|
+
export function validatePointSkin(data: PointSkinData, vertices: number): void {
|
|
51
|
+
const fail = (what: string): never => {
|
|
52
|
+
throw new Error(`point skin: ${what}`);
|
|
53
|
+
};
|
|
54
|
+
for (const [name, values, width] of [
|
|
55
|
+
['joints', data.joints, 4],
|
|
56
|
+
['weights', data.weights, 4],
|
|
57
|
+
['points', data.points, 12],
|
|
58
|
+
['normals', data.normals, 3],
|
|
59
|
+
] as const) {
|
|
60
|
+
if (values.length !== vertices * width) {
|
|
61
|
+
fail(`${name} holds ${values.length / width} vertices and the mesh has ${vertices}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (let at = 0; at < data.joints.length; at++) {
|
|
65
|
+
const joint = data.joints[at] as number;
|
|
66
|
+
if (!(Number.isInteger(joint) && joint >= 0 && joint < MAX_JOINTS)) {
|
|
67
|
+
fail(`vertex ${Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ${MAX_JOINTS - 1}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Measure a skin's reach once, at creation. */
|
|
73
|
+
export function reachOf(data: PointSkinData): PointSkinReach {
|
|
74
|
+
const reach = new Float32Array(MAX_JOINTS).fill(-1);
|
|
75
|
+
let joints = 0;
|
|
76
|
+
let least = Infinity;
|
|
77
|
+
let most = -Infinity;
|
|
78
|
+
const vertices = data.weights.length / 4;
|
|
79
|
+
for (let v = 0; v < vertices; v++) {
|
|
80
|
+
/* The first joint carries the normal, so it is named whatever its weight. */
|
|
81
|
+
joints = Math.max(joints, (data.joints[v * 4] as number) + 1);
|
|
82
|
+
let sum = 0;
|
|
83
|
+
for (let k = 0; k < 4; k++) {
|
|
84
|
+
const weight = data.weights[v * 4 + k] as number;
|
|
85
|
+
if (weight === 0) continue;
|
|
86
|
+
sum += weight;
|
|
87
|
+
const joint = data.joints[v * 4 + k] as number;
|
|
88
|
+
joints = Math.max(joints, joint + 1);
|
|
89
|
+
const at = (v * 4 + k) * 3;
|
|
90
|
+
const x = data.points[at] as number;
|
|
91
|
+
const y = data.points[at + 1] as number;
|
|
92
|
+
const z = data.points[at + 2] as number;
|
|
93
|
+
reach[joint] = Math.max(reach[joint] as number, Math.sqrt(x * x + y * y + z * z));
|
|
94
|
+
}
|
|
95
|
+
least = Math.min(least, sum);
|
|
96
|
+
most = Math.max(most, sum);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
joints,
|
|
100
|
+
reach: reach.slice(0, joints),
|
|
101
|
+
sums: vertices === 0 ? [1, 1] : [least, most],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
|
|
107
|
+
* mesh's model space, sixteen floats, column-major. Allocates nothing.
|
|
108
|
+
*/
|
|
109
|
+
export function poseBounds(matrices: Float32Array, skin: PointSkinReach, out: Bounds): void {
|
|
110
|
+
let minX = Infinity;
|
|
111
|
+
let minY = Infinity;
|
|
112
|
+
let minZ = Infinity;
|
|
113
|
+
let maxX = -Infinity;
|
|
114
|
+
let maxY = -Infinity;
|
|
115
|
+
let maxZ = -Infinity;
|
|
116
|
+
for (let j = 0; j < skin.joints; j++) {
|
|
117
|
+
const reach = skin.reach[j] as number;
|
|
118
|
+
if (reach < 0) continue;
|
|
119
|
+
const m = j * 16;
|
|
120
|
+
const scale = Math.sqrt(
|
|
121
|
+
Math.max(
|
|
122
|
+
columnLength2(matrices, m),
|
|
123
|
+
columnLength2(matrices, m + 4),
|
|
124
|
+
columnLength2(matrices, m + 8),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
const r = reach * scale;
|
|
128
|
+
const x = matrices[m + 12] as number;
|
|
129
|
+
const y = matrices[m + 13] as number;
|
|
130
|
+
const z = matrices[m + 14] as number;
|
|
131
|
+
minX = Math.min(minX, x - r);
|
|
132
|
+
minY = Math.min(minY, y - r);
|
|
133
|
+
minZ = Math.min(minZ, z - r);
|
|
134
|
+
maxX = Math.max(maxX, x + r);
|
|
135
|
+
maxY = Math.max(maxY, y + r);
|
|
136
|
+
maxZ = Math.max(maxZ, z + r);
|
|
137
|
+
}
|
|
138
|
+
if (minX > maxX) {
|
|
139
|
+
out.min.fill(0);
|
|
140
|
+
out.max.fill(0);
|
|
141
|
+
out.centre.fill(0);
|
|
142
|
+
out.radius = 0;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
/* A sum of weights other than one scales the box about the origin, by the least and the most. */
|
|
146
|
+
const [least, most] = skin.sums;
|
|
147
|
+
out.min[0] = Math.min(least * minX, most * minX);
|
|
148
|
+
out.min[1] = Math.min(least * minY, most * minY);
|
|
149
|
+
out.min[2] = Math.min(least * minZ, most * minZ);
|
|
150
|
+
out.max[0] = Math.max(least * maxX, most * maxX);
|
|
151
|
+
out.max[1] = Math.max(least * maxY, most * maxY);
|
|
152
|
+
out.max[2] = Math.max(least * maxZ, most * maxZ);
|
|
153
|
+
let radius2 = 0;
|
|
154
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
155
|
+
const centre = ((out.min[axis] as number) + (out.max[axis] as number)) / 2;
|
|
156
|
+
out.centre[axis] = centre;
|
|
157
|
+
const half = (out.max[axis] as number) - centre;
|
|
158
|
+
radius2 += half * half;
|
|
159
|
+
}
|
|
160
|
+
out.radius = Math.sqrt(radius2);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The squared length of one column's first three components. */
|
|
164
|
+
function columnLength2(m: Float32Array, at: number): number {
|
|
165
|
+
const x = m[at] as number;
|
|
166
|
+
const y = m[at + 1] as number;
|
|
167
|
+
const z = m[at + 2] as number;
|
|
168
|
+
return x * x + y * y + z * z;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Refuse a pose that hands over fewer joints than the skin names. */
|
|
172
|
+
export function validatePose(matrices: Float32Array, skin: PointSkinReach): void {
|
|
173
|
+
if (matrices.length % 16 !== 0 || matrices.length / 16 < skin.joints) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`point skin: a pose of ${matrices.length / 16} joints for a skin that names ${skin.joints}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -125,6 +125,12 @@ export function recordingGl(
|
|
|
125
125
|
ONE: 1,
|
|
126
126
|
ONE_MINUS_SRC_ALPHA: 0x0303,
|
|
127
127
|
TRIANGLES: 0x0004,
|
|
128
|
+
/* Transform feedback's, so a point skin's pose can be seen to discard, capture and undo both. */
|
|
129
|
+
POINTS: 0x0000,
|
|
130
|
+
RASTERIZER_DISCARD: 0x8c89,
|
|
131
|
+
TRANSFORM_FEEDBACK: 0x8e22,
|
|
132
|
+
TRANSFORM_FEEDBACK_BUFFER: 0x8c8e,
|
|
133
|
+
SEPARATE_ATTRIBS: 0x8c8d,
|
|
128
134
|
/* The winding a pass names, so a test can tell which one it chose. */
|
|
129
135
|
CW: 0x0900,
|
|
130
136
|
CCW: 0x0901,
|
package/src/render/shader.ts
CHANGED
|
@@ -5,6 +5,8 @@ export function compileProgram(
|
|
|
5
5
|
vertexSource: string,
|
|
6
6
|
fragmentSource: string,
|
|
7
7
|
label: string,
|
|
8
|
+
/** Vertex outputs captured by transform feedback, one buffer each; named before the link. */
|
|
9
|
+
feedback?: readonly string[],
|
|
8
10
|
): WebGLProgram {
|
|
9
11
|
const vs = compileStage(gl, gl.VERTEX_SHADER, vertexSource, `${label}.vert`);
|
|
10
12
|
const fs = compileStage(gl, gl.FRAGMENT_SHADER, fragmentSource, `${label}.frag`);
|
|
@@ -14,6 +16,9 @@ export function compileProgram(
|
|
|
14
16
|
|
|
15
17
|
gl.attachShader(program, vs);
|
|
16
18
|
gl.attachShader(program, fs);
|
|
19
|
+
if (feedback !== undefined) {
|
|
20
|
+
gl.transformFeedbackVaryings(program, feedback as string[], gl.SEPARATE_ATTRIBS);
|
|
21
|
+
}
|
|
17
22
|
gl.linkProgram(program);
|
|
18
23
|
gl.deleteShader(vs);
|
|
19
24
|
gl.deleteShader(fs);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's tiled detail over whatever its maps made: `SurfaceMaterial.detail`, packed into
|
|
3
|
+
* `uDetail` by `surfaceDetail.ts`.
|
|
4
|
+
*
|
|
5
|
+
* **Each texel wears its own region's detail alone.** The region is read exactly from the ORM
|
|
6
|
+
* array's layer the material names, at the base coordinates, so an index is never blended with its
|
|
7
|
+
* neighbour's at a region's edge; then that region's normal map is read at its repeat and offset,
|
|
8
|
+
* turned through a frame built on the normal the base maps made — the construction a layered
|
|
9
|
+
* material lays its layers over a mesh normal with — and its occlusion multiplied into the
|
|
10
|
+
* surface's. A strength scales the detail's slopes as a layer's look does.
|
|
11
|
+
*
|
|
12
|
+
* **Read with explicit gradients**, the base coordinates' times the region's repeat, because the
|
|
13
|
+
* region is the pixel's own: a read whose level is implicit is legal only in uniform control flow
|
|
14
|
+
* (AGENTS.md 2026-08-07), and across a region's edge the repeated coordinates jump, which would
|
|
15
|
+
* read a level as coarse as the jump. The frame and the gradients are taken first, under the
|
|
16
|
+
* uniform branch that calls this.
|
|
17
|
+
*
|
|
18
|
+
* **A lit switch, `SURFACE_DETAIL`**, off until a material first asks, declared after `LAYERED` and
|
|
19
|
+
* `LAYER_LOOKS` so each keeps its id. Compiled out, its thirty-three vectors are not declared.
|
|
20
|
+
*/
|
|
21
|
+
export function detailGlsl(on: boolean): string {
|
|
22
|
+
if (!on) {
|
|
23
|
+
return /* glsl */ `
|
|
24
|
+
const bool SURFACE_DETAIL = false; // wgsl:override
|
|
25
|
+
bool detailed() { return false; }
|
|
26
|
+
void surfaceDetail(inout vec3 n, inout float occlusion, vec2 uv) {}
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
return /* glsl */ `
|
|
30
|
+
/* Whether this program lays a material's detail. Off but where one has asked: detail.ts. */
|
|
31
|
+
const bool SURFACE_DETAIL = true; // wgsl:override
|
|
32
|
+
/*
|
|
33
|
+
* Thirty-three vectors, as surfaceDetail.ts packs them: the region count (x) and the ORM array's
|
|
34
|
+
* layer holding the region map, or -1 for none (y); then two a region, the normal map's layer (x),
|
|
35
|
+
* the occlusion's or -1 (y), the normal strength (z) and the occlusion strength (w); its repeats
|
|
36
|
+
* across and down (x y) and its offset (z w).
|
|
37
|
+
*/
|
|
38
|
+
uniform vec4 uDetail[33]; // wgsl:material
|
|
39
|
+
|
|
40
|
+
bool detailed() { return uDetail[0].x > 0.0; }
|
|
41
|
+
|
|
42
|
+
/* The region a texel of the base maps is in, read exactly, held to the regions there are. */
|
|
43
|
+
int detailRegion(vec2 uv) {
|
|
44
|
+
int last = int(uDetail[0].x) - 1;
|
|
45
|
+
if (uDetail[0].y < 0.0) return 0;
|
|
46
|
+
ivec2 size = textureSize(uOrmMap, 0).xy;
|
|
47
|
+
ivec2 texel = clamp(ivec2(floor(fract(uv) * vec2(size))), ivec2(0), size - 1);
|
|
48
|
+
int region = int(texelFetch(uOrmMap, ivec3(texel, int(uDetail[0].y)), 0).r * 255.0 + 0.5);
|
|
49
|
+
return clamp(region, 0, last);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void surfaceDetail(inout vec3 n, inout float occlusion, vec2 uv) {
|
|
53
|
+
/* In uniform control flow, before anything that depends on the pixel's region. */
|
|
54
|
+
mat3 frame = tangentFrame(n, vWorldPos, uv, vTangent, vHasTangents);
|
|
55
|
+
vec2 dx = dFdx(uv);
|
|
56
|
+
vec2 dy = dFdy(uv);
|
|
57
|
+
int region = detailRegion(uv);
|
|
58
|
+
vec4 picks = uDetail[1 + region * 2];
|
|
59
|
+
vec4 place = uDetail[2 + region * 2];
|
|
60
|
+
vec2 at = uv * place.xy + place.zw;
|
|
61
|
+
vec2 gx = dx * place.xy;
|
|
62
|
+
vec2 gy = dy * place.xy;
|
|
63
|
+
vec3 d = textureGrad(uModelMap, vec3(at, picks.x), gx, gy).xyz * 2.0 - 1.0;
|
|
64
|
+
/* z rebuilt where none is stored, as the base map's is: normalMap.ts. */
|
|
65
|
+
d.z = d.z > 0.0 ? d.z : sqrt(max(0.0, 1.0 - dot(d.xy, d.xy)));
|
|
66
|
+
d = normalize(vec3(d.xy * picks.z, max(d.z, 1e-4)));
|
|
67
|
+
n = normalize(frame * d);
|
|
68
|
+
if (picks.y >= 0.0) {
|
|
69
|
+
float held = textureGrad(uModelMap, vec3(at, picks.y), gx, gy).r;
|
|
70
|
+
occlusion *= mix(1.0, held, picks.w);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
@@ -40,7 +40,8 @@ export type LitSwitch =
|
|
|
40
40
|
| 'WORLD_UVS'
|
|
41
41
|
| 'MOVING_SUN'
|
|
42
42
|
| 'LAYERED'
|
|
43
|
-
| 'LAYER_LOOKS'
|
|
43
|
+
| 'LAYER_LOOKS'
|
|
44
|
+
| 'SURFACE_DETAIL';
|
|
44
45
|
|
|
45
46
|
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
46
47
|
export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
@@ -57,6 +58,7 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
|
57
58
|
MOVING_SUN: false,
|
|
58
59
|
LAYERED: false,
|
|
59
60
|
LAYER_LOOKS: false,
|
|
61
|
+
SURFACE_DETAIL: false,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -24,6 +24,7 @@ import { overlayGlsl } from './overlay.ts';
|
|
|
24
24
|
import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
|
|
25
25
|
import { worldUvGlsl } from './worldUv.ts';
|
|
26
26
|
import { movingSunGlsl } from './movingSun.ts';
|
|
27
|
+
import { detailGlsl } from './detail.ts';
|
|
27
28
|
import { layeredGlsl } from './layered.ts';
|
|
28
29
|
import { MAP_READS_GLSL } from './mapReads.ts';
|
|
29
30
|
import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.ts';
|
|
@@ -539,6 +540,11 @@ export interface FlatShaderOptions {
|
|
|
539
540
|
* \`uLayerLooks\` and all. True when absent; nothing without \`layered\`. See \`layerLooksGlsl\`.
|
|
540
541
|
*/
|
|
541
542
|
readonly layerLooks?: boolean;
|
|
543
|
+
/**
|
|
544
|
+
* Whether it lays a material's tiled detail over its maps, \`uDetail\` and all
|
|
545
|
+
* (\`SurfaceMaterial.detail\`). True when absent. See \`detailGlsl\`.
|
|
546
|
+
*/
|
|
547
|
+
readonly surfaceDetail?: boolean;
|
|
542
548
|
/**
|
|
543
549
|
* How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
|
|
544
550
|
*
|
|
@@ -692,6 +698,8 @@ export function flatFrag(options: FlatShaderOptions): string {
|
|
|
692
698
|
worldUvGlsl(options.worldUvs ?? true),
|
|
693
699
|
/* After the projection, so its switch takes the next id; the normal map below calls it. */
|
|
694
700
|
layeredGlsl(options.layered ?? true, options.layerLooks ?? true),
|
|
701
|
+
/* After the layers, so its switch takes the next id; main calls it. See detail.ts. */
|
|
702
|
+
detailGlsl(options.surfaceDetail ?? true),
|
|
695
703
|
/* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
|
|
696
704
|
MAP_READS_GLSL,
|
|
697
705
|
/* After the frame and the projection it calls, unconditional for the same reason as the
|
|
@@ -227,6 +227,8 @@ export const MAIN_GLSL = `void main() {
|
|
|
227
227
|
if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt, frameAt, frameTangents);
|
|
228
228
|
/* A draw's wrinkles, by region, over whatever the map made: overlay.ts. */
|
|
229
229
|
if (SURFACE_OVERLAY) n = overlayWrinkle(n, unmapped, surfaceAt.xy);
|
|
230
|
+
/* The material's tiled detail over whatever the maps made, and its occlusion: detail.ts. */
|
|
231
|
+
if (SURFACE_DETAIL && detailed()) surfaceDetail(n, ormOcclusion, surfaceAt.xy);
|
|
230
232
|
|
|
231
233
|
/*
|
|
232
234
|
* Microscopic relief: the surface's own texture, as a turn of the normal rather than a change
|