@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.
Files changed (49) hide show
  1. package/CHANGELOG.json +27 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/webgl2/renderer.d.ts +12 -0
  6. package/dist/render/backend/webgl2/renderer.js +47 -2
  7. package/dist/render/backend/webgpu/clothSetPack.js +1 -0
  8. package/dist/render/backend/webgpu/clothSolvePack.d.ts +5 -2
  9. package/dist/render/backend/webgpu/clothSolvePack.js +2 -1
  10. package/dist/render/backend/webgpu/gpuClothSet.js +1 -1
  11. package/dist/render/backend/webgpu/renderer.d.ts +9 -0
  12. package/dist/render/backend/webgpu/renderer.js +33 -1
  13. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  14. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -5
  15. package/dist/render/hairLobes.d.ts +23 -0
  16. package/dist/render/hairLobes.js +50 -1
  17. package/dist/render/shaders/flat/detail.d.ts +21 -0
  18. package/dist/render/shaders/flat/detail.js +74 -0
  19. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  20. package/dist/render/shaders/flat/glassTint.js +1 -0
  21. package/dist/render/shaders/flat/hair.js +51 -25
  22. package/dist/render/shaders/flat/index.d.ts +5 -0
  23. package/dist/render/shaders/flat/index.js +3 -0
  24. package/dist/render/shaders/flat/main.d.ts +1 -1
  25. package/dist/render/shaders/flat/main.js +5 -2
  26. package/dist/render/shaders/flat/models.js +11 -4
  27. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  28. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  29. package/dist/render/surfaceDetail.d.ts +79 -0
  30. package/dist/render/surfaceDetail.js +86 -0
  31. package/dist/render/surfaceTexture.d.ts +9 -0
  32. package/package.json +6 -6
  33. package/src/index.ts +2 -0
  34. package/src/render/backend/webgl2/renderer.ts +58 -2
  35. package/src/render/backend/webgpu/clothSetPack.ts +1 -0
  36. package/src/render/backend/webgpu/clothSolvePack.ts +6 -2
  37. package/src/render/backend/webgpu/gpuClothSet.ts +1 -1
  38. package/src/render/backend/webgpu/renderer.ts +42 -1
  39. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -5
  40. package/src/render/hairLobes.ts +76 -1
  41. package/src/render/shaders/flat/detail.ts +74 -0
  42. package/src/render/shaders/flat/glassTint.ts +3 -1
  43. package/src/render/shaders/flat/hair.ts +51 -25
  44. package/src/render/shaders/flat/index.ts +8 -0
  45. package/src/render/shaders/flat/main.ts +5 -2
  46. package/src/render/shaders/flat/models.ts +11 -4
  47. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  48. package/src/render/surfaceDetail.ts +136 -0
  49. package/src/render/surfaceTexture.ts +9 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A material's tiled detail: small normal maps, and occlusion, repeated many times over the base
3
+ * maps, a region map choosing which one each texel wears. `SurfaceMaterial.detail`, packed into
4
+ * `uDetail` for `shaders/flat/detail.ts`.
5
+ *
6
+ * **What it is for.** A surface's finest grain — leather, a weave, brushed metal, a face's pores —
7
+ * is a map of a few hundred texels repeated tens of times across it. Baked into the base maps it is
8
+ * lost: a 256-texel detail tiled forty times over a 2048-texel map would want some ten thousand
9
+ * texels a side. So the base maps carry the shape and a detail map the grain, each garment region
10
+ * with its own grain at its own repeat, which is how such surfaces are authored.
11
+ *
12
+ * **Where the maps are.** The lit stage has no texture binding to spare on either backend, so the
13
+ * detail maps are layers of the array read where a shading model's map is (`uModelMap`), and the
14
+ * region map is a layer of the material's own ORM array, one index a texel in its red. What that
15
+ * gives up, said where each is refused: a material whose shading model has a map must put its
16
+ * detail layers in that same array; a layered material whose mask is a map cannot have detail; and
17
+ * the region map shares the ORM array's size and format, which must keep an index exactly, so an
18
+ * uncompressed one.
19
+ *
20
+ * **Sixteen regions, two vectors each**, behind one vector saying how many and where the region
21
+ * map is: thirty-three in all, which is why WebGL2 compiles them in only when a material first asks
22
+ * and refuses them on a part with no room for them.
23
+ */
24
+ /** Regions a material may name. */
25
+ export declare const MAX_DETAIL_REGIONS = 16;
26
+ /** Floats `uDetail` holds: a header vector, then two vectors a region. */
27
+ export declare const DETAIL_FLOATS: number;
28
+ /** One region's detail. */
29
+ export interface SurfaceDetailRegion {
30
+ /** The layer of the detail array holding this region's tangent-space normal map. */
31
+ readonly normalLayer: number;
32
+ /** The layer holding its occlusion, in red; absent, the region adds none. */
33
+ readonly occlusionLayer?: number;
34
+ /** Repeats across the base coordinates, the same both ways or across and down apart. */
35
+ readonly repeat: number | readonly [number, number];
36
+ /** Added to the repeated coordinates, so neighbouring regions need not line up. Default none. */
37
+ readonly offset?: readonly [number, number];
38
+ /** How far the detail turns the normal: 0 lies flat, 1 as drawn, more steeper. Default 1. */
39
+ readonly normalStrength?: number;
40
+ /** How much of its occlusion is laid on, from 0 to 1. Default 1. */
41
+ readonly occlusionStrength?: number;
42
+ }
43
+ /** A material's tiled detail. */
44
+ export interface SurfaceDetail<Texture> {
45
+ /**
46
+ * The array whose layers the regions name, read where a shading model's map is. A material with
47
+ * a `modelMap` puts its detail layers in that array and names it here too.
48
+ */
49
+ readonly maps: Texture;
50
+ /**
51
+ * The layer of the material's ORM array holding the region map: in its red, each texel's region
52
+ * as an index from 0, stored as `index / 255`, read exactly at the base coordinates. Absent, every
53
+ * texel is region 0, which is a surface with one detail all over: a face's pores.
54
+ */
55
+ readonly regionLayer?: number;
56
+ /** Up to sixteen, by index. A texel naming a region past the last wears the last. */
57
+ readonly regions: readonly SurfaceDetailRegion[];
58
+ }
59
+ /**
60
+ * Pack a material's detail into `out`, `DETAIL_FLOATS` long; zeros for none, which the shader reads
61
+ * as no detail. `hasOrm` says whether the material carries the ORM array a region map is a layer
62
+ * of; without one every texel is region 0. Allocates nothing.
63
+ */
64
+ export declare function packSurfaceDetail(detail: SurfaceDetail<unknown> | null, hasOrm: boolean, out: Float32Array): void;
65
+ /** Why a material's detail is drawn without, or null where nothing stands in its way. */
66
+ export type DetailRefusal = 'slot' | 'projection';
67
+ /**
68
+ * Whether a material's detail can be drawn, given what else wants the slot its maps are read
69
+ * through: a shading model's own map, or a layered material's mask where it is a map. Either must
70
+ * be the detail's array itself. And a material placed by the world has no base coordinates for a
71
+ * region map to be read at, so its detail is refused too.
72
+ */
73
+ export declare function detailRefusal(detail: SurfaceDetail<unknown>, slotTakenBy: unknown, projected: boolean): DetailRefusal | null;
74
+ /** A material's detail, or null where it names none or no region. */
75
+ export declare function detailOf<Texture>(material: {
76
+ readonly detail?: SurfaceDetail<Texture> | null;
77
+ } | null): SurfaceDetail<Texture> | null;
78
+ /** The sentence each backend says once when it draws a material without its detail. */
79
+ export declare function detailRefusalWarning(refusal: DetailRefusal): string;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * A material's tiled detail: small normal maps, and occlusion, repeated many times over the base
3
+ * maps, a region map choosing which one each texel wears. `SurfaceMaterial.detail`, packed into
4
+ * `uDetail` for `shaders/flat/detail.ts`.
5
+ *
6
+ * **What it is for.** A surface's finest grain — leather, a weave, brushed metal, a face's pores —
7
+ * is a map of a few hundred texels repeated tens of times across it. Baked into the base maps it is
8
+ * lost: a 256-texel detail tiled forty times over a 2048-texel map would want some ten thousand
9
+ * texels a side. So the base maps carry the shape and a detail map the grain, each garment region
10
+ * with its own grain at its own repeat, which is how such surfaces are authored.
11
+ *
12
+ * **Where the maps are.** The lit stage has no texture binding to spare on either backend, so the
13
+ * detail maps are layers of the array read where a shading model's map is (`uModelMap`), and the
14
+ * region map is a layer of the material's own ORM array, one index a texel in its red. What that
15
+ * gives up, said where each is refused: a material whose shading model has a map must put its
16
+ * detail layers in that same array; a layered material whose mask is a map cannot have detail; and
17
+ * the region map shares the ORM array's size and format, which must keep an index exactly, so an
18
+ * uncompressed one.
19
+ *
20
+ * **Sixteen regions, two vectors each**, behind one vector saying how many and where the region
21
+ * map is: thirty-three in all, which is why WebGL2 compiles them in only when a material first asks
22
+ * and refuses them on a part with no room for them.
23
+ */
24
+ /** Regions a material may name. */
25
+ export const MAX_DETAIL_REGIONS = 16;
26
+ /** Floats `uDetail` holds: a header vector, then two vectors a region. */
27
+ export const DETAIL_FLOATS = 4 + MAX_DETAIL_REGIONS * 8;
28
+ /**
29
+ * Pack a material's detail into `out`, `DETAIL_FLOATS` long; zeros for none, which the shader reads
30
+ * as no detail. `hasOrm` says whether the material carries the ORM array a region map is a layer
31
+ * of; without one every texel is region 0. Allocates nothing.
32
+ */
33
+ export function packSurfaceDetail(detail, hasOrm, out) {
34
+ out.fill(0);
35
+ if (detail === null)
36
+ return;
37
+ const count = Math.min(MAX_DETAIL_REGIONS, detail.regions.length);
38
+ out[0] = count;
39
+ const regionLayer = detail.regionLayer;
40
+ out[1] = hasOrm && regionLayer !== undefined ? Math.max(0, Math.floor(regionLayer)) : -1;
41
+ for (let i = 0; i < count; i++) {
42
+ const region = detail.regions[i];
43
+ const at = 4 + i * 8;
44
+ const repeat = region.repeat;
45
+ const across = typeof repeat === 'number' ? repeat : repeat[0];
46
+ const down = typeof repeat === 'number' ? repeat : repeat[1];
47
+ out[at] = Math.max(0, Math.floor(region.normalLayer));
48
+ out[at + 1] =
49
+ region.occlusionLayer === undefined ? -1 : Math.max(0, Math.floor(region.occlusionLayer));
50
+ out[at + 2] = Math.max(0, region.normalStrength ?? 1);
51
+ out[at + 3] = Math.min(1, Math.max(0, region.occlusionStrength ?? 1));
52
+ /* A repeat at or below zero has no grain to show, so it is one rather than a mirror or nothing. */
53
+ out[at + 4] = across > 0 ? across : 1;
54
+ out[at + 5] = down > 0 ? down : 1;
55
+ out[at + 6] = region.offset?.[0] ?? 0;
56
+ out[at + 7] = region.offset?.[1] ?? 0;
57
+ }
58
+ }
59
+ /**
60
+ * Whether a material's detail can be drawn, given what else wants the slot its maps are read
61
+ * through: a shading model's own map, or a layered material's mask where it is a map. Either must
62
+ * be the detail's array itself. And a material placed by the world has no base coordinates for a
63
+ * region map to be read at, so its detail is refused too.
64
+ */
65
+ export function detailRefusal(detail, slotTakenBy, projected) {
66
+ if (projected)
67
+ return 'projection';
68
+ if (slotTakenBy !== null && slotTakenBy !== undefined && slotTakenBy !== detail.maps) {
69
+ return 'slot';
70
+ }
71
+ return null;
72
+ }
73
+ /** A material's detail, or null where it names none or no region. */
74
+ export function detailOf(material) {
75
+ const detail = material?.detail ?? null;
76
+ return detail === null || detail.regions.length === 0 ? null : detail;
77
+ }
78
+ /** The sentence each backend says once when it draws a material without its detail. */
79
+ export function detailRefusalWarning(refusal) {
80
+ return refusal === 'slot'
81
+ ? "Renderer: a material's detail is drawn without: its maps are read where a shading model's " +
82
+ "map goes, and this material binds another array there, its `modelMap` or its layers' " +
83
+ 'mask. Put the detail layers in the `modelMap` array and name that array as `detail.maps`.'
84
+ : "Renderer: a material's detail is drawn without: it is placed by the world (`projection`), " +
85
+ 'so there are no base coordinates for its regions and repeats to be read at.';
86
+ }
@@ -8,6 +8,7 @@
8
8
  * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
9
  * so a merged block wearing forty facades is one draw.
10
10
  */
11
+ import type { SurfaceDetail } from './surfaceDetail.ts';
11
12
  import type { SurfaceLayers } from './surfaceLayers.ts';
12
13
  import type { SurfaceProjection } from './surfaceProjection.ts';
13
14
  import type { SurfaceModel } from './surfaceModel.ts';
@@ -235,6 +236,14 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
235
236
  * vertices. See `surfaceLayers.ts` for the rest and what it gives up.
236
237
  */
237
238
  layers?: SurfaceLayers<Texture> | null;
239
+ /**
240
+ * Fine grain tiled over the base maps: up to sixteen regions, each a detail normal map, and an
241
+ * occlusion, at its own repeat and strength, a region map in the ORM array choosing which one each
242
+ * texel wears. The detail maps are layers of an array read where `modelMap` goes, so a material
243
+ * with a shading model's map puts them in that array. **A lit switch**, compiled in the first time
244
+ * a material asks. See `surfaceDetail.ts` for the rest and what it gives up.
245
+ */
246
+ detail?: SurfaceDetail<Texture> | null;
238
247
  }
239
248
  export interface SurfaceTextureOptions {
240
249
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.13.2",
3
+ "version": "4.13.4",
4
4
  "description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.13.2",
33
- "@driftengine/physics": "4.13.2",
32
+ "@driftengine/drft": "4.13.4",
33
+ "@driftengine/physics": "4.13.4",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -67,8 +67,8 @@
67
67
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
68
68
  },
69
69
  "devDependencies": {
70
- "@driftengine/nav": "4.13.2",
71
- "@driftengine/network": "4.13.2",
72
- "@driftengine/texture": "4.13.2"
70
+ "@driftengine/nav": "4.13.4",
71
+ "@driftengine/network": "4.13.4",
72
+ "@driftengine/texture": "4.13.4"
73
73
  }
74
74
  }
package/src/index.ts CHANGED
@@ -677,6 +677,8 @@ export { SurfaceTexture } from './render/surfaceTexture.ts';
677
677
  export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
678
678
  export type { SurfaceProjection } from './render/surfaceProjection.ts';
679
679
  export type { SurfaceLayers } from './render/surfaceLayers.ts';
680
+ export type { SurfaceDetail, SurfaceDetailRegion } from './render/surfaceDetail.ts';
681
+ export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.ts';
680
682
  export type { SurfaceLayerLook } from './render/surfaceLayerLooks.ts';
681
683
  /*
682
684
  * Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
@@ -117,6 +117,15 @@ import {
117
117
  packSurfaceLayers,
118
118
  } from '../../surfaceLayers.ts';
119
119
  import type { SurfaceLayers } from '../../surfaceLayers.ts';
120
+ import {
121
+ DETAIL_FLOATS,
122
+ detailOf,
123
+ detailRefusal,
124
+ detailRefusalWarning,
125
+ packSurfaceDetail,
126
+ type DetailRefusal,
127
+ type SurfaceDetail,
128
+ } from '../../surfaceDetail.ts';
120
129
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
121
130
  import type { SurfaceProjection } from '../../surfaceProjection.ts';
122
131
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
@@ -1801,6 +1810,13 @@ export class WebGL2Renderer implements RendererApi {
1801
1810
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
1802
1811
  private looksRoom: boolean | null = null;
1803
1812
  private warnedLooksRoom = false;
1813
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
1814
+ private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
1815
+ /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
1816
+ private detailRoom: boolean | null = null;
1817
+ private warnedDetailRoom = false;
1818
+ /** The refusals already said, each once. */
1819
+ private readonly warnedDetail = new Set<DetailRefusal>();
1804
1820
  private warnedLayersModelMap = false;
1805
1821
  private warnedLayersTriplanar = false;
1806
1822
  private warnedOverlayRoom = false;
@@ -4462,10 +4478,16 @@ export class WebGL2Renderer implements RendererApi {
4462
4478
  const modelMap = material?.modelMap ?? null;
4463
4479
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
4464
4480
  const layers = this.layersOf(material);
4465
- const boundMap = modelMap ?? layerMaskMap(layers);
4481
+ const taken = modelMap ?? layerMaskMap(layers);
4482
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
4483
+ const detail = this.detailOfMaterial(material, taken);
4484
+ const boundMap = taken ?? detail?.maps ?? null;
4466
4485
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
4467
4486
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
4468
4487
  if (boundMap !== null) boundMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
4488
+ packSurfaceDetail(detail, (material?.orm ?? null) !== null, this.detailFloats);
4489
+ if (detail !== null) this.askLit('SURFACE_DETAIL');
4490
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
4469
4491
  packSurfaceLayers(layers, this.layerFloats);
4470
4492
  if (layers !== null) this.askLit('LAYERED');
4471
4493
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
@@ -4771,6 +4793,37 @@ export class WebGL2Renderer implements RendererApi {
4771
4793
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
4772
4794
  * the sentence names the way out.
4773
4795
  */
4796
+ /**
4797
+ * A material's detail, or null where it is refused: its slot holds another array, it is placed by
4798
+ * the world, or the lit stage has no room for its thirty-three vectors. Each said once.
4799
+ */
4800
+ private detailOfMaterial(
4801
+ material: SurfaceMaterial | null,
4802
+ taken: SurfaceTexture | null,
4803
+ ): SurfaceDetail<SurfaceTexture> | null {
4804
+ const detail = detailOf(material) as SurfaceDetail<SurfaceTexture> | null;
4805
+ if (detail === null) return null;
4806
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
4807
+ if (refusal !== null) {
4808
+ if (!this.warnedDetail.has(refusal)) {
4809
+ this.warnedDetail.add(refusal);
4810
+ console.warn(detailRefusalWarning(refusal));
4811
+ }
4812
+ return null;
4813
+ }
4814
+ this.detailRoom ??= this.switchFits('SURFACE_DETAIL');
4815
+ if (this.detailRoom) return detail;
4816
+ if (!this.warnedDetailRoom) {
4817
+ this.warnedDetailRoom = true;
4818
+ console.warn(
4819
+ "Renderer: a material's detail is refused on this device. It takes thirty-three fragment " +
4820
+ 'uniform vectors the lit shader does not have, so the material is drawn without it. A ' +
4821
+ 'lower `maxLights` makes the room, or WebGPU has it.',
4822
+ );
4823
+ }
4824
+ return null;
4825
+ }
4826
+
4774
4827
  private looksFit(layers: SurfaceLayers<SurfaceTexture> | null): boolean {
4775
4828
  if (!asksLayerLooks(layers)) return false;
4776
4829
  this.looksRoom ??= this.switchFits('LAYER_LOOKS', 'LAYERED');
@@ -7121,6 +7174,7 @@ export class WebGL2Renderer implements RendererApi {
7121
7174
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
7122
7175
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
7123
7176
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
7177
+ surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
7124
7178
  maxLights: budget.maxLights,
7125
7179
  maxAreaLights: budget.maxAreaLights,
7126
7180
  });
@@ -9287,9 +9341,10 @@ export class WebGL2Renderer implements RendererApi {
9287
9341
  this.projection.fill(0);
9288
9342
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
9289
9343
  resolveTransmission(null, this.transmission);
9290
- /* Nor layers: one layer until a material asks for more. */
9344
+ /* Nor layers: one layer until a material asks for more. Nor detail. */
9291
9345
  this.layerFloats.fill(0);
9292
9346
  this.lookFloats.fill(0);
9347
+ this.detailFloats.fill(0);
9293
9348
  this.overlayMaps = null;
9294
9349
  this.overlayTexture = null;
9295
9350
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -9465,6 +9520,7 @@ export class WebGL2Renderer implements RendererApi {
9465
9520
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
9466
9521
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
9467
9522
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
9523
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
9468
9524
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
9469
9525
  `bindSeeThrough` for the four numbers in the one vector. */
9470
9526
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -34,6 +34,7 @@ const SECTIONS: readonly (readonly [number, number])[] = [
34
34
  [7, 3],
35
35
  [10, 3],
36
36
  [13, 16],
37
+ [29, 1],
37
38
  ];
38
39
  /** Where the influences start, and where the garment's index sits, in particle counts. */
39
40
  const INFLUENCES = 13;
@@ -20,7 +20,7 @@ import { batchesOf } from '@driftengine/physics';
20
20
  import type { ClothBatches, SkinnedClothSetup } from '@driftengine/physics';
21
21
 
22
22
  /** Floats of statics a particle, across its sections. */
23
- export const CLOTH_STATIC_FLOATS = 29;
23
+ export const CLOTH_STATIC_FLOATS = 30;
24
24
 
25
25
  /** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
26
26
  export const CLOTH_LIMIT_MAX = 1;
@@ -29,7 +29,10 @@ export const CLOTH_LIMIT_FRONT = 4;
29
29
 
30
30
  export interface PackedCloth {
31
31
  readonly count: number;
32
- /** inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3, influences ×16. */
32
+ /**
33
+ * inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3,
34
+ * influences ×16, pull weight.
35
+ */
33
36
  readonly statics: Float32Array;
34
37
  /** Distance records (a, b, rest, compliance), bending (four ids, rest, compliance), tethers (particle, anchor, length). */
35
38
  readonly constraints: Uint32Array;
@@ -77,6 +80,7 @@ export function packCloth(setup: SkinnedClothSetup): PackedCloth {
77
80
  statics[at + i * 2 + 1] = real ? radius : 0;
78
81
  }
79
82
  statics[6 * n + i] = limits.thickness?.[i] ?? 0;
83
+ statics[29 * n + i] = setup.pullWeights?.[i] ?? 1;
80
84
  for (let k = 0; k < 3; k++) {
81
85
  statics[7 * n + i * 3 + k] = setup.positions[i * 3 + k] as number;
82
86
  statics[10 * n + i * 3 + k] = setup.normals?.[i * 3 + k] ?? (k === 2 ? 1 : 0);
@@ -338,7 +338,7 @@ export class GpuClothSet {
338
338
  u[base + 32] = garment.range.colliderBase;
339
339
  u[base + 33] = garment.range.colliders;
340
340
  u[base + 34] = garment.particles.width;
341
- f[base + 35] = 1 / (1 + p.pull * h * h);
341
+ f[base + 35] = p.pull * h * h;
342
342
  f[base + 36] = exactExp(p.damping * h);
343
343
  f[base + 37] = exactExp(p.drag * h);
344
344
  f[base + 38] = p.maxDistanceScale;
@@ -216,6 +216,15 @@ import {
216
216
  packSurfaceLayers,
217
217
  } from '../../surfaceLayers.ts';
218
218
  import type { SurfaceLayers } from '../../surfaceLayers.ts';
219
+ import {
220
+ DETAIL_FLOATS,
221
+ detailOf,
222
+ detailRefusal,
223
+ detailRefusalWarning,
224
+ packSurfaceDetail,
225
+ type DetailRefusal,
226
+ type SurfaceDetail,
227
+ } from '../../surfaceDetail.ts';
219
228
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
220
229
  import {
221
230
  frameReflectionsRefused,
@@ -3038,6 +3047,10 @@ export class WebGPURenderer implements RendererApi {
3038
3047
  */
3039
3048
  private readonly transmission = new Float32Array([-1, -1, -1, 0]);
3040
3049
  private warnedLayersModelMap = false;
3050
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
3051
+ private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
3052
+ /** The detail refusals already said, each once. See `detailOfMaterial`. */
3053
+ private readonly warnedDetail = new Set<DetailRefusal>();
3041
3054
  private warnedLayersTriplanar = false;
3042
3055
  /** Its numbers, `uModelParams`, packed by `packModel`. */
3043
3056
  private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
@@ -3516,7 +3529,10 @@ export class WebGPURenderer implements RendererApi {
3516
3529
  const ownModelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3517
3530
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3518
3531
  const layers = this.layersOf(material);
3519
- const modelMap = ownModelMap ?? layerMaskMap(layers);
3532
+ const taken = ownModelMap ?? layerMaskMap(layers);
3533
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
3534
+ const detail = this.detailOfMaterial(material, taken);
3535
+ const modelMap = taken ?? detail?.maps ?? null;
3520
3536
  this.materialModel = material?.model ?? null;
3521
3537
  const reflectivity = material?.reflectivity;
3522
3538
  const environmentGain = material?.environmentGain;
@@ -3609,6 +3625,10 @@ export class WebGPURenderer implements RendererApi {
3609
3625
  packLayerLooks(layers, this.lookFloats);
3610
3626
  f.set(this.lookFloats, this.materialField('uLayerLooks'));
3611
3627
  if (asksLayerLooks(layers)) this.enableLit('LAYER_LOOKS');
3628
+ /* And its tiled detail, the first time a material lays any: detail.ts. */
3629
+ packSurfaceDetail(detail, orm !== null, this.detailFloats);
3630
+ f.set(this.detailFloats, this.materialField('uDetail'));
3631
+ if (detail !== null) this.enableLit('SURFACE_DETAIL');
3612
3632
  f.set(this.modelParams, this.materialField('uModelParams'));
3613
3633
  });
3614
3634
  /*
@@ -6395,6 +6415,25 @@ export class WebGPURenderer implements RendererApi {
6395
6415
  * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
6396
6416
  * said once, leave one layer.
6397
6417
  */
6418
+ /**
6419
+ * A material's detail, or null where it is refused: its slot holds another array, or it is placed
6420
+ * by the world. Each said once. This backend has room for its vectors wherever it runs.
6421
+ */
6422
+ private detailOfMaterial(
6423
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
6424
+ taken: GpuSurfaceTexture | null,
6425
+ ): SurfaceDetail<GpuSurfaceTexture> | null {
6426
+ const detail = detailOf(material);
6427
+ if (detail === null) return null;
6428
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
6429
+ if (refusal === null) return detail;
6430
+ if (!this.warnedDetail.has(refusal)) {
6431
+ this.warnedDetail.add(refusal);
6432
+ console.warn(detailRefusalWarning(refusal));
6433
+ }
6434
+ return null;
6435
+ }
6436
+
6398
6437
  private layersOf(
6399
6438
  material: SurfaceMaterial<GpuSurfaceTexture> | null,
6400
6439
  ): SurfaceLayers<GpuSurfaceTexture> | null {
@@ -9110,6 +9149,8 @@ export class WebGPURenderer implements RendererApi {
9110
9149
  /* Nor layers: one layer until a material asks for more. */
9111
9150
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
9112
9151
  f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
9152
+ /* Nor detail. */
9153
+ f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
9113
9154
  f[at('uCutout')] = 0;
9114
9155
  f[at('uCutout') + 1] = 0;
9115
9156
  this.cutoutResolveStaged = 'hard';
@@ -88,8 +88,9 @@ struct Pace {
88
88
  colliderBase: u32,
89
89
  colliders: u32,
90
90
  width: u32,
91
- /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
92
- pullKeep: f32,
91
+ /* The garment's pull as a substep's stiffness, pull · h²: a particle weighted w keeps
92
+ 1 / (1 + w · pullStep) of its offset from its skinned position. */
93
+ pullStep: f32,
93
94
  keep: f32,
94
95
  dragKeep: f32,
95
96
  maxDistanceScale: f32,
@@ -119,7 +120,7 @@ struct Batch {
119
120
 
120
121
  /* The record of particle i's garment this round: which garment a particle is, in statics. */
121
122
  fn paceAt(i: u32) -> u32 {
122
- return round.at + u32(statics[29u * c.count + i]);
123
+ return round.at + u32(statics[30u * c.count + i]);
123
124
  }
124
125
 
125
126
  fn stateAt(section: u32, i: u32) -> u32 {
@@ -446,8 +447,9 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
446
447
  var p = load(at);
447
448
  let t = skinnedAt(i, g);
448
449
  /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
449
- if (paces[g].pullKeep < 1.0) {
450
- p = t + (p - t) * paces[g].pullKeep;
450
+ let pull = statics[29u * n + i] * paces[g].pullStep;
451
+ if (pull > 0.0) {
452
+ p = t + (p - t) * (1.0 / (1.0 + pull));
451
453
  }
452
454
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
453
455
  let maxDistance = statics[n + i];
@@ -50,6 +50,17 @@ export const HAIR_MIN_BETA = 0.05;
50
50
  */
51
51
  export const HAIR_SCATTER_SPHERE = 1.33;
52
52
 
53
+ /**
54
+ * How far every lobe is widened for light from every side, in radians: the angle a source would
55
+ * subtend to widen them as much, `sourceRadius / 2·dist` in `hairLight`'s terms.
56
+ *
57
+ * **0.2, the real-time form's own value**, which stands one direction in for the sphere: a lobe as
58
+ * narrow as a lamp's would put the whole sky's light into one highlight wherever the eye's
59
+ * direction happens to meet it. What it gives up is the sky's shape: a sun low in a clear sky and
60
+ * an even overcast light hair through the same widened lobes.
61
+ */
62
+ export const HAIR_AREA = 0.2;
63
+
53
64
  /** How much the scatter term leans toward the strand's own direction rather than the pseudo-normal. */
54
65
  const KAJIYA_SHARE = 0.33;
55
66
 
@@ -98,6 +109,71 @@ export function hairLight(
98
109
  sourceRadius: number,
99
110
  dist: number,
100
111
  out: Float64Array,
112
+ ): void {
113
+ const grow = sourceRadius / Math.max(2 * dist, 1e-3);
114
+ hairLobes(tangent, toLight, toEye, fibre, roughness, shift, scatter, backlit, grow, out);
115
+ }
116
+
117
+ /** The flattened eye `hairIndirect` lights a strand along, as the shader finds it. */
118
+ const flattened = new Float64Array(3);
119
+
120
+ /**
121
+ * The share of light from every side a strand returns, a colour into `out`, with the one
122
+ * direction's lobes it is made of into `lobes` (`HAIR_OUT`'s layout). `normal` is the surface's,
123
+ * which stands in for the flattened eye where the eye runs along the strand and there is none.
124
+ *
125
+ * **One direction for the sphere**: the eye flattened across the strand, `V − T(V·T)`, unshadowed,
126
+ * with no light from behind and so no transmitted lobe, every lobe widened by `HAIR_AREA`. Twice
127
+ * the sum, and at most all of what arrives. **Why twice**: the scatter's half is then
128
+ * `HAIR_SCATTER_SPHERE`, its own answer to an even sky — along the flattened eye its shape is
129
+ * `mix(1/2, 1, 0.33)`, 0.665, which doubled is that constant's 1.33 — so the lobes ride on a
130
+ * scatter the sphere's quadrature already holds to, and R and TRT are what the direction adds.
131
+ */
132
+ export function hairIndirect(
133
+ tangent: ArrayLike<number>,
134
+ toEye: ArrayLike<number>,
135
+ normal: ArrayLike<number>,
136
+ fibre: Vec3,
137
+ roughness: number,
138
+ shift: number,
139
+ scatter: number,
140
+ lobes: Float64Array,
141
+ out: Float64Array,
142
+ ): void {
143
+ const along = dot(tangent, toEye);
144
+ let length2 = 0;
145
+ for (let c = 0; c < 3; c++) {
146
+ const across = (toEye[c] as number) - (tangent[c] as number) * along;
147
+ flattened[c] = across;
148
+ length2 += across * across;
149
+ }
150
+ for (let c = 0; c < 3; c++) {
151
+ flattened[c] =
152
+ length2 > 1e-8 ? (flattened[c] as number) / Math.sqrt(length2) : (normal[c] as number);
153
+ }
154
+ hairLobes(tangent, flattened, toEye, fibre, roughness, shift, scatter, 0, HAIR_AREA, lobes);
155
+ for (let c = 0; c < 3; c++) {
156
+ const sum =
157
+ (lobes[HAIR_OUT.r] as number) +
158
+ (lobes[HAIR_OUT.tt + c] as number) +
159
+ (lobes[HAIR_OUT.trt + c] as number) +
160
+ (lobes[HAIR_OUT.scatter + c] as number);
161
+ out[c] = Math.min(1, 2 * sum);
162
+ }
163
+ }
164
+
165
+ /** `hairLight`'s arithmetic, every lobe widened by `grow` radians. */
166
+ function hairLobes(
167
+ tangent: ArrayLike<number>,
168
+ toLight: ArrayLike<number>,
169
+ toEye: ArrayLike<number>,
170
+ fibre: Vec3,
171
+ roughness: number,
172
+ shift: number,
173
+ scatter: number,
174
+ backlit: number,
175
+ grow: number,
176
+ out: Float64Array,
101
177
  ): void {
102
178
  const sinI = clamp(dot(tangent, toLight), -1, 1);
103
179
  const sinO = clamp(dot(tangent, toEye), -1, 1);
@@ -124,7 +200,6 @@ export function hairLight(
124
200
  const cosPhi = lpvp / Math.sqrt(lp2 * vp2 + 1e-4);
125
201
  const cosHalfPhi = Math.sqrt(clamp(0.5 + 0.5 * cosPhi, 0, 1));
126
202
  const beta = Math.max(roughness * roughness, HAIR_MIN_BETA);
127
- const grow = sourceRadius / Math.max(2 * dist, 1e-3);
128
203
  const x = sinI + sinO;
129
204
 
130
205
  const r =