@driftengine/core 4.8.3 → 4.8.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 (178) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +10 -4
  5. package/dist/index.js +16 -1
  6. package/dist/math/scalar.js +1 -1
  7. package/dist/render/anisotropicLobe.d.ts +7 -0
  8. package/dist/render/anisotropicLobe.js +33 -0
  9. package/dist/render/backend/api.d.ts +41 -4
  10. package/dist/render/backend/select.js +8 -0
  11. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  12. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  13. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  14. package/dist/render/backend/webgl2/renderer.js +604 -63
  15. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  16. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  17. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  18. package/dist/render/backend/webgpu/buffers.js +98 -49
  19. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  20. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  21. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  22. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  24. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  25. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  26. package/dist/render/backend/webgpu/computeHost.js +5 -0
  27. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  28. package/dist/render/backend/webgpu/depthPass.js +39 -9
  29. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  30. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  31. package/dist/render/backend/webgpu/flatPass.js +109 -12
  32. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  34. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  35. package/dist/render/backend/webgpu/motionPass.js +106 -9
  36. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  37. package/dist/render/backend/webgpu/postPass.js +6 -3
  38. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  39. package/dist/render/backend/webgpu/renderer.js +498 -48
  40. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  42. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  44. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  45. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  46. package/dist/render/clothBindingData.d.ts +41 -0
  47. package/dist/render/clothBindingData.js +72 -0
  48. package/dist/render/clothPlacement.d.ts +25 -0
  49. package/dist/render/clothPlacement.js +85 -0
  50. package/dist/render/compressedSource.d.ts +66 -0
  51. package/dist/render/compressedSource.js +138 -0
  52. package/dist/render/cutoutCaster.d.ts +2 -0
  53. package/dist/render/cutoutCaster.js +2 -0
  54. package/dist/render/cutoutDither.d.ts +51 -0
  55. package/dist/render/cutoutDither.js +90 -0
  56. package/dist/render/eyeRefraction.d.ts +35 -0
  57. package/dist/render/eyeRefraction.js +40 -0
  58. package/dist/render/glCompressed.d.ts +27 -0
  59. package/dist/render/glCompressed.js +53 -0
  60. package/dist/render/hairLobes.d.ts +70 -0
  61. package/dist/render/hairLobes.js +120 -0
  62. package/dist/render/lightBudget.d.ts +22 -0
  63. package/dist/render/lightBudget.js +22 -0
  64. package/dist/render/mesh.d.ts +7 -0
  65. package/dist/render/mesh.js +24 -0
  66. package/dist/render/recon/motionVectors.d.ts +4 -2
  67. package/dist/render/recon/motionVectors.js +6 -3
  68. package/dist/render/renderQuality.d.ts +20 -0
  69. package/dist/render/renderQuality.js +5 -0
  70. package/dist/render/rendererHarness.js +4 -0
  71. package/dist/render/sceneTarget.d.ts +12 -0
  72. package/dist/render/sceneTarget.js +14 -0
  73. package/dist/render/shaders/clothBinding.d.ts +22 -0
  74. package/dist/render/shaders/clothBinding.js +80 -0
  75. package/dist/render/shaders/depth.js +42 -6
  76. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  77. package/dist/render/shaders/flat/anisotropic.js +68 -0
  78. package/dist/render/shaders/flat/eye.d.ts +1 -0
  79. package/dist/render/shaders/flat/eye.js +81 -0
  80. package/dist/render/shaders/flat/hair.d.ts +1 -0
  81. package/dist/render/shaders/flat/hair.js +101 -0
  82. package/dist/render/shaders/flat/index.js +36 -2
  83. package/dist/render/shaders/flat/main.d.ts +1 -1
  84. package/dist/render/shaders/flat/main.js +113 -17
  85. package/dist/render/shaders/flat/models.d.ts +14 -0
  86. package/dist/render/shaders/flat/models.js +273 -0
  87. package/dist/render/shaders/flat/preamble.js +26 -24
  88. package/dist/render/shaders/flat/skin.d.ts +1 -0
  89. package/dist/render/shaders/flat/skin.js +114 -0
  90. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  91. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  92. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  93. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  94. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  95. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  96. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  97. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  98. package/dist/render/shaders/skinBlur.d.ts +1 -0
  99. package/dist/render/shaders/skinBlur.js +103 -0
  100. package/dist/render/shaders/skinning.d.ts +3 -1
  101. package/dist/render/shaders/skinning.js +31 -4
  102. package/dist/render/shadowCasters.d.ts +13 -1
  103. package/dist/render/skinBlur.d.ts +41 -0
  104. package/dist/render/skinBlur.js +49 -0
  105. package/dist/render/skinScatter.d.ts +67 -0
  106. package/dist/render/skinScatter.js +80 -0
  107. package/dist/render/skinnedClothRun.d.ts +26 -0
  108. package/dist/render/skinnedClothRun.js +84 -0
  109. package/dist/render/surfaceModel.d.ts +129 -0
  110. package/dist/render/surfaceModel.js +152 -0
  111. package/dist/render/surfaceTexture.d.ts +44 -4
  112. package/dist/render/surfaceTexture.js +60 -46
  113. package/dist/render/vertexDefaults.js +7 -0
  114. package/package.json +6 -6
  115. package/src/geometry/meshConcat.ts +2 -0
  116. package/src/index.ts +40 -1
  117. package/src/math/scalar.ts +1 -1
  118. package/src/render/anisotropicLobe.ts +41 -0
  119. package/src/render/backend/api.ts +52 -3
  120. package/src/render/backend/select.ts +8 -0
  121. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  122. package/src/render/backend/webgl2/renderer.ts +693 -64
  123. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  124. package/src/render/backend/webgpu/buffers.ts +137 -63
  125. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  126. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  127. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  128. package/src/render/backend/webgpu/computeHost.ts +25 -0
  129. package/src/render/backend/webgpu/depthPass.ts +39 -7
  130. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  131. package/src/render/backend/webgpu/flatPass.ts +145 -7
  132. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  133. package/src/render/backend/webgpu/motionPass.ts +128 -9
  134. package/src/render/backend/webgpu/postPass.ts +5 -1
  135. package/src/render/backend/webgpu/renderer.ts +663 -43
  136. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  137. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  138. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  139. package/src/render/clothBindingData.ts +89 -0
  140. package/src/render/clothPlacement.ts +114 -0
  141. package/src/render/compressedSource.ts +198 -0
  142. package/src/render/cutoutCaster.ts +5 -0
  143. package/src/render/cutoutDither.ts +99 -0
  144. package/src/render/eyeRefraction.ts +72 -0
  145. package/src/render/glCompressed.ts +86 -0
  146. package/src/render/hairLobes.ts +188 -0
  147. package/src/render/lightBudget.ts +25 -0
  148. package/src/render/mesh.ts +23 -0
  149. package/src/render/recon/motionVectors.ts +6 -3
  150. package/src/render/renderQuality.ts +28 -0
  151. package/src/render/rendererHarness.ts +4 -0
  152. package/src/render/sceneTarget.ts +21 -0
  153. package/src/render/shaders/clothBinding.ts +81 -0
  154. package/src/render/shaders/depth.ts +42 -6
  155. package/src/render/shaders/flat/anisotropic.ts +68 -0
  156. package/src/render/shaders/flat/eye.ts +82 -0
  157. package/src/render/shaders/flat/hair.ts +102 -0
  158. package/src/render/shaders/flat/index.ts +36 -2
  159. package/src/render/shaders/flat/main.ts +113 -17
  160. package/src/render/shaders/flat/models.ts +286 -0
  161. package/src/render/shaders/flat/preamble.ts +26 -24
  162. package/src/render/shaders/flat/skin.ts +116 -0
  163. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  164. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  165. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  166. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  167. package/src/render/shaders/skinBlur.ts +105 -0
  168. package/src/render/shaders/skinning.ts +31 -4
  169. package/src/render/shadowCasters.ts +13 -0
  170. package/src/render/skinBlur.ts +53 -0
  171. package/src/render/skinScatter.ts +92 -0
  172. package/src/render/skinnedClothRun.ts +128 -0
  173. package/src/render/surfaceModel.ts +274 -0
  174. package/src/render/surfaceTexture.ts +123 -63
  175. package/src/render/vertexDefaults.ts +7 -0
  176. package/dist/math/exact.d.ts +0 -76
  177. package/dist/math/exact.js +0 -327
  178. package/src/math/exact.ts +0 -340
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The skin model's arithmetic, as `shaders/flat/skin.ts` computes it: the reference its tests hold
3
+ * the fit to, and nothing at run time calls it.
4
+ *
5
+ * **Pre-integrated, after Penner 2011, with the profile integrated rather than looked up.** Light
6
+ * entering skin leaves it a little way off, so on a curved surface a point is lit by its
7
+ * neighbours' `N·L` as well as its own: the clamped cosine averaged around a ring of the surface's
8
+ * curvature, weighted by the diffusion profile at each neighbour's distance. The profile is Burley's
9
+ * normalised diffusion, one per channel, at a scatter distance `d` — the model's `radius` times its
10
+ * scatter colour — projected onto the ring's plane, as a cylinder's cross-section sees it. The
11
+ * result depends on `N·L` and on one number per channel, `k = d × curvature`, and at `k = 0` it is
12
+ * Lambert's.
13
+ *
14
+ * **Fitted rather than tabulated**, so no seventeenth sampler and no image: the ring average is the
15
+ * clamped cosine convolved with an angular kernel, so it splits into the cosine's first harmonic —
16
+ * scaled by one number, `κ₁(k)` — and a smoothed `|cos θ|` carrying every even harmonic:
17
+ *
18
+ * D(c, k) = ½ (κ₁ c + A √(c² + ε²) + B)
19
+ *
20
+ * with `κ₁`, `A`, `ε` and `B` closed forms in `k` that are exactly `1, 1, 0, 0` at `k = 0`. The
21
+ * test measures it against the integral done numerically: **worst 0.0105 over `k` to 10**, beyond
22
+ * which the shader holds `k` — a scatter ten times the surface's own radius, where the answer is
23
+ * nearly flat already.
24
+ */
25
+ /** Schlick's base for skin, at index 1.4: `((1 − n) / (1 + n))²`. */
26
+ export declare const SKIN_F0: number;
27
+ /** The largest `k` the fit is held to, and where the shader clamps it. */
28
+ export declare const SKIN_MAX_K = 10;
29
+ /**
30
+ * The fit's eight numbers, found by minimising its worst error against the numeric integral over
31
+ * `c` in [−1, 1] and `k` in (0, 10]:
32
+ *
33
+ * - `kappa`: `κ₁ = (1 + a k² + b k³)^(−1/6)` — the first harmonic's share, quadratic at zero and
34
+ * falling as `1/√k`, as the integral's does;
35
+ * - `spread`: `A = (1 + a k³)^(−1/6)`;
36
+ * - `soften`: `ε = a k / (1 + b k)`, how far `|c|` is rounded at the terminator;
37
+ * - `floor`: `B = (1 − A)(a + b (1 − A))`, the even part's floor as `A` gives up `|c|`.
38
+ */
39
+ export declare const SKIN_FIT: {
40
+ readonly kappa: readonly [22.6772, 101.349];
41
+ readonly spread: 838.87;
42
+ readonly soften: readonly [1.3081, 3.65743];
43
+ readonly floor: readonly [0.508537, 0.129496];
44
+ };
45
+ /** The ring-averaged `N·L` for one channel: `c` is `N·L`, `k` that channel's scatter × curvature. */
46
+ export declare function skinRing(c: number, k: number): number;
47
+ /**
48
+ * How far into a penumbra skin's light reaches, as a width the shadow is assumed to have: the
49
+ * lamp loop reads a shadow where its derivative cannot be taken, so its width in the world is not
50
+ * known there. Two centimetres is a soft edge across a face.
51
+ */
52
+ export declare const SKIN_PENUMBRA_WIDTH = 0.02;
53
+ /**
54
+ * A shadow on skin, one channel at scatter distance `d`: the shadow's own value raised to
55
+ * `1 / (1 + d / width)`, so light reaches further into the penumbra the further that channel
56
+ * travels beneath the surface — red furthest, which is the red edge a shadow on skin has — and a
57
+ * shadow stays whole at 0 and absent at 1. **What it gives up** is the profile itself: Penner
58
+ * convolves a sharpened shadow with it, which needs the penumbra's width in the world and hardens
59
+ * the narrow channels' edge where nothing widened the map to make room.
60
+ */
61
+ export declare function skinPenumbra(shadow: number, d: number): number;
62
+ /**
63
+ * Light through a thin part, one channel: `transmission × exp(−thickness / d)` of the light behind,
64
+ * by how squarely it is behind. A part as thick as its curvature's diameter — an ear's rim at a few
65
+ * millimetres, a cheek at ten centimetres — passes red through the one and nothing through the other.
66
+ */
67
+ export declare function skinTransmission(backNdl: number, thickness: number, d: number, transmission: number): number;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The skin model's arithmetic, as `shaders/flat/skin.ts` computes it: the reference its tests hold
3
+ * the fit to, and nothing at run time calls it.
4
+ *
5
+ * **Pre-integrated, after Penner 2011, with the profile integrated rather than looked up.** Light
6
+ * entering skin leaves it a little way off, so on a curved surface a point is lit by its
7
+ * neighbours' `N·L` as well as its own: the clamped cosine averaged around a ring of the surface's
8
+ * curvature, weighted by the diffusion profile at each neighbour's distance. The profile is Burley's
9
+ * normalised diffusion, one per channel, at a scatter distance `d` — the model's `radius` times its
10
+ * scatter colour — projected onto the ring's plane, as a cylinder's cross-section sees it. The
11
+ * result depends on `N·L` and on one number per channel, `k = d × curvature`, and at `k = 0` it is
12
+ * Lambert's.
13
+ *
14
+ * **Fitted rather than tabulated**, so no seventeenth sampler and no image: the ring average is the
15
+ * clamped cosine convolved with an angular kernel, so it splits into the cosine's first harmonic —
16
+ * scaled by one number, `κ₁(k)` — and a smoothed `|cos θ|` carrying every even harmonic:
17
+ *
18
+ * D(c, k) = ½ (κ₁ c + A √(c² + ε²) + B)
19
+ *
20
+ * with `κ₁`, `A`, `ε` and `B` closed forms in `k` that are exactly `1, 1, 0, 0` at `k = 0`. The
21
+ * test measures it against the integral done numerically: **worst 0.0105 over `k` to 10**, beyond
22
+ * which the shader holds `k` — a scatter ten times the surface's own radius, where the answer is
23
+ * nearly flat already.
24
+ */
25
+ /** Schlick's base for skin, at index 1.4: `((1 − n) / (1 + n))²`. */
26
+ export const SKIN_F0 = ((1 - 1.4) / (1 + 1.4)) ** 2;
27
+ /** The largest `k` the fit is held to, and where the shader clamps it. */
28
+ export const SKIN_MAX_K = 10;
29
+ /**
30
+ * The fit's eight numbers, found by minimising its worst error against the numeric integral over
31
+ * `c` in [−1, 1] and `k` in (0, 10]:
32
+ *
33
+ * - `kappa`: `κ₁ = (1 + a k² + b k³)^(−1/6)` — the first harmonic's share, quadratic at zero and
34
+ * falling as `1/√k`, as the integral's does;
35
+ * - `spread`: `A = (1 + a k³)^(−1/6)`;
36
+ * - `soften`: `ε = a k / (1 + b k)`, how far `|c|` is rounded at the terminator;
37
+ * - `floor`: `B = (1 − A)(a + b (1 − A))`, the even part's floor as `A` gives up `|c|`.
38
+ */
39
+ export const SKIN_FIT = {
40
+ kappa: [22.6772, 101.349],
41
+ spread: 838.87,
42
+ soften: [1.3081, 3.65743],
43
+ floor: [0.508537, 0.129496],
44
+ };
45
+ /** The ring-averaged `N·L` for one channel: `c` is `N·L`, `k` that channel's scatter × curvature. */
46
+ export function skinRing(c, k) {
47
+ const q = Math.min(Math.max(k, 0), SKIN_MAX_K);
48
+ const k2 = q * q;
49
+ const kappa = (1 + SKIN_FIT.kappa[0] * k2 + SKIN_FIT.kappa[1] * k2 * q) ** (-1 / 6);
50
+ const spread = (1 + SKIN_FIT.spread * k2 * q) ** (-1 / 6);
51
+ const soften = (SKIN_FIT.soften[0] * q) / (1 + SKIN_FIT.soften[1] * q);
52
+ const given = 1 - spread;
53
+ const floor = given * (SKIN_FIT.floor[0] + SKIN_FIT.floor[1] * given);
54
+ return 0.5 * (kappa * c + spread * Math.sqrt(c * c + soften * soften) + floor);
55
+ }
56
+ /**
57
+ * How far into a penumbra skin's light reaches, as a width the shadow is assumed to have: the
58
+ * lamp loop reads a shadow where its derivative cannot be taken, so its width in the world is not
59
+ * known there. Two centimetres is a soft edge across a face.
60
+ */
61
+ export const SKIN_PENUMBRA_WIDTH = 0.02;
62
+ /**
63
+ * A shadow on skin, one channel at scatter distance `d`: the shadow's own value raised to
64
+ * `1 / (1 + d / width)`, so light reaches further into the penumbra the further that channel
65
+ * travels beneath the surface — red furthest, which is the red edge a shadow on skin has — and a
66
+ * shadow stays whole at 0 and absent at 1. **What it gives up** is the profile itself: Penner
67
+ * convolves a sharpened shadow with it, which needs the penumbra's width in the world and hardens
68
+ * the narrow channels' edge where nothing widened the map to make room.
69
+ */
70
+ export function skinPenumbra(shadow, d) {
71
+ return Math.max(shadow, 0) ** (1 / (1 + d / SKIN_PENUMBRA_WIDTH));
72
+ }
73
+ /**
74
+ * Light through a thin part, one channel: `transmission × exp(−thickness / d)` of the light behind,
75
+ * by how squarely it is behind. A part as thick as its curvature's diameter — an ear's rim at a few
76
+ * millimetres, a cheek at ten centimetres — passes red through the one and nothing through the other.
77
+ */
78
+ export function skinTransmission(backNdl, thickness, d, transmission) {
79
+ return transmission * Math.exp(-thickness / Math.max(d, 1e-6)) * Math.max(backNdl, 0);
80
+ }
@@ -0,0 +1,26 @@
1
+ import type { SkinnedClothSetup } from '@driftengine/physics';
2
+ import type { ClothParticlesHandle, RendererApi } from './backend/api.ts';
3
+ /** A garment a renderer solves, from `createSkinnedCloth`. */
4
+ export interface SkinnedClothSolver {
5
+ /** What `setCloth(binding, particles)` draws the garment's bound meshes by. */
6
+ readonly particles: ClothParticlesHandle;
7
+ /** Where the solver runs: on the device, or on the CPU beneath a backend without compute. */
8
+ readonly runsOn: 'device' | 'cpu';
9
+ /**
10
+ * One frame: the rig's pose — each joint's global matrix, model space, sixteen floats a joint,
11
+ * and the model matrix — and `dt` seconds, run as whole fixed steps; the particles at the frame's
12
+ * `alpha` are then what the frame draws. **Once a frame, before the draws**, as
13
+ * `updateClothParticles` is: a second in one frame would reach every draw of it on WebGPU.
14
+ */
15
+ step(globals: Float32Array, model: Float32Array, dt: number): void;
16
+ /**
17
+ * The air the cloth drags toward from the next step, world space, metres a second: the frame's
18
+ * sample of the scene's one `WindField`, so a garment moves with everything else in the air.
19
+ */
20
+ setWind(x: number, y: number, z: number): void;
21
+ /** Back to the skinned pose, settled, as a teleport resets it. */
22
+ reset(): void;
23
+ dispose(): void;
24
+ }
25
+ /** A garment for `renderer`, from a set-up checked by name. */
26
+ export declare function createSkinnedCloth(renderer: RendererApi, setup: SkinnedClothSetup): SkinnedClothSolver;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A skinned cloth for a renderer: `@driftengine/physics`' solver, run as compute on WebGPU — the
3
+ * particles never leave the device — and as `SkinnedCloth` on the CPU under WebGL2, which has no
4
+ * compute. One schedule, `ClothControl`, and the same kernels either way; what a frame draws is
5
+ * `particles`, through `setCloth(binding, cloth.particles)`.
6
+ *
7
+ * **A function rather than renderer methods, so a game pays for it only by importing it.** See
8
+ * `backend/webgpu/computeHost.ts` for the door it reaches the device through, and why.
9
+ *
10
+ * **The CPU fallback's cost**, measured on a desktop part: a few hundred particles are a
11
+ * millisecond a frame on the main thread, and 4,500 are twenty — a garment that size wants the
12
+ * device. The device solver ran 4,500 particles in 0.65 ms a frame, most of it the dispatches
13
+ * rather than the particles (`demo/dev/skinnedCloth.html?bench=1`).
14
+ */
15
+ import { SkinnedCloth } from '@driftengine/physics';
16
+ import { computeHostOf } from './backend/webgpu/computeHost.js';
17
+ import { GpuSkinnedCloth } from './backend/webgpu/gpuSkinnedCloth.js';
18
+ /** A garment for `renderer`, from a set-up checked by name. */
19
+ export function createSkinnedCloth(renderer, setup) {
20
+ const host = computeHostOf(renderer);
21
+ return host === null ? new CpuCloth(renderer, setup) : new DeviceCloth(host, setup);
22
+ }
23
+ class DeviceCloth {
24
+ host;
25
+ runsOn = 'device';
26
+ particles;
27
+ cloth;
28
+ constructor(host, setup) {
29
+ this.host = host;
30
+ this.cloth = new GpuSkinnedCloth(host.device(), setup);
31
+ this.particles = this.cloth.particles;
32
+ }
33
+ step(globals, model, dt) {
34
+ if (this.host.lost())
35
+ return;
36
+ this.cloth.advance(globals, model, dt, this.host.changeFrame());
37
+ }
38
+ setWind(x, y, z) {
39
+ this.cloth.setWind(x, y, z);
40
+ }
41
+ reset() {
42
+ if (this.host.lost())
43
+ return;
44
+ this.cloth.reset();
45
+ }
46
+ /** Its buffers at once — its work is submitted — and its textures with the frame's. */
47
+ dispose() {
48
+ this.cloth.dispose();
49
+ this.host.retire(this.cloth.particles.textures);
50
+ }
51
+ /** The particles where the last step left them: the parity check's door, waits on the device. */
52
+ read() {
53
+ return this.cloth.read();
54
+ }
55
+ }
56
+ class CpuCloth {
57
+ renderer;
58
+ runsOn = 'cpu';
59
+ particles;
60
+ cloth;
61
+ /** The frame's particles at `alpha`, before they go up. */
62
+ staging;
63
+ constructor(renderer, setup) {
64
+ this.renderer = renderer;
65
+ this.cloth = new SkinnedCloth(setup);
66
+ this.particles = renderer.createClothParticles(this.cloth.count);
67
+ this.staging = new Float32Array(this.cloth.count * 3);
68
+ }
69
+ step(globals, model, dt) {
70
+ this.cloth.setPose(globals, model);
71
+ this.cloth.advance(dt);
72
+ this.cloth.interpolate(this.cloth.alpha, this.staging);
73
+ this.renderer.updateClothParticles(this.particles, this.staging);
74
+ }
75
+ setWind(x, y, z) {
76
+ this.cloth.setWind(x, y, z);
77
+ }
78
+ reset() {
79
+ this.cloth.reset();
80
+ }
81
+ dispose() {
82
+ this.renderer.disposeClothParticles(this.particles);
83
+ }
84
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * A material's shading model: how its surface answers light, when the standard model is not what
3
+ * it is made of — brushed metal, hair, skin, an eye.
4
+ *
5
+ * **A frozen descriptor, made once by a factory, carrying a kind and that model's numbers.** The
6
+ * standard model is `null`, exactly today's shader, and a scene that never names a model compiles
7
+ * nothing of any of them: each is a pipeline constant of its own (`MODEL_ANISOTROPIC` and the rest
8
+ * in `shaders/flat/models.ts`), off in every pipeline but the ones a material asks for.
9
+ *
10
+ * **Two vectors of numbers a material, and no more.** The lit stage's eight-light build is 254 of
11
+ * the 256 uniform vectors an Adreno 740 offers, so a model's numbers are packed into
12
+ * `uModelParams[2]` — the last component of the second says whether a model map is bound — and
13
+ * every model fits. See `packModel` for each layout.
14
+ *
15
+ * **The eye's axis is the one number that moves.** It is the eye's forward direction in the world,
16
+ * which a turning head or a glance changes every frame; the lit stage has no room left for another
17
+ * varying (sixteen, WebGPU's ceiling), so the renderer turns the descriptor's mesh-local axis by
18
+ * the draw's model matrix and the eye's joint each draw, with `eyeAxisInWorld`.
19
+ */
20
+ import type { Vec3 } from '../math/color.ts';
21
+ /** Strands of brushed metal, satin or hair-like fibre: a highlight stretched across them. */
22
+ export interface AnisotropicModel {
23
+ readonly kind: 'anisotropic';
24
+ /** 0 to 1: how far the highlight is stretched along the tangent. */
25
+ readonly strength: number;
26
+ /** Radians, turning the stretch's direction from the tangent toward the bitangent. */
27
+ readonly rotation: number;
28
+ }
29
+ /**
30
+ * Hair: three lobes along the strand, which is the mesh's tangent, running from root to tip
31
+ * (`hairLobes.ts`). A model map's green turns each strand's tilt by up to half of it either way and
32
+ * its blue is occlusion.
33
+ */
34
+ export interface HairModel {
35
+ readonly kind: 'hair';
36
+ /**
37
+ * Radians the cuticle's scales tilt the strand's surface: positive moves the white highlight
38
+ * toward the root and the coloured one toward the tip. A card whose tangent runs tip to root
39
+ * takes it negative.
40
+ */
41
+ readonly shift: number;
42
+ /** 0 to 1: the light scattered through many strands, tinted by their colour. */
43
+ readonly scatter: number;
44
+ /** How strongly light from behind glows through: the transmitted lobe. */
45
+ readonly backlit: number;
46
+ }
47
+ /** Skin: light scattered beneath the surface, reddening its terminator and its thin parts. */
48
+ export interface SkinModel {
49
+ readonly kind: 'skin';
50
+ /** The colour light keeps travelling beneath the surface — the mean free path, as a colour. */
51
+ readonly scatterColor: Vec3;
52
+ /** Metres: how far light travels beneath it before it leaves. */
53
+ readonly radius: number;
54
+ /** 0 to 1: how much a light behind a thin part shows through it. */
55
+ readonly transmission: number;
56
+ /** Which of up to eight profiles the screen-space scattering blurs this skin by. */
57
+ readonly profile: number;
58
+ }
59
+ /** An eye: an iris seen through a cornea, which refracts it, under a mirror-smooth highlight. */
60
+ export interface EyeModel {
61
+ readonly kind: 'eye';
62
+ /** The iris's radius, in texture coordinates from the centre of the eye's map. */
63
+ readonly irisRadius: number;
64
+ /** Metres from the cornea down to the iris. */
65
+ readonly irisDepth: number;
66
+ /** The cornea's index of refraction. */
67
+ readonly ior: number;
68
+ /** How smooth the cornea's highlight is. */
69
+ readonly corneaRoughness: number;
70
+ /** The eye's forward direction, in the mesh's own space at its bind pose. */
71
+ readonly axis: Vec3;
72
+ /** The joint the eye turns with, or -1 for an eye placed by its model matrix alone. */
73
+ readonly joint: number;
74
+ }
75
+ /** Every model a material can name, by its `kind`: see `SurfaceMaterial.model`. */
76
+ export type SurfaceModel = AnisotropicModel | HairModel | SkinModel | EyeModel;
77
+ /** A model's name, which also picks its pipeline switch: `SURFACE_MODEL_SWITCH`. */
78
+ export type SurfaceModelKind = SurfaceModel['kind'];
79
+ /** Every kind, in the order the lit stage declares their switches. */
80
+ export declare const SURFACE_MODEL_KINDS: readonly SurfaceModelKind[];
81
+ /** The lit stage's switch for each kind. */
82
+ export declare const SURFACE_MODEL_SWITCH: Readonly<Record<SurfaceModelKind, string>>;
83
+ /** Floats a material's model takes: `uModelParams[2]`. */
84
+ export declare const MODEL_PARAM_FLOATS = 8;
85
+ /** Brushed metal and the like, after glTF's `KHR_materials_anisotropy`: a strength and a turn. */
86
+ export declare function anisotropicModel(options?: {
87
+ strength?: number;
88
+ rotation?: number;
89
+ }): AnisotropicModel;
90
+ /** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
91
+ export declare function hairModel(options?: {
92
+ shift?: number;
93
+ scatter?: number;
94
+ backlit?: number;
95
+ }): HairModel;
96
+ /** Skin. The defaults are a fair skin's: a red mean free path a little over a centimetre long. */
97
+ export declare function skinModel(options?: {
98
+ scatterColor?: Vec3;
99
+ radius?: number;
100
+ transmission?: number;
101
+ profile?: number;
102
+ }): SkinModel;
103
+ /** An eye. The defaults are a human eye's, its axis along the mesh's +Z. */
104
+ export declare function eyeModel(options?: {
105
+ irisRadius?: number;
106
+ irisDepth?: number;
107
+ ior?: number;
108
+ corneaRoughness?: number;
109
+ axis?: Vec3;
110
+ joint?: number;
111
+ }): EyeModel;
112
+ /**
113
+ * A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
114
+ * map is bound into the last. Zeros for the standard model. An eye's axis goes in with the
115
+ * descriptor's own until the draw turns it: see `eyeAxisInWorld`.
116
+ *
117
+ * - anisotropic: strength, cos and sin of the rotation · −, −, −, map
118
+ * - hair: shift, scatter, backlit, − · −, −, −, map
119
+ * - skin: the scatter colour, radius · transmission, profile, −, map
120
+ * - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
121
+ */
122
+ export declare function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array): void;
123
+ /**
124
+ * An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
125
+ * turned by its joint's palette entry, where it names one and a palette is set, and then by the
126
+ * draw's model matrix — the turn its vertices take, without the translation. Normalised, because
127
+ * a palette carries a joint's scale.
128
+ */
129
+ export declare function eyeAxisInWorld(model: EyeModel, placement: ArrayLike<number>, palette: ArrayLike<number> | null, out: Float32Array, at: number): void;
@@ -0,0 +1,152 @@
1
+ /** Every kind, in the order the lit stage declares their switches. */
2
+ export const SURFACE_MODEL_KINDS = [
3
+ 'anisotropic',
4
+ 'hair',
5
+ 'skin',
6
+ 'eye',
7
+ ];
8
+ /** The lit stage's switch for each kind. */
9
+ export const SURFACE_MODEL_SWITCH = {
10
+ anisotropic: 'MODEL_ANISOTROPIC',
11
+ hair: 'MODEL_HAIR',
12
+ skin: 'MODEL_SKIN',
13
+ eye: 'MODEL_EYE',
14
+ };
15
+ /** Floats a material's model takes: `uModelParams[2]`. */
16
+ export const MODEL_PARAM_FLOATS = 8;
17
+ function check(model, name, value, low, high) {
18
+ if (!Number.isFinite(value) || value < low || value > high) {
19
+ throw new Error(`${model}: ${name} must be from ${low} to ${high}, not ${String(value)}`);
20
+ }
21
+ return value;
22
+ }
23
+ function unit(model, name, v) {
24
+ const length = Math.hypot(v[0], v[1], v[2]);
25
+ if (!(length > 1e-6))
26
+ throw new Error(`${model}: ${name} must be a direction, not [${v.join(', ')}]`);
27
+ return [v[0] / length, v[1] / length, v[2] / length];
28
+ }
29
+ /** Brushed metal and the like, after glTF's `KHR_materials_anisotropy`: a strength and a turn. */
30
+ export function anisotropicModel(options = {}) {
31
+ return Object.freeze({
32
+ kind: 'anisotropic',
33
+ strength: check('anisotropicModel', 'strength', options.strength ?? 1, 0, 1),
34
+ rotation: check('anisotropicModel', 'rotation', options.rotation ?? 0, -2 * Math.PI, 2 * Math.PI),
35
+ });
36
+ }
37
+ /** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
38
+ export function hairModel(options = {}) {
39
+ return Object.freeze({
40
+ kind: 'hair',
41
+ shift: check('hairModel', 'shift', options.shift ?? (3 * Math.PI) / 180, -0.5, 0.5),
42
+ scatter: check('hairModel', 'scatter', options.scatter ?? 0.7, 0, 1),
43
+ backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
44
+ });
45
+ }
46
+ /** Skin. The defaults are a fair skin's: a red mean free path a little over a centimetre long. */
47
+ export function skinModel(options = {}) {
48
+ const color = options.scatterColor ?? [0.85, 0.35, 0.22];
49
+ for (let c = 0; c < 3; c++)
50
+ check('skinModel', 'scatterColor', color[c], 0, 1);
51
+ const profile = check('skinModel', 'profile', options.profile ?? 0, 0, 7);
52
+ if (!Number.isInteger(profile))
53
+ throw new Error(`skinModel: profile must be a whole number, not ${profile}`);
54
+ return Object.freeze({
55
+ kind: 'skin',
56
+ scatterColor: Object.freeze([color[0], color[1], color[2]]),
57
+ radius: check('skinModel', 'radius', options.radius ?? 0.012, 0, 0.1),
58
+ transmission: check('skinModel', 'transmission', options.transmission ?? 0.5, 0, 1),
59
+ profile,
60
+ });
61
+ }
62
+ /** An eye. The defaults are a human eye's, its axis along the mesh's +Z. */
63
+ export function eyeModel(options = {}) {
64
+ const joint = options.joint ?? -1;
65
+ if (!Number.isInteger(joint) || joint < -1) {
66
+ throw new Error(`eyeModel: joint must be a joint index or -1, not ${String(joint)}`);
67
+ }
68
+ return Object.freeze({
69
+ kind: 'eye',
70
+ irisRadius: check('eyeModel', 'irisRadius', options.irisRadius ?? 0.15, 0.001, 0.5),
71
+ irisDepth: check('eyeModel', 'irisDepth', options.irisDepth ?? 0.003, 0, 0.02),
72
+ ior: check('eyeModel', 'ior', options.ior ?? 1.336, 1, 2),
73
+ corneaRoughness: check('eyeModel', 'corneaRoughness', options.corneaRoughness ?? 0.02, 0, 1),
74
+ axis: Object.freeze(unit('eyeModel', 'axis', options.axis ?? [0, 0, 1])),
75
+ joint,
76
+ });
77
+ }
78
+ /**
79
+ * A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
80
+ * map is bound into the last. Zeros for the standard model. An eye's axis goes in with the
81
+ * descriptor's own until the draw turns it: see `eyeAxisInWorld`.
82
+ *
83
+ * - anisotropic: strength, cos and sin of the rotation · −, −, −, map
84
+ * - hair: shift, scatter, backlit, − · −, −, −, map
85
+ * - skin: the scatter colour, radius · transmission, profile, −, map
86
+ * - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
87
+ */
88
+ export function packModel(model, mapped, out) {
89
+ out.fill(0);
90
+ if (model === null)
91
+ return;
92
+ out[7] = mapped ? 1 : 0;
93
+ switch (model.kind) {
94
+ case 'anisotropic':
95
+ out[0] = model.strength;
96
+ out[1] = Math.cos(model.rotation);
97
+ out[2] = Math.sin(model.rotation);
98
+ return;
99
+ case 'hair':
100
+ out[0] = model.shift;
101
+ out[1] = model.scatter;
102
+ out[2] = model.backlit;
103
+ return;
104
+ case 'skin':
105
+ out[0] = model.scatterColor[0];
106
+ out[1] = model.scatterColor[1];
107
+ out[2] = model.scatterColor[2];
108
+ out[3] = model.radius;
109
+ out[4] = model.transmission;
110
+ out[5] = model.profile;
111
+ return;
112
+ case 'eye':
113
+ out[0] = model.irisRadius;
114
+ out[1] = model.irisDepth;
115
+ out[2] = model.ior;
116
+ out[3] = model.corneaRoughness;
117
+ out[4] = model.axis[0];
118
+ out[5] = model.axis[1];
119
+ out[6] = model.axis[2];
120
+ return;
121
+ }
122
+ }
123
+ /**
124
+ * An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
125
+ * turned by its joint's palette entry, where it names one and a palette is set, and then by the
126
+ * draw's model matrix — the turn its vertices take, without the translation. Normalised, because
127
+ * a palette carries a joint's scale.
128
+ */
129
+ export function eyeAxisInWorld(model, placement, palette, out, at) {
130
+ let [x, y, z] = model.axis;
131
+ const j = model.joint * 16;
132
+ if (palette !== null && model.joint >= 0 && j + 16 <= palette.length) {
133
+ const px = palette[j] * x + palette[j + 4] * y + palette[j + 8] * z;
134
+ const py = palette[j + 1] * x +
135
+ palette[j + 5] * y +
136
+ palette[j + 9] * z;
137
+ const pz = palette[j + 2] * x +
138
+ palette[j + 6] * y +
139
+ palette[j + 10] * z;
140
+ x = px;
141
+ y = py;
142
+ z = pz;
143
+ }
144
+ const m = placement;
145
+ const wx = m[0] * x + m[4] * y + m[8] * z;
146
+ const wy = m[1] * x + m[5] * y + m[9] * z;
147
+ const wz = m[2] * x + m[6] * y + m[10] * z;
148
+ const length = Math.hypot(wx, wy, wz) || 1;
149
+ out[at] = wx / length;
150
+ out[at + 1] = wy / length;
151
+ out[at + 2] = wz / length;
152
+ }
@@ -1,4 +1,17 @@
1
+ /**
2
+ * A GPU image the caller supplies, sampled by the flat shader as surface colour — or several, as
3
+ * the layers of one array.
4
+ *
5
+ * **Every surface texture is an array; a plain image is an array of one.** The lit pass has three of
6
+ * WebGL2's guaranteed sixteen texture units free, so a second set of array samplers beside the 2D
7
+ * ones was never affordable. Making the one set of samplers arrays costs no unit, and a one-layer
8
+ * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
+ * so a merged block wearing forty facades is one draw.
10
+ */
11
+ import type { SurfaceModel } from './surfaceModel.ts';
12
+ import type { CompressedTextureFormat, SurfaceSource } from './compressedSource.ts';
1
13
  import type { SurfaceLayerEffect } from './surfaceEffects.ts';
14
+ import type { CutoutMode } from './cutoutDither.ts';
2
15
  /**
3
16
  * How a texture behaves past its edges and between its texels.
4
17
  *
@@ -68,8 +81,20 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
68
81
  */
69
82
  uOffset?: number;
70
83
  vOffset?: number;
71
- /** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
84
+ /** Alpha below which a fragment is discarded. See `uCutout`. */
72
85
  cutout?: number;
86
+ /**
87
+ * How the cutout's edge is drawn: `'hard'`, the default and every cutout before 4.8.4, or
88
+ * `'dithered'`, which keeps a pixel by the share of it the texture covers so a strand of hair, a
89
+ * lash or a fringe of leaves has a soft edge instead of a stair of pixels.
90
+ *
91
+ * **The frame decides how a dithered edge is resolved**, because the material cannot know: under a
92
+ * temporal resolve (TAA or DriftTR) the pattern moves every frame and the resolve averages it;
93
+ * multisampled, the share goes to the GPU as alpha-to-coverage; with neither, the test is hard,
94
+ * since a dither nothing averages is grain. A translucent draw tests hard whatever this says. See
95
+ * `cutoutDither.ts`.
96
+ */
97
+ cutoutMode?: CutoutMode;
73
98
  /**
74
99
  * Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
75
100
  * without culling, and a back face is lit as its front, with the normal turned to the viewer.
@@ -110,6 +135,19 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
110
135
  * consumer who wants a bound map twice as bright asks here rather than rebuilding the mesh.
111
136
  */
112
137
  emissiveScale?: readonly [number, number, number];
138
+ /**
139
+ * How the surface answers light, when the standard model is not what it is made of: brushed
140
+ * metal, hair, skin, an eye — `anisotropicModel`, `hairModel`, `skinModel`, `eyeModel`. Null or
141
+ * absent is the standard model, exactly as before. **A pipeline of its own**, compiled the first
142
+ * time a draw asks, so a scene that names no model pays for none. See `surfaceModel.ts`.
143
+ */
144
+ model?: SurfaceModel | null;
145
+ /**
146
+ * The model's own channels, in the albedo's coordinates and layers; what each channel means is
147
+ * the model's to say. Absent, each model takes its own neutral. Sampled linear, like any map
148
+ * that is not a colour.
149
+ */
150
+ modelMap?: Texture | null;
113
151
  }
114
152
  export interface SurfaceTextureOptions {
115
153
  /**
@@ -204,12 +242,14 @@ export declare class SurfaceTexture {
204
242
  readonly layers: number;
205
243
  /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
206
244
  private effectsTable;
245
+ /** The block format a compressed texture holds, or null for one uploaded from images. */
246
+ private readonly blockFormat;
207
247
  /**
208
248
  * One image, or an array of images that must share one size (`layerSize` refuses otherwise).
209
- * Uploaded unflipped either way; see the constructor body for why.
249
+ * Uploaded unflipped either way; see the constructor body for why. Or blocks, uploaded as they are
250
+ * where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
210
251
  */
211
- constructor(gl: WebGL2RenderingContext, source: TexImageSource | readonly TexImageSource[], options?: SurfaceTextureOptions);
212
- /** Bind the effects table to `unit`, or `fallback` for a texture given none. */
252
+ constructor(gl: WebGL2RenderingContext, source: SurfaceSource | readonly SurfaceSource[], options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
213
253
  /** Whether this texture carries an effects table, which the lit program reads only once one exists. */
214
254
  get hasEffects(): boolean;
215
255
  bindEffects(gl: WebGL2RenderingContext, unit: number, fallback: WebGLTexture): void;