three-base 2.0.142 → 2.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/DirectionalLight.d.ts +6 -6
  2. package/DirectionalLight.js +68 -68
  3. package/EventMap.d.ts +103 -103
  4. package/EventMap.js +17 -17
  5. package/EventMesh.d.ts +18 -18
  6. package/EventMesh.js +59 -59
  7. package/ExtrudeMesh.d.ts +112 -112
  8. package/ExtrudeMesh.js +342 -342
  9. package/FPScontrol.d.ts +531 -511
  10. package/FPScontrol.js +1866 -1799
  11. package/FPScontrol.ts +1922 -1855
  12. package/Fire/Fire.d.ts +28 -28
  13. package/Fire/Fire.js +80 -80
  14. package/Fire/FireGeometry.d.ts +9 -9
  15. package/Fire/FireGeometry.js +40 -40
  16. package/Fire/FireMaterial.d.ts +28 -28
  17. package/Fire/FireMaterial.js +32 -32
  18. package/Fire/index.d.ts +2 -2
  19. package/Fire/index.js +2 -2
  20. package/GLTFDracoLoader.d.ts +71 -71
  21. package/GLTFDracoLoader.js +266 -266
  22. package/GLTFDracoLoader.ts +1 -1
  23. package/Grass/Grass.d.ts +8 -8
  24. package/Grass/Grass.js +38 -38
  25. package/Grass/index.d.ts +2 -2
  26. package/Grass/index.js +2 -2
  27. package/Ground1/Ground1.d.ts +10 -10
  28. package/Ground1/Ground1.js +71 -71
  29. package/Ground1/index.d.ts +1 -1
  30. package/Ground1/index.js +1 -1
  31. package/MeshBase.d.ts +36 -36
  32. package/MeshBase.js +251 -247
  33. package/MeshBase.ts +6 -2
  34. package/Mirror.d.ts +25 -25
  35. package/Mirror.js +143 -143
  36. package/MktMesh/MktMesh.d.ts +301 -291
  37. package/MktMesh/MktMesh.js +1064 -1045
  38. package/MktMesh/MktMesh.ts +32 -11
  39. package/MktMesh/assetUrl.d.ts +59 -35
  40. package/MktMesh/assetUrl.js +165 -69
  41. package/MktMesh/assetUrl.ts +164 -69
  42. package/MktMesh/mktMappingShader.d.ts +12 -12
  43. package/MktMesh/mktMappingShader.js +138 -138
  44. package/MktMesh/mktMappingShader.ts +139 -139
  45. package/MktMesh/mktShader.d.ts +263 -263
  46. package/MktMesh/mktShader.js +1236 -1236
  47. package/MktMesh/mktShader.ts +1344 -1344
  48. package/MktMesh/panoBitmapTexture.d.ts +25 -25
  49. package/MktMesh/panoBitmapTexture.js +52 -52
  50. package/MktMesh/panoBitmapTexture.ts +54 -54
  51. package/MktMesh/panoSlotBind.d.ts +29 -29
  52. package/MktMesh/panoSlotBind.js +106 -106
  53. package/MktMesh/panoSlotBind.ts +153 -153
  54. package/MktMesh/panoSlotRoster.d.ts +22 -22
  55. package/MktMesh/panoSlotRoster.js +169 -169
  56. package/MktMesh/panoSlotRoster.ts +168 -168
  57. package/MktMesh/panoTileStitcher.d.ts +119 -119
  58. package/MktMesh/panoTileStitcher.js +529 -529
  59. package/MktMesh/panoTileStitcher.ts +616 -616
  60. package/MktMesh/types.d.ts +75 -75
  61. package/MktMesh/types.js +1 -1
  62. package/MktMesh/wallCapTessellate.d.ts +33 -33
  63. package/MktMesh/wallCapTessellate.js +97 -97
  64. package/MktMesh/wallCapTessellate.ts +68 -68
  65. package/PipeMesh/PathGeometry.d.ts +44 -44
  66. package/PipeMesh/PathGeometry.js +307 -307
  67. package/PipeMesh/PathPoint.d.ts +17 -17
  68. package/PipeMesh/PathPoint.js +31 -31
  69. package/PipeMesh/PathPointList.d.ts +31 -31
  70. package/PipeMesh/PathPointList.js +209 -209
  71. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  72. package/PipeMesh/PathTubeGeometry.js +130 -130
  73. package/PipeMesh/PipeMesh.d.ts +25 -25
  74. package/PipeMesh/PipeMesh.js +63 -63
  75. package/PipeMesh/index.d.ts +1 -1
  76. package/PipeMesh/index.js +1 -1
  77. package/PlaneModal1/PlaneModal1.d.ts +34 -34
  78. package/PlaneModal1/PlaneModal1.js +109 -109
  79. package/PlaneModal1/index.d.ts +1 -1
  80. package/PlaneModal1/index.js +1 -1
  81. package/PlaneText.d.ts +27 -27
  82. package/PlaneText.js +35 -35
  83. package/PlaneWithHtml.d.ts +13 -13
  84. package/PlaneWithHtml.js +66 -64
  85. package/PlaneWithHtml.ts +3 -1
  86. package/PointLight/PointLight.d.ts +13 -13
  87. package/PointLight/PointLight.js +64 -64
  88. package/PointLight/index.d.ts +1 -1
  89. package/PointLight/index.js +1 -1
  90. package/Sky/Sky.d.ts +9 -9
  91. package/Sky/Sky.js +22 -22
  92. package/Sky/index.d.ts +1 -1
  93. package/Sky/index.js +1 -1
  94. package/SpotLight.d.ts +6 -6
  95. package/SpotLight.js +72 -72
  96. package/SpriteModal1/SpriteModal1.d.ts +33 -33
  97. package/SpriteModal1/SpriteModal1.js +116 -116
  98. package/SpriteModal1/index.d.ts +1 -1
  99. package/SpriteModal1/index.js +1 -1
  100. package/SpriteText.d.ts +46 -46
  101. package/SpriteText.js +176 -176
  102. package/SpriteText2.d.ts +48 -48
  103. package/SpriteText2.js +223 -223
  104. package/SpriteText3.d.ts +86 -86
  105. package/SpriteText3.js +517 -517
  106. package/SpriteText3.ts +599 -599
  107. package/SpriteWithHtml.d.ts +9 -9
  108. package/SpriteWithHtml.js +46 -44
  109. package/SpriteWithHtml.ts +3 -1
  110. package/Text3D/Text3D.d.ts +20 -20
  111. package/Text3D/Text3D.js +58 -58
  112. package/Text3D/fontController.d.ts +60 -60
  113. package/Text3D/fontController.js +166 -166
  114. package/Text3D/index.d.ts +1 -1
  115. package/Text3D/index.js +1 -1
  116. package/ThreeBase.d.ts +248 -248
  117. package/ThreeBase.js +1202 -1202
  118. package/ThreeBase.ts +1314 -1314
  119. package/ThreeMouse.d.ts +53 -54
  120. package/ThreeMouse.js +453 -453
  121. package/Transition.d.ts +45 -45
  122. package/Transition.js +209 -209
  123. package/Water/Water.d.ts +5 -5
  124. package/Water/Water.js +44 -44
  125. package/Water/index.d.ts +2 -2
  126. package/Water/index.js +2 -2
  127. package/WaterFlow/FloatingDuck.d.ts +88 -88
  128. package/WaterFlow/FloatingDuck.js +307 -307
  129. package/WaterFlow/FloatingDuck.ts +445 -445
  130. package/WaterFlow/GPUFluidSimulation.d.ts +56 -56
  131. package/WaterFlow/GPUFluidSimulation.js +781 -781
  132. package/WaterFlow/GPUFluidSimulation.ts +827 -827
  133. package/WaterFlow/ReservoirSystem.d.ts +106 -106
  134. package/WaterFlow/ReservoirSystem.js +611 -611
  135. package/WaterFlow/ReservoirSystem.ts +754 -754
  136. package/WaterFlow/SPHField.d.ts +35 -35
  137. package/WaterFlow/SPHField.js +203 -203
  138. package/WaterFlow/SPHField.ts +228 -228
  139. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -69
  140. package/WaterFlow/ScreenSpaceFluidPass.js +728 -728
  141. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -789
  142. package/WaterFlow/ShallowWaterSurface.d.ts +45 -45
  143. package/WaterFlow/ShallowWaterSurface.js +438 -438
  144. package/WaterFlow/ShallowWaterSurface.ts +510 -510
  145. package/WaterFlow/WaterFlow.d.ts +87 -87
  146. package/WaterFlow/WaterFlow.js +171 -171
  147. package/WaterFlow/WaterFlow.ts +219 -219
  148. package/WaterFlow/types.d.ts +161 -161
  149. package/WaterFlow/types.js +1 -1
  150. package/WaterFlow/types.ts +189 -189
  151. package/WaterFlow/waterFlowPass.d.ts +3 -3
  152. package/WaterFlow/waterFlowPass.js +20 -20
  153. package/WaterFlow/waterFlowPass.ts +40 -40
  154. package/collisionOBB.d.ts +42 -42
  155. package/collisionOBB.js +112 -112
  156. package/collisionOBB.ts +108 -108
  157. package/declare.d.ts +25 -25
  158. package/index.d.ts +1 -1
  159. package/index.js +1 -1
  160. package/instanced/InstancedBatch.d.ts +57 -57
  161. package/instanced/InstancedBatch.js +348 -348
  162. package/instanced/InstancedBatch.ts +406 -406
  163. package/instanced/InstancedChild.d.ts +174 -174
  164. package/instanced/InstancedChild.js +264 -264
  165. package/instanced/InstancedChild.ts +327 -327
  166. package/instanced/InstancedGroup.d.ts +43 -43
  167. package/instanced/InstancedGroup.js +126 -126
  168. package/instanced/InstancedGroup.ts +137 -137
  169. package/instanced/InstancedMeshBase.d.ts +38 -38
  170. package/instanced/InstancedMeshBase.js +282 -282
  171. package/instanced/InstancedTemplate.d.ts +46 -46
  172. package/instanced/InstancedTemplate.js +125 -125
  173. package/keyControls.d.ts +29 -30
  174. package/keyControls.js +101 -101
  175. package/lensflare/LensFlare.d.ts +39 -39
  176. package/lensflare/LensFlare.js +284 -284
  177. package/package.json +6 -1
  178. package/pass/bloom_pass.d.ts +2 -2
  179. package/pass/bloom_pass.js +174 -174
  180. package/pass/cover_pass.d.ts +2 -2
  181. package/pass/cover_pass.js +108 -108
  182. package/pass/debugDepthState.d.ts +20 -20
  183. package/pass/debugDepthState.js +9 -9
  184. package/pass/debugDepthState.ts +22 -22
  185. package/pass/depthTestBias.d.ts +12 -12
  186. package/pass/depthTestBias.js +12 -12
  187. package/pass/depthTestBias.ts +12 -12
  188. package/pass/effectUserData.d.ts +27 -27
  189. package/pass/effectUserData.js +1 -1
  190. package/pass/effectUserData.ts +46 -46
  191. package/pass/frosted_glass_pass.d.ts +2 -2
  192. package/pass/frosted_glass_pass.js +150 -150
  193. package/pass/fsQuad.d.ts +9 -9
  194. package/pass/fsQuad.js +28 -28
  195. package/pass/glow_pass.d.ts +2 -2
  196. package/pass/glow_pass.js +350 -350
  197. package/pass/screen_bloom_pass.d.ts +11 -11
  198. package/pass/screen_bloom_pass.js +136 -136
  199. package/pass/ssao_pass.d.ts +88 -88
  200. package/pass/ssao_pass.js +799 -799
  201. package/pass/ssao_pass.ts +918 -918
  202. package/pass/stroke_pass.d.ts +2 -2
  203. package/pass/stroke_pass.js +267 -267
  204. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  205. package/shaders/FXAAShaderMaterial.js +20 -20
  206. package/shaders/baseDepthVisShader.d.ts +11 -11
  207. package/shaders/baseDepthVisShader.js +36 -36
  208. package/shaders/baseDepthVisShader.ts +36 -36
  209. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  210. package/shaders/baseTextureToRenderShader.js +19 -19
  211. package/shaders/copyShader.d.ts +3 -3
  212. package/shaders/copyShader.js +13 -13
  213. package/shaders/debugDepthCompareShader.d.ts +14 -14
  214. package/shaders/debugDepthCompareShader.js +49 -49
  215. package/shaders/debugDepthCompareShader.ts +50 -50
  216. package/shaders/debugDepthJudgeShader.d.ts +18 -18
  217. package/shaders/debugDepthJudgeShader.js +52 -52
  218. package/shaders/debugDepthJudgeShader.ts +54 -54
  219. package/shaders/debugExcludedShader.d.ts +13 -13
  220. package/shaders/debugExcludedShader.js +46 -46
  221. package/shaders/debugExcludedShader.ts +48 -48
  222. package/spriteTextBg.d.ts +1 -1
  223. package/spriteTextBg.js +20 -20
  224. package/spriteTextBg.ts +20 -20
  225. package/threeLoader.d.ts +35 -35
  226. package/threeLoader.js +518 -518
  227. package/threeUtils.d.ts +71 -53
  228. package/threeUtils.js +363 -329
  229. package/threeUtils.ts +35 -0
  230. package/types.d.ts +2 -2
  231. package/types.js +6 -6
package/pass/ssao_pass.js CHANGED
@@ -1,808 +1,808 @@
1
- import { Color, DataTexture, DoubleSide, LinearFilter, MeshBasicMaterial, NoBlending, RGBAFormat, RawShaderMaterial, RepeatWrapping, Vector2, Vector3, WebGLRenderTarget, Matrix4, MultiplyBlending, PerspectiveCamera, OrthographicCamera } from "three";
2
- import fsQuad from "./fsQuad";
3
- /**
4
- * 判断是否为「可透视的半透明表面」——这类物体不该在世界空间 AO 里充当实体。
5
- * 让它进入深度/法线图,被它遮挡的区域会发黑,成因分两种:
6
- * a) transparent:true + opacity<1(毛玻璃、透明装饰):深度图与法线图都落在半透明表面上,
7
- * GTAO 在其表面算自遮蔽(褶皱、紧贴其它几何的窄缝)→ 这份 AO 被乘到「透过它看到的
8
- * 实体」上 → 该区域发黑。
9
- * b) transparent:true + depthWrite:false(霓虹灯管、光晕、叠加面):深度图保留身后几何(远),
10
- * 但法线 pass 用 overrideMaterial(MeshNormalMaterial 默认 depthWrite:true)会强行把它的
11
- * 法线写进去(近)→ 深度与法线指的不是同一个点 → GTAO 几乎必然算错发黑。
12
- * 故深度 pass 与法线 pass 统一隐藏:该像素的深度/法线即身后不透明几何,AO 由真实可见几何
13
- * 决定;身后是背景时深度为 1.0,shader 守卫直接判「无遮蔽」(=1),不会发黑。
14
- *
15
- * 判据只看 material.transparent 本身,不再依赖 opacity / depthWrite(2026-08-30 修正):
16
- * 大量半透明是自定义 ShaderMaterial,alpha 由 shader 算出来,而材质的 opacity 仍是默认 1、
17
- * depthWrite 仍是 true —— 典型如 Ground1 地板(mapColor.a 径向渐变)、聚光灯光斑、
18
- * effectMaterial1,靠材质属性完全读不出来,会被漏判并继续污染 AO。
19
- * 代价:Mirror 这类「标了 transparent 但 shader 实际输出 alpha=1.0」的实心反射面会被误排除,
20
- * 故 Mirror 在构造函数里显式标记 userData.aoOpaque = true 保留(见各 Mirror 实现)。
21
- * 其余特例同样可用 userData.aoTransparent=true 强制排除 / userData.aoOpaque=true 强制保留。
22
- *
23
- * @param includeFloor 是否为「含地板」的 AO 数据准备(GTAO 深度/法线)。
24
- * - false(默认,可见深度 / 描边/辉光/毛玻璃遮挡判定):地板按普通半透明排除。
25
- * - true(AO 深度):标了 userData.aoFloor 的地板保留为实体,参与接触 AO 计算。
26
- */
27
- /**
28
- * 可见深度图的运行时开关:**标了 `userData.aoFloor` 的地板是否参与遮挡判定**。
29
- *
30
- * 默认 false = 保持原设计(可透视地板:透过地板看到的物体照常显示,不被地板挡住)。
31
- * 置 true 后,地板会写进 baseDepthTexture,于是**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
32
- * 面板「深度调试 → 地板参与遮挡」可一键切换,用于排查"挡不住"是不是地板(如 Ground1)造成的。
33
- *
34
- * 只影响 aoFloor 地板这一类;transparent 面的处理不受影响(见 isSeeThroughSurface)。
35
- */
36
- export const visibleDepthOptions = { includeFloor: false };
37
- const isSeeThroughSurface = (obj, includeFloor = false) => {
38
- const o = obj;
39
- const userData = o.userData;
40
- if (userData && userData.aoOpaque === true)
41
- return false;
42
- if (userData && userData.aoTransparent === true)
43
- return true;
44
- // 地板类半透明:可见深度(includeFloor=false,描边/辉光/毛玻璃/覆盖色遮挡判定用)里排除
45
- // → 可透视,透过地板看到的物体照常显示;AO 深度(includeFloor=true)里保留 → 接触 AO。
46
- // ⚠️ 例外:visibleDepthOptions.includeFloor=true 时,可见深度里也保留地板 → 地板能挡住特效。
47
- if (userData && userData.aoFloor === true) {
48
- return includeFloor ? false : !visibleDepthOptions.includeFloor;
49
- }
50
- const mat = o.material;
51
- if (!mat)
52
- return false;
53
- const list = Array.isArray(mat) ? mat : [mat];
54
- const transparent = list.some((m) => m !== null && m.transparent === true);
55
- // ⚠️ 关键修复(2026-09-06 回归根因):可见深度(includeFloor=false)必须把"标了
56
- // transparent 但实为实心/不透明"的面当作实体参与遮挡——three-base 大量面(玻璃、特效载体、
57
- // alpha 贴图实体、半透明墙)都标了 transparent:true;若在此排除,它们就不再遮挡身后特效,
58
- // 所有依赖 baseDepthTexture 的深度测试(描边/辉光/毛玻璃/覆盖色)全部失效、一律显示在前面。
59
- // 仅 AO 深度(includeFloor=true)需要排除透明面,避免 GTAO 在半透明面上算错发黑(保留原 AO 修复)。
60
- return includeFloor && transparent;
61
- };
62
- const isNonSurface = (obj, includeFloor = false) => {
63
- const o = obj;
64
- if (o.isSprite === true || o.isLine === true || o.isLine2 === true || o.isLineSegments2 === true || o.isPoints === true) {
65
- return true;
66
- }
67
- return isSeeThroughSurface(obj, includeFloor);
68
- };
69
- const hideNonSurface = (scene, includeFloor = false) => {
70
- const list = [];
71
- scene.traverse((obj) => {
72
- if (isNonSurface(obj, includeFloor)) {
73
- list.push({ obj, prev: obj.visible });
74
- obj.visible = false;
75
- }
76
- });
77
- return list;
78
- };
79
- const restoreVisibility = (list) => {
80
- list.forEach(({ obj, prev }) => (obj.visible = prev));
81
- };
82
- /**
83
- * 深度 pass:固定排除非实体表面后,将场景深度渲染到 depthRenderTarget。
84
- * depthRenderTarget.depthTexture 即 baseDepthTexture,供 SSAO / 描边 / 发光等后续 pass 复用。
85
- * 排除的非实体对象(sprite/线条/点)在深度图保留 clear 深度(1.0=远),与背景一致;排除的
86
- * 半透明对象则保留其身后不透明几何的深度(身后无几何时同样为 1.0)。均不影响颜色通道渲染。
87
- */
88
- export function renderAoDepthPass(renderer, camera, scene, depthRenderTarget, includeFloor = false) {
89
- const hidden = hideNonSurface(scene, includeFloor);
90
- // ⚠️ 深度预通道铁律:必须让所有「未被排除」的可见表面都写入深度,深度图才有真实的几何遮挡信息。
91
- // 透明材质(特效载体、半透明墙、alpha 贴图实体等)大多 transparent:true 且 depthWrite:false,
92
- // 若不强转,它们不会写进 baseDepthTexture → 深度图只剩不透明几何 → 描边/辉光/毛玻璃/覆盖色
93
- // 依赖 baseDepthTexture 的遮挡判定全部失效、一律显示在前(即本次回归根因)。
94
- // 被排除的半透明面(aoTransparent / aoFloor / Mirror-aoOpaque 等)已在 hideNonSurface 中隐藏,
95
- // 此处根本不会触及,故不影响 see-through 设计;其余透明面强制写深度 = 作为实体参与遮挡,符合预期。
96
- const depthWriteRestore = [];
97
- scene.traverse((obj) => {
98
- const mesh = obj;
99
- if (!mesh.isMesh || !mesh.material)
100
- return;
101
- const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
102
- list.forEach((m) => {
103
- if (m && m.depthWrite === false) {
104
- depthWriteRestore.push({ mat: m, val: false });
105
- m.depthWrite = true;
106
- }
107
- });
108
- });
109
- const prevRenderTarget = renderer.getRenderTarget();
110
- renderer.setRenderTarget(depthRenderTarget);
111
- renderer.clear();
112
- renderer.render(scene, camera);
113
- // 还原 depthWrite,避免污染后续颜色通道渲染的透明混合
114
- depthWriteRestore.forEach(({ mat, val }) => { mat.depthWrite = val; });
115
- restoreVisibility(hidden);
116
- renderer.setRenderTarget(prevRenderTarget);
117
- }
118
- /**
119
- * 调试用:把「被排除出可见深度图」的表面单独渲成红色 mask。
120
- *
121
- * 这些面在 renderAoDepthPass 里被 hideNonSurface 隐藏,因此**不会写进 baseDepthTexture**,
122
- * 也就永远挡不住描边/辉光/毛玻璃/覆盖色 —— 这正是"深度测试看起来失效"最常见的成因
123
- * (用户以为是 bug,其实是该面被设计成可透视:sprite/线条/点、userData.aoTransparent、
124
- * 以及标了 userData.aoFloor 的地板 Ground1)。
125
- * 面板「深度调试 → 排除面标记」叠加显示:灰=能挡住的真实遮挡面,红=被排除、挡不住任何东西。
126
- *
127
- * 只渲染 Mesh:sprite / Line2 / Points 在 MeshBasicMaterial override 下渲染异常(见文件头注释),
128
- * 且它们数量少、用户关心的遮挡体基本都是 Mesh,故跳过。
129
- */
130
- export function renderExcludedMaskPass(renderer, camera, scene, maskRenderTarget, includeFloor = false) {
131
- const maskMaterial = new MeshBasicMaterial({
132
- color: 0xff0000,
133
- depthTest: false,
134
- depthWrite: false,
135
- side: DoubleSide,
136
- });
137
- const prevVisible = [];
138
- scene.traverse((obj) => {
139
- const isMesh = obj.isMesh === true;
140
- prevVisible.push({ obj, prev: obj.visible });
141
- // 只显示「被排除的 Mesh」:其余一律隐藏,得到纯黑底 + 红面的 mask
142
- obj.visible = isMesh && isNonSurface(obj, includeFloor);
143
- });
144
- const prevOverride = scene.overrideMaterial;
145
- const prevTarget = renderer.getRenderTarget();
146
- const prevClear = renderer.getClearColor(new Color());
147
- const prevAlpha = renderer.getClearAlpha();
148
- scene.overrideMaterial = maskMaterial;
149
- renderer.setRenderTarget(maskRenderTarget);
150
- renderer.setClearColor(0x000000, 1);
151
- renderer.clear();
152
- renderer.render(scene, camera);
153
- renderer.setClearColor(prevClear, prevAlpha);
154
- renderer.setRenderTarget(prevTarget);
155
- scene.overrideMaterial = prevOverride;
156
- restoreVisibility(prevVisible);
157
- maskMaterial.dispose();
158
- }
159
- /**
160
- * 法线 pass(仅 SSAO 开启时需要):固定排除非实体表面后,以中性灰清屏 + normalMaterial
161
- * override 将视图空间法线渲染到 normalRenderTarget,供 GTAO 风格 SSAO 使用。
162
- * 排除对象在法线图保留中性灰清屏色 → unpackRGBToNormal(0.5,0.5,0.5)=(0,0,0) →
163
- * SSAO 退化法线守卫判"无遮蔽"(=1),避免精灵弹窗被 AO 乘黑。清屏色用后即还原。
164
- */
165
- export function renderAoNormalPass(renderer, camera, scene, normalRenderTarget, normalMaterial, includeFloor = false) {
166
- const hidden = hideNonSurface(scene, includeFloor);
167
- // ⚠️ 与 renderAoDepthPass 同款「强制写深度」:本 pass 的深度会被当作含地板 AO 深度使用
168
- // (normalRenderTarget.depthTexture)。透明面若因 depthWrite:false 不写深度,AO 深度就会漏掉它们,
169
- // 与被本 pass 取代的那次独立深度 pass 内容不一致。pass 结束后立即还原。
170
- const depthWriteRestore = [];
171
- scene.traverse((obj) => {
172
- const mesh = obj;
173
- if (!mesh.isMesh || !mesh.material)
174
- return;
175
- const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
176
- list.forEach((m) => {
177
- if (m && m.depthWrite === false) {
178
- depthWriteRestore.push({ mat: m, val: false });
179
- m.depthWrite = true;
180
- }
181
- });
182
- });
183
- const prevRenderTarget = renderer.getRenderTarget();
184
- const prevClearColor = new Color();
185
- renderer.getClearColor(prevClearColor);
186
- const prevClearAlpha = renderer.getClearAlpha();
187
- renderer.setClearColor(new Color(0.5, 0.5, 0.5), 1);
188
- renderer.setRenderTarget(normalRenderTarget);
189
- renderer.clear();
190
- scene.overrideMaterial = normalMaterial;
191
- renderer.render(scene, camera);
192
- scene.overrideMaterial = null;
193
- restoreVisibility(hidden);
194
- depthWriteRestore.forEach(({ mat, val }) => { mat.depthWrite = val; });
195
- renderer.setClearColor(prevClearColor, prevClearAlpha);
196
- renderer.setRenderTarget(prevRenderTarget);
197
- }
198
- // 噪声贴图尺寸(奇数,magic square 要求奇数)。越大 → 旋转/半径抖动越细,
199
- // GTAO 各向异性(转视角时 AO 范围"呼吸")越弱。17×17=289 种独立样本远多于默认的 5×5=25。
200
- const NOISE_SIZE = 17;
201
- // ---------------------------------------------------------------------------
202
- // GTAO 风格 SSAO(地平线积分)
203
- // 相对旧版手写 SSAO 的两点关键改进:
204
- // 1. 法线直接来自视图空间法线纹理(MeshNormalMaterial 渲染),不再用深度差分重建,
205
- // 从根本上消除噪声与边缘抖动。
206
- // 2. 采样采用 Ground Truth Ambient Occlusion 的地平线积分(horizon-based),
207
- // 几乎无 halo(光晕)与漏光。
208
- // 深度仍使用项目的对数深度(logarithmicDepthBuffer: true),通过 linearizeLogDepth
209
- // 还原 viewZ,与旧版保持一致;法线纹理来自 ThreeBase 的 normalRenderTarget。
210
- // 参考:three/examples/jsm/shaders/GTAOShader.js 的 horizon-based 积分核心。
211
- // 去噪:GTAO 计算的 AO 仍带高频噪声,这里用 Poisson 边缘保持去噪(双边去噪,
212
- // 依赖 normalTexture + baseDepthTexture)取代旧版 separable 高斯模糊,能在去噪的同时
213
- // 严格守住几何边缘。参考 three/examples/jsm/shaders/PoissonDenoiseShader.js。
214
- // ---------------------------------------------------------------------------
215
- const ssaoTextureTarget = new WebGLRenderTarget(512, 512, {
216
- minFilter: LinearFilter,
217
- magFilter: LinearFilter,
218
- format: RGBAFormat,
219
- });
220
- const blurTextureTarget = new WebGLRenderTarget(512, 512, {
221
- minFilter: LinearFilter,
222
- magFilter: LinearFilter,
223
- format: RGBAFormat,
224
- });
225
- const size = new Vector2();
226
- const projectionMatrix = new Matrix4();
227
- const inverseProjectionMatrix = new Matrix4();
228
- // 低差异方向旋转噪声(magic square noise),用于旋转每个像素的采样核,避免带状瑕疵
229
- function generateMagicSquare(size) {
230
- const noiseSize = Math.floor(size) % 2 === 0 ? Math.floor(size) + 1 : Math.floor(size);
231
- const noiseSquareSize = noiseSize * noiseSize;
232
- const magicSquare = new Array(noiseSquareSize).fill(0);
233
- let i = Math.floor(noiseSize / 2);
234
- let j = noiseSize - 1;
235
- for (let num = 1; num <= noiseSquareSize;) {
236
- if (i === -1 && j === noiseSize) {
237
- j = noiseSize - 2;
238
- i = 0;
239
- }
240
- else {
241
- if (j === noiseSize) {
242
- j = 0;
243
- }
244
- if (i < 0) {
245
- i = noiseSize - 1;
246
- }
247
- }
248
- if (magicSquare[i * noiseSize + j] !== 0) {
249
- j -= 2;
250
- i++;
251
- continue;
252
- }
253
- else {
254
- magicSquare[i * noiseSize + j] = num++;
255
- }
256
- j++;
257
- i--;
258
- }
259
- return magicSquare;
260
- }
261
- function generateMagicSquareNoise(size = NOISE_SIZE) {
262
- const noiseSize = Math.floor(size) % 2 === 0 ? Math.floor(size) + 1 : Math.floor(size);
263
- const magicSquare = generateMagicSquare(noiseSize);
264
- const noiseSquareSize = magicSquare.length;
265
- const data = new Uint8Array(noiseSquareSize * 4);
266
- // 用确定性伪随机给每个像素一个 [0,1] 的 w 通道(用于采样半径抖动 sampleDir.w),
267
- // 同时 xy 仍由 magic square 角度决定(保证方向分布均匀无聚集)。
268
- // ⚠️ 旧实现把 w 通道写死 255(=1.0),导致 sampleDir.w 恒等于 1.0,
269
- // 半径抖动完全失效、旋转方向只有 25 种 → GTAO 各向异性明显(转视角 AO 范围"呼吸")。
270
- for (let inx = 0; inx < noiseSquareSize; ++inx) {
271
- const iAng = magicSquare[inx];
272
- const angle = (2 * Math.PI * iAng) / noiseSquareSize;
273
- const randomVec = new Vector3(Math.cos(angle), Math.sin(angle), 0).normalize();
274
- // 每像素独立伪随机种子(基于行列索引),避免 w 通道规律化
275
- const row = Math.floor(inx / noiseSize);
276
- const col = inx % noiseSize;
277
- const rnd = ((row * 73856093) ^ (col * 19349663) ^ (inx * 83492791)) >>> 0;
278
- const wRand = (rnd % 1000) / 1000; // [0,1)
279
- data[inx * 4] = (randomVec.x * 0.5 + 0.5) * 255;
280
- data[inx * 4 + 1] = (randomVec.y * 0.5 + 0.5) * 255;
281
- data[inx * 4 + 2] = 127;
282
- data[inx * 4 + 3] = Math.floor(wRand * 255); // 半径抖动真正生效
283
- }
284
- const noiseTexture = new DataTexture(data, noiseSize, noiseSize);
285
- noiseTexture.wrapS = RepeatWrapping;
286
- noiseTexture.wrapT = RepeatWrapping;
287
- noiseTexture.needsUpdate = true;
288
- return noiseTexture;
289
- }
290
- const noiseTexture = generateMagicSquareNoise();
291
- const ssaoShader = new RawShaderMaterial({
292
- defines: {
293
- SAMPLES: 32,
294
- },
295
- uniforms: {
296
- depthTexture: { value: null },
297
- normalTexture: { value: null },
298
- tNoise: { value: noiseTexture },
299
- resolution: { value: new Vector2(1 / 512, 1 / 512) },
300
- noiseResolution: { value: new Vector2(NOISE_SIZE, NOISE_SIZE) },
301
- projectionMatrix: { value: new Matrix4() },
302
- inverseProjectionMatrix: { value: new Matrix4() },
303
- ssaoRadius: { value: 5.0 },
304
- ssaoFalloff: { value: 5.0 },
305
- ssaoBias: { value: 0.01 },
306
- scale: { value: 1.0 },
307
- cameraNear: { value: 0.1 },
308
- cameraFar: { value: 20000 },
309
- distanceExponent: { value: 1.0 },
310
- distanceFallOff: { value: 1.0 },
311
- },
312
- vertexShader: `
313
- attribute vec3 position;
314
- attribute vec2 uv;
315
- varying vec2 vUv;
316
- void main() {
317
- vUv = uv;
318
- gl_Position = vec4(position, 1.0);
319
- }`,
320
- fragmentShader: `
321
- #ifdef GL_ES
322
- precision highp float;
323
- #endif
324
-
325
- uniform sampler2D depthTexture;
326
- uniform sampler2D normalTexture;
327
- uniform sampler2D tNoise;
328
- uniform vec2 resolution;
329
- uniform vec2 noiseResolution;
330
- uniform mat4 projectionMatrix;
331
- uniform mat4 inverseProjectionMatrix;
332
- uniform float ssaoRadius;
333
- uniform float ssaoFalloff;
334
- uniform float ssaoBias;
335
- uniform float scale;
336
- uniform float cameraNear;
337
- uniform float cameraFar;
338
- uniform float distanceExponent;
339
- uniform float distanceFallOff;
340
-
341
- varying vec2 vUv;
342
-
343
- const float PI = 3.141592653589793;
344
-
345
- // ⚠️ 关键:本项目渲染器 logarithmicDepthBuffer:true,baseDepthTexture 存的是「对数编码」的 gl_FragDepth。
346
- // three.js r167 写深度(logdepthbuf_fragment,透视下 gl_Position.w = -viewZ):
347
- // gl_FragDepth = log2(1.0 - viewZ) / log2(cameraFar + 1.0)
348
- // 反解得真实视图空间 Z(负值 = 相机前方):
349
- // viewZ = 1.0 - 2^(logDepth * log2(cameraFar + 1.0))
350
- // 注:three.js 的 perspectiveDepthToViewZ() 期望的是「常规透视非线性深度」(gl_FragCoord.z),
351
- // 与这里的对数编码是完全不同的两套,不能直接套用——对数深度必须显式解指数。
352
- // (Python 数值复算验证:该公式对任意距离还原误差 = 0,perspectiveDepthToViewZ 误差高达数米级。)
353
- float viewZFromLogDepth(float logDepth) {
354
- return 1.0 - pow(2.0, logDepth * log2(cameraFar + 1.0));
355
- }
356
-
357
- // MeshNormalMaterial 把视图空间法线 pack 到 [0,1],这里解回 [-1,1]
358
- vec3 unpackRGBToNormal(vec3 rgb) {
359
- return rgb * 2.0 - 1.0;
360
- }
361
-
362
- // 由 uv + 真实 viewZ 重建视图空间位置。
363
- // 透视投影下:ndc.xy = clip.xy/clip.w,clip.xy = (f/aspect * X, f * Y),clip.w = -viewZ
364
- // 反解得 X = ndc.x * (-viewZ) / (f/aspect),Y = ndc.y * (-viewZ) / f,Z = viewZ。
365
- // 注意:旧写法用 inverseProjectionMatrix 反投影 ndc.z=0 取「方向」再乘 viewZ/(-dir.z),
366
- // 会把方向翻到相机背后(z/y 反号),导致平地重建到天顶方向 → GTAO 把开阔地面误判遮蔽压黑。
367
- // 这里直接由 viewZ 解析构造,无符号陷阱,且对数深度已在外层 viewZFromLogDepth 还原成真实 viewZ,
368
- // 与本函数无关(不再依赖 projectionMatrix 的逆,故 inverseProjectionMatrix uniform 可保留也可移除)。
369
- vec3 reconstructPosition(vec2 uv, float viewZ) {
370
- // 透视投影:projectionMatrix[0][0] = f/aspect,projectionMatrix[1][1] = f,clip.w = -viewZ
371
- float fDivAspect = projectionMatrix[0][0];
372
- float f = projectionMatrix[1][1];
373
- vec2 ndc = uv * 2.0 - 1.0;
374
- float negZ = -viewZ;
375
- vec3 pos;
376
- pos.x = ndc.x * negZ / fDivAspect;
377
- pos.y = ndc.y * negZ / f;
378
- pos.z = viewZ;
379
- return pos;
380
- }
381
-
382
- float getLogDepth(vec2 uv) {
383
- return texture2D(depthTexture, uv).r;
1
+ import { Color, DataTexture, DoubleSide, LinearFilter, MeshBasicMaterial, NoBlending, RGBAFormat, RawShaderMaterial, RepeatWrapping, Vector2, Vector3, WebGLRenderTarget, Matrix4, MultiplyBlending, PerspectiveCamera, OrthographicCamera } from "three";
2
+ import fsQuad from "./fsQuad";
3
+ /**
4
+ * 判断是否为「可透视的半透明表面」——这类物体不该在世界空间 AO 里充当实体。
5
+ * 让它进入深度/法线图,被它遮挡的区域会发黑,成因分两种:
6
+ * a) transparent:true + opacity<1(毛玻璃、透明装饰):深度图与法线图都落在半透明表面上,
7
+ * GTAO 在其表面算自遮蔽(褶皱、紧贴其它几何的窄缝)→ 这份 AO 被乘到「透过它看到的
8
+ * 实体」上 → 该区域发黑。
9
+ * b) transparent:true + depthWrite:false(霓虹灯管、光晕、叠加面):深度图保留身后几何(远),
10
+ * 但法线 pass 用 overrideMaterial(MeshNormalMaterial 默认 depthWrite:true)会强行把它的
11
+ * 法线写进去(近)→ 深度与法线指的不是同一个点 → GTAO 几乎必然算错发黑。
12
+ * 故深度 pass 与法线 pass 统一隐藏:该像素的深度/法线即身后不透明几何,AO 由真实可见几何
13
+ * 决定;身后是背景时深度为 1.0,shader 守卫直接判「无遮蔽」(=1),不会发黑。
14
+ *
15
+ * 判据只看 material.transparent 本身,不再依赖 opacity / depthWrite(2026-08-30 修正):
16
+ * 大量半透明是自定义 ShaderMaterial,alpha 由 shader 算出来,而材质的 opacity 仍是默认 1、
17
+ * depthWrite 仍是 true —— 典型如 Ground1 地板(mapColor.a 径向渐变)、聚光灯光斑、
18
+ * effectMaterial1,靠材质属性完全读不出来,会被漏判并继续污染 AO。
19
+ * 代价:Mirror 这类「标了 transparent 但 shader 实际输出 alpha=1.0」的实心反射面会被误排除,
20
+ * 故 Mirror 在构造函数里显式标记 userData.aoOpaque = true 保留(见各 Mirror 实现)。
21
+ * 其余特例同样可用 userData.aoTransparent=true 强制排除 / userData.aoOpaque=true 强制保留。
22
+ *
23
+ * @param includeFloor 是否为「含地板」的 AO 数据准备(GTAO 深度/法线)。
24
+ * - false(默认,可见深度 / 描边/辉光/毛玻璃遮挡判定):地板按普通半透明排除。
25
+ * - true(AO 深度):标了 userData.aoFloor 的地板保留为实体,参与接触 AO 计算。
26
+ */
27
+ /**
28
+ * 可见深度图的运行时开关:**标了 `userData.aoFloor` 的地板是否参与遮挡判定**。
29
+ *
30
+ * 默认 false = 保持原设计(可透视地板:透过地板看到的物体照常显示,不被地板挡住)。
31
+ * 置 true 后,地板会写进 baseDepthTexture,于是**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
32
+ * 面板「深度调试 → 地板参与遮挡」可一键切换,用于排查"挡不住"是不是地板(如 Ground1)造成的。
33
+ *
34
+ * 只影响 aoFloor 地板这一类;transparent 面的处理不受影响(见 isSeeThroughSurface)。
35
+ */
36
+ export const visibleDepthOptions = { includeFloor: false };
37
+ const isSeeThroughSurface = (obj, includeFloor = false) => {
38
+ const o = obj;
39
+ const userData = o.userData;
40
+ if (userData && userData.aoOpaque === true)
41
+ return false;
42
+ if (userData && userData.aoTransparent === true)
43
+ return true;
44
+ // 地板类半透明:可见深度(includeFloor=false,描边/辉光/毛玻璃/覆盖色遮挡判定用)里排除
45
+ // → 可透视,透过地板看到的物体照常显示;AO 深度(includeFloor=true)里保留 → 接触 AO。
46
+ // ⚠️ 例外:visibleDepthOptions.includeFloor=true 时,可见深度里也保留地板 → 地板能挡住特效。
47
+ if (userData && userData.aoFloor === true) {
48
+ return includeFloor ? false : !visibleDepthOptions.includeFloor;
384
49
  }
385
-
386
- // 由 uv 直接得到真实视图空间位置(对数深度反解 viewZ 后再重建)
387
- vec3 getViewPosition(vec2 uv) {
388
- return reconstructPosition(uv, viewZFromLogDepth(getLogDepth(uv)));
50
+ const mat = o.material;
51
+ if (!mat)
52
+ return false;
53
+ const list = Array.isArray(mat) ? mat : [mat];
54
+ const transparent = list.some((m) => m !== null && m.transparent === true);
55
+ // ⚠️ 关键修复(2026-09-06 回归根因):可见深度(includeFloor=false)必须把"标了
56
+ // transparent 但实为实心/不透明"的面当作实体参与遮挡——three-base 大量面(玻璃、特效载体、
57
+ // alpha 贴图实体、半透明墙)都标了 transparent:true;若在此排除,它们就不再遮挡身后特效,
58
+ // 所有依赖 baseDepthTexture 的深度测试(描边/辉光/毛玻璃/覆盖色)全部失效、一律显示在前面。
59
+ // 仅 AO 深度(includeFloor=true)需要排除透明面,避免 GTAO 在半透明面上算错发黑(保留原 AO 修复)。
60
+ return includeFloor && transparent;
61
+ };
62
+ const isNonSurface = (obj, includeFloor = false) => {
63
+ const o = obj;
64
+ if (o.isSprite === true || o.isLine === true || o.isLine2 === true || o.isLineSegments2 === true || o.isPoints === true) {
65
+ return true;
389
66
  }
390
-
391
- vec3 getViewNormal(vec2 uv) {
392
- return unpackRGBToNormal(texture2D(normalTexture, uv).rgb);
393
- }
394
-
395
- // 采样点的 uv:直接读该 uv 处的「真实对数深度」,不再走常规投影(投影会把对数深度当线性算错)。
396
- // 这里只返回 uv,深度由 getViewPosition(sampleUv) 内部用对数反解。
397
- vec2 getSceneUv(vec3 sampleViewPos) {
398
- // sampleViewPos 已是真实视图空间坐标,用常规投影求屏幕 uv(投影矩阵本身对位置是对的,
399
- // 错只错在「深度纹理的编码」上,所以位置的投影反算没问题,只需读取采样点的真实对数深度)。
400
- vec4 sampleClipPos = projectionMatrix * vec4(sampleViewPos, 1.0);
401
- return sampleClipPos.xy / sampleClipPos.w * 0.5 + 0.5;
402
- }
403
-
404
- void main() {
405
- float logDepth = getLogDepth(vUv);
406
- if (logDepth >= 1.0) {
407
- gl_FragColor = vec4(1.0);
408
- return;
409
- }
410
- vec3 viewPos = getViewPosition(vUv);
411
- vec3 viewNormal = getViewNormal(vUv);
412
-
413
- // 退化/非法法线(长度为 0 或含 NaN,常见于缺法线的模型)无法做地平线积分,
414
- // 按"无遮蔽"处理,避免整片被乘黑。NaN 检测用 x != x(GLSL ES 1.0 无 isnan)。
415
- float normalLen2 = dot(viewNormal, viewNormal);
416
- if (normalLen2 < 1e-4 || normalLen2 != normalLen2) {
417
- gl_FragColor = vec4(1.0);
418
- return;
419
- }
420
-
421
- float radiusToUse = ssaoRadius;
422
- float thicknessToUse = ssaoFalloff;
423
-
424
- vec2 noiseUv = vUv * resolution / noiseResolution;
425
- vec4 noiseTexel = texture2D(tNoise, noiseUv);
426
- vec3 randomVec = noiseTexel.xyz * 2.0 - 1.0;
427
- vec3 tangent = normalize(vec3(randomVec.xy, 0.0));
428
- vec3 bitangent = vec3(-tangent.y, tangent.x, 0.0);
429
- mat3 kernelMatrix = mat3(tangent, bitangent, vec3(0.0, 0.0, 1.0));
430
-
431
- // SAMPLES 为编译期常量(defines),由此推导 DIRECTIONS / STEPS
432
- const int DIRECTIONS = SAMPLES < 30 ? 3 : 5;
433
- const int STEPS = (SAMPLES + DIRECTIONS - 1) / DIRECTIONS;
434
- float ao = 0.0;
435
-
436
- for (int i = 0; i < DIRECTIONS; ++i) {
437
- float angle = float(i) / float(DIRECTIONS) * PI;
438
- vec4 sampleDir = vec4(cos(angle), sin(angle), 0.0, 0.5 + 0.5 * noiseTexel.w);
439
- sampleDir.xyz = normalize(kernelMatrix * sampleDir.xyz);
440
-
441
- vec3 viewDir = normalize(-viewPos.xyz);
442
- vec3 sliceBitangent = normalize(cross(sampleDir.xyz, viewDir));
443
- vec3 sliceTangent = cross(sliceBitangent, viewDir);
444
- vec3 normalInSlice = normalize(viewNormal - sliceBitangent * dot(viewNormal, sliceBitangent));
445
-
446
- vec3 tangentToNormalInSlice = cross(normalInSlice, sliceBitangent);
447
- vec2 cosHorizons = vec2(dot(viewDir, tangentToNormalInSlice), dot(viewDir, -tangentToNormalInSlice));
448
-
449
- for (int j = 0; j < STEPS; ++j) {
450
- vec3 sampleViewOffset = sampleDir.xyz * radiusToUse * sampleDir.w * pow(float(j + 1) / float(STEPS), distanceExponent);
451
-
452
- vec2 sampleUv0 = getSceneUv(viewPos + sampleViewOffset);
453
- vec3 sampleSceneViewPos = getViewPosition(sampleUv0);
454
- vec3 viewDelta = sampleSceneViewPos - viewPos;
455
- // 切面阈值(bias):只接受"沿法线方向有明显高度突起"的采样点;
456
- // 大平面上同平面远采样点 sampleHeight≈0,低于 bias 即跳过,避免平地自身被误判遮蔽压黑。
457
- float sampleHeight0 = dot(viewDelta, viewNormal);
458
- if (abs(viewDelta.z) < thicknessToUse && sampleHeight0 > ssaoBias) {
459
- float sampleCosHorizon = dot(viewDir, normalize(viewDelta));
460
- cosHorizons.x += max(0.0, (sampleCosHorizon - cosHorizons.x) * mix(1.0, 2.0 / float(j + 2), distanceFallOff));
461
- }
462
-
463
- vec2 sampleUv1 = getSceneUv(viewPos - sampleViewOffset);
464
- sampleSceneViewPos = getViewPosition(sampleUv1);
465
- viewDelta = sampleSceneViewPos - viewPos;
466
- float sampleHeight1 = dot(viewDelta, viewNormal);
467
- if (abs(viewDelta.z) < thicknessToUse && sampleHeight1 > ssaoBias) {
468
- float sampleCosHorizon = dot(viewDir, normalize(viewDelta));
469
- cosHorizons.y += max(0.0, (sampleCosHorizon - cosHorizons.y) * mix(1.0, 2.0 / float(j + 2), distanceFallOff));
470
- }
67
+ return isSeeThroughSurface(obj, includeFloor);
68
+ };
69
+ const hideNonSurface = (scene, includeFloor = false) => {
70
+ const list = [];
71
+ scene.traverse((obj) => {
72
+ if (isNonSurface(obj, includeFloor)) {
73
+ list.push({ obj, prev: obj.visible });
74
+ obj.visible = false;
471
75
  }
472
-
473
- vec2 sinHorizons = sqrt(1.0 - cosHorizons * cosHorizons);
474
- float nx = dot(normalInSlice, sliceTangent);
475
- float ny = dot(normalInSlice, viewDir);
476
- float nxb = 1.0 / 2.0 * (acos(cosHorizons.y) - acos(cosHorizons.x) + sinHorizons.x * cosHorizons.x - sinHorizons.y * cosHorizons.y);
477
- float nyb = 1.0 / 2.0 * (2.0 - cosHorizons.x * cosHorizons.x - cosHorizons.y * cosHorizons.y);
478
- float occlusion = nx * nxb + ny * nyb;
479
- ao += occlusion;
480
- }
481
-
482
- ao = clamp(ao / float(DIRECTIONS), 0.0, 1.0);
483
- ao = pow(ao, scale);
484
-
485
- gl_FragColor = vec4(vec3(ao), 1.0);
486
- }`,
487
- blending: NoBlending,
488
- depthTest: false,
489
- depthWrite: false,
490
- transparent: true,
491
- });
492
- // ---------------------------------------------------------------------------
493
- // Poisson 边缘保持去噪(双边去噪)
494
- // 取代旧版的 separable blur(仅用深度权重的高斯模糊)。这里同时依赖
495
- // 1) 法线(normalTexture,视图空间 RGB 法线,unpackRGBToNormal 解码)
496
- // 2) 深度(baseDepthTexture,对数深度,linearizeLogDepth 还原 viewZ)
497
- // 做双边权重:只有法线相近 + 沿法线方向深度相近的邻域样本才会被加权平均,
498
- // 因此能在去除 GTAO 噪点的同时,严格保住几何边缘,避免边缘被糊成一团。
499
- // 参考:three/examples/jsm/shaders/PoissonDenoiseShader.js(移植到 RawShaderMaterial /
500
- // GLSL ES 1.0,并把深度重建适配本项目的对数深度管线)。
501
- // ---------------------------------------------------------------------------
502
- // Poisson 采样点不再用预编译 GLSL 数组字面量(vec3[N](...) 是 GLSL ES 3.00 语法,
503
- // 本项目 RawShaderMaterial 默认 GLSL ES 1.00 不支持,会导致 shader 编译失败、
504
- // 去噪目标停留在清屏黑 → 整屏被 AO 乘法混合成全黑)。改为在 shader 内按
505
- // 官方 generateDenoiseSamples(samples, rings, radiusExponent) 的公式逐点程序化计算,
506
- // 与原来的数组完全等价,且兼容 GLSL ES 1.00。
507
- const poissonDenoiseShader = new RawShaderMaterial({
508
- defines: {
509
- SAMPLES: 32,
510
- POISSON_RINGS: 2,
511
- POISSON_RADIUS_EXP: 1,
512
- },
513
- uniforms: {
514
- tDiffuse: { value: null },
515
- tNormal: { value: null },
516
- tDepth: { value: null },
517
- tNoise: { value: noiseTexture },
518
- resolution: { value: new Vector2(1 / 512, 1 / 512) },
519
- noiseResolution: { value: new Vector2(NOISE_SIZE, NOISE_SIZE) },
520
- inverseProjectionMatrix: { value: new Matrix4() },
521
- projectionMatrix: { value: new Matrix4() },
522
- cameraNear: { value: 0.1 },
523
- cameraFar: { value: 20000 },
524
- lumaPhi: { value: 5.0 },
525
- depthPhi: { value: 5.0 },
526
- normalPhi: { value: 5.0 },
527
- radius: { value: 4.0 },
528
- },
529
- vertexShader: `
530
- attribute vec3 position;
531
- attribute vec2 uv;
532
- varying vec2 vUv;
533
- void main() {
534
- vUv = uv;
535
- gl_Position = vec4(position, 1.0);
536
- }`,
537
- fragmentShader: `
538
- #ifdef GL_ES
539
- precision highp float;
540
- #endif
541
-
542
- uniform sampler2D tDiffuse;
543
- uniform sampler2D tNormal;
544
- uniform sampler2D tDepth;
545
- uniform sampler2D tNoise;
546
- uniform vec2 resolution;
547
- uniform vec2 noiseResolution;
548
- uniform mat4 inverseProjectionMatrix;
549
- uniform mat4 projectionMatrix;
550
- uniform float cameraNear;
551
- uniform float cameraFar;
552
- uniform float lumaPhi;
553
- uniform float depthPhi;
554
- uniform float normalPhi;
555
- uniform float radius;
556
-
557
- varying vec2 vUv;
558
-
559
- const float PI = 3.141592653589793;
560
-
561
- // 复用与 GTAO 主 pass 完全一致的对数深度还原(logarithmicDepthBuffer 兼容)
562
- // gl_FragDepth = log2(1.0 - viewZ) / log2(cameraFar + 1.0) → 反解 viewZ = 1.0 - 2^(logDepth*log2(far+1))
563
- float viewZFromLogDepth(float logDepth) {
564
- return 1.0 - pow(2.0, logDepth * log2(cameraFar + 1.0));
565
- }
566
- vec3 unpackRGBToNormal(vec3 rgb) {
567
- return rgb * 2.0 - 1.0;
568
- }
569
- vec3 reconstructPosition(vec2 uv, float viewZ) {
570
- // 与主 GTAO pass 同款解析构造(避免 inverseProjectionMatrix 反投影 ndc.z=0 的方向符号陷阱)
571
- float fDivAspect = projectionMatrix[0][0];
572
- float f = projectionMatrix[1][1];
573
- vec2 ndc = uv * 2.0 - 1.0;
574
- float negZ = -viewZ;
575
- vec3 pos;
576
- pos.x = ndc.x * negZ / fDivAspect;
577
- pos.y = ndc.y * negZ / f;
578
- pos.z = viewZ;
579
- return pos;
580
- }
581
- float getLogDepth(vec2 uv) {
582
- return texture2D(tDepth, uv).r;
583
- }
584
- vec3 getViewPosition(vec2 uv) {
585
- return reconstructPosition(uv, viewZFromLogDepth(getLogDepth(uv)));
586
- }
587
- vec3 getViewNormal(vec2 uv) {
588
- return unpackRGBToNormal(texture2D(tNormal, uv).rgb);
76
+ });
77
+ return list;
78
+ };
79
+ const restoreVisibility = (list) => {
80
+ list.forEach(({ obj, prev }) => (obj.visible = prev));
81
+ };
82
+ /**
83
+ * 深度 pass:固定排除非实体表面后,将场景深度渲染到 depthRenderTarget。
84
+ * depthRenderTarget.depthTexture 即 baseDepthTexture,供 SSAO / 描边 / 发光等后续 pass 复用。
85
+ * 排除的非实体对象(sprite/线条/点)在深度图保留 clear 深度(1.0=远),与背景一致;排除的
86
+ * 半透明对象则保留其身后不透明几何的深度(身后无几何时同样为 1.0)。均不影响颜色通道渲染。
87
+ */
88
+ export function renderAoDepthPass(renderer, camera, scene, depthRenderTarget, includeFloor = false) {
89
+ const hidden = hideNonSurface(scene, includeFloor);
90
+ // ⚠️ 深度预通道铁律:必须让所有「未被排除」的可见表面都写入深度,深度图才有真实的几何遮挡信息。
91
+ // 透明材质(特效载体、半透明墙、alpha 贴图实体等)大多 transparent:true 且 depthWrite:false,
92
+ // 若不强转,它们不会写进 baseDepthTexture → 深度图只剩不透明几何 → 描边/辉光/毛玻璃/覆盖色
93
+ // 依赖 baseDepthTexture 的遮挡判定全部失效、一律显示在前(即本次回归根因)。
94
+ // 被排除的半透明面(aoTransparent / aoFloor / Mirror-aoOpaque 等)已在 hideNonSurface 中隐藏,
95
+ // 此处根本不会触及,故不影响 see-through 设计;其余透明面强制写深度 = 作为实体参与遮挡,符合预期。
96
+ const depthWriteRestore = [];
97
+ scene.traverse((obj) => {
98
+ const mesh = obj;
99
+ if (!mesh.isMesh || !mesh.material)
100
+ return;
101
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
102
+ list.forEach((m) => {
103
+ if (m && m.depthWrite === false) {
104
+ depthWriteRestore.push({ mat: m, val: false });
105
+ m.depthWrite = true;
106
+ }
107
+ });
108
+ });
109
+ const prevRenderTarget = renderer.getRenderTarget();
110
+ renderer.setRenderTarget(depthRenderTarget);
111
+ renderer.clear();
112
+ renderer.render(scene, camera);
113
+ // 还原 depthWrite,避免污染后续颜色通道渲染的透明混合
114
+ depthWriteRestore.forEach(({ mat, val }) => { mat.depthWrite = val; });
115
+ restoreVisibility(hidden);
116
+ renderer.setRenderTarget(prevRenderTarget);
117
+ }
118
+ /**
119
+ * 调试用:把「被排除出可见深度图」的表面单独渲成红色 mask。
120
+ *
121
+ * 这些面在 renderAoDepthPass 里被 hideNonSurface 隐藏,因此**不会写进 baseDepthTexture**,
122
+ * 也就永远挡不住描边/辉光/毛玻璃/覆盖色 —— 这正是"深度测试看起来失效"最常见的成因
123
+ * (用户以为是 bug,其实是该面被设计成可透视:sprite/线条/点、userData.aoTransparent、
124
+ * 以及标了 userData.aoFloor 的地板 Ground1)。
125
+ * 面板「深度调试 → 排除面标记」叠加显示:灰=能挡住的真实遮挡面,红=被排除、挡不住任何东西。
126
+ *
127
+ * 只渲染 Mesh:sprite / Line2 / Points 在 MeshBasicMaterial override 下渲染异常(见文件头注释),
128
+ * 且它们数量少、用户关心的遮挡体基本都是 Mesh,故跳过。
129
+ */
130
+ export function renderExcludedMaskPass(renderer, camera, scene, maskRenderTarget, includeFloor = false) {
131
+ const maskMaterial = new MeshBasicMaterial({
132
+ color: 0xff0000,
133
+ depthTest: false,
134
+ depthWrite: false,
135
+ side: DoubleSide,
136
+ });
137
+ const prevVisible = [];
138
+ scene.traverse((obj) => {
139
+ const isMesh = obj.isMesh === true;
140
+ prevVisible.push({ obj, prev: obj.visible });
141
+ // 只显示「被排除的 Mesh」:其余一律隐藏,得到纯黑底 + 红面的 mask
142
+ obj.visible = isMesh && isNonSurface(obj, includeFloor);
143
+ });
144
+ const prevOverride = scene.overrideMaterial;
145
+ const prevTarget = renderer.getRenderTarget();
146
+ const prevClear = renderer.getClearColor(new Color());
147
+ const prevAlpha = renderer.getClearAlpha();
148
+ scene.overrideMaterial = maskMaterial;
149
+ renderer.setRenderTarget(maskRenderTarget);
150
+ renderer.setClearColor(0x000000, 1);
151
+ renderer.clear();
152
+ renderer.render(scene, camera);
153
+ renderer.setClearColor(prevClear, prevAlpha);
154
+ renderer.setRenderTarget(prevTarget);
155
+ scene.overrideMaterial = prevOverride;
156
+ restoreVisibility(prevVisible);
157
+ maskMaterial.dispose();
158
+ }
159
+ /**
160
+ * 法线 pass(仅 SSAO 开启时需要):固定排除非实体表面后,以中性灰清屏 + normalMaterial
161
+ * override 将视图空间法线渲染到 normalRenderTarget,供 GTAO 风格 SSAO 使用。
162
+ * 排除对象在法线图保留中性灰清屏色 → unpackRGBToNormal(0.5,0.5,0.5)=(0,0,0) →
163
+ * SSAO 退化法线守卫判"无遮蔽"(=1),避免精灵弹窗被 AO 乘黑。清屏色用后即还原。
164
+ */
165
+ export function renderAoNormalPass(renderer, camera, scene, normalRenderTarget, normalMaterial, includeFloor = false) {
166
+ const hidden = hideNonSurface(scene, includeFloor);
167
+ // ⚠️ 与 renderAoDepthPass 同款「强制写深度」:本 pass 的深度会被当作含地板 AO 深度使用
168
+ // (normalRenderTarget.depthTexture)。透明面若因 depthWrite:false 不写深度,AO 深度就会漏掉它们,
169
+ // 与被本 pass 取代的那次独立深度 pass 内容不一致。pass 结束后立即还原。
170
+ const depthWriteRestore = [];
171
+ scene.traverse((obj) => {
172
+ const mesh = obj;
173
+ if (!mesh.isMesh || !mesh.material)
174
+ return;
175
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
176
+ list.forEach((m) => {
177
+ if (m && m.depthWrite === false) {
178
+ depthWriteRestore.push({ mat: m, val: false });
179
+ m.depthWrite = true;
180
+ }
181
+ });
182
+ });
183
+ const prevRenderTarget = renderer.getRenderTarget();
184
+ const prevClearColor = new Color();
185
+ renderer.getClearColor(prevClearColor);
186
+ const prevClearAlpha = renderer.getClearAlpha();
187
+ renderer.setClearColor(new Color(0.5, 0.5, 0.5), 1);
188
+ renderer.setRenderTarget(normalRenderTarget);
189
+ renderer.clear();
190
+ scene.overrideMaterial = normalMaterial;
191
+ renderer.render(scene, camera);
192
+ scene.overrideMaterial = null;
193
+ restoreVisibility(hidden);
194
+ depthWriteRestore.forEach(({ mat, val }) => { mat.depthWrite = val; });
195
+ renderer.setClearColor(prevClearColor, prevClearAlpha);
196
+ renderer.setRenderTarget(prevRenderTarget);
197
+ }
198
+ // 噪声贴图尺寸(奇数,magic square 要求奇数)。越大 → 旋转/半径抖动越细,
199
+ // GTAO 各向异性(转视角时 AO 范围"呼吸")越弱。17×17=289 种独立样本远多于默认的 5×5=25。
200
+ const NOISE_SIZE = 17;
201
+ // ---------------------------------------------------------------------------
202
+ // GTAO 风格 SSAO(地平线积分)
203
+ // 相对旧版手写 SSAO 的两点关键改进:
204
+ // 1. 法线直接来自视图空间法线纹理(MeshNormalMaterial 渲染),不再用深度差分重建,
205
+ // 从根本上消除噪声与边缘抖动。
206
+ // 2. 采样采用 Ground Truth Ambient Occlusion 的地平线积分(horizon-based),
207
+ // 几乎无 halo(光晕)与漏光。
208
+ // 深度仍使用项目的对数深度(logarithmicDepthBuffer: true),通过 linearizeLogDepth
209
+ // 还原 viewZ,与旧版保持一致;法线纹理来自 ThreeBase 的 normalRenderTarget。
210
+ // 参考:three/examples/jsm/shaders/GTAOShader.js 的 horizon-based 积分核心。
211
+ // 去噪:GTAO 计算的 AO 仍带高频噪声,这里用 Poisson 边缘保持去噪(双边去噪,
212
+ // 依赖 normalTexture + baseDepthTexture)取代旧版 separable 高斯模糊,能在去噪的同时
213
+ // 严格守住几何边缘。参考 three/examples/jsm/shaders/PoissonDenoiseShader.js。
214
+ // ---------------------------------------------------------------------------
215
+ const ssaoTextureTarget = new WebGLRenderTarget(512, 512, {
216
+ minFilter: LinearFilter,
217
+ magFilter: LinearFilter,
218
+ format: RGBAFormat,
219
+ });
220
+ const blurTextureTarget = new WebGLRenderTarget(512, 512, {
221
+ minFilter: LinearFilter,
222
+ magFilter: LinearFilter,
223
+ format: RGBAFormat,
224
+ });
225
+ const size = new Vector2();
226
+ const projectionMatrix = new Matrix4();
227
+ const inverseProjectionMatrix = new Matrix4();
228
+ // 低差异方向旋转噪声(magic square noise),用于旋转每个像素的采样核,避免带状瑕疵
229
+ function generateMagicSquare(size) {
230
+ const noiseSize = Math.floor(size) % 2 === 0 ? Math.floor(size) + 1 : Math.floor(size);
231
+ const noiseSquareSize = noiseSize * noiseSize;
232
+ const magicSquare = new Array(noiseSquareSize).fill(0);
233
+ let i = Math.floor(noiseSize / 2);
234
+ let j = noiseSize - 1;
235
+ for (let num = 1; num <= noiseSquareSize;) {
236
+ if (i === -1 && j === noiseSize) {
237
+ j = noiseSize - 2;
238
+ i = 0;
239
+ }
240
+ else {
241
+ if (j === noiseSize) {
242
+ j = 0;
243
+ }
244
+ if (i < 0) {
245
+ i = noiseSize - 1;
246
+ }
247
+ }
248
+ if (magicSquare[i * noiseSize + j] !== 0) {
249
+ j -= 2;
250
+ i++;
251
+ continue;
252
+ }
253
+ else {
254
+ magicSquare[i * noiseSize + j] = num++;
255
+ }
256
+ j++;
257
+ i--;
589
258
  }
590
-
591
- // 双边去噪的单次样本加权:luma(AO 灰度,恒近似相等)+ 法线相似度 + 沿法线深度差
592
- // 三者相乘得到权重;AO 是灰度图,luma 项近似为 1,主要靠法线 + 深度做边缘保持。
593
- void denoiseSample(in vec3 center, in vec3 viewNormal, in vec3 viewPos, in vec2 sampleUv, inout vec3 denoised, inout float totalWeight) {
594
- vec3 neighborColor = texture2D(tDiffuse, sampleUv).rgb;
595
- vec3 sampleNormal = getViewNormal(sampleUv);
596
- vec3 viewPosSample = getViewPosition(sampleUv);
597
-
598
- float normalDiff = dot(viewNormal, sampleNormal);
599
- float normalSimilarity = pow(max(normalDiff, 0.0), normalPhi);
600
- float lumaDiff = abs(length(neighborColor) - length(center));
601
- float lumaSimilarity = max(1.0 - lumaDiff / lumaPhi, 0.0);
602
- float depthDiff = abs(dot(viewPos - viewPosSample, viewNormal));
603
- float depthSimilarity = max(1.0 - depthDiff / depthPhi, 0.0);
604
- float w = lumaSimilarity * depthSimilarity * normalSimilarity;
605
-
606
- denoised += w * neighborColor;
607
- totalWeight += w;
259
+ return magicSquare;
260
+ }
261
+ function generateMagicSquareNoise(size = NOISE_SIZE) {
262
+ const noiseSize = Math.floor(size) % 2 === 0 ? Math.floor(size) + 1 : Math.floor(size);
263
+ const magicSquare = generateMagicSquare(noiseSize);
264
+ const noiseSquareSize = magicSquare.length;
265
+ const data = new Uint8Array(noiseSquareSize * 4);
266
+ // 用确定性伪随机给每个像素一个 [0,1] 的 w 通道(用于采样半径抖动 sampleDir.w),
267
+ // 同时 xy 仍由 magic square 角度决定(保证方向分布均匀无聚集)。
268
+ // ⚠️ 旧实现把 w 通道写死 255(=1.0),导致 sampleDir.w 恒等于 1.0,
269
+ // 半径抖动完全失效、旋转方向只有 25 种 → GTAO 各向异性明显(转视角 AO 范围"呼吸")。
270
+ for (let inx = 0; inx < noiseSquareSize; ++inx) {
271
+ const iAng = magicSquare[inx];
272
+ const angle = (2 * Math.PI * iAng) / noiseSquareSize;
273
+ const randomVec = new Vector3(Math.cos(angle), Math.sin(angle), 0).normalize();
274
+ // 每像素独立伪随机种子(基于行列索引),避免 w 通道规律化
275
+ const row = Math.floor(inx / noiseSize);
276
+ const col = inx % noiseSize;
277
+ const rnd = ((row * 73856093) ^ (col * 19349663) ^ (inx * 83492791)) >>> 0;
278
+ const wRand = (rnd % 1000) / 1000; // [0,1)
279
+ data[inx * 4] = (randomVec.x * 0.5 + 0.5) * 255;
280
+ data[inx * 4 + 1] = (randomVec.y * 0.5 + 0.5) * 255;
281
+ data[inx * 4 + 2] = 127;
282
+ data[inx * 4 + 3] = Math.floor(wRand * 255); // 半径抖动真正生效
608
283
  }
609
-
610
- void main() {
611
- float depth = getLogDepth(vUv);
612
- vec4 texel = texture2D(tDiffuse, vUv);
613
- // 背景(无几何):直接透传原 AO(白),不做去噪
614
- if (depth >= 1.0) {
615
- gl_FragColor = texel;
616
- return;
617
- }
618
- vec3 viewNormal = getViewNormal(vUv);
619
- // 退化/非法法线(含 NaN)按原 AO 透传,避免双边去噪把整片刷黑
620
- float nLen2 = dot(viewNormal, viewNormal);
621
- if (nLen2 == 0.0 || nLen2 != nLen2) {
622
- gl_FragColor = texel;
623
- return;
624
- }
625
- vec3 center = texel.rgb;
626
- vec3 viewPos = getViewPosition(vUv);
627
-
628
- vec2 noiseUv = vUv * resolution / noiseResolution;
629
- vec4 noiseTexel = texture2D(tNoise, noiseUv);
630
- vec2 noiseVec = vec2(sin(noiseTexel.x * 2.0 * PI), cos(noiseTexel.x * 2.0 * PI));
631
- mat2 rotationMatrix = mat2(noiseVec.x, -noiseVec.y, noiseVec.x, noiseVec.y);
632
-
633
- float totalWeight = 1.0;
634
- vec3 denoised = texel.rgb;
635
- for (int i = 0; i < SAMPLES; i++) {
636
- // 与官方 generateDenoiseSamples(samples, POISSON_RINGS, POISSON_RADIUS_EXP) 完全等价的逐点计算
637
- float fi = float(i);
638
- float angle = 2.0 * PI * float(POISSON_RINGS) * fi / float(SAMPLES);
639
- float radiusF = pow(fi / float(SAMPLES - 1), float(POISSON_RADIUS_EXP));
640
- vec3 sampleDir = vec3(cos(angle), sin(angle), radiusF);
641
- vec2 offset = rotationMatrix * (sampleDir.xy * (1.0 + sampleDir.z * (radius - 1.0)) / resolution);
642
- vec2 sampleUv = vUv + offset;
643
- denoiseSample(center, viewNormal, viewPos, sampleUv, denoised, totalWeight);
644
- }
645
-
646
- if (totalWeight > 0.0) {
647
- denoised /= totalWeight;
648
- }
649
- gl_FragColor = vec4(denoised, 1.0);
650
- }`,
651
- blending: NoBlending,
652
- depthTest: false,
653
- depthWrite: false,
654
- transparent: true,
655
- });
656
- const outputShader = new RawShaderMaterial({
657
- uniforms: {
658
- ssaoTexture: { value: null },
659
- ssaoStrength: { value: 1.0 },
660
- debug: { value: 0.0 },
661
- // 可见深度(排除地板,= baseDepthTexture):代表"真实可见表面"的深度。
662
- tColorDepth: { value: null },
663
- // AO 深度(含地板,= normalRenderTarget.depthTexture,法线 pass 顺带写入):GTAO 实际采样的深度。
664
- tAoDepth: { value: null },
665
- },
666
- vertexShader: `
667
- attribute vec3 position;
668
- attribute vec2 uv;
669
- varying vec2 vUv;
670
- void main() {
671
- vUv = uv;
672
- gl_Position = vec4(position, 1.0);
673
- }`,
674
- fragmentShader: `
675
- #ifdef GL_ES
676
- precision highp float;
677
- #endif
678
-
679
- uniform sampler2D ssaoTexture;
680
- uniform float ssaoStrength;
681
- uniform float debug;
682
- uniform sampler2D tColorDepth;
683
- uniform sampler2D tAoDepth;
684
-
685
- varying vec2 vUv;
686
-
687
- void main() {
688
- float ssao = texture2D(ssaoTexture, vUv).r;
689
-
690
- // 复合守卫:对数深度越小越近。若"含地板的 AO 深度"比"可见深度"更近,
691
- // 说明该像素的真实可见表面位于半透明地板之后(典型的从下方透视地板看身后实体),
692
- // 地板算出的接触 AO 不该乘到它身上 → 视为无遮蔽(=1),即"被遮挡的部分就不要 ssao"。
693
- // 仅在最终合成时生效;debug 模式直接看 GTAO 原始 AO 图(含地板接触 AO),便于排查。
694
- float dColor = texture2D(tColorDepth, vUv).r;
695
- float dAo = texture2D(tAoDepth, vUv).r;
696
- if (debug < 0.5 && dAo < dColor - 0.0001) {
697
- ssao = 1.0;
698
- }
699
-
700
- float finalOcclusion = mix(1.0, ssao, ssaoStrength);
701
-
702
- if (debug > 0.5) {
703
- gl_FragColor = vec4(vec3(ssao), 1.0);
704
- } else {
705
- gl_FragColor = vec4(vec3(finalOcclusion), 1.0);
706
- }
707
- }`,
708
- blending: MultiplyBlending,
709
- depthTest: false,
710
- depthWrite: false,
711
- transparent: true,
712
- });
713
- export default function ssao_pass(renderer, camera, scene, depthRenderTarget, normalRenderTarget, normalMaterial, devicePixelRatio, options) {
714
- // ⚠️ 早退只看 enable:strength/intensity 是"叠加强度",0 表示"不叠加 AO"而不是"关掉这个 pass"。
715
- // 旧代码写成 `!options.enable || !options.strength`,强度调到 0 会跳过整条 pass(连法线准备也停),
716
- // debug 模式下就再也看不到 AO 图,语义也和"强度 = 0"混在一起。
717
- if (!options.enable) {
284
+ const noiseTexture = new DataTexture(data, noiseSize, noiseSize);
285
+ noiseTexture.wrapS = RepeatWrapping;
286
+ noiseTexture.wrapT = RepeatWrapping;
287
+ noiseTexture.needsUpdate = true;
288
+ return noiseTexture;
289
+ }
290
+ const noiseTexture = generateMagicSquareNoise();
291
+ const ssaoShader = new RawShaderMaterial({
292
+ defines: {
293
+ SAMPLES: 32,
294
+ },
295
+ uniforms: {
296
+ depthTexture: { value: null },
297
+ normalTexture: { value: null },
298
+ tNoise: { value: noiseTexture },
299
+ resolution: { value: new Vector2(1 / 512, 1 / 512) },
300
+ noiseResolution: { value: new Vector2(NOISE_SIZE, NOISE_SIZE) },
301
+ projectionMatrix: { value: new Matrix4() },
302
+ inverseProjectionMatrix: { value: new Matrix4() },
303
+ ssaoRadius: { value: 5.0 },
304
+ ssaoFalloff: { value: 5.0 },
305
+ ssaoBias: { value: 0.01 },
306
+ scale: { value: 1.0 },
307
+ cameraNear: { value: 0.1 },
308
+ cameraFar: { value: 20000 },
309
+ distanceExponent: { value: 1.0 },
310
+ distanceFallOff: { value: 1.0 },
311
+ },
312
+ vertexShader: `
313
+ attribute vec3 position;
314
+ attribute vec2 uv;
315
+ varying vec2 vUv;
316
+ void main() {
317
+ vUv = uv;
318
+ gl_Position = vec4(position, 1.0);
319
+ }`,
320
+ fragmentShader: `
321
+ #ifdef GL_ES
322
+ precision highp float;
323
+ #endif
324
+
325
+ uniform sampler2D depthTexture;
326
+ uniform sampler2D normalTexture;
327
+ uniform sampler2D tNoise;
328
+ uniform vec2 resolution;
329
+ uniform vec2 noiseResolution;
330
+ uniform mat4 projectionMatrix;
331
+ uniform mat4 inverseProjectionMatrix;
332
+ uniform float ssaoRadius;
333
+ uniform float ssaoFalloff;
334
+ uniform float ssaoBias;
335
+ uniform float scale;
336
+ uniform float cameraNear;
337
+ uniform float cameraFar;
338
+ uniform float distanceExponent;
339
+ uniform float distanceFallOff;
340
+
341
+ varying vec2 vUv;
342
+
343
+ const float PI = 3.141592653589793;
344
+
345
+ // ⚠️ 关键:本项目渲染器 logarithmicDepthBuffer:true,baseDepthTexture 存的是「对数编码」的 gl_FragDepth。
346
+ // three.js r167 写深度(logdepthbuf_fragment,透视下 gl_Position.w = -viewZ):
347
+ // gl_FragDepth = log2(1.0 - viewZ) / log2(cameraFar + 1.0)
348
+ // 反解得真实视图空间 Z(负值 = 相机前方):
349
+ // viewZ = 1.0 - 2^(logDepth * log2(cameraFar + 1.0))
350
+ // 注:three.js 的 perspectiveDepthToViewZ() 期望的是「常规透视非线性深度」(gl_FragCoord.z),
351
+ // 与这里的对数编码是完全不同的两套,不能直接套用——对数深度必须显式解指数。
352
+ // (Python 数值复算验证:该公式对任意距离还原误差 = 0,perspectiveDepthToViewZ 误差高达数米级。)
353
+ float viewZFromLogDepth(float logDepth) {
354
+ return 1.0 - pow(2.0, logDepth * log2(cameraFar + 1.0));
355
+ }
356
+
357
+ // MeshNormalMaterial 把视图空间法线 pack 到 [0,1],这里解回 [-1,1]
358
+ vec3 unpackRGBToNormal(vec3 rgb) {
359
+ return rgb * 2.0 - 1.0;
360
+ }
361
+
362
+ // 由 uv + 真实 viewZ 重建视图空间位置。
363
+ // 透视投影下:ndc.xy = clip.xy/clip.w,clip.xy = (f/aspect * X, f * Y),clip.w = -viewZ
364
+ // 反解得 X = ndc.x * (-viewZ) / (f/aspect),Y = ndc.y * (-viewZ) / f,Z = viewZ。
365
+ // 注意:旧写法用 inverseProjectionMatrix 反投影 ndc.z=0 取「方向」再乘 viewZ/(-dir.z),
366
+ // 会把方向翻到相机背后(z/y 反号),导致平地重建到天顶方向 → GTAO 把开阔地面误判遮蔽压黑。
367
+ // 这里直接由 viewZ 解析构造,无符号陷阱,且对数深度已在外层 viewZFromLogDepth 还原成真实 viewZ,
368
+ // 与本函数无关(不再依赖 projectionMatrix 的逆,故 inverseProjectionMatrix uniform 可保留也可移除)。
369
+ vec3 reconstructPosition(vec2 uv, float viewZ) {
370
+ // 透视投影:projectionMatrix[0][0] = f/aspect,projectionMatrix[1][1] = f,clip.w = -viewZ
371
+ float fDivAspect = projectionMatrix[0][0];
372
+ float f = projectionMatrix[1][1];
373
+ vec2 ndc = uv * 2.0 - 1.0;
374
+ float negZ = -viewZ;
375
+ vec3 pos;
376
+ pos.x = ndc.x * negZ / fDivAspect;
377
+ pos.y = ndc.y * negZ / f;
378
+ pos.z = viewZ;
379
+ return pos;
380
+ }
381
+
382
+ float getLogDepth(vec2 uv) {
383
+ return texture2D(depthTexture, uv).r;
384
+ }
385
+
386
+ // 由 uv 直接得到真实视图空间位置(对数深度反解 viewZ 后再重建)
387
+ vec3 getViewPosition(vec2 uv) {
388
+ return reconstructPosition(uv, viewZFromLogDepth(getLogDepth(uv)));
389
+ }
390
+
391
+ vec3 getViewNormal(vec2 uv) {
392
+ return unpackRGBToNormal(texture2D(normalTexture, uv).rgb);
393
+ }
394
+
395
+ // 采样点的 uv:直接读该 uv 处的「真实对数深度」,不再走常规投影(投影会把对数深度当线性算错)。
396
+ // 这里只返回 uv,深度由 getViewPosition(sampleUv) 内部用对数反解。
397
+ vec2 getSceneUv(vec3 sampleViewPos) {
398
+ // sampleViewPos 已是真实视图空间坐标,用常规投影求屏幕 uv(投影矩阵本身对位置是对的,
399
+ // 错只错在「深度纹理的编码」上,所以位置的投影反算没问题,只需读取采样点的真实对数深度)。
400
+ vec4 sampleClipPos = projectionMatrix * vec4(sampleViewPos, 1.0);
401
+ return sampleClipPos.xy / sampleClipPos.w * 0.5 + 0.5;
402
+ }
403
+
404
+ void main() {
405
+ float logDepth = getLogDepth(vUv);
406
+ if (logDepth >= 1.0) {
407
+ gl_FragColor = vec4(1.0);
408
+ return;
409
+ }
410
+ vec3 viewPos = getViewPosition(vUv);
411
+ vec3 viewNormal = getViewNormal(vUv);
412
+
413
+ // 退化/非法法线(长度为 0 或含 NaN,常见于缺法线的模型)无法做地平线积分,
414
+ // 按"无遮蔽"处理,避免整片被乘黑。NaN 检测用 x != x(GLSL ES 1.0 无 isnan)。
415
+ float normalLen2 = dot(viewNormal, viewNormal);
416
+ if (normalLen2 < 1e-4 || normalLen2 != normalLen2) {
417
+ gl_FragColor = vec4(1.0);
718
418
  return;
419
+ }
420
+
421
+ float radiusToUse = ssaoRadius;
422
+ float thicknessToUse = ssaoFalloff;
423
+
424
+ vec2 noiseUv = vUv * resolution / noiseResolution;
425
+ vec4 noiseTexel = texture2D(tNoise, noiseUv);
426
+ vec3 randomVec = noiseTexel.xyz * 2.0 - 1.0;
427
+ vec3 tangent = normalize(vec3(randomVec.xy, 0.0));
428
+ vec3 bitangent = vec3(-tangent.y, tangent.x, 0.0);
429
+ mat3 kernelMatrix = mat3(tangent, bitangent, vec3(0.0, 0.0, 1.0));
430
+
431
+ // SAMPLES 为编译期常量(defines),由此推导 DIRECTIONS / STEPS
432
+ const int DIRECTIONS = SAMPLES < 30 ? 3 : 5;
433
+ const int STEPS = (SAMPLES + DIRECTIONS - 1) / DIRECTIONS;
434
+ float ao = 0.0;
435
+
436
+ for (int i = 0; i < DIRECTIONS; ++i) {
437
+ float angle = float(i) / float(DIRECTIONS) * PI;
438
+ vec4 sampleDir = vec4(cos(angle), sin(angle), 0.0, 0.5 + 0.5 * noiseTexel.w);
439
+ sampleDir.xyz = normalize(kernelMatrix * sampleDir.xyz);
440
+
441
+ vec3 viewDir = normalize(-viewPos.xyz);
442
+ vec3 sliceBitangent = normalize(cross(sampleDir.xyz, viewDir));
443
+ vec3 sliceTangent = cross(sliceBitangent, viewDir);
444
+ vec3 normalInSlice = normalize(viewNormal - sliceBitangent * dot(viewNormal, sliceBitangent));
445
+
446
+ vec3 tangentToNormalInSlice = cross(normalInSlice, sliceBitangent);
447
+ vec2 cosHorizons = vec2(dot(viewDir, tangentToNormalInSlice), dot(viewDir, -tangentToNormalInSlice));
448
+
449
+ for (int j = 0; j < STEPS; ++j) {
450
+ vec3 sampleViewOffset = sampleDir.xyz * radiusToUse * sampleDir.w * pow(float(j + 1) / float(STEPS), distanceExponent);
451
+
452
+ vec2 sampleUv0 = getSceneUv(viewPos + sampleViewOffset);
453
+ vec3 sampleSceneViewPos = getViewPosition(sampleUv0);
454
+ vec3 viewDelta = sampleSceneViewPos - viewPos;
455
+ // 切面阈值(bias):只接受"沿法线方向有明显高度突起"的采样点;
456
+ // 大平面上同平面远采样点 sampleHeight≈0,低于 bias 即跳过,避免平地自身被误判遮蔽压黑。
457
+ float sampleHeight0 = dot(viewDelta, viewNormal);
458
+ if (abs(viewDelta.z) < thicknessToUse && sampleHeight0 > ssaoBias) {
459
+ float sampleCosHorizon = dot(viewDir, normalize(viewDelta));
460
+ cosHorizons.x += max(0.0, (sampleCosHorizon - cosHorizons.x) * mix(1.0, 2.0 / float(j + 2), distanceFallOff));
461
+ }
462
+
463
+ vec2 sampleUv1 = getSceneUv(viewPos - sampleViewOffset);
464
+ sampleSceneViewPos = getViewPosition(sampleUv1);
465
+ viewDelta = sampleSceneViewPos - viewPos;
466
+ float sampleHeight1 = dot(viewDelta, viewNormal);
467
+ if (abs(viewDelta.z) < thicknessToUse && sampleHeight1 > ssaoBias) {
468
+ float sampleCosHorizon = dot(viewDir, normalize(viewDelta));
469
+ cosHorizons.y += max(0.0, (sampleCosHorizon - cosHorizons.y) * mix(1.0, 2.0 / float(j + 2), distanceFallOff));
470
+ }
471
+ }
472
+
473
+ vec2 sinHorizons = sqrt(1.0 - cosHorizons * cosHorizons);
474
+ float nx = dot(normalInSlice, sliceTangent);
475
+ float ny = dot(normalInSlice, viewDir);
476
+ float nxb = 1.0 / 2.0 * (acos(cosHorizons.y) - acos(cosHorizons.x) + sinHorizons.x * cosHorizons.x - sinHorizons.y * cosHorizons.y);
477
+ float nyb = 1.0 / 2.0 * (2.0 - cosHorizons.x * cosHorizons.x - cosHorizons.y * cosHorizons.y);
478
+ float occlusion = nx * nxb + ny * nyb;
479
+ ao += occlusion;
480
+ }
481
+
482
+ ao = clamp(ao / float(DIRECTIONS), 0.0, 1.0);
483
+ ao = pow(ao, scale);
484
+
485
+ gl_FragColor = vec4(vec3(ao), 1.0);
486
+ }`,
487
+ blending: NoBlending,
488
+ depthTest: false,
489
+ depthWrite: false,
490
+ transparent: true,
491
+ });
492
+ // ---------------------------------------------------------------------------
493
+ // Poisson 边缘保持去噪(双边去噪)
494
+ // 取代旧版的 separable blur(仅用深度权重的高斯模糊)。这里同时依赖
495
+ // 1) 法线(normalTexture,视图空间 RGB 法线,unpackRGBToNormal 解码)
496
+ // 2) 深度(baseDepthTexture,对数深度,linearizeLogDepth 还原 viewZ)
497
+ // 做双边权重:只有法线相近 + 沿法线方向深度相近的邻域样本才会被加权平均,
498
+ // 因此能在去除 GTAO 噪点的同时,严格保住几何边缘,避免边缘被糊成一团。
499
+ // 参考:three/examples/jsm/shaders/PoissonDenoiseShader.js(移植到 RawShaderMaterial /
500
+ // GLSL ES 1.0,并把深度重建适配本项目的对数深度管线)。
501
+ // ---------------------------------------------------------------------------
502
+ // Poisson 采样点不再用预编译 GLSL 数组字面量(vec3[N](...) 是 GLSL ES 3.00 语法,
503
+ // 本项目 RawShaderMaterial 默认 GLSL ES 1.00 不支持,会导致 shader 编译失败、
504
+ // 去噪目标停留在清屏黑 → 整屏被 AO 乘法混合成全黑)。改为在 shader 内按
505
+ // 官方 generateDenoiseSamples(samples, rings, radiusExponent) 的公式逐点程序化计算,
506
+ // 与原来的数组完全等价,且兼容 GLSL ES 1.00。
507
+ const poissonDenoiseShader = new RawShaderMaterial({
508
+ defines: {
509
+ SAMPLES: 32,
510
+ POISSON_RINGS: 2,
511
+ POISSON_RADIUS_EXP: 1,
512
+ },
513
+ uniforms: {
514
+ tDiffuse: { value: null },
515
+ tNormal: { value: null },
516
+ tDepth: { value: null },
517
+ tNoise: { value: noiseTexture },
518
+ resolution: { value: new Vector2(1 / 512, 1 / 512) },
519
+ noiseResolution: { value: new Vector2(NOISE_SIZE, NOISE_SIZE) },
520
+ inverseProjectionMatrix: { value: new Matrix4() },
521
+ projectionMatrix: { value: new Matrix4() },
522
+ cameraNear: { value: 0.1 },
523
+ cameraFar: { value: 20000 },
524
+ lumaPhi: { value: 5.0 },
525
+ depthPhi: { value: 5.0 },
526
+ normalPhi: { value: 5.0 },
527
+ radius: { value: 4.0 },
528
+ },
529
+ vertexShader: `
530
+ attribute vec3 position;
531
+ attribute vec2 uv;
532
+ varying vec2 vUv;
533
+ void main() {
534
+ vUv = uv;
535
+ gl_Position = vec4(position, 1.0);
536
+ }`,
537
+ fragmentShader: `
538
+ #ifdef GL_ES
539
+ precision highp float;
540
+ #endif
541
+
542
+ uniform sampler2D tDiffuse;
543
+ uniform sampler2D tNormal;
544
+ uniform sampler2D tDepth;
545
+ uniform sampler2D tNoise;
546
+ uniform vec2 resolution;
547
+ uniform vec2 noiseResolution;
548
+ uniform mat4 inverseProjectionMatrix;
549
+ uniform mat4 projectionMatrix;
550
+ uniform float cameraNear;
551
+ uniform float cameraFar;
552
+ uniform float lumaPhi;
553
+ uniform float depthPhi;
554
+ uniform float normalPhi;
555
+ uniform float radius;
556
+
557
+ varying vec2 vUv;
558
+
559
+ const float PI = 3.141592653589793;
560
+
561
+ // 复用与 GTAO 主 pass 完全一致的对数深度还原(logarithmicDepthBuffer 兼容)
562
+ // gl_FragDepth = log2(1.0 - viewZ) / log2(cameraFar + 1.0) → 反解 viewZ = 1.0 - 2^(logDepth*log2(far+1))
563
+ float viewZFromLogDepth(float logDepth) {
564
+ return 1.0 - pow(2.0, logDepth * log2(cameraFar + 1.0));
565
+ }
566
+ vec3 unpackRGBToNormal(vec3 rgb) {
567
+ return rgb * 2.0 - 1.0;
568
+ }
569
+ vec3 reconstructPosition(vec2 uv, float viewZ) {
570
+ // 与主 GTAO pass 同款解析构造(避免 inverseProjectionMatrix 反投影 ndc.z=0 的方向符号陷阱)
571
+ float fDivAspect = projectionMatrix[0][0];
572
+ float f = projectionMatrix[1][1];
573
+ vec2 ndc = uv * 2.0 - 1.0;
574
+ float negZ = -viewZ;
575
+ vec3 pos;
576
+ pos.x = ndc.x * negZ / fDivAspect;
577
+ pos.y = ndc.y * negZ / f;
578
+ pos.z = viewZ;
579
+ return pos;
719
580
  }
720
- // 记录当前渲染目标,最终 SSAO 乘法叠加到它上面。⚠️ 调用方决定输出目标——
721
- // ThreeBase(2026-09-14 起)在深度 pass 之后、对象级特效之前调用,此时绑定的是 baseRenderTarget,
722
- // 故 AO 直接乘进 base,描边/发光/覆盖/磨砂都叠在 AO 之上不被压暗;末尾统一 FXAA 输出到屏幕,
723
- // 覆盖 SSAO 自身的硬边。改动调用顺序时,本 pass 无需修改(输出到"绑定的那个目标"即可)。
724
- const originRenderTarget = renderer.getRenderTarget();
725
- // 法线 pass:地板保留(includeFloor=true),接触 AO 需要地板的视图空间法线。
726
- // ⭐ 深度白拿(2026-09-14 优化):normalRenderTarget 挂着 aoDepthTexture 作 depth attachment,
727
- // 这一次渲染的深度由光栅器顺带写入 → 直接就是「含地板」的 AO 深度,**不再单独渲一遍深度 pass**。
728
- // (原先法线 pass 之后还要再渲一遍「含地板深度」——它与法线 pass 的排除集完全相同,
729
- // 属于重复遍历全场景,2026-09-14 已删。)
730
- // 前提:renderAoNormalPass 内部已把 depthWrite=false 的透明材质强转 true,保证写入面集合一致。
731
- renderAoNormalPass(renderer, camera, scene, normalRenderTarget, normalMaterial, true);
732
- // 可见深度(地板排除)= baseDepthTexture,既是描边/辉光/毛玻璃的遮挡参考,也是"真实可见表面"参考。
733
- const colorDepthTexture = depthRenderTarget.depthTexture;
734
- // AO 深度(含地板):GTAO 与去噪采样它,能算出物体落在地板上的接触 AO。
735
- const aoDepthTexture = normalRenderTarget.depthTexture;
736
- const normalTexture = normalRenderTarget.texture;
737
- renderer.getSize(size);
738
- const resScale = options.resolutionScale ?? 0.5;
739
- const ssaoW = Math.round(size.x * devicePixelRatio * resScale);
740
- const ssaoH = Math.round(size.y * devicePixelRatio * resScale);
741
- ssaoTextureTarget.setSize(ssaoW, ssaoH);
742
- blurTextureTarget.setSize(ssaoW, ssaoH);
743
- projectionMatrix.copy(camera.projectionMatrix);
744
- inverseProjectionMatrix.copy(projectionMatrix).invert();
745
- // SAMPLES 由 sampleCount 决定(编译期常量,改变需重新编译 shader)
746
- const samples = options.sampleCount || 32;
747
- if (ssaoShader.defines.SAMPLES !== samples) {
748
- ssaoShader.defines.SAMPLES = samples;
749
- ssaoShader.needsUpdate = true;
581
+ float getLogDepth(vec2 uv) {
582
+ return texture2D(tDepth, uv).r;
750
583
  }
751
- ssaoShader.uniforms.depthTexture.value = aoDepthTexture;
752
- ssaoShader.uniforms.normalTexture.value = normalTexture;
753
- // resolution 传像素尺寸:供 GTAO 噪声按屏幕平铺(与官方 GTAOPass 一致)
754
- ssaoShader.uniforms.resolution.value.set(ssaoW, ssaoH);
755
- ssaoShader.uniforms.projectionMatrix.value = projectionMatrix;
756
- ssaoShader.uniforms.inverseProjectionMatrix.value = inverseProjectionMatrix;
757
- ssaoShader.uniforms.ssaoRadius.value = options.radius ?? 5.0;
758
- ssaoShader.uniforms.ssaoFalloff.value = options.falloff ?? 5.0;
759
- ssaoShader.uniforms.ssaoBias.value = options.bias ?? 0.01;
760
- // scale = AO 次幂(gamma 校正),只由 options.scale 控制;intensity 是"叠加强度",走下面 outputShader 的 mix。
761
- // ⚠️ 用 ?? 而非 ||:0 是合法值,用 || 会被吞掉退回默认值。
762
- ssaoShader.uniforms.scale.value = options.scale ?? 1.0;
763
- const camFar = options.cameraFar ||
764
- ((camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera) ? camera.far : 20000);
765
- ssaoShader.uniforms.cameraFar.value = camFar;
766
- ssaoShader.uniforms.cameraNear.value = (camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera)
767
- ? camera.near
768
- : 0.1;
769
- ssaoShader.uniforms.distanceExponent.value = options.distanceExponent ?? 1.0;
770
- ssaoShader.uniforms.distanceFallOff.value = options.distanceFallOff ?? 1.0;
771
- renderer.setRenderTarget(ssaoTextureTarget);
772
- renderer.clear();
773
- fsQuad.render(renderer, ssaoShader);
774
- // Poisson 边缘保持去噪:单 pass 完成(替代旧版 separable blur 双 pass)。
775
- // resolution 传像素尺寸,供噪声平铺与采样偏移缩放(与 GTAO 主 pass 一致)。
776
- const denoiseRes = new Vector2(ssaoW, ssaoH);
777
- // 去噪最大采样半径,单位 = 像素(官方 Poisson 公式里 radius 就是像素倍率)。
778
- // 兜底 1 像素:radius=0 会让 32 个采样点全部落在中心,等于完全不去噪,保留 1px 更符合"最小模糊"。
779
- const denoiseRadius = Math.max(1.0, options.blurRadius ?? 4.0);
780
- // SAMPLES 由 sampleCount 决定(编译期常量,改变需重新编译 shader)
781
- if (poissonDenoiseShader.defines.SAMPLES !== samples) {
782
- poissonDenoiseShader.defines.SAMPLES = samples;
783
- poissonDenoiseShader.needsUpdate = true;
584
+ vec3 getViewPosition(vec2 uv) {
585
+ return reconstructPosition(uv, viewZFromLogDepth(getLogDepth(uv)));
784
586
  }
785
- poissonDenoiseShader.uniforms.tDiffuse.value = ssaoTextureTarget.texture;
786
- poissonDenoiseShader.uniforms.tNormal.value = normalTexture;
787
- poissonDenoiseShader.uniforms.tDepth.value = aoDepthTexture;
788
- poissonDenoiseShader.uniforms.resolution.value.copy(denoiseRes);
789
- poissonDenoiseShader.uniforms.noiseResolution.value.set(NOISE_SIZE, NOISE_SIZE);
790
- poissonDenoiseShader.uniforms.inverseProjectionMatrix.value = inverseProjectionMatrix;
791
- poissonDenoiseShader.uniforms.cameraFar.value = ssaoShader.uniforms.cameraFar.value;
792
- poissonDenoiseShader.uniforms.cameraNear.value = ssaoShader.uniforms.cameraNear.value;
793
- poissonDenoiseShader.uniforms.radius.value = denoiseRadius;
794
- renderer.setRenderTarget(blurTextureTarget);
795
- renderer.clear();
796
- fsQuad.render(renderer, poissonDenoiseShader);
797
- renderer.setRenderTarget(originRenderTarget);
798
- outputShader.uniforms.ssaoTexture.value = blurTextureTarget.texture;
799
- // 复合守卫:可见深度(地板排除)与 AO 深度(含地板)两图比对,
800
- // 地板挡在真实可见表面之前时作废地板的接触 AO(见 outputShader 注释)。
801
- outputShader.uniforms.tColorDepth.value = colorDepthTexture;
802
- outputShader.uniforms.tAoDepth.value = aoDepthTexture;
803
- // 叠加强度:finalOcclusion = mix(1.0, ao, strength),0 = 完全不叠加 AO,1 = 原始强度,>1 外插加深。
804
- // intensity 与 strength 同义,便于调用方只设 intensity;同样用 ?? 保证 0 生效。
805
- outputShader.uniforms.ssaoStrength.value = options.strength ?? options.intensity ?? 1.0;
806
- outputShader.uniforms.debug.value = options.debug ? 1.0 : 0.0;
807
- fsQuad.render(renderer, outputShader);
808
- }
587
+ vec3 getViewNormal(vec2 uv) {
588
+ return unpackRGBToNormal(texture2D(tNormal, uv).rgb);
589
+ }
590
+
591
+ // 双边去噪的单次样本加权:luma(AO 灰度,恒近似相等)+ 法线相似度 + 沿法线深度差
592
+ // 三者相乘得到权重;AO 是灰度图,luma 项近似为 1,主要靠法线 + 深度做边缘保持。
593
+ void denoiseSample(in vec3 center, in vec3 viewNormal, in vec3 viewPos, in vec2 sampleUv, inout vec3 denoised, inout float totalWeight) {
594
+ vec3 neighborColor = texture2D(tDiffuse, sampleUv).rgb;
595
+ vec3 sampleNormal = getViewNormal(sampleUv);
596
+ vec3 viewPosSample = getViewPosition(sampleUv);
597
+
598
+ float normalDiff = dot(viewNormal, sampleNormal);
599
+ float normalSimilarity = pow(max(normalDiff, 0.0), normalPhi);
600
+ float lumaDiff = abs(length(neighborColor) - length(center));
601
+ float lumaSimilarity = max(1.0 - lumaDiff / lumaPhi, 0.0);
602
+ float depthDiff = abs(dot(viewPos - viewPosSample, viewNormal));
603
+ float depthSimilarity = max(1.0 - depthDiff / depthPhi, 0.0);
604
+ float w = lumaSimilarity * depthSimilarity * normalSimilarity;
605
+
606
+ denoised += w * neighborColor;
607
+ totalWeight += w;
608
+ }
609
+
610
+ void main() {
611
+ float depth = getLogDepth(vUv);
612
+ vec4 texel = texture2D(tDiffuse, vUv);
613
+ // 背景(无几何):直接透传原 AO(白),不做去噪
614
+ if (depth >= 1.0) {
615
+ gl_FragColor = texel;
616
+ return;
617
+ }
618
+ vec3 viewNormal = getViewNormal(vUv);
619
+ // 退化/非法法线(含 NaN)按原 AO 透传,避免双边去噪把整片刷黑
620
+ float nLen2 = dot(viewNormal, viewNormal);
621
+ if (nLen2 == 0.0 || nLen2 != nLen2) {
622
+ gl_FragColor = texel;
623
+ return;
624
+ }
625
+ vec3 center = texel.rgb;
626
+ vec3 viewPos = getViewPosition(vUv);
627
+
628
+ vec2 noiseUv = vUv * resolution / noiseResolution;
629
+ vec4 noiseTexel = texture2D(tNoise, noiseUv);
630
+ vec2 noiseVec = vec2(sin(noiseTexel.x * 2.0 * PI), cos(noiseTexel.x * 2.0 * PI));
631
+ mat2 rotationMatrix = mat2(noiseVec.x, -noiseVec.y, noiseVec.x, noiseVec.y);
632
+
633
+ float totalWeight = 1.0;
634
+ vec3 denoised = texel.rgb;
635
+ for (int i = 0; i < SAMPLES; i++) {
636
+ // 与官方 generateDenoiseSamples(samples, POISSON_RINGS, POISSON_RADIUS_EXP) 完全等价的逐点计算
637
+ float fi = float(i);
638
+ float angle = 2.0 * PI * float(POISSON_RINGS) * fi / float(SAMPLES);
639
+ float radiusF = pow(fi / float(SAMPLES - 1), float(POISSON_RADIUS_EXP));
640
+ vec3 sampleDir = vec3(cos(angle), sin(angle), radiusF);
641
+ vec2 offset = rotationMatrix * (sampleDir.xy * (1.0 + sampleDir.z * (radius - 1.0)) / resolution);
642
+ vec2 sampleUv = vUv + offset;
643
+ denoiseSample(center, viewNormal, viewPos, sampleUv, denoised, totalWeight);
644
+ }
645
+
646
+ if (totalWeight > 0.0) {
647
+ denoised /= totalWeight;
648
+ }
649
+ gl_FragColor = vec4(denoised, 1.0);
650
+ }`,
651
+ blending: NoBlending,
652
+ depthTest: false,
653
+ depthWrite: false,
654
+ transparent: true,
655
+ });
656
+ const outputShader = new RawShaderMaterial({
657
+ uniforms: {
658
+ ssaoTexture: { value: null },
659
+ ssaoStrength: { value: 1.0 },
660
+ debug: { value: 0.0 },
661
+ // 可见深度(排除地板,= baseDepthTexture):代表"真实可见表面"的深度。
662
+ tColorDepth: { value: null },
663
+ // AO 深度(含地板,= normalRenderTarget.depthTexture,法线 pass 顺带写入):GTAO 实际采样的深度。
664
+ tAoDepth: { value: null },
665
+ },
666
+ vertexShader: `
667
+ attribute vec3 position;
668
+ attribute vec2 uv;
669
+ varying vec2 vUv;
670
+ void main() {
671
+ vUv = uv;
672
+ gl_Position = vec4(position, 1.0);
673
+ }`,
674
+ fragmentShader: `
675
+ #ifdef GL_ES
676
+ precision highp float;
677
+ #endif
678
+
679
+ uniform sampler2D ssaoTexture;
680
+ uniform float ssaoStrength;
681
+ uniform float debug;
682
+ uniform sampler2D tColorDepth;
683
+ uniform sampler2D tAoDepth;
684
+
685
+ varying vec2 vUv;
686
+
687
+ void main() {
688
+ float ssao = texture2D(ssaoTexture, vUv).r;
689
+
690
+ // 复合守卫:对数深度越小越近。若"含地板的 AO 深度"比"可见深度"更近,
691
+ // 说明该像素的真实可见表面位于半透明地板之后(典型的从下方透视地板看身后实体),
692
+ // 地板算出的接触 AO 不该乘到它身上 → 视为无遮蔽(=1),即"被遮挡的部分就不要 ssao"。
693
+ // 仅在最终合成时生效;debug 模式直接看 GTAO 原始 AO 图(含地板接触 AO),便于排查。
694
+ float dColor = texture2D(tColorDepth, vUv).r;
695
+ float dAo = texture2D(tAoDepth, vUv).r;
696
+ if (debug < 0.5 && dAo < dColor - 0.0001) {
697
+ ssao = 1.0;
698
+ }
699
+
700
+ float finalOcclusion = mix(1.0, ssao, ssaoStrength);
701
+
702
+ if (debug > 0.5) {
703
+ gl_FragColor = vec4(vec3(ssao), 1.0);
704
+ } else {
705
+ gl_FragColor = vec4(vec3(finalOcclusion), 1.0);
706
+ }
707
+ }`,
708
+ blending: MultiplyBlending,
709
+ depthTest: false,
710
+ depthWrite: false,
711
+ transparent: true,
712
+ });
713
+ export default function ssao_pass(renderer, camera, scene, depthRenderTarget, normalRenderTarget, normalMaterial, devicePixelRatio, options) {
714
+ // ⚠️ 早退只看 enable:strength/intensity 是"叠加强度",0 表示"不叠加 AO"而不是"关掉这个 pass"。
715
+ // 旧代码写成 `!options.enable || !options.strength`,强度调到 0 会跳过整条 pass(连法线准备也停),
716
+ // debug 模式下就再也看不到 AO 图,语义也和"强度 = 0"混在一起。
717
+ if (!options.enable) {
718
+ return;
719
+ }
720
+ // 记录当前渲染目标,最终 SSAO 乘法叠加到它上面。⚠️ 调用方决定输出目标——
721
+ // ThreeBase(2026-09-14 起)在深度 pass 之后、对象级特效之前调用,此时绑定的是 baseRenderTarget,
722
+ // 故 AO 直接乘进 base,描边/发光/覆盖/磨砂都叠在 AO 之上不被压暗;末尾统一 FXAA 输出到屏幕,
723
+ // 覆盖 SSAO 自身的硬边。改动调用顺序时,本 pass 无需修改(输出到"绑定的那个目标"即可)。
724
+ const originRenderTarget = renderer.getRenderTarget();
725
+ // 法线 pass:地板保留(includeFloor=true),接触 AO 需要地板的视图空间法线。
726
+ // ⭐ 深度白拿(2026-09-14 优化):normalRenderTarget 挂着 aoDepthTexture 作 depth attachment,
727
+ // 这一次渲染的深度由光栅器顺带写入 → 直接就是「含地板」的 AO 深度,**不再单独渲一遍深度 pass**。
728
+ // (原先法线 pass 之后还要再渲一遍「含地板深度」——它与法线 pass 的排除集完全相同,
729
+ // 属于重复遍历全场景,2026-09-14 已删。)
730
+ // 前提:renderAoNormalPass 内部已把 depthWrite=false 的透明材质强转 true,保证写入面集合一致。
731
+ renderAoNormalPass(renderer, camera, scene, normalRenderTarget, normalMaterial, true);
732
+ // 可见深度(地板排除)= baseDepthTexture,既是描边/辉光/毛玻璃的遮挡参考,也是"真实可见表面"参考。
733
+ const colorDepthTexture = depthRenderTarget.depthTexture;
734
+ // AO 深度(含地板):GTAO 与去噪采样它,能算出物体落在地板上的接触 AO。
735
+ const aoDepthTexture = normalRenderTarget.depthTexture;
736
+ const normalTexture = normalRenderTarget.texture;
737
+ renderer.getSize(size);
738
+ const resScale = options.resolutionScale ?? 0.5;
739
+ const ssaoW = Math.round(size.x * devicePixelRatio * resScale);
740
+ const ssaoH = Math.round(size.y * devicePixelRatio * resScale);
741
+ ssaoTextureTarget.setSize(ssaoW, ssaoH);
742
+ blurTextureTarget.setSize(ssaoW, ssaoH);
743
+ projectionMatrix.copy(camera.projectionMatrix);
744
+ inverseProjectionMatrix.copy(projectionMatrix).invert();
745
+ // SAMPLES 由 sampleCount 决定(编译期常量,改变需重新编译 shader)
746
+ const samples = options.sampleCount || 32;
747
+ if (ssaoShader.defines.SAMPLES !== samples) {
748
+ ssaoShader.defines.SAMPLES = samples;
749
+ ssaoShader.needsUpdate = true;
750
+ }
751
+ ssaoShader.uniforms.depthTexture.value = aoDepthTexture;
752
+ ssaoShader.uniforms.normalTexture.value = normalTexture;
753
+ // resolution 传像素尺寸:供 GTAO 噪声按屏幕平铺(与官方 GTAOPass 一致)
754
+ ssaoShader.uniforms.resolution.value.set(ssaoW, ssaoH);
755
+ ssaoShader.uniforms.projectionMatrix.value = projectionMatrix;
756
+ ssaoShader.uniforms.inverseProjectionMatrix.value = inverseProjectionMatrix;
757
+ ssaoShader.uniforms.ssaoRadius.value = options.radius ?? 5.0;
758
+ ssaoShader.uniforms.ssaoFalloff.value = options.falloff ?? 5.0;
759
+ ssaoShader.uniforms.ssaoBias.value = options.bias ?? 0.01;
760
+ // scale = AO 次幂(gamma 校正),只由 options.scale 控制;intensity 是"叠加强度",走下面 outputShader 的 mix。
761
+ // ⚠️ 用 ?? 而非 ||:0 是合法值,用 || 会被吞掉退回默认值。
762
+ ssaoShader.uniforms.scale.value = options.scale ?? 1.0;
763
+ const camFar = options.cameraFar ||
764
+ ((camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera) ? camera.far : 20000);
765
+ ssaoShader.uniforms.cameraFar.value = camFar;
766
+ ssaoShader.uniforms.cameraNear.value = (camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera)
767
+ ? camera.near
768
+ : 0.1;
769
+ ssaoShader.uniforms.distanceExponent.value = options.distanceExponent ?? 1.0;
770
+ ssaoShader.uniforms.distanceFallOff.value = options.distanceFallOff ?? 1.0;
771
+ renderer.setRenderTarget(ssaoTextureTarget);
772
+ renderer.clear();
773
+ fsQuad.render(renderer, ssaoShader);
774
+ // Poisson 边缘保持去噪:单 pass 完成(替代旧版 separable blur 双 pass)。
775
+ // resolution 传像素尺寸,供噪声平铺与采样偏移缩放(与 GTAO 主 pass 一致)。
776
+ const denoiseRes = new Vector2(ssaoW, ssaoH);
777
+ // 去噪最大采样半径,单位 = 像素(官方 Poisson 公式里 radius 就是像素倍率)。
778
+ // 兜底 1 像素:radius=0 会让 32 个采样点全部落在中心,等于完全不去噪,保留 1px 更符合"最小模糊"。
779
+ const denoiseRadius = Math.max(1.0, options.blurRadius ?? 4.0);
780
+ // SAMPLES 由 sampleCount 决定(编译期常量,改变需重新编译 shader)
781
+ if (poissonDenoiseShader.defines.SAMPLES !== samples) {
782
+ poissonDenoiseShader.defines.SAMPLES = samples;
783
+ poissonDenoiseShader.needsUpdate = true;
784
+ }
785
+ poissonDenoiseShader.uniforms.tDiffuse.value = ssaoTextureTarget.texture;
786
+ poissonDenoiseShader.uniforms.tNormal.value = normalTexture;
787
+ poissonDenoiseShader.uniforms.tDepth.value = aoDepthTexture;
788
+ poissonDenoiseShader.uniforms.resolution.value.copy(denoiseRes);
789
+ poissonDenoiseShader.uniforms.noiseResolution.value.set(NOISE_SIZE, NOISE_SIZE);
790
+ poissonDenoiseShader.uniforms.inverseProjectionMatrix.value = inverseProjectionMatrix;
791
+ poissonDenoiseShader.uniforms.cameraFar.value = ssaoShader.uniforms.cameraFar.value;
792
+ poissonDenoiseShader.uniforms.cameraNear.value = ssaoShader.uniforms.cameraNear.value;
793
+ poissonDenoiseShader.uniforms.radius.value = denoiseRadius;
794
+ renderer.setRenderTarget(blurTextureTarget);
795
+ renderer.clear();
796
+ fsQuad.render(renderer, poissonDenoiseShader);
797
+ renderer.setRenderTarget(originRenderTarget);
798
+ outputShader.uniforms.ssaoTexture.value = blurTextureTarget.texture;
799
+ // 复合守卫:可见深度(地板排除)与 AO 深度(含地板)两图比对,
800
+ // 地板挡在真实可见表面之前时作废地板的接触 AO(见 outputShader 注释)。
801
+ outputShader.uniforms.tColorDepth.value = colorDepthTexture;
802
+ outputShader.uniforms.tAoDepth.value = aoDepthTexture;
803
+ // 叠加强度:finalOcclusion = mix(1.0, ao, strength),0 = 完全不叠加 AO,1 = 原始强度,>1 外插加深。
804
+ // intensity 与 strength 同义,便于调用方只设 intensity;同样用 ?? 保证 0 生效。
805
+ outputShader.uniforms.ssaoStrength.value = options.strength ?? options.intensity ?? 1.0;
806
+ outputShader.uniforms.debug.value = options.debug ? 1.0 : 0.0;
807
+ fsQuad.render(renderer, outputShader);
808
+ }