three-base 2.0.168 → 2.0.172

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 (44) hide show
  1. package/FPScontrol.d.ts +41 -7
  2. package/FPScontrol.js +232 -155
  3. package/FPScontrol.ts +129 -55
  4. package/Ground1/Ground1.js +6 -6
  5. package/Ground1/Ground1.ts +6 -6
  6. package/MktMesh/MktMesh.d.ts +5 -1
  7. package/MktMesh/MktMesh.js +20 -7
  8. package/MktMesh/MktMesh.ts +20 -7
  9. package/MktMesh/panoSlotBind.js +18 -2
  10. package/MktMesh/panoSlotBind.ts +18 -1
  11. package/MktMesh/panoTileStitcher.d.ts +35 -7
  12. package/MktMesh/panoTileStitcher.js +71 -11
  13. package/MktMesh/panoTileStitcher.ts +73 -12
  14. package/MktMesh/types.d.ts +5 -1
  15. package/MktMesh/types.ts +5 -1
  16. package/ThreeBase.d.ts +2 -11
  17. package/ThreeBase.js +14 -26
  18. package/ThreeBase.ts +14 -26
  19. package/WaterFlow/WaterFlow.d.ts +1 -1
  20. package/WaterFlow/WaterFlow.js +1 -1
  21. package/WaterFlow/WaterFlow.ts +1 -1
  22. package/package.json +1 -1
  23. package/pass/bloom_pass.js +7 -14
  24. package/pass/bloom_pass.ts +7 -16
  25. package/pass/cover_pass.js +7 -15
  26. package/pass/cover_pass.ts +7 -17
  27. package/pass/debugDepthState.d.ts +2 -2
  28. package/pass/debugDepthState.ts +2 -2
  29. package/pass/glow_pass.js +8 -16
  30. package/pass/glow_pass.ts +8 -18
  31. package/pass/ssao_pass.d.ts +13 -44
  32. package/pass/ssao_pass.js +60 -78
  33. package/pass/ssao_pass.ts +74 -88
  34. package/pass/stroke_pass.js +10 -16
  35. package/pass/stroke_pass.ts +10 -18
  36. package/shaders/baseDepthVisShader.d.ts +1 -1
  37. package/shaders/baseDepthVisShader.js +1 -1
  38. package/shaders/baseDepthVisShader.ts +1 -1
  39. package/shaders/debugDepthCompareShader.d.ts +1 -1
  40. package/shaders/debugDepthCompareShader.js +1 -1
  41. package/shaders/debugDepthCompareShader.ts +1 -1
  42. package/shaders/debugExcludedShader.d.ts +6 -4
  43. package/shaders/debugExcludedShader.js +6 -4
  44. package/shaders/debugExcludedShader.ts +6 -4
@@ -6,8 +6,14 @@ import { Texture, type WebGLRenderer } from "three";
6
6
  export interface PanoStitcher {
7
7
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
8
8
  ensure(urls: string[]): void;
9
- /** 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」);没画过 / 没这组返回 null。 */
10
- peek(batch: string, slot: number): Texture | null;
9
+ /**
10
+ * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
11
+ * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
12
+ * 先把 RT 建出来(建完即整张涂 `MISSING_TILE_GRAY` 缺失灰)并返回它** ⇒ 调用方切批次的那一帧
13
+ * 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
14
+ * ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
15
+ */
16
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
11
17
  /** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
12
18
  * ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
13
19
  pump(renderer: WebGLRenderer | null): void;
@@ -36,6 +42,12 @@ export declare class PanoTileStitcher implements PanoStitcher {
36
42
  private _retryAt;
37
43
  private _fails;
38
44
  private _dead;
45
+ /** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
46
+ * ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
47
+ * 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
48
+ * —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
49
+ * 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
50
+ private _uploadQueued;
39
51
  /** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
40
52
  private _renderer;
41
53
  /** 推进计数(`progressStamp`)*/
@@ -59,7 +71,7 @@ export declare class PanoTileStitcher implements PanoStitcher {
59
71
  ensure(urls: string[]): void;
60
72
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
61
73
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
62
- peek(batch: string, slot: number): Texture | null;
74
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
63
75
  /** ⭐ 这条路交出去的槽纹理本来就是 GL 渲染目标的附件 —— 像素已经在显存里,没有「上传」这一步。
64
76
  * 消费方(`MktMesh._peekTileAt` / 编辑器 `_peekTileAt`)据此**跳过**「每 tick 一张 `initTexture`」那道闸。 */
65
77
  resident(tex: Texture): boolean;
@@ -82,11 +94,27 @@ export declare class PanoTileStitcher implements PanoStitcher {
82
94
  private _drawPass;
83
95
  /** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
84
96
  * 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
97
+ /**
98
+ * 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
99
+ *
100
+ * ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
101
+ * 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
102
+ * 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
103
+ * (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
104
+ * 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
105
+ *
106
+ * ⭐ 改挂`threeUtils.addWork` 的三个好处:
107
+ * ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
108
+ * ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
109
+ * ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
110
+ * 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
111
+ */
85
112
  private _uploadPass;
86
- /** 上传一片瓦片的纹理(`texImage2D` + mipmap 才是那一下贵的,所以单独成工序)。
87
- * ⚠️ 成败都记 `uploaded`:不记 ⇒ 那一格永远进不了 `_drawPass` ⇒ 该槽永远画不满、每帧白烧一个预算,
88
- * 比「那一帧没错峰」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
89
- * (与 `MktMesh._uploadTileTexture` 同一口径,皇上旨 2026-09-25「队列里只放上传那一句」。) */
113
+ /** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
114
+ * ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
115
+ * 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
116
+ * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
117
+ * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
90
118
  private _uploadTile;
91
119
  /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(16 格里的一格)。
92
120
  * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 16 帧**,
@@ -76,8 +76,15 @@
76
76
  */
77
77
  import { ClampToEdgeWrapping, DataTexture, LinearFilter, LinearMipmapLinearFilter, Mesh, NoBlending, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, SRGBColorSpace, UnsignedByteType, Vector3, Vector4, WebGLRenderTarget, } from "three";
78
78
  import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
79
+ // ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
80
+ import threeUtils from "../threeUtils";
79
81
  import { MISSING_TILE_GRAY, RT_STITCH_CELLS_PER_SLOT, RT_STITCH_SLOT_COUNT, RT_STITCH_SLOT_H, RT_STITCH_SLOT_TILES, RT_STITCH_SLOT_W, RT_STITCH_SLOT_GRID, RT_STITCH_TILES_PER_BATCH, rtStitchBatchOf, rtStitchCellPos, rtStitchTileInset, } from "./mktShader";
80
- /** 每帧最多几张**新**纹理进 GPU(旨②「每一帧同样也是只载入一张新纹理到 GPU 里面去」)*/
82
+ /**
83
+ * ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
84
+ * `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
85
+ * 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
86
+ * ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
87
+ */
81
88
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
82
89
  /** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
83
90
  const PANO_STITCH_DOWNLOADS_PER_FRAME = 2;
@@ -141,6 +148,12 @@ export class PanoTileStitcher {
141
148
  _retryAt = new Map();
142
149
  _fails = new Map();
143
150
  _dead = new Set();
151
+ /** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
152
+ * ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
153
+ * 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
154
+ * —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
155
+ * 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
156
+ _uploadQueued = new Set();
144
157
  /** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
145
158
  _renderer = null;
146
159
  /** 推进计数(`progressStamp`)*/
@@ -198,10 +211,24 @@ export class PanoTileStitcher {
198
211
  }
199
212
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
200
213
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
201
- peek(batch, slot) {
214
+ peek(batch, slot, renderer) {
202
215
  const g = this._groups.get(batch);
203
- if (!g || !g.drawnCount[slot])
216
+ if (!g)
204
217
  return null;
218
+ if (!g.drawnCount[slot]) {
219
+ // ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
220
+ // 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
221
+ // **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
222
+ // 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
223
+ // 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
224
+ // 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
225
+ // ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
226
+ if (!renderer)
227
+ return null;
228
+ if (!this._renderer)
229
+ return null;
230
+ return this._ensureRt(this._renderer, g, slot).texture;
231
+ }
205
232
  const rt = g.rts[slot];
206
233
  return rt ? rt.texture : null;
207
234
  }
@@ -247,6 +274,7 @@ export class PanoTileStitcher {
247
274
  for (const batch of [...this._groups.keys()])
248
275
  this.disposeGroup(batch);
249
276
  this._texs.clear();
277
+ this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
250
278
  this._wanted.clear();
251
279
  this._retryAt.clear();
252
280
  this._fails.clear();
@@ -323,8 +351,28 @@ export class PanoTileStitcher {
323
351
  }
324
352
  /** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
325
353
  * 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
354
+ /**
355
+ * 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
356
+ *
357
+ * ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
358
+ * 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
359
+ * 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
360
+ * (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
361
+ * 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
362
+ *
363
+ * ⭐ 改挂`threeUtils.addWork` 的三个好处:
364
+ * ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
365
+ * ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
366
+ * ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
367
+ * 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
368
+ */
326
369
  _uploadPass(renderer) {
327
- let budget = PANO_STITCH_UPLOADS_PER_FRAME;
370
+ // ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
371
+ // 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
372
+ // 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
373
+ // 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
374
+ if (this._uploadQueued.size > 0)
375
+ return;
328
376
  for (const g of this._groups.values()) {
329
377
  if (g.pending <= 0)
330
378
  continue;
@@ -339,18 +387,25 @@ export class PanoTileStitcher {
339
387
  const rec = this._texs.get(url);
340
388
  if (!rec || !rec.tex || rec.uploaded)
341
389
  continue;
342
- this._uploadTile(rec);
343
- if (--budget <= 0)
344
- return; // 旨②「每一帧只载入一张新纹理」⇒ 这一趟到此为止
390
+ // ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
391
+ if (this._uploadQueued.has(rec))
392
+ continue;
393
+ this._uploadQueued.add(rec);
394
+ threeUtils.addWork(() => { }); // 占一格(什么都不干),把真活推到下一个 tick
395
+ threeUtils.addWork(() => this._uploadTile(rec));
396
+ return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
345
397
  }
346
398
  }
347
399
  }
348
400
  }
349
- /** 上传一片瓦片的纹理(`texImage2D` + mipmap 才是那一下贵的,所以单独成工序)。
350
- * ⚠️ 成败都记 `uploaded`:不记 ⇒ 那一格永远进不了 `_drawPass` ⇒ 该槽永远画不满、每帧白烧一个预算,
351
- * 比「那一帧没错峰」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
352
- * (与 `MktMesh._uploadTileTexture` 同一口径,皇上旨 2026-09-25「队列里只放上传那一句」。) */
401
+ /** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
402
+ * ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
403
+ * 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
404
+ * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
405
+ * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
353
406
  _uploadTile(rec) {
407
+ if (!this._uploadQueued.delete(rec))
408
+ return;
354
409
  const renderer = this._renderer;
355
410
  if (!renderer || !rec.tex)
356
411
  return;
@@ -477,6 +532,8 @@ export class PanoTileStitcher {
477
532
  for (const batch of [...this._groups.keys()])
478
533
  this.disposeGroup(batch);
479
534
  this._texs.clear();
535
+ // ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
536
+ this._uploadQueued.clear();
480
537
  this._wanted.clear();
481
538
  this._retryAt.clear();
482
539
  this._fails.clear();
@@ -510,6 +567,9 @@ export class PanoTileStitcher {
510
567
  const rec = this._texs.get(url);
511
568
  if (rec && rec.tex)
512
569
  releasePanoTexture(rec.tex);
570
+ // ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
571
+ if (rec)
572
+ this._uploadQueued.delete(rec);
513
573
  this._texs.delete(url);
514
574
  this._wanted.delete(url);
515
575
  }
@@ -96,6 +96,8 @@ import {
96
96
  type WebGLRenderer,
97
97
  } from "three";
98
98
  import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
99
+ // ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
100
+ import threeUtils from "../threeUtils";
99
101
  import {
100
102
  MISSING_TILE_GRAY,
101
103
  RT_STITCH_CELLS_PER_SLOT,
@@ -110,7 +112,12 @@ import {
110
112
  rtStitchTileInset,
111
113
  } from "./mktShader";
112
114
 
113
- /** 每帧最多几张**新**纹理进 GPU(旨②「每一帧同样也是只载入一张新纹理到 GPU 里面去」)*/
115
+ /**
116
+ * ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
117
+ * `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
118
+ * 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
119
+ * ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
120
+ */
114
121
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
115
122
 
116
123
  /** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
@@ -199,8 +206,14 @@ interface StitchGroup {
199
206
  export interface PanoStitcher {
200
207
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
201
208
  ensure(urls: string[]): void;
202
- /** 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」);没画过 / 没这组返回 null。 */
203
- peek(batch: string, slot: number): Texture | null;
209
+ /**
210
+ * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
211
+ * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
212
+ * 先把 RT 建出来(建完即整张涂 `MISSING_TILE_GRAY` 缺失灰)并返回它** ⇒ 调用方切批次的那一帧
213
+ * 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
214
+ * ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
215
+ */
216
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
204
217
  /** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
205
218
  * ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
206
219
  pump(renderer: WebGLRenderer | null): void;
@@ -230,6 +243,12 @@ export class PanoTileStitcher implements PanoStitcher {
230
243
  private _retryAt: Map<string, number> = new Map();
231
244
  private _fails: Map<string, number> = new Map();
232
245
  private _dead: Set<string> = new Set();
246
+ /** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
247
+ * ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
248
+ * 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
249
+ * —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
250
+ * 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
251
+ private _uploadQueued: Set<TileRec> = new Set();
233
252
  /** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
234
253
  private _renderer: WebGLRenderer | null = null;
235
254
  /** 推进计数(`progressStamp`)*/
@@ -288,9 +307,21 @@ export class PanoTileStitcher implements PanoStitcher {
288
307
 
289
308
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
290
309
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
291
- peek(batch: string, slot: number): Texture | null {
310
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null {
292
311
  const g = this._groups.get(batch);
293
- if (!g || !g.drawnCount[slot]) return null;
312
+ if (!g) return null;
313
+ if (!g.drawnCount[slot]) {
314
+ // ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
315
+ // 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
316
+ // **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
317
+ // 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
318
+ // 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
319
+ // 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
320
+ // ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
321
+ if (!renderer) return null;
322
+ if (!this._renderer) return null;
323
+ return this._ensureRt(this._renderer, g, slot).texture;
324
+ }
294
325
  const rt = g.rts[slot];
295
326
  return rt ? rt.texture : null;
296
327
  }
@@ -338,6 +369,7 @@ export class PanoTileStitcher implements PanoStitcher {
338
369
  dispose(): void {
339
370
  for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
340
371
  this._texs.clear();
372
+ this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
341
373
  this._wanted.clear();
342
374
  this._retryAt.clear();
343
375
  this._fails.clear();
@@ -408,8 +440,27 @@ export class PanoTileStitcher implements PanoStitcher {
408
440
 
409
441
  /** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
410
442
  * 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
443
+ /**
444
+ * 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
445
+ *
446
+ * ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
447
+ * 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
448
+ * 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
449
+ * (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
450
+ * 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
451
+ *
452
+ * ⭐ 改挂`threeUtils.addWork` 的三个好处:
453
+ * ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
454
+ * ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
455
+ * ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
456
+ * 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
457
+ */
411
458
  private _uploadPass(renderer: WebGLRenderer): void {
412
- let budget = PANO_STITCH_UPLOADS_PER_FRAME;
459
+ // ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
460
+ // 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
461
+ // 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
462
+ // 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
463
+ if (this._uploadQueued.size > 0) return;
413
464
  for (const g of this._groups.values()) {
414
465
  if (g.pending <= 0) continue;
415
466
  for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
@@ -420,18 +471,24 @@ export class PanoTileStitcher implements PanoStitcher {
420
471
  if (!url || this._dead.has(url)) continue;
421
472
  const rec = this._texs.get(url);
422
473
  if (!rec || !rec.tex || rec.uploaded) continue;
423
- this._uploadTile(rec);
424
- if (--budget <= 0) return; // 旨②「每一帧只载入一张新纹理」⇒ 这一趟到此为止
474
+ // ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
475
+ if (this._uploadQueued.has(rec)) continue;
476
+ this._uploadQueued.add(rec);
477
+ threeUtils.addWork(() => {}); // 占一格(什么都不干),把真活推到下一个 tick
478
+ threeUtils.addWork(() => this._uploadTile(rec));
479
+ return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
425
480
  }
426
481
  }
427
482
  }
428
483
  }
429
484
 
430
- /** 上传一片瓦片的纹理(`texImage2D` + mipmap 才是那一下贵的,所以单独成工序)。
431
- * ⚠️ 成败都记 `uploaded`:不记 ⇒ 那一格永远进不了 `_drawPass` ⇒ 该槽永远画不满、每帧白烧一个预算,
432
- * 比「那一帧没错峰」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
433
- * (与 `MktMesh._uploadTileTexture` 同一口径,皇上旨 2026-09-25「队列里只放上传那一句」。) */
485
+ /** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
486
+ * ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
487
+ * 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
488
+ * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
489
+ * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
434
490
  private _uploadTile(rec: TileRec): void {
491
+ if (!this._uploadQueued.delete(rec)) return;
435
492
  const renderer = this._renderer;
436
493
  if (!renderer || !rec.tex) return;
437
494
  try {
@@ -562,6 +619,8 @@ export class PanoTileStitcher implements PanoStitcher {
562
619
  this._renderer = renderer;
563
620
  for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
564
621
  this._texs.clear();
622
+ // ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
623
+ this._uploadQueued.clear();
565
624
  this._wanted.clear();
566
625
  this._retryAt.clear();
567
626
  this._fails.clear();
@@ -595,6 +654,8 @@ export class PanoTileStitcher implements PanoStitcher {
595
654
  private _dropTile(url: string): void {
596
655
  const rec = this._texs.get(url);
597
656
  if (rec && rec.tex) releasePanoTexture(rec.tex);
657
+ // ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
658
+ if (rec) this._uploadQueued.delete(rec);
598
659
  this._texs.delete(url);
599
660
  this._wanted.delete(url);
600
661
  }
@@ -1,6 +1,10 @@
1
1
  import type { Matrix4 } from "three";
2
2
  /**
3
- * 单个纹理的运行时信息:url + 世界矩阵(位置 + 旋转,缩放忽略)。
3
+ * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
4
+ * ⚠️ 这个矩阵记在 MktMesh **自身局部(制图)空间**,与 `wallPoints` 同一空间 —— 不是世界矩阵:
5
+ * 着色器里 `vProjPos = uProjInvMatrix·modelMatrix·position` 恰好把顶点拉回局部,片元再拿
6
+ * `vProjPos - 矩阵位置` 求投影方向,两端必须同空间纹理才贴得住(网格整体旋转/平移时尤其要紧,
7
+ * 名册挑图那把尺子也因此先把世界锚点 `worldToLocal` 再比距离,见 `MktMesh.updateTexture`)。
4
8
  * 纹理矩阵的「位置」即墨卡托投影的中心点(shader 里以它为原点求方向向量)。
5
9
  */
6
10
  export interface MktTextureInfo {
package/MktMesh/types.ts CHANGED
@@ -8,7 +8,11 @@
8
8
  import type { Matrix4 } from "three";
9
9
 
10
10
  /**
11
- * 单个纹理的运行时信息:url + 世界矩阵(位置 + 旋转,缩放忽略)。
11
+ * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
12
+ * ⚠️ 这个矩阵记在 MktMesh **自身局部(制图)空间**,与 `wallPoints` 同一空间 —— 不是世界矩阵:
13
+ * 着色器里 `vProjPos = uProjInvMatrix·modelMatrix·position` 恰好把顶点拉回局部,片元再拿
14
+ * `vProjPos - 矩阵位置` 求投影方向,两端必须同空间纹理才贴得住(网格整体旋转/平移时尤其要紧,
15
+ * 名册挑图那把尺子也因此先把世界锚点 `worldToLocal` 再比距离,见 `MktMesh.updateTexture`)。
12
16
  * 纹理矩阵的「位置」即墨卡托投影的中心点(shader 里以它为原点求方向向量)。
13
17
  */
14
18
  export interface MktTextureInfo {
package/ThreeBase.d.ts CHANGED
@@ -90,8 +90,8 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
90
90
  /**
91
91
  * 调试用:深度预览模式。由面板「深度调试」切换,便于逐一看各深度图排查"深度测试全部失效"根因。
92
92
  * - "none" :正常渲染(默认)
93
- * - "global" :仅预览全局深度图(baseDepthTexture,排除地板)。全白=深度为空→renderAoDepthPass 没写进深度。
94
- * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入的含地板深度)。用于确认深度管线本身是否工作。
93
+ * - "global" :仅预览可见深度图(baseDepthTexture,含地板)。全白=深度为空→renderVisibleDepthPass 没写进深度。
94
+ * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入)。用于确认深度管线本身是否工作。
95
95
  * - "compare" :R=全局 base 深度,G=最近一次 glow/stroke 选区 select 深度。两路任一全白即该侧写入失败。
96
96
  * - "excluded":深度图灰度 + 「被排除出深度图的面」叠红(红=该面写不进深度,永远挡不住后处理)。
97
97
  * 物体区域(G 暗)若 B 全黑 → 判定从不触发;再看该区 R/G 谁大即可定位是谁缺深度。
@@ -99,15 +99,6 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
99
99
  * 通用控制台入口:任意页面都可 `window.__TB_DEBUG_DEPTH = "judge"`(优先级高于面板属性)。
100
100
  */
101
101
  debugDepthPreview: DebugDepthMode;
102
- /**
103
- * 可见深度图是否包含「标了 userData.aoFloor 的地板」(如 Ground1)。
104
- *
105
- * 默认 false —— 原设计:地板可透视,透过地板看到的物体照常显示(不被地板挡住)。
106
- * 置 true 后地板会写进 baseDepthTexture,**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
107
- * 排查"怎么都挡不住"时先开这个试一下:若开了就正常,说明挡你的正是可透视地板。
108
- */
109
- get baseDepthIncludeFloor(): boolean;
110
- set baseDepthIncludeFloor(v: boolean);
111
102
  normalRenderTarget: WebGLRenderTarget<Texture>;
112
103
  screenRenderTarget: WebGLRenderTarget<Texture>;
113
104
  screenCopyShader: import("three").ShaderMaterial;
package/ThreeBase.js CHANGED
@@ -18,7 +18,7 @@ import frosted_glass_pass from "./pass/frosted_glass_pass";
18
18
  import { flushLensflareOverlays } from "./lensflare/LensFlare";
19
19
  import glow_pass from "./pass/glow_pass";
20
20
  import stroke_pass from "./pass/stroke_pass";
21
- import ssao_pass, { renderAoDepthPass, renderExcludedMaskPass, visibleDepthOptions } from "./pass/ssao_pass";
21
+ import ssao_pass, { renderVisibleDepthPass, renderExcludedMaskPass } from "./pass/ssao_pass";
22
22
  import screenBloomPass from "./pass/screen_bloom_pass";
23
23
  import copyShader from "./shaders/copyShader";
24
24
  import { baseTextureToRenderShader } from "./shaders/baseTextureToRenderShader";
@@ -260,12 +260,13 @@ export default class ThreeBase extends EventDispatcher {
260
260
  stencilBuffer: false,
261
261
  depthTexture: this.baseDepthTexture,
262
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 的复合守卫说明。
263
+ // AO 深度纹理:**不再单独渲染一遍** —— 它挂在 normalRenderTarget 上当 depth attachment,
264
+ // 法线 pass(renderAoNormalPass,excludeTransparent=true)渲染时深度由光栅器顺带写入,白拿一张。
265
+ // (2026-09-14 优化:删掉原 ssao_pass 内那次独立的含地板深度 pass,每帧省一次全场景遍历。)
266
+ // SSAO 的 GTAO 采样它(与法线同源);合成时再与 depthRenderTarget(可见深度 = 各物体自身材质渲染)
267
+ // 比对:AO 侧更近说明那像素记到屏幕上不可见的面(抠图空隙 / 单面墙背面)→ 作废该像素 AO。
268
+ // ⚠️ 2026-10-11:地板(Ground1)现在两张图都写(旧的 aoFloor 排除特例已整条拆除),
269
+ // 见 three-base/pass/ssao_pass.ts 文件头与复合守卫说明。
269
270
  aoDepthTexture = (() => {
270
271
  const t = new DepthTexture(4096, 4096);
271
272
  t.format = DepthFormat;
@@ -276,8 +277,8 @@ export default class ThreeBase extends EventDispatcher {
276
277
  /**
277
278
  * 调试用:深度预览模式。由面板「深度调试」切换,便于逐一看各深度图排查"深度测试全部失效"根因。
278
279
  * - "none" :正常渲染(默认)
279
- * - "global" :仅预览全局深度图(baseDepthTexture,排除地板)。全白=深度为空→renderAoDepthPass 没写进深度。
280
- * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入的含地板深度)。用于确认深度管线本身是否工作。
280
+ * - "global" :仅预览可见深度图(baseDepthTexture,含地板)。全白=深度为空→renderVisibleDepthPass 没写进深度。
281
+ * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入)。用于确认深度管线本身是否工作。
281
282
  * - "compare" :R=全局 base 深度,G=最近一次 glow/stroke 选区 select 深度。两路任一全白即该侧写入失败。
282
283
  * - "excluded":深度图灰度 + 「被排除出深度图的面」叠红(红=该面写不进深度,永远挡不住后处理)。
283
284
  * 物体区域(G 暗)若 B 全黑 → 判定从不触发;再看该区 R/G 谁大即可定位是谁缺深度。
@@ -285,20 +286,6 @@ export default class ThreeBase extends EventDispatcher {
285
286
  * 通用控制台入口:任意页面都可 `window.__TB_DEBUG_DEPTH = "judge"`(优先级高于面板属性)。
286
287
  */
287
288
  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
289
  // 法线专用渲染目标(无 MSAA,存储视图空间法线,供 GTAO 风格 SSAO 使用真实法线)
303
290
  normalRenderTarget = new WebGLRenderTarget(128, 128, {
304
291
  minFilter: LinearFilter,
@@ -1019,10 +1006,11 @@ export default class ThreeBase extends EventDispatcher {
1019
1006
  this.renderer.setRenderTarget(this.baseRenderTarget);
1020
1007
  this.renderer.clear();
1021
1008
  this.renderer.render(this.scene, camera);
1022
- // 深度 pass:固定排除非实体表面后渲到 depthRenderTarget(生成 baseDepthTexture)。
1009
+ // 可见深度 pass:用各物体自身材质渲染(含地板;sprite/线条/点与显式 aoTransparent 面除外),
1010
+ // 产出 baseDepthTexture 供描边/发光/覆盖/磨砂做遮挡判定。
1023
1011
  // 必须在 base render 之后、所有依赖深度的特效(描边/发光/覆盖/磨砂)之前更新,
1024
1012
  // 否则这些特效会用到上一帧旧深度 → 遮挡判定错乱。法线 pass 已收进 ssao_pass 内部。
1025
- renderAoDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1013
+ renderVisibleDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1026
1014
  // 切回 baseRenderTarget,后处理 pass 需要把效果叠加到颜色渲染目标上
1027
1015
  this.renderer.setRenderTarget(this.baseRenderTarget);
1028
1016
  // 效果 mask 灰度图尺寸 = effectiveRatio × resolution,与 baseRenderTarget / baseDepthTexture 同倍率,
@@ -1088,7 +1076,7 @@ export default class ThreeBase extends EventDispatcher {
1088
1076
  // 分辨率按实际帧缓冲像素(已乘 dpr)设置,修复高分屏下 FXAA 采样比例偏大失效的问题。
1089
1077
  // ⚠️ 深度调试预览(面板「深度调试」切换,定位"深度测试全部失效"根因):
1090
1078
  // 旧控制台入口 `window.__BASE_DEPTH_VIS = true` 仍可用,等价于切到 "global"。
1091
- // global → baseDepthTexture 灰度。全白=深度为空(renderAoDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1079
+ // global → baseDepthTexture 灰度。全白=深度为空(renderVisibleDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1092
1080
  // ao → aoDepthTexture 灰度(含地板)。确认深度管线本身是否工作(GTAO 是否拿到正确深度)。
1093
1081
  // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
1094
1082
  // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
package/ThreeBase.ts CHANGED
@@ -19,7 +19,7 @@ import frosted_glass_pass from "./pass/frosted_glass_pass";
19
19
  import { flushLensflareOverlays } from "./lensflare/LensFlare";
20
20
  import glow_pass from "./pass/glow_pass";
21
21
  import stroke_pass from "./pass/stroke_pass";
22
- import ssao_pass, { renderAoDepthPass, renderExcludedMaskPass, visibleDepthOptions } from "./pass/ssao_pass";
22
+ import ssao_pass, { renderVisibleDepthPass, renderExcludedMaskPass } from "./pass/ssao_pass";
23
23
  import screenBloomPass from "./pass/screen_bloom_pass";
24
24
  import copyShader from "./shaders/copyShader";
25
25
  import { baseTextureToRenderShader } from "./shaders/baseTextureToRenderShader";
@@ -321,12 +321,13 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
321
321
  stencilBuffer: false,
322
322
  depthTexture: this.baseDepthTexture,
323
323
  });
324
- // AO 深度纹理(含地板):**不再单独渲染一遍** —— 它挂在 normalRenderTarget 上当 depth attachment,
325
- // 法线 pass(renderAoNormalPass,includeFloor=true)渲染时深度由光栅器顺带写入,白拿一张含地板深度。
326
- // (2026-09-14 优化:删掉原 ssao_pass 内那次 renderAoDepthPass(true),每帧省一次全场景遍历。)
327
- // SSAO 的 GTAO 采样它(地板保留为实体 → 接触 AO),合成时再与 depthRenderTarget
328
- // (地板排除 = 可见深度)比对,作废"透过半透明地板看到的实体"上的地板接触 AO。
329
- // 见 three-base/pass/ssao_pass.ts 的复合守卫说明。
324
+ // AO 深度纹理:**不再单独渲染一遍** —— 它挂在 normalRenderTarget 上当 depth attachment,
325
+ // 法线 pass(renderAoNormalPass,excludeTransparent=true)渲染时深度由光栅器顺带写入,白拿一张。
326
+ // (2026-09-14 优化:删掉原 ssao_pass 内那次独立的含地板深度 pass,每帧省一次全场景遍历。)
327
+ // SSAO 的 GTAO 采样它(与法线同源);合成时再与 depthRenderTarget(可见深度 = 各物体自身材质渲染)
328
+ // 比对:AO 侧更近说明那像素记到屏幕上不可见的面(抠图空隙 / 单面墙背面)→ 作废该像素 AO。
329
+ // ⚠️ 2026-10-11:地板(Ground1)现在两张图都写(旧的 aoFloor 排除特例已整条拆除),
330
+ // 见 three-base/pass/ssao_pass.ts 文件头与复合守卫说明。
330
331
  aoDepthTexture: DepthTexture = (() => {
331
332
  const t = new DepthTexture(4096, 4096);
332
333
  t.format = DepthFormat;
@@ -337,8 +338,8 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
337
338
  /**
338
339
  * 调试用:深度预览模式。由面板「深度调试」切换,便于逐一看各深度图排查"深度测试全部失效"根因。
339
340
  * - "none" :正常渲染(默认)
340
- * - "global" :仅预览全局深度图(baseDepthTexture,排除地板)。全白=深度为空→renderAoDepthPass 没写进深度。
341
- * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入的含地板深度)。用于确认深度管线本身是否工作。
341
+ * - "global" :仅预览可见深度图(baseDepthTexture,含地板)。全白=深度为空→renderVisibleDepthPass 没写进深度。
342
+ * - "ao" :仅预览 AO 深度图(aoDepthTexture = 法线 pass 顺带写入)。用于确认深度管线本身是否工作。
342
343
  * - "compare" :R=全局 base 深度,G=最近一次 glow/stroke 选区 select 深度。两路任一全白即该侧写入失败。
343
344
  * - "excluded":深度图灰度 + 「被排除出深度图的面」叠红(红=该面写不进深度,永远挡不住后处理)。
344
345
  * 物体区域(G 暗)若 B 全黑 → 判定从不触发;再看该区 R/G 谁大即可定位是谁缺深度。
@@ -347,20 +348,6 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
347
348
  */
348
349
  public debugDepthPreview: DebugDepthMode = "none";
349
350
 
350
- /**
351
- * 可见深度图是否包含「标了 userData.aoFloor 的地板」(如 Ground1)。
352
- *
353
- * 默认 false —— 原设计:地板可透视,透过地板看到的物体照常显示(不被地板挡住)。
354
- * 置 true 后地板会写进 baseDepthTexture,**被地板挡住的描边/辉光/毛玻璃/覆盖色会被正确遮挡**。
355
- * 排查"怎么都挡不住"时先开这个试一下:若开了就正常,说明挡你的正是可透视地板。
356
- */
357
- public get baseDepthIncludeFloor(): boolean {
358
- return visibleDepthOptions.includeFloor;
359
- }
360
- public set baseDepthIncludeFloor(v: boolean) {
361
- visibleDepthOptions.includeFloor = v;
362
- this.needRender = true;
363
- }
364
351
  // 法线专用渲染目标(无 MSAA,存储视图空间法线,供 GTAO 风格 SSAO 使用真实法线)
365
352
  normalRenderTarget: WebGLRenderTarget<Texture> = new WebGLRenderTarget(128, 128, {
366
353
  minFilter: LinearFilter,
@@ -1131,10 +1118,11 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
1131
1118
  this.renderer.setRenderTarget(this.baseRenderTarget);
1132
1119
  this.renderer.clear();
1133
1120
  this.renderer.render(this.scene, camera);
1134
- // 深度 pass:固定排除非实体表面后渲到 depthRenderTarget(生成 baseDepthTexture)。
1121
+ // 可见深度 pass:用各物体自身材质渲染(含地板;sprite/线条/点与显式 aoTransparent 面除外),
1122
+ // 产出 baseDepthTexture 供描边/发光/覆盖/磨砂做遮挡判定。
1135
1123
  // 必须在 base render 之后、所有依赖深度的特效(描边/发光/覆盖/磨砂)之前更新,
1136
1124
  // 否则这些特效会用到上一帧旧深度 → 遮挡判定错乱。法线 pass 已收进 ssao_pass 内部。
1137
- renderAoDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1125
+ renderVisibleDepthPass(this.renderer, camera, this.scene, this.depthRenderTarget);
1138
1126
  // 切回 baseRenderTarget,后处理 pass 需要把效果叠加到颜色渲染目标上
1139
1127
  this.renderer.setRenderTarget(this.baseRenderTarget);
1140
1128
  // 效果 mask 灰度图尺寸 = effectiveRatio × resolution,与 baseRenderTarget / baseDepthTexture 同倍率,
@@ -1204,7 +1192,7 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
1204
1192
  // 分辨率按实际帧缓冲像素(已乘 dpr)设置,修复高分屏下 FXAA 采样比例偏大失效的问题。
1205
1193
  // ⚠️ 深度调试预览(面板「深度调试」切换,定位"深度测试全部失效"根因):
1206
1194
  // 旧控制台入口 `window.__BASE_DEPTH_VIS = true` 仍可用,等价于切到 "global"。
1207
- // global → baseDepthTexture 灰度。全白=深度为空(renderAoDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1195
+ // global → baseDepthTexture 灰度。全白=深度为空(renderVisibleDepthPass 没写进)→后处理一律显示在前;有灰阶=深度正常,问题在比对侧。
1208
1196
  // ao → aoDepthTexture 灰度(含地板)。确认深度管线本身是否工作(GTAO 是否拿到正确深度)。
1209
1197
  // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
1210
1198
  // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
@@ -26,7 +26,7 @@ import type { BasinConfig, FluidParameters, FluidQuality, ReservoirState, WaterF
26
26
  * depthMode: 'logarithmic',
27
27
  * });
28
28
  * // 每帧:先 renderer.setRenderTarget(baseRenderTarget); renderer.render(scene, camera);
29
- * // 再 renderAoDepthPass(renderer, camera, scene, depthRenderTarget); // 写 baseDepthTexture
29
+ * // 再 renderVisibleDepthPass(renderer, camera, scene, depthRenderTarget); // 写 baseDepthTexture
30
30
  * // 然后 water.update(dt, elapsed);
31
31
  * // 最后 setRenderTarget(screenRT); fsQuad.render(renderer, pass.material);
32
32
  * // 注意:WaterFlow 的 composite 是纯消费 base 纹理的 fsQuad,可直接放进你的链。