@driftengine/core 4.9.0 → 4.11.0

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 (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -40,11 +40,24 @@ export const TANGENT_FRAME_GLSL = `mat3 tangentFrame(vec3 n, vec3 worldPos, vec2
40
40
  * side and nothing else.
41
41
  *
42
42
  * The UV determinant flips with them, so folding its sign back in restores the invariance the
43
- * solve always had. Measured before and after on demo/dev/normal.html: the derived panel stood
43
+ * solve always had — through the orientation below, which flips with them too. Measured before and after on demo/dev/normal.html: the derived panel stood
44
44
  * 86 of 255 rms from the attribute panel on WebGPU and 8.8 on WebGL2; with this they agree.
45
45
  */
46
- float det = duv1.x * duv2.y - duv2.x * duv1.y;
47
- float handed = det < 0.0 ? -1.0 : 1.0;
46
+ /*
47
+ * **And the mapping's own handedness, which the determinant alone does not carry.** The closed
48
+ * form is the determinant times \`Pv × n\` and \`−Pu × n\`, which are \`Pu\` and \`Pv\` where the
49
+ * coordinates are right-handed about the normal and their negations where they are mirrored: a
50
+ * mirrored mapping came out turned half a circle, its normal map lit from the far side. The sign
51
+ * that puts both back is the determinant's times the mapping's handedness, and that product is
52
+ * the surface's orientation on screen, \`dp1 × dp2 = det · (Pu × Pv)\`, against the normal. It
53
+ * flips with \`dFdy\` as the determinant does, so the two backends stay agreed, and it equals the
54
+ * determinant's sign wherever a mapping is not mirrored. Found by a projection laying \`u\` along
55
+ * x and \`v\` along z on ground facing up, which is mirrored: on demo/dev/worldUv.html a normal
56
+ * map leaning along +u read 74.8 derived against 110.8 from the tangent attribute, the light
57
+ * from the side it leans toward, and 110.8 both after. Every published scene and the four panels
58
+ * of demo/dev/normal.html, none of them mirrored and derived, are unchanged to the pixel.
59
+ */
60
+ float handed = dot(cross(dp1, dp2), n) < 0.0 ? -1.0 : 1.0;
48
61
  /* One scale for both axes, so a stretched UV layout does not shear the frame it produces. */
49
62
  float invmax = inversesqrt(max(max(dot(dt, dt), dot(db, db)), 1e-12)) * handed;
50
63
  vec3 derivedT = dt * invmax;
@@ -18,6 +18,13 @@
18
18
  * 4.1 KB across five, so a `#if` here would buy nothing measurable and cost a variant axis.
19
19
  */
20
20
  export declare const CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection;\nuniform float uWindSpeed;\nuniform float uWindGust;\nuniform float uWindTime;\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase;\n";
21
+ /**
22
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
23
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
24
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
25
+ * has; the depth stage keeps the unmarked chunk, having no view block.
26
+ */
27
+ export declare const LIT_CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection; // wgsl:view\nuniform float uWindSpeed; // wgsl:view\nuniform float uWindGust; // wgsl:view\nuniform float uWindTime; // wgsl:view\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase; // wgsl:view\n";
21
28
  /**
22
29
  * Where the wind puts a vertex, given how much of it that vertex takes.
23
30
  *
@@ -37,6 +37,32 @@ uniform float uWindTime;
37
37
  /* Spatial frequency of the travelling gust, per metre. */
38
38
  uniform vec2 uWindSpatialPhase;
39
39
  `;
40
+ /**
41
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
42
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
43
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
44
+ * has; the depth stage keeps the unmarked chunk, having no view block.
45
+ */
46
+ export const LIT_CHANNEL_ATTRIBUTE = `
47
+ layout(location = 13) in vec4 aChannel;
48
+
49
+ /*
50
+ * The frame's wind, already converted by \`resolveScatterDeform\` — a normalised direction, metres
51
+ * of bend, a gust amplitude and a scaled clock.
52
+ *
53
+ * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A
54
+ * second conversion here would be a second wind however identical its inputs, and the failure is
55
+ * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the
56
+ * grass beneath it leans another, with no single element looking wrong. See "One wind, sampled
57
+ * once" in AGENTS.md.
58
+ */
59
+ uniform vec2 uWindDirection; // wgsl:view
60
+ uniform float uWindSpeed; // wgsl:view
61
+ uniform float uWindGust; // wgsl:view
62
+ uniform float uWindTime; // wgsl:view
63
+ /* Spatial frequency of the travelling gust, per metre. */
64
+ uniform vec2 uWindSpatialPhase; // wgsl:view
65
+ `;
40
66
  /**
41
67
  * Where the wind puts a vertex, given how much of it that vertex takes.
42
68
  *
@@ -22,5 +22,33 @@ export interface SkinnedClothSolver {
22
22
  reset(): void;
23
23
  dispose(): void;
24
24
  }
25
+ /** Garments a renderer solves together, from `createSkinnedClothSet`, each named by its index. */
26
+ export interface SkinnedClothSet {
27
+ /** Each garment's particles, in the order its set-up was given: what `setCloth` draws it by. */
28
+ readonly particles: readonly ClothParticlesHandle[];
29
+ /** Where the set runs: on the device, or on the CPU beneath a backend without compute. */
30
+ readonly runsOn: 'device' | 'cpu';
31
+ /**
32
+ * Garment `g`'s rig for the next `step`: each joint's global matrix, model space, sixteen floats a
33
+ * joint, and the model matrix. A garment not posed in a frame steps toward its last pose.
34
+ */
35
+ setPose(g: number, globals: Float32Array, model: Float32Array): void;
36
+ /** The air garment `g` drags toward from its next step: the frame's sample of the one wind. */
37
+ setWind(g: number, x: number, y: number, z: number): void;
38
+ /** Garment `g` back to its skinned pose, settled, as a teleport resets it. */
39
+ reset(g: number): void;
40
+ /**
41
+ * `dt` seconds for every garment, run as whole fixed steps; each garment's particles at its
42
+ * `alpha` are then what the frame draws. **Once a frame, before the draws**, as a solver's `step`.
43
+ */
44
+ step(dt: number): void;
45
+ dispose(): void;
46
+ }
25
47
  /** A garment for `renderer`, from a set-up checked by name. */
26
48
  export declare function createSkinnedCloth(renderer: RendererApi, setup: SkinnedClothSetup): SkinnedClothSolver;
49
+ /**
50
+ * Garments for `renderer` stepped together, from set-ups checked by name: one pass, one submit and
51
+ * one garment's dispatches a step on the device, whatever the count. They must agree on `step`,
52
+ * `substeps`, `iterations` and `maxSteps`, and are refused by the first they disagree on.
53
+ */
54
+ export declare function createSkinnedClothSet(renderer: RendererApi, setups: readonly SkinnedClothSetup[]): SkinnedClothSet;
@@ -11,74 +11,142 @@
11
11
  * millisecond a frame on the main thread, and 4,500 are twenty — a garment that size wants the
12
12
  * device. The device solver ran 4,500 particles in 0.65 ms a frame, most of it the dispatches
13
13
  * rather than the particles (`demo/dev/skinnedCloth.html?bench=1`).
14
+ *
15
+ * **Several garments step as one with `createSkinnedClothSet`**: one pass, one submit and one
16
+ * garment's dispatches a step for all of them, where a garment each was a pass, a submit and its
17
+ * own dispatches each. They must share a step, substeps, iterations and most steps a frame.
14
18
  */
15
19
  import { SkinnedCloth } from '@driftengine/physics';
16
20
  import { computeHostOf } from './backend/webgpu/computeHost.js';
17
- import { GpuSkinnedCloth } from './backend/webgpu/gpuSkinnedCloth.js';
21
+ import { GpuClothSet } from './backend/webgpu/gpuClothSet.js';
18
22
  /** A garment for `renderer`, from a set-up checked by name. */
19
23
  export function createSkinnedCloth(renderer, setup) {
20
24
  const host = computeHostOf(renderer);
21
- return host === null ? new CpuCloth(renderer, setup) : new DeviceCloth(host, setup);
25
+ const set = host === null ? new CpuClothSet(renderer, [setup]) : new DeviceClothSet(host, [setup]);
26
+ return new OneGarment(set);
27
+ }
28
+ /** A set of one, as a solver: what `createSkinnedCloth` hands back. */
29
+ class OneGarment {
30
+ set;
31
+ particles;
32
+ runsOn;
33
+ constructor(set) {
34
+ this.set = set;
35
+ this.particles = set.particles[0];
36
+ this.runsOn = set.runsOn;
37
+ }
38
+ step(globals, model, dt) {
39
+ this.set.setPose(0, globals, model);
40
+ this.set.step(dt);
41
+ }
42
+ setWind(x, y, z) {
43
+ this.set.setWind(0, x, y, z);
44
+ }
45
+ reset() {
46
+ this.set.reset(0);
47
+ }
48
+ dispose() {
49
+ this.set.dispose();
50
+ }
51
+ /** The particles where the last step left them: the parity check's door. */
52
+ read() {
53
+ return this.set.read(0);
54
+ }
22
55
  }
23
- class DeviceCloth {
56
+ /**
57
+ * Garments for `renderer` stepped together, from set-ups checked by name: one pass, one submit and
58
+ * one garment's dispatches a step on the device, whatever the count. They must agree on `step`,
59
+ * `substeps`, `iterations` and `maxSteps`, and are refused by the first they disagree on.
60
+ */
61
+ export function createSkinnedClothSet(renderer, setups) {
62
+ const host = computeHostOf(renderer);
63
+ return host === null ? new CpuClothSet(renderer, setups) : new DeviceClothSet(host, setups);
64
+ }
65
+ class DeviceClothSet {
24
66
  host;
25
67
  runsOn = 'device';
26
68
  particles;
27
- cloth;
28
- constructor(host, setup) {
69
+ set;
70
+ constructor(host, setups) {
29
71
  this.host = host;
30
- this.cloth = new GpuSkinnedCloth(host.device(), setup);
31
- this.particles = this.cloth.particles;
72
+ this.set = new GpuClothSet(host.device(), setups);
73
+ const particles = [];
74
+ for (let g = 0; g < this.set.size; g++)
75
+ particles.push(this.set.particles(g));
76
+ this.particles = particles;
32
77
  }
33
- step(globals, model, dt) {
78
+ setPose(g, globals, model) {
34
79
  if (this.host.lost())
35
80
  return;
36
- this.cloth.advance(globals, model, dt, this.host.changeFrame());
81
+ this.set.setPose(g, globals, model);
37
82
  }
38
- setWind(x, y, z) {
39
- this.cloth.setWind(x, y, z);
83
+ setWind(g, x, y, z) {
84
+ this.set.setWind(g, x, y, z);
40
85
  }
41
- reset() {
86
+ reset(g) {
87
+ if (this.host.lost())
88
+ return;
89
+ this.set.reset(g);
90
+ }
91
+ step(dt) {
42
92
  if (this.host.lost())
43
93
  return;
44
- this.cloth.reset();
94
+ this.set.step(dt, this.host.changeFrame());
45
95
  }
46
96
  /** Its buffers at once — its work is submitted — and its textures with the frame's. */
47
97
  dispose() {
48
- this.cloth.dispose();
49
- this.host.retire(this.cloth.particles.textures);
98
+ this.set.dispose();
99
+ for (let g = 0; g < this.set.size; g++)
100
+ this.host.retire(this.set.particles(g).textures);
50
101
  }
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();
102
+ /** Garment `g`'s particles where its last step left them: the parity check's, waits on the device. */
103
+ read(g) {
104
+ return this.set.read(g);
54
105
  }
55
106
  }
56
- class CpuCloth {
107
+ class CpuClothSet {
57
108
  renderer;
58
109
  runsOn = 'cpu';
59
110
  particles;
60
- cloth;
61
- /** The frame's particles at `alpha`, before they go up. */
111
+ cloths;
112
+ /** Each garment's particles at `alpha`, before they go up. */
62
113
  staging;
63
- constructor(renderer, setup) {
114
+ constructor(renderer, setups) {
64
115
  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);
116
+ this.cloths = setups.map((setup) => new SkinnedCloth(setup));
117
+ this.particles = this.cloths.map((cloth) => renderer.createClothParticles(cloth.count));
118
+ this.staging = this.cloths.map((cloth) => new Float32Array(cloth.count * 3));
68
119
  }
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);
120
+ setPose(g, globals, model) {
121
+ this.at(g).setPose(globals, model);
74
122
  }
75
- setWind(x, y, z) {
76
- this.cloth.setWind(x, y, z);
123
+ setWind(g, x, y, z) {
124
+ this.at(g).setWind(x, y, z);
77
125
  }
78
- reset() {
79
- this.cloth.reset();
126
+ reset(g) {
127
+ this.at(g).reset();
128
+ }
129
+ step(dt) {
130
+ for (let g = 0; g < this.cloths.length; g++) {
131
+ const cloth = this.cloths[g];
132
+ const staging = this.staging[g];
133
+ cloth.advance(dt);
134
+ cloth.interpolate(cloth.alpha, staging);
135
+ this.renderer.updateClothParticles(this.particles[g], staging);
136
+ }
80
137
  }
81
138
  dispose() {
82
- this.renderer.disposeClothParticles(this.particles);
139
+ for (const particles of this.particles)
140
+ this.renderer.disposeClothParticles(particles);
141
+ }
142
+ /** Garment `g`'s particles where its last step left them, as the device set reads its own. */
143
+ read(g) {
144
+ return Promise.resolve(new Float32Array(this.at(g).positions));
145
+ }
146
+ at(g) {
147
+ const cloth = this.cloths[g];
148
+ if (cloth === undefined)
149
+ throw new Error(`skinned cloth set: no garment ${g}`);
150
+ return cloth;
83
151
  }
84
152
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Draws that do not change between frames, captured once from an enumeration and replayed into any
3
+ * colour pass: what `createStaticDraws` keeps, and the one rule both backends replay it by.
4
+ *
5
+ * **Captured, not referenced.** An enumeration is a closure over the caller's state, written to
6
+ * allocate nothing, so it hands a sink its scratch: the matrix it fills for every entry and the
7
+ * count it rewrites for the next batch. A list kept across frames copies both when it is made, so
8
+ * what it draws is what the enumeration said then. Meshes, batches and materials are held by
9
+ * reference: they are the caller's, and disposing one a list still holds is the caller's error.
10
+ *
11
+ * **Rigid meshes and instanced batches only.** A skinned mesh is posed every frame and a scatter
12
+ * batch bends with every frame's wind, so neither can be kept, and both are refused by name rather
13
+ * than captured in the pose they happened to have. A caller draws them each frame as it always has.
14
+ *
15
+ * What a backend does with the list is its own: WebGL2 replays the entries every frame through
16
+ * the scene-caster path, and WebGPU records them once into a render bundle per view. See
17
+ * `RendererApi.createStaticDraws` for what a caller is promised, which is the same on both.
18
+ */
19
+ import type { InstancedHandle, MeshHandle } from './backend/api.ts';
20
+ import type { MeshInstances } from './instances.ts';
21
+ import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from './shadowCasters.ts';
22
+ /** A list from `createStaticDraws`. Opaque but for its size. */
23
+ export interface StaticDrawsHandle {
24
+ /** How many entries it holds: meshes and batches, a batch counting once. */
25
+ readonly draws: number;
26
+ }
27
+ /** One captured entry. */
28
+ export type StaticEntry = {
29
+ readonly kind: 'mesh';
30
+ readonly mesh: MeshHandle;
31
+ readonly model: Float32Array;
32
+ readonly material: SceneCasterMaterial;
33
+ } | {
34
+ readonly kind: 'instanced';
35
+ readonly batch: InstancedHandle;
36
+ readonly data: MeshInstances;
37
+ readonly material: SceneCasterMaterial;
38
+ };
39
+ /** What both backends hold: the entries, and whether the list has been disposed. */
40
+ export interface StaticDrawsList extends StaticDrawsHandle {
41
+ readonly entries: readonly StaticEntry[];
42
+ disposed: boolean;
43
+ }
44
+ /** Enumerate `casters` once into a list. Allocates: it runs when the list is made, not per frame. */
45
+ export declare function captureStaticDraws(casters: ShadowCasters): StaticDrawsList;
46
+ /** Hand every entry to `sink`, in the order it was captured. */
47
+ export declare function replayStaticDraws(list: StaticDrawsList, sink: ShadowCasterSink): void;
@@ -0,0 +1,43 @@
1
+ /** Enumerate `casters` once into a list. Allocates: it runs when the list is made, not per frame. */
2
+ export function captureStaticDraws(casters) {
3
+ const entries = [];
4
+ const sink = {
5
+ mesh: (mesh, model, material) => {
6
+ entries.push({
7
+ kind: 'mesh',
8
+ mesh,
9
+ model: new Float32Array(model),
10
+ material: material ?? null,
11
+ });
12
+ },
13
+ instanced: (batch, data, material) => {
14
+ /* The batch's placements are on the device already; the count is the caller's scratch. */
15
+ entries.push({ kind: 'instanced', batch, data: { ...data }, material: material ?? null });
16
+ },
17
+ skinnedMesh: () => {
18
+ throw new Error('createStaticDraws: a skinned mesh is posed every frame, so a list kept across frames ' +
19
+ 'cannot hold one. Draw it each frame, with drawMesh or drawSceneCasters.');
20
+ },
21
+ scatter: () => {
22
+ throw new Error('createStaticDraws: a scatter batch bends with every frame’s wind, so a list kept across ' +
23
+ 'frames cannot hold one. Draw it each frame with drawScatter.');
24
+ },
25
+ };
26
+ casters(sink);
27
+ return {
28
+ entries,
29
+ get draws() {
30
+ return entries.length;
31
+ },
32
+ disposed: false,
33
+ };
34
+ }
35
+ /** Hand every entry to `sink`, in the order it was captured. */
36
+ export function replayStaticDraws(list, sink) {
37
+ for (const entry of list.entries) {
38
+ if (entry.kind === 'mesh')
39
+ sink.mesh(entry.mesh, entry.model, entry.material);
40
+ else
41
+ sink.instanced?.(entry.batch, entry.data, entry.material);
42
+ }
43
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
3
+ * `shaders/flat/layered.ts` reads it from.
4
+ */
5
+ /** The most layers a material blends: a base and one for each of the mask's four channels. */
6
+ export declare const MAX_SURFACE_LAYERS = 5;
7
+ /**
8
+ * Up to five layers, each its own colour, normal and roughness at its own repeat, blended by a mask.
9
+ *
10
+ * **Layer `i` is layer `i` of the material's arrays**: `albedo`, `normal` and `orm` each an array
11
+ * whose layers are the material's layers in order, read at the mesh's coordinates times
12
+ * `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
13
+ * **The mask** is read at the mesh's own coordinates and lays each layer over those before it: red
14
+ * lays layer 1 over the base, green layer 2 over that, blue layer 3, alpha layer 4. Its layer 0 is
15
+ * read, from where a shading model's map goes, which is why a layered material carries no
16
+ * `modelMap`. `emissiveLayer` gives the material's emissive map to one layer, glowing where that
17
+ * layer shows and nowhere it is covered; absent, the map glows as on any material.
18
+ *
19
+ * **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
20
+ * layers are fifteen reads where one material is three; layers share an array's size and format; a
21
+ * cutout reads the base layer's alpha; and the mask is sampled linear, so it is uploaded as data
22
+ * (`colorSpace: 'linear'`).
23
+ */
24
+ export interface SurfaceLayers<Texture> {
25
+ /** The weights, red to alpha laying layers 1 to 4, read at the mesh's coordinates. */
26
+ readonly mask: Texture;
27
+ /** Each layer's repeats across the mesh's coordinates, base first: one to five of them. */
28
+ readonly repeats: readonly number[];
29
+ /** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
30
+ readonly emissiveLayer?: number;
31
+ }
32
+ /** Floats `packSurfaceLayers` writes: two vectors. */
33
+ export declare const LAYER_FLOATS = 8;
34
+ /**
35
+ * `layers` into `out`: the five repeats (1 for a layer not given), then how many layers there are
36
+ * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
37
+ * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
38
+ */
39
+ export declare function packSurfaceLayers<Texture>(layers: SurfaceLayers<Texture> | null | undefined, out: Float32Array): void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
3
+ * `shaders/flat/layered.ts` reads it from.
4
+ */
5
+ /** The most layers a material blends: a base and one for each of the mask's four channels. */
6
+ export const MAX_SURFACE_LAYERS = 5;
7
+ /** Floats `packSurfaceLayers` writes: two vectors. */
8
+ export const LAYER_FLOATS = 8;
9
+ /**
10
+ * `layers` into `out`: the five repeats (1 for a layer not given), then how many layers there are
11
+ * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
12
+ * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
13
+ */
14
+ export function packSurfaceLayers(layers, out) {
15
+ out.fill(0);
16
+ if (layers === null || layers === undefined || layers.repeats.length === 0)
17
+ return;
18
+ const count = Math.min(layers.repeats.length, MAX_SURFACE_LAYERS);
19
+ for (let i = 0; i < MAX_SURFACE_LAYERS; i++) {
20
+ const repeat = i < count ? layers.repeats[i] : 1;
21
+ out[i] = Number.isFinite(repeat) && repeat > 0 ? repeat : 1;
22
+ }
23
+ out[5] = count;
24
+ const glow = layers.emissiveLayer;
25
+ out[6] = glow !== undefined && Number.isInteger(glow) && glow >= 0 && glow < count ? glow : -1;
26
+ }
@@ -27,12 +27,24 @@ export interface OverlayRegion {
27
27
  * raised to a contrast, times a noise laid in screen space, a slow pulse and a mask, in a colour.
28
28
  * Added to what the surface emits, and not gated on the night as emission is: it is a glow the
29
29
  * surface wears, not light it gives off.
30
+ *
31
+ * **Or, with `mode: 'blend'`, a colour the surface is pulled toward**, lit like the rest of it: the
32
+ * base colour mixed toward the rim's by the edge times `alpha`, before any light reads it, so the
33
+ * rim is bright where light falls and dark where none does. The colour is `colour` times
34
+ * `intensity`, each channel held at 1 as a base colour is, so a colour of 8 red is a red of 1 and
35
+ * not a glow; `alpha` is the weight, which may pass 1 to saturate the edge sooner.
30
36
  */
31
37
  export interface SurfaceRim {
32
38
  readonly colour: Vec3;
33
- /** How bright, 0 for none. Multiplied with `alpha`. */
39
+ /**
40
+ * How bright, 0 for none, multiplied with `alpha`. Where the rim blends it scales `colour`
41
+ * instead, so 0 there is a rim of black, and `alpha` alone says how much: 0 for none.
42
+ */
34
43
  readonly intensity: number;
44
+ /** The rim's weight, 1 unless given. */
35
45
  readonly alpha?: number;
46
+ /** `'add'`, the default, lays the rim on as glow; `'blend'` pulls the base colour toward it, lit. */
47
+ readonly mode?: 'add' | 'blend';
36
48
  /** The edge's exponent: larger keeps the rim nearer the silhouette. 1.5 unless given. */
37
49
  readonly falloff?: number;
38
50
  /**
@@ -118,10 +130,11 @@ export declare const OVERLAY_FLOATS: number;
118
130
  * no overlay. A region with a zero scale is an image not given, and its term reads as none — a
119
131
  * noise of 1, a mask of 0, no wrinkle — and every region is one without `maps`. Fifteen vectors:
120
132
  *
121
- * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim)
133
+ * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim). A blending rim: the colour
134
+ * times intensity, each channel held at 1, and the alpha alone
122
135
  * - 1: falloff, upward lean, contrast, mask weight
123
136
  * - 2: the rim's noise region; 3: its scroll along x and y, its tiling, the pulse's rate
124
- * - 4: the pulse's low and high (1 and 1 where none)
137
+ * - 4: the pulse's low and high (1 and 1 where none), and the mode: 0 adds, 1 blends
125
138
  * - 5: the rim's mask region
126
139
  * - 6: the dissolve's noise region (a zero scale is no dissolve); 7: its tiling, threshold, edge
127
140
  * - 8: the edge's light and its strength; 9: the laid-over colour and its amount
@@ -14,10 +14,11 @@ function region(out, at, r) {
14
14
  * no overlay. A region with a zero scale is an image not given, and its term reads as none — a
15
15
  * noise of 1, a mask of 0, no wrinkle — and every region is one without `maps`. Fifteen vectors:
16
16
  *
17
- * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim)
17
+ * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim). A blending rim: the colour
18
+ * times intensity, each channel held at 1, and the alpha alone
18
19
  * - 1: falloff, upward lean, contrast, mask weight
19
20
  * - 2: the rim's noise region; 3: its scroll along x and y, its tiling, the pulse's rate
20
- * - 4: the pulse's low and high (1 and 1 where none)
21
+ * - 4: the pulse's low and high (1 and 1 where none), and the mode: 0 adds, 1 blends
21
22
  * - 5: the rim's mask region
22
23
  * - 6: the dissolve's noise region (a zero scale is no dissolve); 7: its tiling, threshold, edge
23
24
  * - 8: the edge's light and its strength; 9: the laid-over colour and its amount
@@ -31,10 +32,14 @@ export function packSurfaceOverlay(overlay, out) {
31
32
  const images = overlay.maps !== undefined;
32
33
  const rim = overlay.rim;
33
34
  if (rim !== undefined) {
34
- out[0] = finite(rim.colour[0], 0);
35
- out[1] = finite(rim.colour[1], 0);
36
- out[2] = finite(rim.colour[2], 0);
37
- out[3] = Math.max(0, finite(rim.intensity, 0) * finite(rim.alpha, 1));
35
+ const blend = rim.mode === 'blend';
36
+ const scale = blend ? finite(rim.intensity, 0) : 1;
37
+ for (let c = 0; c < 3; c++) {
38
+ const channel = finite(rim.colour[c], 0) * scale;
39
+ out[c] = blend ? Math.min(Math.max(channel, 0), 1) : channel;
40
+ }
41
+ out[3] = Math.max(0, (blend ? 1 : finite(rim.intensity, 0)) * finite(rim.alpha, 1));
42
+ out[18] = blend ? 1 : 0;
38
43
  out[4] = Math.max(0, finite(rim.falloff, 1.5));
39
44
  out[5] = Math.max(0, finite(rim.upward, 0.4));
40
45
  out[6] = Math.max(0, finite(rim.contrast, 1));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world: `SurfaceMaterial.projection`, and the
3
+ * one vector `shaders/flat/worldUv.ts` reads it from.
4
+ */
5
+ /**
6
+ * Texture coordinates from the world rather than from the mesh.
7
+ *
8
+ * **`'planar'`** lays the maps across the two horizontal axes, `x` and `z`: ground whose mesh
9
+ * carries flat coordinates, or none worth reading, shows its texture at `scale` repeats a metre.
10
+ * **`'triplanar'`** reads each map on the three planes across `x`, `y` and `z` and blends them by
11
+ * how squarely the surface faces each, the normal's components raised to `sharpness`, so walls and
12
+ * rock wear a texture without stretching. See `worldUv.ts` for the normal map's blend.
13
+ *
14
+ * **What it gives up**: the world's coordinates do not move with a mesh, so a moving mesh slides
15
+ * under its texture; triplanar reads every map three times; and the material's `uScale`, `vScale`
16
+ * and offsets, which are the mesh's coordinates', do not apply.
17
+ */
18
+ export interface SurfaceProjection {
19
+ readonly kind: 'planar' | 'triplanar';
20
+ /** Repeats a metre: 2 lays a texture every half metre. */
21
+ readonly scale: number;
22
+ /** How sharply triplanar's planes meet: the exponent on the normal. 4 unless given. */
23
+ readonly sharpness?: number;
24
+ }
25
+ /** Floats `packSurfaceProjection` writes: one vector. */
26
+ export declare const PROJECTION_FLOATS = 4;
27
+ /**
28
+ * `projection` into `out`: its kind (0 none, 1 planar, 2 triplanar), its repeats a metre and its
29
+ * sharpness; zeros for none. A scale that is not finite and above zero projects nothing, so a
30
+ * material is never drawn at a scale that collapses its texture to a point.
31
+ */
32
+ export declare function packSurfaceProjection(projection: SurfaceProjection | null | undefined, out: Float32Array): void;
33
+ /** Whether a material projects its maps at all. */
34
+ export declare function projects(projection: SurfaceProjection | null | undefined): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world: `SurfaceMaterial.projection`, and the
3
+ * one vector `shaders/flat/worldUv.ts` reads it from.
4
+ */
5
+ /** Floats `packSurfaceProjection` writes: one vector. */
6
+ export const PROJECTION_FLOATS = 4;
7
+ /**
8
+ * `projection` into `out`: its kind (0 none, 1 planar, 2 triplanar), its repeats a metre and its
9
+ * sharpness; zeros for none. A scale that is not finite and above zero projects nothing, so a
10
+ * material is never drawn at a scale that collapses its texture to a point.
11
+ */
12
+ export function packSurfaceProjection(projection, out) {
13
+ out.fill(0);
14
+ if (projection === null || projection === undefined)
15
+ return;
16
+ const scale = projection.scale;
17
+ if (!Number.isFinite(scale) || scale <= 0)
18
+ return;
19
+ out[0] = projection.kind === 'triplanar' ? 2 : 1;
20
+ out[1] = scale;
21
+ const sharpness = projection.sharpness ?? 4;
22
+ out[2] = Number.isFinite(sharpness) ? Math.min(Math.max(sharpness, 1), 64) : 4;
23
+ }
24
+ /** Whether a material projects its maps at all. */
25
+ export function projects(projection) {
26
+ return (projection !== null &&
27
+ projection !== undefined &&
28
+ Number.isFinite(projection.scale) &&
29
+ projection.scale > 0);
30
+ }