@zephyr3d/scene 0.9.25 → 0.9.27

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 (299) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  4. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  5. package/dist/animation/joint_dynamics/controller.js +7 -5
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  8. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  9. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  10. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  11. package/dist/animation/morphtrack.js +3 -4
  12. package/dist/animation/morphtrack.js.map +1 -1
  13. package/dist/animation/skeleton.js +79 -25
  14. package/dist/animation/skeleton.js.map +1 -1
  15. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  16. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  17. package/dist/animation/spring/spring_collider.js +4 -3
  18. package/dist/animation/spring/spring_collider.js.map +1 -1
  19. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  20. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  21. package/dist/animation/spring/spring_particle.js +2 -0
  22. package/dist/animation/spring/spring_particle.js.map +1 -1
  23. package/dist/animation/spring/spring_system.js +11 -3
  24. package/dist/animation/spring/spring_system.js.map +1 -1
  25. package/dist/animation/spring_modifier.js +10 -0
  26. package/dist/animation/spring_modifier.js.map +1 -1
  27. package/dist/app/engine.js +53 -43
  28. package/dist/app/engine.js.map +1 -1
  29. package/dist/app/scriptingsystem.js +119 -6
  30. package/dist/app/scriptingsystem.js.map +1 -1
  31. package/dist/app/scriptregistry.js +3 -2
  32. package/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/asset/assetmanager.js +284 -67
  34. package/dist/asset/assetmanager.js.map +1 -1
  35. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  36. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  37. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  38. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  39. package/dist/asset/model.js +184 -69
  40. package/dist/asset/model.js.map +1 -1
  41. package/dist/camera/camera.js +268 -44
  42. package/dist/camera/camera.js.map +1 -1
  43. package/dist/camera/perspectivecamera.js +3 -1
  44. package/dist/camera/perspectivecamera.js.map +1 -1
  45. package/dist/index.d.ts +9044 -5139
  46. package/dist/index.js +17 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/material/eye.js +932 -0
  49. package/dist/material/eye.js.map +1 -0
  50. package/dist/material/hair.js +917 -0
  51. package/dist/material/hair.js.map +1 -0
  52. package/dist/material/hairstrand.js +842 -0
  53. package/dist/material/hairstrand.js.map +1 -0
  54. package/dist/material/hairstrand_data.js +162 -0
  55. package/dist/material/hairstrand_data.js.map +1 -0
  56. package/dist/material/lambert.js +5 -2
  57. package/dist/material/lambert.js.map +1 -1
  58. package/dist/material/material.js +48 -10
  59. package/dist/material/material.js.map +1 -1
  60. package/dist/material/meshmaterial.js +120 -19
  61. package/dist/material/meshmaterial.js.map +1 -1
  62. package/dist/material/mixins/albedocolor.js +1 -1
  63. package/dist/material/mixins/albedocolor.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  65. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  67. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  70. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  71. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  72. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  73. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  74. package/dist/material/mixins/lit.js +106 -19
  75. package/dist/material/mixins/lit.js.map +1 -1
  76. package/dist/material/mixins/pbr/common.js +4 -4
  77. package/dist/material/mixins/pbr/common.js.map +1 -1
  78. package/dist/material/mixins/texture.js +48 -11
  79. package/dist/material/mixins/texture.js.map +1 -1
  80. package/dist/material/mtoon.js +6 -5
  81. package/dist/material/mtoon.js.map +1 -1
  82. package/dist/material/pbrblueprint.js +146 -28
  83. package/dist/material/pbrblueprint.js.map +1 -1
  84. package/dist/material/pbrblueprint_instance.js +501 -0
  85. package/dist/material/pbrblueprint_instance.js.map +1 -0
  86. package/dist/material/pbrmr.js +15 -16
  87. package/dist/material/pbrmr.js.map +1 -1
  88. package/dist/material/shader/helper.js +734 -23
  89. package/dist/material/shader/helper.js.map +1 -1
  90. package/dist/material/shader/terrain_blend.js +55 -0
  91. package/dist/material/shader/terrain_blend.js.map +1 -0
  92. package/dist/material/skin.js +192 -32
  93. package/dist/material/skin.js.map +1 -1
  94. package/dist/material/spriteblueprint.js +16 -1
  95. package/dist/material/spriteblueprint.js.map +1 -1
  96. package/dist/material/subsurfaceprofile.js +86 -23
  97. package/dist/material/subsurfaceprofile.js.map +1 -1
  98. package/dist/material/water.js +2059 -72
  99. package/dist/material/water.js.map +1 -1
  100. package/dist/posteffect/bloom.js +167 -7
  101. package/dist/posteffect/bloom.js.map +1 -1
  102. package/dist/posteffect/coloradjust.js +2 -1
  103. package/dist/posteffect/coloradjust.js.map +1 -1
  104. package/dist/posteffect/compositor.js.map +1 -1
  105. package/dist/posteffect/fxaa.js +2 -1
  106. package/dist/posteffect/fxaa.js.map +1 -1
  107. package/dist/posteffect/grayscale.js +3 -1
  108. package/dist/posteffect/grayscale.js.map +1 -1
  109. package/dist/posteffect/motionblur.js +664 -56
  110. package/dist/posteffect/motionblur.js.map +1 -1
  111. package/dist/posteffect/posteffect.js +12 -1
  112. package/dist/posteffect/posteffect.js.map +1 -1
  113. package/dist/posteffect/sao.js +46 -44
  114. package/dist/posteffect/sao.js.map +1 -1
  115. package/dist/posteffect/skinsss.js +368 -69
  116. package/dist/posteffect/skinsss.js.map +1 -1
  117. package/dist/posteffect/ssgi.js +16 -9
  118. package/dist/posteffect/ssgi.js.map +1 -1
  119. package/dist/posteffect/ssr.js +3 -0
  120. package/dist/posteffect/ssr.js.map +1 -1
  121. package/dist/posteffect/sss.js +121 -36
  122. package/dist/posteffect/sss.js.map +1 -1
  123. package/dist/posteffect/taa.js.map +1 -1
  124. package/dist/posteffect/tonemap.js +44 -1
  125. package/dist/posteffect/tonemap.js.map +1 -1
  126. package/dist/render/abuffer_oit.js +8 -2
  127. package/dist/render/abuffer_oit.js.map +1 -1
  128. package/dist/render/clipmap.js +156 -5
  129. package/dist/render/clipmap.js.map +1 -1
  130. package/dist/render/cull_visitor.js +20 -0
  131. package/dist/render/cull_visitor.js.map +1 -1
  132. package/dist/render/depthpass.js +36 -2
  133. package/dist/render/depthpass.js.map +1 -1
  134. package/dist/render/drawable_mixin.js +26 -1
  135. package/dist/render/drawable_mixin.js.map +1 -1
  136. package/dist/render/dualdepthpeeling_oit.js +3 -2
  137. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  138. package/dist/render/envlight.js +1 -1
  139. package/dist/render/envlight.js.map +1 -1
  140. package/dist/render/fbm_wavegenerator.js +6 -3
  141. package/dist/render/fbm_wavegenerator.js.map +1 -1
  142. package/dist/render/fft_wavegenerator.js +57 -15
  143. package/dist/render/fft_wavegenerator.js.map +1 -1
  144. package/dist/render/gerstner_wavegenerator.js +130 -20
  145. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  146. package/dist/render/globalbindgroup_allocator.js +3 -0
  147. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  148. package/dist/render/hzb.js +80 -22
  149. package/dist/render/hzb.js.map +1 -1
  150. package/dist/render/lightpass.js +6 -3
  151. package/dist/render/lightpass.js.map +1 -1
  152. package/dist/render/oit.js +29 -0
  153. package/dist/render/oit.js.map +1 -0
  154. package/dist/render/render_queue.js +39 -0
  155. package/dist/render/render_queue.js.map +1 -1
  156. package/dist/render/renderer.js +4 -0
  157. package/dist/render/renderer.js.map +1 -1
  158. package/dist/render/rendergraph/blackboard.js +1 -0
  159. package/dist/render/rendergraph/blackboard.js.map +1 -1
  160. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  161. package/dist/render/rendergraph/executor.js +2 -0
  162. package/dist/render/rendergraph/executor.js.map +1 -1
  163. package/dist/render/rendergraph/forward_plus_builder.js +379 -10
  164. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  165. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  166. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  167. package/dist/render/rendergraph/history_resources.js +8 -1
  168. package/dist/render/rendergraph/history_resources.js.map +1 -1
  169. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  170. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  171. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  172. package/dist/render/rendergraph/types.js.map +1 -1
  173. package/dist/render/shadow_mask_pass.js +73 -8
  174. package/dist/render/shadow_mask_pass.js.map +1 -1
  175. package/dist/render/sky.js +166 -126
  176. package/dist/render/sky.js.map +1 -1
  177. package/dist/render/underwater.js +624 -0
  178. package/dist/render/underwater.js.map +1 -0
  179. package/dist/render/water_caustics.js +1380 -0
  180. package/dist/render/water_caustics.js.map +1 -0
  181. package/dist/render/weightedblended_oit.js +3 -2
  182. package/dist/render/weightedblended_oit.js.map +1 -1
  183. package/dist/scene/basesprite.js +8 -0
  184. package/dist/scene/basesprite.js.map +1 -1
  185. package/dist/scene/batchgroup.js +3 -0
  186. package/dist/scene/batchgroup.js.map +1 -1
  187. package/dist/scene/environment.js +4 -1
  188. package/dist/scene/environment.js.map +1 -1
  189. package/dist/scene/hair.js +963 -0
  190. package/dist/scene/hair.js.map +1 -0
  191. package/dist/scene/light.js +5 -1
  192. package/dist/scene/light.js.map +1 -1
  193. package/dist/scene/mesh.js +481 -47
  194. package/dist/scene/mesh.js.map +1 -1
  195. package/dist/scene/meshdrawable.js +6 -0
  196. package/dist/scene/meshdrawable.js.map +1 -1
  197. package/dist/scene/msdftext.js +3 -0
  198. package/dist/scene/msdftext.js.map +1 -1
  199. package/dist/scene/msdftextsprite.js +3 -0
  200. package/dist/scene/msdftextsprite.js.map +1 -1
  201. package/dist/scene/particlesys.js +6 -0
  202. package/dist/scene/particlesys.js.map +1 -1
  203. package/dist/scene/scene.js +3 -0
  204. package/dist/scene/scene.js.map +1 -1
  205. package/dist/scene/scene_node.js +6 -1
  206. package/dist/scene/scene_node.js.map +1 -1
  207. package/dist/scene/terrain-cm/grass.js +3 -0
  208. package/dist/scene/terrain-cm/grass.js.map +1 -1
  209. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  210. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  211. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  212. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  213. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  214. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  215. package/dist/scene/water.js +546 -4
  216. package/dist/scene/water.js.map +1 -1
  217. package/dist/shaders/atmosphere.js.map +1 -1
  218. package/dist/shaders/diffusion.js +54 -0
  219. package/dist/shaders/diffusion.js.map +1 -0
  220. package/dist/shaders/shadow.js +33 -21
  221. package/dist/shaders/shadow.js.map +1 -1
  222. package/dist/shaders/temporal.js +71 -33
  223. package/dist/shaders/temporal.js.map +1 -1
  224. package/dist/shaders/water.js +22 -19
  225. package/dist/shaders/water.js.map +1 -1
  226. package/dist/shaders/water_medium.js +56 -0
  227. package/dist/shaders/water_medium.js.map +1 -0
  228. package/dist/shadow/dom.js +546 -0
  229. package/dist/shadow/dom.js.map +1 -0
  230. package/dist/shadow/pcf_opt.js +3 -5
  231. package/dist/shadow/pcf_opt.js.map +1 -1
  232. package/dist/shadow/pcf_pd.js +1 -1
  233. package/dist/shadow/pcf_pd.js.map +1 -1
  234. package/dist/shadow/receiver_bias.js +41 -0
  235. package/dist/shadow/receiver_bias.js.map +1 -0
  236. package/dist/shadow/shader.js +20 -60
  237. package/dist/shadow/shader.js.map +1 -1
  238. package/dist/shadow/shadow_impl.js +89 -0
  239. package/dist/shadow/shadow_impl.js.map +1 -1
  240. package/dist/shadow/shadow_region.js +1 -1
  241. package/dist/shadow/shadow_region.js.map +1 -1
  242. package/dist/shadow/shadowmapper.js +150 -37
  243. package/dist/shadow/shadowmapper.js.map +1 -1
  244. package/dist/shapes/shape.js +11 -2
  245. package/dist/shapes/shape.js.map +1 -1
  246. package/dist/shapes/sphere.js +50 -9
  247. package/dist/shapes/sphere.js.map +1 -1
  248. package/dist/utility/blueprint/common/constants.js +112 -2
  249. package/dist/utility/blueprint/common/constants.js.map +1 -1
  250. package/dist/utility/blueprint/material/func.js +142 -49
  251. package/dist/utility/blueprint/material/func.js.map +1 -1
  252. package/dist/utility/blueprint/material/ir.js +490 -25
  253. package/dist/utility/blueprint/material/ir.js.map +1 -1
  254. package/dist/utility/blueprint/material/texture.js +898 -4
  255. package/dist/utility/blueprint/material/texture.js.map +1 -1
  256. package/dist/utility/serialization/manager.js +434 -101
  257. package/dist/utility/serialization/manager.js.map +1 -1
  258. package/dist/utility/serialization/scene/animation.js +401 -5
  259. package/dist/utility/serialization/scene/animation.js.map +1 -1
  260. package/dist/utility/serialization/scene/batch.js +4 -0
  261. package/dist/utility/serialization/scene/batch.js.map +1 -1
  262. package/dist/utility/serialization/scene/camera.js +186 -26
  263. package/dist/utility/serialization/scene/camera.js.map +1 -1
  264. package/dist/utility/serialization/scene/common.js +26 -5
  265. package/dist/utility/serialization/scene/common.js.map +1 -1
  266. package/dist/utility/serialization/scene/hair.js +1141 -0
  267. package/dist/utility/serialization/scene/hair.js.map +1 -0
  268. package/dist/utility/serialization/scene/light.js +84 -4
  269. package/dist/utility/serialization/scene/light.js.map +1 -1
  270. package/dist/utility/serialization/scene/material.js +1867 -166
  271. package/dist/utility/serialization/scene/material.js.map +1 -1
  272. package/dist/utility/serialization/scene/mesh.js +231 -14
  273. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  274. package/dist/utility/serialization/scene/node.js +141 -7
  275. package/dist/utility/serialization/scene/node.js.map +1 -1
  276. package/dist/utility/serialization/scene/particle.js +4 -0
  277. package/dist/utility/serialization/scene/particle.js.map +1 -1
  278. package/dist/utility/serialization/scene/primitive.js +18 -0
  279. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  280. package/dist/utility/serialization/scene/sprite.js +4 -0
  281. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  282. package/dist/utility/serialization/scene/terrain.js +4 -0
  283. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  284. package/dist/utility/serialization/scene/text.js +4 -0
  285. package/dist/utility/serialization/scene/text.js.map +1 -1
  286. package/dist/utility/serialization/scene/water.js +1105 -21
  287. package/dist/utility/serialization/scene/water.js.map +1 -1
  288. package/dist/utility/serialization/types.js.map +1 -1
  289. package/dist/utility/shprojector.js +9 -8
  290. package/dist/utility/shprojector.js.map +1 -1
  291. package/dist/values.js +49 -2
  292. package/dist/values.js.map +1 -1
  293. package/package.json +3 -3
  294. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  295. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  296. package/dist/shaders/ssgi.js +0 -63
  297. package/dist/shaders/ssgi.js.map +0 -1
  298. package/dist/shaders/ssgi_reprojection.js +0 -118
  299. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -1,14 +1,17 @@
1
- import { Vector2, Vector4, DEPTH_FARTHEST } from '@zephyr3d/base';
1
+ import { Vector4, Vector3, Vector2, DEPTH_FARTHEST } from '@zephyr3d/base';
2
2
  import { ShaderHelper } from '../material/shader/helper.js';
3
3
  import '../material/lambert.js';
4
4
  import '../material/blinn.js';
5
5
  import '../material/unlit.js';
6
6
  import '../material/particle.js';
7
- import '../material/subsurfaceprofile.js';
8
- import '../material/skin.js';
7
+ import { SubsurfaceProfile } from '../material/subsurfaceprofile.js';
8
+ import { SKIN_SSS_LDR_ENCODE_RANGE } from '../material/skin.js';
9
+ import '../material/eye.js';
9
10
  import '@zephyr3d/device';
10
11
  import '../values.js';
11
12
  import '../material/meshmaterial.js';
13
+ import '../material/hair.js';
14
+ import '../material/hairstrand.js';
12
15
  import '../material/grassmaterial.js';
13
16
  import '../material/terrain-cm.js';
14
17
  import '../material/pbrmr.js';
@@ -18,55 +21,183 @@ import '../material/pbrblueprint.js';
18
21
  import '../material/sprite.js';
19
22
  import '../material/sprite_std.js';
20
23
  import '../utility/blueprint/material/texture.js';
24
+ import { burleyDiffusionWeight } from '../shaders/diffusion.js';
21
25
  import { linearToGamma } from '../shaders/misc.js';
22
26
  import { fetchSampler } from '../utility/misc.js';
23
27
  import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
24
28
 
29
+ /** Taps per side of the separable kernel. The full kernel is `2 * TAPS + 1` wide. */ const SKIN_SSS_TAPS = 8;
25
30
  /**
26
31
  * Stylized screen-space skin scattering pass.
27
32
  *
28
33
  * @remarks
29
- * This is intentionally separate from the profile-based {@link SSS} pass. It follows the simpler
30
- * depth-aware blur used by many character renderers: a skin material writes a lighting multiplier
31
- * into a side buffer, then this pass performs a fixed 9x9 depth-aware blur and composites it back
32
- * over the opaque color.
34
+ * {@link SkinMaterial} writes the diffusible part of its lit color - the stylized diffuse ramp
35
+ * plus back-lit transmission, without specular - into a side buffer. This pass diffuses that
36
+ * buffer with a channel-dependent kernel and swaps it back in:
37
+ *
38
+ * ```
39
+ * result = base + (diffused - original) * scatterTint * strength + diffused * glow
40
+ * ```
41
+ *
42
+ * Because the term added back replaces exactly the term subtracted, the redistribution is energy
43
+ * conserving: light that appears on the dark side of the terminator is light that left the lit
44
+ * side, rather than light invented on top of a finished image. With `scatterTint` white and
45
+ * `glow` zero the total radiance is preserved to within the kernel's normalization error.
46
+ *
47
+ * Two things follow from that formulation and are worth knowing:
48
+ *
49
+ * - Nothing happens where the diffusion changes nothing, so a disabled or zero-radius pass is a
50
+ * true no-op, and quantization from an 8-bit side buffer largely cancels between the subtracted
51
+ * and added terms instead of banding across the whole face.
52
+ * - `scatterTint` colors only the light that actually moved. Tinting the whole diffuse - what the
53
+ * material used to do with `scatterColor` - is what made the old look read as red haze.
54
+ *
55
+ * Stylization survives in two places. The material owns the direct lighting ramp (wrap, softness,
56
+ * shadow tint, brightening) and this pass diffuses whatever ramp it produced; and {@link profile}
57
+ * selects the channel radii, so the `wax` and `jade` presets are as reachable as `skin`.
58
+ *
59
+ * This is intentionally separate from the profile-based {@link SSS} pass, which supports per-pixel
60
+ * profiles via a profile-slot MRT. Here a single global profile lives in uniforms.
33
61
  *
34
62
  * @public
35
63
  */ class SkinSSS extends AbstractPostEffect {
36
- static _program = null;
37
- _bindGroup;
64
+ static _blurProgram = null;
65
+ static _combineProgram = null;
66
+ _blurBindGroupH;
67
+ _blurBindGroupV;
68
+ _combineBindGroup;
69
+ _profile;
38
70
  _strength;
71
+ _glow;
39
72
  _opacity;
40
73
  _sampleStep;
41
74
  _depthScale;
42
75
  _colorBoost;
76
+ _scatterRadius;
77
+ _smoothness;
78
+ _scatterTint;
79
+ _channelRadius;
80
+ _channelFalloff;
81
+ _blurDirection;
82
+ _radiusParams;
83
+ _targetSize;
84
+ _cameraNearFar;
43
85
  constructor(){
44
86
  super();
45
87
  this._layer = PostEffectLayer.opaque;
46
- this._bindGroup = null;
88
+ this._blurBindGroupH = null;
89
+ this._blurBindGroupV = null;
90
+ this._combineBindGroup = null;
91
+ this._profile = null;
47
92
  this._strength = 1;
93
+ this._glow = 0;
48
94
  this._opacity = 0.18;
49
- this._sampleStep = 2;
95
+ this._sampleStep = 3;
50
96
  this._depthScale = 80;
51
97
  this._colorBoost = 1;
98
+ this._scatterRadius = 0.02;
99
+ this._smoothness = 0;
100
+ this._scatterTint = new Vector4(1, 1, 1, 1);
101
+ this._channelRadius = new Vector3(1, 1, 1);
102
+ this._channelFalloff = new Vector3(1, 1, 1);
103
+ this._blurDirection = new Vector2();
104
+ this._radiusParams = new Vector4();
105
+ this._targetSize = new Vector4();
106
+ this._cameraNearFar = new Vector2();
107
+ }
108
+ /**
109
+ * Subsurface profile supplying the per-channel scatter radii and falloff.
110
+ *
111
+ * @remarks
112
+ * Defaults to {@link SubsurfaceProfile.getDefaultSkinProfile} when unset. Only the channel
113
+ * ratios of `scatterRadius` and the `falloffColor` are read; the absolute distance comes from
114
+ * {@link SkinSSS.scatterRadius}, so switching profiles changes the color of the bleed without
115
+ * changing how far it reaches.
116
+ *
117
+ * The channel ratios are what make skin read as skin - red travels several times further than
118
+ * blue, which is the red-to-orange-to-yellow gradient at the terminator. They are also the main
119
+ * stylization lever: the `wax` and `jade` presets are the same code path with different ratios.
120
+ */ get profile() {
121
+ return this._profile;
52
122
  }
53
- /** Final blend strength. */ get strength() {
123
+ set profile(val) {
124
+ this._profile = val ?? null;
125
+ }
126
+ /** Final blend strength of the conserving redistribution. 1 fully diffuses the skin diffuse. */ get strength() {
54
127
  return this._strength;
55
128
  }
56
129
  set strength(val) {
57
130
  this._strength = Math.max(0, val ?? 0);
58
131
  }
59
- /** Bias subtracted from the blurred skin mask before compositing. */ get opacity() {
132
+ /**
133
+ * Additive, deliberately non-conserving bleed layered on top of the conserving result.
134
+ *
135
+ * @remarks
136
+ * 0 (the default) keeps the pass energy conserving. Raising it adds the diffused term a second
137
+ * time without subtracting anything, which is the glow-heavy look the pass produced before it
138
+ * conserved energy; around 1 approximates it. Use it when the art direction wants skin to read
139
+ * as lit from within rather than as a physical surface.
140
+ */ get glow() {
141
+ return this._glow;
142
+ }
143
+ set glow(val) {
144
+ this._glow = Math.max(0, val ?? 0);
145
+ }
146
+ /**
147
+ * Tint applied to the redistributed light.
148
+ *
149
+ * @remarks
150
+ * White (the default) leaves the pass energy conserving. A warm tint pushes the classic reddish
151
+ * terminator further than the profile's channel radii do on their own. Because it multiplies
152
+ * only the difference between the diffused and original diffuse, it cannot wash the whole
153
+ * surface - only the light that moved.
154
+ */ get scatterTint() {
155
+ return this._scatterTint;
156
+ }
157
+ set scatterTint(val) {
158
+ this._scatterTint.set(val);
159
+ }
160
+ /** Skin mask coverage threshold. Scattering fades in as the blurred mask coverage exceeds this. */ get opacity() {
60
161
  return this._opacity;
61
162
  }
62
163
  set opacity(val) {
63
164
  this._opacity = Math.max(0, Math.min(1, val ?? 0));
64
165
  }
65
- /** Pixel spacing between blur taps. The reference shader uses 2. */ get sampleStep() {
166
+ /**
167
+ * Maximum pixel spacing between blur taps. Caps the projected scatter radius for close-ups.
168
+ *
169
+ * @remarks
170
+ * With `SKIN_SSS_TAPS` taps per side the kernel reaches `sampleStep * SKIN_SSS_TAPS` pixels, so
171
+ * this is the real limit on scatter width. The separable form makes taps cheap enough that the
172
+ * default no longer has to be tight: the old single-pass 9x9 kernel capped out at 8 pixels, and
173
+ * a face in close-up projects a 2 cm scatter radius far past that - the shot that needs the most
174
+ * scattering used to get the least.
175
+ */ get sampleStep() {
66
176
  return this._sampleStep;
67
177
  }
68
178
  set sampleStep(val) {
69
- this._sampleStep = Math.max(0.25, val ?? 0.25);
179
+ this._sampleStep = Math.max(0.25, Math.min(6, val ?? 0.25));
180
+ }
181
+ /** World-space scatter radius. The blur width shrinks with distance to keep this constant. */ get scatterRadius() {
182
+ return this._scatterRadius;
183
+ }
184
+ set scatterRadius(val) {
185
+ this._scatterRadius = Math.max(0, val ?? 0);
186
+ }
187
+ /**
188
+ * Skin smoothing amount ("beauty filter"). Blends the lit color toward a mask- and depth-weighted
189
+ * blur of itself on skin pixels, removing pore and albedo detail while facial features stay sharp
190
+ * wherever the skin mask excludes them (eyes, brows, lips). 0 disables smoothing.
191
+ *
192
+ * @remarks
193
+ * This is separate from the diffusion and stacks on top of it. The diffusion blurs *lighting*
194
+ * only; albedo detail such as pores and blemishes passes through it untouched, which is correct
195
+ * but is not what a beauty filter is for.
196
+ */ get smoothness() {
197
+ return this._smoothness;
198
+ }
199
+ set smoothness(val) {
200
+ this._smoothness = Math.max(0, Math.min(1, val ?? 0));
70
201
  }
71
202
  /** Depth rejection scale. The reference shader uses 80. */ get depthScale() {
72
203
  return this._depthScale;
@@ -74,7 +205,7 @@ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
74
205
  set depthScale(val) {
75
206
  this._depthScale = Math.max(0, val ?? 0);
76
207
  }
77
- /** Multiplier applied to the blurred skin lighting multiplier before compositing. */ get colorBoost() {
208
+ /** Multiplier applied to the diffused irradiance before compositing. */ get colorBoost() {
78
209
  return this._colorBoost;
79
210
  }
80
211
  set colorBoost(val) {
@@ -89,55 +220,204 @@ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
89
220
  return;
90
221
  }
91
222
  const device = ctx.device;
92
- let program = SkinSSS._program;
93
- if (!program) {
94
- program = this.createProgram(ctx);
95
- SkinSSS._program = program;
223
+ const outputFramebuffer = device.getFramebuffer();
224
+ const skinTex = ctx.SkinSSSTexture;
225
+ if (!SkinSSS._blurProgram) {
226
+ SkinSSS._blurProgram = this.createBlurProgram(ctx);
227
+ }
228
+ if (!SkinSSS._combineProgram) {
229
+ SkinSSS._combineProgram = this.createCombineProgram(ctx);
96
230
  }
97
- if (!this._bindGroup) {
98
- this._bindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
231
+ const blurProgram = SkinSSS._blurProgram;
232
+ const combineProgram = SkinSSS._combineProgram;
233
+ if (!this._blurBindGroupH) {
234
+ // Two bind groups rather than one program per direction: the only thing
235
+ // that differs is the step vector, and rewriting it between draws would
236
+ // race the first draw's read of it.
237
+ this._blurBindGroupH = device.createBindGroup(blurProgram.bindGroupLayouts[0]);
238
+ this._blurBindGroupV = device.createBindGroup(blurProgram.bindGroupLayouts[0]);
99
239
  }
100
- this._bindGroup.setTexture('colorTex', inputColorTexture, fetchSampler('clamp_linear'));
101
- this._bindGroup.setTexture('skinTex', ctx.SkinSSSTexture, fetchSampler('clamp_linear'));
102
- this._bindGroup.setTexture('depthTex', sceneDepthTexture, fetchSampler('clamp_nearest_nomip'));
103
- this._bindGroup.setValue('cameraNearFar', new Vector2(ctx.camera.getNearPlane(), ctx.camera.getFarPlane()));
104
- this._bindGroup.setValue('targetSize', new Vector4(inputColorTexture.width, inputColorTexture.height, inputColorTexture.width, inputColorTexture.height));
105
- this._bindGroup.setValue('strength', this._strength);
106
- this._bindGroup.setValue('opacity', this._opacity);
107
- this._bindGroup.setValue('sampleStep', this._sampleStep);
108
- this._bindGroup.setValue('depthScale', this._depthScale);
109
- this._bindGroup.setValue('colorBoost', this._colorBoost);
110
- this._bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
111
- this._bindGroup.setValue('srgbOut', srgbOutput ? 1 : 0);
240
+ if (!this._combineBindGroup) {
241
+ this._combineBindGroup = device.createBindGroup(combineProgram.bindGroupLayouts[0]);
242
+ }
243
+ const profile = this._profile ?? SubsurfaceProfile.getDefaultSkinProfile();
244
+ const radius = profile.scatterRadius;
245
+ const maxChannel = Math.max(radius.x, radius.y, radius.z, 1e-5);
246
+ this._channelRadius.setXYZ(radius.x / maxChannel, radius.y / maxChannel, radius.z / maxChannel);
247
+ const falloff = profile.falloffColor;
248
+ this._channelFalloff.setXYZ(falloff.x, falloff.y, falloff.z);
249
+ // Pixels per world unit at unit view depth. The shader divides by view depth
250
+ // so the footprint stays a constant world-space radius.
251
+ const projScale = 0.5 * inputColorTexture.height * ctx.camera.getProjectionMatrix().m11;
252
+ this._radiusParams.setXYZW(this._scatterRadius * projScale, ctx.camera.isPerspective() ? 1 : 0, this._sampleStep, this._depthScale);
253
+ this._targetSize.setXYZW(inputColorTexture.width, inputColorTexture.height, 1 / inputColorTexture.width, 1 / inputColorTexture.height);
254
+ this._cameraNearFar.setXY(ctx.camera.getNearPlane(), ctx.camera.getFarPlane());
255
+ const encodeScale = skinTex.format === 'rgba8unorm' ? SKIN_SSS_LDR_ENCODE_RANGE : 1;
256
+ const blurH = device.pool.fetchTemporalFramebuffer(false, skinTex.width, skinTex.height, skinTex.format, null, false);
257
+ const blurV = device.pool.fetchTemporalFramebuffer(false, skinTex.width, skinTex.height, skinTex.format, null, false);
258
+ device.setFramebuffer(blurH);
259
+ this.blur(ctx, blurProgram, this._blurBindGroupH, skinTex, sceneDepthTexture, true);
260
+ device.setFramebuffer(blurV);
261
+ this.blur(ctx, blurProgram, this._blurBindGroupV, blurH.getColorAttachments()[0], sceneDepthTexture, false);
262
+ device.setFramebuffer(outputFramebuffer);
263
+ this.combine(ctx, combineProgram, inputColorTexture, skinTex, blurV.getColorAttachments()[0], sceneDepthTexture, encodeScale, srgbOutput);
264
+ device.pool.releaseFrameBuffer(blurH);
265
+ device.pool.releaseFrameBuffer(blurV);
266
+ }
267
+ blur(ctx, program, bindGroup, source, sceneDepthTexture, horizontal) {
268
+ const device = ctx.device;
269
+ this._blurDirection.setXY(horizontal ? 1 : 0, horizontal ? 0 : 1);
270
+ bindGroup.setTexture('skinTex', source, fetchSampler('clamp_linear'));
271
+ bindGroup.setTexture('depthTex', sceneDepthTexture, fetchSampler('clamp_nearest_nomip'));
272
+ bindGroup.setValue('cameraNearFar', this._cameraNearFar);
273
+ bindGroup.setValue('targetSize', this._targetSize);
274
+ bindGroup.setValue('radiusParams', this._radiusParams);
275
+ bindGroup.setValue('channelRadius', this._channelRadius);
276
+ bindGroup.setValue('channelFalloff', this._channelFalloff);
277
+ bindGroup.setValue('blurDirection', this._blurDirection);
278
+ bindGroup.setValue('flip', 0);
112
279
  device.setProgram(program);
113
- device.setBindGroup(0, this._bindGroup);
280
+ device.setBindGroup(0, bindGroup);
114
281
  this.drawFullscreenQuad();
115
282
  }
116
- createProgram(ctx) {
283
+ combine(ctx, program, colorTexture, skinTexture, blurredTexture, sceneDepthTexture, encodeScale, srgbOutput) {
284
+ const device = ctx.device;
285
+ const bindGroup = this._combineBindGroup;
286
+ bindGroup.setTexture('colorTex', colorTexture, fetchSampler('clamp_linear'));
287
+ bindGroup.setTexture('skinTex', skinTexture, fetchSampler('clamp_linear'));
288
+ bindGroup.setTexture('blurredTex', blurredTexture, fetchSampler('clamp_linear'));
289
+ bindGroup.setTexture('depthTex', sceneDepthTexture, fetchSampler('clamp_nearest_nomip'));
290
+ bindGroup.setValue('cameraNearFar', this._cameraNearFar);
291
+ bindGroup.setValue('targetSize', this._targetSize);
292
+ bindGroup.setValue('radiusParams', this._radiusParams);
293
+ bindGroup.setValue('scatterTint', this._scatterTint);
294
+ bindGroup.setValue('strength', this._strength);
295
+ bindGroup.setValue('glow', this._glow);
296
+ bindGroup.setValue('opacity', this._opacity);
297
+ bindGroup.setValue('colorBoost', this._colorBoost);
298
+ bindGroup.setValue('encodeScale', encodeScale);
299
+ bindGroup.setValue('smoothness', this._smoothness);
300
+ bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
301
+ bindGroup.setValue('srgbOut', srgbOutput ? 1 : 0);
302
+ device.setProgram(program);
303
+ device.setBindGroup(0, bindGroup);
304
+ this.drawFullscreenQuad();
305
+ }
306
+ /** Shared fullscreen vertex stage for both passes. */ static fullscreenVertex(pb) {
307
+ pb.getGlobalScope().flip = pb.int().uniform(0);
308
+ pb.getGlobalScope().$inputs.pos = pb.vec2().attrib('position');
309
+ pb.getGlobalScope().$outputs.uv = pb.vec2();
310
+ pb.main(function() {
311
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
312
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
313
+ this.$if(pb.notEqual(this.flip, 0), function() {
314
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
315
+ });
316
+ });
317
+ }
318
+ createBlurProgram(ctx) {
117
319
  const program = ctx.device.buildRenderProgram({
118
320
  vertex (pb) {
119
- this.flip = pb.int().uniform(0);
120
- this.$inputs.pos = pb.vec2().attrib('position');
121
- this.$outputs.uv = pb.vec2();
321
+ SkinSSS.fullscreenVertex(pb);
322
+ },
323
+ fragment (pb) {
324
+ this.skinTex = pb.tex2D().uniform(0);
325
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
326
+ this.cameraNearFar = pb.vec2().uniform(0);
327
+ this.targetSize = pb.vec4().uniform(0);
328
+ // x: world scatter radius in pixels at unit depth, y: 1 for perspective,
329
+ // z: max tap spacing, w: depth rejection scale.
330
+ this.radiusParams = pb.vec4().uniform(0);
331
+ this.channelRadius = pb.vec3().uniform(0);
332
+ this.channelFalloff = pb.vec3().uniform(0);
333
+ this.blurDirection = pb.vec2().uniform(0);
334
+ this.$outputs.outColor = pb.vec4();
335
+ pb.func('readDepth01', [
336
+ pb.vec2('uv')
337
+ ], function() {
338
+ this.$return(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0));
339
+ });
122
340
  pb.main(function() {
123
- this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
124
- this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
125
- this.$if(pb.notEqual(this.flip, 0), function() {
126
- this.$builtins.position.y = pb.neg(this.$builtins.position.y);
341
+ this.$l.uv = this.$inputs.uv;
342
+ this.$l.center = pb.textureSampleLevel(this.skinTex, this.uv, 0);
343
+ this.$l.centerDepth01 = this.readDepth01(this.uv);
344
+ this.$outputs.outColor = this.center;
345
+ // Background, or a pixel no skin material wrote: nothing to diffuse.
346
+ this.$if(pb.and(pb.lessThan(this.centerDepth01, 1), pb.greaterThan(this.center.a, 1e-4)), function() {
347
+ this.$l.centerDepth = pb.max(pb.mul(this.centerDepth01, this.cameraNearFar.y), 1e-4);
348
+ // Per-channel radius in pixels. Red reaches several times further
349
+ // than blue, which is the whole point - a shared radius is what
350
+ // makes Gaussian SSS read as a flat haze.
351
+ this.$l.viewScale = pb.max(pb.mix(pb.float(1), this.centerDepth, this.radiusParams.y), 1e-4);
352
+ this.$l.radiusPx = pb.div(pb.mul(this.channelRadius, this.radiusParams.x), this.viewScale);
353
+ // Burley shaping distance per channel; falloff tightens or widens
354
+ // the profile without changing its reach.
355
+ this.$l.shape = pb.vec3(pb.mix(0.72, 1.46, pb.clamp(this.channelFalloff.x, 0.05, 1)), pb.mix(0.72, 1.46, pb.clamp(this.channelFalloff.y, 0.05, 1)), pb.mix(0.72, 1.46, pb.clamp(this.channelFalloff.z, 0.05, 1)));
356
+ this.$l.shapeDist = pb.max(pb.mul(this.radiusPx, this.shape), pb.vec3(1e-3));
357
+ // Tap spacing follows the widest channel so the longest tail is
358
+ // resolved; the narrower channels simply weight down sooner.
359
+ this.$l.maxRadiusPx = pb.max(pb.max(this.radiusPx.x, this.radiusPx.y), this.radiusPx.z);
360
+ this.$l.stepPx = pb.clamp(pb.div(this.maxRadiusPx, SKIN_SSS_TAPS), 0, this.radiusParams.z);
361
+ this.$l.stepUV = pb.mul(this.blurDirection, this.stepPx, this.targetSize.zw);
362
+ this.$l.sum = pb.vec3(0);
363
+ this.$l.weightSum = pb.vec3(0);
364
+ this.$l.coverage = pb.float(0);
365
+ this.$l.coverageWeight = pb.float(0);
366
+ this.$for(pb.int('i'), -8, SKIN_SSS_TAPS + 1, function() {
367
+ this.$l.fi = pb.float(this.i);
368
+ this.$l.sampleUV = pb.clamp(pb.add(this.uv, pb.mul(this.stepUV, this.fi)), pb.vec2(0), pb.vec2(1));
369
+ this.$l.dist = pb.mul(pb.abs(this.fi), this.stepPx);
370
+ this.$l.w = pb.vec3(burleyDiffusionWeight(this, this.dist, this.shapeDist.x), burleyDiffusionWeight(this, this.dist, this.shapeDist.y), burleyDiffusionWeight(this, this.dist, this.shapeDist.z));
371
+ this.$l.sampleDepth01 = this.readDepth01(this.sampleUV);
372
+ this.$l.sampleDepth = pb.max(pb.mul(this.sampleDepth01, this.cameraNearFar.y), 1e-4);
373
+ this.$l.depthWeight = pb.clamp(pb.sub(1, pb.div(pb.mul(pb.abs(pb.sub(this.centerDepth, this.sampleDepth)), this.radiusParams.w), this.centerDepth)), 0, 1);
374
+ this.depthWeight = pb.mul(this.depthWeight, this.depthWeight, pb.sub(3, pb.mul(this.depthWeight, 2)));
375
+ this.$l.tapWeight = pb.mul(this.w, this.depthWeight);
376
+ this.$l.tapSample = pb.textureSampleLevel(this.skinTex, this.sampleUV, 0);
377
+ // Non-skin taps are excluded from the numerator *and* the
378
+ // denominator. Leaving them in the denominator - as this pass
379
+ // used to - drags the diffuse toward zero near every silhouette,
380
+ // which was a mild darkening under the old additive composite
381
+ // but would eat real light now that the term is subtracted back
382
+ // out of the base color.
383
+ this.$l.isSkin = pb.float(pb.greaterThan(this.tapSample.a, 1e-4));
384
+ this.sum = pb.add(this.sum, pb.mul(this.tapSample.rgb, this.tapWeight, this.isSkin));
385
+ this.weightSum = pb.add(this.weightSum, pb.mul(this.tapWeight, this.isSkin));
386
+ // Coverage keeps its own accumulator: it is the skin fraction of
387
+ // the neighbourhood, so it must count every tap in the
388
+ // denominator, skin or not.
389
+ this.coverage = pb.add(this.coverage, pb.mul(this.tapWeight.x, this.isSkin, this.tapSample.a));
390
+ this.coverageWeight = pb.add(this.coverageWeight, this.tapWeight.x);
391
+ });
392
+ this.$l.diffused = pb.div(this.sum, pb.max(this.weightSum, pb.vec3(1e-6)));
393
+ this.$outputs.outColor = pb.vec4(this.diffused, pb.div(this.coverage, pb.max(this.coverageWeight, 1e-6)));
127
394
  });
128
395
  });
396
+ }
397
+ });
398
+ program.name = '@SkinSSSBlur';
399
+ return program;
400
+ }
401
+ createCombineProgram(ctx) {
402
+ const program = ctx.device.buildRenderProgram({
403
+ vertex (pb) {
404
+ SkinSSS.fullscreenVertex(pb);
129
405
  },
130
406
  fragment (pb) {
131
407
  this.colorTex = pb.tex2D().uniform(0);
132
408
  this.skinTex = pb.tex2D().uniform(0);
409
+ this.blurredTex = pb.tex2D().uniform(0);
133
410
  this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
134
411
  this.cameraNearFar = pb.vec2().uniform(0);
135
412
  this.targetSize = pb.vec4().uniform(0);
413
+ this.radiusParams = pb.vec4().uniform(0);
414
+ this.scatterTint = pb.vec4().uniform(0);
136
415
  this.strength = pb.float().uniform(0);
416
+ this.glow = pb.float().uniform(0);
137
417
  this.opacity = pb.float().uniform(0);
138
- this.sampleStep = pb.float().uniform(0);
139
- this.depthScale = pb.float().uniform(0);
140
418
  this.colorBoost = pb.float().uniform(0);
419
+ this.encodeScale = pb.float().uniform(0);
420
+ this.smoothness = pb.float().uniform(0);
141
421
  this.srgbOut = pb.int().uniform(0);
142
422
  this.$outputs.outColor = pb.vec4();
143
423
  pb.func('readDepth01', [
@@ -149,33 +429,52 @@ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
149
429
  this.$l.uv = this.$inputs.uv;
150
430
  this.$l.baseColor = pb.textureSampleLevel(this.colorTex, this.uv, 0);
151
431
  this.$l.result = this.baseColor.rgb;
432
+ this.$l.centerSkin = pb.textureSampleLevel(this.skinTex, this.uv, 0);
152
433
  this.$l.centerDepth01 = this.readDepth01(this.uv);
153
- this.$if(pb.lessThan(this.centerDepth01, 1), function() {
154
- this.$l.centerDepth = pb.max(pb.mul(this.centerDepth01, this.cameraNearFar.y), 1e-4);
155
- this.$l.sum = pb.vec4(0);
156
- this.$l.weightSum = pb.float(0);
157
- this.$for(pb.int('y'), -4, 5, function() {
158
- this.$for(pb.int('x'), -4, 5, function() {
159
- this.$l.offsetPx = pb.mul(pb.vec2(pb.float(this.x), pb.float(this.y)), this.sampleStep);
160
- this.$l.sampleUV = pb.clamp(pb.add(this.uv, pb.div(this.offsetPx, this.targetSize.xy)), pb.vec2(0), pb.vec2(1));
161
- this.$l.skinSample = pb.textureSampleLevel(this.skinTex, this.sampleUV, 0);
162
- this.$if(pb.lessThanEqual(this.skinSample.a, 1e-4), function() {
163
- this.skinSample = pb.vec4(0, 0, 0, this.opacity);
434
+ this.$if(pb.and(pb.lessThan(this.centerDepth01, 1), pb.greaterThan(this.centerSkin.a, 1e-4)), function() {
435
+ this.$l.blurred = pb.textureSampleLevel(this.blurredTex, this.uv, 0);
436
+ // Both buffers carry the same LDR encoding, so decode once here.
437
+ this.$l.original = pb.mul(this.centerSkin.rgb, this.encodeScale);
438
+ this.$l.diffused = pb.mul(this.blurred.rgb, this.encodeScale, this.colorBoost);
439
+ this.$l.coverage = pb.smoothStep(this.opacity, pb.add(this.opacity, 0.35), this.blurred.a);
440
+ // Energy conserving core: swap the original diffuse for the
441
+ // diffused one. The added term is exactly the subtracted term, so
442
+ // brightening the dark side of the terminator darkens the lit side
443
+ // by the same amount rather than adding light to the frame.
444
+ this.$l.redistributed = pb.mul(pb.sub(this.diffused, this.original), this.scatterTint.rgb, this.strength, this.coverage);
445
+ this.result = pb.add(this.baseColor.rgb, this.redistributed);
446
+ // Explicit, opt-in departure from conservation.
447
+ this.$if(pb.greaterThan(this.glow, 0), function() {
448
+ this.result = pb.add(this.result, pb.mul(this.diffused, this.scatterTint.rgb, this.glow, this.coverage));
449
+ });
450
+ // Beauty filter. Unlike the diffusion above this blurs the shaded
451
+ // color itself, so it removes albedo detail (pores, blemishes)
452
+ // that scattering physically leaves alone. Uniform branch: free
453
+ // when disabled.
454
+ this.$if(pb.greaterThan(this.smoothness, 0), function() {
455
+ this.$l.centerDepth = pb.max(pb.mul(this.centerDepth01, this.cameraNearFar.y), 1e-4);
456
+ this.$l.viewScale = pb.max(pb.mix(pb.float(1), this.centerDepth, this.radiusParams.y), 1e-4);
457
+ this.$l.smoothStepPx = pb.clamp(pb.div(pb.mul(this.radiusParams.x, 0.5), this.viewScale), 0, this.radiusParams.z);
458
+ this.$l.colorSum = pb.vec3(0);
459
+ this.$l.colorWeight = pb.float(0);
460
+ this.$for(pb.int('y'), -2, 3, function() {
461
+ this.$for(pb.int('x'), -2, 3, function() {
462
+ this.$l.offset = pb.mul(pb.vec2(pb.float(this.x), pb.float(this.y)), this.smoothStepPx, this.targetSize.zw);
463
+ this.$l.sampleUV = pb.clamp(pb.add(this.uv, this.offset), pb.vec2(0), pb.vec2(1));
464
+ // The skin mask is the edge stop, so eyes, brows and lips
465
+ // (mask 0) never bleed onto the surrounding skin.
466
+ this.$l.maskSample = pb.textureSampleLevel(this.skinTex, this.sampleUV, 0).a;
467
+ this.$l.w = pb.float(pb.greaterThan(this.maskSample, 1e-4));
468
+ this.colorSum = pb.add(this.colorSum, pb.mul(pb.textureSampleLevel(this.colorTex, this.sampleUV, 0).rgb, this.w));
469
+ this.colorWeight = pb.add(this.colorWeight, this.w);
164
470
  });
165
- this.$l.sampleDepth01 = this.readDepth01(this.sampleUV);
166
- this.$l.sampleDepth = pb.max(pb.mul(this.sampleDepth01, this.cameraNearFar.y), 1e-4);
167
- this.$l.depthWeight = pb.clamp(pb.sub(1, pb.div(pb.mul(pb.abs(pb.sub(this.centerDepth, this.sampleDepth)), this.depthScale), this.centerDepth)), 0, 1);
168
- this.depthWeight = pb.mul(this.depthWeight, this.depthWeight, pb.sub(3, pb.mul(this.depthWeight, 2)));
169
- this.sum = pb.add(this.sum, pb.mul(this.skinSample, this.depthWeight));
170
- this.weightSum = pb.add(this.weightSum, this.depthWeight);
471
+ });
472
+ this.$if(pb.greaterThan(this.colorWeight, 1e-4), function() {
473
+ this.result = pb.mix(this.result, pb.div(this.colorSum, this.colorWeight), pb.mul(this.smoothness, this.coverage, this.centerSkin.a));
171
474
  });
172
475
  });
173
- this.$if(pb.greaterThan(this.weightSum, 1e-4), function() {
174
- this.$l.blurredSkin = pb.div(this.sum, this.weightSum);
175
- this.$l.alphaDelta = pb.sub(this.blurredSkin.a, this.opacity);
176
- this.$l.referenceLit = pb.mul(this.baseColor.rgb, pb.add(pb.vec3(pb.sub(1, this.alphaDelta)), pb.mul(this.blurredSkin.rgb, pb.max(pb.sub(1, this.opacity), 0), this.colorBoost)));
177
- this.result = pb.add(this.baseColor.rgb, pb.mul(pb.sub(this.referenceLit, this.baseColor.rgb), this.strength));
178
- });
476
+ // Precision can push the subtraction a hair below zero.
477
+ this.result = pb.max(this.result, pb.vec3(0));
179
478
  });
180
479
  this.$if(pb.equal(this.srgbOut, 0), function() {
181
480
  this.$outputs.outColor = pb.vec4(this.result, this.baseColor.a);
@@ -185,7 +484,7 @@ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
185
484
  });
186
485
  }
187
486
  });
188
- program.name = '@SkinSSS';
487
+ program.name = '@SkinSSSCombine';
189
488
  return program;
190
489
  }
191
490
  }