three-base 2.0.142 → 2.0.145

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 (227) 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 +511 -511
  10. package/FPScontrol.js +1799 -1799
  11. package/FPScontrol.ts +1855 -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 +247 -247
  33. package/Mirror.d.ts +25 -25
  34. package/Mirror.js +143 -143
  35. package/MktMesh/MktMesh.d.ts +301 -291
  36. package/MktMesh/MktMesh.js +1064 -1045
  37. package/MktMesh/MktMesh.ts +32 -11
  38. package/MktMesh/assetUrl.d.ts +59 -35
  39. package/MktMesh/assetUrl.js +154 -69
  40. package/MktMesh/assetUrl.ts +155 -69
  41. package/MktMesh/mktMappingShader.d.ts +12 -12
  42. package/MktMesh/mktMappingShader.js +138 -138
  43. package/MktMesh/mktMappingShader.ts +139 -139
  44. package/MktMesh/mktShader.d.ts +263 -263
  45. package/MktMesh/mktShader.js +1236 -1236
  46. package/MktMesh/mktShader.ts +1344 -1344
  47. package/MktMesh/panoBitmapTexture.d.ts +25 -25
  48. package/MktMesh/panoBitmapTexture.js +52 -52
  49. package/MktMesh/panoBitmapTexture.ts +54 -54
  50. package/MktMesh/panoSlotBind.d.ts +29 -29
  51. package/MktMesh/panoSlotBind.js +106 -106
  52. package/MktMesh/panoSlotBind.ts +153 -153
  53. package/MktMesh/panoSlotRoster.d.ts +22 -22
  54. package/MktMesh/panoSlotRoster.js +169 -169
  55. package/MktMesh/panoSlotRoster.ts +168 -168
  56. package/MktMesh/panoTileStitcher.d.ts +119 -119
  57. package/MktMesh/panoTileStitcher.js +529 -529
  58. package/MktMesh/panoTileStitcher.ts +616 -616
  59. package/MktMesh/types.d.ts +75 -75
  60. package/MktMesh/types.js +1 -1
  61. package/MktMesh/wallCapTessellate.d.ts +33 -33
  62. package/MktMesh/wallCapTessellate.js +97 -97
  63. package/MktMesh/wallCapTessellate.ts +68 -68
  64. package/PipeMesh/PathGeometry.d.ts +44 -44
  65. package/PipeMesh/PathGeometry.js +307 -307
  66. package/PipeMesh/PathPoint.d.ts +17 -17
  67. package/PipeMesh/PathPoint.js +31 -31
  68. package/PipeMesh/PathPointList.d.ts +31 -31
  69. package/PipeMesh/PathPointList.js +209 -209
  70. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  71. package/PipeMesh/PathTubeGeometry.js +130 -130
  72. package/PipeMesh/PipeMesh.d.ts +25 -25
  73. package/PipeMesh/PipeMesh.js +63 -63
  74. package/PipeMesh/index.d.ts +1 -1
  75. package/PipeMesh/index.js +1 -1
  76. package/PlaneModal1/PlaneModal1.d.ts +34 -34
  77. package/PlaneModal1/PlaneModal1.js +109 -109
  78. package/PlaneModal1/index.d.ts +1 -1
  79. package/PlaneModal1/index.js +1 -1
  80. package/PlaneText.d.ts +27 -27
  81. package/PlaneText.js +35 -35
  82. package/PlaneWithHtml.d.ts +13 -13
  83. package/PlaneWithHtml.js +64 -64
  84. package/PointLight/PointLight.d.ts +13 -13
  85. package/PointLight/PointLight.js +64 -64
  86. package/PointLight/index.d.ts +1 -1
  87. package/PointLight/index.js +1 -1
  88. package/Sky/Sky.d.ts +9 -9
  89. package/Sky/Sky.js +22 -22
  90. package/Sky/index.d.ts +1 -1
  91. package/Sky/index.js +1 -1
  92. package/SpotLight.d.ts +6 -6
  93. package/SpotLight.js +72 -72
  94. package/SpriteModal1/SpriteModal1.d.ts +33 -33
  95. package/SpriteModal1/SpriteModal1.js +116 -116
  96. package/SpriteModal1/index.d.ts +1 -1
  97. package/SpriteModal1/index.js +1 -1
  98. package/SpriteText.d.ts +46 -46
  99. package/SpriteText.js +176 -176
  100. package/SpriteText2.d.ts +48 -48
  101. package/SpriteText2.js +223 -223
  102. package/SpriteText3.d.ts +86 -86
  103. package/SpriteText3.js +517 -517
  104. package/SpriteText3.ts +599 -599
  105. package/SpriteWithHtml.d.ts +9 -9
  106. package/SpriteWithHtml.js +44 -44
  107. package/Text3D/Text3D.d.ts +20 -20
  108. package/Text3D/Text3D.js +58 -58
  109. package/Text3D/fontController.d.ts +60 -60
  110. package/Text3D/fontController.js +166 -166
  111. package/Text3D/index.d.ts +1 -1
  112. package/Text3D/index.js +1 -1
  113. package/ThreeBase.d.ts +248 -248
  114. package/ThreeBase.js +1202 -1202
  115. package/ThreeBase.ts +1314 -1314
  116. package/ThreeMouse.d.ts +53 -54
  117. package/ThreeMouse.js +453 -453
  118. package/Transition.d.ts +45 -45
  119. package/Transition.js +209 -209
  120. package/Water/Water.d.ts +5 -5
  121. package/Water/Water.js +44 -44
  122. package/Water/index.d.ts +2 -2
  123. package/Water/index.js +2 -2
  124. package/WaterFlow/FloatingDuck.d.ts +88 -88
  125. package/WaterFlow/FloatingDuck.js +307 -307
  126. package/WaterFlow/FloatingDuck.ts +445 -445
  127. package/WaterFlow/GPUFluidSimulation.d.ts +56 -56
  128. package/WaterFlow/GPUFluidSimulation.js +781 -781
  129. package/WaterFlow/GPUFluidSimulation.ts +827 -827
  130. package/WaterFlow/ReservoirSystem.d.ts +106 -106
  131. package/WaterFlow/ReservoirSystem.js +611 -611
  132. package/WaterFlow/ReservoirSystem.ts +754 -754
  133. package/WaterFlow/SPHField.d.ts +35 -35
  134. package/WaterFlow/SPHField.js +203 -203
  135. package/WaterFlow/SPHField.ts +228 -228
  136. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -69
  137. package/WaterFlow/ScreenSpaceFluidPass.js +728 -728
  138. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -789
  139. package/WaterFlow/ShallowWaterSurface.d.ts +45 -45
  140. package/WaterFlow/ShallowWaterSurface.js +438 -438
  141. package/WaterFlow/ShallowWaterSurface.ts +510 -510
  142. package/WaterFlow/WaterFlow.d.ts +87 -87
  143. package/WaterFlow/WaterFlow.js +171 -171
  144. package/WaterFlow/WaterFlow.ts +219 -219
  145. package/WaterFlow/types.d.ts +161 -161
  146. package/WaterFlow/types.js +1 -1
  147. package/WaterFlow/types.ts +189 -189
  148. package/WaterFlow/waterFlowPass.d.ts +3 -3
  149. package/WaterFlow/waterFlowPass.js +20 -20
  150. package/WaterFlow/waterFlowPass.ts +40 -40
  151. package/collisionOBB.d.ts +42 -42
  152. package/collisionOBB.js +112 -112
  153. package/collisionOBB.ts +108 -108
  154. package/declare.d.ts +25 -25
  155. package/index.d.ts +1 -1
  156. package/index.js +1 -1
  157. package/instanced/InstancedBatch.d.ts +57 -57
  158. package/instanced/InstancedBatch.js +348 -348
  159. package/instanced/InstancedBatch.ts +406 -406
  160. package/instanced/InstancedChild.d.ts +174 -174
  161. package/instanced/InstancedChild.js +264 -264
  162. package/instanced/InstancedChild.ts +327 -327
  163. package/instanced/InstancedGroup.d.ts +43 -43
  164. package/instanced/InstancedGroup.js +126 -126
  165. package/instanced/InstancedGroup.ts +137 -137
  166. package/instanced/InstancedMeshBase.d.ts +38 -38
  167. package/instanced/InstancedMeshBase.js +282 -282
  168. package/instanced/InstancedTemplate.d.ts +46 -46
  169. package/instanced/InstancedTemplate.js +125 -125
  170. package/keyControls.d.ts +29 -30
  171. package/keyControls.js +101 -101
  172. package/lensflare/LensFlare.d.ts +39 -39
  173. package/lensflare/LensFlare.js +284 -284
  174. package/package.json +6 -1
  175. package/pass/bloom_pass.d.ts +2 -2
  176. package/pass/bloom_pass.js +174 -174
  177. package/pass/cover_pass.d.ts +2 -2
  178. package/pass/cover_pass.js +108 -108
  179. package/pass/debugDepthState.d.ts +20 -20
  180. package/pass/debugDepthState.js +9 -9
  181. package/pass/debugDepthState.ts +22 -22
  182. package/pass/depthTestBias.d.ts +12 -12
  183. package/pass/depthTestBias.js +12 -12
  184. package/pass/depthTestBias.ts +12 -12
  185. package/pass/effectUserData.d.ts +27 -27
  186. package/pass/effectUserData.js +1 -1
  187. package/pass/effectUserData.ts +46 -46
  188. package/pass/frosted_glass_pass.d.ts +2 -2
  189. package/pass/frosted_glass_pass.js +150 -150
  190. package/pass/fsQuad.d.ts +9 -9
  191. package/pass/fsQuad.js +28 -28
  192. package/pass/glow_pass.d.ts +2 -2
  193. package/pass/glow_pass.js +350 -350
  194. package/pass/screen_bloom_pass.d.ts +11 -11
  195. package/pass/screen_bloom_pass.js +136 -136
  196. package/pass/ssao_pass.d.ts +88 -88
  197. package/pass/ssao_pass.js +799 -799
  198. package/pass/ssao_pass.ts +918 -918
  199. package/pass/stroke_pass.d.ts +2 -2
  200. package/pass/stroke_pass.js +267 -267
  201. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  202. package/shaders/FXAAShaderMaterial.js +20 -20
  203. package/shaders/baseDepthVisShader.d.ts +11 -11
  204. package/shaders/baseDepthVisShader.js +36 -36
  205. package/shaders/baseDepthVisShader.ts +36 -36
  206. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  207. package/shaders/baseTextureToRenderShader.js +19 -19
  208. package/shaders/copyShader.d.ts +3 -3
  209. package/shaders/copyShader.js +13 -13
  210. package/shaders/debugDepthCompareShader.d.ts +14 -14
  211. package/shaders/debugDepthCompareShader.js +49 -49
  212. package/shaders/debugDepthCompareShader.ts +50 -50
  213. package/shaders/debugDepthJudgeShader.d.ts +18 -18
  214. package/shaders/debugDepthJudgeShader.js +52 -52
  215. package/shaders/debugDepthJudgeShader.ts +54 -54
  216. package/shaders/debugExcludedShader.d.ts +13 -13
  217. package/shaders/debugExcludedShader.js +46 -46
  218. package/shaders/debugExcludedShader.ts +48 -48
  219. package/spriteTextBg.d.ts +1 -1
  220. package/spriteTextBg.js +20 -20
  221. package/spriteTextBg.ts +20 -20
  222. package/threeLoader.d.ts +35 -35
  223. package/threeLoader.js +518 -518
  224. package/threeUtils.d.ts +53 -53
  225. package/threeUtils.js +329 -329
  226. package/types.d.ts +2 -2
  227. package/types.js +6 -6
package/ThreeBase.js CHANGED
@@ -1,1202 +1,1202 @@
1
- /*
2
- * @Author: xiaosihan
3
- * @Date: 2021-12-20 13:38:22
4
- * @Last Modified by: xiaosihan
5
- * @Last Modified time: 2025-01-28 04:26:41
6
- */
7
- import { round } from "lodash";
8
- import { AxesHelper, Box3, DepthFormat, DepthTexture, DoubleSide, EventDispatcher, GridHelper, HalfFloatType, LinearFilter, Line, Mesh, MeshNormalMaterial, MOUSE, NearestFilter, NoBlending, NoColorSpace, NormalBlending, OrthographicCamera, PerspectiveCamera, Points, RGBAFormat, RGBFormat, FloatType, UnsignedByteType, NoToneMapping, Scene, SRGBColorSpace, TOUCH, UnsignedIntType, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
9
- import { MathUtils } from "three";
10
- const { degToRad, generateUUID } = MathUtils;
11
- import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
12
- import ThreeMouse from "./ThreeMouse";
13
- import InstancedGroup from "./instanced/InstancedGroup";
14
- import Transition from "./Transition";
15
- import bloom_pass from "./pass/bloom_pass";
16
- import cover_pass from "./pass/cover_pass";
17
- import frosted_glass_pass from "./pass/frosted_glass_pass";
18
- import { flushLensflareOverlays } from "./lensflare/LensFlare";
19
- import glow_pass from "./pass/glow_pass";
20
- import stroke_pass from "./pass/stroke_pass";
21
- import ssao_pass, { renderAoDepthPass, renderExcludedMaskPass, visibleDepthOptions } from "./pass/ssao_pass";
22
- import screenBloomPass from "./pass/screen_bloom_pass";
23
- import copyShader from "./shaders/copyShader";
24
- import { baseTextureToRenderShader } from "./shaders/baseTextureToRenderShader";
25
- import baseDepthVisShader from "./shaders/baseDepthVisShader";
26
- import debugDepthCompareShader from "./shaders/debugDepthCompareShader";
27
- import debugDepthJudgeShader from "./shaders/debugDepthJudgeShader";
28
- import debugExcludedShader from "./shaders/debugExcludedShader";
29
- import { debugDepthState } from "./pass/debugDepthState";
30
- import { WaterFlow } from "./WaterFlow/WaterFlow";
31
- import waterFlowPass from "./WaterFlow/waterFlowPass";
32
- import threeLoader from "./threeLoader";
33
- import threeUtils from "./threeUtils";
34
- import fsQuad from "./pass/fsQuad";
35
- /** OrbitControls 阻尼基准值(等价于 60fps 下的单帧衰减比例)。
36
- * render 循环会按真实帧间隔把它换算成「帧率无关」的单帧衰减比例(见下方 controls.update 处)。
37
- * 取值历史:0.05(1826ab1 验证过"加阻尼也不晃"的正常值,配合 rotateSpeed=0.2)→
38
- * a9f3ec9 把 rotateSpeed 提到 1.0 才导致"快速晃动中心点飘",与阻尼无关;该次顺手把阻尼关成 false 诊断。
39
- * 本轮复原:enableDamping=true + dampingFactor=0.05(回到不飘配置),rotateSpeed 折中 0.5(0.2 跟手、
40
- * 1.0 易飘)。若 0.5 仍飘,降到 0.2 完全复原 1826ab1;若想更跟手,升到 0.3~0.4 之间试。 */
41
- const CONTROLS_DAMPING_BASE = 0.05;
42
- // three.js 的基础对象
43
- export default class ThreeBase extends EventDispatcher {
44
- constructor() {
45
- super();
46
- // 资源加载进度有更新时, 需要渲染
47
- threeLoader.addEventListener("loadding", () => {
48
- this.dispatchEvent({ type: "loadding" });
49
- setTimeout(() => {
50
- this.needRender = true;
51
- }, 100);
52
- });
53
- threeLoader.addEventListener("onLoad", () => {
54
- this.dispatchEvent({ type: "onLoad" });
55
- setTimeout(() => {
56
- this.needRender = true;
57
- }, 100);
58
- });
59
- // 固定的实例化分组器挂进场景,并注入到鼠标事件系统用于 InstancedMesh 命中路由
60
- this.scene.add(this.instancedGroup);
61
- this.threeMouse.instancedGroup = this.instancedGroup;
62
- this.init();
63
- }
64
- // 设置最小分辨率
65
- minDevicePixelRatio = 1;
66
- // 实际帧缓冲像素比硬上限:SSAA 超采样可能把总像素比推得过高导致卡顿,
67
- // 这里硬性限制 effectiveRatio 不超过该值(无论设备像素比或 renderScale 多大)。
68
- // 高 dpr 屏(如 2×)下 effectiveRatio=2 会让整条后处理链在 2× 分辨率跑,快速晃动掉帧顿挫、
69
- // 像素抖动(用户感知为"飘");收紧到 1(高 dpr 屏 base RT 最多 1×2=2× 每维)进一步压中间 RT 填充率。注意 renderer 自身
70
- // setPixelRatio 仍按 dpr 输出最终屏幕,这里只压中间 RT 计算量,不影响最终清晰度。
71
- maxPixelRatio = 1;
72
- // 超采样倍数(SSAA):在帧缓冲分辨率基础上再乘 renderScale 渲染,
73
- // 最后由 FXAA 降采样输出到屏幕。renderScale>1 可消除"纹理缩小产生的摩尔纹"——
74
- // MSAA 只处理几何边缘、管不到着色器内的纹理采样;SSAA 才是纹理摩尔纹的治本手段
75
- // (纹理被欠采样时的拍频,必须靠提高实际采样率才能消除)。
76
- // 1 = 关闭超采样(仅设备 pixelRatio);2 = 2 倍超采样(质量最高,片元开销 ×4,需强 GPU)。
77
- renderScale = 1;
78
- // 全局超采样/分辨率倍率(SSAA):base / 深度 / 法线 / 效果 mask / 屏幕合成 全部按
79
- // effectiveRatio × resolution 内部超采样渲染,终处理 FXAA 降采样输出到屏幕。
80
- // 全链路同倍率 → 各 RT 严格 1:1 对齐:几何锯齿、效果边缘锯齿、辉光漏光(深度 vs mask 错位)、
81
- // mask 合成 UV 缩放错位、SSAO 深度错位变黑 一并消除。默认 2 = 每维 2×、总像素 4×;
82
- // 代价全链路填充 ×4(base + 深度 + 法线 + 后处理 SSAO/cover/frosted)。要省性能整体降 1
83
- // (关超采样,仅 FXAA 兜底,几何硬边锯齿回弹)。历史坑:baseRenderScale/auxRenderScale/
84
- // effectRenderScale 三参各自为政,倍率不同步必错位,故统一为单参数 resolution 管全局。
85
- // ⚠️ 必须用 getter/setter,不能是普通字段:所有 RT 尺寸只在 updateRenderTargetSizes()
86
- // 里按 effectiveRatio × resolution 计算,而该方法过去只被 setSize()(容器尺寸变化)调用
87
- // → 运行时改 resolution 不会重建任何 RT,画面毫无变化(表现为"调分辨率没感觉")。
88
- // 故 setter 里改动倍率后立刻重建全部 RT。
89
- _resolution = 2;
90
- get resolution() {
91
- return this._resolution;
92
- }
93
- set resolution(value) {
94
- // 夹到合理区间:0 或负值会让 RT 尺寸为 0,WebGL 报 framebuffer incomplete
95
- const next = Math.min(4, Math.max(0.25, Number.isFinite(value) ? value : 2));
96
- if (next === this._resolution) {
97
- return;
98
- }
99
- this._resolution = next;
100
- // 尚未布局(size 为 0)时跳过,等首次 setSize 时自然会带上新倍率
101
- if (this.size.x > 0 && this.size.y > 0) {
102
- this.updateRenderTargetSizes(this.size.x, this.size.y);
103
- }
104
- }
105
- /**
106
- * 终处理抖动强度(消除 8bit 输出量化的色带,见 baseTextureToRenderShader)。
107
- * 1.0 = 标准 ±0.5 LSB(默认,肉眼无感但能打散色带);0 = 完全关闭。
108
- * ⚠️ 若某处柔和渐变(外辉光/天空)看着"发脏/有颗粒",先把它设 0 做 A/B 对比:
109
- * 0 时若色带回来,说明抖动是必需的,应调小到 0.5 而不是关掉。
110
- */
111
- get ditherStrength() {
112
- return baseTextureToRenderShader.uniforms.uDitherStrength.value;
113
- }
114
- set ditherStrength(value) {
115
- const next = Math.max(0, Number.isFinite(value) ? value : 1);
116
- baseTextureToRenderShader.uniforms.uDitherStrength.value = next;
117
- }
118
- /**
119
- * 渲染目标(RT)底层配置(运行时可调,改即重建 base / screen 两个主颜色 RT)。
120
- * 皇上要的「RT 上面的配置」= type / format / colorSpace / filter;
121
- * 默认与原始硬编码一致(HalfFloatType + RGBAFormat + 线性 NoColorSpace + Linear 滤波)。
122
- * 调这些能直观复现/消除典型问题:
123
- * - type=UnsignedByteType(8bit 线性):柔和渐变被压成 3~6 色阶 → 同心环色带(banding)
124
- * - colorSpace=SRGBColorSpace:RT 已按 sRGB 编码 + 终处理再编一次 → 双重编码发白
125
- * - filter=NearestFilter:颜色插值变最近邻 → 边缘像素化/糊
126
- */
127
- _rtOptions = {
128
- type: HalfFloatType,
129
- colorSpace: NoColorSpace,
130
- format: RGBAFormat,
131
- filter: LinearFilter,
132
- };
133
- /** 面板/代码里的字符串名 → THREE 常量(单一映射点,避免散落硬编码数字) */
134
- static rtNameToConstant(key, name) {
135
- switch (key) {
136
- case "type":
137
- return { HalfFloat: HalfFloatType, Byte: UnsignedByteType, Float: FloatType }[name] ?? HalfFloatType;
138
- case "colorSpace":
139
- return { Linear: NoColorSpace, SRGB: SRGBColorSpace }[name] ?? NoColorSpace;
140
- case "format":
141
- return { RGBA: RGBAFormat, RGB: RGBFormat }[name] ?? RGBAFormat;
142
- case "filter":
143
- return { Linear: LinearFilter, Nearest: NearestFilter }[name] ?? LinearFilter;
144
- }
145
- }
146
- /**
147
- * 运行时改 RT 底层配置:更新 _rtOptions 后重建 base / screen 两个主颜色 RT(保留当前尺寸)。
148
- * 必须在已布局(size>0)后才重建;否则等下次 setSize 自然带新选项。
149
- * 仅重建主颜色 RT(base/screen);深度/法线/aoDepth 是辅助通道,其 type 由对应 depthTexture 决定,
150
- * 不在面板暴露,避免破坏深度比对 / SSAO 深度黑。
151
- */
152
- applyRenderTargetOption(key, name) {
153
- switch (key) {
154
- case "type":
155
- this._rtOptions.type = ThreeBase.rtNameToConstant("type", name);
156
- break;
157
- case "colorSpace":
158
- this._rtOptions.colorSpace = ThreeBase.rtNameToConstant("colorSpace", name);
159
- break;
160
- case "format":
161
- this._rtOptions.format = ThreeBase.rtNameToConstant("format", name);
162
- break;
163
- case "filter":
164
- this._rtOptions.filter = ThreeBase.rtNameToConstant("filter", name);
165
- break;
166
- }
167
- this.rebuildRenderTargets();
168
- }
169
- rebuildRenderTargets() {
170
- if (this.size.x <= 0 || this.size.y <= 0) {
171
- return;
172
- }
173
- const w = Math.max(1, Math.round(this.size.x * this.effectiveRatio * this._resolution));
174
- const h = Math.max(1, Math.round(this.size.y * this.effectiveRatio * this._resolution));
175
- const opts = {
176
- minFilter: this._rtOptions.filter,
177
- magFilter: this._rtOptions.filter,
178
- format: this._rtOptions.format,
179
- type: this._rtOptions.type,
180
- stencilBuffer: false,
181
- anisotropy: 16,
182
- };
183
- this.baseRenderTarget.dispose();
184
- this.baseRenderTarget = new WebGLRenderTarget(w, h, { ...opts });
185
- this.baseRenderTarget.texture.colorSpace = this._rtOptions.colorSpace;
186
- this.screenRenderTarget.dispose();
187
- this.screenRenderTarget = new WebGLRenderTarget(w, h, { ...opts });
188
- this.screenRenderTarget.texture.colorSpace = this._rtOptions.colorSpace;
189
- this.needRender = true;
190
- }
191
- // 效果 mask 像素比硬上限:避免高 dpr 屏(如 2×)上 effectiveRatio×resolution 爆到 4×,
192
- // 让效果 mask RT(含辉光多次高斯迭代)填充量失控。经验值 2×:相对设备 dpr 输出足够平滑,
193
- // 且把"效果 mask 超采样"的填充量控制在场景同级的 2× 以内(原本 3× 在高 dpr 屏是"还是卡"的高嫌疑点)。
194
- effectMaxPixelRatio = 2;
195
- // 超采样倍率已统一为上方 resolution 字段(base / 深度 / 法线 / mask / 屏幕全部取它)。
196
- // 辅助通道(深度/法线)分辨率系数。
197
- // 关键:描边/发光/辉光/毛玻璃都拿 baseDepthTexture 做遮挡比对(deathTestShader 以全分辨率
198
- // UV 采样半分辨率深度,并与全分辨率物体深度做硬阈值比较)。半分辨率深度(Nearest 滤波)被
199
- // 放大采样后与锐利的物体深度在边缘逐像素翻转 discard,产生边缘摩尔纹/锯齿——这是这 4 个特效
200
- // 共同的摩尔纹根因。故深度必须与 mask 同分辨率(全分辨率),1:1 采样才无放大错位。
201
- // 法线仅 SSAO 使用(首页默认关闭、不渲染),全分辨率无额外开销。
202
- // 之前为"不卡"降到 0.5 反而引入摩尔纹,已恢复为 1。
203
- // 辅助通道(深度/法线)倍率已并入 resolution,不再单独设 auxRenderScale。
204
- // 实际帧缓冲像素比 = min( max(devicePixelRatio, minDevicePixelRatio) * renderScale, maxPixelRatio )
205
- get effectiveRatio() {
206
- return Math.min(Math.max(window.devicePixelRatio, this.minDevicePixelRatio) * this.renderScale, this.maxPixelRatio);
207
- }
208
- // 基础深度纹理
209
- baseDepthTexture = (() => {
210
- const baseDepthTexture = new DepthTexture(4096, 4096);
211
- baseDepthTexture.format = DepthFormat;
212
- // 升级为 24/32 位深度(UnsignedIntType),相比原 16 位(UnsignedShortType)
213
- // 能显著提升对数深度在远场的精度,消除 SSAO 与深度比对的 banding 瑕疵。
214
- baseDepthTexture.type = UnsignedIntType;
215
- baseDepthTexture.anisotropy = 16;
216
- return baseDepthTexture;
217
- })();
218
- // 基础纹理渲染对象(无 MSAA):抗锯齿统一由终处理 FXAA 承担。
219
- // ⚠️ 必须保持与 depthRenderTarget 同样的「无 MSAA」策略——颜色 MSAA 会让物体边缘平滑成
220
- // 半透明渐入,而 baseDepth(无 MSAA 硬边缘)在相交像素仍记完整深度,二者错位会导致
221
- // depthTestShader 判定边界与人眼视觉不一致,描边/辉光在遮挡边界呈现锯齿状闪缩。
222
- // 统一无 MSAA 后颜色与深度边缘严格对齐,边缘抗锯齿改由最终 FXAA 兜底(见 renderRendererTarget)。
223
- // ⚠️ type 必须是 HalfFloatType(16bit 浮点),不能用默认的 UnsignedByteType(8bit):
224
- // 本 RT 存的是「线性」颜色(NoColorSpace),而 8bit 线性空间在暗部码值极度稀疏
225
- // (linear 码值 0/1/2/3 对应 sRGB 0/12.7/21.7/28.2,相邻码值跨度肉眼可见),
226
- // 导致柔和渐变(外辉光、天空、雾、bloom 衰减)被压成 3~6 个色阶 → 呈现"一层一层"色带。
227
- // 实测(2026-09-06 SpriteText3 外辉光 42px 扫描线):
228
- // 8bit linear:R 通道 42px 内仅 3 个色阶(每 14px 一跳)、G 通道 6 个色阶
229
- // sRGB 8bit :B 通道 42px 内 42 个色阶(每像素一阶,完全平滑)
230
- // 改半浮点后渐变不再在中间量化,终处理仍只做一次 sRGB 编码(见 baseTextureToRenderShader)。
231
- baseRenderTarget = (() => {
232
- const baseRenderTarget = new WebGLRenderTarget(128, 128, {
233
- minFilter: LinearFilter,
234
- magFilter: LinearFilter,
235
- format: RGBAFormat,
236
- type: HalfFloatType,
237
- stencilBuffer: false,
238
- anisotropy: 16,
239
- });
240
- return baseRenderTarget;
241
- })();
242
- /** 可选的屏幕空间水流对象(WaterFlow)。默认 null;调用 enableWaterFlow() 后创建。启用时
243
- * 每帧消费 baseRenderTarget/baseDepthTexture,把水面合成进后处理链(不额外渲染主场景)。 */
244
- waterFlow = null;
245
- _waterFlowLastTime = 0;
246
- waterFlowElapsed = 0;
247
- // 调试用:被排除面 mask 渲染目标(仅「排除面标记」调试档使用,尺寸随主渲染目标同步)
248
- excludedMaskRenderTarget = new WebGLRenderTarget(128, 128, {
249
- minFilter: LinearFilter,
250
- magFilter: LinearFilter,
251
- format: RGBAFormat,
252
- stencilBuffer: false,
253
- depthBuffer: false,
254
- });
255
- // 深度专用渲染目标(无 MSAA,深度值稳定,供后处理深度比对使用)
256
- depthRenderTarget = new WebGLRenderTarget(128, 128, {
257
- minFilter: LinearFilter,
258
- magFilter: LinearFilter,
259
- format: RGBAFormat,
260
- stencilBuffer: false,
261
- depthTexture: this.baseDepthTexture,
262
- });
263
- // AO 深度纹理(含地板):**不再单独渲染一遍** —— 它挂在 normalRenderTarget 上当 depth attachment,
264
- // 法线 pass(renderAoNormalPass,includeFloor=true)渲染时深度由光栅器顺带写入,白拿一张含地板深度。
265
- // (2026-09-14 优化:删掉原 ssao_pass 内那次 renderAoDepthPass(true),每帧省一次全场景遍历。)
266
- // SSAO 的 GTAO 采样它(地板保留为实体 → 接触 AO),合成时再与 depthRenderTarget
267
- // (地板排除 = 可见深度)比对,作废"透过半透明地板看到的实体"上的地板接触 AO。
268
- // 见 three-base/pass/ssao_pass.ts 的复合守卫说明。
269
- aoDepthTexture = (() => {
270
- const t = new DepthTexture(4096, 4096);
271
- t.format = DepthFormat;
272
- t.type = UnsignedIntType;
273
- t.anisotropy = 16;
274
- return t;
275
- })();
276
- /**
277
- * 调试用:深度预览模式。由面板「深度调试」切换,便于逐一看各深度图排查"深度测试全部失效"根因。
278
- * - "none" :正常渲染(默认)
279
- * - "global" :仅预览全局深度图(baseDepthTexture,排除地板)。全白=深度为空→renderAoDepthPass 没写进深度。
280
- * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入的含地板深度)。用于确认深度管线本身是否工作。
281
- * - "compare" :R=全局 base 深度,G=最近一次 glow/stroke 选区 select 深度。两路任一全白即该侧写入失败。
282
- * - "excluded":深度图灰度 + 「被排除出深度图的面」叠红(红=该面写不进深度,永远挡不住后处理)。
283
- * 物体区域(G 暗)若 B 全黑 → 判定从不触发;再看该区 R/G 谁大即可定位是谁缺深度。
284
- * 兼容旧控制台入口:设置 `window.__BASE_DEPTH_VIS = true` 等价于切到 "global"。
285
- * 通用控制台入口:任意页面都可 `window.__TB_DEBUG_DEPTH = "judge"`(优先级高于面板属性)。
286
- */
287
- debugDepthPreview = "none";
288
- /**
289
- * 可见深度图是否包含「标了 userData.aoFloor 的地板」(如 Ground1)。
290
- *
291
- * 默认 false —— 原设计:地板可透视,透过地板看到的物体照常显示(不被地板挡住)。
292
- * 置 true 后地板会写进 baseDepthTexture,**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
293
- * 排查"怎么都挡不住"时先开这个试一下:若开了就正常,说明挡你的正是可透视地板。
294
- */
295
- get baseDepthIncludeFloor() {
296
- return visibleDepthOptions.includeFloor;
297
- }
298
- set baseDepthIncludeFloor(v) {
299
- visibleDepthOptions.includeFloor = v;
300
- this.needRender = true;
301
- }
302
- // 法线专用渲染目标(无 MSAA,存储视图空间法线,供 GTAO 风格 SSAO 使用真实法线)
303
- normalRenderTarget = new WebGLRenderTarget(128, 128, {
304
- minFilter: LinearFilter,
305
- magFilter: LinearFilter,
306
- format: RGBAFormat,
307
- stencilBuffer: false,
308
- // ⚠️ 挂 depthTexture:法线 pass 渲染时深度由光栅器顺带写进来,直接就是「含地板」的 AO 深度
309
- // (零成本,无需再单独渲一遍深度 pass)。见上方 aoDepthTexture 注释。
310
- depthTexture: this.aoDepthTexture,
311
- });
312
- // 屏幕合成目标(无 MSAA):base(含 SSAO + 描边/发光/bloom)拷进来后,
313
- // 颜色叠加/磨砂玻璃/水流/屏幕辉光 在此目标上叠加,最后由 FXAA 终处理输出到屏幕。
314
- // 不挂 MSAA(FXAA 负责边缘)。
315
- // ⚠️ 注意:SSAO 不在本目标上做——2026-09-14 起 SSAO 提前到对象级特效之前、直接乘进 baseRenderTarget
316
- // (见 render() 内注释与 test/depthDebug.test.js K 组护栏)。
317
- // 与 baseRenderTarget 同为线性(NoColorSpace):后处理全程在线性空间做颜色混合,
318
- // 仅由终处理 baseTextureToRenderShader 用 sRGBTransferOETF 做一次 sRGB 编码输出屏幕,
319
- // 避免「RT 已按 sRGB 编码 + 终处理再编一次」导致的双重编码发白。
320
- // ⚠️ 同 baseRenderTarget:必须为 HalfFloatType。SSAO/颜色叠加/磨砂玻璃都在本目标上做线性混合,
321
- // 若这里是 8bit,前面 base 用半浮点保住的渐变会在这一步重新被量化成色带(前功尽弃)。
322
- screenRenderTarget = new WebGLRenderTarget(128, 128, {
323
- minFilter: LinearFilter,
324
- magFilter: LinearFilter,
325
- format: RGBAFormat,
326
- type: HalfFloatType,
327
- stencilBuffer: false,
328
- });
329
- // 基础纹理 -> 屏幕合成目标的纯拷贝(关掉深度,仅透传颜色),用于把 base 图像拷入
330
- // screenRenderTarget 供后续后处理叠加。克隆自共享 copyShader,避免改动其状态。
331
- screenCopyShader = (() => {
332
- const shader = copyShader.clone();
333
- shader.depthTest = false;
334
- shader.depthWrite = false;
335
- shader.transparent = false;
336
- return shader;
337
- })();
338
- // 法线材质(MeshNormalMaterial 输出视图空间法线 pack 到 RGB,与 SSAO 的 unpackRGBToNormal 对应)
339
- // ⚠️ side 必须 DoubleSide:本材质还兼职产出「含地板 AO 深度」(挂在 normalRenderTarget 上),
340
- // 若只渲正面,DoubleSide / 背面朝向的几何会丢深度,与被它取代的那次独立深度 pass 行为不一致。
341
- // 深度测试会让最近面胜出,故法线图内容不会因 DoubleSide 变差。
342
- normalMaterial = new MeshNormalMaterial({ side: DoubleSide });
343
- // 公用渲染目标1(无 MSAA:超采样已收敛到基础纹理与选中目标,其余通道去掉以提速)
344
- renderTarget = new WebGLRenderTarget(128, 128, {
345
- minFilter: LinearFilter,
346
- magFilter: LinearFilter,
347
- format: RGBAFormat,
348
- });
349
- // 渲染目标2(无 MSAA)
350
- renderTarget2 = new WebGLRenderTarget(128, 128, {
351
- minFilter: LinearFilter,
352
- magFilter: LinearFilter,
353
- format: RGBAFormat,
354
- });
355
- // 公用二维向量
356
- tempVector2 = new Vector2();
357
- // 公用的三维向量
358
- tempVector3 = new Vector3();
359
- //渲染器的尺寸
360
- size = new Vector2(0, 0);
361
- //上下文丢失
362
- rendererLost = false;
363
- // 渲染器对象
364
- renderer = (() => {
365
- const renderer = new WebGLRenderer({
366
- alpha: true,
367
- precision: "highp",
368
- logarithmicDepthBuffer: true,
369
- preserveDrawingBuffer: true,
370
- stencil: false,
371
- powerPreference: 'high-performance' // 高性能模式
372
- });
373
- renderer.domElement.id = generateUUID();
374
- //监听上下文的销毁事件
375
- renderer.domElement.addEventListener("webglcontextlost", (event) => {
376
- this.rendererLost = true;
377
- }, false);
378
- // 监听上下文的恢复事件
379
- renderer.domElement.addEventListener("webglcontextrestored", (event) => {
380
- this.rendererLost = false;
381
- }, false);
382
- renderer.localClippingEnabled = true;
383
- renderer.setPixelRatio(Math.max(window.devicePixelRatio, this.minDevicePixelRatio)),
384
- renderer.autoClear = false;
385
- renderer.clear();
386
- renderer.outputColorSpace = SRGBColorSpace;
387
- // 颜色空间:图片什么颜色、3D 里就是什么颜色(sRGB 直出,所见即所得,不发白不褪色)。
388
- // 关闭色调映射后,场景渲染进线性 baseRenderTarget 时不做 tonemap 压缩,终处理只做一次 sRGB
389
- // 编码(baseTextureToRenderShader 手写 sRGBTransferOETF),全链路无额外提亮/降饱和,
390
- // 颜色与原贴图一致。如需电影感可改回 ACESFilmicToneMapping(但会带来发白/褪色观感)。
391
- renderer.toneMapping = NoToneMapping;
392
- threeLoader.setAnisotropy(renderer.capabilities.getMaxAnisotropy());
393
- return renderer;
394
- })();
395
- //透视相机
396
- perspectiveCamera = new PerspectiveCamera(45, 1, 0.01, 20000);
397
- //正交相机
398
- orthographicCamera = new OrthographicCamera(-1, 1, 1, -1, 0.01, 20000);
399
- //相机的类型 有透视相机和正交相机
400
- cameraType = "perspectiveCamera";
401
- setCameraType(cameraType) {
402
- if (this.cameraType !== cameraType) {
403
- this.cameraType = cameraType;
404
- if (this.cameraType === "perspectiveCamera") {
405
- this.threeMouse._camera = this.perspectiveCamera;
406
- this.controls.object = this.perspectiveCamera;
407
- }
408
- else if (this.cameraType === "orthographicCamera") {
409
- this.threeMouse._camera = this.orthographicCamera;
410
- this.controls.object = this.orthographicCamera;
411
- }
412
- }
413
- }
414
- //获取当前使用的的相机
415
- getCamera() {
416
- switch (this.cameraType) {
417
- case "orthographicCamera":
418
- return this.orthographicCamera;
419
- case "perspectiveCamera":
420
- default:
421
- return this.perspectiveCamera;
422
- }
423
- }
424
- // 场景对象
425
- scene = (() => {
426
- const scene = new Scene();
427
- return scene;
428
- })();
429
- // 这个方法等效 this.scene.add
430
- // 想少写2个字母就用这个方法
431
- add(...object) {
432
- this.scene.add(...object);
433
- return this;
434
- }
435
- ///鼠标事件对象
436
- threeMouse = new ThreeMouse(this.renderer, this.perspectiveCamera, this.scene);
437
- // 轨道控制器
438
- controls = (() => {
439
- const controls = new OrbitControls(this.perspectiveCamera, this.renderer.domElement);
440
- controls.addEventListener("change", () => {
441
- this.needRender = true;
442
- });
443
- // 转速:git 历史实证——以前(1826ab1)rotateSpeed=0.2 + enableDamping=true + dampingFactor=0.05
444
- // 时「加阻尼也不晃」;a9f3ec9 把 rotateSpeed 提到 1.0(快 5 倍)才是"快速晃动中心点飘"的真因,
445
- // 与阻尼无关。此处折中到 0.5:比 0.2 跟手、比 1.0 不易飘。若仍飘可降到 0.2 完全复原。
446
- controls.rotateSpeed = 0.5;
447
- controls.maxPolarAngle = Math.PI;
448
- // 阻尼:回到以前不飘的配置(enableDamping=true + dampingFactor=0.05)。
449
- // 0.05 是 1826ab1 验证过"加阻尼不晃"的值,配合下面 render 循环的帧率无关换算。
450
- controls.enableDamping = true;
451
- controls.dampingFactor = CONTROLS_DAMPING_BASE;
452
- controls.screenSpacePanning = true;
453
- return controls;
454
- })();
455
- // 帧率无关阻尼用的上一帧时间戳(performance.now() 毫秒),初始 0 表示首帧用 60fps 基准
456
- _lastControlsTime = 0;
457
- // 固定的实例化分组器:自动把所有 InstancedChild 合并进 InstancedMesh 批量渲染
458
- instancedGroup = new InstancedGroup();
459
- /**
460
- * 自定义描边目标:不在 scene 内的独立对象(如 InstancedChild 的"选择代理"),
461
- * 渲染循环在效果阶段单独对其调用 stroke_pass,使其获得与普通 Mesh 一致的描边,
462
- * 且不会被正常 base pass 重复绘制(因为它不在 scene 树内)。
463
- * 典型用途:InstancedChild / 一组 InstancedChild 没有自身几何,无法直接描边,
464
- * 用 buildSelectProxy 重建其几何代理后再注册到这里即可。
465
- */
466
- _strokeObjects = [];
467
- /** 注册一个自定义描边对象(效果阶段对其描边) */
468
- addStrokeObject(object) {
469
- if (this._strokeObjects.indexOf(object) < 0) {
470
- this._strokeObjects.push(object);
471
- }
472
- return this;
473
- }
474
- /** 移除自定义描边对象 */
475
- removeStrokeObject(object) {
476
- const i = this._strokeObjects.indexOf(object);
477
- if (i >= 0) {
478
- this._strokeObjects.splice(i, 1);
479
- }
480
- return this;
481
- }
482
- // 设置 dom 容器
483
- async setContainer(div) {
484
- if (div) {
485
- div.style.overflow = "hidden";
486
- div.innerHTML = "";
487
- div.append(this.renderer.domElement);
488
- if (this.rendererLost) {
489
- this.renderer.forceContextRestore();
490
- this.needRender = true;
491
- setTimeout(() => {
492
- this.needRender = true;
493
- }, 100);
494
- setTimeout(() => {
495
- this.renderer.domElement.style.opacity = "1";
496
- }, 200);
497
- }
498
- this.setSize(div.clientWidth, div.clientHeight);
499
- // 开启时时渲染
500
- threeUtils.removeEventListener("requestAnimationFrame", this.renderBindThis);
501
- threeUtils.addEventListener("requestAnimationFrame", this.renderBindThis);
502
- }
503
- }
504
- // 修改尺寸
505
- setSize(width, height) {
506
- // 防御容器未布局/隐藏导致的 0 尺寸:相机 aspect、renderer、RT 都不能接受 0,
507
- // 否则 WebGL 会报 GL_INVALID_VALUE / Framebuffer incomplete: Attachment has zero size。
508
- width = Math.max(1, Math.floor(width));
509
- height = Math.max(1, Math.floor(height));
510
- //修改透视相机的比例
511
- this.perspectiveCamera.aspect = width / height;
512
- this.perspectiveCamera.updateProjectionMatrix();
513
- //修改正交相机的比例
514
- // this.orthographicCamera.top = height / width;
515
- // this.orthographicCamera.bottom = -height / width;
516
- this.orthographicCamera.left = -width / height;
517
- this.orthographicCamera.right = width / height;
518
- this.renderer.setPixelRatio(Math.max(window.devicePixelRatio, this.minDevicePixelRatio)),
519
- this.renderer.setSize(width, height);
520
- // 所有 RT 尺寸按 effectiveRatio × resolution 重建(容器尺寸变化与 resolution 变化共用)
521
- this.updateRenderTargetSizes(width, height);
522
- this.size.set(width, height);
523
- setTimeout(() => {
524
- this.needRender = true;
525
- }, 100);
526
- // 渲染场景
527
- // this.renderer.clear();
528
- // this.renderer.render(this.scene, this.camera);
529
- }
530
- /**
531
- * 按「容器尺寸 × effectiveRatio × resolution」重建所有渲染目标尺寸。
532
- * 由 setSize()(容器尺寸变化)与 resolution setter(运行时调倍率)共用 ——
533
- * 关键:resolution 是运行时可调的性能/质量旋钮,改完必须立刻重建 RT,
534
- * 否则倍率变了而 RT 尺寸没变,画面毫无变化(历史坑:"调分辨率没感觉")。
535
- */
536
- updateRenderTargetSizes(width, height) {
537
- // 防御 0 尺寸:RT 不接受 0,否则 WebGL 报 framebuffer incomplete
538
- const w = Math.max(1, Math.round(width * this.effectiveRatio * this._resolution));
539
- const h = Math.max(1, Math.round(height * this.effectiveRatio * this._resolution));
540
- // 主画面:超采样渲染,纹理摩尔纹在此被根治
541
- this.baseRenderTarget.setSize(w, h);
542
- // 深度/法线辅助通道:与 base 同倍率,保证 depthTest 1:1 无漏光、SSAO 深度不黑。
543
- // 其 depthTexture(baseDepthTexture) 尺寸由 renderer 在 setRenderTarget 时
544
- // 强制同步为 depthRenderTarget 尺寸,下面再手动对齐一次避免首帧抖动。
545
- this.depthRenderTarget.setSize(w, h);
546
- this.normalRenderTarget.setSize(w, h);
547
- this.excludedMaskRenderTarget.setSize(w, h);
548
- // 屏幕合成目标:同样跟随 resolution,全链路 1:1 对齐
549
- this.screenRenderTarget.setSize(w, h);
550
- // 水流后处理目标跟随同倍率重建(pass 内部粒子 RT 仍按 scale 降采样,compositeTarget 全分辨率)
551
- this.waterFlow?.resize(w, h);
552
- this.baseDepthTexture.image.width = w;
553
- this.baseDepthTexture.image.height = h;
554
- // 立即渲染一帧,避免静态场景下改了倍率却停在旧画面
555
- this.needRender = true;
556
- }
557
- // 视角距离过度
558
- controlsDistanceTransition = (() => {
559
- const controlsDistanceTransition = new Transition({ distance: 0 });
560
- controlsDistanceTransition.setDuration(1000);
561
- return controlsDistanceTransition;
562
- })();
563
- //控制器的Y值过度计算
564
- controlsYTransition = (() => {
565
- const controlsYTransition = new Transition({ y: 0 });
566
- controlsYTransition.setDuration(1000);
567
- return controlsYTransition;
568
- })();
569
- //控制器的XZ值过度计算
570
- controlsXZTransition = (() => {
571
- const controlsXZTransition = new Transition({ x: 0, z: 0 });
572
- controlsXZTransition.setDuration(1000);
573
- return controlsXZTransition;
574
- })();
575
- // 控制器的 朝向带缓动的计算的顶点对象
576
- controlsCenter = (() => {
577
- const controlsCenter = new Transition({ x: 0, y: 0, z: 0 });
578
- controlsCenter.setDuration(1000);
579
- controlsCenter.onChange(({ x, y, z }) => {
580
- this.controls.target.set(x, y, z);
581
- this.perspectiveCamera.lookAt(this.controls.target);
582
- this.orthographicCamera.lookAt(this.controls.target);
583
- this.needRender = true;
584
- });
585
- return controlsCenter;
586
- })();
587
- // 控制器的位置缓动的计算的顶点对象
588
- controlsPosition = (() => {
589
- const controlsPosition = new Transition({ x: 100, y: 100, z: 100 });
590
- controlsPosition.setDuration(1000);
591
- controlsPosition.onChange(({ x, y, z }) => {
592
- const distance = this.controlsDistanceTransition.get("distance");
593
- this.perspectiveCamera.position.set(x + 0.0001, y, z + 0.0001);
594
- this.perspectiveCamera.lookAt(this.controls.target);
595
- //计算正交相机的缓动
596
- this.orthographicCamera.position
597
- .set(x + 0.01, y, z + 0.01)
598
- .sub(this.controls.target)
599
- .setLength(5000)
600
- .add(this.controls.target);
601
- this.orthographicCamera.lookAt(this.controls.target);
602
- this.needRender = true;
603
- });
604
- return controlsPosition;
605
- })();
606
- // 网格辅助线
607
- gridHelper = (() => {
608
- const gridHelper = new GridHelper(1000, 100, 0x303030, 0x303030);
609
- if (threeUtils.isDev) {
610
- this.scene.add(gridHelper);
611
- }
612
- return gridHelper;
613
- })();
614
- //坐标轴线
615
- axesHelper = (() => {
616
- const axesHelper = new AxesHelper(10000);
617
- if (threeUtils.isDev) {
618
- this.scene.add(axesHelper);
619
- }
620
- return axesHelper;
621
- })();
622
- init() {
623
- // 屏幕空间水流:?waterflow=1 时自动启用(默认关闭,需显式 enableWaterFlow 或此 URL 参数)。
624
- // 创建后每帧消费 base 深度 + 后处理合成画面,把水面合成进后处理链,不自渲染主场景。
625
- try {
626
- const params = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
627
- if (params.get('waterflow') === '1') {
628
- this.enableWaterFlow();
629
- }
630
- }
631
- catch (_e) {
632
- // 非浏览器环境(如 Node 测试/SSR)忽略
633
- }
634
- }
635
- /**
636
- * 启用屏幕空间水流(GPGPU 粒子流体)。默认关闭,需显式调用或携带 ?waterflow=1。
637
- * 创建后每帧自动消费 base 深度 + 当前合成画面,把水面合成进后处理链,不自渲染主场景。
638
- * @param options.size 粒子网格边长(size×size 粒子),越大越细腻、越费
639
- * @param options.speed 射流初速
640
- * @param options.aperture 喷嘴开度
641
- * @param options.nozzle 喷流起点坐标 [x,y,z],默认 [0.58,1.55,0]
642
- * @param options.jetDirection 喷流方向(轴向,会被归一化),默认 [1,0,0](沿 +X)
643
- * @param options.collision 是否开启粒子与 basin/地面碰撞回弹,默认 true
644
- */
645
- enableWaterFlow(options = {}) {
646
- if (this.waterFlow) {
647
- console.warn('[ThreeBase] WaterFlow 已启用,重复调用无效');
648
- return;
649
- }
650
- const { size = 200, speed = 2.4, aperture = 0.78, nozzle, jetDirection, collision = true, } = options;
651
- const waterFlow = new WaterFlow(this.renderer, {
652
- size,
653
- speed,
654
- aperture,
655
- nozzle: nozzle ? new Vector3(nozzle[0], nozzle[1], nozzle[2]) : undefined,
656
- jetDirection: jetDirection ? new Vector3(jetDirection[0], jetDirection[1], jetDirection[2]) : undefined,
657
- collision,
658
- });
659
- if (!waterFlow.supported) {
660
- console.warn('[ThreeBase] 当前环境不支持浮点纹理,WaterFlow 未启用');
661
- waterFlow.dispose();
662
- return;
663
- }
664
- // 创建 pass 并注入基础纹理(externalBase 模式);后续每帧 renderInto 会刷新注入的纹理。
665
- // ⚠️ 2026-10-08(WaterFlow 全量 TS 化):ScreenSpaceFluidPass 内部按透视相机建模
666
- // (setSize 读 camera.fov 算 pointScale、depthMaterial 走 projectionMatrix 重建深度),
667
- // 故此处显式窄化。getCamera() 在 cameraType==="orthographicCamera" 时会返回
668
- // OrthographicCamera —— 原 JS 同样会把 fov=undefined 传进去(pointScale 变 NaN),
669
- // 本次**只补类型、未改运行时行为**,保持与线上一致。
670
- waterFlow.createPass(this.scene, this.getCamera(), {
671
- colorTexture: this.baseRenderTarget.texture,
672
- depthTexture: this.baseDepthTexture,
673
- depthMode: 'logarithmic',
674
- });
675
- // 首帧按当前容器尺寸对齐(后续 setSize/resolution 变化由 updateRenderTargetSizes 兜底)。
676
- const w = Math.max(1, Math.round(this.size.x * this.effectiveRatio * this._resolution));
677
- const h = Math.max(1, Math.round(this.size.y * this.effectiveRatio * this._resolution));
678
- if (this.size.x > 1 && this.size.y > 1)
679
- waterFlow.resize(w, h);
680
- this.waterFlow = waterFlow;
681
- this._waterFlowLastTime = 0;
682
- this.waterFlowElapsed = 0;
683
- console.info('[ThreeBase] WaterFlow 已启用');
684
- }
685
- /** 运行时改喷流起点(喷嘴)坐标(WaterFlow 已启用时有效) */
686
- setWaterFlowNozzle(x, y, z) {
687
- this.waterFlow?.setNozzle(x, y, z);
688
- }
689
- /** 运行时改喷流方向(轴向,会被归一化) */
690
- setWaterFlowJetDirection(x, y, z) {
691
- this.waterFlow?.setJetDirection(x, y, z);
692
- }
693
- /** 运行时开关粒子与 basin/地面碰撞回弹 */
694
- setWaterFlowCollision(enabled) {
695
- this.waterFlow?.setCollision(enabled);
696
- }
697
- /**
698
- * 关闭屏幕空间水流并释放其 GPU 资源(enableWaterFlow 的反操作)。
699
- * 切走页面时必须调用,否则其它页面 render 仍会合成水面(this.waterFlow 非空)。
700
- */
701
- disableWaterFlow() {
702
- if (!this.waterFlow)
703
- return;
704
- this.waterFlow.dispose();
705
- this.waterFlow = null;
706
- this._waterFlowLastTime = 0;
707
- this.waterFlowElapsed = 0;
708
- console.info('[ThreeBase] WaterFlow 已关闭');
709
- }
710
- // 重置控制器的位置
711
- resetLookAt({ position = this.perspectiveCamera.position, center = this.controls.target, }) {
712
- // 停止控制器的惯性
713
- const originEnableDamping = this.controls.enableDamping;
714
- this.controls.saveState();
715
- this.controls.update();
716
- this.controls.reset();
717
- this.controls.enableDamping = originEnableDamping;
718
- this.controlsDistanceTransition.reset({
719
- distance: new Vector3(position.x, position.y, position.z).distanceTo(new Vector3(center.x, center.y, center.z))
720
- });
721
- position &&
722
- this.controlsPosition.reset({
723
- x: position.x,
724
- y: position.y,
725
- z: position.z,
726
- });
727
- center &&
728
- this.controlsCenter.reset({ x: center.x, y: center.y, z: center.z });
729
- this.needRender = true;
730
- }
731
- // 设置控制器的位置 以动画的方式移动控制器
732
- async setLookAt({ position = this.perspectiveCamera.position, center = this.controls.target, duration = 1000, }) {
733
- // 停止控制器的惯性
734
- const originEnableDamping = this.controls.enableDamping;
735
- this.controls.saveState();
736
- this.controls.update();
737
- this.controls.reset();
738
- this.controls.enableDamping = originEnableDamping;
739
- let currentPosition = this.perspectiveCamera.position.clone();
740
- let currentCenter = this.controls.target.clone();
741
- let currentDistance = this.perspectiveCamera.position.distanceTo(this.controls.target);
742
- let targetPosition = new Vector3(position.x, position.y, position.z);
743
- let targetCenter = new Vector3(center.x, center.y, center.z);
744
- let targetDistance = targetPosition.distanceTo(targetCenter);
745
- // 等待动画执行完成
746
- await Promise.all([
747
- new Promise(async (resolve, reject) => {
748
- this.controlsDistanceTransition.setDuration(duration).reset({
749
- distance: currentDistance,
750
- });
751
- await this.controlsDistanceTransition.set({
752
- distance: targetDistance
753
- });
754
- resolve();
755
- }),
756
- new Promise(async (resolve, reject) => {
757
- this.controlsPosition.setDuration(duration).reset({
758
- x: currentPosition.x,
759
- y: currentPosition.y,
760
- z: currentPosition.z,
761
- });
762
- await this.controlsPosition.set({
763
- x: position.x,
764
- y: position.y,
765
- z: position.z,
766
- });
767
- resolve();
768
- }),
769
- new Promise(async (resolve, reject) => {
770
- this.controlsCenter.setDuration(duration).reset({
771
- x: currentCenter.x,
772
- y: currentCenter.y,
773
- z: currentCenter.z,
774
- });
775
- await this.controlsCenter.set({
776
- x: center.x,
777
- y: center.y,
778
- z: center.z,
779
- });
780
- resolve();
781
- }),
782
- ]);
783
- }
784
- // 获取控制的位置和朝向 保留3位小数
785
- getLookAt() {
786
- //判断需要使用那个相机
787
- const position = this.perspectiveCamera.position
788
- .toArray()
789
- .map((v) => round(v, 3));
790
- const center = this.controls.target.toArray().map((v) => round(v, 3));
791
- return {
792
- center: new Vector3(...center),
793
- position: new Vector3(...position),
794
- centerPosition: `center: new Vector3(${center.join(",")}),position: new Vector3(${position.join(",")})`,
795
- };
796
- }
797
- // 查看对象
798
- lookatObjects({ objects, duration = 1000, direction = new Vector3(0.3, 0.3, 1), offset = new Vector3(0, 0, 0), distance = 1, }) {
799
- const box3 = new Box3();
800
- objects.map((o) => {
801
- o.traverseVisible((oo) => {
802
- box3.expandByObject(oo);
803
- });
804
- });
805
- const size = new Vector3();
806
- box3.getSize(size);
807
- const center = new Vector3();
808
- box3.getCenter(center);
809
- center.add(offset);
810
- const modelSize = size.length() * distance;
811
- if (this.cameraType === "perspectiveCamera") {
812
- const { fov } = this.getCamera();
813
- const cameraDistance = modelSize / (2 * Math.tan((fov * Math.PI) / 360));
814
- const position = direction.setLength(cameraDistance).add(center);
815
- this.setLookAt({ center, position, duration });
816
- }
817
- }
818
- // 三维向量转化成屏幕上的2维向量
819
- v3tov2(v3) {
820
- //判断需要使用那个相机
821
- const camera = this.getCamera();
822
- v3.project(camera);
823
- const v2 = new Vector2((v3.x + 1) * this.size.x * 0.5, (-v3.y + 1) * this.size.y * 0.5);
824
- return v2;
825
- }
826
- // 控制器类型
827
- controlType = "OrbitControls";
828
- // 记录一下空间平移方式 在切换控制器的时候保留一下平移方式
829
- originScreenSpacePanning = false;
830
- //改变控制器的类型
831
- setControlType(controlType) {
832
- if (controlType === "OrbitControls") {
833
- this.controls.screenSpacePanning = this.originScreenSpacePanning;
834
- this.controls.mouseButtons = {
835
- LEFT: MOUSE.ROTATE,
836
- MIDDLE: MOUSE.DOLLY,
837
- RIGHT: MOUSE.PAN,
838
- };
839
- this.controls.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
840
- }
841
- else if (controlType === "MapControls") {
842
- this.originScreenSpacePanning = this.controls.screenSpacePanning;
843
- // pan orthogonal to world-space direction camera.up
844
- this.controls.screenSpacePanning = false;
845
- this.controls.mouseButtons.LEFT = MOUSE.PAN;
846
- this.controls.mouseButtons.RIGHT = MOUSE.ROTATE;
847
- this.controls.touches.ONE = TOUCH.PAN;
848
- this.controls.touches.TWO = TOUCH.DOLLY_ROTATE;
849
- }
850
- }
851
- // 是否自动渲染
852
- autoRender = true;
853
- //是否需要渲染, 当autoRender=false的时候就需要 手动触发渲染
854
- needRender = false;
855
- // SSAO 配置
856
- ssao = {
857
- enable: false,
858
- debug: false,
859
- radius: 5.0,
860
- falloff: 5.0,
861
- bias: 0.3,
862
- intensity: 1.0,
863
- sampleCount: 16,
864
- blurRadius: 4.0,
865
- strength: 1.0,
866
- resolutionScale: 0.5,
867
- distanceExponent: 1.0,
868
- distanceFallOff: 1.0,
869
- scale: 1.0, // GTAO:AO 次幂(gamma 校正)
870
- };
871
- // 屏幕空间辉光(经典 gaussian bloom)配置:纯颜色、无深度依赖,在 screenRenderTarget 上做加性叠加
872
- bloom = {
873
- enable: false,
874
- strength: 0.6,
875
- radius: 0.4,
876
- threshold: 0.85,
877
- };
878
- // 停止渲染
879
- stopRender() {
880
- threeUtils.removeEventListener("requestAnimationFrame", this.renderBindThis);
881
- if (!this.rendererLost) {
882
- this.renderer.forceContextLoss();
883
- this.setSize(100, 100);
884
- this.renderer.domElement.style.opacity = "0";
885
- }
886
- }
887
- // 彻底销毁当前渲染器实例:停止渲染循环、释放 WebGL 上下文与场景资源、卸载画布 DOM。
888
- // 用于「每个 demo 视图各自持有渲染器、切换页面时自行销毁」的生命周期管理。
889
- dispose() {
890
- // 1. 停止渲染循环(移除 rAF 监听 + 丢失上下文)
891
- this.stopRender();
892
- // 2. 轨道控制器(移除其 DOM 事件监听)
893
- this.controls?.dispose();
894
- // 3. 鼠标拾取(移除 canvas 上的事件监听)
895
- this.threeMouse?.dispose();
896
- // 4. 释放场景内所有几何体 / 材质 / 纹理
897
- this.scene.traverse((obj) => {
898
- const mesh = obj;
899
- if (mesh.geometry) {
900
- mesh.geometry.dispose();
901
- }
902
- const material = mesh.material;
903
- if (Array.isArray(material)) {
904
- material.forEach((m) => this._disposeMaterial(m));
905
- }
906
- else if (material) {
907
- this._disposeMaterial(material);
908
- }
909
- });
910
- // 5. 渲染器与画布 DOM
911
- const dom = this.renderer.domElement;
912
- if (dom.parentElement) {
913
- dom.parentElement.removeChild(dom);
914
- }
915
- this.renderer.dispose();
916
- }
917
- /** 释放单个材质及其引用的纹理(遍历材质属性找 Texture 并 dispose) */
918
- _disposeMaterial(material) {
919
- const record = material;
920
- for (const key in record) {
921
- const value = record[key];
922
- if (value && value.isTexture) {
923
- value.dispose();
924
- }
925
- }
926
- material.dispose();
927
- }
928
- // 渲染场景
929
- render() {
930
- // canvas 没有在页面上面的话 就停止渲染
931
- if (!threeUtils.inDocument(this.renderer.domElement)) {
932
- this.stopRender();
933
- return;
934
- }
935
- // 相机的朝向
936
- const center = new Vector3(...this.controls.target.toArray().map((v) => round(v, 3)));
937
- //透视相机和正交相机参数的时时转换
938
- if (this.cameraType === "perspectiveCamera") {
939
- const { fov } = this.perspectiveCamera;
940
- const distaned = this.perspectiveCamera.position
941
- .clone()
942
- .sub(center)
943
- .length();
944
- const zoom = 1 / (Math.tan(degToRad(fov) / 2) * distaned);
945
- this.orthographicCamera.zoom = zoom;
946
- this.orthographicCamera.position
947
- .copy(this.perspectiveCamera.position.clone().sub(center))
948
- .setLength(2500)
949
- .add(center);
950
- }
951
- else if (this.cameraType === "orthographicCamera") {
952
- const { fov } = this.perspectiveCamera;
953
- const { zoom } = this.orthographicCamera;
954
- const distaned = 1 / zoom / Math.tan(degToRad(fov) / 2);
955
- this.perspectiveCamera.position
956
- .copy(this.orthographicCamera.position.clone().sub(center))
957
- .setLength(distaned)
958
- .add(center);
959
- }
960
- //判断需要使用那个相机
961
- const camera = this.getCamera();
962
- //判断尺寸发生改变时重置尺寸
963
- const parentNode = this.renderer.domElement.parentNode;
964
- if (parentNode.clientWidth !== this.size.x ||
965
- parentNode.clientHeight !== this.size.y) {
966
- this.setSize(parentNode.clientWidth, parentNode.clientHeight);
967
- }
968
- // 分发鼠标事件
969
- this.threeMouse.dispatchEvent();
970
- // 在不自动渲染时, needRender=false 就跳过渲染
971
- if (!this.autoRender && !this.needRender) {
972
- return; // 标识未渲染
973
- }
974
- this.needRender = false;
975
- // 水流模拟推进:独立于相机控制,按真实帧间隔累计时间推进 GPGPU 粒子模拟。
976
- // 默认 this.waterFlow 为 null(关闭),调用 enableWaterFlow() 或 ?waterflow=1 才创建。
977
- if (this.waterFlow && this.waterFlow.supported) {
978
- const wnow = performance.now();
979
- const wdt = this._waterFlowLastTime > 0
980
- ? Math.min((wnow - this._waterFlowLastTime) / 1000, 0.05)
981
- : 1 / 60;
982
- this._waterFlowLastTime = wnow;
983
- this.waterFlowElapsed += wdt;
984
- this.waterFlow.update(wdt, this.waterFlowElapsed);
985
- }
986
- if (this.controls.enabled) {
987
- // 让 OrbitControls 始终接管"当前渲染相机":OrbitControls 构造时只绑定了
988
- // perspectiveCamera,但渲染相机随 cameraType 切换。若切到正交相机时仍让 OrbitControls
989
- // 控制 perspectiveCamera、却用正交相机渲染,且每帧用正交相机反推并覆盖 perspectiveCamera,
990
- // 两者打架 → 旋转中心漂移、运动错乱。此处把 controls.object 同步为渲染相机即可根治。
991
- if (this.controls.object !== camera) {
992
- this.controls.object = camera;
993
- }
994
- // 帧率无关的阻尼换算:OrbitControls 的 dampingFactor 是「每帧」衰减比例,固定值会随帧率变化
995
- // 导致阻尼手感不一致——后处理管线重,快速晃动掉帧时单帧间隔变大、衰减比例却不变,
996
- // 镜头运动一顿一顿、中心点发飘。这里把基准 dampingFactor 按真实帧间隔归一化到 60fps:
997
- // effective = 1 - (1 - BASE)^(dt*60)
998
- // 掉帧(dt 变大)时自动加大单帧衰减比例,让阻尼手感与帧率解耦。
999
- const now = performance.now();
1000
- const dt = this._lastControlsTime > 0
1001
- ? Math.min((now - this._lastControlsTime) / 1000, 0.05)
1002
- : 1 / 60;
1003
- this._lastControlsTime = now;
1004
- // 帧率无关阻尼换算:OrbitControls 的 dampingFactor 是「每帧」衰减比例,固定值会随帧率变化
1005
- // 导致阻尼手感不一致(后处理管线重、快速晃动掉帧时单帧间隔变大、衰减比例却不变 → 一顿一顿)。
1006
- // 把基准 dampingFactor 按真实帧间隔归一化到 60fps:effective = 1 - (1 - BASE)^(dt*60),
1007
- // 掉帧时自动加大单帧衰减比例,让阻尼手感与帧率解耦。每帧无条件 update()(与 1826ab1 一致,
1008
- // 该版本 enableDamping=true + dampingFactor=0.05 已验证不飘)。
1009
- this.controls.dampingFactor = 1 - Math.pow(1 - CONTROLS_DAMPING_BASE, dt * 60);
1010
- this.controls.update();
1011
- }
1012
- {
1013
- // 先派发 beforeRender,让用户动画在当前帧修改 transform(修改会在下方一次性更新进世界矩阵)
1014
- this.dispatchEvent({ type: "beforeRender" });
1015
- // 实例化分组器:场景世界矩阵更新后,把 InstancedChild 同步进真实 InstancedMesh
1016
- this.instancedGroup.update();
1017
- // 渲染场景
1018
- // 把基础场景渲染到 帧缓冲区 在后期通道中会用到
1019
- this.renderer.setRenderTarget(this.baseRenderTarget);
1020
- this.renderer.clear();
1021
- this.renderer.render(this.scene, camera);
1022
- // 深度 pass:固定排除非实体表面后渲到 depthRenderTarget(生成 baseDepthTexture)。
1023
- // 必须在 base render 之后、所有依赖深度的特效(描边/发光/覆盖/磨砂)之前更新,
1024
- // 否则这些特效会用到上一帧旧深度 → 遮挡判定错乱。法线 pass 已收进 ssao_pass 内部。
1025
- renderAoDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1026
- // 切回 baseRenderTarget,后处理 pass 需要把效果叠加到颜色渲染目标上
1027
- this.renderer.setRenderTarget(this.baseRenderTarget);
1028
- // 效果 mask 灰度图尺寸 = effectiveRatio × resolution,与 baseRenderTarget / baseDepthTexture 同倍率,
1029
- // 全链路 1:1 对齐(mask 跟随 resolution,深度也跟随 resolution,辉光漏光/mask 合成 UV 错位均消除)。
1030
- // 几何/效果边缘抗锯齿由「baseRenderTarget/screenRenderTarget 超采样(resolution) + 终处理 FXAA 降采样」承担。
1031
- const dpr = this.effectiveRatio * this.resolution;
1032
- // SSAO 后处理(基于深度计算遮蔽,乘法叠加):数据准备 + 计算全在 ssao_pass 内部,
1033
- // 结果输出到「调用时绑定的渲染目标」。
1034
- // ⚠️ 顺序(2026-09-14 皇上旨):SSAO 必须排在对象级特效(描边/发光/bloom/覆盖/磨砂)之前——
1035
- // 遮蔽只作用于纯场景底色,特效叠在 AO 之上,描边/辉光不会被 AO 压暗、也不会被 AO 的噪声污染。
1036
- // 此处绑定的是 baseRenderTarget,故 AO 直接乘进 base;后面的特效与「base → screen 拷贝」
1037
- // 全部基于「已含 AO」的底色,后续 pass 无需任何改动。
1038
- ssao_pass(this.renderer, camera, this.scene, this.depthRenderTarget, this.normalRenderTarget, this.normalMaterial, dpr, this.ssao);
1039
- // 后处理特效渲染(描边,发光,颜色覆盖,bloom,磨砂玻璃):叠加到 baseRenderTarget(此时已含 SSAO)
1040
- this.scene.traverseVisible((child) => {
1041
- if (child instanceof Mesh || child instanceof Line || child instanceof Points) {
1042
- stroke_pass(this.renderer, camera, child, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1043
- glow_pass(this.renderer, camera, child, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1044
- bloom_pass(this.renderer, camera, child, this.baseRenderTarget, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1045
- }
1046
- });
1047
- // 自定义描边对象(如 InstancedChild 的选择代理):不在 scene 内,单独在效果阶段描边。
1048
- // 代理用实例自身的 matrixWorld + 同款 geometry/material 重建,故其深度/灰度与真实实例严格对齐,
1049
- // 并复用同一张 baseDepthTexture 做遮挡判定(被其他物体挡住的部分不描边)。
1050
- for (const so of this._strokeObjects) {
1051
- stroke_pass(this.renderer, camera, so, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1052
- glow_pass(this.renderer, camera, so, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1053
- bloom_pass(this.renderer, camera, so, this.baseRenderTarget, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1054
- }
1055
- // 把 base(含 SSAO)+描边/发光/bloom 合成结果拷到屏幕合成目标,后续 颜色叠加/磨砂玻璃
1056
- // 在此目标上叠加;最后统一跑一次 FXAA 终处理输出到屏幕,覆盖全部后处理的锯齿/摩尔纹。
1057
- this.renderer.setRenderTarget(this.screenRenderTarget);
1058
- this.renderer.clear();
1059
- this.screenCopyShader.uniforms.map.value = this.baseRenderTarget.texture;
1060
- fsQuad.render(this.renderer, this.screenCopyShader);
1061
- // 颜色叠加
1062
- this.scene.traverseVisible((child) => {
1063
- if (child instanceof Mesh || child instanceof Line || child instanceof Points) {
1064
- cover_pass(this.renderer, camera, child, this.baseDepthTexture, dpr);
1065
- }
1066
- });
1067
- for (const so of this._strokeObjects) {
1068
- cover_pass(this.renderer, camera, so, this.baseDepthTexture, dpr);
1069
- }
1070
- // 一次性处理所有支持毛玻璃特效的对象
1071
- frosted_glass_pass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, dpr);
1072
- // 屏幕空间水流:消费 base 深度 + 当前合成画面(含 ssao/覆盖/磨砂),把水面合成进
1073
- // screenRenderTarget。插在磨砂之后、screenBloom 之前,使水面高光也能被泛光处理。
1074
- // 默认关闭(this.waterFlow 为 null 时不产生任何开销)。
1075
- if (this.waterFlow && this.waterFlow.supported) {
1076
- waterFlowPass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, this.waterFlow);
1077
- }
1078
- // 屏幕空间辉光(经典 gaussian bloom):纯颜色、无深度依赖,把亮度超阈值的部分做
1079
- // 连续高斯模糊后加性叠加回 screenRenderTarget。单分辨率多迭代,柔和均匀、无 mip 分层。
1080
- if (this.bloom.enable) {
1081
- screenBloomPass(this.renderer, this.screenRenderTarget, {
1082
- strength: this.bloom.strength,
1083
- radius: this.bloom.radius,
1084
- threshold: this.bloom.threshold,
1085
- });
1086
- }
1087
- // FXAA 终处理:对最终合成图像(含 SSAO/AO/叠加/磨砂/辉光)跑一次 FXAA 统一兜底锯齿与摩尔纹,
1088
- // 分辨率按实际帧缓冲像素(已乘 dpr)设置,修复高分屏下 FXAA 采样比例偏大失效的问题。
1089
- // ⚠️ 深度调试预览(面板「深度调试」切换,定位"深度测试全部失效"根因):
1090
- // 旧控制台入口 `window.__BASE_DEPTH_VIS = true` 仍可用,等价于切到 "global"。
1091
- // global → baseDepthTexture 灰度。全白=深度为空(renderAoDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1092
- // ao → aoDepthTexture 灰度(含地板)。确认深度管线本身是否工作(GTAO 是否拿到正确深度)。
1093
- // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
1094
- // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
1095
- // 通用控制台入口(任意页面可用,优先级高于面板属性):
1096
- // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge"`,设回 "none" 恢复。
1097
- const dbgWin = window.__TB_DEBUG_DEPTH;
1098
- const dbgMode = window.__BASE_DEPTH_VIS
1099
- ? "global"
1100
- : (dbgWin ?? this.debugDepthPreview);
1101
- if (dbgMode !== "none") {
1102
- this.renderer.setRenderTarget(null);
1103
- this.renderer.clear();
1104
- if (dbgMode === "global") {
1105
- baseDepthVisShader.uniforms.tDepth.value = this.baseDepthTexture;
1106
- fsQuad.render(this.renderer, baseDepthVisShader);
1107
- }
1108
- else if (dbgMode === "ao") {
1109
- baseDepthVisShader.uniforms.tDepth.value = this.aoDepthTexture;
1110
- fsQuad.render(this.renderer, baseDepthVisShader);
1111
- }
1112
- else if (dbgMode === "compare") {
1113
- debugDepthCompareShader.uniforms.tBase.value = this.baseDepthTexture;
1114
- debugDepthCompareShader.uniforms.tSelect.value =
1115
- debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1116
- debugDepthCompareShader.uniforms.cameraFar.value = camera.far;
1117
- fsQuad.render(this.renderer, debugDepthCompareShader);
1118
- }
1119
- else if (dbgMode === "judge") {
1120
- debugDepthJudgeShader.uniforms.tBase.value = this.baseDepthTexture;
1121
- debugDepthJudgeShader.uniforms.tSelect.value =
1122
- debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1123
- fsQuad.render(this.renderer, debugDepthJudgeShader);
1124
- }
1125
- else if (dbgMode === "excluded") {
1126
- // 先渲「被排除面 mask」,再与深度图叠加:红=挡不住任何后处理的面
1127
- renderExcludedMaskPass(this.renderer, camera, this.scene, this.excludedMaskRenderTarget);
1128
- debugExcludedShader.uniforms.tDepth.value = this.baseDepthTexture;
1129
- debugExcludedShader.uniforms.tMask.value = this.excludedMaskRenderTarget.texture;
1130
- fsQuad.render(this.renderer, debugExcludedShader);
1131
- }
1132
- flushLensflareOverlays(this.renderer, camera);
1133
- return;
1134
- }
1135
- this.renderer.setRenderTarget(null);
1136
- this.renderer.clear();
1137
- this.renderRendererTarget(this.screenRenderTarget);
1138
- // 镜头光晕(Lensflare)解耦:渲染器输出屏幕后统一叠加所有可见鬼影。
1139
- // flushLensflareOverlays 内部仅当渲染目标为屏幕时生效,不污染任何中间后处理 target;
1140
- // LensFlare 实例自己管理状态(构造注册进全局表、dispose 注销),ThreeBase 不遍历场景、不认识其类型。
1141
- flushLensflareOverlays(this.renderer, camera);
1142
- }
1143
- }
1144
- renderBindThis = this.render.bind(this);
1145
- //获取渲染图blob对象
1146
- async getThumbBlob() {
1147
- this.renderer.setRenderTarget(this.baseRenderTarget);
1148
- this.renderer.clear();
1149
- this.renderer.render(this.scene, this.getCamera());
1150
- this.renderer.setRenderTarget(null);
1151
- this.renderer.clear();
1152
- this.renderRendererTarget(this.baseRenderTarget);
1153
- const blob = await new Promise((resolve, reject) => {
1154
- this.renderer.domElement.toBlob((blob) => {
1155
- resolve(blob);
1156
- }, "image/png", 1);
1157
- });
1158
- return blob;
1159
- }
1160
- //获取渲染图base64
1161
- getThumbBase64() {
1162
- this.renderer.setRenderTarget(this.baseRenderTarget);
1163
- this.renderer.clear();
1164
- this.renderer.render(this.scene, this.getCamera());
1165
- this.renderer.setRenderTarget(null);
1166
- this.renderer.clear();
1167
- this.renderRendererTarget(this.baseRenderTarget);
1168
- const base64 = this.renderer.domElement.toDataURL("image/png");
1169
- return base64;
1170
- }
1171
- //获取渲染图文件对象
1172
- async getThumbFile() {
1173
- const blob = await this.getThumbBlob();
1174
- const file = new File([blob], "thumb.png", { type: blob.type });
1175
- return file;
1176
- }
1177
- // 下载渲染图
1178
- async download(fileName = "download") {
1179
- const blob = await this.getThumbBlob();
1180
- let saveLink = document.createElement("a");
1181
- saveLink.download = `${fileName}.png`;
1182
- saveLink.href = URL.createObjectURL(blob);
1183
- document.body.append(saveLink);
1184
- saveLink.click();
1185
- }
1186
- //把渲染目标渲染到canvas上
1187
- renderRendererTarget(renderTarget) {
1188
- // FXAA 分辨率按实际帧缓冲像素尺寸(乘 devicePixelRatio)设置,否则高分屏下
1189
- // 采样步长被当成 CSS 像素,FXAA 失效甚至引入摩尔纹。
1190
- const dpr = Math.max(window.devicePixelRatio, this.minDevicePixelRatio);
1191
- baseTextureToRenderShader.uniforms.resolution.value.set(1 / (this.size.x * dpr), 1 / (this.size.y * dpr));
1192
- baseTextureToRenderShader.uniforms.tDiffuse.value = renderTarget.texture;
1193
- fsQuad.render(this.renderer, baseTextureToRenderShader);
1194
- }
1195
- // 渲染纹理
1196
- renderTexture(texture, alpha = false) {
1197
- copyShader.uniforms.map.value = texture;
1198
- copyShader.uniforms.opacity.value = 1.0;
1199
- copyShader.blending = alpha ? NoBlending : NormalBlending;
1200
- fsQuad.render(this.renderer, copyShader);
1201
- }
1202
- }
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2021-12-20 13:38:22
4
+ * @Last Modified by: xiaosihan
5
+ * @Last Modified time: 2025-01-28 04:26:41
6
+ */
7
+ import { round } from "lodash";
8
+ import { AxesHelper, Box3, DepthFormat, DepthTexture, DoubleSide, EventDispatcher, GridHelper, HalfFloatType, LinearFilter, Line, Mesh, MeshNormalMaterial, MOUSE, NearestFilter, NoBlending, NoColorSpace, NormalBlending, OrthographicCamera, PerspectiveCamera, Points, RGBAFormat, RGBFormat, FloatType, UnsignedByteType, NoToneMapping, Scene, SRGBColorSpace, TOUCH, UnsignedIntType, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
9
+ import { MathUtils } from "three";
10
+ const { degToRad, generateUUID } = MathUtils;
11
+ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
12
+ import ThreeMouse from "./ThreeMouse";
13
+ import InstancedGroup from "./instanced/InstancedGroup";
14
+ import Transition from "./Transition";
15
+ import bloom_pass from "./pass/bloom_pass";
16
+ import cover_pass from "./pass/cover_pass";
17
+ import frosted_glass_pass from "./pass/frosted_glass_pass";
18
+ import { flushLensflareOverlays } from "./lensflare/LensFlare";
19
+ import glow_pass from "./pass/glow_pass";
20
+ import stroke_pass from "./pass/stroke_pass";
21
+ import ssao_pass, { renderAoDepthPass, renderExcludedMaskPass, visibleDepthOptions } from "./pass/ssao_pass";
22
+ import screenBloomPass from "./pass/screen_bloom_pass";
23
+ import copyShader from "./shaders/copyShader";
24
+ import { baseTextureToRenderShader } from "./shaders/baseTextureToRenderShader";
25
+ import baseDepthVisShader from "./shaders/baseDepthVisShader";
26
+ import debugDepthCompareShader from "./shaders/debugDepthCompareShader";
27
+ import debugDepthJudgeShader from "./shaders/debugDepthJudgeShader";
28
+ import debugExcludedShader from "./shaders/debugExcludedShader";
29
+ import { debugDepthState } from "./pass/debugDepthState";
30
+ import { WaterFlow } from "./WaterFlow/WaterFlow";
31
+ import waterFlowPass from "./WaterFlow/waterFlowPass";
32
+ import threeLoader from "./threeLoader";
33
+ import threeUtils from "./threeUtils";
34
+ import fsQuad from "./pass/fsQuad";
35
+ /** OrbitControls 阻尼基准值(等价于 60fps 下的单帧衰减比例)。
36
+ * render 循环会按真实帧间隔把它换算成「帧率无关」的单帧衰减比例(见下方 controls.update 处)。
37
+ * 取值历史:0.05(1826ab1 验证过"加阻尼也不晃"的正常值,配合 rotateSpeed=0.2)→
38
+ * a9f3ec9 把 rotateSpeed 提到 1.0 才导致"快速晃动中心点飘",与阻尼无关;该次顺手把阻尼关成 false 诊断。
39
+ * 本轮复原:enableDamping=true + dampingFactor=0.05(回到不飘配置),rotateSpeed 折中 0.5(0.2 跟手、
40
+ * 1.0 易飘)。若 0.5 仍飘,降到 0.2 完全复原 1826ab1;若想更跟手,升到 0.3~0.4 之间试。 */
41
+ const CONTROLS_DAMPING_BASE = 0.05;
42
+ // three.js 的基础对象
43
+ export default class ThreeBase extends EventDispatcher {
44
+ constructor() {
45
+ super();
46
+ // 资源加载进度有更新时, 需要渲染
47
+ threeLoader.addEventListener("loadding", () => {
48
+ this.dispatchEvent({ type: "loadding" });
49
+ setTimeout(() => {
50
+ this.needRender = true;
51
+ }, 100);
52
+ });
53
+ threeLoader.addEventListener("onLoad", () => {
54
+ this.dispatchEvent({ type: "onLoad" });
55
+ setTimeout(() => {
56
+ this.needRender = true;
57
+ }, 100);
58
+ });
59
+ // 固定的实例化分组器挂进场景,并注入到鼠标事件系统用于 InstancedMesh 命中路由
60
+ this.scene.add(this.instancedGroup);
61
+ this.threeMouse.instancedGroup = this.instancedGroup;
62
+ this.init();
63
+ }
64
+ // 设置最小分辨率
65
+ minDevicePixelRatio = 1;
66
+ // 实际帧缓冲像素比硬上限:SSAA 超采样可能把总像素比推得过高导致卡顿,
67
+ // 这里硬性限制 effectiveRatio 不超过该值(无论设备像素比或 renderScale 多大)。
68
+ // 高 dpr 屏(如 2×)下 effectiveRatio=2 会让整条后处理链在 2× 分辨率跑,快速晃动掉帧顿挫、
69
+ // 像素抖动(用户感知为"飘");收紧到 1(高 dpr 屏 base RT 最多 1×2=2× 每维)进一步压中间 RT 填充率。注意 renderer 自身
70
+ // setPixelRatio 仍按 dpr 输出最终屏幕,这里只压中间 RT 计算量,不影响最终清晰度。
71
+ maxPixelRatio = 1;
72
+ // 超采样倍数(SSAA):在帧缓冲分辨率基础上再乘 renderScale 渲染,
73
+ // 最后由 FXAA 降采样输出到屏幕。renderScale>1 可消除"纹理缩小产生的摩尔纹"——
74
+ // MSAA 只处理几何边缘、管不到着色器内的纹理采样;SSAA 才是纹理摩尔纹的治本手段
75
+ // (纹理被欠采样时的拍频,必须靠提高实际采样率才能消除)。
76
+ // 1 = 关闭超采样(仅设备 pixelRatio);2 = 2 倍超采样(质量最高,片元开销 ×4,需强 GPU)。
77
+ renderScale = 1;
78
+ // 全局超采样/分辨率倍率(SSAA):base / 深度 / 法线 / 效果 mask / 屏幕合成 全部按
79
+ // effectiveRatio × resolution 内部超采样渲染,终处理 FXAA 降采样输出到屏幕。
80
+ // 全链路同倍率 → 各 RT 严格 1:1 对齐:几何锯齿、效果边缘锯齿、辉光漏光(深度 vs mask 错位)、
81
+ // mask 合成 UV 缩放错位、SSAO 深度错位变黑 一并消除。默认 2 = 每维 2×、总像素 4×;
82
+ // 代价全链路填充 ×4(base + 深度 + 法线 + 后处理 SSAO/cover/frosted)。要省性能整体降 1
83
+ // (关超采样,仅 FXAA 兜底,几何硬边锯齿回弹)。历史坑:baseRenderScale/auxRenderScale/
84
+ // effectRenderScale 三参各自为政,倍率不同步必错位,故统一为单参数 resolution 管全局。
85
+ // ⚠️ 必须用 getter/setter,不能是普通字段:所有 RT 尺寸只在 updateRenderTargetSizes()
86
+ // 里按 effectiveRatio × resolution 计算,而该方法过去只被 setSize()(容器尺寸变化)调用
87
+ // → 运行时改 resolution 不会重建任何 RT,画面毫无变化(表现为"调分辨率没感觉")。
88
+ // 故 setter 里改动倍率后立刻重建全部 RT。
89
+ _resolution = 2;
90
+ get resolution() {
91
+ return this._resolution;
92
+ }
93
+ set resolution(value) {
94
+ // 夹到合理区间:0 或负值会让 RT 尺寸为 0,WebGL 报 framebuffer incomplete
95
+ const next = Math.min(4, Math.max(0.25, Number.isFinite(value) ? value : 2));
96
+ if (next === this._resolution) {
97
+ return;
98
+ }
99
+ this._resolution = next;
100
+ // 尚未布局(size 为 0)时跳过,等首次 setSize 时自然会带上新倍率
101
+ if (this.size.x > 0 && this.size.y > 0) {
102
+ this.updateRenderTargetSizes(this.size.x, this.size.y);
103
+ }
104
+ }
105
+ /**
106
+ * 终处理抖动强度(消除 8bit 输出量化的色带,见 baseTextureToRenderShader)。
107
+ * 1.0 = 标准 ±0.5 LSB(默认,肉眼无感但能打散色带);0 = 完全关闭。
108
+ * ⚠️ 若某处柔和渐变(外辉光/天空)看着"发脏/有颗粒",先把它设 0 做 A/B 对比:
109
+ * 0 时若色带回来,说明抖动是必需的,应调小到 0.5 而不是关掉。
110
+ */
111
+ get ditherStrength() {
112
+ return baseTextureToRenderShader.uniforms.uDitherStrength.value;
113
+ }
114
+ set ditherStrength(value) {
115
+ const next = Math.max(0, Number.isFinite(value) ? value : 1);
116
+ baseTextureToRenderShader.uniforms.uDitherStrength.value = next;
117
+ }
118
+ /**
119
+ * 渲染目标(RT)底层配置(运行时可调,改即重建 base / screen 两个主颜色 RT)。
120
+ * 皇上要的「RT 上面的配置」= type / format / colorSpace / filter;
121
+ * 默认与原始硬编码一致(HalfFloatType + RGBAFormat + 线性 NoColorSpace + Linear 滤波)。
122
+ * 调这些能直观复现/消除典型问题:
123
+ * - type=UnsignedByteType(8bit 线性):柔和渐变被压成 3~6 色阶 → 同心环色带(banding)
124
+ * - colorSpace=SRGBColorSpace:RT 已按 sRGB 编码 + 终处理再编一次 → 双重编码发白
125
+ * - filter=NearestFilter:颜色插值变最近邻 → 边缘像素化/糊
126
+ */
127
+ _rtOptions = {
128
+ type: HalfFloatType,
129
+ colorSpace: NoColorSpace,
130
+ format: RGBAFormat,
131
+ filter: LinearFilter,
132
+ };
133
+ /** 面板/代码里的字符串名 → THREE 常量(单一映射点,避免散落硬编码数字) */
134
+ static rtNameToConstant(key, name) {
135
+ switch (key) {
136
+ case "type":
137
+ return { HalfFloat: HalfFloatType, Byte: UnsignedByteType, Float: FloatType }[name] ?? HalfFloatType;
138
+ case "colorSpace":
139
+ return { Linear: NoColorSpace, SRGB: SRGBColorSpace }[name] ?? NoColorSpace;
140
+ case "format":
141
+ return { RGBA: RGBAFormat, RGB: RGBFormat }[name] ?? RGBAFormat;
142
+ case "filter":
143
+ return { Linear: LinearFilter, Nearest: NearestFilter }[name] ?? LinearFilter;
144
+ }
145
+ }
146
+ /**
147
+ * 运行时改 RT 底层配置:更新 _rtOptions 后重建 base / screen 两个主颜色 RT(保留当前尺寸)。
148
+ * 必须在已布局(size>0)后才重建;否则等下次 setSize 自然带新选项。
149
+ * 仅重建主颜色 RT(base/screen);深度/法线/aoDepth 是辅助通道,其 type 由对应 depthTexture 决定,
150
+ * 不在面板暴露,避免破坏深度比对 / SSAO 深度黑。
151
+ */
152
+ applyRenderTargetOption(key, name) {
153
+ switch (key) {
154
+ case "type":
155
+ this._rtOptions.type = ThreeBase.rtNameToConstant("type", name);
156
+ break;
157
+ case "colorSpace":
158
+ this._rtOptions.colorSpace = ThreeBase.rtNameToConstant("colorSpace", name);
159
+ break;
160
+ case "format":
161
+ this._rtOptions.format = ThreeBase.rtNameToConstant("format", name);
162
+ break;
163
+ case "filter":
164
+ this._rtOptions.filter = ThreeBase.rtNameToConstant("filter", name);
165
+ break;
166
+ }
167
+ this.rebuildRenderTargets();
168
+ }
169
+ rebuildRenderTargets() {
170
+ if (this.size.x <= 0 || this.size.y <= 0) {
171
+ return;
172
+ }
173
+ const w = Math.max(1, Math.round(this.size.x * this.effectiveRatio * this._resolution));
174
+ const h = Math.max(1, Math.round(this.size.y * this.effectiveRatio * this._resolution));
175
+ const opts = {
176
+ minFilter: this._rtOptions.filter,
177
+ magFilter: this._rtOptions.filter,
178
+ format: this._rtOptions.format,
179
+ type: this._rtOptions.type,
180
+ stencilBuffer: false,
181
+ anisotropy: 16,
182
+ };
183
+ this.baseRenderTarget.dispose();
184
+ this.baseRenderTarget = new WebGLRenderTarget(w, h, { ...opts });
185
+ this.baseRenderTarget.texture.colorSpace = this._rtOptions.colorSpace;
186
+ this.screenRenderTarget.dispose();
187
+ this.screenRenderTarget = new WebGLRenderTarget(w, h, { ...opts });
188
+ this.screenRenderTarget.texture.colorSpace = this._rtOptions.colorSpace;
189
+ this.needRender = true;
190
+ }
191
+ // 效果 mask 像素比硬上限:避免高 dpr 屏(如 2×)上 effectiveRatio×resolution 爆到 4×,
192
+ // 让效果 mask RT(含辉光多次高斯迭代)填充量失控。经验值 2×:相对设备 dpr 输出足够平滑,
193
+ // 且把"效果 mask 超采样"的填充量控制在场景同级的 2× 以内(原本 3× 在高 dpr 屏是"还是卡"的高嫌疑点)。
194
+ effectMaxPixelRatio = 2;
195
+ // 超采样倍率已统一为上方 resolution 字段(base / 深度 / 法线 / mask / 屏幕全部取它)。
196
+ // 辅助通道(深度/法线)分辨率系数。
197
+ // 关键:描边/发光/辉光/毛玻璃都拿 baseDepthTexture 做遮挡比对(deathTestShader 以全分辨率
198
+ // UV 采样半分辨率深度,并与全分辨率物体深度做硬阈值比较)。半分辨率深度(Nearest 滤波)被
199
+ // 放大采样后与锐利的物体深度在边缘逐像素翻转 discard,产生边缘摩尔纹/锯齿——这是这 4 个特效
200
+ // 共同的摩尔纹根因。故深度必须与 mask 同分辨率(全分辨率),1:1 采样才无放大错位。
201
+ // 法线仅 SSAO 使用(首页默认关闭、不渲染),全分辨率无额外开销。
202
+ // 之前为"不卡"降到 0.5 反而引入摩尔纹,已恢复为 1。
203
+ // 辅助通道(深度/法线)倍率已并入 resolution,不再单独设 auxRenderScale。
204
+ // 实际帧缓冲像素比 = min( max(devicePixelRatio, minDevicePixelRatio) * renderScale, maxPixelRatio )
205
+ get effectiveRatio() {
206
+ return Math.min(Math.max(window.devicePixelRatio, this.minDevicePixelRatio) * this.renderScale, this.maxPixelRatio);
207
+ }
208
+ // 基础深度纹理
209
+ baseDepthTexture = (() => {
210
+ const baseDepthTexture = new DepthTexture(4096, 4096);
211
+ baseDepthTexture.format = DepthFormat;
212
+ // 升级为 24/32 位深度(UnsignedIntType),相比原 16 位(UnsignedShortType)
213
+ // 能显著提升对数深度在远场的精度,消除 SSAO 与深度比对的 banding 瑕疵。
214
+ baseDepthTexture.type = UnsignedIntType;
215
+ baseDepthTexture.anisotropy = 16;
216
+ return baseDepthTexture;
217
+ })();
218
+ // 基础纹理渲染对象(无 MSAA):抗锯齿统一由终处理 FXAA 承担。
219
+ // ⚠️ 必须保持与 depthRenderTarget 同样的「无 MSAA」策略——颜色 MSAA 会让物体边缘平滑成
220
+ // 半透明渐入,而 baseDepth(无 MSAA 硬边缘)在相交像素仍记完整深度,二者错位会导致
221
+ // depthTestShader 判定边界与人眼视觉不一致,描边/辉光在遮挡边界呈现锯齿状闪缩。
222
+ // 统一无 MSAA 后颜色与深度边缘严格对齐,边缘抗锯齿改由最终 FXAA 兜底(见 renderRendererTarget)。
223
+ // ⚠️ type 必须是 HalfFloatType(16bit 浮点),不能用默认的 UnsignedByteType(8bit):
224
+ // 本 RT 存的是「线性」颜色(NoColorSpace),而 8bit 线性空间在暗部码值极度稀疏
225
+ // (linear 码值 0/1/2/3 对应 sRGB 0/12.7/21.7/28.2,相邻码值跨度肉眼可见),
226
+ // 导致柔和渐变(外辉光、天空、雾、bloom 衰减)被压成 3~6 个色阶 → 呈现"一层一层"色带。
227
+ // 实测(2026-09-06 SpriteText3 外辉光 42px 扫描线):
228
+ // 8bit linear:R 通道 42px 内仅 3 个色阶(每 14px 一跳)、G 通道 6 个色阶
229
+ // sRGB 8bit :B 通道 42px 内 42 个色阶(每像素一阶,完全平滑)
230
+ // 改半浮点后渐变不再在中间量化,终处理仍只做一次 sRGB 编码(见 baseTextureToRenderShader)。
231
+ baseRenderTarget = (() => {
232
+ const baseRenderTarget = new WebGLRenderTarget(128, 128, {
233
+ minFilter: LinearFilter,
234
+ magFilter: LinearFilter,
235
+ format: RGBAFormat,
236
+ type: HalfFloatType,
237
+ stencilBuffer: false,
238
+ anisotropy: 16,
239
+ });
240
+ return baseRenderTarget;
241
+ })();
242
+ /** 可选的屏幕空间水流对象(WaterFlow)。默认 null;调用 enableWaterFlow() 后创建。启用时
243
+ * 每帧消费 baseRenderTarget/baseDepthTexture,把水面合成进后处理链(不额外渲染主场景)。 */
244
+ waterFlow = null;
245
+ _waterFlowLastTime = 0;
246
+ waterFlowElapsed = 0;
247
+ // 调试用:被排除面 mask 渲染目标(仅「排除面标记」调试档使用,尺寸随主渲染目标同步)
248
+ excludedMaskRenderTarget = new WebGLRenderTarget(128, 128, {
249
+ minFilter: LinearFilter,
250
+ magFilter: LinearFilter,
251
+ format: RGBAFormat,
252
+ stencilBuffer: false,
253
+ depthBuffer: false,
254
+ });
255
+ // 深度专用渲染目标(无 MSAA,深度值稳定,供后处理深度比对使用)
256
+ depthRenderTarget = new WebGLRenderTarget(128, 128, {
257
+ minFilter: LinearFilter,
258
+ magFilter: LinearFilter,
259
+ format: RGBAFormat,
260
+ stencilBuffer: false,
261
+ depthTexture: this.baseDepthTexture,
262
+ });
263
+ // AO 深度纹理(含地板):**不再单独渲染一遍** —— 它挂在 normalRenderTarget 上当 depth attachment,
264
+ // 法线 pass(renderAoNormalPass,includeFloor=true)渲染时深度由光栅器顺带写入,白拿一张含地板深度。
265
+ // (2026-09-14 优化:删掉原 ssao_pass 内那次 renderAoDepthPass(true),每帧省一次全场景遍历。)
266
+ // SSAO 的 GTAO 采样它(地板保留为实体 → 接触 AO),合成时再与 depthRenderTarget
267
+ // (地板排除 = 可见深度)比对,作废"透过半透明地板看到的实体"上的地板接触 AO。
268
+ // 见 three-base/pass/ssao_pass.ts 的复合守卫说明。
269
+ aoDepthTexture = (() => {
270
+ const t = new DepthTexture(4096, 4096);
271
+ t.format = DepthFormat;
272
+ t.type = UnsignedIntType;
273
+ t.anisotropy = 16;
274
+ return t;
275
+ })();
276
+ /**
277
+ * 调试用:深度预览模式。由面板「深度调试」切换,便于逐一看各深度图排查"深度测试全部失效"根因。
278
+ * - "none" :正常渲染(默认)
279
+ * - "global" :仅预览全局深度图(baseDepthTexture,排除地板)。全白=深度为空→renderAoDepthPass 没写进深度。
280
+ * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入的含地板深度)。用于确认深度管线本身是否工作。
281
+ * - "compare" :R=全局 base 深度,G=最近一次 glow/stroke 选区 select 深度。两路任一全白即该侧写入失败。
282
+ * - "excluded":深度图灰度 + 「被排除出深度图的面」叠红(红=该面写不进深度,永远挡不住后处理)。
283
+ * 物体区域(G 暗)若 B 全黑 → 判定从不触发;再看该区 R/G 谁大即可定位是谁缺深度。
284
+ * 兼容旧控制台入口:设置 `window.__BASE_DEPTH_VIS = true` 等价于切到 "global"。
285
+ * 通用控制台入口:任意页面都可 `window.__TB_DEBUG_DEPTH = "judge"`(优先级高于面板属性)。
286
+ */
287
+ debugDepthPreview = "none";
288
+ /**
289
+ * 可见深度图是否包含「标了 userData.aoFloor 的地板」(如 Ground1)。
290
+ *
291
+ * 默认 false —— 原设计:地板可透视,透过地板看到的物体照常显示(不被地板挡住)。
292
+ * 置 true 后地板会写进 baseDepthTexture,**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
293
+ * 排查"怎么都挡不住"时先开这个试一下:若开了就正常,说明挡你的正是可透视地板。
294
+ */
295
+ get baseDepthIncludeFloor() {
296
+ return visibleDepthOptions.includeFloor;
297
+ }
298
+ set baseDepthIncludeFloor(v) {
299
+ visibleDepthOptions.includeFloor = v;
300
+ this.needRender = true;
301
+ }
302
+ // 法线专用渲染目标(无 MSAA,存储视图空间法线,供 GTAO 风格 SSAO 使用真实法线)
303
+ normalRenderTarget = new WebGLRenderTarget(128, 128, {
304
+ minFilter: LinearFilter,
305
+ magFilter: LinearFilter,
306
+ format: RGBAFormat,
307
+ stencilBuffer: false,
308
+ // ⚠️ 挂 depthTexture:法线 pass 渲染时深度由光栅器顺带写进来,直接就是「含地板」的 AO 深度
309
+ // (零成本,无需再单独渲一遍深度 pass)。见上方 aoDepthTexture 注释。
310
+ depthTexture: this.aoDepthTexture,
311
+ });
312
+ // 屏幕合成目标(无 MSAA):base(含 SSAO + 描边/发光/bloom)拷进来后,
313
+ // 颜色叠加/磨砂玻璃/水流/屏幕辉光 在此目标上叠加,最后由 FXAA 终处理输出到屏幕。
314
+ // 不挂 MSAA(FXAA 负责边缘)。
315
+ // ⚠️ 注意:SSAO 不在本目标上做——2026-09-14 起 SSAO 提前到对象级特效之前、直接乘进 baseRenderTarget
316
+ // (见 render() 内注释与 test/depthDebug.test.js K 组护栏)。
317
+ // 与 baseRenderTarget 同为线性(NoColorSpace):后处理全程在线性空间做颜色混合,
318
+ // 仅由终处理 baseTextureToRenderShader 用 sRGBTransferOETF 做一次 sRGB 编码输出屏幕,
319
+ // 避免「RT 已按 sRGB 编码 + 终处理再编一次」导致的双重编码发白。
320
+ // ⚠️ 同 baseRenderTarget:必须为 HalfFloatType。SSAO/颜色叠加/磨砂玻璃都在本目标上做线性混合,
321
+ // 若这里是 8bit,前面 base 用半浮点保住的渐变会在这一步重新被量化成色带(前功尽弃)。
322
+ screenRenderTarget = new WebGLRenderTarget(128, 128, {
323
+ minFilter: LinearFilter,
324
+ magFilter: LinearFilter,
325
+ format: RGBAFormat,
326
+ type: HalfFloatType,
327
+ stencilBuffer: false,
328
+ });
329
+ // 基础纹理 -> 屏幕合成目标的纯拷贝(关掉深度,仅透传颜色),用于把 base 图像拷入
330
+ // screenRenderTarget 供后续后处理叠加。克隆自共享 copyShader,避免改动其状态。
331
+ screenCopyShader = (() => {
332
+ const shader = copyShader.clone();
333
+ shader.depthTest = false;
334
+ shader.depthWrite = false;
335
+ shader.transparent = false;
336
+ return shader;
337
+ })();
338
+ // 法线材质(MeshNormalMaterial 输出视图空间法线 pack 到 RGB,与 SSAO 的 unpackRGBToNormal 对应)
339
+ // ⚠️ side 必须 DoubleSide:本材质还兼职产出「含地板 AO 深度」(挂在 normalRenderTarget 上),
340
+ // 若只渲正面,DoubleSide / 背面朝向的几何会丢深度,与被它取代的那次独立深度 pass 行为不一致。
341
+ // 深度测试会让最近面胜出,故法线图内容不会因 DoubleSide 变差。
342
+ normalMaterial = new MeshNormalMaterial({ side: DoubleSide });
343
+ // 公用渲染目标1(无 MSAA:超采样已收敛到基础纹理与选中目标,其余通道去掉以提速)
344
+ renderTarget = new WebGLRenderTarget(128, 128, {
345
+ minFilter: LinearFilter,
346
+ magFilter: LinearFilter,
347
+ format: RGBAFormat,
348
+ });
349
+ // 渲染目标2(无 MSAA)
350
+ renderTarget2 = new WebGLRenderTarget(128, 128, {
351
+ minFilter: LinearFilter,
352
+ magFilter: LinearFilter,
353
+ format: RGBAFormat,
354
+ });
355
+ // 公用二维向量
356
+ tempVector2 = new Vector2();
357
+ // 公用的三维向量
358
+ tempVector3 = new Vector3();
359
+ //渲染器的尺寸
360
+ size = new Vector2(0, 0);
361
+ //上下文丢失
362
+ rendererLost = false;
363
+ // 渲染器对象
364
+ renderer = (() => {
365
+ const renderer = new WebGLRenderer({
366
+ alpha: true, // 透明背景
367
+ precision: "highp", //highp/mediump/lowp着色精度选择
368
+ logarithmicDepthBuffer: true, // 对数深度缓冲区 避免当2个面挨的很近时闪烁
369
+ preserveDrawingBuffer: true, // 是否保存绘图缓冲
370
+ stencil: false, // 模版
371
+ powerPreference: 'high-performance' // 高性能模式
372
+ });
373
+ renderer.domElement.id = generateUUID();
374
+ //监听上下文的销毁事件
375
+ renderer.domElement.addEventListener("webglcontextlost", (event) => {
376
+ this.rendererLost = true;
377
+ }, false);
378
+ // 监听上下文的恢复事件
379
+ renderer.domElement.addEventListener("webglcontextrestored", (event) => {
380
+ this.rendererLost = false;
381
+ }, false);
382
+ renderer.localClippingEnabled = true;
383
+ renderer.setPixelRatio(Math.max(window.devicePixelRatio, this.minDevicePixelRatio)),
384
+ renderer.autoClear = false;
385
+ renderer.clear();
386
+ renderer.outputColorSpace = SRGBColorSpace;
387
+ // 颜色空间:图片什么颜色、3D 里就是什么颜色(sRGB 直出,所见即所得,不发白不褪色)。
388
+ // 关闭色调映射后,场景渲染进线性 baseRenderTarget 时不做 tonemap 压缩,终处理只做一次 sRGB
389
+ // 编码(baseTextureToRenderShader 手写 sRGBTransferOETF),全链路无额外提亮/降饱和,
390
+ // 颜色与原贴图一致。如需电影感可改回 ACESFilmicToneMapping(但会带来发白/褪色观感)。
391
+ renderer.toneMapping = NoToneMapping;
392
+ threeLoader.setAnisotropy(renderer.capabilities.getMaxAnisotropy());
393
+ return renderer;
394
+ })();
395
+ //透视相机
396
+ perspectiveCamera = new PerspectiveCamera(45, 1, 0.01, 20000);
397
+ //正交相机
398
+ orthographicCamera = new OrthographicCamera(-1, 1, 1, -1, 0.01, 20000);
399
+ //相机的类型 有透视相机和正交相机
400
+ cameraType = "perspectiveCamera";
401
+ setCameraType(cameraType) {
402
+ if (this.cameraType !== cameraType) {
403
+ this.cameraType = cameraType;
404
+ if (this.cameraType === "perspectiveCamera") {
405
+ this.threeMouse._camera = this.perspectiveCamera;
406
+ this.controls.object = this.perspectiveCamera;
407
+ }
408
+ else if (this.cameraType === "orthographicCamera") {
409
+ this.threeMouse._camera = this.orthographicCamera;
410
+ this.controls.object = this.orthographicCamera;
411
+ }
412
+ }
413
+ }
414
+ //获取当前使用的的相机
415
+ getCamera() {
416
+ switch (this.cameraType) {
417
+ case "orthographicCamera":
418
+ return this.orthographicCamera;
419
+ case "perspectiveCamera":
420
+ default:
421
+ return this.perspectiveCamera;
422
+ }
423
+ }
424
+ // 场景对象
425
+ scene = (() => {
426
+ const scene = new Scene();
427
+ return scene;
428
+ })();
429
+ // 这个方法等效 this.scene.add
430
+ // 想少写2个字母就用这个方法
431
+ add(...object) {
432
+ this.scene.add(...object);
433
+ return this;
434
+ }
435
+ ///鼠标事件对象
436
+ threeMouse = new ThreeMouse(this.renderer, this.perspectiveCamera, this.scene);
437
+ // 轨道控制器
438
+ controls = (() => {
439
+ const controls = new OrbitControls(this.perspectiveCamera, this.renderer.domElement);
440
+ controls.addEventListener("change", () => {
441
+ this.needRender = true;
442
+ });
443
+ // 转速:git 历史实证——以前(1826ab1)rotateSpeed=0.2 + enableDamping=true + dampingFactor=0.05
444
+ // 时「加阻尼也不晃」;a9f3ec9 把 rotateSpeed 提到 1.0(快 5 倍)才是"快速晃动中心点飘"的真因,
445
+ // 与阻尼无关。此处折中到 0.5:比 0.2 跟手、比 1.0 不易飘。若仍飘可降到 0.2 完全复原。
446
+ controls.rotateSpeed = 0.5;
447
+ controls.maxPolarAngle = Math.PI;
448
+ // 阻尼:回到以前不飘的配置(enableDamping=true + dampingFactor=0.05)。
449
+ // 0.05 是 1826ab1 验证过"加阻尼不晃"的值,配合下面 render 循环的帧率无关换算。
450
+ controls.enableDamping = true;
451
+ controls.dampingFactor = CONTROLS_DAMPING_BASE;
452
+ controls.screenSpacePanning = true;
453
+ return controls;
454
+ })();
455
+ // 帧率无关阻尼用的上一帧时间戳(performance.now() 毫秒),初始 0 表示首帧用 60fps 基准
456
+ _lastControlsTime = 0;
457
+ // 固定的实例化分组器:自动把所有 InstancedChild 合并进 InstancedMesh 批量渲染
458
+ instancedGroup = new InstancedGroup();
459
+ /**
460
+ * 自定义描边目标:不在 scene 内的独立对象(如 InstancedChild 的"选择代理"),
461
+ * 渲染循环在效果阶段单独对其调用 stroke_pass,使其获得与普通 Mesh 一致的描边,
462
+ * 且不会被正常 base pass 重复绘制(因为它不在 scene 树内)。
463
+ * 典型用途:InstancedChild / 一组 InstancedChild 没有自身几何,无法直接描边,
464
+ * 用 buildSelectProxy 重建其几何代理后再注册到这里即可。
465
+ */
466
+ _strokeObjects = [];
467
+ /** 注册一个自定义描边对象(效果阶段对其描边) */
468
+ addStrokeObject(object) {
469
+ if (this._strokeObjects.indexOf(object) < 0) {
470
+ this._strokeObjects.push(object);
471
+ }
472
+ return this;
473
+ }
474
+ /** 移除自定义描边对象 */
475
+ removeStrokeObject(object) {
476
+ const i = this._strokeObjects.indexOf(object);
477
+ if (i >= 0) {
478
+ this._strokeObjects.splice(i, 1);
479
+ }
480
+ return this;
481
+ }
482
+ // 设置 dom 容器
483
+ async setContainer(div) {
484
+ if (div) {
485
+ div.style.overflow = "hidden";
486
+ div.innerHTML = "";
487
+ div.append(this.renderer.domElement);
488
+ if (this.rendererLost) {
489
+ this.renderer.forceContextRestore();
490
+ this.needRender = true;
491
+ setTimeout(() => {
492
+ this.needRender = true;
493
+ }, 100);
494
+ setTimeout(() => {
495
+ this.renderer.domElement.style.opacity = "1";
496
+ }, 200);
497
+ }
498
+ this.setSize(div.clientWidth, div.clientHeight);
499
+ // 开启时时渲染
500
+ threeUtils.removeEventListener("requestAnimationFrame", this.renderBindThis);
501
+ threeUtils.addEventListener("requestAnimationFrame", this.renderBindThis);
502
+ }
503
+ }
504
+ // 修改尺寸
505
+ setSize(width, height) {
506
+ // 防御容器未布局/隐藏导致的 0 尺寸:相机 aspect、renderer、RT 都不能接受 0,
507
+ // 否则 WebGL 会报 GL_INVALID_VALUE / Framebuffer incomplete: Attachment has zero size。
508
+ width = Math.max(1, Math.floor(width));
509
+ height = Math.max(1, Math.floor(height));
510
+ //修改透视相机的比例
511
+ this.perspectiveCamera.aspect = width / height;
512
+ this.perspectiveCamera.updateProjectionMatrix();
513
+ //修改正交相机的比例
514
+ // this.orthographicCamera.top = height / width;
515
+ // this.orthographicCamera.bottom = -height / width;
516
+ this.orthographicCamera.left = -width / height;
517
+ this.orthographicCamera.right = width / height;
518
+ this.renderer.setPixelRatio(Math.max(window.devicePixelRatio, this.minDevicePixelRatio)),
519
+ this.renderer.setSize(width, height);
520
+ // 所有 RT 尺寸按 effectiveRatio × resolution 重建(容器尺寸变化与 resolution 变化共用)
521
+ this.updateRenderTargetSizes(width, height);
522
+ this.size.set(width, height);
523
+ setTimeout(() => {
524
+ this.needRender = true;
525
+ }, 100);
526
+ // 渲染场景
527
+ // this.renderer.clear();
528
+ // this.renderer.render(this.scene, this.camera);
529
+ }
530
+ /**
531
+ * 按「容器尺寸 × effectiveRatio × resolution」重建所有渲染目标尺寸。
532
+ * 由 setSize()(容器尺寸变化)与 resolution setter(运行时调倍率)共用 ——
533
+ * 关键:resolution 是运行时可调的性能/质量旋钮,改完必须立刻重建 RT,
534
+ * 否则倍率变了而 RT 尺寸没变,画面毫无变化(历史坑:"调分辨率没感觉")。
535
+ */
536
+ updateRenderTargetSizes(width, height) {
537
+ // 防御 0 尺寸:RT 不接受 0,否则 WebGL 报 framebuffer incomplete
538
+ const w = Math.max(1, Math.round(width * this.effectiveRatio * this._resolution));
539
+ const h = Math.max(1, Math.round(height * this.effectiveRatio * this._resolution));
540
+ // 主画面:超采样渲染,纹理摩尔纹在此被根治
541
+ this.baseRenderTarget.setSize(w, h);
542
+ // 深度/法线辅助通道:与 base 同倍率,保证 depthTest 1:1 无漏光、SSAO 深度不黑。
543
+ // 其 depthTexture(baseDepthTexture) 尺寸由 renderer 在 setRenderTarget 时
544
+ // 强制同步为 depthRenderTarget 尺寸,下面再手动对齐一次避免首帧抖动。
545
+ this.depthRenderTarget.setSize(w, h);
546
+ this.normalRenderTarget.setSize(w, h);
547
+ this.excludedMaskRenderTarget.setSize(w, h);
548
+ // 屏幕合成目标:同样跟随 resolution,全链路 1:1 对齐
549
+ this.screenRenderTarget.setSize(w, h);
550
+ // 水流后处理目标跟随同倍率重建(pass 内部粒子 RT 仍按 scale 降采样,compositeTarget 全分辨率)
551
+ this.waterFlow?.resize(w, h);
552
+ this.baseDepthTexture.image.width = w;
553
+ this.baseDepthTexture.image.height = h;
554
+ // 立即渲染一帧,避免静态场景下改了倍率却停在旧画面
555
+ this.needRender = true;
556
+ }
557
+ // 视角距离过度
558
+ controlsDistanceTransition = (() => {
559
+ const controlsDistanceTransition = new Transition({ distance: 0 });
560
+ controlsDistanceTransition.setDuration(1000);
561
+ return controlsDistanceTransition;
562
+ })();
563
+ //控制器的Y值过度计算
564
+ controlsYTransition = (() => {
565
+ const controlsYTransition = new Transition({ y: 0 });
566
+ controlsYTransition.setDuration(1000);
567
+ return controlsYTransition;
568
+ })();
569
+ //控制器的XZ值过度计算
570
+ controlsXZTransition = (() => {
571
+ const controlsXZTransition = new Transition({ x: 0, z: 0 });
572
+ controlsXZTransition.setDuration(1000);
573
+ return controlsXZTransition;
574
+ })();
575
+ // 控制器的 朝向带缓动的计算的顶点对象
576
+ controlsCenter = (() => {
577
+ const controlsCenter = new Transition({ x: 0, y: 0, z: 0 });
578
+ controlsCenter.setDuration(1000);
579
+ controlsCenter.onChange(({ x, y, z }) => {
580
+ this.controls.target.set(x, y, z);
581
+ this.perspectiveCamera.lookAt(this.controls.target);
582
+ this.orthographicCamera.lookAt(this.controls.target);
583
+ this.needRender = true;
584
+ });
585
+ return controlsCenter;
586
+ })();
587
+ // 控制器的位置缓动的计算的顶点对象
588
+ controlsPosition = (() => {
589
+ const controlsPosition = new Transition({ x: 100, y: 100, z: 100 });
590
+ controlsPosition.setDuration(1000);
591
+ controlsPosition.onChange(({ x, y, z }) => {
592
+ const distance = this.controlsDistanceTransition.get("distance");
593
+ this.perspectiveCamera.position.set(x + 0.0001, y, z + 0.0001);
594
+ this.perspectiveCamera.lookAt(this.controls.target);
595
+ //计算正交相机的缓动
596
+ this.orthographicCamera.position
597
+ .set(x + 0.01, y, z + 0.01)
598
+ .sub(this.controls.target)
599
+ .setLength(5000)
600
+ .add(this.controls.target);
601
+ this.orthographicCamera.lookAt(this.controls.target);
602
+ this.needRender = true;
603
+ });
604
+ return controlsPosition;
605
+ })();
606
+ // 网格辅助线
607
+ gridHelper = (() => {
608
+ const gridHelper = new GridHelper(1000, 100, 0x303030, 0x303030);
609
+ if (threeUtils.isDev) {
610
+ this.scene.add(gridHelper);
611
+ }
612
+ return gridHelper;
613
+ })();
614
+ //坐标轴线
615
+ axesHelper = (() => {
616
+ const axesHelper = new AxesHelper(10000);
617
+ if (threeUtils.isDev) {
618
+ this.scene.add(axesHelper);
619
+ }
620
+ return axesHelper;
621
+ })();
622
+ init() {
623
+ // 屏幕空间水流:?waterflow=1 时自动启用(默认关闭,需显式 enableWaterFlow 或此 URL 参数)。
624
+ // 创建后每帧消费 base 深度 + 后处理合成画面,把水面合成进后处理链,不自渲染主场景。
625
+ try {
626
+ const params = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
627
+ if (params.get('waterflow') === '1') {
628
+ this.enableWaterFlow();
629
+ }
630
+ }
631
+ catch (_e) {
632
+ // 非浏览器环境(如 Node 测试/SSR)忽略
633
+ }
634
+ }
635
+ /**
636
+ * 启用屏幕空间水流(GPGPU 粒子流体)。默认关闭,需显式调用或携带 ?waterflow=1。
637
+ * 创建后每帧自动消费 base 深度 + 当前合成画面,把水面合成进后处理链,不自渲染主场景。
638
+ * @param options.size 粒子网格边长(size×size 粒子),越大越细腻、越费
639
+ * @param options.speed 射流初速
640
+ * @param options.aperture 喷嘴开度
641
+ * @param options.nozzle 喷流起点坐标 [x,y,z],默认 [0.58,1.55,0]
642
+ * @param options.jetDirection 喷流方向(轴向,会被归一化),默认 [1,0,0](沿 +X)
643
+ * @param options.collision 是否开启粒子与 basin/地面碰撞回弹,默认 true
644
+ */
645
+ enableWaterFlow(options = {}) {
646
+ if (this.waterFlow) {
647
+ console.warn('[ThreeBase] WaterFlow 已启用,重复调用无效');
648
+ return;
649
+ }
650
+ const { size = 200, speed = 2.4, aperture = 0.78, nozzle, jetDirection, collision = true, } = options;
651
+ const waterFlow = new WaterFlow(this.renderer, {
652
+ size,
653
+ speed,
654
+ aperture,
655
+ nozzle: nozzle ? new Vector3(nozzle[0], nozzle[1], nozzle[2]) : undefined,
656
+ jetDirection: jetDirection ? new Vector3(jetDirection[0], jetDirection[1], jetDirection[2]) : undefined,
657
+ collision,
658
+ });
659
+ if (!waterFlow.supported) {
660
+ console.warn('[ThreeBase] 当前环境不支持浮点纹理,WaterFlow 未启用');
661
+ waterFlow.dispose();
662
+ return;
663
+ }
664
+ // 创建 pass 并注入基础纹理(externalBase 模式);后续每帧 renderInto 会刷新注入的纹理。
665
+ // ⚠️ 2026-10-08(WaterFlow 全量 TS 化):ScreenSpaceFluidPass 内部按透视相机建模
666
+ // (setSize 读 camera.fov 算 pointScale、depthMaterial 走 projectionMatrix 重建深度),
667
+ // 故此处显式窄化。getCamera() 在 cameraType==="orthographicCamera" 时会返回
668
+ // OrthographicCamera —— 原 JS 同样会把 fov=undefined 传进去(pointScale 变 NaN),
669
+ // 本次**只补类型、未改运行时行为**,保持与线上一致。
670
+ waterFlow.createPass(this.scene, this.getCamera(), {
671
+ colorTexture: this.baseRenderTarget.texture,
672
+ depthTexture: this.baseDepthTexture,
673
+ depthMode: 'logarithmic',
674
+ });
675
+ // 首帧按当前容器尺寸对齐(后续 setSize/resolution 变化由 updateRenderTargetSizes 兜底)。
676
+ const w = Math.max(1, Math.round(this.size.x * this.effectiveRatio * this._resolution));
677
+ const h = Math.max(1, Math.round(this.size.y * this.effectiveRatio * this._resolution));
678
+ if (this.size.x > 1 && this.size.y > 1)
679
+ waterFlow.resize(w, h);
680
+ this.waterFlow = waterFlow;
681
+ this._waterFlowLastTime = 0;
682
+ this.waterFlowElapsed = 0;
683
+ console.info('[ThreeBase] WaterFlow 已启用');
684
+ }
685
+ /** 运行时改喷流起点(喷嘴)坐标(WaterFlow 已启用时有效) */
686
+ setWaterFlowNozzle(x, y, z) {
687
+ this.waterFlow?.setNozzle(x, y, z);
688
+ }
689
+ /** 运行时改喷流方向(轴向,会被归一化) */
690
+ setWaterFlowJetDirection(x, y, z) {
691
+ this.waterFlow?.setJetDirection(x, y, z);
692
+ }
693
+ /** 运行时开关粒子与 basin/地面碰撞回弹 */
694
+ setWaterFlowCollision(enabled) {
695
+ this.waterFlow?.setCollision(enabled);
696
+ }
697
+ /**
698
+ * 关闭屏幕空间水流并释放其 GPU 资源(enableWaterFlow 的反操作)。
699
+ * 切走页面时必须调用,否则其它页面 render 仍会合成水面(this.waterFlow 非空)。
700
+ */
701
+ disableWaterFlow() {
702
+ if (!this.waterFlow)
703
+ return;
704
+ this.waterFlow.dispose();
705
+ this.waterFlow = null;
706
+ this._waterFlowLastTime = 0;
707
+ this.waterFlowElapsed = 0;
708
+ console.info('[ThreeBase] WaterFlow 已关闭');
709
+ }
710
+ // 重置控制器的位置
711
+ resetLookAt({ position = this.perspectiveCamera.position, center = this.controls.target, }) {
712
+ // 停止控制器的惯性
713
+ const originEnableDamping = this.controls.enableDamping;
714
+ this.controls.saveState();
715
+ this.controls.update();
716
+ this.controls.reset();
717
+ this.controls.enableDamping = originEnableDamping;
718
+ this.controlsDistanceTransition.reset({
719
+ distance: new Vector3(position.x, position.y, position.z).distanceTo(new Vector3(center.x, center.y, center.z))
720
+ });
721
+ position &&
722
+ this.controlsPosition.reset({
723
+ x: position.x,
724
+ y: position.y,
725
+ z: position.z,
726
+ });
727
+ center &&
728
+ this.controlsCenter.reset({ x: center.x, y: center.y, z: center.z });
729
+ this.needRender = true;
730
+ }
731
+ // 设置控制器的位置 以动画的方式移动控制器
732
+ async setLookAt({ position = this.perspectiveCamera.position, center = this.controls.target, duration = 1000, }) {
733
+ // 停止控制器的惯性
734
+ const originEnableDamping = this.controls.enableDamping;
735
+ this.controls.saveState();
736
+ this.controls.update();
737
+ this.controls.reset();
738
+ this.controls.enableDamping = originEnableDamping;
739
+ let currentPosition = this.perspectiveCamera.position.clone();
740
+ let currentCenter = this.controls.target.clone();
741
+ let currentDistance = this.perspectiveCamera.position.distanceTo(this.controls.target);
742
+ let targetPosition = new Vector3(position.x, position.y, position.z);
743
+ let targetCenter = new Vector3(center.x, center.y, center.z);
744
+ let targetDistance = targetPosition.distanceTo(targetCenter);
745
+ // 等待动画执行完成
746
+ await Promise.all([
747
+ new Promise(async (resolve, reject) => {
748
+ this.controlsDistanceTransition.setDuration(duration).reset({
749
+ distance: currentDistance,
750
+ });
751
+ await this.controlsDistanceTransition.set({
752
+ distance: targetDistance
753
+ });
754
+ resolve();
755
+ }),
756
+ new Promise(async (resolve, reject) => {
757
+ this.controlsPosition.setDuration(duration).reset({
758
+ x: currentPosition.x,
759
+ y: currentPosition.y,
760
+ z: currentPosition.z,
761
+ });
762
+ await this.controlsPosition.set({
763
+ x: position.x,
764
+ y: position.y,
765
+ z: position.z,
766
+ });
767
+ resolve();
768
+ }),
769
+ new Promise(async (resolve, reject) => {
770
+ this.controlsCenter.setDuration(duration).reset({
771
+ x: currentCenter.x,
772
+ y: currentCenter.y,
773
+ z: currentCenter.z,
774
+ });
775
+ await this.controlsCenter.set({
776
+ x: center.x,
777
+ y: center.y,
778
+ z: center.z,
779
+ });
780
+ resolve();
781
+ }),
782
+ ]);
783
+ }
784
+ // 获取控制的位置和朝向 保留3位小数
785
+ getLookAt() {
786
+ //判断需要使用那个相机
787
+ const position = this.perspectiveCamera.position
788
+ .toArray()
789
+ .map((v) => round(v, 3));
790
+ const center = this.controls.target.toArray().map((v) => round(v, 3));
791
+ return {
792
+ center: new Vector3(...center),
793
+ position: new Vector3(...position),
794
+ centerPosition: `center: new Vector3(${center.join(",")}),position: new Vector3(${position.join(",")})`,
795
+ };
796
+ }
797
+ // 查看对象
798
+ lookatObjects({ objects, duration = 1000, direction = new Vector3(0.3, 0.3, 1), offset = new Vector3(0, 0, 0), distance = 1, }) {
799
+ const box3 = new Box3();
800
+ objects.map((o) => {
801
+ o.traverseVisible((oo) => {
802
+ box3.expandByObject(oo);
803
+ });
804
+ });
805
+ const size = new Vector3();
806
+ box3.getSize(size);
807
+ const center = new Vector3();
808
+ box3.getCenter(center);
809
+ center.add(offset);
810
+ const modelSize = size.length() * distance;
811
+ if (this.cameraType === "perspectiveCamera") {
812
+ const { fov } = this.getCamera();
813
+ const cameraDistance = modelSize / (2 * Math.tan((fov * Math.PI) / 360));
814
+ const position = direction.setLength(cameraDistance).add(center);
815
+ this.setLookAt({ center, position, duration });
816
+ }
817
+ }
818
+ // 三维向量转化成屏幕上的2维向量
819
+ v3tov2(v3) {
820
+ //判断需要使用那个相机
821
+ const camera = this.getCamera();
822
+ v3.project(camera);
823
+ const v2 = new Vector2((v3.x + 1) * this.size.x * 0.5, (-v3.y + 1) * this.size.y * 0.5);
824
+ return v2;
825
+ }
826
+ // 控制器类型
827
+ controlType = "OrbitControls";
828
+ // 记录一下空间平移方式 在切换控制器的时候保留一下平移方式
829
+ originScreenSpacePanning = false;
830
+ //改变控制器的类型
831
+ setControlType(controlType) {
832
+ if (controlType === "OrbitControls") {
833
+ this.controls.screenSpacePanning = this.originScreenSpacePanning;
834
+ this.controls.mouseButtons = {
835
+ LEFT: MOUSE.ROTATE,
836
+ MIDDLE: MOUSE.DOLLY,
837
+ RIGHT: MOUSE.PAN,
838
+ };
839
+ this.controls.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
840
+ }
841
+ else if (controlType === "MapControls") {
842
+ this.originScreenSpacePanning = this.controls.screenSpacePanning;
843
+ // pan orthogonal to world-space direction camera.up
844
+ this.controls.screenSpacePanning = false;
845
+ this.controls.mouseButtons.LEFT = MOUSE.PAN;
846
+ this.controls.mouseButtons.RIGHT = MOUSE.ROTATE;
847
+ this.controls.touches.ONE = TOUCH.PAN;
848
+ this.controls.touches.TWO = TOUCH.DOLLY_ROTATE;
849
+ }
850
+ }
851
+ // 是否自动渲染
852
+ autoRender = true;
853
+ //是否需要渲染, 当autoRender=false的时候就需要 手动触发渲染
854
+ needRender = false;
855
+ // SSAO 配置
856
+ ssao = {
857
+ enable: false,
858
+ debug: false,
859
+ radius: 5.0, // 世界空间遮蔽半径(单位:世界空间单位)
860
+ falloff: 5.0, // 世界空间衰减距离
861
+ bias: 0.3, // 切面距离阈值(过滤平坦面)
862
+ intensity: 1.0, // AO 叠加强度 mix(1.0, ao, intensity)
863
+ sampleCount: 16, // 采样数
864
+ blurRadius: 4.0, // 去噪半径(像素,越大越平滑、降噪越强)
865
+ strength: 1.0, // 与 intensity 同义,两者都设时以 strength 为准
866
+ resolutionScale: 0.5, // SSAO 计算分辨率缩放(0.5 = 半分辨率,性能更好)
867
+ distanceExponent: 1.0, // GTAO:采样点沿半径分布的幂次
868
+ distanceFallOff: 1.0, // GTAO:距离衰减混合系数
869
+ scale: 1.0, // GTAO:AO 次幂(gamma 校正)
870
+ };
871
+ // 屏幕空间辉光(经典 gaussian bloom)配置:纯颜色、无深度依赖,在 screenRenderTarget 上做加性叠加
872
+ bloom = {
873
+ enable: false,
874
+ strength: 0.6,
875
+ radius: 0.4,
876
+ threshold: 0.85,
877
+ };
878
+ // 停止渲染
879
+ stopRender() {
880
+ threeUtils.removeEventListener("requestAnimationFrame", this.renderBindThis);
881
+ if (!this.rendererLost) {
882
+ this.renderer.forceContextLoss();
883
+ this.setSize(100, 100);
884
+ this.renderer.domElement.style.opacity = "0";
885
+ }
886
+ }
887
+ // 彻底销毁当前渲染器实例:停止渲染循环、释放 WebGL 上下文与场景资源、卸载画布 DOM。
888
+ // 用于「每个 demo 视图各自持有渲染器、切换页面时自行销毁」的生命周期管理。
889
+ dispose() {
890
+ // 1. 停止渲染循环(移除 rAF 监听 + 丢失上下文)
891
+ this.stopRender();
892
+ // 2. 轨道控制器(移除其 DOM 事件监听)
893
+ this.controls?.dispose();
894
+ // 3. 鼠标拾取(移除 canvas 上的事件监听)
895
+ this.threeMouse?.dispose();
896
+ // 4. 释放场景内所有几何体 / 材质 / 纹理
897
+ this.scene.traverse((obj) => {
898
+ const mesh = obj;
899
+ if (mesh.geometry) {
900
+ mesh.geometry.dispose();
901
+ }
902
+ const material = mesh.material;
903
+ if (Array.isArray(material)) {
904
+ material.forEach((m) => this._disposeMaterial(m));
905
+ }
906
+ else if (material) {
907
+ this._disposeMaterial(material);
908
+ }
909
+ });
910
+ // 5. 渲染器与画布 DOM
911
+ const dom = this.renderer.domElement;
912
+ if (dom.parentElement) {
913
+ dom.parentElement.removeChild(dom);
914
+ }
915
+ this.renderer.dispose();
916
+ }
917
+ /** 释放单个材质及其引用的纹理(遍历材质属性找 Texture 并 dispose) */
918
+ _disposeMaterial(material) {
919
+ const record = material;
920
+ for (const key in record) {
921
+ const value = record[key];
922
+ if (value && value.isTexture) {
923
+ value.dispose();
924
+ }
925
+ }
926
+ material.dispose();
927
+ }
928
+ // 渲染场景
929
+ render() {
930
+ // canvas 没有在页面上面的话 就停止渲染
931
+ if (!threeUtils.inDocument(this.renderer.domElement)) {
932
+ this.stopRender();
933
+ return;
934
+ }
935
+ // 相机的朝向
936
+ const center = new Vector3(...this.controls.target.toArray().map((v) => round(v, 3)));
937
+ //透视相机和正交相机参数的时时转换
938
+ if (this.cameraType === "perspectiveCamera") {
939
+ const { fov } = this.perspectiveCamera;
940
+ const distaned = this.perspectiveCamera.position
941
+ .clone()
942
+ .sub(center)
943
+ .length();
944
+ const zoom = 1 / (Math.tan(degToRad(fov) / 2) * distaned);
945
+ this.orthographicCamera.zoom = zoom;
946
+ this.orthographicCamera.position
947
+ .copy(this.perspectiveCamera.position.clone().sub(center))
948
+ .setLength(2500)
949
+ .add(center);
950
+ }
951
+ else if (this.cameraType === "orthographicCamera") {
952
+ const { fov } = this.perspectiveCamera;
953
+ const { zoom } = this.orthographicCamera;
954
+ const distaned = 1 / zoom / Math.tan(degToRad(fov) / 2);
955
+ this.perspectiveCamera.position
956
+ .copy(this.orthographicCamera.position.clone().sub(center))
957
+ .setLength(distaned)
958
+ .add(center);
959
+ }
960
+ //判断需要使用那个相机
961
+ const camera = this.getCamera();
962
+ //判断尺寸发生改变时重置尺寸
963
+ const parentNode = this.renderer.domElement.parentNode;
964
+ if (parentNode.clientWidth !== this.size.x ||
965
+ parentNode.clientHeight !== this.size.y) {
966
+ this.setSize(parentNode.clientWidth, parentNode.clientHeight);
967
+ }
968
+ // 分发鼠标事件
969
+ this.threeMouse.dispatchEvent();
970
+ // 在不自动渲染时, needRender=false 就跳过渲染
971
+ if (!this.autoRender && !this.needRender) {
972
+ return; // 标识未渲染
973
+ }
974
+ this.needRender = false;
975
+ // 水流模拟推进:独立于相机控制,按真实帧间隔累计时间推进 GPGPU 粒子模拟。
976
+ // 默认 this.waterFlow 为 null(关闭),调用 enableWaterFlow() 或 ?waterflow=1 才创建。
977
+ if (this.waterFlow && this.waterFlow.supported) {
978
+ const wnow = performance.now();
979
+ const wdt = this._waterFlowLastTime > 0
980
+ ? Math.min((wnow - this._waterFlowLastTime) / 1000, 0.05)
981
+ : 1 / 60;
982
+ this._waterFlowLastTime = wnow;
983
+ this.waterFlowElapsed += wdt;
984
+ this.waterFlow.update(wdt, this.waterFlowElapsed);
985
+ }
986
+ if (this.controls.enabled) {
987
+ // 让 OrbitControls 始终接管"当前渲染相机":OrbitControls 构造时只绑定了
988
+ // perspectiveCamera,但渲染相机随 cameraType 切换。若切到正交相机时仍让 OrbitControls
989
+ // 控制 perspectiveCamera、却用正交相机渲染,且每帧用正交相机反推并覆盖 perspectiveCamera,
990
+ // 两者打架 → 旋转中心漂移、运动错乱。此处把 controls.object 同步为渲染相机即可根治。
991
+ if (this.controls.object !== camera) {
992
+ this.controls.object = camera;
993
+ }
994
+ // 帧率无关的阻尼换算:OrbitControls 的 dampingFactor 是「每帧」衰减比例,固定值会随帧率变化
995
+ // 导致阻尼手感不一致——后处理管线重,快速晃动掉帧时单帧间隔变大、衰减比例却不变,
996
+ // 镜头运动一顿一顿、中心点发飘。这里把基准 dampingFactor 按真实帧间隔归一化到 60fps:
997
+ // effective = 1 - (1 - BASE)^(dt*60)
998
+ // 掉帧(dt 变大)时自动加大单帧衰减比例,让阻尼手感与帧率解耦。
999
+ const now = performance.now();
1000
+ const dt = this._lastControlsTime > 0
1001
+ ? Math.min((now - this._lastControlsTime) / 1000, 0.05)
1002
+ : 1 / 60;
1003
+ this._lastControlsTime = now;
1004
+ // 帧率无关阻尼换算:OrbitControls 的 dampingFactor 是「每帧」衰减比例,固定值会随帧率变化
1005
+ // 导致阻尼手感不一致(后处理管线重、快速晃动掉帧时单帧间隔变大、衰减比例却不变 → 一顿一顿)。
1006
+ // 把基准 dampingFactor 按真实帧间隔归一化到 60fps:effective = 1 - (1 - BASE)^(dt*60),
1007
+ // 掉帧时自动加大单帧衰减比例,让阻尼手感与帧率解耦。每帧无条件 update()(与 1826ab1 一致,
1008
+ // 该版本 enableDamping=true + dampingFactor=0.05 已验证不飘)。
1009
+ this.controls.dampingFactor = 1 - Math.pow(1 - CONTROLS_DAMPING_BASE, dt * 60);
1010
+ this.controls.update();
1011
+ }
1012
+ {
1013
+ // 先派发 beforeRender,让用户动画在当前帧修改 transform(修改会在下方一次性更新进世界矩阵)
1014
+ this.dispatchEvent({ type: "beforeRender" });
1015
+ // 实例化分组器:场景世界矩阵更新后,把 InstancedChild 同步进真实 InstancedMesh
1016
+ this.instancedGroup.update();
1017
+ // 渲染场景
1018
+ // 把基础场景渲染到 帧缓冲区 在后期通道中会用到
1019
+ this.renderer.setRenderTarget(this.baseRenderTarget);
1020
+ this.renderer.clear();
1021
+ this.renderer.render(this.scene, camera);
1022
+ // 深度 pass:固定排除非实体表面后渲到 depthRenderTarget(生成 baseDepthTexture)。
1023
+ // 必须在 base render 之后、所有依赖深度的特效(描边/发光/覆盖/磨砂)之前更新,
1024
+ // 否则这些特效会用到上一帧旧深度 → 遮挡判定错乱。法线 pass 已收进 ssao_pass 内部。
1025
+ renderAoDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1026
+ // 切回 baseRenderTarget,后处理 pass 需要把效果叠加到颜色渲染目标上
1027
+ this.renderer.setRenderTarget(this.baseRenderTarget);
1028
+ // 效果 mask 灰度图尺寸 = effectiveRatio × resolution,与 baseRenderTarget / baseDepthTexture 同倍率,
1029
+ // 全链路 1:1 对齐(mask 跟随 resolution,深度也跟随 resolution,辉光漏光/mask 合成 UV 错位均消除)。
1030
+ // 几何/效果边缘抗锯齿由「baseRenderTarget/screenRenderTarget 超采样(resolution) + 终处理 FXAA 降采样」承担。
1031
+ const dpr = this.effectiveRatio * this.resolution;
1032
+ // SSAO 后处理(基于深度计算遮蔽,乘法叠加):数据准备 + 计算全在 ssao_pass 内部,
1033
+ // 结果输出到「调用时绑定的渲染目标」。
1034
+ // ⚠️ 顺序(2026-09-14 皇上旨):SSAO 必须排在对象级特效(描边/发光/bloom/覆盖/磨砂)之前——
1035
+ // 遮蔽只作用于纯场景底色,特效叠在 AO 之上,描边/辉光不会被 AO 压暗、也不会被 AO 的噪声污染。
1036
+ // 此处绑定的是 baseRenderTarget,故 AO 直接乘进 base;后面的特效与「base → screen 拷贝」
1037
+ // 全部基于「已含 AO」的底色,后续 pass 无需任何改动。
1038
+ ssao_pass(this.renderer, camera, this.scene, this.depthRenderTarget, this.normalRenderTarget, this.normalMaterial, dpr, this.ssao);
1039
+ // 后处理特效渲染(描边,发光,颜色覆盖,bloom,磨砂玻璃):叠加到 baseRenderTarget(此时已含 SSAO)
1040
+ this.scene.traverseVisible((child) => {
1041
+ if (child instanceof Mesh || child instanceof Line || child instanceof Points) {
1042
+ stroke_pass(this.renderer, camera, child, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1043
+ glow_pass(this.renderer, camera, child, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1044
+ bloom_pass(this.renderer, camera, child, this.baseRenderTarget, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1045
+ }
1046
+ });
1047
+ // 自定义描边对象(如 InstancedChild 的选择代理):不在 scene 内,单独在效果阶段描边。
1048
+ // 代理用实例自身的 matrixWorld + 同款 geometry/material 重建,故其深度/灰度与真实实例严格对齐,
1049
+ // 并复用同一张 baseDepthTexture 做遮挡判定(被其他物体挡住的部分不描边)。
1050
+ for (const so of this._strokeObjects) {
1051
+ stroke_pass(this.renderer, camera, so, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1052
+ glow_pass(this.renderer, camera, so, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1053
+ bloom_pass(this.renderer, camera, so, this.baseRenderTarget, this.baseDepthTexture, this.effectiveRatio * this.resolution);
1054
+ }
1055
+ // 把 base(含 SSAO)+描边/发光/bloom 合成结果拷到屏幕合成目标,后续 颜色叠加/磨砂玻璃
1056
+ // 在此目标上叠加;最后统一跑一次 FXAA 终处理输出到屏幕,覆盖全部后处理的锯齿/摩尔纹。
1057
+ this.renderer.setRenderTarget(this.screenRenderTarget);
1058
+ this.renderer.clear();
1059
+ this.screenCopyShader.uniforms.map.value = this.baseRenderTarget.texture;
1060
+ fsQuad.render(this.renderer, this.screenCopyShader);
1061
+ // 颜色叠加
1062
+ this.scene.traverseVisible((child) => {
1063
+ if (child instanceof Mesh || child instanceof Line || child instanceof Points) {
1064
+ cover_pass(this.renderer, camera, child, this.baseDepthTexture, dpr);
1065
+ }
1066
+ });
1067
+ for (const so of this._strokeObjects) {
1068
+ cover_pass(this.renderer, camera, so, this.baseDepthTexture, dpr);
1069
+ }
1070
+ // 一次性处理所有支持毛玻璃特效的对象
1071
+ frosted_glass_pass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, dpr);
1072
+ // 屏幕空间水流:消费 base 深度 + 当前合成画面(含 ssao/覆盖/磨砂),把水面合成进
1073
+ // screenRenderTarget。插在磨砂之后、screenBloom 之前,使水面高光也能被泛光处理。
1074
+ // 默认关闭(this.waterFlow 为 null 时不产生任何开销)。
1075
+ if (this.waterFlow && this.waterFlow.supported) {
1076
+ waterFlowPass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, this.waterFlow);
1077
+ }
1078
+ // 屏幕空间辉光(经典 gaussian bloom):纯颜色、无深度依赖,把亮度超阈值的部分做
1079
+ // 连续高斯模糊后加性叠加回 screenRenderTarget。单分辨率多迭代,柔和均匀、无 mip 分层。
1080
+ if (this.bloom.enable) {
1081
+ screenBloomPass(this.renderer, this.screenRenderTarget, {
1082
+ strength: this.bloom.strength,
1083
+ radius: this.bloom.radius,
1084
+ threshold: this.bloom.threshold,
1085
+ });
1086
+ }
1087
+ // FXAA 终处理:对最终合成图像(含 SSAO/AO/叠加/磨砂/辉光)跑一次 FXAA 统一兜底锯齿与摩尔纹,
1088
+ // 分辨率按实际帧缓冲像素(已乘 dpr)设置,修复高分屏下 FXAA 采样比例偏大失效的问题。
1089
+ // ⚠️ 深度调试预览(面板「深度调试」切换,定位"深度测试全部失效"根因):
1090
+ // 旧控制台入口 `window.__BASE_DEPTH_VIS = true` 仍可用,等价于切到 "global"。
1091
+ // global → baseDepthTexture 灰度。全白=深度为空(renderAoDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1092
+ // ao → aoDepthTexture 灰度(含地板)。确认深度管线本身是否工作(GTAO 是否拿到正确深度)。
1093
+ // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
1094
+ // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
1095
+ // 通用控制台入口(任意页面可用,优先级高于面板属性):
1096
+ // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge"`,设回 "none" 恢复。
1097
+ const dbgWin = window.__TB_DEBUG_DEPTH;
1098
+ const dbgMode = window.__BASE_DEPTH_VIS
1099
+ ? "global"
1100
+ : (dbgWin ?? this.debugDepthPreview);
1101
+ if (dbgMode !== "none") {
1102
+ this.renderer.setRenderTarget(null);
1103
+ this.renderer.clear();
1104
+ if (dbgMode === "global") {
1105
+ baseDepthVisShader.uniforms.tDepth.value = this.baseDepthTexture;
1106
+ fsQuad.render(this.renderer, baseDepthVisShader);
1107
+ }
1108
+ else if (dbgMode === "ao") {
1109
+ baseDepthVisShader.uniforms.tDepth.value = this.aoDepthTexture;
1110
+ fsQuad.render(this.renderer, baseDepthVisShader);
1111
+ }
1112
+ else if (dbgMode === "compare") {
1113
+ debugDepthCompareShader.uniforms.tBase.value = this.baseDepthTexture;
1114
+ debugDepthCompareShader.uniforms.tSelect.value =
1115
+ debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1116
+ debugDepthCompareShader.uniforms.cameraFar.value = camera.far;
1117
+ fsQuad.render(this.renderer, debugDepthCompareShader);
1118
+ }
1119
+ else if (dbgMode === "judge") {
1120
+ debugDepthJudgeShader.uniforms.tBase.value = this.baseDepthTexture;
1121
+ debugDepthJudgeShader.uniforms.tSelect.value =
1122
+ debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1123
+ fsQuad.render(this.renderer, debugDepthJudgeShader);
1124
+ }
1125
+ else if (dbgMode === "excluded") {
1126
+ // 先渲「被排除面 mask」,再与深度图叠加:红=挡不住任何后处理的面
1127
+ renderExcludedMaskPass(this.renderer, camera, this.scene, this.excludedMaskRenderTarget);
1128
+ debugExcludedShader.uniforms.tDepth.value = this.baseDepthTexture;
1129
+ debugExcludedShader.uniforms.tMask.value = this.excludedMaskRenderTarget.texture;
1130
+ fsQuad.render(this.renderer, debugExcludedShader);
1131
+ }
1132
+ flushLensflareOverlays(this.renderer, camera);
1133
+ return;
1134
+ }
1135
+ this.renderer.setRenderTarget(null);
1136
+ this.renderer.clear();
1137
+ this.renderRendererTarget(this.screenRenderTarget);
1138
+ // 镜头光晕(Lensflare)解耦:渲染器输出屏幕后统一叠加所有可见鬼影。
1139
+ // flushLensflareOverlays 内部仅当渲染目标为屏幕时生效,不污染任何中间后处理 target;
1140
+ // LensFlare 实例自己管理状态(构造注册进全局表、dispose 注销),ThreeBase 不遍历场景、不认识其类型。
1141
+ flushLensflareOverlays(this.renderer, camera);
1142
+ }
1143
+ }
1144
+ renderBindThis = this.render.bind(this);
1145
+ //获取渲染图blob对象
1146
+ async getThumbBlob() {
1147
+ this.renderer.setRenderTarget(this.baseRenderTarget);
1148
+ this.renderer.clear();
1149
+ this.renderer.render(this.scene, this.getCamera());
1150
+ this.renderer.setRenderTarget(null);
1151
+ this.renderer.clear();
1152
+ this.renderRendererTarget(this.baseRenderTarget);
1153
+ const blob = await new Promise((resolve, reject) => {
1154
+ this.renderer.domElement.toBlob((blob) => {
1155
+ resolve(blob);
1156
+ }, "image/png", 1);
1157
+ });
1158
+ return blob;
1159
+ }
1160
+ //获取渲染图base64
1161
+ getThumbBase64() {
1162
+ this.renderer.setRenderTarget(this.baseRenderTarget);
1163
+ this.renderer.clear();
1164
+ this.renderer.render(this.scene, this.getCamera());
1165
+ this.renderer.setRenderTarget(null);
1166
+ this.renderer.clear();
1167
+ this.renderRendererTarget(this.baseRenderTarget);
1168
+ const base64 = this.renderer.domElement.toDataURL("image/png");
1169
+ return base64;
1170
+ }
1171
+ //获取渲染图文件对象
1172
+ async getThumbFile() {
1173
+ const blob = await this.getThumbBlob();
1174
+ const file = new File([blob], "thumb.png", { type: blob.type });
1175
+ return file;
1176
+ }
1177
+ // 下载渲染图
1178
+ async download(fileName = "download") {
1179
+ const blob = await this.getThumbBlob();
1180
+ let saveLink = document.createElement("a");
1181
+ saveLink.download = `${fileName}.png`;
1182
+ saveLink.href = URL.createObjectURL(blob);
1183
+ document.body.append(saveLink);
1184
+ saveLink.click();
1185
+ }
1186
+ //把渲染目标渲染到canvas上
1187
+ renderRendererTarget(renderTarget) {
1188
+ // FXAA 分辨率按实际帧缓冲像素尺寸(乘 devicePixelRatio)设置,否则高分屏下
1189
+ // 采样步长被当成 CSS 像素,FXAA 失效甚至引入摩尔纹。
1190
+ const dpr = Math.max(window.devicePixelRatio, this.minDevicePixelRatio);
1191
+ baseTextureToRenderShader.uniforms.resolution.value.set(1 / (this.size.x * dpr), 1 / (this.size.y * dpr));
1192
+ baseTextureToRenderShader.uniforms.tDiffuse.value = renderTarget.texture;
1193
+ fsQuad.render(this.renderer, baseTextureToRenderShader);
1194
+ }
1195
+ // 渲染纹理
1196
+ renderTexture(texture, alpha = false) {
1197
+ copyShader.uniforms.map.value = texture;
1198
+ copyShader.uniforms.opacity.value = 1.0;
1199
+ copyShader.blending = alpha ? NoBlending : NormalBlending;
1200
+ fsQuad.render(this.renderer, copyShader);
1201
+ }
1202
+ }