@driftengine/core 4.8.2 → 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 (259) hide show
  1. package/CHANGELOG.json +127 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +11 -4
  5. package/dist/index.js +22 -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/materialChanges.d.ts +7 -2
  11. package/dist/render/backend/materialChanges.js +8 -3
  12. package/dist/render/backend/select.js +8 -0
  13. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  14. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  15. package/dist/render/backend/webgl2/renderer.d.ts +159 -3
  16. package/dist/render/backend/webgl2/renderer.js +662 -80
  17. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  18. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  19. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  20. package/dist/render/backend/webgpu/buffers.js +98 -49
  21. package/dist/render/backend/webgpu/causticsPass.js +2 -1
  22. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  23. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  24. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  25. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  26. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  27. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  28. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  29. package/dist/render/backend/webgpu/computeHost.js +5 -0
  30. package/dist/render/backend/webgpu/depthPass.d.ts +29 -9
  31. package/dist/render/backend/webgpu/depthPass.js +43 -12
  32. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  33. package/dist/render/backend/webgpu/filmPass.js +2 -1
  34. package/dist/render/backend/webgpu/flatPass.d.ts +43 -4
  35. package/dist/render/backend/webgpu/flatPass.js +111 -13
  36. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  37. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  38. package/dist/render/backend/webgpu/linePass.js +2 -1
  39. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  40. package/dist/render/backend/webgpu/motionPass.js +106 -9
  41. package/dist/render/backend/webgpu/panelPass.js +2 -1
  42. package/dist/render/backend/webgpu/particlePass.js +2 -1
  43. package/dist/render/backend/webgpu/plumePass.js +2 -1
  44. package/dist/render/backend/webgpu/postPass.d.ts +15 -3
  45. package/dist/render/backend/webgpu/postPass.js +25 -8
  46. package/dist/render/backend/webgpu/renderer.d.ts +123 -6
  47. package/dist/render/backend/webgpu/renderer.js +534 -58
  48. package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
  49. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  50. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  51. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  52. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  55. package/dist/render/backend/webgpu/textPass.js +2 -1
  56. package/dist/render/backend/webgpu/waterPass.js +2 -1
  57. package/dist/render/backend/webgpu/windStreakPass.js +2 -1
  58. package/dist/render/boltBatch.js +2 -1
  59. package/dist/render/causticsRenderer.js +2 -1
  60. package/dist/render/clothBindingData.d.ts +41 -0
  61. package/dist/render/clothBindingData.js +72 -0
  62. package/dist/render/clothPlacement.d.ts +25 -0
  63. package/dist/render/clothPlacement.js +85 -0
  64. package/dist/render/compressedSource.d.ts +66 -0
  65. package/dist/render/compressedSource.js +138 -0
  66. package/dist/render/cutoutCaster.d.ts +5 -1
  67. package/dist/render/cutoutCaster.js +6 -0
  68. package/dist/render/cutoutDither.d.ts +51 -0
  69. package/dist/render/cutoutDither.js +90 -0
  70. package/dist/render/decalPass.js +2 -1
  71. package/dist/render/eyeRefraction.d.ts +35 -0
  72. package/dist/render/eyeRefraction.js +40 -0
  73. package/dist/render/glCompressed.d.ts +27 -0
  74. package/dist/render/glCompressed.js +53 -0
  75. package/dist/render/globalMediumPass.js +3 -2
  76. package/dist/render/hairLobes.d.ts +70 -0
  77. package/dist/render/hairLobes.js +120 -0
  78. package/dist/render/lightBudget.d.ts +22 -0
  79. package/dist/render/lightBudget.js +22 -0
  80. package/dist/render/lineBatch.js +5 -1
  81. package/dist/render/mesh.d.ts +7 -0
  82. package/dist/render/mesh.js +24 -0
  83. package/dist/render/oitPass.js +2 -1
  84. package/dist/render/particleBatch.js +3 -2
  85. package/dist/render/particlePool.d.ts +6 -2
  86. package/dist/render/plumeRenderer.js +3 -2
  87. package/dist/render/recon/motionVectors.d.ts +4 -2
  88. package/dist/render/recon/motionVectors.js +6 -3
  89. package/dist/render/renderQuality.d.ts +29 -2
  90. package/dist/render/renderQuality.js +5 -0
  91. package/dist/render/rendererHarness.js +4 -0
  92. package/dist/render/sceneCoverage.d.ts +54 -0
  93. package/dist/render/sceneCoverage.js +72 -0
  94. package/dist/render/sceneTarget.d.ts +12 -0
  95. package/dist/render/sceneTarget.js +14 -0
  96. package/dist/render/sdfTextRenderer.js +2 -1
  97. package/dist/render/shaders/clothBinding.d.ts +22 -0
  98. package/dist/render/shaders/clothBinding.js +80 -0
  99. package/dist/render/shaders/depth.js +45 -8
  100. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  101. package/dist/render/shaders/flat/anisotropic.js +68 -0
  102. package/dist/render/shaders/flat/eye.d.ts +1 -0
  103. package/dist/render/shaders/flat/eye.js +81 -0
  104. package/dist/render/shaders/flat/hair.d.ts +1 -0
  105. package/dist/render/shaders/flat/hair.js +101 -0
  106. package/dist/render/shaders/flat/index.js +42 -3
  107. package/dist/render/shaders/flat/main.d.ts +1 -1
  108. package/dist/render/shaders/flat/main.js +113 -17
  109. package/dist/render/shaders/flat/models.d.ts +14 -0
  110. package/dist/render/shaders/flat/models.js +273 -0
  111. package/dist/render/shaders/flat/preamble.js +26 -24
  112. package/dist/render/shaders/flat/skin.d.ts +1 -0
  113. package/dist/render/shaders/flat/skin.js +114 -0
  114. package/dist/render/shaders/generated/depth.wgsl.d.ts +66 -24
  115. package/dist/render/shaders/generated/depth.wgsl.js +66 -24
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1815 -1704
  117. package/dist/render/shaders/generated/flat.wgsl.js +1853 -1742
  118. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
  121. package/dist/render/shaders/generated/particle.wgsl.js +3 -3
  122. package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/rush.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  127. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  128. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  130. package/dist/render/shaders/line.d.ts +1 -1
  131. package/dist/render/shaders/outputTransform.d.ts +14 -1
  132. package/dist/render/shaders/outputTransform.js +23 -1
  133. package/dist/render/shaders/particle.d.ts +3 -3
  134. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  135. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  136. package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
  137. package/dist/render/shaders/rush.js +13 -2
  138. package/dist/render/shaders/sdfText.d.ts +1 -1
  139. package/dist/render/shaders/skinBlur.d.ts +1 -0
  140. package/dist/render/shaders/skinBlur.js +103 -0
  141. package/dist/render/shaders/skinning.d.ts +3 -1
  142. package/dist/render/shaders/skinning.js +31 -4
  143. package/dist/render/shaders/temporalResolve.js +12 -5
  144. package/dist/render/shadowCasters.d.ts +13 -1
  145. package/dist/render/skinBlur.d.ts +41 -0
  146. package/dist/render/skinBlur.js +49 -0
  147. package/dist/render/skinScatter.d.ts +67 -0
  148. package/dist/render/skinScatter.js +80 -0
  149. package/dist/render/skinnedClothRun.d.ts +26 -0
  150. package/dist/render/skinnedClothRun.js +84 -0
  151. package/dist/render/ssrPass.js +2 -1
  152. package/dist/render/surfaceModel.d.ts +129 -0
  153. package/dist/render/surfaceModel.js +152 -0
  154. package/dist/render/surfaceTexture.d.ts +55 -4
  155. package/dist/render/surfaceTexture.js +60 -46
  156. package/dist/render/textRenderer.js +2 -1
  157. package/dist/render/vertexDefaults.js +8 -0
  158. package/dist/render/waterRenderer.js +2 -1
  159. package/dist/render/windStreakRenderer.js +2 -1
  160. package/package.json +6 -6
  161. package/src/geometry/meshConcat.ts +2 -0
  162. package/src/index.ts +53 -1
  163. package/src/math/scalar.ts +1 -1
  164. package/src/render/anisotropicLobe.ts +41 -0
  165. package/src/render/backend/api.ts +52 -3
  166. package/src/render/backend/materialChanges.ts +8 -3
  167. package/src/render/backend/select.ts +8 -0
  168. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  169. package/src/render/backend/webgl2/renderer.ts +750 -80
  170. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  171. package/src/render/backend/webgpu/buffers.ts +137 -63
  172. package/src/render/backend/webgpu/causticsPass.ts +2 -1
  173. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  174. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  175. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  176. package/src/render/backend/webgpu/computeHost.ts +25 -0
  177. package/src/render/backend/webgpu/depthPass.ts +43 -10
  178. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  179. package/src/render/backend/webgpu/filmPass.ts +2 -1
  180. package/src/render/backend/webgpu/flatPass.ts +147 -8
  181. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  182. package/src/render/backend/webgpu/linePass.ts +2 -1
  183. package/src/render/backend/webgpu/motionPass.ts +128 -9
  184. package/src/render/backend/webgpu/panelPass.ts +2 -1
  185. package/src/render/backend/webgpu/particlePass.ts +2 -1
  186. package/src/render/backend/webgpu/plumePass.ts +2 -1
  187. package/src/render/backend/webgpu/postPass.ts +25 -6
  188. package/src/render/backend/webgpu/renderer.ts +701 -52
  189. package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
  190. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  191. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  192. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  193. package/src/render/backend/webgpu/textPass.ts +2 -1
  194. package/src/render/backend/webgpu/waterPass.ts +2 -1
  195. package/src/render/backend/webgpu/windStreakPass.ts +2 -1
  196. package/src/render/boltBatch.ts +2 -1
  197. package/src/render/causticsRenderer.ts +2 -1
  198. package/src/render/clothBindingData.ts +89 -0
  199. package/src/render/clothPlacement.ts +114 -0
  200. package/src/render/compressedSource.ts +198 -0
  201. package/src/render/cutoutCaster.ts +20 -2
  202. package/src/render/cutoutDither.ts +99 -0
  203. package/src/render/decalPass.ts +2 -1
  204. package/src/render/eyeRefraction.ts +72 -0
  205. package/src/render/glCompressed.ts +86 -0
  206. package/src/render/globalMediumPass.ts +3 -2
  207. package/src/render/hairLobes.ts +188 -0
  208. package/src/render/lightBudget.ts +25 -0
  209. package/src/render/lineBatch.ts +3 -1
  210. package/src/render/mesh.ts +23 -0
  211. package/src/render/oitPass.ts +2 -1
  212. package/src/render/particleBatch.ts +3 -2
  213. package/src/render/particlePool.ts +6 -2
  214. package/src/render/plumeRenderer.ts +3 -2
  215. package/src/render/recon/motionVectors.ts +6 -3
  216. package/src/render/renderQuality.ts +37 -2
  217. package/src/render/rendererHarness.ts +4 -0
  218. package/src/render/sceneCoverage.ts +78 -0
  219. package/src/render/sceneTarget.ts +21 -0
  220. package/src/render/sdfTextRenderer.ts +2 -1
  221. package/src/render/shaders/clothBinding.ts +81 -0
  222. package/src/render/shaders/depth.ts +45 -8
  223. package/src/render/shaders/flat/anisotropic.ts +68 -0
  224. package/src/render/shaders/flat/eye.ts +82 -0
  225. package/src/render/shaders/flat/hair.ts +102 -0
  226. package/src/render/shaders/flat/index.ts +42 -3
  227. package/src/render/shaders/flat/main.ts +113 -17
  228. package/src/render/shaders/flat/models.ts +286 -0
  229. package/src/render/shaders/flat/preamble.ts +26 -24
  230. package/src/render/shaders/flat/skin.ts +116 -0
  231. package/src/render/shaders/generated/depth.wgsl.ts +66 -24
  232. package/src/render/shaders/generated/flat.wgsl.ts +1853 -1742
  233. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  234. package/src/render/shaders/generated/particle.wgsl.ts +3 -3
  235. package/src/render/shaders/generated/rush.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  238. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  239. package/src/render/shaders/outputTransform.ts +24 -1
  240. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  241. package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
  242. package/src/render/shaders/rush.ts +13 -2
  243. package/src/render/shaders/skinBlur.ts +105 -0
  244. package/src/render/shaders/skinning.ts +31 -4
  245. package/src/render/shaders/temporalResolve.ts +12 -5
  246. package/src/render/shadowCasters.ts +13 -0
  247. package/src/render/skinBlur.ts +53 -0
  248. package/src/render/skinScatter.ts +92 -0
  249. package/src/render/skinnedClothRun.ts +128 -0
  250. package/src/render/ssrPass.ts +2 -1
  251. package/src/render/surfaceModel.ts +274 -0
  252. package/src/render/surfaceTexture.ts +134 -63
  253. package/src/render/textRenderer.ts +2 -1
  254. package/src/render/vertexDefaults.ts +8 -0
  255. package/src/render/waterRenderer.ts +2 -1
  256. package/src/render/windStreakRenderer.ts +2 -1
  257. package/dist/math/exact.d.ts +0 -76
  258. package/dist/math/exact.js +0 -327
  259. package/src/math/exact.ts +0 -340
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The hair model's lobes, as `shaders/flat/hair.ts` computes them: the reference its tests hold the
3
+ * arithmetic to, and nothing at run time calls it.
4
+ *
5
+ * **A strand is a rough dielectric cylinder, index 1.55, after Marschner et al. 2003 in the
6
+ * real-time form Karis gave in 2016.** Light leaves it three ways, each a longitudinal Gaussian in
7
+ * `sinθi + sinθo` — the angles of light and eye out of the plane normal to the strand — times an
8
+ * azimuthal term in φ, the turn between them around it:
9
+ *
10
+ * - **R**, reflected off the surface: white, and shifted toward the root by twice the tilt of the
11
+ * cuticle's scales, which point toward the tip;
12
+ * - **TT**, through the strand and out: the glow of a backlit head, shifted toward the tip by the
13
+ * tilt, half as wide, and tinted by the fibre for the path it took;
14
+ * - **TRT**, reflected once inside: the coloured second highlight, three tilts toward the tip and
15
+ * twice as wide;
16
+ *
17
+ * and a fourth, the light scattered through many strands, as a wrapped diffuse against a normal
18
+ * turned toward the eye across the strand, tinted by a root of the fibre's colour.
19
+ *
20
+ * **The tangent runs from root to tip**, and the tilt is positive for a strand authored that way. A
21
+ * card authored the other way round swaps its two highlights' ends, and takes a negative shift.
22
+ *
23
+ * **In this engine's light units.** A lamp's diffuse here is `albedo · cosθ` with no 1/π, so the
24
+ * published lobes, which are per unit irradiance, are multiplied by π; the scatter term's 1/π cancels.
25
+ * **What it gives up** is the fit's own: these are Karis's approximations to the azimuthal
26
+ * integrals, not the integrals, and they do not conserve energy exactly — the tests hold the sum
27
+ * under what a strand receives.
28
+ */
29
+ import type { Vec3 } from '../math/color.ts';
30
+
31
+ /** Schlick's base for a strand at index 1.55: `((1 − n) / (1 + n))²`. */
32
+ export const HAIR_F0 = ((1 - 1.55) / (1 + 1.55)) ** 2;
33
+
34
+ /**
35
+ * The narrowest longitudinal width a strand is drawn at, in radians.
36
+ *
37
+ * **Below every measured fibre**: Marschner gives the reflected lobe 5° to 10°, and this is about
38
+ * 3°, so no real hair is clamped. What it stops is a roughness of 0 making a Gaussian narrower than
39
+ * a pixel can sample — the speckle `MIN_LOBE_ALPHA` argues against for the standard lobe.
40
+ */
41
+ export const HAIR_MIN_BETA = 0.05;
42
+
43
+ /**
44
+ * The scatter term's answer to light arriving evenly from every side, per unit of it: the term's
45
+ * shape integrated over the sphere and divided by π, which is how this engine's lamp units add up.
46
+ *
47
+ * Derived: the wrapped half `(N·L + 1) / 4` averages 1/4 over the sphere, `1 − |T·L|` averages 1/2,
48
+ * and the shape is `mix(wrapped, 1 − |T·L|, 0.33)`, so its mean is 0.67 · 0.25 + 0.33 · 0.5 = 0.3325
49
+ * and its integral 4π times that. Over π: **1.33**.
50
+ */
51
+ export const HAIR_SCATTER_SPHERE = 1.33;
52
+
53
+ /** How much the scatter term leans toward the strand's own direction rather than the pseudo-normal. */
54
+ const KAJIYA_SHARE = 0.33;
55
+
56
+ /** A longitudinal lobe: a normalised Gaussian of width `beta` at `x` from its centre. */
57
+ export function hairLongitudinal(beta: number, x: number): number {
58
+ return Math.exp((-0.5 * x * x) / (beta * beta)) / (Math.sqrt(2 * Math.PI) * beta);
59
+ }
60
+
61
+ /** Schlick's Fresnel for the strand at a cosine, clamped to the unit interval. */
62
+ export function hairFresnel(cosine: number): number {
63
+ const c = 1 - Math.min(Math.max(cosine, 0), 1);
64
+ return HAIR_F0 + (1 - HAIR_F0) * c * c * c * c * c;
65
+ }
66
+
67
+ function dot(a: ArrayLike<number>, b: ArrayLike<number>): number {
68
+ return (
69
+ (a[0] as number) * (b[0] as number) +
70
+ (a[1] as number) * (b[1] as number) +
71
+ (a[2] as number) * (b[2] as number)
72
+ );
73
+ }
74
+
75
+ const clamp = (x: number, low: number, high: number): number => Math.min(Math.max(x, low), high);
76
+
77
+ /** Where `hairLight` leaves its answer: R, then TT, TRT and the scatter, each a colour. */
78
+ export const HAIR_OUT = { r: 0, tt: 1, trt: 4, scatter: 7, length: 10 } as const;
79
+
80
+ /**
81
+ * One light on a strand, in lamp units and unshadowed, into `out` (`HAIR_OUT`'s layout: R is grey,
82
+ * the rest colours). `tangent`, `toLight` and `toEye` are unit vectors but for a tangent that could
83
+ * not be made one, which comes in as zeros; `fibre` is the base colour.
84
+ *
85
+ * **A lamp with a size widens every lobe by the angle it subtends**, `sourceRadius / 2·dist` as
86
+ * `sphereLobe` takes it, and the lobes are normalised Gaussians, so a wider one carries the same
87
+ * light rather than more. The sun passes zeros.
88
+ */
89
+ export function hairLight(
90
+ tangent: ArrayLike<number>,
91
+ toLight: ArrayLike<number>,
92
+ toEye: ArrayLike<number>,
93
+ fibre: Vec3,
94
+ roughness: number,
95
+ shift: number,
96
+ scatter: number,
97
+ backlit: number,
98
+ sourceRadius: number,
99
+ dist: number,
100
+ out: Float64Array,
101
+ ): void {
102
+ const sinI = clamp(dot(tangent, toLight), -1, 1);
103
+ const sinO = clamp(dot(tangent, toEye), -1, 1);
104
+ /*
105
+ * The half difference angle, rounded to single precision as the device holds it: at its limit,
106
+ * light and eye at opposite ends of the strand, that is π/2 rounded up, whose cosine is −4.4e-8
107
+ * rather than double precision's +6e-17 — and the transmitted powers divide by it. The floor is
108
+ * for the device's number, and the rounding is what lets a test here see it.
109
+ */
110
+ const cosD = Math.max(
111
+ Math.cos(Math.fround(0.5 * Math.abs(Math.asin(sinO) - Math.asin(sinI)))),
112
+ 1e-3,
113
+ );
114
+ let lp2 = 0;
115
+ let vp2 = 0;
116
+ let lpvp = 0;
117
+ for (let c = 0; c < 3; c++) {
118
+ const lp = (toLight[c] as number) - sinI * (tangent[c] as number);
119
+ const vp = (toEye[c] as number) - sinO * (tangent[c] as number);
120
+ lp2 += lp * lp;
121
+ vp2 += vp * vp;
122
+ lpvp += lp * vp;
123
+ }
124
+ const cosPhi = lpvp / Math.sqrt(lp2 * vp2 + 1e-4);
125
+ const cosHalfPhi = Math.sqrt(clamp(0.5 + 0.5 * cosPhi, 0, 1));
126
+ const beta = Math.max(roughness * roughness, HAIR_MIN_BETA);
127
+ const grow = sourceRadius / Math.max(2 * dist, 1e-3);
128
+ const x = sinI + sinO;
129
+
130
+ const r =
131
+ hairLongitudinal(beta + grow, x + 2 * shift) *
132
+ 0.25 *
133
+ cosHalfPhi *
134
+ hairFresnel(Math.sqrt(clamp(0.5 + 0.5 * dot(toLight, toEye), 0, 1)));
135
+
136
+ const a = 1 / (1.19 / cosD + 0.36 * cosD);
137
+ const h = cosHalfPhi * (1 + a * (0.6 - 0.8 * cosPhi));
138
+ const fTT = hairFresnel(cosD * Math.sqrt(clamp(1 - h * h, 0, 1)));
139
+ const tt =
140
+ hairLongitudinal(0.5 * beta + grow, x - shift) *
141
+ Math.exp(-3.65 * cosPhi - 3.98) *
142
+ (1 - fTT) *
143
+ (1 - fTT) *
144
+ backlit;
145
+ const ttPower = (0.5 * Math.sqrt(clamp(1 - h * h * a * a, 0, 1))) / cosD;
146
+
147
+ const fTRT = hairFresnel(0.5 * cosD);
148
+ const trt =
149
+ hairLongitudinal(2 * beta + grow, x - 3 * shift) *
150
+ Math.exp(17 * cosPhi - 16.78) *
151
+ (1 - fTRT) *
152
+ (1 - fTRT) *
153
+ fTRT;
154
+ const trtPower = 0.8 / cosD;
155
+
156
+ let across0 = (toEye[0] as number) - (tangent[0] as number) * sinO;
157
+ let across1 = (toEye[1] as number) - (tangent[1] as number) * sinO;
158
+ let across2 = (toEye[2] as number) - (tangent[2] as number) * sinO;
159
+ const acrossLength = Math.hypot(across0, across1, across2);
160
+ if (acrossLength > 1e-4) {
161
+ across0 /= acrossLength;
162
+ across1 /= acrossLength;
163
+ across2 /= acrossLength;
164
+ }
165
+ const wrapped = clamp(
166
+ (across0 * (toLight[0] as number) +
167
+ across1 * (toLight[1] as number) +
168
+ across2 * (toLight[2] as number) +
169
+ 1) /
170
+ 4,
171
+ 0,
172
+ 1,
173
+ );
174
+ const shape = wrapped + (1 - Math.abs(sinI) - wrapped) * KAJIYA_SHARE;
175
+
176
+ out[HAIR_OUT.r] = Math.PI * r;
177
+ for (let c = 0; c < 3; c++) {
178
+ /*
179
+ * Above zero, so the device's `pow` never forms the logarithm of zero: an infinity in the
180
+ * middle of an expression is something a shader compiler may assume never happens. Nothing in
181
+ * double precision can show the difference, so no test here does.
182
+ */
183
+ const colour = Math.max(fibre[c], 1e-4);
184
+ out[HAIR_OUT.tt + c] = Math.PI * tt * colour ** ttPower;
185
+ out[HAIR_OUT.trt + c] = Math.PI * trt * colour ** trtPower;
186
+ out[HAIR_OUT.scatter + c] = Math.sqrt(fibre[c]) * scatter * shape;
187
+ }
188
+ }
@@ -238,6 +238,31 @@ export const POINT_GLASS_TINT_TEXTURE_UNIT = SUN_GLASS_TINT_TEXTURE_UNIT + 1;
238
238
  */
239
239
  export const SURFACE_EFFECTS_TEXTURE_UNIT = POINT_GLASS_TINT_TEXTURE_UNIT + 1;
240
240
 
241
+ /**
242
+ * A cloth binding's three vertex-stage textures — the binding, the particles now, the particles at
243
+ * rest (`clothBinding.ts`). Vertex samplers, so they count against the vertex stage's own sixteen
244
+ * beside the palette and the morph deltas, not the lit stage's; numbered past every fragment unit,
245
+ * inside the combined pool of thirty-two.
246
+ */
247
+ export const CLOTH_BINDING_TEXTURE_UNIT = SURFACE_EFFECTS_TEXTURE_UNIT + 1;
248
+ export const CLOTH_PARTICLES_TEXTURE_UNIT = CLOTH_BINDING_TEXTURE_UNIT + 1;
249
+ export const CLOTH_REST_TEXTURE_UNIT = CLOTH_PARTICLES_TEXTURE_UNIT + 1;
250
+
251
+ /**
252
+ * A shading model's own channels (`shaders/flat/models.ts`), the lit stage's **sixteenth** fragment
253
+ * sampler — exactly what WebGL2 guarantees a stage, which `textureUnitBudget.test.ts` counts from
254
+ * the source. Numbered past the cloth's vertex units, inside the combined pool of thirty-two.
255
+ */
256
+ export const MODEL_MAP_TEXTURE_UNIT = CLOTH_REST_TEXTURE_UNIT + 1;
257
+
258
+ /**
259
+ * The two units skin's screen-space blur reads on WebGL2 — its target and the frame's depth — past
260
+ * every unit a lit program binds, so spreading skin in the middle of a frame unbinds nothing the
261
+ * draws after it sample. WebGL2 guarantees thirty-two.
262
+ */
263
+ export const SKIN_BLUR_TEXTURE_UNIT = MODEL_MAP_TEXTURE_UNIT + 1;
264
+ export const SKIN_BLUR_DEPTH_UNIT = SKIN_BLUR_TEXTURE_UNIT + 1;
265
+
241
266
  /**
242
267
  * A cookie's tile, in texels a side.
243
268
  *
@@ -2,6 +2,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
2
2
  import { LINE_FRAG, LINE_VERT } from './shaders/line.ts';
3
3
  import { buildSegmentQuads, expandLineSegments } from './segmentQuads.ts';
4
4
  import type { LineSegments } from './linePoints.ts';
5
+ import { blendCovering, blendKeeping } from './sceneCoverage.ts';
5
6
 
6
7
  /**
7
8
  * One draw call for a polyline with a real width — an audio waveform, a trail, a plotted
@@ -102,7 +103,8 @@ export class LineBatch {
102
103
  drawTo(gl: WebGL2RenderingContext, segmentCount: number, additive = false): void {
103
104
  if (segmentCount === 0) return;
104
105
  gl.enable(gl.BLEND);
105
- gl.blendFunc(gl.SRC_ALPHA, additive ? gl.ONE : gl.ONE_MINUS_SRC_ALPHA);
106
+ if (additive) blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
107
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
106
108
  gl.depthMask(false);
107
109
  // The quad billboards toward the view direction the same way a bolt's does — see
108
110
  // `line.ts`'s own `cross(dir, view)` — so its winding can flip with the camera exactly
@@ -57,6 +57,12 @@ const ATTR_JOINTS = 11;
57
57
  const ATTR_CHANNEL = 13;
58
58
 
59
59
  const ATTR_WEIGHTS = 12;
60
+ /*
61
+ * The second four influences, at the last two locations: a skinned draw spends 11, 12 and 13 and an
62
+ * instanced one cannot skin, so 14 and 15 are free exactly where they are read. See skinning.ts.
63
+ */
64
+ const ATTR_JOINTS2 = 14;
65
+ const ATTR_WEIGHTS2 = 15;
60
66
 
61
67
  /**
62
68
  * One absent attribute's constant, at the width the table gave it.
@@ -136,8 +142,15 @@ export class Mesh {
136
142
  readonly bounds: Bounds = createBounds();
137
143
  /** Whether this mesh carries a rig, which decides which flat program draws it. */
138
144
  readonly isSkinned: boolean;
145
+ /**
146
+ * Whether its rig moves a vertex by eight influences rather than four, which decides whether the
147
+ * skinned program it takes is built with `SKIN_EIGHT` on. See `skinning.ts`.
148
+ */
149
+ readonly isSkinnedEight: boolean;
139
150
  /** Whether it has texture coordinates, without which a material's maps read one texel. */
140
151
  readonly hasUvs: boolean;
152
+ /** How many vertices it has, which a cloth binding is checked against. */
153
+ readonly vertexCount: number;
141
154
 
142
155
  /**
143
156
  * This mesh's morph deltas, or null for geometry that does not deform.
@@ -276,6 +289,7 @@ export class Mesh {
276
289
  * takes; see `WebGL2Renderer.drawMesh`.
277
290
  */
278
291
  this.isSkinned = data.joints !== undefined;
292
+ this.vertexCount = data.positions.length / 3;
279
293
  this.morph =
280
294
  data.morphTargets === undefined || data.morphTargetCount === undefined
281
295
  ? null
@@ -287,6 +301,15 @@ export class Mesh {
287
301
  this.attachAttribute(gl, ATTR_JOINTS, data.joints, 4);
288
302
  this.attachAttribute(gl, ATTR_WEIGHTS, data.weights, 4);
289
303
  }
304
+ /* The fifth to eighth influences, or zeros, which add nothing. See `isSkinnedEight`. */
305
+ this.isSkinnedEight = this.isSkinned && data.joints2 !== undefined;
306
+ if (data.joints2 === undefined || data.weights2 === undefined) {
307
+ this.constants.push({ location: ATTR_JOINTS2, value: ABSENT_ATTRIBUTE['joints2'] });
308
+ this.constants.push({ location: ATTR_WEIGHTS2, value: ABSENT_ATTRIBUTE['weights2'] });
309
+ } else {
310
+ this.attachAttribute(gl, ATTR_JOINTS2, data.joints2, 4);
311
+ this.attachAttribute(gl, ATTR_WEIGHTS2, data.weights2, 4);
312
+ }
290
313
 
291
314
  this.hasTangents = data.tangents !== undefined;
292
315
  if (data.tangents === undefined) {
@@ -1,6 +1,7 @@
1
1
  import { compileProgram, uniformLocations } from './shader.ts';
2
2
  import { FULLSCREEN_VERT } from './shaders/fullscreen.ts';
3
3
  import { OIT_RESOLVE_FRAG } from './shaders/oitResolve.ts';
4
+ import { blendTransmitting } from './sceneCoverage.ts';
4
5
 
5
6
  /**
6
7
  * Order-independent transparency on WebGL2: two targets, two passes over the same geometry, and a
@@ -179,7 +180,7 @@ export class OitPass {
179
180
  gl.depthMask(false);
180
181
  gl.disable(gl.CULL_FACE);
181
182
  gl.enable(gl.BLEND);
182
- gl.blendFunc(gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);
183
+ blendTransmitting(gl, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);
183
184
 
184
185
  gl.useProgram(this.program);
185
186
  gl.activeTexture(gl.TEXTURE0);
@@ -2,6 +2,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
2
2
  import { particleShaders, type ParticleMaterial } from './particleMaterial.ts';
3
3
  import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from './shaders/particle.ts';
4
4
  import type { ParticleInstances } from './particlePool.ts';
5
+ import { blendCovering, blendKeeping } from './sceneCoverage.ts';
5
6
 
6
7
  /**
7
8
  * One draw call for a live particle pool: one camera-facing quad per particle, or a
@@ -239,8 +240,8 @@ export class ParticleBatch {
239
240
  drawTo(gl: WebGL2RenderingContext, count: number): void {
240
241
  if (count === 0) return;
241
242
  gl.enable(gl.BLEND);
242
- if (this.blend === 'additive') gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
243
- else gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
243
+ if (this.blend === 'additive') blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
244
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
244
245
  gl.depthMask(false);
245
246
  gl.disable(gl.CULL_FACE);
246
247
 
@@ -21,7 +21,11 @@ import type { Vec3 } from '../math/color.ts';
21
21
  export interface ParticleInstances {
22
22
  /** World position, three floats each. */
23
23
  positions: Float32Array;
24
- /** Metres across, and radians of roll about the particle's own axis. */
24
+ /**
25
+ * Metres from the centre to an edge, a half-width: the quad spans twice this. Then radians of
26
+ * roll about the particle's own axis. This said metres across until 4.8.3, and a caller who took
27
+ * it at its word drew every particle twice as wide as meant.
28
+ */
25
29
  sizes: Float32Array;
26
30
  spins: Float32Array;
27
31
  /** Linear colour, three floats each. */
@@ -72,7 +76,7 @@ export interface ParticlePoolOptions {
72
76
  capacity: number;
73
77
  /** Seconds a particle lives. */
74
78
  lifeSec: number;
75
- /** Size at birth and at death, metres. */
79
+ /** Size at birth and at death: metres from the centre to an edge, so the quad is twice as wide. */
76
80
  sizeStart: number;
77
81
  sizeEnd: number;
78
82
  /** Colour at birth and at death. */
@@ -6,6 +6,7 @@ import { plumeShaders, type PlumeMaterial } from './plumeMaterial.ts';
6
6
  import { buildPlumeGeometry } from './plumeGeometry.ts';
7
7
  import type { Atmosphere } from './atmosphere.ts';
8
8
  import type { Vec3 } from '../math/color.ts';
9
+ import { blendCovering, blendKeeping } from './sceneCoverage.ts';
9
10
 
10
11
  /**
11
12
  * Batched camera-facing quads for volumetric effects — flames, smoke, steam,
@@ -202,8 +203,8 @@ export class PlumeRenderer {
202
203
  bindAtmosphere(gl, u, atmosphere, atmosphereCameraY, underwaterEnabled);
203
204
 
204
205
  gl.enable(gl.BLEND);
205
- if (this.blend === 'additive') gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
206
- else gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
206
+ if (this.blend === 'additive') blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
207
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
207
208
  gl.depthMask(false);
208
209
  gl.disable(gl.CULL_FACE);
209
210
 
@@ -85,9 +85,11 @@ export function objectMotion(
85
85
  }
86
86
 
87
87
  /**
88
- * A vertex through a skin palette: the four joints' matrices weighted and summed, then applied —
88
+ * A vertex through a skin palette: the joints' matrices weighted and summed, then applied —
89
89
  * `SKINNING_GLSL`'s `skinMatrix()` times the position, in model space, before the model matrix.
90
- * `palette` is sixteen floats a joint, column-major. Written into `out`, four values.
90
+ * `palette` is sixteen floats a joint, column-major. `joints` and `weights` hold four influences, or
91
+ * eight for a mesh with a second set (the first set then the second, as the shader reads them with
92
+ * `SKIN_EIGHT` on). Written into `out`, four values.
91
93
  */
92
94
  export function skinVertex(
93
95
  palette: ArrayLike<number>,
@@ -103,7 +105,8 @@ export function skinVertex(
103
105
  out[1] = 0;
104
106
  out[2] = 0;
105
107
  out[3] = 0;
106
- for (let influence = 0; influence < 4; influence += 1) {
108
+ const influences = Math.min(joints.length, weights.length, 8);
109
+ for (let influence = 0; influence < influences; influence += 1) {
107
110
  const weight = weights[influence] as number;
108
111
  if (weight === 0) continue;
109
112
  const at = (joints[influence] as number) * 16;
@@ -20,8 +20,14 @@ import { PREFILTER_SAMPLE_COUNTS } from './prefilterEnvMap.ts';
20
20
  * `aces` adds filmic tone mapping before that, rolling highlights off instead of
21
21
  * clipping them flat. Together they are what three.js does by default, which is why a
22
22
  * world ported from it looks wrong until this exists.
23
+ *
24
+ * `shoulder` keeps sRGB's midtones exactly and eases only the highlights: past 0.8 the brightest
25
+ * channel rolls toward 1 and the other two are scaled with it, so an overbright red spark stays
26
+ * red where `srgb` clips each channel and turns its core pink and wide. It is the curve a ported
27
+ * game's own resolve applied — for a port that has to match one — and it has none of ACES's toe or
28
+ * desaturation. See `HIGHLIGHT_SHOULDER_GLSL` for the arithmetic.
23
29
  */
24
- export type OutputTransform = 'none' | 'srgb' | 'aces';
30
+ export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder';
25
31
 
26
32
  /**
27
33
  * The three settings whose accepted values are a closed set, written as one.
@@ -40,6 +46,9 @@ export type WaterReflectionFilterTaps = 1 | 5 | 9;
40
46
  /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
41
47
  export type GlassShadows = 'full' | 'half' | 'off';
42
48
 
49
+ /** How light scattered beneath skin is drawn: see `RenderQuality.skinScattering`. */
50
+ export type SkinScattering = 'pre-integrated' | 'screen-space';
51
+
43
52
  export interface RenderQuality {
44
53
  /**
45
54
  * The transfer curve applied at the end of every shaded pass.
@@ -69,7 +78,8 @@ export interface RenderQuality {
69
78
  * chooses is a look rather than a standard — three.js uses 1/0.6 and says so. This
70
79
  * engine holds no opinion and takes the number from whoever is authoring the world, so
71
80
  * that matching another renderer, or simply preferring a different grade, is a setting
72
- * rather than a patch. Ignored when `outputTransform` is not `aces`.
81
+ * rather than a patch. Scales into `aces` and into `shoulder`, the two curves; ignored by
82
+ * `none` and `srgb`, which have none.
73
83
  */
74
84
  readonly outputExposure: number;
75
85
  /**
@@ -203,6 +213,24 @@ export interface RenderQuality {
203
213
  * lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
204
214
  */
205
215
  readonly glassShadows: GlassShadows;
216
+ /**
217
+ * How light that scatters beneath skin is drawn, for a material whose model is `skinModel`.
218
+ *
219
+ * **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
220
+ * reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
221
+ * as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
222
+ * Burley's blur in the picture as well: a skin draw writes its diffuse to a target of its own (it
223
+ * is drawn twice to do so), which is spread across the pixels its scatter distance covers and
224
+ * added back before anything blended is drawn, so light crosses a shadow's edge and a nostril's
225
+ * rim in the picture rather than in a fit. See `skinBlur.ts`.
226
+ *
227
+ * Construction-time: two half-float targets the size of the frame, and a multisampled one under
228
+ * multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
229
+ * where the frame is still linear light to add to; without one, skin is pre-integrated and the
230
+ * renderer says so once. What would make the default wrong is a close-up face that reads as wax
231
+ * at a shadow's edge — that is what the blur is for.
232
+ */
233
+ readonly skinScattering: SkinScattering;
206
234
  /** Horizontal world-space reach over which a directional shadow dissolves. */
207
235
  readonly directionalShadowMaxDistance: number;
208
236
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -913,6 +941,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
913
941
  lightVolumeSamples: 32,
914
942
  directionalShadowDepthLayers: 2,
915
943
  glassShadows: 'full',
944
+ skinScattering: 'pre-integrated',
916
945
  directionalShadowMaxDistance: 6,
917
946
  directionalShadowMaxSlope: 3,
918
947
  pointShadowRebakeDistance: 0.001,
@@ -1077,6 +1106,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1077
1106
  directionalShadowDepthLayers:
1078
1107
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1079
1108
  glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1109
+ skinScattering: options.skinScattering ?? DEFAULT_RENDER_QUALITY.skinScattering,
1080
1110
  directionalShadowMaxDistance:
1081
1111
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
1082
1112
  directionalShadowMaxSlope:
@@ -1231,6 +1261,11 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1231
1261
  `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1232
1262
  );
1233
1263
  }
1264
+ if (quality.skinScattering !== 'pre-integrated' && quality.skinScattering !== 'screen-space') {
1265
+ throw new Error(
1266
+ `RenderQuality.skinScattering must be 'pre-integrated' or 'screen-space', got ${String(quality.skinScattering)}`,
1267
+ );
1268
+ }
1234
1269
  if (quality.plumeNoiseOctaves > 3) {
1235
1270
  throw new Error(
1236
1271
  `RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`,
@@ -104,6 +104,10 @@ export function recordingGl(
104
104
  NONE: 0,
105
105
  ALWAYS: 0x0207,
106
106
  LESS: 0x0201,
107
+ EQUAL: 0x0202,
108
+ GREATER: 0x0204,
109
+ GEQUAL: 0x0206,
110
+ LEQUAL: 0x0203,
107
111
  TEXTURE_MIN_FILTER: 0x2801,
108
112
  TEXTURE_MAG_FILTER: 0x2800,
109
113
  TEXTURE_WRAP_S: 0x2802,
@@ -0,0 +1,78 @@
1
+ /**
2
+ * What a blended draw into the scene does to its alpha, which holds how much of each pixel is still
3
+ * the opaque surface — the share ambient occlusion is allowed to darken.
4
+ *
5
+ * **Occlusion is applied in the composite, from the depth the opaque world left**, after every
6
+ * translucent draw has landed, and a particle or a pane writes no depth: so a puff of smoke in front
7
+ * of a wall was darkened by the corner behind it. Reported from a game as walls and two figures
8
+ * standing behind a cloud showing through it as outlines, and nothing at all with occlusion off.
9
+ *
10
+ * So the scene's alpha is a running share. The frame clears it to 1 and an opaque draw writes 1,
11
+ * which is the whole of the surface; then each draw blended in front of it says which of three
12
+ * things it is, and the composite darkens `share` of the pixel and leaves the rest:
13
+ *
14
+ * - **It covers**: glass, smoke, water, a line, text in the world, a panel. The share falls by what
15
+ * the draw lets through, `share * (1 - a)`.
16
+ * - **It transmits**: the medium, the order-independent composite. Its alpha already *is* what gets
17
+ * through, `share * a`.
18
+ * - **It keeps**: light added to the surface or a layer of the surface itself — caustics, a light
19
+ * volume, a bolt, a wet film, a decal, a reflection. The share is unchanged.
20
+ *
21
+ * **What it gives up** is added light in front of an occluded corner: a glow or a spark adds to the
22
+ * pixel and keeps the share, so the composite darkens what it added along with the surface — which
23
+ * is how every blended draw behaved before this, and one multiply cannot tell the two apart. **What
24
+ * would make it wrong** is an opaque pass writing an alpha below 1: its surface would then take less
25
+ * occlusion than it should, which is the safe direction, and is the lit shader's own case at a
26
+ * cutout's edge or under a per-vertex alpha lane.
27
+ *
28
+ * **It survives the temporal passes.** A temporal resolve passes the frame's alpha through, since
29
+ * it is a fact about this frame and takes no history; a reconstruction reads it from the scene it
30
+ * rebuilt into the picture it shows, and the blended draws that land after it cover that picture.
31
+ *
32
+ * **One pipeline does not take part**: the GPU-driven pass blends its own surfaces into a target
33
+ * whose alpha is the mask its blit reads to tell a drawn pixel from an undrawn one, and that blit
34
+ * writes the share back to 1. Its panes still take the occlusion of what they cover. What would
35
+ * close it is a channel for that mask other than alpha.
36
+ *
37
+ * A contributed pass drawing something blended into the scene states the same thing for its own
38
+ * pipeline with `SCENE_ALPHA_COVERS`, `SCENE_ALPHA_TRANSMITS` or `SCENE_ALPHA_KEEPS`.
39
+ */
40
+
41
+ /** A blended draw that covers the surface by its alpha: `share * (1 - a)`. */
42
+ export const SCENE_ALPHA_COVERS: GPUBlendComponent = {
43
+ srcFactor: 'zero',
44
+ dstFactor: 'one-minus-src-alpha',
45
+ operation: 'add',
46
+ };
47
+
48
+ /** A draw whose alpha is what gets through it, a transmittance or a revealage: `share * a`. */
49
+ export const SCENE_ALPHA_TRANSMITS: GPUBlendComponent = {
50
+ srcFactor: 'zero',
51
+ dstFactor: 'src-alpha',
52
+ operation: 'add',
53
+ };
54
+
55
+ /** Light added to the surface, or a layer of it: the share unchanged. */
56
+ export const SCENE_ALPHA_KEEPS: GPUBlendComponent = {
57
+ srcFactor: 'zero',
58
+ dstFactor: 'one',
59
+ operation: 'add',
60
+ };
61
+
62
+ /**
63
+ * The same three on WebGL2, as the alpha half of `blendFuncSeparate` beside the colour half a
64
+ * caller already had. `src` and `dst` are the colour factors.
65
+ */
66
+ export function blendCovering(gl: WebGL2RenderingContext, src: number, dst: number): void {
67
+ gl.blendFuncSeparate(src, dst, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);
68
+ }
69
+
70
+ /** `SCENE_ALPHA_TRANSMITS` on WebGL2, beside the colour factors `src` and `dst`. */
71
+ export function blendTransmitting(gl: WebGL2RenderingContext, src: number, dst: number): void {
72
+ gl.blendFuncSeparate(src, dst, gl.ZERO, gl.SRC_ALPHA);
73
+ }
74
+
75
+ /** `SCENE_ALPHA_KEEPS` on WebGL2, beside the colour factors `src` and `dst`. */
76
+ export function blendKeeping(gl: WebGL2RenderingContext, src: number, dst: number): void {
77
+ gl.blendFuncSeparate(src, dst, gl.ZERO, gl.ONE);
78
+ }
@@ -497,6 +497,27 @@ export class SceneTarget {
497
497
  * must reject it in the same place or the accumulation and the revealage cover different
498
498
  * fragments.
499
499
  */
500
+ /**
501
+ * The depth the frame draws against, for a second framebuffer to share: skin's diffuse half,
502
+ * which tests against it and writes none. The multisampled renderbuffer where the frame is
503
+ * multisampled, the texture otherwise, with the size and sample count a colour beside it needs.
504
+ */
505
+ sharedDepth(): {
506
+ texture: WebGLTexture | null;
507
+ renderbuffer: WebGLRenderbuffer | null;
508
+ samples: number;
509
+ width: number;
510
+ height: number;
511
+ } {
512
+ return {
513
+ texture: this.msaaDepth === null ? this.depth : null,
514
+ renderbuffer: this.msaaDepth,
515
+ samples: this.samples,
516
+ width: this.width,
517
+ height: this.height,
518
+ };
519
+ }
520
+
500
521
  depthAttachment(): WebGLTexture | null {
501
522
  return this.depth;
502
523
  }
@@ -7,6 +7,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
7
7
  import { SDF_TEXT_FRAG, SDF_TEXT_VERT } from './shaders/sdfText.ts';
8
8
  import type { SurfaceTexture } from './surfaceTexture.ts';
9
9
  import { SDF_TEXT_TEXTURE_UNIT } from './lightBudget.ts';
10
+ import { blendCovering } from './sceneCoverage.ts';
10
11
 
11
12
  /**
12
13
  * Text from a signed distance field, on WebGL2.
@@ -144,7 +145,7 @@ export class SdfTextRenderer {
144
145
  */
145
146
  gl.depthMask(false);
146
147
  gl.enable(gl.BLEND);
147
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
148
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
148
149
 
149
150
  this.atlas.bind(gl, SDF_TEXT_TEXTURE_UNIT);
150
151