@driftengine/core 4.8.3 → 4.8.5

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 (180) hide show
  1. package/CHANGELOG.json +82 -0
  2. package/README.md +1 -1
  3. package/dist/build/shaderComments.js +0 -0
  4. package/dist/geometry/meshConcat.js +2 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.js +16 -1
  7. package/dist/math/scalar.js +1 -1
  8. package/dist/render/anisotropicLobe.d.ts +7 -0
  9. package/dist/render/anisotropicLobe.js +33 -0
  10. package/dist/render/backend/api.d.ts +41 -4
  11. package/dist/render/backend/select.js +8 -0
  12. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  13. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  14. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  15. package/dist/render/backend/webgl2/renderer.js +604 -63
  16. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  17. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  18. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  19. package/dist/render/backend/webgpu/buffers.js +98 -49
  20. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  21. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  22. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  23. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  24. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  25. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  26. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  27. package/dist/render/backend/webgpu/computeHost.js +5 -0
  28. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  29. package/dist/render/backend/webgpu/depthPass.js +39 -9
  30. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  31. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  32. package/dist/render/backend/webgpu/flatPass.js +109 -12
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  34. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  35. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  36. package/dist/render/backend/webgpu/motionPass.js +106 -9
  37. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  38. package/dist/render/backend/webgpu/postPass.js +6 -3
  39. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  40. package/dist/render/backend/webgpu/renderer.js +498 -48
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  42. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  44. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  47. package/dist/render/clothBindingData.d.ts +41 -0
  48. package/dist/render/clothBindingData.js +72 -0
  49. package/dist/render/clothPlacement.d.ts +25 -0
  50. package/dist/render/clothPlacement.js +85 -0
  51. package/dist/render/compressedSource.d.ts +66 -0
  52. package/dist/render/compressedSource.js +138 -0
  53. package/dist/render/cutoutCaster.d.ts +2 -0
  54. package/dist/render/cutoutCaster.js +2 -0
  55. package/dist/render/cutoutDither.d.ts +51 -0
  56. package/dist/render/cutoutDither.js +90 -0
  57. package/dist/render/eyeRefraction.d.ts +35 -0
  58. package/dist/render/eyeRefraction.js +40 -0
  59. package/dist/render/glCompressed.d.ts +27 -0
  60. package/dist/render/glCompressed.js +53 -0
  61. package/dist/render/hairLobes.d.ts +70 -0
  62. package/dist/render/hairLobes.js +120 -0
  63. package/dist/render/lightBudget.d.ts +22 -0
  64. package/dist/render/lightBudget.js +22 -0
  65. package/dist/render/mesh.d.ts +7 -0
  66. package/dist/render/mesh.js +24 -0
  67. package/dist/render/recon/motionVectors.d.ts +4 -2
  68. package/dist/render/recon/motionVectors.js +6 -3
  69. package/dist/render/renderQuality.d.ts +20 -0
  70. package/dist/render/renderQuality.js +5 -0
  71. package/dist/render/rendererHarness.js +4 -0
  72. package/dist/render/sceneTarget.d.ts +12 -0
  73. package/dist/render/sceneTarget.js +14 -0
  74. package/dist/render/shaders/clothBinding.d.ts +22 -0
  75. package/dist/render/shaders/clothBinding.js +80 -0
  76. package/dist/render/shaders/depth.js +42 -6
  77. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  78. package/dist/render/shaders/flat/anisotropic.js +68 -0
  79. package/dist/render/shaders/flat/eye.d.ts +1 -0
  80. package/dist/render/shaders/flat/eye.js +81 -0
  81. package/dist/render/shaders/flat/hair.d.ts +1 -0
  82. package/dist/render/shaders/flat/hair.js +101 -0
  83. package/dist/render/shaders/flat/index.js +36 -2
  84. package/dist/render/shaders/flat/main.d.ts +1 -1
  85. package/dist/render/shaders/flat/main.js +113 -17
  86. package/dist/render/shaders/flat/models.d.ts +14 -0
  87. package/dist/render/shaders/flat/models.js +273 -0
  88. package/dist/render/shaders/flat/preamble.js +26 -24
  89. package/dist/render/shaders/flat/skin.d.ts +1 -0
  90. package/dist/render/shaders/flat/skin.js +114 -0
  91. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  92. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  93. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  94. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  95. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  96. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  97. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  98. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  99. package/dist/render/shaders/skinBlur.d.ts +1 -0
  100. package/dist/render/shaders/skinBlur.js +103 -0
  101. package/dist/render/shaders/skinning.d.ts +3 -1
  102. package/dist/render/shaders/skinning.js +31 -4
  103. package/dist/render/shadowCasters.d.ts +13 -1
  104. package/dist/render/skinBlur.d.ts +41 -0
  105. package/dist/render/skinBlur.js +49 -0
  106. package/dist/render/skinScatter.d.ts +67 -0
  107. package/dist/render/skinScatter.js +80 -0
  108. package/dist/render/skinnedClothRun.d.ts +26 -0
  109. package/dist/render/skinnedClothRun.js +84 -0
  110. package/dist/render/surfaceModel.d.ts +129 -0
  111. package/dist/render/surfaceModel.js +152 -0
  112. package/dist/render/surfaceTexture.d.ts +44 -4
  113. package/dist/render/surfaceTexture.js +60 -46
  114. package/dist/render/vertexDefaults.js +7 -0
  115. package/package.json +6 -6
  116. package/src/build/shaderComments.ts +0 -0
  117. package/src/geometry/meshConcat.ts +2 -0
  118. package/src/index.ts +40 -1
  119. package/src/math/scalar.ts +1 -1
  120. package/src/render/anisotropicLobe.ts +41 -0
  121. package/src/render/backend/api.ts +52 -3
  122. package/src/render/backend/select.ts +8 -0
  123. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  124. package/src/render/backend/webgl2/renderer.ts +693 -64
  125. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  126. package/src/render/backend/webgpu/buffers.ts +137 -63
  127. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  128. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  129. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  130. package/src/render/backend/webgpu/computeHost.ts +25 -0
  131. package/src/render/backend/webgpu/depthPass.ts +39 -7
  132. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  133. package/src/render/backend/webgpu/flatPass.ts +145 -7
  134. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  135. package/src/render/backend/webgpu/motionPass.ts +128 -9
  136. package/src/render/backend/webgpu/postPass.ts +5 -1
  137. package/src/render/backend/webgpu/renderer.ts +663 -43
  138. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  139. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  140. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  141. package/src/render/clothBindingData.ts +89 -0
  142. package/src/render/clothPlacement.ts +114 -0
  143. package/src/render/compressedSource.ts +198 -0
  144. package/src/render/cutoutCaster.ts +5 -0
  145. package/src/render/cutoutDither.ts +99 -0
  146. package/src/render/eyeRefraction.ts +72 -0
  147. package/src/render/glCompressed.ts +86 -0
  148. package/src/render/hairLobes.ts +188 -0
  149. package/src/render/lightBudget.ts +25 -0
  150. package/src/render/mesh.ts +23 -0
  151. package/src/render/recon/motionVectors.ts +6 -3
  152. package/src/render/renderQuality.ts +28 -0
  153. package/src/render/rendererHarness.ts +4 -0
  154. package/src/render/sceneTarget.ts +21 -0
  155. package/src/render/shaders/clothBinding.ts +81 -0
  156. package/src/render/shaders/depth.ts +42 -6
  157. package/src/render/shaders/flat/anisotropic.ts +68 -0
  158. package/src/render/shaders/flat/eye.ts +82 -0
  159. package/src/render/shaders/flat/hair.ts +102 -0
  160. package/src/render/shaders/flat/index.ts +36 -2
  161. package/src/render/shaders/flat/main.ts +113 -17
  162. package/src/render/shaders/flat/models.ts +286 -0
  163. package/src/render/shaders/flat/preamble.ts +26 -24
  164. package/src/render/shaders/flat/skin.ts +116 -0
  165. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  166. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  167. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  168. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  169. package/src/render/shaders/skinBlur.ts +105 -0
  170. package/src/render/shaders/skinning.ts +31 -4
  171. package/src/render/shadowCasters.ts +13 -0
  172. package/src/render/skinBlur.ts +53 -0
  173. package/src/render/skinScatter.ts +92 -0
  174. package/src/render/skinnedClothRun.ts +128 -0
  175. package/src/render/surfaceModel.ts +274 -0
  176. package/src/render/surfaceTexture.ts +123 -63
  177. package/src/render/vertexDefaults.ts +7 -0
  178. package/dist/math/exact.d.ts +0 -76
  179. package/dist/math/exact.js +0 -327
  180. package/src/math/exact.ts +0 -340
@@ -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
  *
@@ -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) {
@@ -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;
@@ -46,6 +46,9 @@ export type WaterReflectionFilterTaps = 1 | 5 | 9;
46
46
  /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
47
47
  export type GlassShadows = 'full' | 'half' | 'off';
48
48
 
49
+ /** How light scattered beneath skin is drawn: see `RenderQuality.skinScattering`. */
50
+ export type SkinScattering = 'pre-integrated' | 'screen-space';
51
+
49
52
  export interface RenderQuality {
50
53
  /**
51
54
  * The transfer curve applied at the end of every shaded pass.
@@ -210,6 +213,24 @@ export interface RenderQuality {
210
213
  * lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
211
214
  */
212
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;
213
234
  /** Horizontal world-space reach over which a directional shadow dissolves. */
214
235
  readonly directionalShadowMaxDistance: number;
215
236
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -920,6 +941,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
920
941
  lightVolumeSamples: 32,
921
942
  directionalShadowDepthLayers: 2,
922
943
  glassShadows: 'full',
944
+ skinScattering: 'pre-integrated',
923
945
  directionalShadowMaxDistance: 6,
924
946
  directionalShadowMaxSlope: 3,
925
947
  pointShadowRebakeDistance: 0.001,
@@ -1084,6 +1106,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1084
1106
  directionalShadowDepthLayers:
1085
1107
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1086
1108
  glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1109
+ skinScattering: options.skinScattering ?? DEFAULT_RENDER_QUALITY.skinScattering,
1087
1110
  directionalShadowMaxDistance:
1088
1111
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
1089
1112
  directionalShadowMaxSlope:
@@ -1238,6 +1261,11 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1238
1261
  `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1239
1262
  );
1240
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
+ }
1241
1269
  if (quality.plumeNoiseOctaves > 3) {
1242
1270
  throw new Error(
1243
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,
@@ -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
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The vertex-stage cloth binding: a skinned vertex moved toward the cloth simulation's triangle it
3
+ * is bound to, by a weight painted per vertex — the GLSL of `clothPlacement.ts`, line for line.
4
+ *
5
+ * **Textures read by vertex index, not attributes**, for the reason morph targets are: the skinned
6
+ * variants already use all sixteen attribute locations once eight influences are in. Three
7
+ * textures, each wrapped onto rows at its own width (read back with `textureSize`, so no uniform
8
+ * joins the vertex block and no shared field moves):
9
+ *
10
+ * - **the binding**, two texels a render vertex: the three particle indices and the weight, then
11
+ * the barycentric (u, v) for the second and third particle and the offset along the normal;
12
+ * - **the particles now**, one texel a particle, world space — the simulation's output;
13
+ * - **the particles at rest**, one texel a particle, model space — the rest frames are built from
14
+ * these, so a vertex's own bind-pose normal turns as its triangle has turned.
15
+ *
16
+ * **A pipeline constant, not a permutation**, as `SKIN_EIGHT` is: declared in the skinned variants
17
+ * and folded away where a draw binds no cloth. The textures are bound either way, to stand-ins
18
+ * where there is no cloth.
19
+ */
20
+ export const CLOTH_BINDING_GLSL = `
21
+ /** Whether this pipeline places vertices by a cloth binding. */
22
+ const bool CLOTH_BOUND = false; // wgsl:override
23
+ uniform highp sampler2D uClothBinding;
24
+ uniform highp sampler2D uClothParticles;
25
+ uniform highp sampler2D uClothRest;
26
+
27
+ /** Texel i of a texture wrapped onto rows of the given width. */
28
+ ivec2 clothTexel(int i, int width) {
29
+ return ivec2(i % width, i / width);
30
+ }
31
+
32
+ /** A triangle's frame: its first edge, the normal crossed with it, and its normal. */
33
+ mat3 clothFrame(vec3 a, vec3 b, vec3 c) {
34
+ vec3 edge = b - a;
35
+ vec3 n = normalize(cross(edge, c - a));
36
+ vec3 t = normalize(edge);
37
+ return mat3(t, cross(n, t), n);
38
+ }
39
+
40
+ /**
41
+ * Where this vertex's triangle puts it, the turn its normal takes, and how far it follows (the
42
+ * painted weight: 0 skinned, 1 cloth). One particle named three times is a vertex bound to that
43
+ * particle alone: on it, with no turn.
44
+ */
45
+ float clothPlace(out vec3 position, out mat3 turn) {
46
+ int bindingWidth = textureSize(uClothBinding, 0).x;
47
+ int particleWidth = textureSize(uClothParticles, 0).x;
48
+ int restWidth = textureSize(uClothRest, 0).x;
49
+ vec4 indices = texelFetch(uClothBinding, clothTexel(gl_VertexID * 2, bindingWidth), 0);
50
+ vec4 at = texelFetch(uClothBinding, clothTexel(gl_VertexID * 2 + 1, bindingWidth), 0);
51
+ int a = int(indices.x);
52
+ int b = int(indices.y);
53
+ int c = int(indices.z);
54
+ vec3 pa = texelFetch(uClothParticles, clothTexel(a, particleWidth), 0).xyz;
55
+ if (a == b && b == c) {
56
+ position = pa;
57
+ turn = mat3(1.0);
58
+ return indices.w;
59
+ }
60
+ vec3 pb = texelFetch(uClothParticles, clothTexel(b, particleWidth), 0).xyz;
61
+ vec3 pc = texelFetch(uClothParticles, clothTexel(c, particleWidth), 0).xyz;
62
+ mat3 now = clothFrame(pa, pb, pc);
63
+ mat3 rest = clothFrame(
64
+ texelFetch(uClothRest, clothTexel(a, restWidth), 0).xyz,
65
+ texelFetch(uClothRest, clothTexel(b, restWidth), 0).xyz,
66
+ texelFetch(uClothRest, clothTexel(c, restWidth), 0).xyz
67
+ );
68
+ position = pa * (1.0 - at.x - at.y) + pb * at.x + pc * at.y + now[2] * at.z;
69
+ turn = now * transpose(rest);
70
+ return indices.w;
71
+ }
72
+ `;
73
+
74
+ /** A skinned vertex stage with the cloth binding on: the same source, the constant flipped. */
75
+ export function clothBound(source: string): string {
76
+ const off = 'const bool CLOTH_BOUND = false;';
77
+ if (!source.includes(off)) {
78
+ throw new Error('clothBound: this vertex stage declares no CLOTH_BOUND constant');
79
+ }
80
+ return source.replace(off, 'const bool CLOTH_BOUND = true;');
81
+ }
@@ -1,7 +1,9 @@
1
1
  import { resolveConditionals } from './conditionals.ts';
2
2
  import { SUN_STATIC_LAYER } from '../shadowMap.ts';
3
3
  import { CUTOUT_COVERAGE_GLSL } from './cutoutCoverage.ts';
4
+ import { CUTOUT_DITHER_GLSL } from '../cutoutDither.ts';
4
5
  import { SKINNING_GLSL } from './skinning.ts';
6
+ import { CLOTH_BINDING_GLSL } from './clothBinding.ts';
5
7
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.ts';
6
8
 
7
9
  /**
@@ -21,6 +23,11 @@ const DEPTH_VERT_SOURCE = `#version 300 es
21
23
  layout(location = 0) in vec3 aPosition;
22
24
  #if SKINNED
23
25
  ${SKINNING_GLSL}
26
+ /* Not in the glass-tint caster: a garment is not glass, and its layout keeps its bindings. */
27
+ #if GLASS
28
+ #else
29
+ ${CLOTH_BINDING_GLSL}
30
+ #endif
24
31
  #endif
25
32
 
26
33
  uniform mat4 uLightViewProj;
@@ -67,14 +74,15 @@ ${CHANNEL_BEND}
67
74
  layout(location = 5) in vec3 aUv;
68
75
  uniform vec2 uUvScale;
69
76
  uniform vec2 uUvOffset;
70
- uniform float uAlphaCutout;
77
+ /* x the cutoff, y 1 for a dithered edge: see the cutout fragment stages below. */
78
+ uniform vec2 uAlphaCutout;
71
79
  #endif
72
80
 
73
81
  out vec4 vLightPosition;
74
82
  #if CUTOUT
75
83
  /* The texture coordinate and, in z, the texture-array layer the face wears. */
76
84
  out vec3 vUv;
77
- flat out float vAlphaCutout;
85
+ flat out vec2 vAlphaCutout;
78
86
  #endif
79
87
  #if GLASS
80
88
  /* Where on the pane this is, for the tint's own normal and its angle to the light. Last, so the
@@ -101,6 +109,18 @@ void main() {
101
109
  vec3 bent = world.xyz;
102
110
  #else
103
111
  vec3 bent = channelBend(world.xyz, aChannel.x);
112
+ #endif
113
+ #if SKINNED
114
+ #if GLASS
115
+ #else
116
+ /* Toward the cloth exactly as the colour pass moves it, or the shadow parts from the garment. */
117
+ if (CLOTH_BOUND) {
118
+ vec3 clothPosition;
119
+ mat3 turn;
120
+ float follow = clothPlace(clothPosition, turn);
121
+ bent = mix(bent, clothPosition, follow);
122
+ }
123
+ #endif
104
124
  #endif
105
125
  vLightPosition = uLightViewProj * vec4(bent, world.w);
106
126
  gl_Position = vLightPosition;
@@ -199,12 +219,13 @@ export const DEPTH_CUTOUT_FRAG = `#version 300 es
199
219
  precision highp float;
200
220
  in vec4 vLightPosition;
201
221
  in vec3 vUv;
202
- flat in float vAlphaCutout;
222
+ flat in vec2 vAlphaCutout;
203
223
 
204
224
  uniform highp sampler2DArray uPreviousShadowMap;
205
225
  uniform int uPeelShadowLayer;
206
226
  uniform highp sampler2DArray uCutoutMap;
207
227
  ${CUTOUT_COVERAGE_GLSL}
228
+ ${CUTOUT_DITHER_GLSL}
208
229
  void main() {
209
230
  /* Credited for its mip level as the surface's own test is, so a leaf casts the shape it draws. */
210
231
  vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
@@ -215,7 +236,16 @@ void main() {
215
236
  float previousDepth = textureLod(uPreviousShadowMap, vec3(uv, ${SUN_STATIC_LAYER}.0), 0.0).r;
216
237
  if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
217
238
  }
218
- if (alpha < vAlphaCutout) discard;
239
+ /*
240
+ * A dithered edge casts the share it covers, with a pattern that does not move: a shadow map has
241
+ * no temporal resolve of its own and a static layer keeps whatever it was drawn with, so a moving
242
+ * pattern would shimmer there. The map's filter averages it into a soft edge.
243
+ */
244
+ if (vAlphaCutout.y > 0.5) {
245
+ if (!cutoutKeeps(cutoutShare(alpha, vAlphaCutout.x), gl_FragCoord.xy, 0.0)) discard;
246
+ } else if (alpha < vAlphaCutout.x) {
247
+ discard;
248
+ }
219
249
  }
220
250
  `;
221
251
 
@@ -293,13 +323,14 @@ export const GLASS_TINT_CUTOUT_FRAG = `#version 300 es
293
323
  precision highp float;
294
324
  in vec4 vLightPosition;
295
325
  in vec3 vUv;
296
- flat in float vAlphaCutout;
326
+ flat in vec2 vAlphaCutout;
297
327
  in vec3 vGlassWorld;
298
328
 
299
329
  uniform vec4 uGlassPane;
300
330
  uniform vec4 uGlassLight;
301
331
  uniform highp sampler2DArray uCutoutMap;
302
332
  ${CUTOUT_COVERAGE_GLSL}
333
+ ${CUTOUT_DITHER_GLSL}
303
334
  out vec4 outTint;
304
335
 
305
336
  void main() {
@@ -307,6 +338,11 @@ void main() {
307
338
  vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
308
339
  float alpha = cutoutAlpha(texture(uCutoutMap, at).a, vUv.xy * vec2(textureSize(uCutoutMap, 0).xy));
309
340
  ${GLASS_TINT_BODY}
310
- if (alpha < vAlphaCutout) discard;
341
+ /* The depth cutout's own test, so the colour lands exactly where the depth does. */
342
+ if (vAlphaCutout.y > 0.5) {
343
+ if (!cutoutKeeps(cutoutShare(alpha, vAlphaCutout.x), gl_FragCoord.xy, 0.0)) discard;
344
+ } else if (alpha < vAlphaCutout.x) {
345
+ discard;
346
+ }
311
347
  }
312
348
  `;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The anisotropic model in GLSL: GGX stretched along a direction in the surface, after glTF's
3
+ * `KHR_materials_anisotropy` — `anisotropicLobe.ts` is the same arithmetic, and its tests hold it to
4
+ * the standard lobe at strength 0 and to the widths' ratio at its half maximum.
5
+ *
6
+ * **The direction** is the tangent frame's, turned by the material's rotation, and by the model
7
+ * map's red and green where one is bound (a direction encoded −1 to 1, glTF's packing), its blue
8
+ * scaling the strength. **The environment** is reflected about a normal bent toward the stretch, as
9
+ * Filament and three.js do: a streak of sky across a brushed disc rather than a round spot, at the
10
+ * cost of a reflection that is not the exact anisotropic integral.
11
+ *
12
+ * Lamps widen both of the lobe's widths by the source's angle, as `sphereLobe` widens the standard
13
+ * lobe, with the energy kept; at strength 0 that is `sphereLobe` exactly.
14
+ */
15
+ export const ANISOTROPIC_GLSL = /* glsl */ `
16
+ /* The stretch's frame and strength, gathered once with the surface. */
17
+ vec3 aAlong;
18
+ vec3 aAcross;
19
+ float aStrength;
20
+
21
+ void anisotropicSurface() {
22
+ vec2 direction = vec2(1.0, 0.0);
23
+ float strength = uModelParams[0].x;
24
+ if (uModelParams[1].w > 0.0) {
25
+ vec2 mapped = mMap.rg * 2.0 - 1.0;
26
+ direction = dot(mapped, mapped) > 1e-8 ? normalize(mapped) : direction;
27
+ strength *= mMap.b;
28
+ }
29
+ vec2 turned = vec2(
30
+ direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,
31
+ direction.x * uModelParams[0].z + direction.y * uModelParams[0].y
32
+ );
33
+ aAlong = normalize(mTangent * turned.x + mBitangent * turned.y);
34
+ aAcross = cross(mNormal, aAlong);
35
+ aStrength = strength;
36
+ }
37
+
38
+ /** The stretched lobe at a half vector, 1 at the mirror direction: anisotropicLobe.ts. */
39
+ float anisotropicLobe(vec3 h, float sourceRadius, float dist) {
40
+ float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);
41
+ float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;
42
+ float grow = sourceRadius / max(2.0 * dist, 1e-3);
43
+ float alongWide = clamp(alongWidth + grow, alongWidth, 1.0);
44
+ float acrossWide = clamp(alpha + grow, alpha, 1.0);
45
+ float energy = (alongWidth / alongWide) * (alpha / acrossWide);
46
+ float x = dot(aAlong, h) / alongWide;
47
+ float y = dot(aAcross, h) / acrossWide;
48
+ float z = dot(mNormal, h);
49
+ float d = x * x + y * y + z * z;
50
+ return energy / max(d * d, 1e-8);
51
+ }
52
+
53
+ void anisotropicLight(vec3 l, float sourceRadius, float dist) {
54
+ float ndl = max(dot(mNormal, l), 0.0);
55
+ vec3 h = normalize(l + mToEye);
56
+ vec3 tint = mix(mSpecColor, vec3(1.0), pow(1.0 - max(dot(mToEye, h), 0.0), 5.0) * mMetal);
57
+ mDiffuse = mAlbedo * ndl * (1.0 - mMetal);
58
+ mSpecular = ndl > 0.0 ? tint * anisotropicLobe(h, sourceRadius, dist) : vec3(0.0);
59
+ }
60
+
61
+ /* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */
62
+ vec3 anisotropicReflectNormal(vec3 n) {
63
+ vec3 across = cross(aAcross, mToEye);
64
+ vec3 bent = cross(across, aAcross);
65
+ float bend = aStrength * clamp(5.0 * mRoughness, 0.0, 1.0);
66
+ return normalize(mix(n, bent, bend));
67
+ }
68
+ `;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The eye model in GLSL: `eyeRefraction.ts` is the refraction's arithmetic, and its tests hold it
3
+ * to no shift head-on, the refracted crossing at 30°, a bounded shift edge-on and the map-less iris.
4
+ *
5
+ * **The iris is moved before any map is read** (`modelSurfaceAt`), by how far the eye's ray crosses
6
+ * the iris plane after the cornea bends it, so the whole iris — colour, relief, the model's own
7
+ * channels — moves as one. The crossing is turned into texture coordinates by the tangent frame and
8
+ * by how many of them a metre spans here, which the screen's derivatives say: so it is in uniform
9
+ * control flow, as `modelSurfaceAt` is. **What it gives up** is an anisotropic mapping — a texture
10
+ * stretched one way more than the other moves by the average of the two.
11
+ *
12
+ * **The model map**: red is the iris (1 inside), green the cornea's height above the iris plane as a
13
+ * share of `irisDepth`. Without one, the iris is a disc of `irisRadius` about the texture's centre
14
+ * and the height is `irisDepth` throughout.
15
+ *
16
+ * **Lit as an eye is**: the iris by a normal mirrored about the axis — concave where the cornea is
17
+ * convex, so light pools on the side away from it — and the sclera by the surface's own; and over
18
+ * both, the cornea's highlight, normalised GGX at `corneaRoughness` with the Fresnel of index 1.336,
19
+ * in lamp units like skin's. The environment is reflected at the cornea's split-sum share.
20
+ */
21
+ import { CORNEA_F0, EYE_MIN_DESCENT } from '../../eyeRefraction.ts';
22
+
23
+ export const EYE_GLSL = /* glsl */ `
24
+ const float CORNEA_F0 = ${CORNEA_F0.toFixed(9)};
25
+ const float EYE_MIN_DESCENT = ${EYE_MIN_DESCENT.toFixed(4)};
26
+
27
+ /* How much of this texel is iris, gathered with the surface. */
28
+ float eIris;
29
+
30
+ /** Where the iris is without a map: a disc of the model's radius about the centre, a soft limbus. */
31
+ float eyeIrisDisc(vec2 uv) {
32
+ float r = length(uv - 0.5);
33
+ return 1.0 - smoothstep(uModelParams[0].x - 0.01, uModelParams[0].x + 0.01, r);
34
+ }
35
+
36
+ /** The iris's texture coordinate, moved by the cornea: \`eyeRefraction.ts\`. Before any map is read. */
37
+ vec3 eyeRefract(vec3 at) {
38
+ vec3 n = normalize(vNormal);
39
+ vec3 axis = uModelParams[1].xyz;
40
+ vec4 map = texture(uModelMap, at);
41
+ bool mapped = uModelParams[1].w > 0.0;
42
+ float iris = mapped ? map.r : eyeIrisDisc(at.xy);
43
+ float height = uModelParams[0].y * (mapped ? map.g : 1.0);
44
+ vec3 ray = refract(-normalize(uCameraPos - vWorldPos), n, 1.0 / uModelParams[0].z);
45
+ float descent = max(-dot(ray, axis), EYE_MIN_DESCENT);
46
+ vec3 travel = ray * (height / descent);
47
+ vec3 across = travel - axis * dot(travel, axis);
48
+ mat3 frame = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
49
+ /* How many texture coordinates a metre spans here, from the screen's two derivatives. */
50
+ float perMetre = length(fwidth(vUv.xy)) / max(length(fwidth(vWorldPos)), 1e-9);
51
+ vec2 shift = vec2(dot(across, frame[0]), dot(across, frame[1])) * (perMetre * iris);
52
+ return vec3(at.xy + shift, at.z);
53
+ }
54
+
55
+ void eyeSurface(vec3 at) {
56
+ eIris = uModelParams[1].w > 0.0 ? mMap.r : eyeIrisDisc(at.xy);
57
+ mRoughness = uModelParams[0].w;
58
+ }
59
+
60
+ void eyeLight(vec3 l, float sourceRadius, float dist) {
61
+ vec3 axis = uModelParams[1].xyz;
62
+ /* The iris is concave where the cornea over it is convex: the surface's normal mirrored about the axis. */
63
+ vec3 irisNormal = normalize(2.0 * dot(mNormal, axis) * axis - mNormal);
64
+ vec3 lit = normalize(mix(mNormal, irisNormal, eIris));
65
+ mDiffuse = mAlbedo * max(dot(lit, l), 0.0) * (1.0 - mMetal);
66
+
67
+ float ndl = max(dot(mNormal, l), 0.0);
68
+ vec3 h = normalize(l + mToEye);
69
+ float ndh = max(dot(mNormal, h), 0.0);
70
+ float ndv = max(dot(mNormal, mToEye), 1e-4);
71
+ float grow = sourceRadius / max(2.0 * dist, 1e-3);
72
+ float alpha = min(max(mRoughness * mRoughness, MIN_LOBE_ALPHA) + grow, 1.0);
73
+ float f = CORNEA_F0 + (1.0 - CORNEA_F0) * pow(1.0 - max(dot(mToEye, h), 0.0), 5.0);
74
+ mSpecular = vec3(3.14159265 * f * smithMasking(ndl, ndv, alpha) * ggxLobe(ndh, alpha) * ndl);
75
+ }
76
+
77
+ /** The environment at the cornea's Fresnel, by the split sum's share at its own roughness. */
78
+ float eyeReflectance() {
79
+ vec2 dfg = envBrdfApprox(max(dot(mNormal, mToEye), 0.0), uModelParams[0].w);
80
+ return CORNEA_F0 * dfg.x + dfg.y;
81
+ }
82
+ `;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The hair model in GLSL: `hairLobes.ts` is the same arithmetic, and its tests hold the lobes to
3
+ * their tilted peaks, the transmitted lobe to the backlight, the sum to what a white strand
4
+ * receives, and every term to a finite answer where the geometry degenerates.
5
+ *
6
+ * **The strand is the tangent, root to tip.** The model map's green turns each strand's tilt by up
7
+ * to half of it either way (0.5 is none) and its blue is occlusion, which the sky, DriftLight and
8
+ * the environment's reflection take and the lamps do not — their shadows are theirs to cast. The
9
+ * red is not read: a colour from root to tip is the albedo map's to carry.
10
+ *
11
+ * **The environment** is the reflected lobe's: mirrored about a normal turned toward the eye across
12
+ * the strand, which is the cone R lies on, at the strand's own Fresnel — the lobe integrates to it
13
+ * over every direction of arrival. **What it gives up** is the two transmitted lobes against the
14
+ * sky, which a head of hair lit from behind by an open sky would show as a rim; the lamps carry
15
+ * them.
16
+ */
17
+ import { HAIR_F0, HAIR_MIN_BETA, HAIR_SCATTER_SPHERE } from '../../hairLobes.ts';
18
+
19
+ export const HAIR_GLSL = /* glsl */ `
20
+ const float HAIR_F0 = ${HAIR_F0.toFixed(9)};
21
+ const float HAIR_MIN_BETA = ${HAIR_MIN_BETA.toFixed(3)};
22
+ const float HAIR_SCATTER_SPHERE = ${HAIR_SCATTER_SPHERE.toFixed(3)};
23
+
24
+ /* This texel's tilt, gathered once with the surface. */
25
+ float hShift;
26
+
27
+ void hairSurface() {
28
+ hShift = uModelParams[0].x * (0.5 + mMap.g);
29
+ }
30
+
31
+ /** A longitudinal lobe: a normalised Gaussian of width \`beta\` at \`x\` from its centre. */
32
+ float hairLongitudinal(float beta, float x) {
33
+ return exp(-0.5 * x * x / (beta * beta)) / (2.5066283 * beta);
34
+ }
35
+
36
+ float hairFresnel(float cosine) {
37
+ float c = 1.0 - clamp(cosine, 0.0, 1.0);
38
+ return HAIR_F0 + (1.0 - HAIR_F0) * c * c * c * c * c;
39
+ }
40
+
41
+ /** The normal turned toward the eye across the strand: the scatter's and the environment's. */
42
+ vec3 hairAcross() {
43
+ vec3 across = mToEye - mTangent * dot(mTangent, mToEye);
44
+ float length2 = dot(across, across);
45
+ return length2 > 1e-8 ? across * inversesqrt(length2) : mNormal;
46
+ }
47
+
48
+ void hairLight(vec3 l, float sourceRadius, float dist) {
49
+ vec3 t = mTangent;
50
+ float sinI = clamp(dot(t, l), -1.0, 1.0);
51
+ float sinO = clamp(dot(t, mToEye), -1.0, 1.0);
52
+ /* Floored for the half angle's limit, whose single-precision cosine is negative: hairLobes.ts. */
53
+ float cosD = max(cos(0.5 * abs(asin(sinO) - asin(sinI))), 1e-3);
54
+ vec3 lp = l - sinI * t;
55
+ vec3 vp = mToEye - sinO * t;
56
+ float cosPhi = dot(lp, vp) * inversesqrt(dot(lp, lp) * dot(vp, vp) + 1e-4);
57
+ float cosHalfPhi = sqrt(clamp(0.5 + 0.5 * cosPhi, 0.0, 1.0));
58
+ float beta = max(mRoughness * mRoughness, HAIR_MIN_BETA);
59
+ float grow = sourceRadius / max(2.0 * dist, 1e-3);
60
+ float x = sinI + sinO;
61
+
62
+ float r = hairLongitudinal(beta + grow, x + 2.0 * hShift) * 0.25 * cosHalfPhi
63
+ * hairFresnel(sqrt(clamp(0.5 + 0.5 * dot(l, mToEye), 0.0, 1.0)));
64
+
65
+ float a = 1.0 / (1.19 / cosD + 0.36 * cosD);
66
+ float h = cosHalfPhi * (1.0 + a * (0.6 - 0.8 * cosPhi));
67
+ float fTT = hairFresnel(cosD * sqrt(clamp(1.0 - h * h, 0.0, 1.0)));
68
+ float tt = hairLongitudinal(0.5 * beta + grow, x - hShift) * exp(-3.65 * cosPhi - 3.98)
69
+ * (1.0 - fTT) * (1.0 - fTT) * uModelParams[0].z;
70
+ float ttPower = 0.5 * sqrt(clamp(1.0 - h * h * a * a, 0.0, 1.0)) / cosD;
71
+
72
+ float fTRT = hairFresnel(0.5 * cosD);
73
+ float trt = hairLongitudinal(2.0 * beta + grow, x - 3.0 * hShift) * exp(17.0 * cosPhi - 16.78)
74
+ * (1.0 - fTRT) * (1.0 - fTRT) * fTRT;
75
+
76
+ /* Above zero, so pow never takes the logarithm of nothing: hairLobes.ts. */
77
+ vec3 fibre = max(mAlbedo, vec3(1e-4));
78
+ /* π: the published lobes are per unit irradiance, and a lamp here is π of that. */
79
+ mSpecular = 3.14159265 * (vec3(r) + tt * pow(fibre, vec3(ttPower)) + trt * pow(fibre, vec3(0.8 / cosD)));
80
+ float wrapped = clamp((dot(hairAcross(), l) + 1.0) * 0.25, 0.0, 1.0);
81
+ mDiffuse = sqrt(mAlbedo) * uModelParams[0].y * mix(wrapped, 1.0 - abs(sinI), 0.33);
82
+ }
83
+
84
+ /**
85
+ * A shadow on the scatter: light that reaches a strand in another's shadow has passed through
86
+ * hair to get there, so a penumbra deepens toward the fibre's own hue.
87
+ */
88
+ vec3 hairShade(float shadow) {
89
+ vec3 hue = mAlbedo / max(dot(mAlbedo, vec3(0.2126, 0.7152, 0.0722)), 1e-4);
90
+ return shadow * pow(max(hue, vec3(1e-4)), vec3(1.0 - shadow));
91
+ }
92
+
93
+ /** Light from every side, through the scatter term: \`HAIR_SCATTER_SPHERE\` says why that number. */
94
+ vec3 hairAround(vec3 light) {
95
+ return sqrt(mAlbedo) * (uModelParams[0].y * HAIR_SCATTER_SPHERE * mMap.b) * light;
96
+ }
97
+
98
+ /** The reflected lobe's share of the environment: its Fresnel, which is what it integrates to. */
99
+ float hairReflectance() {
100
+ return hairFresnel(dot(hairAcross(), mToEye)) * mMap.b;
101
+ }
102
+ `;