three-base 2.0.172 → 2.0.180

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 (127) hide show
  1. package/FPScontrol.d.ts +15 -24
  2. package/FPScontrol.js +63 -87
  3. package/FPScontrol.ts +64 -88
  4. package/Ground1/Ground1.d.ts +4 -2
  5. package/Ground1/Ground1.js +77 -57
  6. package/Ground1/Ground1.ts +77 -57
  7. package/Mirror.js +0 -4
  8. package/Mirror.ts +0 -4
  9. package/MktMesh/MktMesh.d.ts +3 -2
  10. package/MktMesh/MktMesh.js +27 -8
  11. package/MktMesh/MktMesh.ts +24 -8
  12. package/MktMesh/boxLine2.d.ts +42 -0
  13. package/MktMesh/boxLine2.js +185 -0
  14. package/MktMesh/boxLine2.ts +222 -0
  15. package/MktMesh/imagePlane.d.ts +60 -0
  16. package/MktMesh/imagePlane.js +158 -0
  17. package/MktMesh/imagePlane.ts +203 -0
  18. package/MktMesh/imageTileLoader.d.ts +19 -0
  19. package/MktMesh/imageTileLoader.js +55 -0
  20. package/MktMesh/imageTileLoader.ts +64 -0
  21. package/MktMesh/mktMappingShader.d.ts +6 -5
  22. package/MktMesh/mktMappingShader.js +26 -19
  23. package/MktMesh/mktMappingShader.ts +28 -19
  24. package/MktMesh/mktShader.d.ts +127 -58
  25. package/MktMesh/mktShader.js +192 -117
  26. package/MktMesh/mktShader.ts +251 -127
  27. package/MktMesh/modelSlot.d.ts +123 -0
  28. package/MktMesh/modelSlot.js +339 -0
  29. package/MktMesh/modelSlot.ts +383 -0
  30. package/MktMesh/panoSlotRoster.d.ts +2 -2
  31. package/MktMesh/panoSlotRoster.js +9 -7
  32. package/MktMesh/panoSlotRoster.ts +9 -7
  33. package/MktMesh/panoTileStitcher.d.ts +20 -13
  34. package/MktMesh/panoTileStitcher.js +97 -76
  35. package/MktMesh/panoTileStitcher.ts +119 -90
  36. package/PipeMesh/PipeJoint.d.ts +22 -0
  37. package/PipeMesh/PipeJoint.js +33 -0
  38. package/PipeMesh/PipeJoint.ts +38 -0
  39. package/PipeMesh/PipeMesh.d.ts +94 -22
  40. package/PipeMesh/PipeMesh.js +192 -53
  41. package/PipeMesh/PipeMesh.ts +223 -76
  42. package/PipeMesh/PipeStraight.d.ts +19 -0
  43. package/PipeMesh/PipeStraight.js +34 -0
  44. package/PipeMesh/PipeStraight.ts +45 -0
  45. package/PipeMesh/axisSnap.d.ts +52 -0
  46. package/PipeMesh/axisSnap.js +52 -0
  47. package/PipeMesh/axisSnap.ts +93 -0
  48. package/PipeMesh/directRoute.d.ts +85 -0
  49. package/PipeMesh/directRoute.js +126 -0
  50. package/PipeMesh/directRoute.ts +190 -0
  51. package/PipeMesh/index.d.ts +6 -0
  52. package/PipeMesh/index.js +6 -0
  53. package/PipeMesh/index.ts +7 -1
  54. package/PipeMesh/pipeMaterial.d.ts +16 -0
  55. package/PipeMesh/pipeMaterial.js +23 -0
  56. package/PipeMesh/pipeMaterial.ts +26 -0
  57. package/ThreeBase.d.ts +1 -39
  58. package/ThreeBase.js +38 -147
  59. package/ThreeBase.ts +1176 -1302
  60. package/instanced/InstancedBatch.d.ts +14 -1
  61. package/instanced/InstancedBatch.js +53 -3
  62. package/instanced/InstancedBatch.ts +62 -3
  63. package/instanced/InstancedChild.d.ts +24 -0
  64. package/instanced/InstancedChild.js +40 -0
  65. package/instanced/InstancedChild.ts +41 -0
  66. package/instanced/InstancedGroup.d.ts +48 -0
  67. package/instanced/InstancedGroup.js +45 -1
  68. package/instanced/InstancedGroup.ts +85 -0
  69. package/package.json +22 -7
  70. package/pass/debugDepthState.d.ts +16 -8
  71. package/pass/debugDepthState.js +4 -0
  72. package/pass/debugDepthState.ts +27 -9
  73. package/pass/glow_pass.js +11 -94
  74. package/pass/glow_pass.ts +11 -97
  75. package/pass/ssao_pass.d.ts +9 -23
  76. package/pass/ssao_pass.js +41 -128
  77. package/pass/ssao_pass.ts +36 -166
  78. package/pass/stroke_pass.js +3 -0
  79. package/pass/stroke_pass.ts +4 -0
  80. package/shaders/debugDepthCompareShader.d.ts +1 -1
  81. package/shaders/debugDepthCompareShader.js +1 -1
  82. package/shaders/debugDepthCompareShader.ts +1 -1
  83. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  84. package/shaders/debugDepthJudgeShader.js +1 -1
  85. package/shaders/debugDepthJudgeShader.ts +1 -1
  86. package/PipeMesh/PathGeometry.d.ts +0 -44
  87. package/PipeMesh/PathGeometry.js +0 -307
  88. package/PipeMesh/PathGeometry.ts +0 -343
  89. package/PipeMesh/PathPoint.d.ts +0 -17
  90. package/PipeMesh/PathPoint.js +0 -31
  91. package/PipeMesh/PathPoint.ts +0 -36
  92. package/PipeMesh/PathPointList.d.ts +0 -31
  93. package/PipeMesh/PathPointList.js +0 -209
  94. package/PipeMesh/PathPointList.ts +0 -227
  95. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  96. package/PipeMesh/PathTubeGeometry.js +0 -130
  97. package/PipeMesh/PathTubeGeometry.ts +0 -146
  98. package/WaterFlow/FloatingDuck.d.ts +0 -88
  99. package/WaterFlow/FloatingDuck.js +0 -307
  100. package/WaterFlow/FloatingDuck.ts +0 -445
  101. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  102. package/WaterFlow/GPUFluidSimulation.js +0 -785
  103. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  104. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  105. package/WaterFlow/ReservoirSystem.js +0 -621
  106. package/WaterFlow/ReservoirSystem.ts +0 -754
  107. package/WaterFlow/SPHField.d.ts +0 -35
  108. package/WaterFlow/SPHField.js +0 -204
  109. package/WaterFlow/SPHField.ts +0 -228
  110. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  111. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  112. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  113. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  114. package/WaterFlow/ShallowWaterSurface.js +0 -441
  115. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  116. package/WaterFlow/WaterFlow.d.ts +0 -87
  117. package/WaterFlow/WaterFlow.js +0 -171
  118. package/WaterFlow/WaterFlow.ts +0 -219
  119. package/WaterFlow/types.d.ts +0 -161
  120. package/WaterFlow/types.js +0 -1
  121. package/WaterFlow/types.ts +0 -189
  122. package/WaterFlow/waterFlowPass.d.ts +0 -3
  123. package/WaterFlow/waterFlowPass.js +0 -20
  124. package/WaterFlow/waterFlowPass.ts +0 -40
  125. package/shaders/debugExcludedShader.d.ts +0 -15
  126. package/shaders/debugExcludedShader.js +0 -48
  127. package/shaders/debugExcludedShader.ts +0 -50
@@ -43,8 +43,8 @@ import { Color, DataTexture, DoubleSide, Matrix4, ShaderMaterial, SRGBColorSpace
43
43
  * 现口径已统一为等距柱状线性,皇上验收「不交换 / 不翻转」那态即正确 ⇒ **方向参数整条撤除、写死**
44
44
  * (见 🎛️ 段)。本诊断开关保留,用于日后再次核对「哪块瓦片落在哪个象限」。
45
45
  *
46
- * 用法:页面 URL 带 `?panoTileDebug=1` 时,两张槽纹理**不再采样纹理**,各自输出一个纯色 ——
47
- * ⭐ 10-06 晚起只有左右两槽(竖着不再分块):红=左半幅(槽0) 绿=右半幅(槽1)
46
+ * 用法:页面 URL 带 `?panoTileDebug=1` 时,三张槽纹理**不再采样纹理**,各自输出一个纯色 ——
47
+ * ⭐ 10-23 起横 3 槽(竖着不再分块):红=左列(槽0) 绿=中列(槽1) 蓝=右列(槽2)
48
48
  * (旧 4 象限版是 红=tiles[0] 绿=tiles[1] 蓝=tiles[2] 黄=tiles[3],已随四槽一并作废)
49
49
  * 皇上只要说出「画面上下左右各是什么颜色」,即可一次反推出真实映射。
50
50
  *
@@ -313,7 +313,7 @@ export function setMktPanoPoints(material, matrices) {
313
313
  *
314
314
  * 🧩 **第二个根因(与方向参数无关,故本次写死后依然保留)**:
315
315
  * 默认场景与旧存档的 `textures[].tiles` **只有 1 条地址**(当年上传切片功能之后才有的字段),
316
- * 喂料处回退成「各槽同为 `url`」(10-06 晚起是 2 槽:`[url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
316
+ * 喂料处回退成「各槽同为 `url`」(10-23 起是 3 槽:`[url, url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
317
317
  * ① 各槽同图 ⇒ 选哪一格都一样(当时看到的「交换零效果」症状,写死后该症状无从出现);
318
318
  * ② 更要命的是仍按「瓦片内 UV」`localU/localV` 去采整图 ⇒ 整图被按 1/2 拉伸铺满整个球面(旧版是 1/4):
319
319
  * 墙面仰角 +10° 采到原图 **−70°(脚底/三脚架)** —— 这才是「全景映射在模型上的位置不太对」的另一半,
@@ -321,7 +321,7 @@ export function setMktPanoPoints(material, matrices) {
321
321
  * 修法 = `uPanoTiled`(单全景标量 / 融合每点数组):0 时把瓦片内 UV **还原成整图 UV**
322
322
  * `panoU = u`、`panoV = v`(口径写死后这就是恒等式:横向 `0.5(2u)=u` / `0.5+0.5(2u−1)=u`;
323
323
  * 纵向 10-06 晚起 localV 本就等于 v ⇒ 两个方向都退化成恒等式)。
324
- * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~32 条」,10-06 晚旨只支持 8×4=32 张、不足涂灰),且每个回退喂料点都要写标记,
324
+ * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~9 条」,10-23 旨只支持 3×3=9 张、不足涂灰),且每个回退喂料点都要写标记,
325
325
  * 漏一处该对象就悄悄退回 1/2 拉伸。默认值取 1(已切片的真实上传数据行为不变)。
326
326
  * 护栏:`test/panoVMapping.test.js`。
327
327
  * 🔄 2026-09-22 追加(皇上口谕):本标记现由**用户可选的「全景纹理形态」`PanoTexMode`**(tiled/whole)与数据
@@ -395,41 +395,44 @@ const vertexShader = /* glsl */ `
395
395
  */
396
396
  // ⭐ 下面这一组「切片 / 拼合槽」几何常量**必须排在 `fragmentShader` 之前**:单全景那份片元是模块级
397
397
  // 常量字符串,里面要用 `${RT_STITCH_SLOT_COUNT}` 声明 sampler 数组长度(TS 的块级作用域禁止先用后声明)。
398
- /** ⭐ 32 瓦片拼合(2026-10-06 晚皇上旨:一张全景切 **横 8 × 竖 4 = 32 片**,每 **4×4 = 16 片**离屏拼成
399
- * 1 张 **4096×4096 正方形**槽纹理,**左右 2 槽**组成完整全景;上一版是同日早些时候的 8×8 = 64 片 / 4 槽
400
- * 2×2 象限,再上一版是 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
398
+ /** ⭐ 9 瓦片拼合(2026-10-23 皇上旨:一张全景切 **横 3 × 竖 3 = 9 片**,每 **横 1 × 竖 3 = 3 片**离屏拼成
399
+ * 1 张 **4096×6144** 槽纹理,**左/中/右 3 槽**组成完整全景;上一版是 10-06 晚旨的 8×4 = 32 片 / 左右 2 槽
400
+ * 4096²,再上一版 8×8 = 64 片 / 4 槽 2×2 象限,最早 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
401
401
  * 虚拟键不是真 URL,纹理由 `PanoTileStitcher` 用 shader 逐格画进 `WebGLRenderTarget` 提供,
402
402
  * 消费方(基类 `_ensurePanoTiles` / 编辑器 `_peekTileAt`)见前缀即走拼合器,⛔ 不得当 URL 下载。
403
403
  * ⚠️ 键协议 `rt:<batch>#<slot>` 与 `uPanoTiled` 这套对外接口**一字未改** —— 变的只有「每批几条地址」
404
- * 和「槽怎么分」;`slot` 的取值范围从 0..3 缩到 0..1,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
405
- * ⚠️ 旧存档不兼容(皇上 2026-10-06 拍板):存档里 64 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
404
+ * 和「槽怎么分」;`slot` 取值 0..2,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
405
+ * ⚠️ 旧存档不兼容(与 10-06 同一口径):存档里 32 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
406
406
  export const RT_STITCH_KEY_PREFIX = "rt:";
407
- /** 原图**横向**(经度方向 u)切几片(旨「横 8」) */
408
- export const RT_STITCH_COLS = 8;
409
- /** 原图**纵向**(纬度方向 v)切几片(旨「竖 4」)⇒ 竖着不再分块,一个 RT 就装下整幅的上下 180° */
410
- export const RT_STITCH_ROWS = 4;
411
- /** 一个批次(= 一个全景点)的瓦片总数 = 8 × 4 = 32;`tiles` 存档数组的硬上限 */
407
+ /** 原图**横向**(经度方向 u)切几片(旨「横 3」) */
408
+ export const RT_STITCH_COLS = 3;
409
+ /** 原图**纵向**(纬度方向 v)切几片(旨「竖 3」)⇒ 一槽在纵向上装满整幅 3 片,横向才分左/中/右 */
410
+ export const RT_STITCH_ROWS = 3;
411
+ /** 一个批次(= 一个全景点)的瓦片总数 = 3 × 3 = 9;`tiles` 存档数组的硬上限 */
412
412
  export const RT_STITCH_TILES_PER_BATCH = RT_STITCH_COLS * RT_STITCH_ROWS;
413
- /** 一个拼合槽每边几格(旨「每个 RT 由 16 个全景瓦片组成」⇒ 4×4 = 16)*/
414
- export const RT_STITCH_SLOT_GRID = 4;
415
- /** 一个拼合槽的格数 = 4² = 16(画满 16 格 = 这一槽齐活) */
416
- export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_GRID * RT_STITCH_SLOT_GRID;
417
- /** 拼合槽个数 = **左右 2 槽**(旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 一个全景点只占 **2 个 sampler**。
418
- * ⛔ 别以为 32 片就要 32 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16 = 2 槽 × 8 景点,
419
- * 这一条是硬顶,也是「必须拼进 RT」而不是「直接喂瓦片」的根本原因。
420
- * ⭐ 分块口径:槽 0 = **左半幅**(列 0-3、行 0-3)、槽 1 = **右半幅**(列 4-7、行 0-3),
421
- * 即「只按 u 分左右,v 不再分上下」⇒ 每槽覆盖 **横向 180° × 纵向 180°**,是原图左/右半个**正方形**
422
- * ⇒ 槽纹理必须跟着变正方形(见下 `RT_STITCH_SLOT_W/H`),否则纵向被压 2 倍。 */
423
- export const RT_STITCH_SLOT_COUNT = 2;
424
- /** 单槽像素尺寸 **4096×4096 正方形**(皇上 2026-10-06 拍板保画质):2 槽 = 有效分辨率 8192×4096。
425
- * ⚠️ 为什么必须变正方形:每槽覆盖的原图区域从「横 180° × 竖 90°(2:1)」变成「横 180° × 竖 180°(1:1)」,
426
- * RT 若仍是 4096×2048 ⇒ 纵向缩放比是横向的 2 倍、**纵向分辨率腰斩**,直接踩「不降质」红线。
427
- * 每格 = 1024×1024;一片瓦片的原图边长 = 横 W/8、竖 H/4 ⇒ 横向缩放比 = (W/8)/1024 = 1.4533(11904 宽原图),
428
- * 与 64 片那一版**逐字相同** ⇒ 本次改版**不涨也不降画质**;变的是同屏全景数 4 → 8、
429
- * 单全景下载片数 64 → 32(减半)、单全景 RT 显存 128MB(不变,只是从 4 张 32MB 变 2 张 64MB)。
413
+ /** 一个拼合槽**横向**几格(旨「竖直排列 3 个瓦片」⇒ 一槽就是原图的一整列,横 1 格)*/
414
+ export const RT_STITCH_SLOT_CELL_COLS = 1;
415
+ /** 一个拼合槽**纵向**几格(旨「竖直排列 3 个瓦片」⇒ 竖 3 格)
416
+ * ⚠️ 10-23 起横竖**不再同数**:旧字段 `RT_STITCH_SLOT_GRID`(方形假设)已整条作废,
417
+ * 格数、格位、quad 摆位一律分轴取(`rtStitchCellPos` / `PanoTileStitcher._stitchCell`)。 */
418
+ export const RT_STITCH_SLOT_CELL_ROWS = 3;
419
+ /** 一个拼合槽的格数 = 1 × 3 = 3(画满 3 格 = 这一槽齐活) */
420
+ export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_CELL_COLS * RT_STITCH_SLOT_CELL_ROWS;
421
+ /** 拼合槽个数 = **左/中/右 3 槽**(旨「全景的 RT 由原来的 2 个分成 3 个」)⇒ 一个全景点占 **3 个 sampler**。
422
+ * ⛔ 别以为 9 片就要 9 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 **16** 是硬顶,
423
+ * 3 槽 × 同屏 8 景点 = 24 会连编都编不过 ⇒ 同屏景点数由 `MKT_PANO_MAX_SLOTS` 收到 **5**(5×3=15 ≤ 16)。
424
+ * ⭐ 分块口径:槽 `s` = 原图**第 s 列**(列 s、行 0-2)⇒ 每槽覆盖 **横向 120° × 纵向 180°**,
425
+ * 与「一槽一列、竖 3 格」同一条口径(槽纹理尺寸见下 `RT_STITCH_SLOT_W/H`)。 */
426
+ export const RT_STITCH_SLOT_COUNT = 3;
427
+ /** 单槽像素尺寸 **4096×6144**(皇上 2026-10-23 旨「每个 RT 的尺寸是 4096 * 6144 竖直排列 3 个瓦片」):
428
+ * 3 槽 = 有效分辨率 **12288×6144**,与 3×3 切片的原图(12288×6144 ⇒ 每片 4096×2048)**逐像素 1:1**;
429
+ * 上一版 2 槽 4096² 是把 71.6 Mpx 的原图**缩小**塞进 33.5 Mpx ⇒ 本次是升分辨率,⛔ 不是降质。
430
+ * ⚠️ 每格 = 4096×2048(横竖**不等** ⇒ 摆格必须分轴,见 `RT_STITCH_SLOT_CELL_COLS/ROWS`)。
431
+ * ⚠️ 两条代价(臣已量过并报给皇上拍板):单全景 RT 显存 128MB → **302MB**(×2.25,mipmap 再 +1/3);
432
+ * sampler 3 槽 × 8 景点 = 24 超硬顶 16 ⇒ 同屏景点 `MKT_PANO_MAX_SLOTS` 8 → **5**。
430
433
  * ⚠️ 红线(「千万不要降低质量」):这两个数与「原图切几片」无关,⛔ 不许当成省显存的旋钮去缩。 */
431
434
  export const RT_STITCH_SLOT_W = 4096;
432
- export const RT_STITCH_SLOT_H = 4096;
435
+ export const RT_STITCH_SLOT_H = 6144;
433
436
  export const floorVertexShader = /* glsl */ `
434
437
  #include <common>
435
438
  #include <logdepthbuf_pars_vertex>
@@ -478,13 +481,29 @@ export const floorVertexShader = /* glsl */ `
478
481
  #include <logdepthbuf_vertex>
479
482
  }
480
483
  `;
484
+ /** 槽下标必须在编译期是常量(ANGLE/D3D 禁止动态索引 sampler 数组)⇒ 按 `RT_STITCH_SLOT_COUNT`
485
+ * 生成「字面量下标 if 链」。`emit(槽下标)` 给出该分支的赋值语句,最后一条写成裸 `else` 兜住 u==1 的边界。 */
486
+ const panoSlotBranches = (emit) => {
487
+ const last = RT_STITCH_SLOT_COUNT - 1;
488
+ const out = [];
489
+ for (let s = 0; s <= last; s++) {
490
+ out.push(s === last
491
+ ? `else ${emit(s)}`
492
+ : s === 0
493
+ ? `if (tileIdx == ${s}) ${emit(s)}`
494
+ : `else if (tileIdx == ${s}) ${emit(s)}`);
495
+ }
496
+ return out.join("\n ");
497
+ };
498
+ /** 🔬 归属诊断用的每槽色相(`?panoTile=1` 那条直通分支;条数必须 ≥ `RT_STITCH_SLOT_COUNT`) */
499
+ const PANO_SLOT_DEBUG_COLORS = ["vec3(1.0, 0.0, 0.0)", "vec3(0.0, 1.0, 0.0)", "vec3(0.0, 0.0, 1.0)"];
481
500
  const fragmentShader = /* glsl */ `
482
501
  // 对数深度缓冲(与顶点着色器配套,保证 MktMesh 深度与场景一致)
483
502
  #include <logdepthbuf_pars_fragment>
484
503
 
485
- // 一个全景点 = 2 张**拼合槽纹理**(原图切 8×4=32 片,每 4×4=16 片拼成一张槽,10-06 晚皇上旨)。
486
- // panoramas[0..1] = [左半幅, 右半幅] 两个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
487
- // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(tileIdx 命中的分支内 sampler 下标为字面量)。
504
+ // 一个全景点 = 3 张**拼合槽纹理**(原图切 3×3=9 片,每「横 1 × 竖 3」= 3 片拼成一张槽,10-23 皇上旨)。
505
+ // panoramas[0..2] = [左列, 中列, 右列] 三个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 恒全高)。
506
+ // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(见 panoSlotBranches)。
488
507
  uniform sampler2D panoramas[${RT_STITCH_SLOT_COUNT}];
489
508
  uniform mat4 textureMatrix; // 纹理世界矩阵:位置(第4列 xyz)=投影中心,旋转(左上3x3)=纹理朝向
490
509
  uniform float uPanoramaVisible; // 全景投射显隐:1=显示全景,0=仅显示中性素色(编辑态切换/投射显隐开关用)
@@ -492,9 +511,9 @@ const fragmentShader = /* glsl */ `
492
511
  // 原 uniform float uColorSpace; + 片元里那段 sRGB→linear 的 pow 分支已整条撤除,勿加回(原因见下方采样处)。
493
512
  // 全景上下方向**已写死**(等距柱状线性 v + 不交换 + 不翻转,2026-09-22 皇上定论,理由见文件头 🎛️ 段):
494
513
  // 曾有 uPanoSwapRow / uPanoFlipTile / uPanoVertOff 三个可调 uniform,现已整条撤除,勿加回。
495
- // ⚠️ uPanoTiled = panoramas[0..1] 装的是「拼合好的左右两张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
514
+ // ⚠️ uPanoTiled = panoramas[0..2] 装的是「拼合好的左/中/右三张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
496
515
  // 由「用户选的纹理形态 PanoTexMode + 该点有没有切过片」共同决定(见下方 mktPanoTiledFlag)。
497
- // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左半幅拉伸到整个
516
+ // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左一档拉伸到整个
498
517
  // 球面(墙面仰角 +10° 采到原图 −70° 的地板)—— 2026-09-22「全景位置不对」的第二真因,见文件头 🧩 段。
499
518
  uniform float uPanoTiled;${PANO_TINT_ON ? `
500
519
  // 🔬 归属诊断色(仅 ?panoTint=1 时存在):本材质当前投的就是「选中的那一个」全景,
@@ -531,29 +550,27 @@ const fragmentShader = /* glsl */ `
531
550
  float lat = asin(clamp(dir.y, -1.0, 1.0));
532
551
  float v = 0.5 + lat * 0.3183098861837907; // 1/PI
533
552
 
534
- // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
535
- // ⭐ 2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:**竖着不再分块** ⇒ 槽只随 **u** 走,
536
- // v 方向一槽装下整幅上下 180°(这也是槽纹理必须改成 4096² 正方形的原因,见 RT_STITCH_SLOT_W/H)。
537
- // ⛔ 别把 tileRow 加回来:加回来 = 又变成 2×2 四槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽。
538
- int tileIdx = (u >= 0.5) ? 1 : 0;
539
- // 槽内局部 UV(连续、无 fract 边界跳变):横向按左右减半;
540
- // ⛔ 纵向**不再减半**(槽就是全高)⇒ localV 恒等于 v。写成 2v / 2v-1 = 纵向分辨率白丢一半。
541
- float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
553
+ // 左/中/右三列 -> 拼合槽下标 [左列=0, 中列=1, 右列=2](每槽 = 3×3 网格里「横 1 × 竖 3」= 3 片,见 RT_STITCH_SLOT_TILES)
554
+ // ⭐ 2026-10-23 旨「全景的 RT 由原来的 2 个分成 3 个、每个 4096×6144 竖直排列 3 个瓦片」:
555
+ // 槽只随 **u** 三等分走,v 方向一槽装下整幅上下 180°(槽纹理尺寸见 RT_STITCH_SLOT_W/H)。
556
+ int tileIdx = int(clamp(floor(u * ${RT_STITCH_SLOT_COUNT}.0), 0.0, ${RT_STITCH_SLOT_COUNT - 1}.0));
557
+ // 槽内局部 UV(连续、无 fract 边界跳变):横向按槽数等分;
558
+ // ⛔ 纵向**不再切**(槽就是全高)⇒ localV 恒等于 v。写成 nv / nv-1 = 纵向分辨率白丢一档。
559
+ float localU = u * ${RT_STITCH_SLOT_COUNT}.0 - float(tileIdx);
542
560
  float localV = v;
543
561
 
544
- // 🧩 uPanoTiled == 0(两槽装的是**同一张完整等距柱状图**,没有拼合槽):
562
+ // 🧩 uPanoTiled == 0(各槽装的是**同一张完整等距柱状图**,没有拼合槽):
545
563
  // 必须还原成**全图 UV** 再采样,也就是直接用 (u, v) ——
546
- // 横向还原式 (半==左 ? 0.0 : 0.5) + 0.5*localU 代入 localU 后恒等于 u;纵向 localV 本就等于 v,
564
+ // 横向还原式 (tileIdx/N) + localU/N 代入后恒等于 u;纵向 localV 本就等于 v,
547
565
  // **两个方向都退化成恒等式**(方向已写死:无交换、无翻转、无偏移)。
548
- // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左半幅铺满整个球面。
566
+ // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左一档铺满整个球面。
549
567
  float panoU = (uPanoTiled > 0.5) ? localU : u;
550
568
  float panoV = (uPanoTiled > 0.5) ? localV : v;
551
569
 
552
570
  // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
553
571
  if (${TILE_DEBUG_GLSL} > 0.5) {
554
572
  vec3 dbgCol;
555
- if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
556
- else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
573
+ ${panoSlotBranches((s) => `dbgCol = ${PANO_SLOT_DEBUG_COLORS[s]};`)}
557
574
  gl_FragColor = vec4(dbgCol, 1.0);
558
575
  #include <colorspace_fragment>
559
576
  #include <logdepthbuf_fragment>
@@ -566,8 +583,7 @@ const fragmentShader = /* glsl */ `
566
583
  // 末端的 <colorspace_fragment> / 终处理再编码一次 ⇒ 一进一出,屏幕上与源图逐字节还原。
567
584
  // ⛔ 别加回那段 pow():它在硬件已解码之上再解一遍 = 二次解码压暗(中灰 #808080 掉到 #373737、#333333 掉到 #080808)。
568
585
  vec3 projCol;
569
- if (tileIdx == 0) projCol = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
570
- else projCol = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
586
+ ${panoSlotBranches((s) => `projCol = texture2D(panoramas[${s}], vec2(panoU, panoV)).rgb;`)}
571
587
 
572
588
  // 全景投射显隐:关闭时(仅墙体/地板,回落中性素色,不投射全景);默认 1 完全显示
573
589
  if (uPanoramaVisible < 0.5) {
@@ -600,9 +616,15 @@ ${PANO_TINT_ON ? `
600
616
  #include <logdepthbuf_fragment>
601
617
  }
602
618
  `;
603
- /** 1×1 白色占位纹理:panorama 在尚未激活任何纹理前使用,避免空纹理采样报错 */
619
+ /** 1×1 **中性灰**占位纹理:panorama 在尚未激活任何纹理 / 该点还没设图片时使用,既避免空纹理采样报错,
620
+ * 也让「没图」这件事在屏幕上是**一片纯灰**而不是一块白。
621
+ *
622
+ * ⭐ 2026-10-25 皇上旨「把默认图去掉,图片没有加载的时候就显示纯灰色」:原先是纯白 `[255,255,255,255]`。
623
+ * 色值取 **46 = round(0.18 × 255)**,与 mapping shader 的 `uFaceMissingColor`(线性 0.18 ≈ #767676)逐字一致;
624
+ * DataTexture 默认 `NoColorSpace`(不做 sRGB 解码)⇒ 采样出来就是 0.18 线性,经终处理 sRGB 编码后正是 #767676。
625
+ * ⛔ 别改成 sRGB 的 118:那样会被当线性值用,屏幕上是 #B4B4B4,与缺失格的灰对不上。 */
604
626
  export function createPlaceholderTexture() {
605
- const tex = new DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
627
+ const tex = new DataTexture(new Uint8Array([46, 46, 46, 255]), 1, 1);
606
628
  tex.needsUpdate = true;
607
629
  return tex;
608
630
  }
@@ -628,7 +650,76 @@ export const MISSING_TILE_GRAY = "#767676";
628
650
  * `2 × 1.45 ≈ 3` texel ⇒ 8px 有 ≈2.7 倍余量;再小一档(4px)也够用,取 8 是给「原图比 8192 窄 ⇒ 格子
629
651
  * 反而在放大」那类点留边界。**代价**:每片文件比「正好 1/8」宽 16px、高 16px(≈ +4% 面积),⛔ 不是降质。 */
630
652
  export const RT_STITCH_SEAM_MARGIN_PX = 8;
631
- /** 32 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
653
+ /** 全景那一份网格 = 上面这组 `RT_STITCH_*` 常数的**同一份数值**(⛔ 不是另立一套:常数由它反向派生) */
654
+ export const PANO_TILE_GRID = {
655
+ cols: RT_STITCH_COLS,
656
+ rows: RT_STITCH_ROWS,
657
+ slotCellCols: RT_STITCH_SLOT_CELL_COLS,
658
+ slotCellRows: RT_STITCH_SLOT_CELL_ROWS,
659
+ slotCount: RT_STITCH_SLOT_COUNT,
660
+ seamMarginPx: RT_STITCH_SEAM_MARGIN_PX,
661
+ };
662
+ /**
663
+ * 图片平面(皇上旨 2026-10-20「先添加一个平面,再给这个平面上传图片,上传的这个图切成 4*4=16 片」
664
+ * → 2026-10-21 改口「切成 2*2=4 份,不用切成 16 份了」)的网格:
665
+ * 横 2 × 竖 2 = 4 片,全部装进 **1 个拼合槽**(平面是一张普通矩形贴图,不像全景那样左右半幅分 sampler)。
666
+ * `seamMarginPx` 与全景**同档**(8px):缝的病根一模一样(格子边界上采样到邻居),这一档不随网格重调,
667
+ * ⛔ 也不为这一旨新加「图片专用缝宽」那种旋钮(网格只有一份来源,改片数只动 cols/rows/slotCellCols/slotCellRows)。
668
+ * ⚠️ 改这里只影响**新上传**的平面:老存档里那 16 条地址喂进 4 片的布局会被 `ensure` 的
669
+ * 「张数 > 满批 ⇒ 直接不收」挡掉(平面停在缺失灰),按 10-20 的口径不做兼容、重传即可。
670
+ */
671
+ export const IMAGE_TILE_GRID = {
672
+ cols: 2,
673
+ rows: 2,
674
+ slotCellCols: 2,
675
+ slotCellRows: 2,
676
+ slotCount: 1,
677
+ seamMarginPx: RT_STITCH_SEAM_MARGIN_PX,
678
+ };
679
+ /** 该网格一批几条地址 */
680
+ export function tilesPerBatchOf(grid) {
681
+ return grid.cols * grid.rows;
682
+ }
683
+ /** 该网格一槽几格 */
684
+ export function cellsPerSlotOf(grid) {
685
+ return grid.slotCellCols * grid.slotCellRows;
686
+ }
687
+ /**
688
+ * 槽 `slot` 用哪几片瓦片(原图 `cols × rows` 行优先下标 `r*cols+c`;槽内格序 = `slotCellCols×slotCellRows`
689
+ * 行优先、图像左上起)。
690
+ * ⛔ 手抄下标表必然出错(全景那张表就是靠生成 + 护栏逐个数钉住的),故两种网格共用这一座生成器。
691
+ */
692
+ export function slotTilesOf(grid) {
693
+ // 横向几个槽(cols/slotCellCols);纵向槽数 = rows/slotCellRows(全景 3/3=1 ⇒ sr 恒 0 = 全高)
694
+ const slotCols = grid.cols / grid.slotCellCols;
695
+ const slots = [];
696
+ for (let slot = 0; slot < grid.slotCount; slot++) {
697
+ const sr = Math.floor(slot / slotCols) * grid.slotCellRows;
698
+ const sc = (slot % slotCols) * grid.slotCellCols;
699
+ const ids = [];
700
+ for (let r = 0; r < grid.slotCellRows; r++) {
701
+ for (let c = 0; c < grid.slotCellCols; c++)
702
+ ids.push((sr + r) * grid.cols + (sc + c));
703
+ }
704
+ slots.push(ids);
705
+ }
706
+ return slots;
707
+ }
708
+ /** 全景的标准布局(3×3 网格 / 3 槽 / 4096×6144 RT)—— 一槽 = 原图一整列 */
709
+ export const PANO_TILE_LAYOUT = { ...PANO_TILE_GRID, rtW: RT_STITCH_SLOT_W, rtH: RT_STITCH_SLOT_H };
710
+ /**
711
+ * 图片平面的布局:2×2 网格 / 1 槽,RT 宽高比 = 原图宽高比,长边 4096(10-21 旨把 16 片改成 4 片,
712
+ * RT 那一份分辨率一个字没动 ⇒ 最终盖在平面上的仍是 4096×按比例 这一张,每格 RT 像素 / 每格原图像素的
713
+ * 配比也与 16 片那版恒同,⛔ 不是降质,只是少切 12 片、待画格从 16 格减到 4 格)。
714
+ * 平面材质 UV [0,1]² ⇒ 不管平面尺寸多大都不会拉伸,RT 尺寸只跟原图走。
715
+ */
716
+ export function imageTileLayout(natW, natH) {
717
+ const LONG = 4096;
718
+ const rtW = natW >= natH ? LONG : Math.round(LONG * natW / natH);
719
+ const rtH = natH > natW ? LONG : Math.round(LONG * natH / natW);
720
+ return { ...IMAGE_TILE_GRID, rtW, rtH };
721
+ }
722
+ /** 9 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
632
723
  * 09-29 晚旨「不足张数缺的地方涂灰」后批次里允许有空串占位 ⇒ 取首个非空,防首张缺失时键为空串互撞) */
633
724
  export function rtStitchBatchOf(tiles) {
634
725
  return tiles.find((u) => u) || "";
@@ -637,7 +728,7 @@ export function rtStitchBatchOf(tiles) {
637
728
  export function rtStitchKey(batch, slot) {
638
729
  return `${RT_STITCH_KEY_PREFIX}${batch}#${slot}`;
639
730
  }
640
- /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 片 / 64 片 / 现行 32 片**同一个前缀**,键本身不含片数信息 */
731
+ /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 / 32 / 64 / 现行 9 片**同一个前缀**,键本身不含片数信息 */
641
732
  export function isRtStitchKey(key) {
642
733
  return key.startsWith(RT_STITCH_KEY_PREFIX);
643
734
  }
@@ -651,77 +742,61 @@ export function rtStitchKeyParts(key) {
651
742
  return null;
652
743
  return { batch: body.slice(0, i), slot: Number(body.slice(i + 1)) };
653
744
  }
654
- /** 槽 slot 用哪 16 张瓦片(原图 **8 列 × 4 行 行优先**下标 `r*8+c`;**槽内格序 = 4×4 行优先、图像左上起**):
655
- * 槽 0 = 图像**左半幅**(行 0-3 × 列 0-3)→ `[0,1,2,3, 8,9,10,11, 16,17,18,19, 24,25,26,27]`;
656
- * 槽 1 = **右半幅**(行 0-3 × 列 4-7)→ `[4,5,6,7, 12,13,14,15, 20,21,22,23, 28,29,30,31]`。
657
- * ⭐ 竖着**不再分块**(皇上旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 两个槽**都是全高**,只按 u 分左右,
658
- * 故槽纹理也必须跟着变正方形(见 `RT_STITCH_SLOT_W/H` 那条注释,⛔ 别只改这里漏了那边)。
659
- * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 32 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
745
+ /** 槽 slot 用哪 3 张瓦片(原图 **3 列 × 3 行 行优先**下标 `r*3+c`;**槽内格序 = 横 1 × 竖 3、图像左上起**):
746
+ * 槽 0 = **左列** → `[0,3,6]`;槽 1 = **中列** → `[1,4,7]`;槽 2 = **右列** → `[2,5,8]`。
747
+ * ⭐ 只按 **u** 分槽(皇上旨 2026-10-23「每个 RT 竖直排列 3 个瓦片」⇒ 一槽 = 原图一整列 = 横 120° × 全高 180°),
748
+ * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 9 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
660
749
  * 对着手写死的字面量逐个数断言 —— 改了网格常数而护栏没跟着改,当场红。 */
661
- export const RT_STITCH_SLOT_TILES = (() => {
662
- // 横向几个槽(COLS/SLOT_GRID = 8/4 = 2);纵向恒 1 个槽(ROWS/SLOT_GRID = 4/4 = 1 ⇒ sr 恒 0 = 全高)
663
- const slotCols = RT_STITCH_COLS / RT_STITCH_SLOT_GRID;
664
- const slots = [];
665
- for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
666
- const sr = Math.floor(slot / slotCols) * RT_STITCH_SLOT_GRID;
667
- const sc = (slot % slotCols) * RT_STITCH_SLOT_GRID;
668
- const ids = [];
669
- for (let r = 0; r < RT_STITCH_SLOT_GRID; r++) {
670
- for (let c = 0; c < RT_STITCH_SLOT_GRID; c++)
671
- ids.push((sr + r) * RT_STITCH_COLS + (sc + c));
672
- }
673
- slots.push(ids);
674
- }
675
- return slots;
676
- })();
750
+ export const RT_STITCH_SLOT_TILES = slotTilesOf(PANO_TILE_GRID);
677
751
  /** 槽内第 cell 格在**图像方向**的位置 `[列, 行]`(0 起,与 `RT_STITCH_SLOT_TILES` 的格序同一口径)。
678
- * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。 */
679
- export function rtStitchCellPos(cell) {
680
- return [cell % RT_STITCH_SLOT_GRID, Math.floor(cell / RT_STITCH_SLOT_GRID)];
752
+ * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。
753
+ * `slotCellCols` 默认全景那档(1 = 一槽就是一竖列);图片平面传 2。 */
754
+ export function rtStitchCellPos(cell, slotCellCols = PANO_TILE_GRID.slotCellCols) {
755
+ return [cell % slotCellCols, Math.floor(cell / slotCellCols)];
681
756
  }
682
757
  /** 网格取整:第 `i` 条分界线在原图 `n` 像素上的位置(`round` ⇒ 余数摊到各格,
683
758
  * ⛔ 不必再像旧上传代码那样特判「末行末列吸收余数」)。
684
- * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):10-06 晚起两个方向
685
- * **不再相等**(8×4),必须显式传,⛔ 不许再拿一个「每边片数」两边共用 —— 竖着按 8 切 = 纵向切错。 */
759
+ * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):两个方向必须各自显式传,
760
+ * ⛔ 不许再拿一个「每边片数」两边共用 —— 横 3 竖 8 这类混用 = 纵向切错。 */
686
761
  function gridLine(i, n, steps) {
687
762
  return Math.round((i * n) / steps);
688
763
  }
689
- /** 32 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*8+c` 与 `RT_STITCH_SLOT_TILES` 同源;
690
- * ⭐ 10-06 晚旨「横 8 × 竖 4 = 32」:`r` 走 `RT_STITCH_ROWS`(4)、`c` 走 `RT_STITCH_COLS`(8),不再相等)。
764
+ /** 9 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*3+c` 与 `RT_STITCH_SLOT_TILES` 同源;
765
+ * ⭐ 10-23 旨「横 3 × 竖 3 = 9」:`r` 走 `RT_STITCH_ROWS`(3)、`c` 走 `RT_STITCH_COLS`(3)。
691
766
  *
692
767
  * ⭐ **上传侧(`panoramaUpload`)与拼合侧(`PanoTileStitcher`)必须共用这一份几何**:
693
768
  * 上传多切多少、拼合就内缩多少;两边各算一次就是「缝没治掉、反而把画面挤歪」。
694
- * 拼合侧拿不到原图尺寸(它只有 32 个 URL 和 32 张已解码纹理),所以它用 `rtStitchTileInset`
769
+ * 拼合侧拿不到原图尺寸(它只有 9 个 URL 和 9 张已解码纹理),所以它用 `rtStitchTileInset`
695
770
  * —— 那条只需「下标 + 该瓦片自己的像素尺寸」,两者等价的前提是「多切量 = 常数 M、只有贴边那几片被裁」,
696
771
  * 而「贴不贴边」纯由下标决定 ⇒ 护栏 BE 组用同一批 (W,H) 把两条算路对表钉死。 */
697
- export function rtStitchTileRects(w, h) {
772
+ export function rtStitchTileRects(w, h, grid = PANO_TILE_GRID) {
698
773
  const out = [];
699
- for (let r = 0; r < RT_STITCH_ROWS; r++) {
700
- for (let c = 0; c < RT_STITCH_COLS; c++) {
701
- const x0 = gridLine(c, w, RT_STITCH_COLS);
702
- const x1 = gridLine(c + 1, w, RT_STITCH_COLS);
703
- const y0 = gridLine(r, h, RT_STITCH_ROWS);
704
- const y1 = gridLine(r + 1, h, RT_STITCH_ROWS);
705
- const ml = Math.min(RT_STITCH_SEAM_MARGIN_PX, x0);
706
- const mr = Math.min(RT_STITCH_SEAM_MARGIN_PX, w - x1);
707
- const mt = Math.min(RT_STITCH_SEAM_MARGIN_PX, y0);
708
- const mb = Math.min(RT_STITCH_SEAM_MARGIN_PX, h - y1);
774
+ for (let r = 0; r < grid.rows; r++) {
775
+ for (let c = 0; c < grid.cols; c++) {
776
+ const x0 = gridLine(c, w, grid.cols);
777
+ const x1 = gridLine(c + 1, w, grid.cols);
778
+ const y0 = gridLine(r, h, grid.rows);
779
+ const y1 = gridLine(r + 1, h, grid.rows);
780
+ const ml = Math.min(grid.seamMarginPx, x0);
781
+ const mr = Math.min(grid.seamMarginPx, w - x1);
782
+ const mt = Math.min(grid.seamMarginPx, y0);
783
+ const mb = Math.min(grid.seamMarginPx, h - y1);
709
784
  out.push({ sx: x0 - ml, sy: y0 - mt, sw: x1 + mr - (x0 - ml), sh: y1 + mb - (y0 - mt), ml, mt, mr, mb });
710
785
  }
711
786
  }
712
787
  return out;
713
788
  }
714
- /** 这片瓦片有没有贴原图边界(8×4 ⇒ 32 片里四周那 20 片至少贴一条边;贴边的**那一侧**多切量被裁成 0)。
789
+ /** 这片瓦片有没有贴原图边界(3×3 ⇒ 9 片里四周那 8 片至少贴一条边,只有正中心那片四边都不贴;贴边的**那一侧**多切量被裁成 0)。
715
790
  * 拼合侧靠它 + 瓦片自己的像素尺寸还原内缩量 ⇒ 不必知道原图多大。
716
- * ⚠️ 下标拆分必须按 **列数 COLS=8**(`i = r*8+c`),⛔ 不是 ROWS。 */
717
- export function rtStitchTileEdgeClip(i) {
718
- const r = Math.floor(i / RT_STITCH_COLS);
719
- const c = i % RT_STITCH_COLS;
791
+ * ⚠️ 下标拆分必须按 **列数 COLS**(`i = r*cols+c`),⛔ 不是 ROWS。 */
792
+ export function rtStitchTileEdgeClip(i, grid = PANO_TILE_GRID) {
793
+ const r = Math.floor(i / grid.cols);
794
+ const c = i % grid.cols;
720
795
  return {
721
796
  left: c === 0,
722
- right: c === RT_STITCH_COLS - 1,
797
+ right: c === grid.cols - 1,
723
798
  top: r === 0,
724
- bottom: r === RT_STITCH_ROWS - 1,
799
+ bottom: r === grid.rows - 1,
725
800
  };
726
801
  }
727
802
  /** 拼合时的采样窗口 = 该瓦片纹理的 UV 里「**只留基础格、丢掉多切那一圈**」的那一块,
@@ -733,9 +808,9 @@ export function rtStitchTileEdgeClip(i) {
733
808
  *
734
809
  * ⚠️ v 方向口径(与 `panoBitmapTexture` 绑死):瓦片走 `imageOrientation:"flipY"` 预翻 + `texture.flipY=false`
735
810
  * ⇒ 纹理 **v=0 = 图像下边**,所以图像顶部余量 `mt` 落在 `v1`、底部 `mb` 落在 `v0`。搞反 = 上下颠倒。 */
736
- export function rtStitchTileInset(i, tw, th) {
737
- const e = rtStitchTileEdgeClip(i);
738
- const m = RT_STITCH_SEAM_MARGIN_PX;
811
+ export function rtStitchTileInset(i, tw, th, grid = PANO_TILE_GRID) {
812
+ const e = rtStitchTileEdgeClip(i, grid);
813
+ const m = grid.seamMarginPx;
739
814
  const u0 = e.left ? 0 : Math.min(0.5, m / tw);
740
815
  const u1 = e.right ? 1 : 1 - Math.min(0.5, m / tw);
741
816
  const v0 = e.bottom ? 0 : Math.min(0.5, m / th);
@@ -743,13 +818,13 @@ export function rtStitchTileInset(i, tw, th) {
743
818
  return [u0, v0, u1, v1];
744
819
  }
745
820
  /**
746
- * 「这 `RT_STITCH_SLOT_COUNT`(2) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
821
+ * 「这 `RT_STITCH_SLOT_COUNT`(3) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
747
822
  * 只改标记不改地址(或反之)= 用整图 UV 去采一张瓦片 / 用瓦片内 UV 去采整图,画面立刻错乱。
748
823
  * 非 tiled 态(或该点压根没切过片)一律回退成「各槽同为一张整图」。
749
- * ⭐ 2026-10-06 晚旨「只支持 32 张(8×4)」:只要有瓦片(1~32 条)一律走拼合器(rt: 虚拟键),
750
- * 不足 32 张的位置由拼合器**涂灰**(一眼看出数据不全);8×8=64 张与 4×4=16 张那两版**都不做兼容**
824
+ * ⭐ 2026-10-23 旨「只支持 9 张(3×3)」:只要有瓦片(1~9 条)一律走拼合器(rt: 虚拟键),
825
+ * 不足 9 张的位置由拼合器**涂灰**(一眼看出数据不全);8×4=32 张、8×8=64 张与 4×4=16 张那几版**都不做兼容**
751
826
  * (皇上「无须兼容正常使用,有异常我会重传」⇒ 旧存档的 64 条地址超张数被判废、重传即可)。
752
- * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 2 个 rtStitchKey —— 槽数再变会漏改)。
827
+ * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 3 个 rtStitchKey —— 槽数再变会漏改)。
753
828
  */
754
829
  export function mktPanoTexUrls(url, tiles, mode = "tiled") {
755
830
  if (mode === "tiled" && tiles && tiles.length > 0) {
@@ -759,8 +834,8 @@ export function mktPanoTexUrls(url, tiles, mode = "tiled") {
759
834
  return Array.from({ length: RT_STITCH_SLOT_COUNT }, () => url);
760
835
  }
761
836
  /**
762
- * 「这 2 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
763
- * `tiles` 有 **任意 1~32 条**地址(不足 32 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
837
+ * 「这 3 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
838
+ * `tiles` 有 **任意 1~9 条**地址(不足 9 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
764
839
  * 否则各槽同为一张整图(旧数据 / 种子图 / 用户切整片)。
765
840
  */
766
841
  export function mktPanoTiledFlag(tiles, mode = "tiled") {
@@ -794,10 +869,10 @@ export function setMktPanoTiled(material, tiled, slot = 0) {
794
869
  export function createMktShaderMaterial() {
795
870
  return new ShaderMaterial({
796
871
  uniforms: {
797
- // 一个全景点恒占 **2 个 sampler**(2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」):
798
- // panoramas[0..1] = 2 张**离屏拼合槽纹理**([左半幅, 右半幅],每槽由 `RT_STITCH_CELLS_PER_SLOT`
799
- // 片瓦片拼成),片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
800
- // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 2 张)—— 槽数再变这里会漏改。
872
+ // 一个全景点恒占 **3 个 sampler**(2026-10-23 旨「全景的 RT 由原来的 2 个分成 3 个」):
873
+ // panoramas[0..2] = 3 张**离屏拼合槽纹理**([左列, 中列, 右列],每槽由 `RT_STITCH_CELLS_PER_SLOT`
874
+ // 片瓦片竖着拼成),片元按全景 UV 的 **u** 三等分挑槽采样(v 恒全高)。
875
+ // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 3 张)—— 槽数再变这里会漏改。
801
876
  panoramas: {
802
877
  value: Array.from({ length: RT_STITCH_SLOT_COUNT }, () => createPlaceholderTexture()),
803
878
  },