three-base 2.0.169 → 2.0.175

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 (124) hide show
  1. package/FPScontrol.d.ts +55 -30
  2. package/FPScontrol.js +278 -225
  3. package/FPScontrol.ts +177 -127
  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 +8 -3
  10. package/MktMesh/MktMesh.js +43 -14
  11. package/MktMesh/MktMesh.ts +40 -14
  12. package/MktMesh/mktMappingShader.d.ts +6 -5
  13. package/MktMesh/mktMappingShader.js +26 -19
  14. package/MktMesh/mktMappingShader.ts +28 -19
  15. package/MktMesh/mktShader.d.ts +127 -58
  16. package/MktMesh/mktShader.js +192 -117
  17. package/MktMesh/mktShader.ts +251 -127
  18. package/MktMesh/panoSlotRoster.d.ts +2 -2
  19. package/MktMesh/panoSlotRoster.js +9 -7
  20. package/MktMesh/panoSlotRoster.ts +9 -7
  21. package/MktMesh/panoTileStitcher.d.ts +20 -13
  22. package/MktMesh/panoTileStitcher.js +97 -76
  23. package/MktMesh/panoTileStitcher.ts +119 -90
  24. package/MktMesh/types.d.ts +5 -1
  25. package/MktMesh/types.ts +5 -1
  26. package/PipeMesh/PipeJoint.d.ts +22 -0
  27. package/PipeMesh/PipeJoint.js +33 -0
  28. package/PipeMesh/PipeJoint.ts +38 -0
  29. package/PipeMesh/PipeMesh.d.ts +94 -22
  30. package/PipeMesh/PipeMesh.js +192 -53
  31. package/PipeMesh/PipeMesh.ts +223 -76
  32. package/PipeMesh/PipeStraight.d.ts +19 -0
  33. package/PipeMesh/PipeStraight.js +34 -0
  34. package/PipeMesh/PipeStraight.ts +45 -0
  35. package/PipeMesh/axisSnap.d.ts +52 -0
  36. package/PipeMesh/axisSnap.js +52 -0
  37. package/PipeMesh/axisSnap.ts +93 -0
  38. package/PipeMesh/directRoute.d.ts +85 -0
  39. package/PipeMesh/directRoute.js +126 -0
  40. package/PipeMesh/directRoute.ts +190 -0
  41. package/PipeMesh/index.d.ts +6 -0
  42. package/PipeMesh/index.js +6 -0
  43. package/PipeMesh/index.ts +7 -1
  44. package/PipeMesh/pipeMaterial.d.ts +16 -0
  45. package/PipeMesh/pipeMaterial.js +23 -0
  46. package/PipeMesh/pipeMaterial.ts +26 -0
  47. package/ThreeBase.d.ts +2 -49
  48. package/ThreeBase.js +43 -164
  49. package/ThreeBase.ts +1176 -1314
  50. package/instanced/InstancedBatch.d.ts +14 -1
  51. package/instanced/InstancedBatch.js +53 -3
  52. package/instanced/InstancedBatch.ts +62 -3
  53. package/instanced/InstancedChild.d.ts +24 -0
  54. package/instanced/InstancedChild.js +40 -0
  55. package/instanced/InstancedChild.ts +41 -0
  56. package/instanced/InstancedGroup.d.ts +48 -0
  57. package/instanced/InstancedGroup.js +45 -1
  58. package/instanced/InstancedGroup.ts +85 -0
  59. package/package.json +2 -7
  60. package/pass/bloom_pass.js +7 -14
  61. package/pass/bloom_pass.ts +7 -16
  62. package/pass/cover_pass.js +7 -15
  63. package/pass/cover_pass.ts +7 -17
  64. package/pass/debugDepthState.d.ts +16 -8
  65. package/pass/debugDepthState.js +4 -0
  66. package/pass/debugDepthState.ts +27 -9
  67. package/pass/glow_pass.js +19 -110
  68. package/pass/glow_pass.ts +19 -115
  69. package/pass/ssao_pass.d.ts +14 -59
  70. package/pass/ssao_pass.js +66 -171
  71. package/pass/ssao_pass.ts +63 -207
  72. package/pass/stroke_pass.js +13 -16
  73. package/pass/stroke_pass.ts +14 -18
  74. package/shaders/baseDepthVisShader.d.ts +1 -1
  75. package/shaders/baseDepthVisShader.js +1 -1
  76. package/shaders/baseDepthVisShader.ts +1 -1
  77. package/shaders/debugDepthCompareShader.d.ts +2 -2
  78. package/shaders/debugDepthCompareShader.js +2 -2
  79. package/shaders/debugDepthCompareShader.ts +2 -2
  80. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  81. package/shaders/debugDepthJudgeShader.js +1 -1
  82. package/shaders/debugDepthJudgeShader.ts +1 -1
  83. package/PipeMesh/PathGeometry.d.ts +0 -44
  84. package/PipeMesh/PathGeometry.js +0 -307
  85. package/PipeMesh/PathGeometry.ts +0 -343
  86. package/PipeMesh/PathPoint.d.ts +0 -17
  87. package/PipeMesh/PathPoint.js +0 -31
  88. package/PipeMesh/PathPoint.ts +0 -36
  89. package/PipeMesh/PathPointList.d.ts +0 -31
  90. package/PipeMesh/PathPointList.js +0 -209
  91. package/PipeMesh/PathPointList.ts +0 -227
  92. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  93. package/PipeMesh/PathTubeGeometry.js +0 -130
  94. package/PipeMesh/PathTubeGeometry.ts +0 -146
  95. package/WaterFlow/FloatingDuck.d.ts +0 -88
  96. package/WaterFlow/FloatingDuck.js +0 -307
  97. package/WaterFlow/FloatingDuck.ts +0 -445
  98. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  99. package/WaterFlow/GPUFluidSimulation.js +0 -785
  100. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  101. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  102. package/WaterFlow/ReservoirSystem.js +0 -621
  103. package/WaterFlow/ReservoirSystem.ts +0 -754
  104. package/WaterFlow/SPHField.d.ts +0 -35
  105. package/WaterFlow/SPHField.js +0 -204
  106. package/WaterFlow/SPHField.ts +0 -228
  107. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  108. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  109. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  110. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  111. package/WaterFlow/ShallowWaterSurface.js +0 -441
  112. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  113. package/WaterFlow/WaterFlow.d.ts +0 -87
  114. package/WaterFlow/WaterFlow.js +0 -171
  115. package/WaterFlow/WaterFlow.ts +0 -219
  116. package/WaterFlow/types.d.ts +0 -161
  117. package/WaterFlow/types.js +0 -1
  118. package/WaterFlow/types.ts +0 -189
  119. package/WaterFlow/waterFlowPass.d.ts +0 -3
  120. package/WaterFlow/waterFlowPass.js +0 -20
  121. package/WaterFlow/waterFlowPass.ts +0 -40
  122. package/shaders/debugExcludedShader.d.ts +0 -13
  123. package/shaders/debugExcludedShader.js +0 -46
  124. package/shaders/debugExcludedShader.ts +0 -48
@@ -44,8 +44,8 @@ import { Color, DataTexture, DoubleSide, Matrix4, ShaderMaterial, SRGBColorSpace
44
44
  * 现口径已统一为等距柱状线性,皇上验收「不交换 / 不翻转」那态即正确 ⇒ **方向参数整条撤除、写死**
45
45
  * (见 🎛️ 段)。本诊断开关保留,用于日后再次核对「哪块瓦片落在哪个象限」。
46
46
  *
47
- * 用法:页面 URL 带 `?panoTileDebug=1` 时,两张槽纹理**不再采样纹理**,各自输出一个纯色 ——
48
- * ⭐ 10-06 晚起只有左右两槽(竖着不再分块):红=左半幅(槽0) 绿=右半幅(槽1)
47
+ * 用法:页面 URL 带 `?panoTileDebug=1` 时,三张槽纹理**不再采样纹理**,各自输出一个纯色 ——
48
+ * ⭐ 10-23 起横 3 槽(竖着不再分块):红=左列(槽0) 绿=中列(槽1) 蓝=右列(槽2)
49
49
  * (旧 4 象限版是 红=tiles[0] 绿=tiles[1] 蓝=tiles[2] 黄=tiles[3],已随四槽一并作废)
50
50
  * 皇上只要说出「画面上下左右各是什么颜色」,即可一次反推出真实映射。
51
51
  *
@@ -66,7 +66,7 @@ function panoDebugOn(name: string): boolean {
66
66
  }
67
67
  }
68
68
 
69
- /** ⭐ 2026-10-09 随blend 材质搬去编辑器,故改为导出(那边拼片元 GLSL 时要用)。 */
69
+ /** ⭐ 2026-10-09 随blend 材质搬去编辑器,故改为导出(那边拼片元 GLSL 时要用)。 */
70
70
  export const TILE_DEBUG_GLSL = panoDebugOn("panoTileDebug") ? "1.0" : "0.0";
71
71
 
72
72
  /**
@@ -104,7 +104,7 @@ export const MKT_PANO_TINT_ON = PANO_TINT_ON;
104
104
  * 照片本身看不清);皇上旨 2026-09-26 晚「叠加的颜色比例少一点」⇒ 取 **0.5**:色相仍是主色、一眼分得出归属,
105
105
  * 但底下那张全景图的明暗与颜色能同时读出来。两份材质(融合 / 单全景)**共用这一个值**。
106
106
  */
107
- /** ⭐ 2026-10-09 同上:随 blend 材质搬去编辑器,故改为导出。 */
107
+ /** ⭐ 2026-10-09 同上:随 blend 材质搬去编辑器,故改为导出。 */
108
108
  export const PANO_TINT_MIX = 0.5;
109
109
 
110
110
  /**
@@ -325,7 +325,7 @@ export function setMktPanoPoints(
325
325
  *
326
326
  * 🧩 **第二个根因(与方向参数无关,故本次写死后依然保留)**:
327
327
  * 默认场景与旧存档的 `textures[].tiles` **只有 1 条地址**(当年上传切片功能之后才有的字段),
328
- * 喂料处回退成「各槽同为 `url`」(10-06 晚起是 2 槽:`[url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
328
+ * 喂料处回退成「各槽同为 `url`」(10-23 起是 3 槽:`[url, url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
329
329
  * ① 各槽同图 ⇒ 选哪一格都一样(当时看到的「交换零效果」症状,写死后该症状无从出现);
330
330
  * ② 更要命的是仍按「瓦片内 UV」`localU/localV` 去采整图 ⇒ 整图被按 1/2 拉伸铺满整个球面(旧版是 1/4):
331
331
  * 墙面仰角 +10° 采到原图 **−70°(脚底/三脚架)** —— 这才是「全景映射在模型上的位置不太对」的另一半,
@@ -333,7 +333,7 @@ export function setMktPanoPoints(
333
333
  * 修法 = `uPanoTiled`(单全景标量 / 融合每点数组):0 时把瓦片内 UV **还原成整图 UV**
334
334
  * `panoU = u`、`panoV = v`(口径写死后这就是恒等式:横向 `0.5(2u)=u` / `0.5+0.5(2u−1)=u`;
335
335
  * 纵向 10-06 晚起 localV 本就等于 v ⇒ 两个方向都退化成恒等式)。
336
- * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~32 条」,10-06 晚旨只支持 8×4=32 张、不足涂灰),且每个回退喂料点都要写标记,
336
+ * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~9 条」,10-23 旨只支持 3×3=9 张、不足涂灰),且每个回退喂料点都要写标记,
337
337
  * 漏一处该对象就悄悄退回 1/2 拉伸。默认值取 1(已切片的真实上传数据行为不变)。
338
338
  * 护栏:`test/panoVMapping.test.js`。
339
339
  * 🔄 2026-09-22 追加(皇上口谕):本标记现由**用户可选的「全景纹理形态」`PanoTexMode`**(tiled/whole)与数据
@@ -409,48 +409,52 @@ const vertexShader = /* glsl */ `
409
409
  */
410
410
  // ⭐ 下面这一组「切片 / 拼合槽」几何常量**必须排在 `fragmentShader` 之前**:单全景那份片元是模块级
411
411
  // 常量字符串,里面要用 `${RT_STITCH_SLOT_COUNT}` 声明 sampler 数组长度(TS 的块级作用域禁止先用后声明)。
412
- /** ⭐ 32 瓦片拼合(2026-10-06 晚皇上旨:一张全景切 **横 8 × 竖 4 = 32 片**,每 **4×4 = 16 片**离屏拼成
413
- * 1 张 **4096×4096 正方形**槽纹理,**左右 2 槽**组成完整全景;上一版是同日早些时候的 8×8 = 64 片 / 4 槽
414
- * 2×2 象限,再上一版是 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
412
+ /** ⭐ 9 瓦片拼合(2026-10-23 皇上旨:一张全景切 **横 3 × 竖 3 = 9 片**,每 **横 1 × 竖 3 = 3 片**离屏拼成
413
+ * 1 张 **4096×6144** 槽纹理,**左/中/右 3 槽**组成完整全景;上一版是 10-06 晚旨的 8×4 = 32 片 / 左右 2 槽
414
+ * 4096²,再上一版 8×8 = 64 片 / 4 槽 2×2 象限,最早 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
415
415
  * 虚拟键不是真 URL,纹理由 `PanoTileStitcher` 用 shader 逐格画进 `WebGLRenderTarget` 提供,
416
416
  * 消费方(基类 `_ensurePanoTiles` / 编辑器 `_peekTileAt`)见前缀即走拼合器,⛔ 不得当 URL 下载。
417
417
  * ⚠️ 键协议 `rt:<batch>#<slot>` 与 `uPanoTiled` 这套对外接口**一字未改** —— 变的只有「每批几条地址」
418
- * 和「槽怎么分」;`slot` 的取值范围从 0..3 缩到 0..1,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
419
- * ⚠️ 旧存档不兼容(皇上 2026-10-06 拍板):存档里 64 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
418
+ * 和「槽怎么分」;`slot` 取值 0..2,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
419
+ * ⚠️ 旧存档不兼容(与 10-06 同一口径):存档里 32 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
420
420
  export const RT_STITCH_KEY_PREFIX = "rt:";
421
421
 
422
- /** 原图**横向**(经度方向 u)切几片(旨「横 8」) */
423
- export const RT_STITCH_COLS = 8;
422
+ /** 原图**横向**(经度方向 u)切几片(旨「横 3」) */
423
+ export const RT_STITCH_COLS = 3;
424
424
 
425
- /** 原图**纵向**(纬度方向 v)切几片(旨「竖 4」)⇒ 竖着不再分块,一个 RT 就装下整幅的上下 180° */
426
- export const RT_STITCH_ROWS = 4;
425
+ /** 原图**纵向**(纬度方向 v)切几片(旨「竖 3」)⇒ 一槽在纵向上装满整幅 3 片,横向才分左/中/右 */
426
+ export const RT_STITCH_ROWS = 3;
427
427
 
428
- /** 一个批次(= 一个全景点)的瓦片总数 = 8 × 4 = 32;`tiles` 存档数组的硬上限 */
428
+ /** 一个批次(= 一个全景点)的瓦片总数 = 3 × 3 = 9;`tiles` 存档数组的硬上限 */
429
429
  export const RT_STITCH_TILES_PER_BATCH = RT_STITCH_COLS * RT_STITCH_ROWS;
430
430
 
431
- /** 一个拼合槽每边几格(旨「每个 RT 由 16 个全景瓦片组成」⇒ 4×4 = 16)*/
432
- export const RT_STITCH_SLOT_GRID = 4;
433
-
434
- /** 一个拼合槽的格数 = 4² = 16(画满 16 格 = 这一槽齐活) */
435
- export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_GRID * RT_STITCH_SLOT_GRID;
436
-
437
- /** 拼合槽个数 = **左右 2 槽**(旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 一个全景点只占 **2 个 sampler**。
438
- * ⛔ 别以为 32 片就要 32 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16 = 2 槽 × 8 景点,
439
- * 这一条是硬顶,也是「必须拼进 RT」而不是「直接喂瓦片」的根本原因。
440
- * ⭐ 分块口径:槽 0 = **左半幅**(列 0-3、行 0-3)、槽 1 = **右半幅**(列 4-7、行 0-3),
441
- * 即「只按 u 分左右,v 不再分上下」⇒ 每槽覆盖 **横向 180° × 纵向 180°**,是原图左/右半个**正方形**
442
- * ⇒ 槽纹理必须跟着变正方形(见下 `RT_STITCH_SLOT_W/H`),否则纵向被压 2 倍。 */
443
- export const RT_STITCH_SLOT_COUNT = 2;
444
-
445
- /** 单槽像素尺寸 **4096×4096 正方形**(皇上 2026-10-06 拍板保画质):2 槽 = 有效分辨率 8192×4096。
446
- * ⚠️ 为什么必须变正方形:每槽覆盖的原图区域从「横 180° × 竖 90°(2:1)」变成「横 180° × 竖 180°(1:1)」,
447
- * RT 若仍是 4096×2048 ⇒ 纵向缩放比是横向的 2 倍、**纵向分辨率腰斩**,直接踩「不降质」红线。
448
- * 每格 = 1024×1024;一片瓦片的原图边长 = 横 W/8、竖 H/4 ⇒ 横向缩放比 = (W/8)/1024 = 1.4533(11904 宽原图),
449
- * 与 64 片那一版**逐字相同** ⇒ 本次改版**不涨也不降画质**;变的是同屏全景数 4 → 8、
450
- * 单全景下载片数 64 → 32(减半)、单全景 RT 显存 128MB(不变,只是从 4 张 32MB 变 2 张 64MB)。
431
+ /** 一个拼合槽**横向**几格(旨「竖直排列 3 个瓦片」⇒ 一槽就是原图的一整列,横 1 格)*/
432
+ export const RT_STITCH_SLOT_CELL_COLS = 1;
433
+
434
+ /** 一个拼合槽**纵向**几格(旨「竖直排列 3 个瓦片」⇒ 竖 3 格)
435
+ * ⚠️ 10-23 起横竖**不再同数**:旧字段 `RT_STITCH_SLOT_GRID`(方形假设)已整条作废,
436
+ * 格数、格位、quad 摆位一律分轴取(`rtStitchCellPos` / `PanoTileStitcher._stitchCell`)。 */
437
+ export const RT_STITCH_SLOT_CELL_ROWS = 3;
438
+
439
+ /** 一个拼合槽的格数 = 1 × 3 = 3(画满 3 格 = 这一槽齐活) */
440
+ export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_CELL_COLS * RT_STITCH_SLOT_CELL_ROWS;
441
+
442
+ /** 拼合槽个数 = **左/中/右 3 槽**(旨「全景的 RT 由原来的 2 个分成 3 个」)⇒ 一个全景点占 **3 个 sampler**。
443
+ * ⛔ 别以为 9 片就要 9 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 **16** 是硬顶,
444
+ * 3 槽 × 同屏 8 景点 = 24 会连编都编不过 ⇒ 同屏景点数由 `MKT_PANO_MAX_SLOTS` 收到 **5**(5×3=15 ≤ 16)。
445
+ * ⭐ 分块口径:槽 `s` = 原图**第 s 列**(列 s、行 0-2)⇒ 每槽覆盖 **横向 120° × 纵向 180°**,
446
+ * 与「一槽一列、竖 3 格」同一条口径(槽纹理尺寸见下 `RT_STITCH_SLOT_W/H`)。 */
447
+ export const RT_STITCH_SLOT_COUNT = 3;
448
+
449
+ /** 单槽像素尺寸 **4096×6144**(皇上 2026-10-23 旨「每个 RT 的尺寸是 4096 * 6144 竖直排列 3 个瓦片」):
450
+ * 3 槽 = 有效分辨率 **12288×6144**,与 3×3 切片的原图(12288×6144 ⇒ 每片 4096×2048)**逐像素 1:1**;
451
+ * 上一版 2 槽 4096² 是把 71.6 Mpx 的原图**缩小**塞进 33.5 Mpx ⇒ 本次是升分辨率,⛔ 不是降质。
452
+ * ⚠️ 每格 = 4096×2048(横竖**不等** ⇒ 摆格必须分轴,见 `RT_STITCH_SLOT_CELL_COLS/ROWS`)。
453
+ * ⚠️ 两条代价(臣已量过并报给皇上拍板):单全景 RT 显存 128MB → **302MB**(×2.25,mipmap 再 +1/3);
454
+ * sampler 3 槽 × 8 景点 = 24 超硬顶 16 ⇒ 同屏景点 `MKT_PANO_MAX_SLOTS` 8 → **5**。
451
455
  * ⚠️ 红线(「千万不要降低质量」):这两个数与「原图切几片」无关,⛔ 不许当成省显存的旋钮去缩。 */
452
456
  export const RT_STITCH_SLOT_W = 4096;
453
- export const RT_STITCH_SLOT_H = 4096;
457
+ export const RT_STITCH_SLOT_H = 6144;
454
458
 
455
459
  export const floorVertexShader = /* glsl */ `
456
460
  #include <common>
@@ -501,13 +505,33 @@ export const floorVertexShader = /* glsl */ `
501
505
  }
502
506
  `;
503
507
 
508
+ /** 槽下标必须在编译期是常量(ANGLE/D3D 禁止动态索引 sampler 数组)⇒ 按 `RT_STITCH_SLOT_COUNT`
509
+ * 生成「字面量下标 if 链」。`emit(槽下标)` 给出该分支的赋值语句,最后一条写成裸 `else` 兜住 u==1 的边界。 */
510
+ const panoSlotBranches = (emit: (slot: number) => string): string => {
511
+ const last = RT_STITCH_SLOT_COUNT - 1;
512
+ const out: string[] = [];
513
+ for (let s = 0; s <= last; s++) {
514
+ out.push(
515
+ s === last
516
+ ? `else ${emit(s)}`
517
+ : s === 0
518
+ ? `if (tileIdx == ${s}) ${emit(s)}`
519
+ : `else if (tileIdx == ${s}) ${emit(s)}`,
520
+ );
521
+ }
522
+ return out.join("\n ");
523
+ };
524
+
525
+ /** 🔬 归属诊断用的每槽色相(`?panoTile=1` 那条直通分支;条数必须 ≥ `RT_STITCH_SLOT_COUNT`) */
526
+ 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)"];
527
+
504
528
  const fragmentShader = /* glsl */ `
505
529
  // 对数深度缓冲(与顶点着色器配套,保证 MktMesh 深度与场景一致)
506
530
  #include <logdepthbuf_pars_fragment>
507
531
 
508
- // 一个全景点 = 2 张**拼合槽纹理**(原图切 8×4=32 片,每 4×4=16 片拼成一张槽,10-06 晚皇上旨)。
509
- // panoramas[0..1] = [左半幅, 右半幅] 两个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
510
- // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(tileIdx 命中的分支内 sampler 下标为字面量)。
532
+ // 一个全景点 = 3 张**拼合槽纹理**(原图切 3×3=9 片,每「横 1 × 竖 3」= 3 片拼成一张槽,10-23 皇上旨)。
533
+ // panoramas[0..2] = [左列, 中列, 右列] 三个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 恒全高)。
534
+ // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(见 panoSlotBranches)。
511
535
  uniform sampler2D panoramas[${RT_STITCH_SLOT_COUNT}];
512
536
  uniform mat4 textureMatrix; // 纹理世界矩阵:位置(第4列 xyz)=投影中心,旋转(左上3x3)=纹理朝向
513
537
  uniform float uPanoramaVisible; // 全景投射显隐:1=显示全景,0=仅显示中性素色(编辑态切换/投射显隐开关用)
@@ -515,9 +539,9 @@ const fragmentShader = /* glsl */ `
515
539
  // 原 uniform float uColorSpace; + 片元里那段 sRGB→linear 的 pow 分支已整条撤除,勿加回(原因见下方采样处)。
516
540
  // 全景上下方向**已写死**(等距柱状线性 v + 不交换 + 不翻转,2026-09-22 皇上定论,理由见文件头 🎛️ 段):
517
541
  // 曾有 uPanoSwapRow / uPanoFlipTile / uPanoVertOff 三个可调 uniform,现已整条撤除,勿加回。
518
- // ⚠️ uPanoTiled = panoramas[0..1] 装的是「拼合好的左右两张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
542
+ // ⚠️ uPanoTiled = panoramas[0..2] 装的是「拼合好的左/中/右三张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
519
543
  // 由「用户选的纹理形态 PanoTexMode + 该点有没有切过片」共同决定(见下方 mktPanoTiledFlag)。
520
- // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左半幅拉伸到整个
544
+ // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左一档拉伸到整个
521
545
  // 球面(墙面仰角 +10° 采到原图 −70° 的地板)—— 2026-09-22「全景位置不对」的第二真因,见文件头 🧩 段。
522
546
  uniform float uPanoTiled;${PANO_TINT_ON ? `
523
547
  // 🔬 归属诊断色(仅 ?panoTint=1 时存在):本材质当前投的就是「选中的那一个」全景,
@@ -554,29 +578,27 @@ const fragmentShader = /* glsl */ `
554
578
  float lat = asin(clamp(dir.y, -1.0, 1.0));
555
579
  float v = 0.5 + lat * 0.3183098861837907; // 1/PI
556
580
 
557
- // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
558
- // ⭐ 2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:**竖着不再分块** ⇒ 槽只随 **u** 走,
559
- // v 方向一槽装下整幅上下 180°(这也是槽纹理必须改成 4096² 正方形的原因,见 RT_STITCH_SLOT_W/H)。
560
- // ⛔ 别把 tileRow 加回来:加回来 = 又变成 2×2 四槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽。
561
- int tileIdx = (u >= 0.5) ? 1 : 0;
562
- // 槽内局部 UV(连续、无 fract 边界跳变):横向按左右减半;
563
- // ⛔ 纵向**不再减半**(槽就是全高)⇒ localV 恒等于 v。写成 2v / 2v-1 = 纵向分辨率白丢一半。
564
- float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
581
+ // 左/中/右三列 -> 拼合槽下标 [左列=0, 中列=1, 右列=2](每槽 = 3×3 网格里「横 1 × 竖 3」= 3 片,见 RT_STITCH_SLOT_TILES)
582
+ // ⭐ 2026-10-23 旨「全景的 RT 由原来的 2 个分成 3 个、每个 4096×6144 竖直排列 3 个瓦片」:
583
+ // 槽只随 **u** 三等分走,v 方向一槽装下整幅上下 180°(槽纹理尺寸见 RT_STITCH_SLOT_W/H)。
584
+ int tileIdx = int(clamp(floor(u * ${RT_STITCH_SLOT_COUNT}.0), 0.0, ${RT_STITCH_SLOT_COUNT - 1}.0));
585
+ // 槽内局部 UV(连续、无 fract 边界跳变):横向按槽数等分;
586
+ // ⛔ 纵向**不再切**(槽就是全高)⇒ localV 恒等于 v。写成 nv / nv-1 = 纵向分辨率白丢一档。
587
+ float localU = u * ${RT_STITCH_SLOT_COUNT}.0 - float(tileIdx);
565
588
  float localV = v;
566
589
 
567
- // 🧩 uPanoTiled == 0(两槽装的是**同一张完整等距柱状图**,没有拼合槽):
590
+ // 🧩 uPanoTiled == 0(各槽装的是**同一张完整等距柱状图**,没有拼合槽):
568
591
  // 必须还原成**全图 UV** 再采样,也就是直接用 (u, v) ——
569
- // 横向还原式 (半==左 ? 0.0 : 0.5) + 0.5*localU 代入 localU 后恒等于 u;纵向 localV 本就等于 v,
592
+ // 横向还原式 (tileIdx/N) + localU/N 代入后恒等于 u;纵向 localV 本就等于 v,
570
593
  // **两个方向都退化成恒等式**(方向已写死:无交换、无翻转、无偏移)。
571
- // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左半幅铺满整个球面。
594
+ // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左一档铺满整个球面。
572
595
  float panoU = (uPanoTiled > 0.5) ? localU : u;
573
596
  float panoV = (uPanoTiled > 0.5) ? localV : v;
574
597
 
575
598
  // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
576
599
  if (${TILE_DEBUG_GLSL} > 0.5) {
577
600
  vec3 dbgCol;
578
- if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
579
- else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
601
+ ${panoSlotBranches((s) => `dbgCol = ${PANO_SLOT_DEBUG_COLORS[s]};`)}
580
602
  gl_FragColor = vec4(dbgCol, 1.0);
581
603
  #include <colorspace_fragment>
582
604
  #include <logdepthbuf_fragment>
@@ -589,8 +611,7 @@ const fragmentShader = /* glsl */ `
589
611
  // 末端的 <colorspace_fragment> / 终处理再编码一次 ⇒ 一进一出,屏幕上与源图逐字节还原。
590
612
  // ⛔ 别加回那段 pow():它在硬件已解码之上再解一遍 = 二次解码压暗(中灰 #808080 掉到 #373737、#333333 掉到 #080808)。
591
613
  vec3 projCol;
592
- if (tileIdx == 0) projCol = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
593
- else projCol = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
614
+ ${panoSlotBranches((s) => `projCol = texture2D(panoramas[${s}], vec2(panoU, panoV)).rgb;`)}
594
615
 
595
616
  // 全景投射显隐:关闭时(仅墙体/地板,回落中性素色,不投射全景);默认 1 完全显示
596
617
  if (uPanoramaVisible < 0.5) {
@@ -624,9 +645,15 @@ ${PANO_TINT_ON ? `
624
645
  }
625
646
  `;
626
647
 
627
- /** 1×1 白色占位纹理:panorama 在尚未激活任何纹理前使用,避免空纹理采样报错 */
648
+ /** 1×1 **中性灰**占位纹理:panorama 在尚未激活任何纹理 / 该点还没设图片时使用,既避免空纹理采样报错,
649
+ * 也让「没图」这件事在屏幕上是**一片纯灰**而不是一块白。
650
+ *
651
+ * ⭐ 2026-10-25 皇上旨「把默认图去掉,图片没有加载的时候就显示纯灰色」:原先是纯白 `[255,255,255,255]`。
652
+ * 色值取 **46 = round(0.18 × 255)**,与 mapping shader 的 `uFaceMissingColor`(线性 0.18 ≈ #767676)逐字一致;
653
+ * DataTexture 默认 `NoColorSpace`(不做 sRGB 解码)⇒ 采样出来就是 0.18 线性,经终处理 sRGB 编码后正是 #767676。
654
+ * ⛔ 别改成 sRGB 的 118:那样会被当线性值用,屏幕上是 #B4B4B4,与缺失格的灰对不上。 */
628
655
  export function createPlaceholderTexture(): DataTexture {
629
- const tex = new DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
656
+ const tex = new DataTexture(new Uint8Array([46, 46, 46, 255]), 1, 1);
630
657
  tex.needsUpdate = true;
631
658
  return tex;
632
659
  }
@@ -634,9 +661,10 @@ export function createPlaceholderTexture(): DataTexture {
634
661
  /**
635
662
  * 全景纹理形态(皇上旨 2026-09-22「全景球可以切成 4 片和一整片,两者效果很不一样,做个功能可以切换」)。
636
663
  *
637
- * "tiled" = 2 个 sampler 各装一张**离屏拼合好的槽纹理**(原图 **8×4 = 32 片**,每 16 片拼一张;
638
- * 10-06 晚旨;再早是同日的 8×8 = 64 片 / 4 槽、09-29 的 4×4 = 16 片),`uPanoTiled` 写 1;
639
- * "whole" = 2 个 sampler 都装**同一张完整等距柱状原图**,`uPanoTiled` 写 0(改用全图 UV)。
664
+ * "tiled" = 3 个 sampler 各装一张**离屏拼合好的槽纹理**(原图 **3×3 = 9 片**,每「横 1 × 竖 3」= 3 片拼一张;
665
+ * 10-23 旨;再早是 10-06 晚的 8×4 = 32 片 / 2 槽、同日的 8×8 = 64 片 / 4 槽、09-29 的 4×4 = 16 片),
666
+ * `uPanoTiled` 写 1;
667
+ * "whole" = 3 个 sampler 都装**同一张完整等距柱状原图**,`uPanoTiled` 写 0(改用全图 UV)。
640
668
  *
641
669
  * ⚠️ 两种形态在**几何上应当逐像素一致**(`panoU/panoV` 的还原式就是恒等式,见文件头 🧩 段与
642
670
  * `test/panoVMapping.test.js` 的等价性数值证明)。所以线上若看着「很不一样」,差异只可能来自下面两条,
@@ -648,12 +676,12 @@ export function createPlaceholderTexture(): DataTexture {
648
676
  * whole 态 = 整图按档(w_1024 ⇒ 等效 1024×512)。
649
677
  * ⚠️ 两档清晰度差**不是固定的倍数**,而取决于「原图宽 ÷ 所选档位」;又因 `m_lfit` 只缩不放,
650
678
  * 原图宽 ≤ 2×档位时 whole 态其实也没被缩,两态清晰度自然接近 —— 别把这当成「切换坏了」。
651
- * ② **瓦片边界**(10-06 晚切 32 片后这一条分两层,⚠️ 口径不同):
652
- * · **拼合槽之间**(u=0.5 那一竖;⭐ v=0.5 不再有横缝 —— 竖着已不分块):2 张槽是 2 张独立纹理,
679
+ * ② **瓦片边界**(10-06 晚切 32 片起这一条分两层,10-23 改 3×3 后两层口径仍不同):
680
+ * · **拼合槽之间**(u=1/3、u=2/3 那两竖;⭐ v 方向不再有横缝 —— 每槽就是全高):3 张槽是 3 张独立纹理,
653
681
  * 各自 mip + ClampToEdge ⇒ 仍可能有细亮/细暗线,这一条**与 09-29 那版一字未变**,
654
682
  * `RT_STITCH_SEAM_MARGIN_PX` 管不到它(它治的是「同一张纹理内」的格子边界);
655
683
  * whole 态是同一张贴图,天然无此缝。
656
- * · **拼合槽内部**(每张 4096×4096 里的 3 横 3 竖格子线,16 片时代只有 1 横 1 竖):
684
+ * · **拼合槽内部**(每张 4096×6144 里 3 片竖叠 ⇒ 2 道横缝、0 道竖缝;上一版 4096² 是 3 横 3 竖):
657
685
  * 靠「上传多切 `RT_STITCH_SEAM_MARGIN_PX` 像素 + 拼合只采中间那块」消掉,见该常量的说明。
658
686
  *
659
687
  * 旧数据 / 种子图没有 `tiles`(或只有 1 条)⇒ 两种形态都只能退回整图,`whole` 与 `tiled` 表现相同。
@@ -685,7 +713,110 @@ export const MISSING_TILE_GRAY = "#767676";
685
713
  * 反而在放大」那类点留边界。**代价**:每片文件比「正好 1/8」宽 16px、高 16px(≈ +4% 面积),⛔ 不是降质。 */
686
714
  export const RT_STITCH_SEAM_MARGIN_PX = 8;
687
715
 
688
- /** 32 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
716
+ /**
717
+ * 「一张图切成几张瓦片、瓦片怎么分组进拼合槽」的**网格描述** —— 皇上旨 2026-10-20「上传的这个图切成
718
+ * 4*4=16 片吧,加载的时候,也是一帧载入一片」⇒ 拼合器从此要同时服务两种网格(全景 3×3 / 图片平面那一份),
719
+ * 于是这一组参数必须能**当数据传**,⛔ 不许在拼合器里再写死一遍全景那套数字(那就是两份实现)。
720
+ * ⭐ 同一条参数化让 10-21 旨「平面切成 2*2=4 份,不用切成 16 份了」只需要改 `IMAGE_TILE_GRID` 一处。
721
+ *
722
+ * ⚠️ 只描述「格子怎么排」,不含槽纹理像素尺寸(那是 GL 资源,由拼合器按消费方给的 layout 建)。
723
+ * ⚠️ `slotCellCols` / `slotCellRows` = **每槽横几格 / 竖几格**(10-23 起两个数独立,全景是 1×3 一竖列;
724
+ * 旧的单一 `slotGrid`「横竖同数」假设已整条作废 ⇒ 格数 = `slotCellCols * slotCellRows`)。
725
+ */
726
+ export interface TileGridLayout {
727
+ /** 原图横向切几片 */
728
+ cols: number;
729
+ /** 原图纵向切几片 */
730
+ rows: number;
731
+ /** 一个拼合槽横向几格 */
732
+ slotCellCols: number;
733
+ /** 一个拼合槽纵向几格 */
734
+ slotCellRows: number;
735
+ /** 拼合槽个数 */
736
+ slotCount: number;
737
+ /** 每片向四边多切的原图像素(治槽内格子缝,见 `RT_STITCH_SEAM_MARGIN_PX`) */
738
+ seamMarginPx: number;
739
+ }
740
+
741
+ /** 全景那一份网格 = 上面这组 `RT_STITCH_*` 常数的**同一份数值**(⛔ 不是另立一套:常数由它反向派生) */
742
+ export const PANO_TILE_GRID: TileGridLayout = {
743
+ cols: RT_STITCH_COLS,
744
+ rows: RT_STITCH_ROWS,
745
+ slotCellCols: RT_STITCH_SLOT_CELL_COLS,
746
+ slotCellRows: RT_STITCH_SLOT_CELL_ROWS,
747
+ slotCount: RT_STITCH_SLOT_COUNT,
748
+ seamMarginPx: RT_STITCH_SEAM_MARGIN_PX,
749
+ };
750
+
751
+ /**
752
+ * 图片平面(皇上旨 2026-10-20「先添加一个平面,再给这个平面上传图片,上传的这个图切成 4*4=16 片」
753
+ * → 2026-10-21 改口「切成 2*2=4 份,不用切成 16 份了」)的网格:
754
+ * 横 2 × 竖 2 = 4 片,全部装进 **1 个拼合槽**(平面是一张普通矩形贴图,不像全景那样左右半幅分 sampler)。
755
+ * `seamMarginPx` 与全景**同档**(8px):缝的病根一模一样(格子边界上采样到邻居),这一档不随网格重调,
756
+ * ⛔ 也不为这一旨新加「图片专用缝宽」那种旋钮(网格只有一份来源,改片数只动 cols/rows/slotCellCols/slotCellRows)。
757
+ * ⚠️ 改这里只影响**新上传**的平面:老存档里那 16 条地址喂进 4 片的布局会被 `ensure` 的
758
+ * 「张数 > 满批 ⇒ 直接不收」挡掉(平面停在缺失灰),按 10-20 的口径不做兼容、重传即可。
759
+ */
760
+ export const IMAGE_TILE_GRID: TileGridLayout = {
761
+ cols: 2,
762
+ rows: 2,
763
+ slotCellCols: 2,
764
+ slotCellRows: 2,
765
+ slotCount: 1,
766
+ seamMarginPx: RT_STITCH_SEAM_MARGIN_PX,
767
+ };
768
+
769
+ /** 该网格一批几条地址 */
770
+ export function tilesPerBatchOf(grid: TileGridLayout): number {
771
+ return grid.cols * grid.rows;
772
+ }
773
+
774
+ /** 该网格一槽几格 */
775
+ export function cellsPerSlotOf(grid: TileGridLayout): number {
776
+ return grid.slotCellCols * grid.slotCellRows;
777
+ }
778
+
779
+ /**
780
+ * 槽 `slot` 用哪几片瓦片(原图 `cols × rows` 行优先下标 `r*cols+c`;槽内格序 = `slotCellCols×slotCellRows`
781
+ * 行优先、图像左上起)。
782
+ * ⛔ 手抄下标表必然出错(全景那张表就是靠生成 + 护栏逐个数钉住的),故两种网格共用这一座生成器。
783
+ */
784
+ export function slotTilesOf(grid: TileGridLayout): number[][] {
785
+ // 横向几个槽(cols/slotCellCols);纵向槽数 = rows/slotCellRows(全景 3/3=1 ⇒ sr 恒 0 = 全高)
786
+ const slotCols = grid.cols / grid.slotCellCols;
787
+ const slots: number[][] = [];
788
+ for (let slot = 0; slot < grid.slotCount; slot++) {
789
+ const sr = Math.floor(slot / slotCols) * grid.slotCellRows;
790
+ const sc = (slot % slotCols) * grid.slotCellCols;
791
+ const ids: number[] = [];
792
+ for (let r = 0; r < grid.slotCellRows; r++) {
793
+ for (let c = 0; c < grid.slotCellCols; c++) ids.push((sr + r) * grid.cols + (sc + c));
794
+ }
795
+ slots.push(ids);
796
+ }
797
+ return slots;
798
+ }
799
+
800
+ /** 拼合器内部使用的完整布局 = 网格描述 + RT 像素尺寸(GL 资源参数,消费方建组时传入)。 */
801
+ export type TileLayout = TileGridLayout & { rtW: number; rtH: number };
802
+
803
+ /** 全景的标准布局(3×3 网格 / 3 槽 / 4096×6144 RT)—— 一槽 = 原图一整列 */
804
+ export const PANO_TILE_LAYOUT: TileLayout = { ...PANO_TILE_GRID, rtW: RT_STITCH_SLOT_W, rtH: RT_STITCH_SLOT_H };
805
+
806
+ /**
807
+ * 图片平面的布局:2×2 网格 / 1 槽,RT 宽高比 = 原图宽高比,长边 4096(10-21 旨把 16 片改成 4 片,
808
+ * RT 那一份分辨率一个字没动 ⇒ 最终盖在平面上的仍是 4096×按比例 这一张,每格 RT 像素 / 每格原图像素的
809
+ * 配比也与 16 片那版恒同,⛔ 不是降质,只是少切 12 片、待画格从 16 格减到 4 格)。
810
+ * 平面材质 UV [0,1]² ⇒ 不管平面尺寸多大都不会拉伸,RT 尺寸只跟原图走。
811
+ */
812
+ export function imageTileLayout(natW: number, natH: number): TileLayout {
813
+ const LONG = 4096;
814
+ const rtW = natW >= natH ? LONG : Math.round(LONG * natW / natH);
815
+ const rtH = natH > natW ? LONG : Math.round(LONG * natH / natW);
816
+ return { ...IMAGE_TILE_GRID, rtW, rtH };
817
+ }
818
+
819
+ /** 9 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
689
820
  * 09-29 晚旨「不足张数缺的地方涂灰」后批次里允许有空串占位 ⇒ 取首个非空,防首张缺失时键为空串互撞) */
690
821
  export function rtStitchBatchOf(tiles: string[]): string {
691
822
  return tiles.find((u) => u) || "";
@@ -696,7 +827,7 @@ export function rtStitchKey(batch: string, slot: number): string {
696
827
  return `${RT_STITCH_KEY_PREFIX}${batch}#${slot}`;
697
828
  }
698
829
 
699
- /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 片 / 64 片 / 现行 32 片**同一个前缀**,键本身不含片数信息 */
830
+ /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 / 32 / 64 / 现行 9 片**同一个前缀**,键本身不含片数信息 */
700
831
  export function isRtStitchKey(key: string): boolean {
701
832
  return key.startsWith(RT_STITCH_KEY_PREFIX);
702
833
  }
@@ -710,33 +841,18 @@ export function rtStitchKeyParts(key: string): { batch: string; slot: number } |
710
841
  return { batch: body.slice(0, i), slot: Number(body.slice(i + 1)) };
711
842
  }
712
843
 
713
- /** 槽 slot 用哪 16 张瓦片(原图 **8 列 × 4 行 行优先**下标 `r*8+c`;**槽内格序 = 4×4 行优先、图像左上起**):
714
- * 槽 0 = 图像**左半幅**(行 0-3 × 列 0-3)→ `[0,1,2,3, 8,9,10,11, 16,17,18,19, 24,25,26,27]`;
715
- * 槽 1 = **右半幅**(行 0-3 × 列 4-7)→ `[4,5,6,7, 12,13,14,15, 20,21,22,23, 28,29,30,31]`。
716
- * ⭐ 竖着**不再分块**(皇上旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 两个槽**都是全高**,只按 u 分左右,
717
- * 故槽纹理也必须跟着变正方形(见 `RT_STITCH_SLOT_W/H` 那条注释,⛔ 别只改这里漏了那边)。
718
- * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 32 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
844
+ /** 槽 slot 用哪 3 张瓦片(原图 **3 列 × 3 行 行优先**下标 `r*3+c`;**槽内格序 = 横 1 × 竖 3、图像左上起**):
845
+ * 槽 0 = **左列** → `[0,3,6]`;槽 1 = **中列** → `[1,4,7]`;槽 2 = **右列** → `[2,5,8]`。
846
+ * ⭐ 只按 **u** 分槽(皇上旨 2026-10-23「每个 RT 竖直排列 3 个瓦片」⇒ 一槽 = 原图一整列 = 横 120° × 全高 180°),
847
+ * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 9 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
719
848
  * 对着手写死的字面量逐个数断言 —— 改了网格常数而护栏没跟着改,当场红。 */
720
- export const RT_STITCH_SLOT_TILES: ReadonlyArray<ReadonlyArray<number>> = (() => {
721
- // 横向几个槽(COLS/SLOT_GRID = 8/4 = 2);纵向恒 1 个槽(ROWS/SLOT_GRID = 4/4 = 1 ⇒ sr 恒 0 = 全高)
722
- const slotCols = RT_STITCH_COLS / RT_STITCH_SLOT_GRID;
723
- const slots: number[][] = [];
724
- for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
725
- const sr = Math.floor(slot / slotCols) * RT_STITCH_SLOT_GRID;
726
- const sc = (slot % slotCols) * RT_STITCH_SLOT_GRID;
727
- const ids: number[] = [];
728
- for (let r = 0; r < RT_STITCH_SLOT_GRID; r++) {
729
- for (let c = 0; c < RT_STITCH_SLOT_GRID; c++) ids.push((sr + r) * RT_STITCH_COLS + (sc + c));
730
- }
731
- slots.push(ids);
732
- }
733
- return slots;
734
- })();
849
+ export const RT_STITCH_SLOT_TILES: ReadonlyArray<ReadonlyArray<number>> = slotTilesOf(PANO_TILE_GRID);
735
850
 
736
851
  /** 槽内第 cell 格在**图像方向**的位置 `[列, 行]`(0 起,与 `RT_STITCH_SLOT_TILES` 的格序同一口径)。
737
- * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。 */
738
- export function rtStitchCellPos(cell: number): [number, number] {
739
- return [cell % RT_STITCH_SLOT_GRID, Math.floor(cell / RT_STITCH_SLOT_GRID)];
852
+ * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。
853
+ * `slotCellCols` 默认全景那档(1 = 一槽就是一竖列);图片平面传 2。 */
854
+ export function rtStitchCellPos(cell: number, slotCellCols: number = PANO_TILE_GRID.slotCellCols): [number, number] {
855
+ return [cell % slotCellCols, Math.floor(cell / slotCellCols)];
740
856
  }
741
857
 
742
858
  /** 一片瓦片的切图矩形(原图像素)。`sx/sy/sw/sh` = **含多切余量**的实际切片区;
@@ -754,49 +870,52 @@ export interface RtStitchTileRect {
754
870
 
755
871
  /** 网格取整:第 `i` 条分界线在原图 `n` 像素上的位置(`round` ⇒ 余数摊到各格,
756
872
  * ⛔ 不必再像旧上传代码那样特判「末行末列吸收余数」)。
757
- * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):10-06 晚起两个方向
758
- * **不再相等**(8×4),必须显式传,⛔ 不许再拿一个「每边片数」两边共用 —— 竖着按 8 切 = 纵向切错。 */
873
+ * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):两个方向必须各自显式传,
874
+ * ⛔ 不许再拿一个「每边片数」两边共用 —— 横 3 竖 8 这类混用 = 纵向切错。 */
759
875
  function gridLine(i: number, n: number, steps: number): number {
760
876
  return Math.round((i * n) / steps);
761
877
  }
762
878
 
763
- /** 32 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*8+c` 与 `RT_STITCH_SLOT_TILES` 同源;
764
- * ⭐ 10-06 晚旨「横 8 × 竖 4 = 32」:`r` 走 `RT_STITCH_ROWS`(4)、`c` 走 `RT_STITCH_COLS`(8),不再相等)。
879
+ /** 9 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*3+c` 与 `RT_STITCH_SLOT_TILES` 同源;
880
+ * ⭐ 10-23 旨「横 3 × 竖 3 = 9」:`r` 走 `RT_STITCH_ROWS`(3)、`c` 走 `RT_STITCH_COLS`(3)。
765
881
  *
766
882
  * ⭐ **上传侧(`panoramaUpload`)与拼合侧(`PanoTileStitcher`)必须共用这一份几何**:
767
883
  * 上传多切多少、拼合就内缩多少;两边各算一次就是「缝没治掉、反而把画面挤歪」。
768
- * 拼合侧拿不到原图尺寸(它只有 32 个 URL 和 32 张已解码纹理),所以它用 `rtStitchTileInset`
884
+ * 拼合侧拿不到原图尺寸(它只有 9 个 URL 和 9 张已解码纹理),所以它用 `rtStitchTileInset`
769
885
  * —— 那条只需「下标 + 该瓦片自己的像素尺寸」,两者等价的前提是「多切量 = 常数 M、只有贴边那几片被裁」,
770
886
  * 而「贴不贴边」纯由下标决定 ⇒ 护栏 BE 组用同一批 (W,H) 把两条算路对表钉死。 */
771
- export function rtStitchTileRects(w: number, h: number): RtStitchTileRect[] {
887
+ export function rtStitchTileRects(w: number, h: number, grid: TileGridLayout = PANO_TILE_GRID): RtStitchTileRect[] {
772
888
  const out: RtStitchTileRect[] = [];
773
- for (let r = 0; r < RT_STITCH_ROWS; r++) {
774
- for (let c = 0; c < RT_STITCH_COLS; c++) {
775
- const x0 = gridLine(c, w, RT_STITCH_COLS);
776
- const x1 = gridLine(c + 1, w, RT_STITCH_COLS);
777
- const y0 = gridLine(r, h, RT_STITCH_ROWS);
778
- const y1 = gridLine(r + 1, h, RT_STITCH_ROWS);
779
- const ml = Math.min(RT_STITCH_SEAM_MARGIN_PX, x0);
780
- const mr = Math.min(RT_STITCH_SEAM_MARGIN_PX, w - x1);
781
- const mt = Math.min(RT_STITCH_SEAM_MARGIN_PX, y0);
782
- const mb = Math.min(RT_STITCH_SEAM_MARGIN_PX, h - y1);
889
+ for (let r = 0; r < grid.rows; r++) {
890
+ for (let c = 0; c < grid.cols; c++) {
891
+ const x0 = gridLine(c, w, grid.cols);
892
+ const x1 = gridLine(c + 1, w, grid.cols);
893
+ const y0 = gridLine(r, h, grid.rows);
894
+ const y1 = gridLine(r + 1, h, grid.rows);
895
+ const ml = Math.min(grid.seamMarginPx, x0);
896
+ const mr = Math.min(grid.seamMarginPx, w - x1);
897
+ const mt = Math.min(grid.seamMarginPx, y0);
898
+ const mb = Math.min(grid.seamMarginPx, h - y1);
783
899
  out.push({ sx: x0 - ml, sy: y0 - mt, sw: x1 + mr - (x0 - ml), sh: y1 + mb - (y0 - mt), ml, mt, mr, mb });
784
900
  }
785
901
  }
786
902
  return out;
787
903
  }
788
904
 
789
- /** 这片瓦片有没有贴原图边界(8×4 ⇒ 32 片里四周那 20 片至少贴一条边;贴边的**那一侧**多切量被裁成 0)。
905
+ /** 这片瓦片有没有贴原图边界(3×3 ⇒ 9 片里四周那 8 片至少贴一条边,只有正中心那片四边都不贴;贴边的**那一侧**多切量被裁成 0)。
790
906
  * 拼合侧靠它 + 瓦片自己的像素尺寸还原内缩量 ⇒ 不必知道原图多大。
791
- * ⚠️ 下标拆分必须按 **列数 COLS=8**(`i = r*8+c`),⛔ 不是 ROWS。 */
792
- export function rtStitchTileEdgeClip(i: number): { left: boolean; right: boolean; top: boolean; bottom: boolean } {
793
- const r = Math.floor(i / RT_STITCH_COLS);
794
- const c = i % RT_STITCH_COLS;
907
+ * ⚠️ 下标拆分必须按 **列数 COLS**(`i = r*cols+c`),⛔ 不是 ROWS。 */
908
+ export function rtStitchTileEdgeClip(
909
+ i: number,
910
+ grid: TileGridLayout = PANO_TILE_GRID
911
+ ): { left: boolean; right: boolean; top: boolean; bottom: boolean } {
912
+ const r = Math.floor(i / grid.cols);
913
+ const c = i % grid.cols;
795
914
  return {
796
915
  left: c === 0,
797
- right: c === RT_STITCH_COLS - 1,
916
+ right: c === grid.cols - 1,
798
917
  top: r === 0,
799
- bottom: r === RT_STITCH_ROWS - 1,
918
+ bottom: r === grid.rows - 1,
800
919
  };
801
920
  }
802
921
 
@@ -809,9 +928,14 @@ export function rtStitchTileEdgeClip(i: number): { left: boolean; right: boolean
809
928
  *
810
929
  * ⚠️ v 方向口径(与 `panoBitmapTexture` 绑死):瓦片走 `imageOrientation:"flipY"` 预翻 + `texture.flipY=false`
811
930
  * ⇒ 纹理 **v=0 = 图像下边**,所以图像顶部余量 `mt` 落在 `v1`、底部 `mb` 落在 `v0`。搞反 = 上下颠倒。 */
812
- export function rtStitchTileInset(i: number, tw: number, th: number): [number, number, number, number] {
813
- const e = rtStitchTileEdgeClip(i);
814
- const m = RT_STITCH_SEAM_MARGIN_PX;
931
+ export function rtStitchTileInset(
932
+ i: number,
933
+ tw: number,
934
+ th: number,
935
+ grid: TileGridLayout = PANO_TILE_GRID
936
+ ): [number, number, number, number] {
937
+ const e = rtStitchTileEdgeClip(i, grid);
938
+ const m = grid.seamMarginPx;
815
939
  const u0 = e.left ? 0 : Math.min(0.5, m / tw);
816
940
  const u1 = e.right ? 1 : 1 - Math.min(0.5, m / tw);
817
941
  const v0 = e.bottom ? 0 : Math.min(0.5, m / th);
@@ -820,13 +944,13 @@ export function rtStitchTileInset(i: number, tw: number, th: number): [number, n
820
944
  }
821
945
 
822
946
  /**
823
- * 「这 `RT_STITCH_SLOT_COUNT`(2) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
947
+ * 「这 `RT_STITCH_SLOT_COUNT`(3) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
824
948
  * 只改标记不改地址(或反之)= 用整图 UV 去采一张瓦片 / 用瓦片内 UV 去采整图,画面立刻错乱。
825
949
  * 非 tiled 态(或该点压根没切过片)一律回退成「各槽同为一张整图」。
826
- * ⭐ 2026-10-06 晚旨「只支持 32 张(8×4)」:只要有瓦片(1~32 条)一律走拼合器(rt: 虚拟键),
827
- * 不足 32 张的位置由拼合器**涂灰**(一眼看出数据不全);8×8=64 张与 4×4=16 张那两版**都不做兼容**
950
+ * ⭐ 2026-10-23 旨「只支持 9 张(3×3)」:只要有瓦片(1~9 条)一律走拼合器(rt: 虚拟键),
951
+ * 不足 9 张的位置由拼合器**涂灰**(一眼看出数据不全);8×4=32 张、8×8=64 张与 4×4=16 张那几版**都不做兼容**
828
952
  * (皇上「无须兼容正常使用,有异常我会重传」⇒ 旧存档的 64 条地址超张数被判废、重传即可)。
829
- * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 2 个 rtStitchKey —— 槽数再变会漏改)。
953
+ * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 3 个 rtStitchKey —— 槽数再变会漏改)。
830
954
  */
831
955
  export function mktPanoTexUrls(url: string, tiles: string[] | undefined, mode: PanoTexMode = "tiled"): string[] {
832
956
  if (mode === "tiled" && tiles && tiles.length > 0) {
@@ -837,8 +961,8 @@ export function mktPanoTexUrls(url: string, tiles: string[] | undefined, mode: P
837
961
  }
838
962
 
839
963
  /**
840
- * 「这 2 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
841
- * `tiles` 有 **任意 1~32 条**地址(不足 32 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
964
+ * 「这 3 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
965
+ * `tiles` 有 **任意 1~9 条**地址(不足 9 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
842
966
  * 否则各槽同为一张整图(旧数据 / 种子图 / 用户切整片)。
843
967
  */
844
968
  export function mktPanoTiledFlag(tiles: string[] | undefined, mode: PanoTexMode = "tiled"): 0 | 1 {
@@ -874,10 +998,10 @@ export function setMktPanoTiled(
874
998
  export function createMktShaderMaterial(): ShaderMaterial {
875
999
  return new ShaderMaterial({
876
1000
  uniforms: {
877
- // 一个全景点恒占 **2 个 sampler**(2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」):
878
- // panoramas[0..1] = 2 张**离屏拼合槽纹理**([左半幅, 右半幅],每槽由 `RT_STITCH_CELLS_PER_SLOT`
879
- // 片瓦片拼成),片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
880
- // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 2 张)—— 槽数再变这里会漏改。
1001
+ // 一个全景点恒占 **3 个 sampler**(2026-10-23 旨「全景的 RT 由原来的 2 个分成 3 个」):
1002
+ // panoramas[0..2] = 3 张**离屏拼合槽纹理**([左列, 中列, 右列],每槽由 `RT_STITCH_CELLS_PER_SLOT`
1003
+ // 片瓦片竖着拼成),片元按全景 UV 的 **u** 三等分挑槽采样(v 恒全高)。
1004
+ // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 3 张)—— 槽数再变这里会漏改。
881
1005
  panoramas: {
882
1006
  value: Array.from({ length: RT_STITCH_SLOT_COUNT }, () => createPlaceholderTexture()),
883
1007
  },
@@ -11,8 +11,8 @@ export interface PanoRosterCand<T> {
11
11
  * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
12
12
  * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
13
13
  * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
14
- * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
15
- * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
14
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-23 旨后是 **5**:5 点 × 每个全景
15
+ * 左中右 3 张 RT = 15 sampler,GLSL 物理上限仍是 16。⛔ 本函数不许假设它恒等于 4 或 8)
16
16
  * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
17
17
  * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
18
18
  * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,