@driftengine/core 4.13.2 → 4.13.4
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 +27 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/render/backend/webgl2/renderer.d.ts +12 -0
- package/dist/render/backend/webgl2/renderer.js +47 -2
- package/dist/render/backend/webgpu/clothSetPack.js +1 -0
- package/dist/render/backend/webgpu/clothSolvePack.d.ts +5 -2
- package/dist/render/backend/webgpu/clothSolvePack.js +2 -1
- package/dist/render/backend/webgpu/gpuClothSet.js +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +9 -0
- package/dist/render/backend/webgpu/renderer.js +33 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -5
- package/dist/render/hairLobes.d.ts +23 -0
- package/dist/render/hairLobes.js +50 -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/hair.js +51 -25
- 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 +5 -2
- package/dist/render/shaders/flat/models.js +11 -4
- 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/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/render/backend/webgl2/renderer.ts +58 -2
- package/src/render/backend/webgpu/clothSetPack.ts +1 -0
- package/src/render/backend/webgpu/clothSolvePack.ts +6 -2
- package/src/render/backend/webgpu/gpuClothSet.ts +1 -1
- package/src/render/backend/webgpu/renderer.ts +42 -1
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -5
- package/src/render/hairLobes.ts +76 -1
- package/src/render/shaders/flat/detail.ts +74 -0
- package/src/render/shaders/flat/glassTint.ts +3 -1
- package/src/render/shaders/flat/hair.ts +51 -25
- package/src/render/shaders/flat/index.ts +8 -0
- package/src/render/shaders/flat/main.ts +5 -2
- package/src/render/shaders/flat/models.ts +11 -4
- 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/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.13.4",
|
|
4
|
+
"date": "2026-10-12",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "changed",
|
|
8
|
+
"short": "Hair takes the sky, a probe's light and DriftLight through its own highlights, for the eye's side of each strand, and reflects none of the environment, so a fibre keeps its colour in shade.",
|
|
9
|
+
"text": "Hair is lit from every side through its own lobes. The sky, a probe's irradiance and DriftLight reached a hair surface through its scatter alone, and the environment's reflection was laid over it, white, at the strand's Fresnel: a consumer's golden hair, base colour (1, 0.43, 0.115) and scatter 0.2, was reported drawing as platinum where its source draws it gold. Now the lobes are evaluated once for a light along the eye flattened across the strand, unshadowed, with no transmitted lobe because that direction stands for the eye's side, and every lobe widened by 0.2 radians as a source that large would widen it; twice their sum, at most 1, multiplies the light from every side, under the model map's occlusion as before. The scatter's half of that is exactly what the scatter alone gave, its own average over the sphere, so what is new is the reflected and the internally reflected lobes, which carry the fibre's colour into the shade. Hair reflects none of the environment, the probe, the sky's gradient and screen-space reflections alike, because the reflected lobe already holds that light. Measured on `demo/dev/models.html?view=hair&sky=only`, strands under an even white sky: a strand's middle drew (149, 127, 102) and draws (235, 189, 142), where the reference in `hairLobes.ts` gives (234, 189, 142) for the same geometry; the row of five models moves 8,340 pixels, every one of them the hair sphere's; and WebGPU and WebGL2, on Vulkan and on OpenGL, agree as closely as they did. **What it gives up**: hair lit from behind by an open sky shows no rim from the sky, which the lamps still give it, and the share moves with the eye as the lobes do rather than holding still as an average would."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "added",
|
|
13
|
+
"short": "A skinned cloth's pull is weighed particle by particle, so a strap is held at its root and hangs free at its tip: `SkinnedClothSetup.pullWeights`.",
|
|
14
|
+
"text": "`SkinnedClothSetup.pullWeights` weighs a skinned cloth's pull particle by particle, one a particle from 0 to 1: each keeps `1 / (1 + w · pull · h²)` of its offset from its skinned place a substep, and hangs `9.81 / (w · pull)` metres below it at rest. One pull for a whole garment had to choose, as a consumer reported: held at its mean, a belt's tips stood out from the waist they hang against; held at none, a styled fringe fell past the jaw it is swept up from. Now a strap is held at its root and hangs free at its tip, a fringe is held nearly whole and a coat's body swings free, in one garment. Absent, every particle takes the whole pull, as before; a weight of 0 is no pull at all, a free particle rather than a pinned one, and a weight outside 0 to 1 is refused by its index. The device carries the weights as a section of its statics and the pull as a substep's stiffness, `pull · h²`, and divides as the CPU does. Measured on `demo/dev/skinnedCloth.html?pull=400&weights=ramp`, a cape weighed 1 at its shoulders to 0 at its hem: it moves 39,267 pixels from the same cape under the whole pull; weights of 1 everywhere draw the unweighted cape to the pixel; WebGPU and WebGL2 agree to 159 pixels; and the device parts from the CPU by 1.8 mm at most and 0.03 mm by the 150th frame, inside what an unweighted pull of 100 parted by on the build before (1.8 mm, 0.07 mm), a hem that hangs free being as sensitive as a weak pull. An unweighted pull draws within 11 pixels of what it drew, the device now dividing in single precision where it was handed the share."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "4.13.3",
|
|
20
|
+
"date": "2026-10-12",
|
|
21
|
+
"entries": [
|
|
22
|
+
{
|
|
23
|
+
"kind": "added",
|
|
24
|
+
"short": "A material can lay fine grain over its maps, a region at a time: up to sixteen detail normals and occlusions at their own repeats, chosen per texel: `SurfaceMaterial.detail`.",
|
|
25
|
+
"text": "`SurfaceMaterial.detail` lays a surface's finest grain over its base maps: up to sixteen regions, each a tangent-space detail normal map, and an occlusion, at its own repeat, offset and strengths, and a region map choosing which one each texel wears. A surface's grain, leather, a weave, brushed metal, a face's pores, is a map of a few hundred texels repeated tens of times across it, and baked into the base maps it is lost: a 256-texel detail tiled forty times over a 2048-texel map would want some ten thousand texels a side, so a consumer's characters looked smooth beside the surfaces they were authored with. Each texel reads its own region's detail alone, turned through a frame built on the normal the base maps made, and multiplies its occlusion into the surface's; the reads take explicit gradients, the base coordinates' times the region's repeat, since the region is the pixel's own. **Where the maps are, and why**: the lit stage had no texture binding to spare on either backend, so the detail maps are layers of the array read where a shading model's map is, and a material with a `modelMap` puts its detail layers in that array; the region map is a layer of the material's ORM array, one index a texel in its red, read exactly so a region's edge is sharp, which wants an uncompressed array. A material whose model-map slot holds another array, or that is placed by the world, is drawn without its detail, said once. Sixteen regions are thirty-three vectors: WebGL2 compiles them in only when a material first asks (`SURFACE_DETAIL`) and refuses them, said once, on a part with no room; on WebGPU every material's block grows from 1,024 bytes to 1,536. Measured on an RX 9070 XT (`demo/dev/detail.html`): where a material reads region 0 it draws exactly what one region with no region map draws, on WebGPU and on WebGL2 over Vulkan and over OpenGL; its other regions differ from it by 166,558 pixels; and the backends part by at most one level in a channel."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
2
29
|
{
|
|
3
30
|
"version": "4.13.2",
|
|
4
31
|
"date": "2026-10-11",
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the ren
|
|
|
6
6
|
input, the scene graph, geometry, text and the whole of `@driftengine/physics`, which it re-exports.
|
|
7
7
|
Everything else is a separate package a game adds when it reaches for it.
|
|
8
8
|
|
|
9
|
-
**Cost:
|
|
9
|
+
**Cost: 930.5 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
|
|
10
10
|
which fails the build if the figure drifts by more than 3%.
|
|
11
11
|
|
|
12
12
|
The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
|
package/dist/index.d.ts
CHANGED
|
@@ -231,6 +231,8 @@ export { SurfaceTexture } from './render/surfaceTexture.ts';
|
|
|
231
231
|
export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
|
|
232
232
|
export type { SurfaceProjection } from './render/surfaceProjection.ts';
|
|
233
233
|
export type { SurfaceLayers } from './render/surfaceLayers.ts';
|
|
234
|
+
export type { SurfaceDetail, SurfaceDetailRegion } from './render/surfaceDetail.ts';
|
|
235
|
+
export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.ts';
|
|
234
236
|
export type { SurfaceLayerLook } from './render/surfaceLayerLooks.ts';
|
|
235
237
|
export { compressedFormatName, isCompressedSource, levelBytes, uploadsCompressed, } from './render/compressedSource.ts';
|
|
236
238
|
export type { BlockFormat, CompressedTextureFormat, CompressedTextureSource, SurfaceSource, } from './render/compressedSource.ts';
|
package/dist/index.js
CHANGED
|
@@ -287,6 +287,7 @@ export { MAX_POINT_LIGHTS, SURFACE_TEXTURE_UNIT } from './render/lightBudget.js'
|
|
|
287
287
|
/* The width a light buffer can usefully be with `clusteredLights` on; `createPointLightBuffer`. */
|
|
288
288
|
export { MAX_CLUSTERED_LIGHTS } from './render/clusteredLights.js';
|
|
289
289
|
export { SurfaceTexture } from './render/surfaceTexture.js';
|
|
290
|
+
export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.js';
|
|
290
291
|
/*
|
|
291
292
|
* Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
|
|
292
293
|
* the one question to ask before handing one over — whether this device takes its format
|
|
@@ -1300,6 +1300,13 @@ export declare class WebGL2Renderer implements RendererApi {
|
|
|
1300
1300
|
/** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
|
|
1301
1301
|
private looksRoom;
|
|
1302
1302
|
private warnedLooksRoom;
|
|
1303
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
1304
|
+
private readonly detailFloats;
|
|
1305
|
+
/** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
|
|
1306
|
+
private detailRoom;
|
|
1307
|
+
private warnedDetailRoom;
|
|
1308
|
+
/** The refusals already said, each once. */
|
|
1309
|
+
private readonly warnedDetail;
|
|
1303
1310
|
private warnedLayersModelMap;
|
|
1304
1311
|
private warnedLayersTriplanar;
|
|
1305
1312
|
private warnedOverlayRoom;
|
|
@@ -2199,6 +2206,11 @@ export declare class WebGL2Renderer implements RendererApi {
|
|
|
2199
2206
|
* as they are, said once — which on a material sharing its arrays reads the wrong textures, so
|
|
2200
2207
|
* the sentence names the way out.
|
|
2201
2208
|
*/
|
|
2209
|
+
/**
|
|
2210
|
+
* A material's detail, or null where it is refused: its slot holds another array, it is placed by
|
|
2211
|
+
* the world, or the lit stage has no room for its thirty-three vectors. Each said once.
|
|
2212
|
+
*/
|
|
2213
|
+
private detailOfMaterial;
|
|
2202
2214
|
private looksFit;
|
|
2203
2215
|
/** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
|
|
2204
2216
|
private warnLayersTriplanar;
|
|
@@ -41,6 +41,7 @@ import { resolveTransmission } from '../../transmission.js';
|
|
|
41
41
|
import { resolveOcclusionFade } from '../../occlusionFade.js';
|
|
42
42
|
import { orthographicDepthSpan } from '../../lightMatrix.js';
|
|
43
43
|
import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
|
|
44
|
+
import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
|
|
44
45
|
import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
|
|
45
46
|
import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
|
|
46
47
|
import { SceneTarget } from '../../sceneTarget.js';
|
|
@@ -976,6 +977,13 @@ export class WebGL2Renderer {
|
|
|
976
977
|
/** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
|
|
977
978
|
looksRoom = null;
|
|
978
979
|
warnedLooksRoom = false;
|
|
980
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
981
|
+
detailFloats = new Float32Array(DETAIL_FLOATS);
|
|
982
|
+
/** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
|
|
983
|
+
detailRoom = null;
|
|
984
|
+
warnedDetailRoom = false;
|
|
985
|
+
/** The refusals already said, each once. */
|
|
986
|
+
warnedDetail = new Set();
|
|
979
987
|
warnedLayersModelMap = false;
|
|
980
988
|
warnedLayersTriplanar = false;
|
|
981
989
|
warnedOverlayRoom = false;
|
|
@@ -3307,11 +3315,18 @@ export class WebGL2Renderer {
|
|
|
3307
3315
|
const modelMap = material?.modelMap ?? null;
|
|
3308
3316
|
/* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
|
|
3309
3317
|
const layers = this.layersOf(material);
|
|
3310
|
-
const
|
|
3318
|
+
const taken = modelMap ?? layerMaskMap(layers);
|
|
3319
|
+
/* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
|
|
3320
|
+
const detail = this.detailOfMaterial(material, taken);
|
|
3321
|
+
const boundMap = taken ?? detail?.maps ?? null;
|
|
3311
3322
|
gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
|
|
3312
3323
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
|
|
3313
3324
|
if (boundMap !== null)
|
|
3314
3325
|
boundMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
|
|
3326
|
+
packSurfaceDetail(detail, (material?.orm ?? null) !== null, this.detailFloats);
|
|
3327
|
+
if (detail !== null)
|
|
3328
|
+
this.askLit('SURFACE_DETAIL');
|
|
3329
|
+
gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
|
|
3315
3330
|
packSurfaceLayers(layers, this.layerFloats);
|
|
3316
3331
|
if (layers !== null)
|
|
3317
3332
|
this.askLit('LAYERED');
|
|
@@ -3598,6 +3613,33 @@ export class WebGL2Renderer {
|
|
|
3598
3613
|
* as they are, said once — which on a material sharing its arrays reads the wrong textures, so
|
|
3599
3614
|
* the sentence names the way out.
|
|
3600
3615
|
*/
|
|
3616
|
+
/**
|
|
3617
|
+
* A material's detail, or null where it is refused: its slot holds another array, it is placed by
|
|
3618
|
+
* the world, or the lit stage has no room for its thirty-three vectors. Each said once.
|
|
3619
|
+
*/
|
|
3620
|
+
detailOfMaterial(material, taken) {
|
|
3621
|
+
const detail = detailOf(material);
|
|
3622
|
+
if (detail === null)
|
|
3623
|
+
return null;
|
|
3624
|
+
const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
|
|
3625
|
+
if (refusal !== null) {
|
|
3626
|
+
if (!this.warnedDetail.has(refusal)) {
|
|
3627
|
+
this.warnedDetail.add(refusal);
|
|
3628
|
+
console.warn(detailRefusalWarning(refusal));
|
|
3629
|
+
}
|
|
3630
|
+
return null;
|
|
3631
|
+
}
|
|
3632
|
+
this.detailRoom ??= this.switchFits('SURFACE_DETAIL');
|
|
3633
|
+
if (this.detailRoom)
|
|
3634
|
+
return detail;
|
|
3635
|
+
if (!this.warnedDetailRoom) {
|
|
3636
|
+
this.warnedDetailRoom = true;
|
|
3637
|
+
console.warn("Renderer: a material's detail is refused on this device. It takes thirty-three fragment " +
|
|
3638
|
+
'uniform vectors the lit shader does not have, so the material is drawn without it. A ' +
|
|
3639
|
+
'lower `maxLights` makes the room, or WebGPU has it.');
|
|
3640
|
+
}
|
|
3641
|
+
return null;
|
|
3642
|
+
}
|
|
3601
3643
|
looksFit(layers) {
|
|
3602
3644
|
if (!asksLayerLooks(layers))
|
|
3603
3645
|
return false;
|
|
@@ -5595,6 +5637,7 @@ export class WebGL2Renderer {
|
|
|
5595
5637
|
movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
|
|
5596
5638
|
layered: asIf.LAYERED ?? this.litOn.LAYERED,
|
|
5597
5639
|
layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
|
|
5640
|
+
surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
|
|
5598
5641
|
maxLights: budget.maxLights,
|
|
5599
5642
|
maxAreaLights: budget.maxAreaLights,
|
|
5600
5643
|
});
|
|
@@ -7454,9 +7497,10 @@ export class WebGL2Renderer {
|
|
|
7454
7497
|
this.projection.fill(0);
|
|
7455
7498
|
/* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
|
|
7456
7499
|
resolveTransmission(null, this.transmission);
|
|
7457
|
-
/* Nor layers: one layer until a material asks for more. */
|
|
7500
|
+
/* Nor layers: one layer until a material asks for more. Nor detail. */
|
|
7458
7501
|
this.layerFloats.fill(0);
|
|
7459
7502
|
this.lookFloats.fill(0);
|
|
7503
|
+
this.detailFloats.fill(0);
|
|
7460
7504
|
this.overlayMaps = null;
|
|
7461
7505
|
this.overlayTexture = null;
|
|
7462
7506
|
/* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
|
|
@@ -7601,6 +7645,7 @@ export class WebGL2Renderer {
|
|
|
7601
7645
|
gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
|
|
7602
7646
|
gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
|
|
7603
7647
|
gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
|
|
7648
|
+
gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
|
|
7604
7649
|
/* Neither refracting nor glass, which is every draw that does not say otherwise. See
|
|
7605
7650
|
`bindSeeThrough` for the four numbers in the one vector. */
|
|
7606
7651
|
gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { SkinnedClothSetup } from '@driftengine/physics';
|
|
2
2
|
/** Floats of statics a particle, across its sections. */
|
|
3
|
-
export declare const CLOTH_STATIC_FLOATS =
|
|
3
|
+
export declare const CLOTH_STATIC_FLOATS = 30;
|
|
4
4
|
/** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
|
|
5
5
|
export declare const CLOTH_LIMIT_MAX = 1;
|
|
6
6
|
export declare const CLOTH_LIMIT_BACK = 2;
|
|
7
7
|
export declare const CLOTH_LIMIT_FRONT = 4;
|
|
8
8
|
export interface PackedCloth {
|
|
9
9
|
readonly count: number;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3,
|
|
12
|
+
* influences ×16, pull weight.
|
|
13
|
+
*/
|
|
11
14
|
readonly statics: Float32Array;
|
|
12
15
|
/** Distance records (a, b, rest, compliance), bending (four ids, rest, compliance), tethers (particle, anchor, length). */
|
|
13
16
|
readonly constraints: Uint32Array;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { batchesOf } from '@driftengine/physics';
|
|
20
20
|
/** Floats of statics a particle, across its sections. */
|
|
21
|
-
export const CLOTH_STATIC_FLOATS =
|
|
21
|
+
export const CLOTH_STATIC_FLOATS = 30;
|
|
22
22
|
/** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
|
|
23
23
|
export const CLOTH_LIMIT_MAX = 1;
|
|
24
24
|
export const CLOTH_LIMIT_BACK = 2;
|
|
@@ -50,6 +50,7 @@ export function packCloth(setup) {
|
|
|
50
50
|
statics[at + i * 2 + 1] = real ? radius : 0;
|
|
51
51
|
}
|
|
52
52
|
statics[6 * n + i] = limits.thickness?.[i] ?? 0;
|
|
53
|
+
statics[29 * n + i] = setup.pullWeights?.[i] ?? 1;
|
|
53
54
|
for (let k = 0; k < 3; k++) {
|
|
54
55
|
statics[7 * n + i * 3 + k] = setup.positions[i * 3 + k];
|
|
55
56
|
statics[10 * n + i * 3 + k] = setup.normals?.[i * 3 + k] ?? (k === 2 ? 1 : 0);
|
|
@@ -278,7 +278,7 @@ export class GpuClothSet {
|
|
|
278
278
|
u[base + 32] = garment.range.colliderBase;
|
|
279
279
|
u[base + 33] = garment.range.colliders;
|
|
280
280
|
u[base + 34] = garment.particles.width;
|
|
281
|
-
f[base + 35] =
|
|
281
|
+
f[base + 35] = p.pull * h * h;
|
|
282
282
|
f[base + 36] = exactExp(p.damping * h);
|
|
283
283
|
f[base + 37] = exactExp(p.drag * h);
|
|
284
284
|
f[base + 38] = p.maxDistanceScale;
|
|
@@ -1208,6 +1208,10 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
1208
1208
|
*/
|
|
1209
1209
|
private readonly transmission;
|
|
1210
1210
|
private warnedLayersModelMap;
|
|
1211
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
1212
|
+
private readonly detailFloats;
|
|
1213
|
+
/** The detail refusals already said, each once. See `detailOfMaterial`. */
|
|
1214
|
+
private readonly warnedDetail;
|
|
1211
1215
|
private warnedLayersTriplanar;
|
|
1212
1216
|
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
1213
1217
|
private readonly modelParams;
|
|
@@ -2112,6 +2116,11 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
2112
2116
|
* A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
|
|
2113
2117
|
* said once, leave one layer.
|
|
2114
2118
|
*/
|
|
2119
|
+
/**
|
|
2120
|
+
* A material's detail, or null where it is refused: its slot holds another array, or it is placed
|
|
2121
|
+
* by the world. Each said once. This backend has room for its vectors wherever it runs.
|
|
2122
|
+
*/
|
|
2123
|
+
private detailOfMaterial;
|
|
2115
2124
|
private layersOf;
|
|
2116
2125
|
/** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
|
|
2117
2126
|
private warnLayersTriplanar;
|
|
@@ -70,6 +70,7 @@ import { resolveTransmission } from '../../transmission.js';
|
|
|
70
70
|
import { resolveOcclusionFade } from '../../occlusionFade.js';
|
|
71
71
|
import { orthographicDepthSpan } from '../../lightMatrix.js';
|
|
72
72
|
import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
|
|
73
|
+
import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
|
|
73
74
|
import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
|
|
74
75
|
import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
|
|
75
76
|
import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
|
|
@@ -2284,6 +2285,10 @@ export class WebGPURenderer {
|
|
|
2284
2285
|
*/
|
|
2285
2286
|
transmission = new Float32Array([-1, -1, -1, 0]);
|
|
2286
2287
|
warnedLayersModelMap = false;
|
|
2288
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
2289
|
+
detailFloats = new Float32Array(DETAIL_FLOATS);
|
|
2290
|
+
/** The detail refusals already said, each once. See `detailOfMaterial`. */
|
|
2291
|
+
warnedDetail = new Set();
|
|
2287
2292
|
warnedLayersTriplanar = false;
|
|
2288
2293
|
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
2289
2294
|
modelParams = new Float32Array(MODEL_PARAM_FLOATS);
|
|
@@ -2688,7 +2693,10 @@ export class WebGPURenderer {
|
|
|
2688
2693
|
const ownModelMap = (material?.modelMap ?? null);
|
|
2689
2694
|
/* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
|
|
2690
2695
|
const layers = this.layersOf(material);
|
|
2691
|
-
const
|
|
2696
|
+
const taken = ownModelMap ?? layerMaskMap(layers);
|
|
2697
|
+
/* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
|
|
2698
|
+
const detail = this.detailOfMaterial(material, taken);
|
|
2699
|
+
const modelMap = taken ?? detail?.maps ?? null;
|
|
2692
2700
|
this.materialModel = material?.model ?? null;
|
|
2693
2701
|
const reflectivity = material?.reflectivity;
|
|
2694
2702
|
const environmentGain = material?.environmentGain;
|
|
@@ -2772,6 +2780,11 @@ export class WebGPURenderer {
|
|
|
2772
2780
|
f.set(this.lookFloats, this.materialField('uLayerLooks'));
|
|
2773
2781
|
if (asksLayerLooks(layers))
|
|
2774
2782
|
this.enableLit('LAYER_LOOKS');
|
|
2783
|
+
/* And its tiled detail, the first time a material lays any: detail.ts. */
|
|
2784
|
+
packSurfaceDetail(detail, orm !== null, this.detailFloats);
|
|
2785
|
+
f.set(this.detailFloats, this.materialField('uDetail'));
|
|
2786
|
+
if (detail !== null)
|
|
2787
|
+
this.enableLit('SURFACE_DETAIL');
|
|
2775
2788
|
f.set(this.modelParams, this.materialField('uModelParams'));
|
|
2776
2789
|
});
|
|
2777
2790
|
/*
|
|
@@ -5142,6 +5155,23 @@ export class WebGPURenderer {
|
|
|
5142
5155
|
* A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
|
|
5143
5156
|
* said once, leave one layer.
|
|
5144
5157
|
*/
|
|
5158
|
+
/**
|
|
5159
|
+
* A material's detail, or null where it is refused: its slot holds another array, or it is placed
|
|
5160
|
+
* by the world. Each said once. This backend has room for its vectors wherever it runs.
|
|
5161
|
+
*/
|
|
5162
|
+
detailOfMaterial(material, taken) {
|
|
5163
|
+
const detail = detailOf(material);
|
|
5164
|
+
if (detail === null)
|
|
5165
|
+
return null;
|
|
5166
|
+
const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
|
|
5167
|
+
if (refusal === null)
|
|
5168
|
+
return detail;
|
|
5169
|
+
if (!this.warnedDetail.has(refusal)) {
|
|
5170
|
+
this.warnedDetail.add(refusal);
|
|
5171
|
+
console.warn(detailRefusalWarning(refusal));
|
|
5172
|
+
}
|
|
5173
|
+
return null;
|
|
5174
|
+
}
|
|
5145
5175
|
layersOf(material) {
|
|
5146
5176
|
const layers = material?.layers ?? null;
|
|
5147
5177
|
if (layers === null || layers.repeats.length === 0)
|
|
@@ -7312,6 +7342,8 @@ export class WebGPURenderer {
|
|
|
7312
7342
|
/* Nor layers: one layer until a material asks for more. */
|
|
7313
7343
|
f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
|
|
7314
7344
|
f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
|
|
7345
|
+
/* Nor detail. */
|
|
7346
|
+
f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
|
|
7315
7347
|
f[at('uCutout')] = 0;
|
|
7316
7348
|
f[at('uCutout') + 1] = 0;
|
|
7317
7349
|
this.cutoutResolveStaged = 'hard';
|
|
@@ -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 /* 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";
|
|
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 garment's pull as a substep's stiffness, pull \u00B7 h\u00B2: a particle weighted w keeps\n 1 / (1 + w \u00B7 pullStep) of its offset from its skinned position. */\n pullStep: 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[30u * 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 let pull = statics[29u * n + i] * paces[g].pullStep;\n if (pull > 0.0) {\n p = t + (p - t) * (1.0 / (1.0 + pull));\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,8 +87,9 @@ struct Pace {
|
|
|
87
87
|
colliderBase: u32,
|
|
88
88
|
colliders: u32,
|
|
89
89
|
width: u32,
|
|
90
|
-
/* The
|
|
91
|
-
|
|
90
|
+
/* The garment's pull as a substep's stiffness, pull · h²: a particle weighted w keeps
|
|
91
|
+
1 / (1 + w · pullStep) of its offset from its skinned position. */
|
|
92
|
+
pullStep: f32,
|
|
92
93
|
keep: f32,
|
|
93
94
|
dragKeep: f32,
|
|
94
95
|
maxDistanceScale: f32,
|
|
@@ -118,7 +119,7 @@ struct Batch {
|
|
|
118
119
|
|
|
119
120
|
/* The record of particle i's garment this round: which garment a particle is, in statics. */
|
|
120
121
|
fn paceAt(i: u32) -> u32 {
|
|
121
|
-
return round.at + u32(statics[
|
|
122
|
+
return round.at + u32(statics[30u * c.count + i]);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
fn stateAt(section: u32, i: u32) -> u32 {
|
|
@@ -445,8 +446,9 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
445
446
|
var p = load(at);
|
|
446
447
|
let t = skinnedAt(i, g);
|
|
447
448
|
/* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
|
|
448
|
-
|
|
449
|
-
|
|
449
|
+
let pull = statics[29u * n + i] * paces[g].pullStep;
|
|
450
|
+
if (pull > 0.0) {
|
|
451
|
+
p = t + (p - t) * (1.0 / (1.0 + pull));
|
|
450
452
|
}
|
|
451
453
|
/* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
|
|
452
454
|
let maxDistance = statics[n + i];
|
|
@@ -46,6 +46,16 @@ export declare const HAIR_MIN_BETA = 0.05;
|
|
|
46
46
|
* and its integral 4π times that. Over π: **1.33**.
|
|
47
47
|
*/
|
|
48
48
|
export declare const HAIR_SCATTER_SPHERE = 1.33;
|
|
49
|
+
/**
|
|
50
|
+
* How far every lobe is widened for light from every side, in radians: the angle a source would
|
|
51
|
+
* subtend to widen them as much, `sourceRadius / 2·dist` in `hairLight`'s terms.
|
|
52
|
+
*
|
|
53
|
+
* **0.2, the real-time form's own value**, which stands one direction in for the sphere: a lobe as
|
|
54
|
+
* narrow as a lamp's would put the whole sky's light into one highlight wherever the eye's
|
|
55
|
+
* direction happens to meet it. What it gives up is the sky's shape: a sun low in a clear sky and
|
|
56
|
+
* an even overcast light hair through the same widened lobes.
|
|
57
|
+
*/
|
|
58
|
+
export declare const HAIR_AREA = 0.2;
|
|
49
59
|
/** A longitudinal lobe: a normalised Gaussian of width `beta` at `x` from its centre. */
|
|
50
60
|
export declare function hairLongitudinal(beta: number, x: number): number;
|
|
51
61
|
/** Schlick's Fresnel for the strand at a cosine, clamped to the unit interval. */
|
|
@@ -68,3 +78,16 @@ export declare const HAIR_OUT: {
|
|
|
68
78
|
* light rather than more. The sun passes zeros.
|
|
69
79
|
*/
|
|
70
80
|
export declare function hairLight(tangent: ArrayLike<number>, toLight: ArrayLike<number>, toEye: ArrayLike<number>, fibre: Vec3, roughness: number, shift: number, scatter: number, backlit: number, sourceRadius: number, dist: number, out: Float64Array): void;
|
|
81
|
+
/**
|
|
82
|
+
* The share of light from every side a strand returns, a colour into `out`, with the one
|
|
83
|
+
* direction's lobes it is made of into `lobes` (`HAIR_OUT`'s layout). `normal` is the surface's,
|
|
84
|
+
* which stands in for the flattened eye where the eye runs along the strand and there is none.
|
|
85
|
+
*
|
|
86
|
+
* **One direction for the sphere**: the eye flattened across the strand, `V − T(V·T)`, unshadowed,
|
|
87
|
+
* with no light from behind and so no transmitted lobe, every lobe widened by `HAIR_AREA`. Twice
|
|
88
|
+
* the sum, and at most all of what arrives. **Why twice**: the scatter's half is then
|
|
89
|
+
* `HAIR_SCATTER_SPHERE`, its own answer to an even sky — along the flattened eye its shape is
|
|
90
|
+
* `mix(1/2, 1, 0.33)`, 0.665, which doubled is that constant's 1.33 — so the lobes ride on a
|
|
91
|
+
* scatter the sphere's quadrature already holds to, and R and TRT are what the direction adds.
|
|
92
|
+
*/
|
|
93
|
+
export declare function hairIndirect(tangent: ArrayLike<number>, toEye: ArrayLike<number>, normal: ArrayLike<number>, fibre: Vec3, roughness: number, shift: number, scatter: number, lobes: Float64Array, out: Float64Array): void;
|
package/dist/render/hairLobes.js
CHANGED
|
@@ -17,6 +17,16 @@ export const HAIR_MIN_BETA = 0.05;
|
|
|
17
17
|
* and its integral 4π times that. Over π: **1.33**.
|
|
18
18
|
*/
|
|
19
19
|
export const HAIR_SCATTER_SPHERE = 1.33;
|
|
20
|
+
/**
|
|
21
|
+
* How far every lobe is widened for light from every side, in radians: the angle a source would
|
|
22
|
+
* subtend to widen them as much, `sourceRadius / 2·dist` in `hairLight`'s terms.
|
|
23
|
+
*
|
|
24
|
+
* **0.2, the real-time form's own value**, which stands one direction in for the sphere: a lobe as
|
|
25
|
+
* narrow as a lamp's would put the whole sky's light into one highlight wherever the eye's
|
|
26
|
+
* direction happens to meet it. What it gives up is the sky's shape: a sun low in a clear sky and
|
|
27
|
+
* an even overcast light hair through the same widened lobes.
|
|
28
|
+
*/
|
|
29
|
+
export const HAIR_AREA = 0.2;
|
|
20
30
|
/** How much the scatter term leans toward the strand's own direction rather than the pseudo-normal. */
|
|
21
31
|
const KAJIYA_SHARE = 0.33;
|
|
22
32
|
/** A longitudinal lobe: a normalised Gaussian of width `beta` at `x` from its centre. */
|
|
@@ -46,6 +56,46 @@ export const HAIR_OUT = { r: 0, tt: 1, trt: 4, scatter: 7, length: 10 };
|
|
|
46
56
|
* light rather than more. The sun passes zeros.
|
|
47
57
|
*/
|
|
48
58
|
export function hairLight(tangent, toLight, toEye, fibre, roughness, shift, scatter, backlit, sourceRadius, dist, out) {
|
|
59
|
+
const grow = sourceRadius / Math.max(2 * dist, 1e-3);
|
|
60
|
+
hairLobes(tangent, toLight, toEye, fibre, roughness, shift, scatter, backlit, grow, out);
|
|
61
|
+
}
|
|
62
|
+
/** The flattened eye `hairIndirect` lights a strand along, as the shader finds it. */
|
|
63
|
+
const flattened = new Float64Array(3);
|
|
64
|
+
/**
|
|
65
|
+
* The share of light from every side a strand returns, a colour into `out`, with the one
|
|
66
|
+
* direction's lobes it is made of into `lobes` (`HAIR_OUT`'s layout). `normal` is the surface's,
|
|
67
|
+
* which stands in for the flattened eye where the eye runs along the strand and there is none.
|
|
68
|
+
*
|
|
69
|
+
* **One direction for the sphere**: the eye flattened across the strand, `V − T(V·T)`, unshadowed,
|
|
70
|
+
* with no light from behind and so no transmitted lobe, every lobe widened by `HAIR_AREA`. Twice
|
|
71
|
+
* the sum, and at most all of what arrives. **Why twice**: the scatter's half is then
|
|
72
|
+
* `HAIR_SCATTER_SPHERE`, its own answer to an even sky — along the flattened eye its shape is
|
|
73
|
+
* `mix(1/2, 1, 0.33)`, 0.665, which doubled is that constant's 1.33 — so the lobes ride on a
|
|
74
|
+
* scatter the sphere's quadrature already holds to, and R and TRT are what the direction adds.
|
|
75
|
+
*/
|
|
76
|
+
export function hairIndirect(tangent, toEye, normal, fibre, roughness, shift, scatter, lobes, out) {
|
|
77
|
+
const along = dot(tangent, toEye);
|
|
78
|
+
let length2 = 0;
|
|
79
|
+
for (let c = 0; c < 3; c++) {
|
|
80
|
+
const across = toEye[c] - tangent[c] * along;
|
|
81
|
+
flattened[c] = across;
|
|
82
|
+
length2 += across * across;
|
|
83
|
+
}
|
|
84
|
+
for (let c = 0; c < 3; c++) {
|
|
85
|
+
flattened[c] =
|
|
86
|
+
length2 > 1e-8 ? flattened[c] / Math.sqrt(length2) : normal[c];
|
|
87
|
+
}
|
|
88
|
+
hairLobes(tangent, flattened, toEye, fibre, roughness, shift, scatter, 0, HAIR_AREA, lobes);
|
|
89
|
+
for (let c = 0; c < 3; c++) {
|
|
90
|
+
const sum = lobes[HAIR_OUT.r] +
|
|
91
|
+
lobes[HAIR_OUT.tt + c] +
|
|
92
|
+
lobes[HAIR_OUT.trt + c] +
|
|
93
|
+
lobes[HAIR_OUT.scatter + c];
|
|
94
|
+
out[c] = Math.min(1, 2 * sum);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** `hairLight`'s arithmetic, every lobe widened by `grow` radians. */
|
|
98
|
+
function hairLobes(tangent, toLight, toEye, fibre, roughness, shift, scatter, backlit, grow, out) {
|
|
49
99
|
const sinI = clamp(dot(tangent, toLight), -1, 1);
|
|
50
100
|
const sinO = clamp(dot(tangent, toEye), -1, 1);
|
|
51
101
|
/*
|
|
@@ -68,7 +118,6 @@ export function hairLight(tangent, toLight, toEye, fibre, roughness, shift, scat
|
|
|
68
118
|
const cosPhi = lpvp / Math.sqrt(lp2 * vp2 + 1e-4);
|
|
69
119
|
const cosHalfPhi = Math.sqrt(clamp(0.5 + 0.5 * cosPhi, 0, 1));
|
|
70
120
|
const beta = Math.max(roughness * roughness, HAIR_MIN_BETA);
|
|
71
|
-
const grow = sourceRadius / Math.max(2 * dist, 1e-3);
|
|
72
121
|
const x = sinI + sinO;
|
|
73
122
|
const r = hairLongitudinal(beta + grow, x + 2 * shift) *
|
|
74
123
|
0.25 *
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's tiled detail over whatever its maps made: `SurfaceMaterial.detail`, packed into
|
|
3
|
+
* `uDetail` by `surfaceDetail.ts`.
|
|
4
|
+
*
|
|
5
|
+
* **Each texel wears its own region's detail alone.** The region is read exactly from the ORM
|
|
6
|
+
* array's layer the material names, at the base coordinates, so an index is never blended with its
|
|
7
|
+
* neighbour's at a region's edge; then that region's normal map is read at its repeat and offset,
|
|
8
|
+
* turned through a frame built on the normal the base maps made — the construction a layered
|
|
9
|
+
* material lays its layers over a mesh normal with — and its occlusion multiplied into the
|
|
10
|
+
* surface's. A strength scales the detail's slopes as a layer's look does.
|
|
11
|
+
*
|
|
12
|
+
* **Read with explicit gradients**, the base coordinates' times the region's repeat, because the
|
|
13
|
+
* region is the pixel's own: a read whose level is implicit is legal only in uniform control flow
|
|
14
|
+
* (AGENTS.md 2026-08-07), and across a region's edge the repeated coordinates jump, which would
|
|
15
|
+
* read a level as coarse as the jump. The frame and the gradients are taken first, under the
|
|
16
|
+
* uniform branch that calls this.
|
|
17
|
+
*
|
|
18
|
+
* **A lit switch, `SURFACE_DETAIL`**, off until a material first asks, declared after `LAYERED` and
|
|
19
|
+
* `LAYER_LOOKS` so each keeps its id. Compiled out, its thirty-three vectors are not declared.
|
|
20
|
+
*/
|
|
21
|
+
export declare function detailGlsl(on: boolean): string;
|