@zephyr3d/scene 0.9.25 → 0.9.26

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 (262) hide show
  1. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  2. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  3. package/dist/animation/joint_dynamics/controller.js +7 -5
  4. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  5. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  6. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  7. package/dist/animation/morphtrack.js +3 -4
  8. package/dist/animation/morphtrack.js.map +1 -1
  9. package/dist/animation/skeleton.js +58 -24
  10. package/dist/animation/skeleton.js.map +1 -1
  11. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  12. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  13. package/dist/animation/spring/spring_collider.js +4 -3
  14. package/dist/animation/spring/spring_collider.js.map +1 -1
  15. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  16. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  17. package/dist/animation/spring/spring_particle.js +2 -0
  18. package/dist/animation/spring/spring_particle.js.map +1 -1
  19. package/dist/animation/spring/spring_system.js +11 -3
  20. package/dist/animation/spring/spring_system.js.map +1 -1
  21. package/dist/animation/spring_modifier.js +10 -0
  22. package/dist/animation/spring_modifier.js.map +1 -1
  23. package/dist/app/engine.js +53 -43
  24. package/dist/app/engine.js.map +1 -1
  25. package/dist/app/scriptingsystem.js +119 -6
  26. package/dist/app/scriptingsystem.js.map +1 -1
  27. package/dist/app/scriptregistry.js +3 -2
  28. package/dist/app/scriptregistry.js.map +1 -1
  29. package/dist/asset/assetmanager.js +284 -67
  30. package/dist/asset/assetmanager.js.map +1 -1
  31. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  32. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  33. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  34. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  35. package/dist/asset/model.js +178 -67
  36. package/dist/asset/model.js.map +1 -1
  37. package/dist/camera/camera.js +193 -21
  38. package/dist/camera/camera.js.map +1 -1
  39. package/dist/camera/perspectivecamera.js +3 -1
  40. package/dist/camera/perspectivecamera.js.map +1 -1
  41. package/dist/index.d.ts +4019 -1278
  42. package/dist/index.js +15 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/material/eye.js +932 -0
  45. package/dist/material/eye.js.map +1 -0
  46. package/dist/material/hair.js +917 -0
  47. package/dist/material/hair.js.map +1 -0
  48. package/dist/material/hairstrand.js +842 -0
  49. package/dist/material/hairstrand.js.map +1 -0
  50. package/dist/material/hairstrand_data.js +162 -0
  51. package/dist/material/hairstrand_data.js.map +1 -0
  52. package/dist/material/lambert.js +5 -2
  53. package/dist/material/lambert.js.map +1 -1
  54. package/dist/material/material.js +338 -300
  55. package/dist/material/material.js.map +1 -1
  56. package/dist/material/meshmaterial.js +120 -19
  57. package/dist/material/meshmaterial.js.map +1 -1
  58. package/dist/material/mixins/albedocolor.js +1 -1
  59. package/dist/material/mixins/albedocolor.js.map +1 -1
  60. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  61. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  62. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  63. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  65. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  67. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  70. package/dist/material/mixins/lit.js +73 -18
  71. package/dist/material/mixins/lit.js.map +1 -1
  72. package/dist/material/mixins/pbr/common.js +4 -4
  73. package/dist/material/mixins/pbr/common.js.map +1 -1
  74. package/dist/material/mixins/texture.js +48 -11
  75. package/dist/material/mixins/texture.js.map +1 -1
  76. package/dist/material/mtoon.js +6 -5
  77. package/dist/material/mtoon.js.map +1 -1
  78. package/dist/material/pbrblueprint.js +146 -28
  79. package/dist/material/pbrblueprint.js.map +1 -1
  80. package/dist/material/pbrblueprint_instance.js +501 -0
  81. package/dist/material/pbrblueprint_instance.js.map +1 -0
  82. package/dist/material/pbrmr.js +15 -16
  83. package/dist/material/pbrmr.js.map +1 -1
  84. package/dist/material/shader/helper.js +330 -20
  85. package/dist/material/shader/helper.js.map +1 -1
  86. package/dist/material/shader/terrain_blend.js +55 -0
  87. package/dist/material/shader/terrain_blend.js.map +1 -0
  88. package/dist/material/skin.js +192 -32
  89. package/dist/material/skin.js.map +1 -1
  90. package/dist/material/spriteblueprint.js +16 -1
  91. package/dist/material/spriteblueprint.js.map +1 -1
  92. package/dist/material/subsurfaceprofile.js +86 -23
  93. package/dist/material/subsurfaceprofile.js.map +1 -1
  94. package/dist/material/water.js +3 -1
  95. package/dist/material/water.js.map +1 -1
  96. package/dist/posteffect/bloom.js +167 -7
  97. package/dist/posteffect/bloom.js.map +1 -1
  98. package/dist/posteffect/coloradjust.js +2 -1
  99. package/dist/posteffect/coloradjust.js.map +1 -1
  100. package/dist/posteffect/compositor.js.map +1 -1
  101. package/dist/posteffect/fxaa.js +2 -1
  102. package/dist/posteffect/fxaa.js.map +1 -1
  103. package/dist/posteffect/grayscale.js +3 -1
  104. package/dist/posteffect/grayscale.js.map +1 -1
  105. package/dist/posteffect/motionblur.js +4 -1
  106. package/dist/posteffect/motionblur.js.map +1 -1
  107. package/dist/posteffect/posteffect.js.map +1 -1
  108. package/dist/posteffect/skinsss.js +368 -69
  109. package/dist/posteffect/skinsss.js.map +1 -1
  110. package/dist/posteffect/ssgi.js +16 -9
  111. package/dist/posteffect/ssgi.js.map +1 -1
  112. package/dist/posteffect/ssr.js +3 -0
  113. package/dist/posteffect/ssr.js.map +1 -1
  114. package/dist/posteffect/sss.js +91 -34
  115. package/dist/posteffect/sss.js.map +1 -1
  116. package/dist/posteffect/taa.js.map +1 -1
  117. package/dist/posteffect/tonemap.js +44 -1
  118. package/dist/posteffect/tonemap.js.map +1 -1
  119. package/dist/render/abuffer_oit.js +45 -39
  120. package/dist/render/abuffer_oit.js.map +1 -1
  121. package/dist/render/cull_visitor.js +19 -0
  122. package/dist/render/cull_visitor.js.map +1 -1
  123. package/dist/render/depthpass.js +36 -2
  124. package/dist/render/depthpass.js.map +1 -1
  125. package/dist/render/drawable_mixin.js +26 -1
  126. package/dist/render/drawable_mixin.js.map +1 -1
  127. package/dist/render/dualdepthpeeling_oit.js +52 -51
  128. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  129. package/dist/render/envlight.js +1 -1
  130. package/dist/render/envlight.js.map +1 -1
  131. package/dist/render/fbm_wavegenerator.js +3 -0
  132. package/dist/render/fbm_wavegenerator.js.map +1 -1
  133. package/dist/render/gerstner_wavegenerator.js +3 -0
  134. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  135. package/dist/render/globalbindgroup_allocator.js +3 -0
  136. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  137. package/dist/render/oit.js +29 -0
  138. package/dist/render/oit.js.map +1 -0
  139. package/dist/render/render_queue.js +3 -0
  140. package/dist/render/render_queue.js.map +1 -1
  141. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  142. package/dist/render/rendergraph/executor.js +2 -0
  143. package/dist/render/rendergraph/executor.js.map +1 -1
  144. package/dist/render/rendergraph/forward_plus_builder.js +61 -1
  145. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  146. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  147. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  148. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  149. package/dist/render/rendergraph/types.js.map +1 -1
  150. package/dist/render/shadow_mask_pass.js +73 -8
  151. package/dist/render/shadow_mask_pass.js.map +1 -1
  152. package/dist/render/sky.js +166 -126
  153. package/dist/render/sky.js.map +1 -1
  154. package/dist/render/weightedblended_oit.js +36 -35
  155. package/dist/render/weightedblended_oit.js.map +1 -1
  156. package/dist/scene/basesprite.js +8 -0
  157. package/dist/scene/basesprite.js.map +1 -1
  158. package/dist/scene/batchgroup.js +3 -0
  159. package/dist/scene/batchgroup.js.map +1 -1
  160. package/dist/scene/environment.js +4 -1
  161. package/dist/scene/environment.js.map +1 -1
  162. package/dist/scene/hair.js +963 -0
  163. package/dist/scene/hair.js.map +1 -0
  164. package/dist/scene/light.js +5 -1
  165. package/dist/scene/light.js.map +1 -1
  166. package/dist/scene/mesh.js +481 -47
  167. package/dist/scene/mesh.js.map +1 -1
  168. package/dist/scene/meshdrawable.js +6 -0
  169. package/dist/scene/meshdrawable.js.map +1 -1
  170. package/dist/scene/msdftext.js +3 -0
  171. package/dist/scene/msdftext.js.map +1 -1
  172. package/dist/scene/msdftextsprite.js +3 -0
  173. package/dist/scene/msdftextsprite.js.map +1 -1
  174. package/dist/scene/particlesys.js +6 -0
  175. package/dist/scene/particlesys.js.map +1 -1
  176. package/dist/scene/scene.js +3 -0
  177. package/dist/scene/scene.js.map +1 -1
  178. package/dist/scene/scene_node.js +6 -1
  179. package/dist/scene/scene_node.js.map +1 -1
  180. package/dist/scene/terrain-cm/grass.js +3 -0
  181. package/dist/scene/terrain-cm/grass.js.map +1 -1
  182. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  183. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  184. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  185. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  186. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  187. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  188. package/dist/scene/water.js +6 -0
  189. package/dist/scene/water.js.map +1 -1
  190. package/dist/shaders/atmosphere.js.map +1 -1
  191. package/dist/shaders/diffusion.js +54 -0
  192. package/dist/shaders/diffusion.js.map +1 -0
  193. package/dist/shaders/shadow.js +33 -21
  194. package/dist/shaders/shadow.js.map +1 -1
  195. package/dist/shaders/temporal.js +71 -33
  196. package/dist/shaders/temporal.js.map +1 -1
  197. package/dist/shadow/dom.js +546 -0
  198. package/dist/shadow/dom.js.map +1 -0
  199. package/dist/shadow/pcf_opt.js +3 -5
  200. package/dist/shadow/pcf_opt.js.map +1 -1
  201. package/dist/shadow/pcf_pd.js +1 -1
  202. package/dist/shadow/pcf_pd.js.map +1 -1
  203. package/dist/shadow/receiver_bias.js +41 -0
  204. package/dist/shadow/receiver_bias.js.map +1 -0
  205. package/dist/shadow/shader.js +20 -60
  206. package/dist/shadow/shader.js.map +1 -1
  207. package/dist/shadow/shadow_impl.js +89 -0
  208. package/dist/shadow/shadow_impl.js.map +1 -1
  209. package/dist/shadow/shadow_region.js +50 -50
  210. package/dist/shadow/shadow_region.js.map +1 -1
  211. package/dist/shadow/shadowmapper.js +150 -37
  212. package/dist/shadow/shadowmapper.js.map +1 -1
  213. package/dist/shapes/shape.js +11 -2
  214. package/dist/shapes/shape.js.map +1 -1
  215. package/dist/shapes/sphere.js +50 -9
  216. package/dist/shapes/sphere.js.map +1 -1
  217. package/dist/utility/blueprint/common/constants.js +112 -2
  218. package/dist/utility/blueprint/common/constants.js.map +1 -1
  219. package/dist/utility/blueprint/material/func.js +142 -49
  220. package/dist/utility/blueprint/material/func.js.map +1 -1
  221. package/dist/utility/blueprint/material/ir.js +490 -25
  222. package/dist/utility/blueprint/material/ir.js.map +1 -1
  223. package/dist/utility/blueprint/material/texture.js +898 -4
  224. package/dist/utility/blueprint/material/texture.js.map +1 -1
  225. package/dist/utility/serialization/manager.js +431 -100
  226. package/dist/utility/serialization/manager.js.map +1 -1
  227. package/dist/utility/serialization/scene/animation.js +370 -3
  228. package/dist/utility/serialization/scene/animation.js.map +1 -1
  229. package/dist/utility/serialization/scene/batch.js +4 -0
  230. package/dist/utility/serialization/scene/batch.js.map +1 -1
  231. package/dist/utility/serialization/scene/camera.js +133 -1
  232. package/dist/utility/serialization/scene/camera.js.map +1 -1
  233. package/dist/utility/serialization/scene/common.js +26 -5
  234. package/dist/utility/serialization/scene/common.js.map +1 -1
  235. package/dist/utility/serialization/scene/hair.js +1141 -0
  236. package/dist/utility/serialization/scene/hair.js.map +1 -0
  237. package/dist/utility/serialization/scene/light.js +84 -4
  238. package/dist/utility/serialization/scene/light.js.map +1 -1
  239. package/dist/utility/serialization/scene/material.js +1867 -166
  240. package/dist/utility/serialization/scene/material.js.map +1 -1
  241. package/dist/utility/serialization/scene/mesh.js +231 -14
  242. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  243. package/dist/utility/serialization/scene/node.js +141 -7
  244. package/dist/utility/serialization/scene/node.js.map +1 -1
  245. package/dist/utility/serialization/scene/particle.js +4 -0
  246. package/dist/utility/serialization/scene/particle.js.map +1 -1
  247. package/dist/utility/serialization/scene/primitive.js +18 -0
  248. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  249. package/dist/utility/serialization/scene/sprite.js +4 -0
  250. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  251. package/dist/utility/serialization/scene/terrain.js +4 -0
  252. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  253. package/dist/utility/serialization/scene/text.js +4 -0
  254. package/dist/utility/serialization/scene/text.js.map +1 -1
  255. package/dist/utility/serialization/scene/water.js +4 -0
  256. package/dist/utility/serialization/scene/water.js.map +1 -1
  257. package/dist/utility/serialization/types.js.map +1 -1
  258. package/dist/utility/shprojector.js +96 -95
  259. package/dist/utility/shprojector.js.map +1 -1
  260. package/dist/values.js +49 -2
  261. package/dist/values.js.map +1 -1
  262. package/package.json +3 -3
@@ -0,0 +1,932 @@
1
+ import { Vector4, Quaternion, Vector3 } from '@zephyr3d/base';
2
+ import { applyMaterialMixins, MeshMaterial } from './meshmaterial.js';
3
+ import { mixinLight } from './mixins/lit.js';
4
+ import { mixinPBRBRDF } from './mixins/pbr/brdf.js';
5
+ import { mixinTextureProps } from './mixins/texture.js';
6
+ import { ShaderHelper } from './shader/helper.js';
7
+ import { SSR_interleavedGradientNoise } from '../shaders/ssr.js';
8
+ import { RENDER_PASS_TYPE_LIGHT, LIGHT_TYPE_POINT } from '../values.js';
9
+
10
+ /** Taps in the contact-occlusion disc. 16 rather than 8: close-ups need the density. */ const CONTACT_AO_SAMPLES = 16;
11
+ /**
12
+ * Cap on the on-screen radius of the contact-occlusion disc, in pixels.
13
+ * Keeps the sample density usable in close-up, where the projected radius
14
+ * would otherwise reach hundreds of pixels and read as grain.
15
+ */ const CONTACT_AO_MAX_PIXEL_RADIUS = 48;
16
+ /** Golden angle, in radians. Spreads the disc taps without any table. */ const GOLDEN_ANGLE = 2.399963229728653;
17
+ /**
18
+ * Realtime eye material for realistic and semi-realistic characters.
19
+ *
20
+ * @remarks
21
+ * Shades the whole eyeball - sclera, limbus and iris - from a single mesh and a
22
+ * single material. There is no separate cornea layer: the corneal bulge is
23
+ * simulated by refracting the view ray and offsetting the iris lookup, so no
24
+ * transparent second surface, OIT or depth sorting is involved.
25
+ *
26
+ * The refraction works in tangent space (per-fragment TBN) rather than object
27
+ * space, because the shader cannot reconstruct a fixed object-space gaze axis
28
+ * after skinning. The residual error over the sphere's curvature is small at
29
+ * the depths an iris actually sits.
30
+ *
31
+ * Regions are derived analytically from the UV distance to the iris centre, so
32
+ * no mask texture is required - which is why the asset must place the iris at a
33
+ * known UV location; see {@link EyeMaterial.irisCenter}.
34
+ *
35
+ * Expected asset setup:
36
+ * - A single eyeball mesh (sphere or front dome) carrying normals *and*
37
+ * tangents; without tangents there is no TBN and refraction degrades to a
38
+ * flat iris.
39
+ * - UVs that place the pupil centre at {@link EyeMaterial.irisCenter}, with the
40
+ * iris occupying a disc of {@link EyeMaterial.irisRadius} around it.
41
+ * - An iris texture with the pupil at its centre. The sclera texture is
42
+ * optional; without one the sclera is shaded from {@link EyeMaterial.scleraColor}.
43
+ *
44
+ * @public
45
+ */ class EyeMaterial extends applyMaterialMixins(MeshMaterial, mixinLight, mixinPBRBRDF, mixinTextureProps('iris'), mixinTextureProps('sclera')) {
46
+ static FEATURE_VERTEX_NORMAL = this.defineFeature();
47
+ static FEATURE_VERTEX_TANGENT = this.defineFeature();
48
+ static FEATURE_SOCKET_OCCLUSION = this.defineFeature();
49
+ static FEATURE_CONTACT_AO = this.defineFeature();
50
+ static FEATURE_CONTACT_AO_JITTER = this.defineFeature();
51
+ _irisCenter;
52
+ _irisRadius;
53
+ _irisDepth;
54
+ _ior;
55
+ _pupilRadius;
56
+ _pupilDilation;
57
+ _irisColor;
58
+ _irisBrightness;
59
+ _limbalRingWidth;
60
+ _limbalRingStrength;
61
+ _scleraColor;
62
+ _scleraWrap;
63
+ _scleraEdgeTint;
64
+ _corneaSpecularStrength;
65
+ _corneaRoughness;
66
+ _socketRotation;
67
+ _upperLidAngle;
68
+ _lowerLidAngle;
69
+ _socketOcclusionSoftness;
70
+ _socketOcclusionStrength;
71
+ _contactAORadius;
72
+ _contactAOMinDistance;
73
+ _contactAOMaxDistance;
74
+ _contactAOStrength;
75
+ // Derived from _socketRotation; never serialized or exposed.
76
+ _socketUp;
77
+ _socketForward;
78
+ _socketCos;
79
+ // Derived from the contact AO distances; never serialized or exposed.
80
+ _contactAORange;
81
+ constructor(){
82
+ super();
83
+ this._irisCenter = new Vector4(0.5, 0.5, 0, 0);
84
+ this._irisRadius = 0.22;
85
+ this._irisDepth = 0.06;
86
+ this._ior = 1.376;
87
+ this._pupilRadius = 0.35;
88
+ this._pupilDilation = 0;
89
+ this._irisColor = new Vector4(1, 1, 1, 1);
90
+ this._irisBrightness = 0.15;
91
+ this._limbalRingWidth = 0.15;
92
+ this._limbalRingStrength = 0.7;
93
+ this._scleraColor = new Vector4(0.9, 0.88, 0.86, 1);
94
+ this._scleraWrap = 0.35;
95
+ this._scleraEdgeTint = new Vector4(0.55, 0.22, 0.18, 1);
96
+ this._corneaSpecularStrength = 1;
97
+ this._corneaRoughness = 0.05;
98
+ this._socketRotation = new Vector4(0, 0, 0, 0);
99
+ this._upperLidAngle = 50;
100
+ this._lowerLidAngle = 65;
101
+ this._socketOcclusionSoftness = 15;
102
+ this._socketOcclusionStrength = 0.65;
103
+ // Metric defaults for a ~12mm-radius eyeball in a one-unit-per-metre
104
+ // scene. Assets authored at a different world scale must scale all three.
105
+ this._contactAORadius = 0.006;
106
+ this._contactAOMinDistance = 0.0004;
107
+ this._contactAOMaxDistance = 0.006;
108
+ this._contactAOStrength = 0.7;
109
+ this._socketUp = new Vector4(0, 1, 0, 0);
110
+ this._socketForward = new Vector4(0, 0, 1, 0);
111
+ this._socketCos = new Vector4();
112
+ this._contactAORange = new Vector4();
113
+ this.useFeature(EyeMaterial.FEATURE_VERTEX_NORMAL, true);
114
+ // Initialised explicitly: featureUsed() returns undefined for a feature
115
+ // never touched, which would disagree with the serialization descriptor's
116
+ // declared default and silently change assets on load.
117
+ this.useFeature(EyeMaterial.FEATURE_VERTEX_TANGENT, false);
118
+ this.useFeature(EyeMaterial.FEATURE_SOCKET_OCCLUSION, false);
119
+ this.useFeature(EyeMaterial.FEATURE_CONTACT_AO, false);
120
+ this.useFeature(EyeMaterial.FEATURE_CONTACT_AO_JITTER, false);
121
+ }
122
+ clone() {
123
+ const other = new EyeMaterial();
124
+ other.copyFrom(this);
125
+ return other;
126
+ }
127
+ copyFrom(other) {
128
+ super.copyFrom(other);
129
+ this.vertexNormal = other.vertexNormal;
130
+ this.vertexTangent = other.vertexTangent;
131
+ this.irisCenter = other.irisCenter;
132
+ this.irisRadius = other.irisRadius;
133
+ this.irisDepth = other.irisDepth;
134
+ this.ior = other.ior;
135
+ this.pupilRadius = other.pupilRadius;
136
+ this.pupilDilation = other.pupilDilation;
137
+ this.irisColor = other.irisColor;
138
+ this.irisBrightness = other.irisBrightness;
139
+ this.limbalRingWidth = other.limbalRingWidth;
140
+ this.limbalRingStrength = other.limbalRingStrength;
141
+ this.scleraColor = other.scleraColor;
142
+ this.scleraWrap = other.scleraWrap;
143
+ this.scleraEdgeTint = other.scleraEdgeTint;
144
+ this.corneaSpecularStrength = other.corneaSpecularStrength;
145
+ this.corneaRoughness = other.corneaRoughness;
146
+ this.socketOcclusion = other.socketOcclusion;
147
+ this.socketRotation = other.socketRotation;
148
+ this.upperLidAngle = other.upperLidAngle;
149
+ this.lowerLidAngle = other.lowerLidAngle;
150
+ this.socketOcclusionSoftness = other.socketOcclusionSoftness;
151
+ this.socketOcclusionStrength = other.socketOcclusionStrength;
152
+ this.contactAO = other.contactAO;
153
+ this.contactAORadius = other.contactAORadius;
154
+ this.contactAOMinDistance = other.contactAOMinDistance;
155
+ this.contactAOMaxDistance = other.contactAOMaxDistance;
156
+ this.contactAOStrength = other.contactAOStrength;
157
+ this.contactAOTemporalJitter = other.contactAOTemporalJitter;
158
+ }
159
+ /** true if vertex normal attribute presents */ get vertexNormal() {
160
+ return this.featureUsed(EyeMaterial.FEATURE_VERTEX_NORMAL);
161
+ }
162
+ set vertexNormal(val) {
163
+ this.useFeature(EyeMaterial.FEATURE_VERTEX_NORMAL, !!val);
164
+ }
165
+ /**
166
+ * true if vertex tangent attribute presents. Refraction needs a tangent
167
+ * frame; without one the iris renders flat, with no parallax.
168
+ */ get vertexTangent() {
169
+ return this.featureUsed(EyeMaterial.FEATURE_VERTEX_TANGENT);
170
+ }
171
+ set vertexTangent(val) {
172
+ this.useFeature(EyeMaterial.FEATURE_VERTEX_TANGENT, !!val);
173
+ }
174
+ /** UV coordinate of the pupil centre. Only xy is used. */ get irisCenter() {
175
+ return this._irisCenter;
176
+ }
177
+ set irisCenter(val) {
178
+ if (val && !this._irisCenter.equalsTo(val)) {
179
+ this._irisCenter.set(val);
180
+ this.uniformChanged();
181
+ }
182
+ }
183
+ /** Iris disc radius, in UV units. */ get irisRadius() {
184
+ return this._irisRadius;
185
+ }
186
+ set irisRadius(val) {
187
+ const next = Math.max(0.001, val ?? 0.001);
188
+ if (next !== this._irisRadius) {
189
+ this._irisRadius = next;
190
+ this.uniformChanged();
191
+ }
192
+ }
193
+ /**
194
+ * Depth of the iris plane below the corneal surface, in UV units - the
195
+ * parallax strength. 0 is flat; 0.05-0.08 reads as a real anterior chamber.
196
+ */ get irisDepth() {
197
+ return this._irisDepth;
198
+ }
199
+ set irisDepth(val) {
200
+ const next = Math.max(0, val ?? 0);
201
+ if (next !== this._irisDepth) {
202
+ this._irisDepth = next;
203
+ this.uniformChanged();
204
+ }
205
+ }
206
+ /** Index of refraction of the cornea. 1.376 is the physical value. */ get ior() {
207
+ return this._ior;
208
+ }
209
+ set ior(val) {
210
+ const next = Math.max(1, val ?? 1);
211
+ if (next !== this._ior) {
212
+ this._ior = next;
213
+ this.uniformChanged();
214
+ }
215
+ }
216
+ /** Pupil radius as a fraction of the iris radius, at zero dilation. */ get pupilRadius() {
217
+ return this._pupilRadius;
218
+ }
219
+ set pupilRadius(val) {
220
+ const next = Math.min(0.95, Math.max(0.01, val ?? 0.01));
221
+ if (next !== this._pupilRadius) {
222
+ this._pupilRadius = next;
223
+ this.uniformChanged();
224
+ }
225
+ }
226
+ /** Pupil dilation, -1 (fully constricted) to 1 (fully dilated). Animatable. */ get pupilDilation() {
227
+ return this._pupilDilation;
228
+ }
229
+ set pupilDilation(val) {
230
+ const next = Math.min(1, Math.max(-1, val ?? 0));
231
+ if (next !== this._pupilDilation) {
232
+ this._pupilDilation = next;
233
+ this.uniformChanged();
234
+ }
235
+ }
236
+ /** Tint multiplied over the iris texture. */ get irisColor() {
237
+ return this._irisColor;
238
+ }
239
+ set irisColor(val) {
240
+ if (val && !this._irisColor.equalsTo(val)) {
241
+ this._irisColor.set(val);
242
+ this.uniformChanged();
243
+ }
244
+ }
245
+ /** Fake internal scattering that keeps the iris from going black in shadow. */ get irisBrightness() {
246
+ return this._irisBrightness;
247
+ }
248
+ set irisBrightness(val) {
249
+ const next = Math.max(0, val ?? 0);
250
+ if (next !== this._irisBrightness) {
251
+ this._irisBrightness = next;
252
+ this.uniformChanged();
253
+ }
254
+ }
255
+ /**
256
+ * Width of the dark ring at the iris edge, as a fraction of
257
+ * {@link EyeMaterial.irisRadius} - relative, so resizing the iris keeps the
258
+ * ring in proportion.
259
+ */ get limbalRingWidth() {
260
+ return this._limbalRingWidth;
261
+ }
262
+ set limbalRingWidth(val) {
263
+ const next = Math.min(1, Math.max(0, val ?? 0));
264
+ if (next !== this._limbalRingWidth) {
265
+ this._limbalRingWidth = next;
266
+ this.uniformChanged();
267
+ }
268
+ }
269
+ /** How dark the limbal ring gets. 0 disables it. */ get limbalRingStrength() {
270
+ return this._limbalRingStrength;
271
+ }
272
+ set limbalRingStrength(val) {
273
+ const next = Math.min(1, Math.max(0, val ?? 0));
274
+ if (next !== this._limbalRingStrength) {
275
+ this._limbalRingStrength = next;
276
+ this.uniformChanged();
277
+ }
278
+ }
279
+ /** Base sclera colour. Pure white reads as plastic; keep it slightly warm. */ get scleraColor() {
280
+ return this._scleraColor;
281
+ }
282
+ set scleraColor(val) {
283
+ if (val && !this._scleraColor.equalsTo(val)) {
284
+ this._scleraColor.set(val);
285
+ this.uniformChanged();
286
+ }
287
+ }
288
+ /** Diffuse wrap for the sclera, standing in for its subsurface softness. */ get scleraWrap() {
289
+ return this._scleraWrap;
290
+ }
291
+ set scleraWrap(val) {
292
+ const next = Math.max(0, val ?? 0);
293
+ if (next !== this._scleraWrap) {
294
+ this._scleraWrap = next;
295
+ this.uniformChanged();
296
+ }
297
+ }
298
+ /** Colour bled into the sclera away from the iris, standing in for vasculature. */ get scleraEdgeTint() {
299
+ return this._scleraEdgeTint;
300
+ }
301
+ set scleraEdgeTint(val) {
302
+ if (val && !this._scleraEdgeTint.equalsTo(val)) {
303
+ this._scleraEdgeTint.set(val);
304
+ this.uniformChanged();
305
+ }
306
+ }
307
+ /** Strength of the corneal highlight - the main cue that an eye is wet. */ get corneaSpecularStrength() {
308
+ return this._corneaSpecularStrength;
309
+ }
310
+ set corneaSpecularStrength(val) {
311
+ const next = Math.max(0, val ?? 0);
312
+ if (next !== this._corneaSpecularStrength) {
313
+ this._corneaSpecularStrength = next;
314
+ this.uniformChanged();
315
+ }
316
+ }
317
+ /** Corneal roughness. Keep it very low; the cornea is close to a mirror. */ get corneaRoughness() {
318
+ return this._corneaRoughness;
319
+ }
320
+ set corneaRoughness(val) {
321
+ const next = Math.min(1, Math.max(0.001, val ?? 0.001));
322
+ if (next !== this._corneaRoughness) {
323
+ this._corneaRoughness = next;
324
+ this.uniformChanged();
325
+ }
326
+ }
327
+ /**
328
+ * Enables eye-socket occlusion - the soft shadowing of the eyeball by the
329
+ * eyelids and socket.
330
+ *
331
+ * @remarks
332
+ * Analytic: an asymmetric aperture defined by
333
+ * {@link EyeMaterial.upperLidAngle} and {@link EyeMaterial.lowerLidAngle}
334
+ * around an axis fixed in the eyeball mesh's object space (rotatable via
335
+ * {@link EyeMaterial.socketRotation}). Darkens ambient diffuse, ambient
336
+ * specular and direct lights outside the aperture; no mask texture, extra
337
+ * pass or per-frame scripting. Object space means the frame follows skinning
338
+ * and node motion automatically, and vertical gaze drags the occlusion with
339
+ * the eye - approximating how real upper lids follow it.
340
+ */ get socketOcclusion() {
341
+ return this.featureUsed(EyeMaterial.FEATURE_SOCKET_OCCLUSION);
342
+ }
343
+ set socketOcclusion(val) {
344
+ this.useFeature(EyeMaterial.FEATURE_SOCKET_OCCLUSION, !!val);
345
+ }
346
+ /**
347
+ * Euler rotation (degrees, xyz) applied to the canonical socket frame.
348
+ * At (0, 0, 0) the frame matches the documented asset convention: +Y towards
349
+ * the upper lid, +Z along the gaze. Angles rather than unit vectors because
350
+ * angles are the inspector-editable form.
351
+ */ get socketRotation() {
352
+ return this._socketRotation;
353
+ }
354
+ set socketRotation(val) {
355
+ if (val && !this._socketRotation.equalsTo(val)) {
356
+ this._socketRotation.set(val);
357
+ this.updateSocketFrame();
358
+ this.uniformChanged();
359
+ }
360
+ }
361
+ /** Aperture half-angle towards the upper lid, in degrees from the socket up axis. */ get upperLidAngle() {
362
+ return this._upperLidAngle;
363
+ }
364
+ set upperLidAngle(val) {
365
+ const next = Math.min(90, Math.max(5, val ?? 5));
366
+ if (next !== this._upperLidAngle) {
367
+ this._upperLidAngle = next;
368
+ this.uniformChanged();
369
+ }
370
+ }
371
+ /**
372
+ * Aperture half-angle towards the lower lid, in degrees. Defaults wider than
373
+ * {@link EyeMaterial.upperLidAngle}; that asymmetry is what makes the
374
+ * occlusion read as an eye socket.
375
+ */ get lowerLidAngle() {
376
+ return this._lowerLidAngle;
377
+ }
378
+ set lowerLidAngle(val) {
379
+ const next = Math.min(90, Math.max(5, val ?? 5));
380
+ if (next !== this._lowerLidAngle) {
381
+ this._lowerLidAngle = next;
382
+ this.uniformChanged();
383
+ }
384
+ }
385
+ /** Width of the lit-to-shadowed transition, in degrees. */ get socketOcclusionSoftness() {
386
+ return this._socketOcclusionSoftness;
387
+ }
388
+ set socketOcclusionSoftness(val) {
389
+ const next = Math.min(45, Math.max(1, val ?? 1));
390
+ if (next !== this._socketOcclusionSoftness) {
391
+ this._socketOcclusionSoftness = next;
392
+ this.uniformChanged();
393
+ }
394
+ }
395
+ /** Overall socket occlusion strength, 0 to 1. */ get socketOcclusionStrength() {
396
+ return this._socketOcclusionStrength;
397
+ }
398
+ set socketOcclusionStrength(val) {
399
+ const next = Math.min(1, Math.max(0, val ?? 0));
400
+ if (next !== this._socketOcclusionStrength) {
401
+ this._socketOcclusionStrength = next;
402
+ this.uniformChanged();
403
+ }
404
+ }
405
+ /**
406
+ * Enables screen-space contact occlusion of the eyeball by whatever sits in
407
+ * front of it - in practice the eyelid margin and the lashes.
408
+ *
409
+ * @remarks
410
+ * Complements {@link EyeMaterial.socketOcclusion}: the socket aperture is an
411
+ * analytic model of the lids, while this term reads the depth prepass and so
412
+ * follows the real lid mesh - blinks, asymmetry and all. Dims the ambient
413
+ * terms and the diffuse response of direct lights (whose shadow maps cannot
414
+ * resolve the millimetre lid gap); direct specular is left untouched so the
415
+ * corneal highlight survives.
416
+ *
417
+ * Requires a depth prepass. Forward+ always runs one; where none is bound the
418
+ * term silently resolves to no occlusion.
419
+ */ get contactAO() {
420
+ return this.featureUsed(EyeMaterial.FEATURE_CONTACT_AO);
421
+ }
422
+ set contactAO(val) {
423
+ this.useFeature(EyeMaterial.FEATURE_CONTACT_AO, !!val);
424
+ }
425
+ /**
426
+ * Radius of the contact occlusion disc, in world units - how far from the
427
+ * lid the darkening reaches. Rescale for assets not authored at one unit per
428
+ * metre; roughly half the eyeball radius is a good starting point.
429
+ */ get contactAORadius() {
430
+ return this._contactAORadius;
431
+ }
432
+ set contactAORadius(val) {
433
+ const next = Math.max(1e-6, val ?? 1e-6);
434
+ if (next !== this._contactAORadius) {
435
+ this._contactAORadius = next;
436
+ this.uniformChanged();
437
+ }
438
+ }
439
+ /**
440
+ * Smallest depth step that counts as an occluder, in world units. Raise it
441
+ * until the eyeball stops shading itself. Compared against the eyeball's own
442
+ * tangent plane rather than raw depth, so only residual curvature - not
443
+ * view-dependent slope - has to clear it.
444
+ */ get contactAOMinDistance() {
445
+ return this._contactAOMinDistance;
446
+ }
447
+ set contactAOMinDistance(val) {
448
+ const next = Math.max(1e-6, val ?? 1e-6);
449
+ if (next !== this._contactAOMinDistance) {
450
+ this._contactAOMinDistance = next;
451
+ this.uniformChanged();
452
+ }
453
+ }
454
+ /**
455
+ * Largest depth step that counts as an occluder, in world units. Anything
456
+ * further in front is unrelated geometry overlapping the eye on screen and
457
+ * must not paint a shadow onto it - keep it at the order of the lid gap.
458
+ * Forced to at least 4x {@link EyeMaterial.contactAOMinDistance} on upload
459
+ * so the two smoothstep ramps cannot cross.
460
+ */ get contactAOMaxDistance() {
461
+ return this._contactAOMaxDistance;
462
+ }
463
+ set contactAOMaxDistance(val) {
464
+ const next = Math.max(1e-6, val ?? 1e-6);
465
+ if (next !== this._contactAOMaxDistance) {
466
+ this._contactAOMaxDistance = next;
467
+ this.uniformChanged();
468
+ }
469
+ }
470
+ /**
471
+ * Overall contact occlusion strength, 0 to 1. Calibrated so that 1 drives
472
+ * the ambient terms to zero right at the contact line.
473
+ */ get contactAOStrength() {
474
+ return this._contactAOStrength;
475
+ }
476
+ set contactAOStrength(val) {
477
+ const next = Math.min(1, Math.max(0, val ?? 0));
478
+ if (next !== this._contactAOStrength) {
479
+ this._contactAOStrength = next;
480
+ this.uniformChanged();
481
+ }
482
+ }
483
+ /**
484
+ * Advances the contact-occlusion noise pattern every frame so TAA can
485
+ * integrate the grain away. Opt-in: without a temporal accumulator the
486
+ * advancing pattern reads as flicker, worse than the static grain it
487
+ * replaces. Enable it if and only if the pipeline resolves temporally.
488
+ */ get contactAOTemporalJitter() {
489
+ return this.featureUsed(EyeMaterial.FEATURE_CONTACT_AO_JITTER);
490
+ }
491
+ set contactAOTemporalJitter(val) {
492
+ this.useFeature(EyeMaterial.FEATURE_CONTACT_AO_JITTER, !!val);
493
+ }
494
+ /**
495
+ * Recomputes the object-space socket up/forward vectors from
496
+ * {@link EyeMaterial.socketRotation}.
497
+ *
498
+ * @internal
499
+ */ updateSocketFrame() {
500
+ const d2r = Math.PI / 180;
501
+ const rot = Quaternion.fromEulerAngle(this._socketRotation.x * d2r, this._socketRotation.y * d2r, this._socketRotation.z * d2r).toMatrix3x3();
502
+ const up = rot.transformVector(new Vector3(0, 1, 0));
503
+ const forward = rot.transformVector(new Vector3(0, 0, 1));
504
+ this._socketUp.setXYZW(up.x, up.y, up.z, 0);
505
+ this._socketForward.setXYZW(forward.x, forward.y, forward.z, 0);
506
+ }
507
+ vertexShader(scope) {
508
+ super.vertexShader(scope);
509
+ const pb = scope.$builder;
510
+ scope.$l.oPos = ShaderHelper.resolveVertexPosition(scope);
511
+ scope.$outputs.worldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.oPos, 1)).xyz;
512
+ ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1)));
513
+ if (this.vertexNormal) {
514
+ scope.$l.oNorm = ShaderHelper.resolveVertexNormal(scope);
515
+ scope.$outputs.wNorm = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oNorm, 0)).xyz;
516
+ if (this.vertexTangent) {
517
+ scope.$l.oTangent = ShaderHelper.resolveVertexTangent(scope);
518
+ scope.$outputs.wTangent = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oTangent.xyz, 0)).xyz;
519
+ scope.$outputs.wBinormal = pb.mul(pb.cross(scope.$outputs.wNorm, scope.$outputs.wTangent), scope.oTangent.w);
520
+ }
521
+ if (this.socketOcclusion && this.drawContext.renderPass.type === RENDER_PASS_TYPE_LIGHT) {
522
+ // The socket axes take the same transform chain as the normal
523
+ // attribute - skin matrix, then normal matrix - so the occlusion stays
524
+ // attached to the eye through node motion and skeletal animation.
525
+ scope.zEyeSocketUp = pb.vec4().uniform(2);
526
+ scope.zEyeSocketForward = pb.vec4().uniform(2);
527
+ const skinMatrix = ShaderHelper.getSkinMatrix(scope);
528
+ scope.$l.oSocketUp = skinMatrix ? pb.mul(skinMatrix, pb.vec4(scope.zEyeSocketUp.xyz, 0)).xyz : scope.zEyeSocketUp.xyz;
529
+ scope.$l.oSocketForward = skinMatrix ? pb.mul(skinMatrix, pb.vec4(scope.zEyeSocketForward.xyz, 0)).xyz : scope.zEyeSocketForward.xyz;
530
+ scope.$outputs.wSocketUp = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oSocketUp, 0)).xyz;
531
+ scope.$outputs.wSocketForward = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oSocketForward, 0)).xyz;
532
+ }
533
+ }
534
+ }
535
+ /**
536
+ * Fractional visibility of a world-space direction through the socket
537
+ * aperture: 1 fully visible, approaching 0 towards the lids. Evaluated on
538
+ * the surface normal, the reflection vector and light directions so all
539
+ * lighting terms agree on where the lids are. The rear-hemisphere falloff
540
+ * (`forLight`) applies to light directions only - applying it to surface
541
+ * normals would paint the whole rear of a bare eyeball dark.
542
+ *
543
+ * @internal
544
+ */ socketAperture(scope, dir, forLight) {
545
+ const pb = scope.$builder;
546
+ const funcName = forLight ? 'Z_eyeSocketApertureLight' : 'Z_eyeSocketApertureSurface';
547
+ pb.func(funcName, [
548
+ pb.vec3('dir'),
549
+ pb.vec4('lidCos'),
550
+ pb.float('strength')
551
+ ], function() {
552
+ this.$l.up = pb.normalize(this.$inputs.wSocketUp);
553
+ this.$l.lat = pb.dot(this.dir, this.up);
554
+ // lidCos packs cos(angle +/- softness) per lid, precomputed on the CPU:
555
+ // (upper outer, upper inner, lower outer, lower inner).
556
+ this.$l.occUpper = pb.smoothStep(this.lidCos.x, this.lidCos.y, this.lat);
557
+ this.$l.occLower = pb.smoothStep(this.lidCos.z, this.lidCos.w, pb.neg(this.lat));
558
+ this.$l.visibility = pb.sub(1, pb.max(this.occUpper, this.occLower));
559
+ if (forLight) {
560
+ // Light from behind the eye's equator is blocked by the head itself;
561
+ // the ramp straddles zero so the dimming eases in early.
562
+ this.$l.forward = pb.normalize(this.$inputs.wSocketForward);
563
+ this.visibility = pb.mul(this.visibility, pb.smoothStep(-0.4, 0.25, pb.dot(this.dir, this.forward)));
564
+ }
565
+ this.$return(pb.mix(pb.float(1), this.visibility, this.strength));
566
+ });
567
+ return pb.getGlobalScope()[funcName](dir, scope.zEyeSocketCos, scope.zEyeSocketStrength);
568
+ }
569
+ /**
570
+ * Fraction of the ambient hemisphere still reaching this fragment once the
571
+ * geometry standing in front of the eyeball is accounted for: 1 open, 0 fully
572
+ * buried under the lid.
573
+ *
574
+ * @remarks
575
+ * Samples the depth prepass in a disc around the fragment and counts the taps
576
+ * in front of the eyeball's own local tangent plane. Comparing against that
577
+ * plane rather than raw depth cancels the surface's slope and foreshortening,
578
+ * which is what lets the min/max thresholds be plain world-space distances.
579
+ *
580
+ * Must be called from uniform control flow - it takes screen-space
581
+ * derivatives, which WGSL only permits there.
582
+ *
583
+ * @internal
584
+ */ contactAOTerm(scope) {
585
+ const pb = scope.$builder;
586
+ if (!ShaderHelper.getLinearDepthTexture(scope)) {
587
+ // No depth prepass bound for this pass. Forward+ always runs one, but a
588
+ // material must not assume the pipeline it is drawn by.
589
+ return pb.float(1);
590
+ }
591
+ const funcName = 'Z_eyeContactAO';
592
+ const temporalJitter = !!this.contactAOTemporalJitter;
593
+ pb.func(funcName, [
594
+ pb.vec3('worldPos'),
595
+ pb.vec4('range'),
596
+ pb.float('radius'),
597
+ pb.float('strength')
598
+ ], function() {
599
+ this.$l.renderSize = ShaderHelper.getRenderSize(this);
600
+ this.$l.viewPos = pb.mul(ShaderHelper.getViewMatrix(this), pb.vec4(this.worldPos, 1)).xyz;
601
+ // Distance along the view axis, which is what the depth prepass writes
602
+ // once scaled back up by the far plane.
603
+ this.$l.centerZ = pb.neg(this.viewPos.z);
604
+ // Derivatives first: the fade below returns early, and everything after
605
+ // that early return is non-uniform control flow.
606
+ this.$l.dzdx = pb.dpdx(this.centerZ);
607
+ this.$l.dzdy = pb.dpdy(this.centerZ);
608
+ // Project the world-space radius rather than reading the projection
609
+ // matrix diagonal, so off-centre/oblique projections stay correct.
610
+ this.$l.projMatrix = ShaderHelper.getProjectionMatrix(this);
611
+ this.$l.h0 = pb.mul(this.projMatrix, pb.vec4(this.viewPos, 1));
612
+ this.$l.h1 = pb.mul(this.projMatrix, pb.vec4(pb.add(this.viewPos, pb.vec3(this.radius, 0, 0)), 1));
613
+ this.$l.pixelRadius = pb.min(pb.length(pb.mul(pb.sub(pb.div(this.h1.xy, this.h1.w), pb.div(this.h0.xy, this.h0.w)), 0.5, this.renderSize)), CONTACT_AO_MAX_PIXEL_RADIUS);
614
+ // A disc of only a couple of pixels is pure noise; fade it out rather
615
+ // than sample it.
616
+ this.$l.fade = pb.smoothStep(1.5, 4, this.pixelRadius);
617
+ this.$if(pb.lessThanEqual(this.fade, 0), function() {
618
+ this.$return(pb.float(1));
619
+ });
620
+ this.$l.screenUV = pb.div(this.$builtins.fragCoord.xy, this.renderSize);
621
+ this.$l.far = ShaderHelper.getCameraParams(this).y;
622
+ // Per-pixel rotation of the spiral breaks up banding. Static unless
623
+ // temporal jitter is opted in (see contactAOTemporalJitter); the
624
+ // framestamp is wrapped to keep the IGN math inside float precision.
625
+ this.$l.frameId = temporalJitter ? pb.mod(pb.float(ShaderHelper.getFramestamp(this)), 64) : pb.float(0);
626
+ this.$l.rotation = pb.mul(SSR_interleavedGradientNoise(this, this.$builtins.fragCoord.xy, this.frameId), Math.PI * 2);
627
+ this.$l.occlusion = pb.float(0);
628
+ this.$l.weightSum = pb.float(0);
629
+ this.$for(pb.int('i'), 0, CONTACT_AO_SAMPLES, function() {
630
+ this.$l.t = pb.div(pb.add(pb.float(this.i), 0.5), CONTACT_AO_SAMPLES);
631
+ this.$l.angle = pb.add(this.rotation, pb.mul(pb.float(this.i), GOLDEN_ANGLE));
632
+ // sqrt spreads the taps evenly over the disc. Without it they pile up
633
+ // at the centre and the outer half of the radius goes unsampled.
634
+ this.$l.offset = pb.mul(pb.vec2(pb.cos(this.angle), pb.sin(this.angle)), pb.mul(pb.sqrt(this.t), this.pixelRadius));
635
+ this.$l.sampleZ = pb.mul(ShaderHelper.sampleLinearDepth(this, ShaderHelper.getLinearDepthTexture(this), pb.add(this.screenUV, pb.div(this.offset, this.renderSize)), 0), this.far);
636
+ // Where the eyeball itself would be at this tap.
637
+ this.$l.predicted = pb.add(this.centerZ, pb.mul(this.dzdx, this.offset.x), pb.mul(this.dzdy, this.offset.y));
638
+ this.$l.delta = pb.sub(this.predicted, this.sampleZ);
639
+ // range packs (min in, min out, max in, max out), ordered on the CPU.
640
+ // Under the near pair the step is the eyeball shading itself; over
641
+ // the far pair it is unrelated geometry overlapping the eye on
642
+ // screen, which must not paint a shadow onto it.
643
+ this.$l.accept = pb.mul(pb.smoothStep(this.range.x, this.range.y, this.delta), pb.sub(1, pb.smoothStep(this.range.z, this.range.w, this.delta)));
644
+ // Nearer taps occlude more, turning the lid edge into a gradient.
645
+ // The slope is gentle on purpose: the lid signal lives on the outer
646
+ // rim of the disc, which a steep falloff would discount.
647
+ this.$l.falloff = pb.sub(1, pb.mul(this.t, 0.75));
648
+ this.occlusion = pb.add(this.occlusion, pb.mul(this.accept, this.falloff));
649
+ this.weightSum = pb.add(this.weightSum, this.falloff);
650
+ });
651
+ // Semi-disc calibration: a lid edge at the fragment covers at most half
652
+ // the disc (the other half is the eyeball itself), so raw coverage tops
653
+ // out near 0.5. Doubling maps that to full occlusion; the smoothstep's
654
+ // low edge discards lone-stray-tap grain.
655
+ this.$l.coverage = pb.smoothStep(0.1, 0.9, pb.mul(pb.div(this.occlusion, pb.max(this.weightSum, 1e-4)), 2));
656
+ this.$l.ao = pb.sub(1, pb.mul(this.coverage, this.strength));
657
+ this.$return(pb.mix(pb.float(1), this.ao, this.fade));
658
+ });
659
+ const globalScope = pb.getGlobalScope();
660
+ return globalScope[funcName](scope.$inputs.worldPos, scope.zEyeContactAORange, scope.zEyeContactAORadius, scope.zEyeContactAOStrength);
661
+ }
662
+ /**
663
+ * Offsets the iris lookup to account for refraction through the cornea:
664
+ * refracts the view direction in tangent space and walks the refracted ray
665
+ * down to the iris plane. Falls back to the unmodified UV when there is no
666
+ * tangent frame to refract in.
667
+ *
668
+ * @internal
669
+ */ refractIrisUV(scope, uv, viewVec) {
670
+ const pb = scope.$builder;
671
+ if (!this.vertexTangent) {
672
+ return uv;
673
+ }
674
+ const that = this;
675
+ const funcName = 'Z_eyeRefractIrisUV';
676
+ pb.func(funcName, [
677
+ pb.vec2('uv'),
678
+ pb.vec3('viewVec'),
679
+ pb.vec3('worldPos'),
680
+ pb.float('depth'),
681
+ pb.float('ior')
682
+ ], function() {
683
+ this.$l.TBN = that.calculateTBN(this, this.worldPos, this.$inputs.wNorm, this.$inputs.wTangent, this.$inputs.wBinormal);
684
+ // World -> tangent is the transpose of the (orthonormal) TBN.
685
+ this.$l.vTangent = pb.normalize(pb.vec3(pb.dot(this.viewVec, this.TBN[0]), pb.dot(this.viewVec, this.TBN[1]), pb.dot(this.viewVec, this.TBN[2])));
686
+ // Incident direction points into the surface, hence the negation.
687
+ this.$l.refracted = pb.refract(pb.neg(this.vTangent), pb.vec3(0, 0, 1), pb.div(1, this.ior));
688
+ // Total internal reflection returns a zero vector; leave the UV alone.
689
+ this.$if(pb.lessThan(pb.dot(this.refracted, this.refracted), 0.0001), function() {
690
+ this.$return(this.uv);
691
+ });
692
+ // March to the iris plane. The guard keeps grazing angles, where the
693
+ // ray runs nearly parallel to the plane, from exploding the offset.
694
+ this.$l.travel = pb.div(this.depth, pb.max(pb.abs(this.refracted.z), 0.15));
695
+ this.$return(pb.add(this.uv, pb.mul(this.refracted.xy, this.travel)));
696
+ });
697
+ return pb.getGlobalScope()[funcName](uv, viewVec, scope.$inputs.worldPos, scope.zEyeIrisDepth, scope.zEyeIOR);
698
+ }
699
+ /**
700
+ * Rescales the iris radially so the pupil can open and close: a power curve
701
+ * pinned at the iris edge moves the pupil boundary while the limbus stays
702
+ * put, matching how a real iris deforms.
703
+ *
704
+ * @internal
705
+ */ applyPupilDilation(scope, irisUV) {
706
+ const pb = scope.$builder;
707
+ const funcName = 'Z_eyeApplyPupilDilation';
708
+ pb.func(funcName, [
709
+ pb.vec2('irisUV'),
710
+ pb.float('dilation')
711
+ ], function() {
712
+ // Operates in iris-texture space, where the pupil sits at (0.5, 0.5) and
713
+ // the iris rim is at radius 0.5.
714
+ this.$l.delta = pb.sub(this.irisUV, pb.vec2(0.5));
715
+ this.$l.dist = pb.length(this.delta);
716
+ this.$if(pb.lessThan(this.dist, 0.00001), function() {
717
+ this.$return(this.irisUV);
718
+ });
719
+ this.$l.norm = pb.clamp(pb.div(this.dist, 0.5), 0, 1);
720
+ // Maps a display radius to the texture radius it samples: an exponent
721
+ // above 1 samples further in, dilating the pupil. Pinned at norm = 1
722
+ // either way, so dilation never disturbs the limbus.
723
+ this.$l.exponent = pb.exp(pb.mul(this.dilation, 0.9));
724
+ this.$l.remapped = pb.mul(pb.pow(this.norm, this.exponent), 0.5);
725
+ this.$return(pb.add(pb.vec2(0.5), pb.mul(pb.div(this.delta, this.dist), this.remapped)));
726
+ });
727
+ return pb.getGlobalScope()[funcName](irisUV, scope.zEyePupilDilation);
728
+ }
729
+ fragmentShader(scope) {
730
+ super.fragmentShader(scope);
731
+ const pb = scope.$builder;
732
+ const that = this;
733
+ if (this.needFragmentColorInput()) {
734
+ const litPass = this.drawContext.renderPass.type === RENDER_PASS_TYPE_LIGHT;
735
+ if (litPass) {
736
+ scope.zEyeIrisCenter = pb.vec4().uniform(2);
737
+ scope.zEyeIrisRadius = pb.float().uniform(2);
738
+ scope.zEyeIrisDepth = pb.float().uniform(2);
739
+ scope.zEyeIOR = pb.float().uniform(2);
740
+ scope.zEyePupilRadius = pb.float().uniform(2);
741
+ scope.zEyePupilDilation = pb.float().uniform(2);
742
+ scope.zEyeIrisColor = pb.vec4().uniform(2);
743
+ scope.zEyeIrisBrightness = pb.float().uniform(2);
744
+ scope.zEyeLimbalRingWidth = pb.float().uniform(2);
745
+ scope.zEyeLimbalRingStrength = pb.float().uniform(2);
746
+ scope.zEyeScleraColor = pb.vec4().uniform(2);
747
+ scope.zEyeScleraWrap = pb.float().uniform(2);
748
+ scope.zEyeScleraEdgeTint = pb.vec4().uniform(2);
749
+ scope.zEyeCorneaSpecularStrength = pb.float().uniform(2);
750
+ scope.zEyeCorneaRoughness = pb.float().uniform(2);
751
+ if (this.socketOcclusion && this.vertexNormal) {
752
+ scope.zEyeSocketCos = pb.vec4().uniform(2);
753
+ scope.zEyeSocketStrength = pb.float().uniform(2);
754
+ }
755
+ if (this.contactAO) {
756
+ scope.zEyeContactAORange = pb.vec4().uniform(2);
757
+ scope.zEyeContactAORadius = pb.float().uniform(2);
758
+ scope.zEyeContactAOStrength = pb.float().uniform(2);
759
+ }
760
+ }
761
+ if (!litPass) {
762
+ // Depth/shadow style passes only need coverage, not shading.
763
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(1));
764
+ return;
765
+ }
766
+ // TBN[2] is the pre-normal-map geometric normal, needed for shadow normal
767
+ // offset bias.
768
+ scope.$l.normalInfo = this.calculateNormalAndTBN(scope, scope.$inputs.worldPos, scope.$inputs.wNorm, scope.$inputs.wTangent, scope.$inputs.wBinormal);
769
+ scope.$l.normal = scope.normalInfo.normal;
770
+ scope.$l.viewVec = this.calculateViewVector(scope, scope.$inputs.worldPos);
771
+ scope.$l.NoV = pb.clamp(pb.dot(scope.normal, scope.viewVec), 0.001, 1);
772
+ // Surface UV drives the region split; the iris lookup is refracted from it.
773
+ scope.$l.surfaceUV = this.hasIrisTexture() ? this.getIrisTexCoord(scope) : this.hasScleraTexture() ? this.getScleraTexCoord(scope) : pb.vec2(0.5);
774
+ scope.$l.surfaceOffset = pb.sub(scope.surfaceUV, scope.zEyeIrisCenter.xy);
775
+ scope.$l.surfaceDist = pb.length(scope.surfaceOffset);
776
+ // Refract on the surface UV, then remap the iris disc onto the full iris
777
+ // texture, which is authored as one iris filling the frame.
778
+ scope.$l.refractedUV = this.refractIrisUV(scope, scope.surfaceUV, scope.viewVec);
779
+ scope.$l.irisUV = pb.add(pb.vec2(0.5), pb.div(pb.sub(scope.refractedUV, scope.zEyeIrisCenter.xy), pb.mul(scope.zEyeIrisRadius, 2)));
780
+ scope.irisUV = this.applyPupilDilation(scope, scope.irisUV);
781
+ // Sclera.
782
+ scope.$l.scleraAlbedo = scope.zEyeScleraColor.rgb;
783
+ if (this.hasScleraTexture()) {
784
+ scope.scleraAlbedo = pb.mul(scope.scleraAlbedo, this.sampleScleraTexture(scope, scope.surfaceUV).rgb);
785
+ }
786
+ // Vasculature reads strongest away from the iris.
787
+ scope.$l.edgeAmount = pb.smoothStep(scope.zEyeIrisRadius, pb.add(scope.zEyeIrisRadius, 0.35), scope.surfaceDist);
788
+ scope.scleraAlbedo = pb.mix(scope.scleraAlbedo, pb.mul(scope.scleraAlbedo, scope.zEyeScleraEdgeTint.rgb), pb.mul(scope.edgeAmount, scope.zEyeScleraEdgeTint.a));
789
+ // Iris.
790
+ scope.$l.irisAlbedo = scope.zEyeIrisColor.rgb;
791
+ if (this.hasIrisTexture()) {
792
+ scope.irisAlbedo = pb.mul(scope.irisAlbedo, this.sampleIrisTexture(scope, scope.irisUV).rgb);
793
+ }
794
+ // Limbal ring, keyed off the surface radius so refraction cannot smear it.
795
+ scope.$l.limbalWidth = pb.mul(scope.zEyeIrisRadius, scope.zEyeLimbalRingWidth);
796
+ scope.$l.limbal = pb.smoothStep(pb.sub(scope.zEyeIrisRadius, scope.limbalWidth), scope.zEyeIrisRadius, scope.surfaceDist);
797
+ scope.irisAlbedo = pb.mul(scope.irisAlbedo, pb.sub(1, pb.mul(scope.limbal, scope.zEyeLimbalRingStrength)));
798
+ // Region blend. The transition is a fraction of the iris radius so it
799
+ // scales with the eye instead of being a fixed UV distance.
800
+ scope.$l.irisMask = pb.sub(1, pb.smoothStep(pb.mul(scope.zEyeIrisRadius, 0.97), pb.mul(scope.zEyeIrisRadius, 1.03), scope.surfaceDist));
801
+ scope.$l.albedo = pb.mix(scope.scleraAlbedo, scope.irisAlbedo, scope.irisMask);
802
+ scope.$l.diffuseLighting = pb.vec3(0);
803
+ scope.$l.specularLighting = pb.vec3(0);
804
+ scope.$l.alphaRoughness = pb.mul(scope.zEyeCorneaRoughness, scope.zEyeCorneaRoughness);
805
+ // Water/cornea reflectance at normal incidence.
806
+ scope.$l.F0 = pb.vec3(0.04);
807
+ scope.$l.F90 = pb.vec3(1);
808
+ const socketOcc = !!this.socketOcclusion && !!this.vertexNormal;
809
+ const baseLightPass = !this.drawContext.lightBlending;
810
+ // Evaluated once at function scope: it takes screen-space derivatives
811
+ // (uniform control flow only) and is view- but not light-dependent. Also
812
+ // needed in additive light passes, since it dims direct diffuse too.
813
+ scope.$l.contactAO = this.contactAO ? this.contactAOTerm(scope) : pb.float(1);
814
+ if (this.needCalculateEnvLight() && baseLightPass) {
815
+ // Socket occlusion: irradiance keyed off the surface normal, radiance
816
+ // off the reflection vector, so a reflection ray aimed at the eyelid
817
+ // dims even where the surface itself is open.
818
+ scope.$l.envIrradiance = this.getEnvLightIrradiance(scope, scope.normal);
819
+ if (socketOcc) {
820
+ scope.envIrradiance = pb.mul(scope.envIrradiance, this.socketAperture(scope, scope.normal, false));
821
+ }
822
+ scope.envIrradiance = pb.mul(scope.envIrradiance, scope.contactAO);
823
+ scope.diffuseLighting = pb.add(scope.diffuseLighting, scope.envIrradiance);
824
+ scope.$l.reflectVec = this.calculateReflectionVector(scope, scope.normal, scope.viewVec);
825
+ scope.$l.envFresnel = this.fresnelSchlick(scope, scope.NoV, scope.F0, scope.F90);
826
+ scope.$l.envRadiance = pb.mul(this.getEnvLightRadiance(scope, scope.reflectVec, scope.zEyeCorneaRoughness), scope.envFresnel, scope.zEyeCorneaSpecularStrength);
827
+ if (socketOcc) {
828
+ scope.envRadiance = pb.mul(scope.envRadiance, this.socketAperture(scope, scope.reflectVec, false));
829
+ }
830
+ // The lid occludes the sky the cornea mirrors just as much as the sky
831
+ // it diffuses.
832
+ scope.envRadiance = pb.mul(scope.envRadiance, scope.contactAO);
833
+ scope.specularLighting = pb.add(scope.specularLighting, scope.envRadiance);
834
+ }
835
+ this.forEachLight(scope, function(type, posRange, dirCutoff, colorIntensity, extra, shadow) {
836
+ this.$l.diffuseScale = pb.float(1);
837
+ this.$l.specularScale = pb.float(1);
838
+ this.$if(pb.equal(type, LIGHT_TYPE_POINT), function() {
839
+ this.diffuseScale = extra.x;
840
+ this.specularScale = extra.y;
841
+ });
842
+ this.$l.lightAtten = that.calculateLightAttenuation(this, type, this.$inputs.worldPos, posRange, dirCutoff, extra);
843
+ this.$l.lightDir = that.calculateLightDirection(this, type, this.$inputs.worldPos, posRange, dirCutoff);
844
+ this.$l.rawNoL = pb.dot(this.normal, this.lightDir);
845
+ this.$l.NoL = pb.clamp(this.rawNoL, 0, 1);
846
+ // calculateShadow() samples with implicit derivatives, which WGSL only
847
+ // permits in uniform control flow - so this must never sit inside a
848
+ // dynamic branch.
849
+ this.$l.shadowTerm = shadow ? that.calculateShadow(this, this.$inputs.worldPos, scope.normalInfo.TBN[2], pb.max(this.NoL, 1e-5)) : pb.float(1);
850
+ this.$l.lightColor = pb.mul(colorIntensity.rgb, colorIntensity.a, this.lightAtten, this.shadowTerm);
851
+ if (socketOcc) {
852
+ // A light arriving from outside the lid aperture is blocked before
853
+ // it reaches the eyeball - a shadow map cannot resolve this, since
854
+ // the lid sits inside any workable depth bias.
855
+ this.lightColor = pb.mul(this.lightColor, that.socketAperture(this, this.lightDir, true));
856
+ }
857
+ // Wrapped diffuse only for the sclera; the iris is lit through the
858
+ // cornea and stays comparatively crisp.
859
+ this.$l.NoLWrap = pb.clamp(pb.div(pb.add(this.rawNoL, this.zEyeScleraWrap), pb.add(1, this.zEyeScleraWrap)), 0, 1);
860
+ this.$l.diffuseTerm = pb.mix(this.NoLWrap, this.NoL, this.irisMask);
861
+ // The contact term dims direct diffuse too, or the contact shadow
862
+ // vanishes under direct lighting. Direct specular is left out: if the
863
+ // light is visible from the fragment, its highlight should be as well.
864
+ this.diffuseLighting = pb.add(this.diffuseLighting, pb.mul(this.lightColor, this.diffuseTerm, this.diffuseScale, this.contactAO));
865
+ this.$l.halfVec = pb.normalize(pb.add(this.lightDir, this.viewVec));
866
+ this.$l.NoH = pb.clamp(pb.dot(this.normal, this.halfVec), 0, 1);
867
+ this.$l.VoH = pb.clamp(pb.dot(this.viewVec, this.halfVec), 0, 1);
868
+ this.$l.D = that.distributionGGX(this, this.NoH, this.alphaRoughness);
869
+ this.$l.Vis = that.visGGX(this, this.NoV, this.NoL, this.alphaRoughness);
870
+ this.$l.F = that.fresnelSchlick(this, this.VoH, this.F0, this.F90);
871
+ this.specularLighting = pb.add(this.specularLighting, pb.mul(this.lightColor, this.F, pb.mul(this.D, this.Vis, this.NoL), this.zEyeCorneaSpecularStrength, this.specularScale));
872
+ });
873
+ // Light that has entered the iris and bounced around inside it - without
874
+ // this the iris crushes to black in shadow. Fed by environment
875
+ // irradiance (already carrying the occlusion terms), scaled by
876
+ // irisBrightness.
877
+ scope.$l.irisEnvLight = baseLightPass && this.needCalculateEnvLight() ? scope.envIrradiance : pb.vec3(0);
878
+ scope.$l.internal = pb.mul(scope.albedo, scope.irisEnvLight, scope.zEyeIrisBrightness, scope.irisMask);
879
+ scope.$l.litColor = pb.add(pb.mul(scope.albedo, scope.diffuseLighting), scope.internal, scope.specularLighting);
880
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.litColor, 1));
881
+ } else {
882
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
883
+ }
884
+ }
885
+ applyUniformValues(bindGroup, ctx, pass) {
886
+ super.applyUniformValues(bindGroup, ctx, pass);
887
+ if (this.needFragmentColor(ctx) && ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT) {
888
+ bindGroup.setValue('zEyeIrisCenter', this._irisCenter);
889
+ bindGroup.setValue('zEyeIrisRadius', this._irisRadius);
890
+ bindGroup.setValue('zEyeIrisDepth', this._irisDepth);
891
+ bindGroup.setValue('zEyeIOR', this._ior);
892
+ bindGroup.setValue('zEyePupilRadius', this._pupilRadius);
893
+ bindGroup.setValue('zEyePupilDilation', this._pupilDilation);
894
+ bindGroup.setValue('zEyeIrisColor', this._irisColor);
895
+ bindGroup.setValue('zEyeIrisBrightness', this._irisBrightness);
896
+ bindGroup.setValue('zEyeLimbalRingWidth', this._limbalRingWidth);
897
+ bindGroup.setValue('zEyeLimbalRingStrength', this._limbalRingStrength);
898
+ bindGroup.setValue('zEyeScleraColor', this._scleraColor);
899
+ bindGroup.setValue('zEyeScleraWrap', this._scleraWrap);
900
+ bindGroup.setValue('zEyeScleraEdgeTint', this._scleraEdgeTint);
901
+ bindGroup.setValue('zEyeCorneaSpecularStrength', this._corneaSpecularStrength);
902
+ bindGroup.setValue('zEyeCorneaRoughness', this._corneaRoughness);
903
+ if (this.socketOcclusion && this.vertexNormal) {
904
+ const d2r = Math.PI / 180;
905
+ const softness = this._socketOcclusionSoftness;
906
+ // (upper outer, upper inner, lower outer, lower inner) - see
907
+ // socketAperture. Clamped so the smoothstep edges never coincide.
908
+ this._socketCos.setXYZW(Math.cos(Math.min(this._upperLidAngle + softness, 179) * d2r), Math.cos(Math.max(this._upperLidAngle - softness, 1) * d2r), Math.cos(Math.min(this._lowerLidAngle + softness, 179) * d2r), Math.cos(Math.max(this._lowerLidAngle - softness, 1) * d2r));
909
+ bindGroup.setValue('zEyeSocketUp', this._socketUp);
910
+ bindGroup.setValue('zEyeSocketForward', this._socketForward);
911
+ bindGroup.setValue('zEyeSocketCos', this._socketCos);
912
+ bindGroup.setValue('zEyeSocketStrength', this._socketOcclusionStrength);
913
+ }
914
+ if (this.contactAO) {
915
+ const minD = this._contactAOMinDistance;
916
+ // The near pair ramps a tap in as an occluder, the far pair ramps it
917
+ // back out as unrelated geometry. Widen the outer distance (never
918
+ // clamp the inner one) to keep the two ramps from crossing.
919
+ const maxD = Math.max(this._contactAOMaxDistance, minD * 4);
920
+ // The far ramp starts at 0.75x rather than halfway: ramping out
921
+ // earlier discounts the thicker parts of the lid margin.
922
+ this._contactAORange.setXYZW(minD, minD * 2, maxD * 0.75, maxD);
923
+ bindGroup.setValue('zEyeContactAORange', this._contactAORange);
924
+ bindGroup.setValue('zEyeContactAORadius', this._contactAORadius);
925
+ bindGroup.setValue('zEyeContactAOStrength', this._contactAOStrength);
926
+ }
927
+ }
928
+ }
929
+ }
930
+
931
+ export { EyeMaterial };
932
+ //# sourceMappingURL=eye.js.map