@driftengine/core 4.13.0 → 4.13.2
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 +2 -1
- 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 +21 -0
- package/dist/render/backend/webgl2/renderer.js +41 -2
- 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 +31 -7
- package/dist/render/backend/webgpu/renderer.js +118 -47
- 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/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +2 -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 +63 -2
- 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 +150 -82
- 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/vertexPacking.ts +25 -5
|
@@ -60,6 +60,9 @@ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.js';
|
|
|
60
60
|
import { sceneCaptureTexels } from '../../sceneCapture.js';
|
|
61
61
|
import { gpuCompressedFormats } from './compressedUpload.js';
|
|
62
62
|
import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
|
|
63
|
+
import { GroupShelf } from './groupShelf.js';
|
|
64
|
+
import { GpuPointSkin, createPointSkinPipeline } from './pointSkinPass.js';
|
|
65
|
+
import { poseBounds, validatePointSkin, validatePose, } from '../../pointSkin.js';
|
|
63
66
|
import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
|
|
64
67
|
import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
|
|
65
68
|
import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.js';
|
|
@@ -920,7 +923,9 @@ export class WebGPURenderer {
|
|
|
920
923
|
* thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
|
|
921
924
|
* for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
|
|
922
925
|
* are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
|
|
923
|
-
* by.
|
|
926
|
+
* by. **And by what each group was built with, on a shelf a material** (`groupShelf.ts`): a
|
|
927
|
+
* garment's particles take turns between two textures and several meshes may follow one cloth,
|
|
928
|
+
* so one group a material was made again draw after draw.
|
|
924
929
|
*/
|
|
925
930
|
skinnedGroups = [];
|
|
926
931
|
/** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
|
|
@@ -4931,17 +4936,14 @@ export class WebGPURenderer {
|
|
|
4931
4936
|
/** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
|
|
4932
4937
|
shadowSkinnedLayout;
|
|
4933
4938
|
/**
|
|
4934
|
-
* The skinned shadow bind groups,
|
|
4939
|
+
* The skinned shadow bind groups, a shelf per palette slot, each found by what it was built with.
|
|
4935
4940
|
*
|
|
4936
4941
|
* The same argument `flatBindGroupFor` makes, and per slot for the same reason: the group it was
|
|
4937
4942
|
* built beside, and the palette view, which now differs between two characters casting into one
|
|
4938
|
-
* bake
|
|
4943
|
+
* bake, and the cloth's particles and binding, which take turns. Comparing object identity makes
|
|
4944
|
+
* a missed invalidation impossible rather than unlikely. See `groupShelf.ts`.
|
|
4939
4945
|
*/
|
|
4940
|
-
|
|
4941
|
-
shadowSkinnedGroupsBeside = [];
|
|
4942
|
-
shadowSkinnedGroupsPalette = [];
|
|
4943
|
-
shadowSkinnedGroupsCloth = [];
|
|
4944
|
-
shadowSkinnedGroupsClothBinding = [];
|
|
4946
|
+
shadowSkinnedShelves = [];
|
|
4945
4947
|
/** Held from construction so the skinned group can be rebuilt without them being threaded. */
|
|
4946
4948
|
shadowBlankView = null;
|
|
4947
4949
|
shadowBlankSampler = null;
|
|
@@ -5098,18 +5100,17 @@ export class WebGPURenderer {
|
|
|
5098
5100
|
*/
|
|
5099
5101
|
shadowSkinnedGroup(palette, at, cloth) {
|
|
5100
5102
|
const beside = this.shadowBindGroup;
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
this.
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
return this.shadowSkinnedGroups[at];
|
|
5103
|
+
let shelf = this.shadowSkinnedShelves[at];
|
|
5104
|
+
if (shelf === undefined) {
|
|
5105
|
+
shelf = new GroupShelf();
|
|
5106
|
+
this.shadowSkinnedShelves[at] = shelf;
|
|
5107
|
+
}
|
|
5108
|
+
const kept = shelf.find(beside, palette, cloth.particles, cloth.binding);
|
|
5109
|
+
if (kept !== null)
|
|
5110
|
+
return kept;
|
|
5111
|
+
const group = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette, null, cloth);
|
|
5112
|
+
shelf.keep(group, beside, palette, cloth.particles, cloth.binding);
|
|
5113
|
+
return group;
|
|
5113
5114
|
}
|
|
5114
5115
|
scatterDepthGroup() {
|
|
5115
5116
|
return this.shadowLayerIsPeel && this.scatterDepthPeelBindGroup !== null
|
|
@@ -6863,7 +6864,7 @@ export class WebGPURenderer {
|
|
|
6863
6864
|
* neither the pipeline set nor the key depends on whether the geometry lands in one call or
|
|
6864
6865
|
* over six frames.
|
|
6865
6866
|
*/
|
|
6866
|
-
warmFlatPipelines(data, dynamic) {
|
|
6867
|
+
warmFlatPipelines(data, dynamic, dynamicColors = false) {
|
|
6867
6868
|
/*
|
|
6868
6869
|
* **Every optional attribute, not the two that come to mind.** `present` decides where
|
|
6869
6870
|
* each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
|
|
@@ -6901,9 +6902,10 @@ export class WebGPURenderer {
|
|
|
6901
6902
|
}
|
|
6902
6903
|
/*
|
|
6903
6904
|
* And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
|
|
6904
|
-
* thirty-two, or not interleaved at all, moves every field after it, so the key says so.
|
|
6905
|
+
* thirty-two, or not interleaved at all, moves every field after it, so the key says so. A
|
|
6906
|
+
* `dynamicColors` mesh's colours are one of those, as `:colors~stream`.
|
|
6905
6907
|
*/
|
|
6906
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
6908
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
6907
6909
|
markPacking(present, packing);
|
|
6908
6910
|
key += packingKey(packing);
|
|
6909
6911
|
const fullKey = `${this.variant}|${key}`;
|
|
@@ -6993,8 +6995,10 @@ export class WebGPURenderer {
|
|
|
6993
6995
|
createMesh(given, options = {}) {
|
|
6994
6996
|
/* The streams it is drawn with, before anything records which it has. See `withLightmapUvs`. */
|
|
6995
6997
|
const data = withLightmapUvs(given);
|
|
6996
|
-
const
|
|
6997
|
-
|
|
6998
|
+
const dynamic = options.dynamic === true;
|
|
6999
|
+
const dynamicColors = options.dynamicColors === true;
|
|
7000
|
+
const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
|
|
7001
|
+
return this.keyed(createGpuMesh(this.surface.device, data, dynamic, dynamicColors), fullKey);
|
|
6998
7002
|
}
|
|
6999
7003
|
/**
|
|
7000
7004
|
* Geometry in, handle out — but the geometry lands over as many frames as the caller gives it.
|
|
@@ -7007,8 +7011,10 @@ export class WebGPURenderer {
|
|
|
7007
7011
|
*/
|
|
7008
7012
|
createMeshIncremental(given, options = {}) {
|
|
7009
7013
|
const data = withLightmapUvs(given);
|
|
7010
|
-
const
|
|
7011
|
-
const
|
|
7014
|
+
const dynamic = options.dynamic === true;
|
|
7015
|
+
const dynamicColors = options.dynamicColors === true;
|
|
7016
|
+
const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
|
|
7017
|
+
const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, dynamic, dynamicColors);
|
|
7012
7018
|
const surface = this.surface;
|
|
7013
7019
|
return {
|
|
7014
7020
|
mesh: this.keyed(mesh, fullKey),
|
|
@@ -7102,6 +7108,75 @@ export class WebGPURenderer {
|
|
|
7102
7108
|
}
|
|
7103
7109
|
mesh.update(this.surface.device, positions, normals, staged);
|
|
7104
7110
|
}
|
|
7111
|
+
/** The pipeline every point skin poses with, made at the first one. See `pointSkinPass.ts`. */
|
|
7112
|
+
pointSkinPipeline = null;
|
|
7113
|
+
/**
|
|
7114
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces. Refused in words
|
|
7115
|
+
* on a mesh that is not dynamic, whose positions share one buffer with the rest of each vertex.
|
|
7116
|
+
*/
|
|
7117
|
+
createPointSkin(mesh, data) {
|
|
7118
|
+
if (mesh.update === null) {
|
|
7119
|
+
throw new Error('a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
|
|
7120
|
+
'have buffers of their own for the pose to write. Say so at `createMesh`.');
|
|
7121
|
+
}
|
|
7122
|
+
validatePointSkin(data, mesh.vertexCount);
|
|
7123
|
+
const device = this.surface.device;
|
|
7124
|
+
this.pointSkinPipeline ??= createPointSkinPipeline(device);
|
|
7125
|
+
return new GpuPointSkin(device, this.pointSkinPipeline, mesh, data);
|
|
7126
|
+
}
|
|
7127
|
+
/**
|
|
7128
|
+
* Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats,
|
|
7129
|
+
* column-major. Recorded into the encoder the frame's next submit leads with, behind a copy of
|
|
7130
|
+
* last frame's positions where a reconstruction keeps them, as `updateMesh` records its rewrite.
|
|
7131
|
+
*/
|
|
7132
|
+
posePointSkin(skin, matrices) {
|
|
7133
|
+
validatePose(matrices, skin.reach);
|
|
7134
|
+
if (this.surface.lost)
|
|
7135
|
+
return;
|
|
7136
|
+
const mesh = skin.mesh;
|
|
7137
|
+
poseBounds(matrices, skin.reach, mesh.bounds);
|
|
7138
|
+
const encoder = this.rewriteCopies();
|
|
7139
|
+
const motion = mesh.motion;
|
|
7140
|
+
if (motion !== null && this.reconstructionWanted) {
|
|
7141
|
+
const frame = frameOfChange(this.moverFrame, this.inFrame);
|
|
7142
|
+
if (keepsPrevious(motion.changed, frame)) {
|
|
7143
|
+
const size = mesh.vertexBuffers[0]?.size ?? 0;
|
|
7144
|
+
motion.previous ??= this.surface.device.createBuffer({
|
|
7145
|
+
label: 'recon.dynamicPrevious',
|
|
7146
|
+
size,
|
|
7147
|
+
usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
|
|
7148
|
+
});
|
|
7149
|
+
encoder.copyBufferToBuffer(mesh.vertexBuffers[0], 0, motion.previous, 0, size);
|
|
7150
|
+
}
|
|
7151
|
+
motion.changed = frame;
|
|
7152
|
+
}
|
|
7153
|
+
skin.pose(this.surface.device, encoder, matrices);
|
|
7154
|
+
}
|
|
7155
|
+
/**
|
|
7156
|
+
* **The pending rewrites go first**, since a pose recorded into them reads the skin's buffers and
|
|
7157
|
+
* a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
|
|
7158
|
+
* the rewrites are submitted, which only moves them earlier, the buffers can go at once.
|
|
7159
|
+
*/
|
|
7160
|
+
disposePointSkin(skin) {
|
|
7161
|
+
if (this.pendingRewrites !== null && !this.surface.lost)
|
|
7162
|
+
this.submit([]);
|
|
7163
|
+
skin.dispose();
|
|
7164
|
+
}
|
|
7165
|
+
/**
|
|
7166
|
+
* Rewrite a mesh's colours. Refused in words on a mesh that did not ask for `dynamicColors`,
|
|
7167
|
+
* whose colours are interleaved with the rest of a vertex or are one value with no buffer at all.
|
|
7168
|
+
* See `GpuMesh.updateColors`.
|
|
7169
|
+
*/
|
|
7170
|
+
updateMeshColors(mesh, colors) {
|
|
7171
|
+
if (mesh.updateColors === null) {
|
|
7172
|
+
throw new Error('this mesh was not created with { dynamicColors: true }, so its colours are packed with ' +
|
|
7173
|
+
'the rest of each vertex, or are one value with no buffer at all, and cannot be ' +
|
|
7174
|
+
'rewritten. Say so at `createMesh`: the flag gives the colours a buffer of their own.');
|
|
7175
|
+
}
|
|
7176
|
+
if (this.surface.lost)
|
|
7177
|
+
return;
|
|
7178
|
+
mesh.updateColors(this.surface.device, colors);
|
|
7179
|
+
}
|
|
7105
7180
|
/**
|
|
7106
7181
|
* Settle everything the frame shares: the camera, the lights and the fog.
|
|
7107
7182
|
*
|
|
@@ -10067,26 +10142,22 @@ export class WebGPURenderer {
|
|
|
10067
10142
|
byMaterial = new Map();
|
|
10068
10143
|
this.skinnedGroups[at] = byMaterial;
|
|
10069
10144
|
}
|
|
10070
|
-
let
|
|
10071
|
-
if (
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
};
|
|
10087
|
-
byMaterial.set(base, twin);
|
|
10088
|
-
}
|
|
10089
|
-
return twin.group;
|
|
10145
|
+
let shelf = byMaterial.get(base);
|
|
10146
|
+
if (shelf === undefined) {
|
|
10147
|
+
shelf = new GroupShelf();
|
|
10148
|
+
byMaterial.set(base, shelf);
|
|
10149
|
+
}
|
|
10150
|
+
const particles = cloth?.particles ?? null;
|
|
10151
|
+
const binding = cloth?.binding ?? null;
|
|
10152
|
+
const kept = shelf.find(palette, deltas, particles, binding);
|
|
10153
|
+
if (kept !== null)
|
|
10154
|
+
return kept;
|
|
10155
|
+
/* Built from the live material, with the overlay's atlas where `base` is its twin. */
|
|
10156
|
+
this.overlayBinding = base !== this.bindGroup;
|
|
10157
|
+
const group = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures, palette, deltas, cloth);
|
|
10158
|
+
this.overlayBinding = false;
|
|
10159
|
+
shelf.keep(group, palette, deltas, particles, binding);
|
|
10160
|
+
return group;
|
|
10090
10161
|
}
|
|
10091
10162
|
/** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
|
|
10092
10163
|
flatLayoutFor(skinned, morphed) {
|
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
* a reset, sits the round out while the rest of the set runs it.
|
|
38
38
|
*/
|
|
39
39
|
export declare const CLOTH_WORKGROUP = 64;
|
|
40
|
-
export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n pad0: u32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
|
|
40
|
+
export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */\n pullKeep: f32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */\n if (paces[g].pullKeep < 1.0) {\n p = t + (p - t) * paces[g].pullKeep;\n }\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
|
|
@@ -87,7 +87,8 @@ struct Pace {
|
|
|
87
87
|
colliderBase: u32,
|
|
88
88
|
colliders: u32,
|
|
89
89
|
width: u32,
|
|
90
|
-
|
|
90
|
+
/* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
|
|
91
|
+
pullKeep: f32,
|
|
91
92
|
keep: f32,
|
|
92
93
|
dragKeep: f32,
|
|
93
94
|
maxDistanceScale: f32,
|
|
@@ -427,7 +428,7 @@ fn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {
|
|
|
427
428
|
return centre + d * (radius / sqrt(d2));
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
|
|
431
|
+
/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
|
|
431
432
|
@compute @workgroup_size(64)
|
|
432
433
|
fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
433
434
|
let i = id.x;
|
|
@@ -443,6 +444,10 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
443
444
|
let at = stateAt(POSITION, i);
|
|
444
445
|
var p = load(at);
|
|
445
446
|
let t = skinnedAt(i, g);
|
|
447
|
+
/* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
|
|
448
|
+
if (paces[g].pullKeep < 1.0) {
|
|
449
|
+
p = t + (p - t) * paces[g].pullKeep;
|
|
450
|
+
}
|
|
446
451
|
/* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
|
|
447
452
|
let maxDistance = statics[n + i];
|
|
448
453
|
if ((limits & 1u) != 0u && maxDistance >= 0.0) {
|
|
@@ -0,0 +1,13 @@
|
|
|
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 declare const POINT_SKIN_WORKGROUP = 64;
|
|
13
|
+
export declare const POINT_SKIN_WGSL = "\nstruct Params {\n count: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;\n@group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;\n@group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;\n@group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;\n@group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;\n\n@compute @workgroup_size(64)\nfn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {\n let v = id.x;\n if (v >= params.count) {\n return;\n }\n let j = named[v];\n var p = vec3<f32>(0.0, 0.0, 0.0);\n for (var k = 0u; k < 4u; k = k + 1u) {\n let q = points[v * 4u + k];\n if (q.w != 0.0) {\n p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;\n }\n }\n let m = joints[j.x];\n let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;\n let l = length(raw);\n var n = raw;\n if (l > 0.0) {\n n = raw / l;\n }\n posedPositions[v * 3u] = p.x;\n posedPositions[v * 3u + 1u] = p.y;\n posedPositions[v * 3u + 2u] = p.z;\n posedNormals[v * 3u] = n.x;\n posedNormals[v * 3u + 1u] = n.y;\n posedNormals[v * 3u + 2u] = n.z;\n}\n";
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
export const POINT_SKIN_WGSL = /* wgsl */ `
|
|
14
|
+
struct Params {
|
|
15
|
+
count: u32,
|
|
16
|
+
pad0: u32,
|
|
17
|
+
pad1: u32,
|
|
18
|
+
pad2: u32,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
22
|
+
@group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;
|
|
23
|
+
@group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;
|
|
24
|
+
@group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;
|
|
25
|
+
@group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;
|
|
26
|
+
@group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;
|
|
27
|
+
@group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;
|
|
28
|
+
|
|
29
|
+
@compute @workgroup_size(${POINT_SKIN_WORKGROUP})
|
|
30
|
+
fn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
31
|
+
let v = id.x;
|
|
32
|
+
if (v >= params.count) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let j = named[v];
|
|
36
|
+
var p = vec3<f32>(0.0, 0.0, 0.0);
|
|
37
|
+
for (var k = 0u; k < 4u; k = k + 1u) {
|
|
38
|
+
let q = points[v * 4u + k];
|
|
39
|
+
if (q.w != 0.0) {
|
|
40
|
+
p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let m = joints[j.x];
|
|
44
|
+
let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;
|
|
45
|
+
let l = length(raw);
|
|
46
|
+
var n = raw;
|
|
47
|
+
if (l > 0.0) {
|
|
48
|
+
n = raw / l;
|
|
49
|
+
}
|
|
50
|
+
posedPositions[v * 3u] = p.x;
|
|
51
|
+
posedPositions[v * 3u + 1u] = p.y;
|
|
52
|
+
posedPositions[v * 3u + 2u] = p.z;
|
|
53
|
+
posedNormals[v * 3u] = n.x;
|
|
54
|
+
posedNormals[v * 3u + 1u] = n.y;
|
|
55
|
+
posedNormals[v * 3u + 2u] = n.z;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
@@ -10,7 +10,7 @@ import { PACKED_ATTRIBUTES, toFixed16, } from '../../vertexPacking.js';
|
|
|
10
10
|
* which reads three. Every field stays a whole number of four-byte words, so a vertex does too.
|
|
11
11
|
*/
|
|
12
12
|
/** The ways an attribute can travel other than as floats, as the key words name them. */
|
|
13
|
-
const PACKINGS = ['snorm16', 'unorm16', 'constant'];
|
|
13
|
+
const PACKINGS = ['snorm16', 'unorm16', 'constant', 'stream'];
|
|
14
14
|
/** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
|
|
15
15
|
function word(name, packing) {
|
|
16
16
|
return `${name}~${packing}`;
|
|
@@ -253,6 +253,12 @@ export declare const PARTICLE_DEPTH_UNIT: number;
|
|
|
253
253
|
export declare const BONE_PLACES_TEXTURE_UNIT: number;
|
|
254
254
|
export declare const BONE_TURNS_TEXTURE_UNIT: number;
|
|
255
255
|
export declare const INSTANCE_CLOCKS_TEXTURE_UNIT: number;
|
|
256
|
+
/**
|
|
257
|
+
* A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
|
|
258
|
+
* every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
|
|
259
|
+
* Twenty-nine.
|
|
260
|
+
*/
|
|
261
|
+
export declare const POINT_SKIN_TEXTURE_UNIT: number;
|
|
256
262
|
/**
|
|
257
263
|
* A cookie's tile, in texels a side.
|
|
258
264
|
*
|
|
@@ -254,6 +254,12 @@ export const PARTICLE_DEPTH_UNIT = PARTICLE_SPRITE_UNIT + 1;
|
|
|
254
254
|
export const BONE_PLACES_TEXTURE_UNIT = PARTICLE_DEPTH_UNIT + 1;
|
|
255
255
|
export const BONE_TURNS_TEXTURE_UNIT = BONE_PLACES_TEXTURE_UNIT + 1;
|
|
256
256
|
export const INSTANCE_CLOCKS_TEXTURE_UNIT = BONE_TURNS_TEXTURE_UNIT + 1;
|
|
257
|
+
/**
|
|
258
|
+
* A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
|
|
259
|
+
* every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
|
|
260
|
+
* Twenty-nine.
|
|
261
|
+
*/
|
|
262
|
+
export const POINT_SKIN_TEXTURE_UNIT = INSTANCE_CLOCKS_TEXTURE_UNIT + 1;
|
|
257
263
|
/**
|
|
258
264
|
* A cookie's tile, in texels a side.
|
|
259
265
|
*
|
package/dist/render/mesh.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export declare class Mesh {
|
|
|
17
17
|
* this — two references, and only where a consumer asked.
|
|
18
18
|
*/
|
|
19
19
|
private readonly deformable;
|
|
20
|
+
/** A dynamic mesh's position and normal buffers, for a pose written on the device; else null. */
|
|
21
|
+
get streams(): {
|
|
22
|
+
readonly positions: WebGLBuffer;
|
|
23
|
+
readonly normals: WebGLBuffer;
|
|
24
|
+
} | null;
|
|
25
|
+
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
26
|
+
private readonly recolourable;
|
|
20
27
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
21
28
|
private readonly positionFloats;
|
|
22
29
|
/**
|
|
@@ -114,8 +121,9 @@ export declare class Mesh {
|
|
|
114
121
|
* and the default is the fast path: filling a buffer with `bufferData` in one call skips the
|
|
115
122
|
* zero-fill that `bufferData(size)` then `bufferSubData` pays for, and every mesh in the
|
|
116
123
|
* engine that does not need to be spread should keep skipping it.
|
|
124
|
+
* @param dynamicColors Colours as floats in a buffer kept for `updateColors`.
|
|
117
125
|
*/
|
|
118
|
-
constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, spread?: boolean);
|
|
126
|
+
constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, spread?: boolean, dynamicColors?: boolean);
|
|
119
127
|
/** Caller is responsible for having the flat program + uniforms bound. */
|
|
120
128
|
draw(gl: WebGL2RenderingContext): void;
|
|
121
129
|
/**
|
|
@@ -135,15 +143,21 @@ export declare class Mesh {
|
|
|
135
143
|
/**
|
|
136
144
|
* Rewrite this mesh's positions, and its normals where the caller has them.
|
|
137
145
|
*
|
|
138
|
-
* **Only the two attributes that move.**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
146
|
+
* **Only the two attributes that move.** Emissive and the rest describe what a surface *is* and
|
|
147
|
+
* do not change because it bent; rewriting them would be uploading unchanged bytes every frame.
|
|
148
|
+
* A caller whose colours change has `updateColors`, on a mesh made `dynamicColors`.
|
|
141
149
|
*
|
|
142
150
|
* **The bounds are recomputed**, because everything that decides whether this is on screen
|
|
143
151
|
* starts from them — a cloth that blew sideways out of its original box would be culled while
|
|
144
152
|
* still visible, which is the kind of bug that only shows up at the edge of the frame.
|
|
145
153
|
*/
|
|
146
154
|
update(gl: WebGL2RenderingContext, positions: Float32Array, normals?: Float32Array): void;
|
|
155
|
+
/**
|
|
156
|
+
* Rewrite this mesh's colours, three floats a vertex, into the buffer they have to themselves.
|
|
157
|
+
*
|
|
158
|
+
* Nothing else moves, the bounds included: a colour does not change where a vertex is.
|
|
159
|
+
*/
|
|
160
|
+
updateColors(gl: WebGL2RenderingContext, colors: Float32Array): void;
|
|
147
161
|
/**
|
|
148
162
|
* One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
|
|
149
163
|
* WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
|
|
@@ -198,6 +212,6 @@ export interface IncrementalMesh {
|
|
|
198
212
|
* the allocations. The bounds are the deliberate one — a streamer places and culls a region from
|
|
199
213
|
* them before a byte of it has landed.
|
|
200
214
|
*/
|
|
201
|
-
export declare function createMeshIncremental(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean): IncrementalMesh;
|
|
215
|
+
export declare function createMeshIncremental(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): IncrementalMesh;
|
|
202
216
|
/** Texture coordinates and layers interleaved as (u, v, layer), one row a vertex. */
|
|
203
217
|
export declare function packUvLayers(uvs: Float32Array, layers: Float32Array): Float32Array;
|
package/dist/render/mesh.js
CHANGED
|
@@ -92,6 +92,12 @@ export class Mesh {
|
|
|
92
92
|
* this — two references, and only where a consumer asked.
|
|
93
93
|
*/
|
|
94
94
|
deformable = null;
|
|
95
|
+
/** A dynamic mesh's position and normal buffers, for a pose written on the device; else null. */
|
|
96
|
+
get streams() {
|
|
97
|
+
return this.deformable;
|
|
98
|
+
}
|
|
99
|
+
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
100
|
+
recolourable = null;
|
|
95
101
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
96
102
|
positionFloats;
|
|
97
103
|
/**
|
|
@@ -191,8 +197,9 @@ export class Mesh {
|
|
|
191
197
|
* and the default is the fast path: filling a buffer with `bufferData` in one call skips the
|
|
192
198
|
* zero-fill that `bufferData(size)` then `bufferSubData` pays for, and every mesh in the
|
|
193
199
|
* engine that does not need to be spread should keep skipping it.
|
|
200
|
+
* @param dynamicColors Colours as floats in a buffer kept for `updateColors`.
|
|
194
201
|
*/
|
|
195
|
-
constructor(gl, data, dynamic = false, spread = false) {
|
|
202
|
+
constructor(gl, data, dynamic = false, spread = false, dynamicColors = false) {
|
|
196
203
|
validateMeshData(data);
|
|
197
204
|
this.spread = spread;
|
|
198
205
|
this.hasUvs = data.uvs !== undefined;
|
|
@@ -212,12 +219,17 @@ export class Mesh {
|
|
|
212
219
|
*/
|
|
213
220
|
const usage = dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
|
|
214
221
|
/* How each attribute travels: sixteen-bit fixed point, or one value as a constant. */
|
|
215
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
222
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
216
223
|
const positions = this.attachAttribute(gl, ATTR_POSITION, data.positions, 3, usage);
|
|
217
224
|
const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage, packing.normals);
|
|
218
225
|
if (dynamic)
|
|
219
226
|
this.deformable = { positions, normals };
|
|
220
|
-
|
|
227
|
+
if (packing.colors === 'stream') {
|
|
228
|
+
this.recolourable = this.attachAttribute(gl, ATTR_COLOR, data.colors, 3, gl.DYNAMIC_DRAW);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
|
|
232
|
+
}
|
|
221
233
|
this.attachShared(gl, ATTR_EMISSIVE, data.emissive, 1, packing.emissive);
|
|
222
234
|
/*
|
|
223
235
|
* Absent means "not shiny", supplied as a constant rather than as a buffer of
|
|
@@ -418,9 +430,9 @@ export class Mesh {
|
|
|
418
430
|
/**
|
|
419
431
|
* Rewrite this mesh's positions, and its normals where the caller has them.
|
|
420
432
|
*
|
|
421
|
-
* **Only the two attributes that move.**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
433
|
+
* **Only the two attributes that move.** Emissive and the rest describe what a surface *is* and
|
|
434
|
+
* do not change because it bent; rewriting them would be uploading unchanged bytes every frame.
|
|
435
|
+
* A caller whose colours change has `updateColors`, on a mesh made `dynamicColors`.
|
|
424
436
|
*
|
|
425
437
|
* **The bounds are recomputed**, because everything that decides whether this is on screen
|
|
426
438
|
* starts from them — a cloth that blew sideways out of its original box would be culled while
|
|
@@ -445,6 +457,25 @@ export class Mesh {
|
|
|
445
457
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, normals);
|
|
446
458
|
}
|
|
447
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Rewrite this mesh's colours, three floats a vertex, into the buffer they have to themselves.
|
|
462
|
+
*
|
|
463
|
+
* Nothing else moves, the bounds included: a colour does not change where a vertex is.
|
|
464
|
+
*/
|
|
465
|
+
updateColors(gl, colors) {
|
|
466
|
+
if (this.recolourable === null) {
|
|
467
|
+
throw new Error('this mesh was not created with { dynamicColors: true }, so its colours are packed or are ' +
|
|
468
|
+
'one value with no buffer at all, and cannot be rewritten. Say so at `createMesh`: the ' +
|
|
469
|
+
'flag gives the colours a buffer of their own.');
|
|
470
|
+
}
|
|
471
|
+
if (colors.length !== this.positionFloats) {
|
|
472
|
+
throw new Error(`this mesh has ${this.positionFloats / 3} vertices and the colours have ` +
|
|
473
|
+
`${colors.length / 3}. A mesh's vertex count is fixed at creation: the index buffer, ` +
|
|
474
|
+
'the pipeline and every other attribute are sized against it.');
|
|
475
|
+
}
|
|
476
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.recolourable);
|
|
477
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);
|
|
478
|
+
}
|
|
448
479
|
/**
|
|
449
480
|
* One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
|
|
450
481
|
* WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
|
|
@@ -537,8 +568,8 @@ export class Mesh {
|
|
|
537
568
|
* the allocations. The bounds are the deliberate one — a streamer places and culls a region from
|
|
538
569
|
* them before a byte of it has landed.
|
|
539
570
|
*/
|
|
540
|
-
export function createMeshIncremental(gl, data, dynamic = false) {
|
|
541
|
-
const mesh = new Mesh(gl, data, dynamic, true);
|
|
571
|
+
export function createMeshIncremental(gl, data, dynamic = false, dynamicColors = false) {
|
|
572
|
+
const mesh = new Mesh(gl, data, dynamic, true, dynamicColors);
|
|
542
573
|
return { mesh, upload: mesh.uploads(gl) };
|
|
543
574
|
}
|
|
544
575
|
/** Texture coordinates and layers interleaved as (u, v, layer), one row a vertex. */
|