@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A point skin on WebGPU: its points, joints and normals in storage buffers made once, and a pose
|
|
3
|
+
* recorded as one compute dispatch into the dynamic mesh's own position and normal buffers. What a
|
|
4
|
+
* point skin is, and why it poses into a mesh rather than in a vertex stage, is `pointSkin.ts`.
|
|
5
|
+
*
|
|
6
|
+
* **Each skin owns its joints buffer**, so two skins posed in one frame read their own: a write to
|
|
7
|
+
* one buffer between two dispatches would hand both the last, the per-draw rule `AGENTS.md` keeps
|
|
8
|
+
* for every resource a later write in the frame overwrites. A skin posed twice in a frame draws its
|
|
9
|
+
* last pose everywhere, as a mesh rewritten twice with `updateMesh` does.
|
|
10
|
+
*/
|
|
11
|
+
import { reachOf, type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
|
|
12
|
+
import type { GpuMesh } from './buffers.ts';
|
|
13
|
+
import { POINT_SKIN_WGSL, POINT_SKIN_WORKGROUP } from './shaders/pointSkin.wgsl.ts';
|
|
14
|
+
|
|
15
|
+
const STORAGE = 0x0080;
|
|
16
|
+
const UNIFORM = 0x0040;
|
|
17
|
+
const COPY_DST = 0x0008;
|
|
18
|
+
|
|
19
|
+
/** The pipeline every skin on a device poses with, made at the first skin. */
|
|
20
|
+
export function createPointSkinPipeline(device: GPUDevice): GPUComputePipeline {
|
|
21
|
+
return device.createComputePipeline({
|
|
22
|
+
label: 'pointSkin.pose',
|
|
23
|
+
layout: 'auto',
|
|
24
|
+
compute: {
|
|
25
|
+
module: device.createShaderModule({ label: 'pointSkin.pose', code: POINT_SKIN_WGSL }),
|
|
26
|
+
entryPoint: 'posePoints',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class GpuPointSkin {
|
|
32
|
+
readonly mesh: GpuMesh;
|
|
33
|
+
readonly reach: PointSkinReach;
|
|
34
|
+
private readonly count: number;
|
|
35
|
+
private readonly pipeline: GPUComputePipeline;
|
|
36
|
+
private readonly joints: GPUBuffer;
|
|
37
|
+
private readonly buffers: readonly GPUBuffer[];
|
|
38
|
+
private readonly group: GPUBindGroup;
|
|
39
|
+
|
|
40
|
+
constructor(device: GPUDevice, pipeline: GPUComputePipeline, mesh: GpuMesh, data: PointSkinData) {
|
|
41
|
+
this.mesh = mesh;
|
|
42
|
+
this.pipeline = pipeline;
|
|
43
|
+
this.reach = reachOf(data);
|
|
44
|
+
const count = data.weights.length / 4;
|
|
45
|
+
this.count = count;
|
|
46
|
+
|
|
47
|
+
/* Each influence a vec4: its point, and its weight in w. */
|
|
48
|
+
const points = new Float32Array(count * 16);
|
|
49
|
+
for (let i = 0; i < count * 4; i++) {
|
|
50
|
+
points.set(data.points.subarray(i * 3, i * 3 + 3), i * 4);
|
|
51
|
+
points[i * 4 + 3] = data.weights[i] as number;
|
|
52
|
+
}
|
|
53
|
+
const named = Uint32Array.from(data.joints);
|
|
54
|
+
const normals = new Float32Array(count * 4);
|
|
55
|
+
for (let v = 0; v < count; v++) normals.set(data.normals.subarray(v * 3, v * 3 + 3), v * 4);
|
|
56
|
+
const params = new Uint32Array([count, 0, 0, 0]);
|
|
57
|
+
|
|
58
|
+
const made = (
|
|
59
|
+
label: string,
|
|
60
|
+
contents: Float32Array | Uint32Array,
|
|
61
|
+
usage: number,
|
|
62
|
+
): GPUBuffer => {
|
|
63
|
+
const buffer = device.createBuffer({
|
|
64
|
+
label,
|
|
65
|
+
size: Math.max(16, contents.byteLength),
|
|
66
|
+
usage: usage | COPY_DST,
|
|
67
|
+
});
|
|
68
|
+
device.queue.writeBuffer(buffer, 0, contents);
|
|
69
|
+
return buffer;
|
|
70
|
+
};
|
|
71
|
+
this.joints = device.createBuffer({
|
|
72
|
+
label: 'pointSkin.joints',
|
|
73
|
+
size: Math.max(1, this.reach.joints) * 64,
|
|
74
|
+
usage: STORAGE | COPY_DST,
|
|
75
|
+
});
|
|
76
|
+
const buffers = [
|
|
77
|
+
made('pointSkin.params', params, UNIFORM),
|
|
78
|
+
made('pointSkin.points', points, STORAGE),
|
|
79
|
+
made('pointSkin.named', named, STORAGE),
|
|
80
|
+
made('pointSkin.normals', normals, STORAGE),
|
|
81
|
+
];
|
|
82
|
+
this.buffers = buffers;
|
|
83
|
+
const [positionsOut, normalsOut] = mesh.vertexBuffers as [GPUBuffer, GPUBuffer];
|
|
84
|
+
this.group = device.createBindGroup({
|
|
85
|
+
label: 'pointSkin.pose',
|
|
86
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
87
|
+
entries: [
|
|
88
|
+
{ binding: 0, resource: { buffer: buffers[0] as GPUBuffer } },
|
|
89
|
+
{ binding: 1, resource: { buffer: this.joints } },
|
|
90
|
+
{ binding: 2, resource: { buffer: buffers[1] as GPUBuffer } },
|
|
91
|
+
{ binding: 3, resource: { buffer: buffers[2] as GPUBuffer } },
|
|
92
|
+
{ binding: 4, resource: { buffer: buffers[3] as GPUBuffer } },
|
|
93
|
+
{ binding: 5, resource: { buffer: positionsOut } },
|
|
94
|
+
{ binding: 6, resource: { buffer: normalsOut } },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
|
|
100
|
+
pose(device: GPUDevice, encoder: GPUCommandEncoder, matrices: Float32Array): void {
|
|
101
|
+
if (this.reach.joints > 0) {
|
|
102
|
+
device.queue.writeBuffer(this.joints, 0, matrices, 0, this.reach.joints * 16);
|
|
103
|
+
}
|
|
104
|
+
const pass = encoder.beginComputePass({ label: 'pointSkin.pose' });
|
|
105
|
+
pass.setPipeline(this.pipeline);
|
|
106
|
+
pass.setBindGroup(0, this.group);
|
|
107
|
+
pass.dispatchWorkgroups(Math.ceil(this.count / POINT_SKIN_WORKGROUP));
|
|
108
|
+
pass.end();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose(): void {
|
|
112
|
+
this.joints.destroy();
|
|
113
|
+
for (const buffer of this.buffers) buffer.destroy();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -181,6 +181,14 @@ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.ts';
|
|
|
181
181
|
import { sceneCaptureTexels } from '../../sceneCapture.ts';
|
|
182
182
|
import { gpuCompressedFormats } from './compressedUpload.ts';
|
|
183
183
|
import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
|
|
184
|
+
import { GroupShelf } from './groupShelf.ts';
|
|
185
|
+
import { GpuPointSkin, createPointSkinPipeline } from './pointSkinPass.ts';
|
|
186
|
+
import {
|
|
187
|
+
poseBounds,
|
|
188
|
+
validatePointSkin,
|
|
189
|
+
validatePose,
|
|
190
|
+
type PointSkinData,
|
|
191
|
+
} from '../../pointSkin.ts';
|
|
184
192
|
import type { ClothViews } from './clothTextures.ts';
|
|
185
193
|
import type { ComputeHost } from './computeHost.ts';
|
|
186
194
|
import {
|
|
@@ -208,6 +216,15 @@ import {
|
|
|
208
216
|
packSurfaceLayers,
|
|
209
217
|
} from '../../surfaceLayers.ts';
|
|
210
218
|
import type { SurfaceLayers } from '../../surfaceLayers.ts';
|
|
219
|
+
import {
|
|
220
|
+
DETAIL_FLOATS,
|
|
221
|
+
detailOf,
|
|
222
|
+
detailRefusal,
|
|
223
|
+
detailRefusalWarning,
|
|
224
|
+
packSurfaceDetail,
|
|
225
|
+
type DetailRefusal,
|
|
226
|
+
type SurfaceDetail,
|
|
227
|
+
} from '../../surfaceDetail.ts';
|
|
211
228
|
import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
|
|
212
229
|
import {
|
|
213
230
|
frameReflectionsRefused,
|
|
@@ -1476,15 +1493,6 @@ interface SunTintTarget {
|
|
|
1476
1493
|
readonly mips: readonly [SnapshotMips, SnapshotMips];
|
|
1477
1494
|
}
|
|
1478
1495
|
|
|
1479
|
-
/** A skinned or morphed flat group, and what it was built with, compared by identity. */
|
|
1480
|
-
interface SkinnedTwin {
|
|
1481
|
-
readonly group: GPUBindGroup;
|
|
1482
|
-
readonly palette: GPUTextureView | null;
|
|
1483
|
-
readonly deltas: GPUTextureView | null;
|
|
1484
|
-
readonly cloth: GPUTextureView | null;
|
|
1485
|
-
readonly clothBinding: GPUTextureView | null;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1496
|
export class WebGPURenderer implements RendererApi {
|
|
1489
1497
|
/**
|
|
1490
1498
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -1602,9 +1610,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1602
1610
|
* thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
|
|
1603
1611
|
* for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
|
|
1604
1612
|
* are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
|
|
1605
|
-
* by.
|
|
1613
|
+
* by. **And by what each group was built with, on a shelf a material** (`groupShelf.ts`): a
|
|
1614
|
+
* garment's particles take turns between two textures and several meshes may follow one cloth,
|
|
1615
|
+
* so one group a material was made again draw after draw.
|
|
1606
1616
|
*/
|
|
1607
|
-
private readonly skinnedGroups: (Map<GPUBindGroup,
|
|
1617
|
+
private readonly skinnedGroups: (Map<GPUBindGroup, GroupShelf> | undefined)[] = [];
|
|
1608
1618
|
/** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
|
|
1609
1619
|
private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
|
|
1610
1620
|
private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
|
|
@@ -3037,6 +3047,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3037
3047
|
*/
|
|
3038
3048
|
private readonly transmission = new Float32Array([-1, -1, -1, 0]);
|
|
3039
3049
|
private warnedLayersModelMap = false;
|
|
3050
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
3051
|
+
private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
|
|
3052
|
+
/** The detail refusals already said, each once. See `detailOfMaterial`. */
|
|
3053
|
+
private readonly warnedDetail = new Set<DetailRefusal>();
|
|
3040
3054
|
private warnedLayersTriplanar = false;
|
|
3041
3055
|
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
3042
3056
|
private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
|
|
@@ -3515,7 +3529,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3515
3529
|
const ownModelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
|
|
3516
3530
|
/* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
|
|
3517
3531
|
const layers = this.layersOf(material);
|
|
3518
|
-
const
|
|
3532
|
+
const taken = ownModelMap ?? layerMaskMap(layers);
|
|
3533
|
+
/* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
|
|
3534
|
+
const detail = this.detailOfMaterial(material, taken);
|
|
3535
|
+
const modelMap = taken ?? detail?.maps ?? null;
|
|
3519
3536
|
this.materialModel = material?.model ?? null;
|
|
3520
3537
|
const reflectivity = material?.reflectivity;
|
|
3521
3538
|
const environmentGain = material?.environmentGain;
|
|
@@ -3608,6 +3625,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3608
3625
|
packLayerLooks(layers, this.lookFloats);
|
|
3609
3626
|
f.set(this.lookFloats, this.materialField('uLayerLooks'));
|
|
3610
3627
|
if (asksLayerLooks(layers)) this.enableLit('LAYER_LOOKS');
|
|
3628
|
+
/* And its tiled detail, the first time a material lays any: detail.ts. */
|
|
3629
|
+
packSurfaceDetail(detail, orm !== null, this.detailFloats);
|
|
3630
|
+
f.set(this.detailFloats, this.materialField('uDetail'));
|
|
3631
|
+
if (detail !== null) this.enableLit('SURFACE_DETAIL');
|
|
3611
3632
|
f.set(this.modelParams, this.materialField('uModelParams'));
|
|
3612
3633
|
});
|
|
3613
3634
|
/*
|
|
@@ -6157,17 +6178,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6157
6178
|
/** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
|
|
6158
6179
|
private readonly shadowSkinnedLayout: GPUBindGroupLayout;
|
|
6159
6180
|
/**
|
|
6160
|
-
* The skinned shadow bind groups,
|
|
6181
|
+
* The skinned shadow bind groups, a shelf per palette slot, each found by what it was built with.
|
|
6161
6182
|
*
|
|
6162
6183
|
* The same argument `flatBindGroupFor` makes, and per slot for the same reason: the group it was
|
|
6163
6184
|
* built beside, and the palette view, which now differs between two characters casting into one
|
|
6164
|
-
* bake
|
|
6185
|
+
* bake, and the cloth's particles and binding, which take turns. Comparing object identity makes
|
|
6186
|
+
* a missed invalidation impossible rather than unlikely. See `groupShelf.ts`.
|
|
6165
6187
|
*/
|
|
6166
|
-
private readonly
|
|
6167
|
-
private readonly shadowSkinnedGroupsBeside: (GPUBindGroup | null)[] = [];
|
|
6168
|
-
private readonly shadowSkinnedGroupsPalette: (GPUTextureView | null)[] = [];
|
|
6169
|
-
private readonly shadowSkinnedGroupsCloth: (GPUTextureView | null)[] = [];
|
|
6170
|
-
private readonly shadowSkinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
|
|
6188
|
+
private readonly shadowSkinnedShelves: (GroupShelf | undefined)[] = [];
|
|
6171
6189
|
/** Held from construction so the skinned group can be rebuilt without them being threaded. */
|
|
6172
6190
|
private shadowBlankView: GPUTextureView | null = null;
|
|
6173
6191
|
private shadowBlankSampler: GPUSampler | null = null;
|
|
@@ -6343,30 +6361,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6343
6361
|
*/
|
|
6344
6362
|
private shadowSkinnedGroup(palette: GPUTextureView, at: number, cloth: ClothViews): GPUBindGroup {
|
|
6345
6363
|
const beside = this.shadowBindGroup;
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
this.
|
|
6350
|
-
this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
|
|
6351
|
-
this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding
|
|
6352
|
-
) {
|
|
6353
|
-
this.shadowSkinnedGroups[at] = createDepthBindGroup(
|
|
6354
|
-
this.surface.device,
|
|
6355
|
-
this.shadowSkinnedLayout,
|
|
6356
|
-
this.shadowDraws.buffer,
|
|
6357
|
-
this.shadowPassUniforms as GPUBuffer,
|
|
6358
|
-
this.shadowBlankView as GPUTextureView,
|
|
6359
|
-
this.shadowBlankSampler as GPUSampler,
|
|
6360
|
-
palette,
|
|
6361
|
-
null,
|
|
6362
|
-
cloth,
|
|
6363
|
-
);
|
|
6364
|
-
this.shadowSkinnedGroupsBeside[at] = beside;
|
|
6365
|
-
this.shadowSkinnedGroupsPalette[at] = palette;
|
|
6366
|
-
this.shadowSkinnedGroupsCloth[at] = cloth.particles;
|
|
6367
|
-
this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
|
|
6364
|
+
let shelf = this.shadowSkinnedShelves[at];
|
|
6365
|
+
if (shelf === undefined) {
|
|
6366
|
+
shelf = new GroupShelf();
|
|
6367
|
+
this.shadowSkinnedShelves[at] = shelf;
|
|
6368
6368
|
}
|
|
6369
|
-
|
|
6369
|
+
const kept = shelf.find(beside, palette, cloth.particles, cloth.binding);
|
|
6370
|
+
if (kept !== null) return kept;
|
|
6371
|
+
const group = createDepthBindGroup(
|
|
6372
|
+
this.surface.device,
|
|
6373
|
+
this.shadowSkinnedLayout,
|
|
6374
|
+
this.shadowDraws.buffer,
|
|
6375
|
+
this.shadowPassUniforms as GPUBuffer,
|
|
6376
|
+
this.shadowBlankView as GPUTextureView,
|
|
6377
|
+
this.shadowBlankSampler as GPUSampler,
|
|
6378
|
+
palette,
|
|
6379
|
+
null,
|
|
6380
|
+
cloth,
|
|
6381
|
+
);
|
|
6382
|
+
shelf.keep(group, beside, palette, cloth.particles, cloth.binding);
|
|
6383
|
+
return group;
|
|
6370
6384
|
}
|
|
6371
6385
|
|
|
6372
6386
|
private scatterDepthGroup(): GPUBindGroup {
|
|
@@ -6401,6 +6415,25 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6401
6415
|
* A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
|
|
6402
6416
|
* said once, leave one layer.
|
|
6403
6417
|
*/
|
|
6418
|
+
/**
|
|
6419
|
+
* A material's detail, or null where it is refused: its slot holds another array, or it is placed
|
|
6420
|
+
* by the world. Each said once. This backend has room for its vectors wherever it runs.
|
|
6421
|
+
*/
|
|
6422
|
+
private detailOfMaterial(
|
|
6423
|
+
material: SurfaceMaterial<GpuSurfaceTexture> | null,
|
|
6424
|
+
taken: GpuSurfaceTexture | null,
|
|
6425
|
+
): SurfaceDetail<GpuSurfaceTexture> | null {
|
|
6426
|
+
const detail = detailOf(material);
|
|
6427
|
+
if (detail === null) return null;
|
|
6428
|
+
const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
|
|
6429
|
+
if (refusal === null) return detail;
|
|
6430
|
+
if (!this.warnedDetail.has(refusal)) {
|
|
6431
|
+
this.warnedDetail.add(refusal);
|
|
6432
|
+
console.warn(detailRefusalWarning(refusal));
|
|
6433
|
+
}
|
|
6434
|
+
return null;
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6404
6437
|
private layersOf(
|
|
6405
6438
|
material: SurfaceMaterial<GpuSurfaceTexture> | null,
|
|
6406
6439
|
): SurfaceLayers<GpuSurfaceTexture> | null {
|
|
@@ -8621,7 +8654,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8621
8654
|
* neither the pipeline set nor the key depends on whether the geometry lands in one call or
|
|
8622
8655
|
* over six frames.
|
|
8623
8656
|
*/
|
|
8624
|
-
private warmFlatPipelines(data: MeshData, dynamic: boolean): string {
|
|
8657
|
+
private warmFlatPipelines(data: MeshData, dynamic: boolean, dynamicColors = false): string {
|
|
8625
8658
|
/*
|
|
8626
8659
|
* **Every optional attribute, not the two that come to mind.** `present` decides where
|
|
8627
8660
|
* each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
|
|
@@ -8658,9 +8691,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8658
8691
|
}
|
|
8659
8692
|
/*
|
|
8660
8693
|
* And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
|
|
8661
|
-
* thirty-two, or not interleaved at all, moves every field after it, so the key says so.
|
|
8694
|
+
* thirty-two, or not interleaved at all, moves every field after it, so the key says so. A
|
|
8695
|
+
* `dynamicColors` mesh's colours are one of those, as `:colors~stream`.
|
|
8662
8696
|
*/
|
|
8663
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
8697
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
8664
8698
|
markPacking(present, packing);
|
|
8665
8699
|
key += packingKey(packing);
|
|
8666
8700
|
const fullKey = `${this.variant}|${key}`;
|
|
@@ -8777,8 +8811,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8777
8811
|
createMesh(given: MeshData, options: MeshOptions = {}): GpuMesh {
|
|
8778
8812
|
/* The streams it is drawn with, before anything records which it has. See `withLightmapUvs`. */
|
|
8779
8813
|
const data = withLightmapUvs(given);
|
|
8780
|
-
const
|
|
8781
|
-
|
|
8814
|
+
const dynamic = options.dynamic === true;
|
|
8815
|
+
const dynamicColors = options.dynamicColors === true;
|
|
8816
|
+
const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
|
|
8817
|
+
return this.keyed(createGpuMesh(this.surface.device, data, dynamic, dynamicColors), fullKey);
|
|
8782
8818
|
}
|
|
8783
8819
|
|
|
8784
8820
|
/**
|
|
@@ -8792,11 +8828,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8792
8828
|
*/
|
|
8793
8829
|
createMeshIncremental(given: MeshData, options: MeshOptions = {}): IncrementalMeshHandle {
|
|
8794
8830
|
const data = withLightmapUvs(given);
|
|
8795
|
-
const
|
|
8831
|
+
const dynamic = options.dynamic === true;
|
|
8832
|
+
const dynamicColors = options.dynamicColors === true;
|
|
8833
|
+
const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
|
|
8796
8834
|
const { mesh, upload } = createGpuMeshIncremental(
|
|
8797
8835
|
this.surface.device,
|
|
8798
8836
|
data,
|
|
8799
|
-
|
|
8837
|
+
dynamic,
|
|
8838
|
+
dynamicColors,
|
|
8800
8839
|
);
|
|
8801
8840
|
const surface = this.surface;
|
|
8802
8841
|
return {
|
|
@@ -8897,6 +8936,81 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8897
8936
|
mesh.update(this.surface.device, positions, normals, staged);
|
|
8898
8937
|
}
|
|
8899
8938
|
|
|
8939
|
+
/** The pipeline every point skin poses with, made at the first one. See `pointSkinPass.ts`. */
|
|
8940
|
+
private pointSkinPipeline: GPUComputePipeline | null = null;
|
|
8941
|
+
|
|
8942
|
+
/**
|
|
8943
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces. Refused in words
|
|
8944
|
+
* on a mesh that is not dynamic, whose positions share one buffer with the rest of each vertex.
|
|
8945
|
+
*/
|
|
8946
|
+
createPointSkin(mesh: GpuMesh, data: PointSkinData): GpuPointSkin {
|
|
8947
|
+
if (mesh.update === null) {
|
|
8948
|
+
throw new Error(
|
|
8949
|
+
'a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
|
|
8950
|
+
'have buffers of their own for the pose to write. Say so at `createMesh`.',
|
|
8951
|
+
);
|
|
8952
|
+
}
|
|
8953
|
+
validatePointSkin(data, mesh.vertexCount);
|
|
8954
|
+
const device = this.surface.device;
|
|
8955
|
+
this.pointSkinPipeline ??= createPointSkinPipeline(device);
|
|
8956
|
+
return new GpuPointSkin(device, this.pointSkinPipeline, mesh, data);
|
|
8957
|
+
}
|
|
8958
|
+
|
|
8959
|
+
/**
|
|
8960
|
+
* Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats,
|
|
8961
|
+
* column-major. Recorded into the encoder the frame's next submit leads with, behind a copy of
|
|
8962
|
+
* last frame's positions where a reconstruction keeps them, as `updateMesh` records its rewrite.
|
|
8963
|
+
*/
|
|
8964
|
+
posePointSkin(skin: GpuPointSkin, matrices: Float32Array): void {
|
|
8965
|
+
validatePose(matrices, skin.reach);
|
|
8966
|
+
if (this.surface.lost) return;
|
|
8967
|
+
const mesh = skin.mesh;
|
|
8968
|
+
poseBounds(matrices, skin.reach, mesh.bounds);
|
|
8969
|
+
const encoder = this.rewriteCopies();
|
|
8970
|
+
const motion = mesh.motion;
|
|
8971
|
+
if (motion !== null && this.reconstructionWanted) {
|
|
8972
|
+
const frame = frameOfChange(this.moverFrame, this.inFrame);
|
|
8973
|
+
if (keepsPrevious(motion.changed, frame)) {
|
|
8974
|
+
const size = mesh.vertexBuffers[0]?.size ?? 0;
|
|
8975
|
+
motion.previous ??= this.surface.device.createBuffer({
|
|
8976
|
+
label: 'recon.dynamicPrevious',
|
|
8977
|
+
size,
|
|
8978
|
+
usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
|
|
8979
|
+
});
|
|
8980
|
+
encoder.copyBufferToBuffer(mesh.vertexBuffers[0] as GPUBuffer, 0, motion.previous, 0, size);
|
|
8981
|
+
}
|
|
8982
|
+
motion.changed = frame;
|
|
8983
|
+
}
|
|
8984
|
+
skin.pose(this.surface.device, encoder, matrices);
|
|
8985
|
+
}
|
|
8986
|
+
|
|
8987
|
+
/**
|
|
8988
|
+
* **The pending rewrites go first**, since a pose recorded into them reads the skin's buffers and
|
|
8989
|
+
* a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
|
|
8990
|
+
* the rewrites are submitted, which only moves them earlier, the buffers can go at once.
|
|
8991
|
+
*/
|
|
8992
|
+
disposePointSkin(skin: GpuPointSkin): void {
|
|
8993
|
+
if (this.pendingRewrites !== null && !this.surface.lost) this.submit([]);
|
|
8994
|
+
skin.dispose();
|
|
8995
|
+
}
|
|
8996
|
+
|
|
8997
|
+
/**
|
|
8998
|
+
* Rewrite a mesh's colours. Refused in words on a mesh that did not ask for `dynamicColors`,
|
|
8999
|
+
* whose colours are interleaved with the rest of a vertex or are one value with no buffer at all.
|
|
9000
|
+
* See `GpuMesh.updateColors`.
|
|
9001
|
+
*/
|
|
9002
|
+
updateMeshColors(mesh: GpuMesh, colors: Float32Array): void {
|
|
9003
|
+
if (mesh.updateColors === null) {
|
|
9004
|
+
throw new Error(
|
|
9005
|
+
'this mesh was not created with { dynamicColors: true }, so its colours are packed with ' +
|
|
9006
|
+
'the rest of each vertex, or are one value with no buffer at all, and cannot be ' +
|
|
9007
|
+
'rewritten. Say so at `createMesh`: the flag gives the colours a buffer of their own.',
|
|
9008
|
+
);
|
|
9009
|
+
}
|
|
9010
|
+
if (this.surface.lost) return;
|
|
9011
|
+
mesh.updateColors(this.surface.device, colors);
|
|
9012
|
+
}
|
|
9013
|
+
|
|
8900
9014
|
/**
|
|
8901
9015
|
* Settle everything the frame shares: the camera, the lights and the fog.
|
|
8902
9016
|
*
|
|
@@ -9035,6 +9149,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9035
9149
|
/* Nor layers: one layer until a material asks for more. */
|
|
9036
9150
|
f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
|
|
9037
9151
|
f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
|
|
9152
|
+
/* Nor detail. */
|
|
9153
|
+
f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
|
|
9038
9154
|
f[at('uCutout')] = 0;
|
|
9039
9155
|
f[at('uCutout') + 1] = 0;
|
|
9040
9156
|
this.cutoutResolveStaged = 'hard';
|
|
@@ -12391,40 +12507,33 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12391
12507
|
byMaterial = new Map();
|
|
12392
12508
|
this.skinnedGroups[at] = byMaterial;
|
|
12393
12509
|
}
|
|
12394
|
-
let
|
|
12395
|
-
if (
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
)
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
deltas,
|
|
12422
|
-
cloth: cloth?.particles ?? null,
|
|
12423
|
-
clothBinding: cloth?.binding ?? null,
|
|
12424
|
-
};
|
|
12425
|
-
byMaterial.set(base, twin);
|
|
12426
|
-
}
|
|
12427
|
-
return twin.group;
|
|
12510
|
+
let shelf = byMaterial.get(base);
|
|
12511
|
+
if (shelf === undefined) {
|
|
12512
|
+
shelf = new GroupShelf();
|
|
12513
|
+
byMaterial.set(base, shelf);
|
|
12514
|
+
}
|
|
12515
|
+
const particles = cloth?.particles ?? null;
|
|
12516
|
+
const binding = cloth?.binding ?? null;
|
|
12517
|
+
const kept = shelf.find(palette, deltas, particles, binding);
|
|
12518
|
+
if (kept !== null) return kept;
|
|
12519
|
+
/* Built from the live material, with the overlay's atlas where `base` is its twin. */
|
|
12520
|
+
this.overlayBinding = base !== this.bindGroup;
|
|
12521
|
+
const group = createFlatBindGroup(
|
|
12522
|
+
this.surface.device,
|
|
12523
|
+
this.flatLayoutFor(skinned, morphed),
|
|
12524
|
+
this.variant,
|
|
12525
|
+
this.perDraw.buffer,
|
|
12526
|
+
this.passRing.buffer,
|
|
12527
|
+
this.materialStore.buffer,
|
|
12528
|
+
this.viewRing.buffer,
|
|
12529
|
+
this.flatTextures,
|
|
12530
|
+
palette,
|
|
12531
|
+
deltas,
|
|
12532
|
+
cloth,
|
|
12533
|
+
);
|
|
12534
|
+
this.overlayBinding = false;
|
|
12535
|
+
shelf.keep(group, palette, deltas, particles, binding);
|
|
12536
|
+
return group;
|
|
12428
12537
|
}
|
|
12429
12538
|
|
|
12430
12539
|
/** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
|
|
@@ -88,7 +88,8 @@ struct Pace {
|
|
|
88
88
|
colliderBase: u32,
|
|
89
89
|
colliders: u32,
|
|
90
90
|
width: u32,
|
|
91
|
-
|
|
91
|
+
/* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
|
|
92
|
+
pullKeep: f32,
|
|
92
93
|
keep: f32,
|
|
93
94
|
dragKeep: f32,
|
|
94
95
|
maxDistanceScale: f32,
|
|
@@ -428,7 +429,7 @@ fn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {
|
|
|
428
429
|
return centre + d * (radius / sqrt(d2));
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
|
|
432
|
+
/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
|
|
432
433
|
@compute @workgroup_size(64)
|
|
433
434
|
fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
434
435
|
let i = id.x;
|
|
@@ -444,6 +445,10 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
444
445
|
let at = stateAt(POSITION, i);
|
|
445
446
|
var p = load(at);
|
|
446
447
|
let t = skinnedAt(i, g);
|
|
448
|
+
/* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
|
|
449
|
+
if (paces[g].pullKeep < 1.0) {
|
|
450
|
+
p = t + (p - t) * paces[g].pullKeep;
|
|
451
|
+
}
|
|
447
452
|
/* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
|
|
448
453
|
let maxDistance = statics[n + i];
|
|
449
454
|
if ((limits & 1u) != 0u && maxDistance >= 0.0) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A point skin posed on the device, in WGSL, hand-authored under the compute exception `AGENTS.md`
|
|
3
|
+
* records (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl`
|
|
4
|
+
* neither writes nor polices this file. WebGL2 poses the same skin by transform feedback, in
|
|
5
|
+
* `backend/webgl2/pointSkinFeedback.ts`, by the same arithmetic in the same order.
|
|
6
|
+
*
|
|
7
|
+
* One invocation a vertex: its position the sum of each weighted point moved by its joint, an
|
|
8
|
+
* influence of weight 0 skipped, and its normal the first joint's turn of the normal it was given,
|
|
9
|
+
* made unit length where it has any. Written into the mesh's own position and normal buffers, three
|
|
10
|
+
* floats a vertex, which every pass then draws as it draws any dynamic mesh.
|
|
11
|
+
*/
|
|
12
|
+
export const POINT_SKIN_WORKGROUP = 64;
|
|
13
|
+
|
|
14
|
+
export const POINT_SKIN_WGSL = /* wgsl */ `
|
|
15
|
+
struct Params {
|
|
16
|
+
count: u32,
|
|
17
|
+
pad0: u32,
|
|
18
|
+
pad1: u32,
|
|
19
|
+
pad2: u32,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
23
|
+
@group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;
|
|
24
|
+
@group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;
|
|
25
|
+
@group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;
|
|
26
|
+
@group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;
|
|
27
|
+
@group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;
|
|
28
|
+
@group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;
|
|
29
|
+
|
|
30
|
+
@compute @workgroup_size(${POINT_SKIN_WORKGROUP})
|
|
31
|
+
fn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
32
|
+
let v = id.x;
|
|
33
|
+
if (v >= params.count) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let j = named[v];
|
|
37
|
+
var p = vec3<f32>(0.0, 0.0, 0.0);
|
|
38
|
+
for (var k = 0u; k < 4u; k = k + 1u) {
|
|
39
|
+
let q = points[v * 4u + k];
|
|
40
|
+
if (q.w != 0.0) {
|
|
41
|
+
p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let m = joints[j.x];
|
|
45
|
+
let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;
|
|
46
|
+
let l = length(raw);
|
|
47
|
+
var n = raw;
|
|
48
|
+
if (l > 0.0) {
|
|
49
|
+
n = raw / l;
|
|
50
|
+
}
|
|
51
|
+
posedPositions[v * 3u] = p.x;
|
|
52
|
+
posedPositions[v * 3u + 1u] = p.y;
|
|
53
|
+
posedPositions[v * 3u + 2u] = p.z;
|
|
54
|
+
posedNormals[v * 3u] = n.x;
|
|
55
|
+
posedNormals[v * 3u + 1u] = n.y;
|
|
56
|
+
posedNormals[v * 3u + 2u] = n.z;
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** The ways an attribute can travel other than as floats, as the key words name them. */
|
|
20
|
-
const PACKINGS = ['snorm16', 'unorm16', 'constant'] as const;
|
|
20
|
+
const PACKINGS = ['snorm16', 'unorm16', 'constant', 'stream'] as const;
|
|
21
21
|
|
|
22
22
|
/** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
|
|
23
23
|
function word(name: string, packing: AttributePacking): string {
|
|
@@ -287,6 +287,13 @@ export const BONE_PLACES_TEXTURE_UNIT = PARTICLE_DEPTH_UNIT + 1;
|
|
|
287
287
|
export const BONE_TURNS_TEXTURE_UNIT = BONE_PLACES_TEXTURE_UNIT + 1;
|
|
288
288
|
export const INSTANCE_CLOCKS_TEXTURE_UNIT = BONE_TURNS_TEXTURE_UNIT + 1;
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
|
|
292
|
+
* every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
|
|
293
|
+
* Twenty-nine.
|
|
294
|
+
*/
|
|
295
|
+
export const POINT_SKIN_TEXTURE_UNIT = INSTANCE_CLOCKS_TEXTURE_UNIT + 1;
|
|
296
|
+
|
|
290
297
|
/**
|
|
291
298
|
* A cookie's tile, in texels a side.
|
|
292
299
|
*
|