three-base 2.0.142 → 2.0.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/DirectionalLight.d.ts +6 -6
  2. package/DirectionalLight.js +68 -68
  3. package/EventMap.d.ts +103 -103
  4. package/EventMap.js +17 -17
  5. package/EventMesh.d.ts +18 -18
  6. package/EventMesh.js +59 -59
  7. package/ExtrudeMesh.d.ts +112 -112
  8. package/ExtrudeMesh.js +342 -342
  9. package/FPScontrol.d.ts +511 -511
  10. package/FPScontrol.js +1799 -1799
  11. package/FPScontrol.ts +1855 -1855
  12. package/Fire/Fire.d.ts +28 -28
  13. package/Fire/Fire.js +80 -80
  14. package/Fire/FireGeometry.d.ts +9 -9
  15. package/Fire/FireGeometry.js +40 -40
  16. package/Fire/FireMaterial.d.ts +28 -28
  17. package/Fire/FireMaterial.js +32 -32
  18. package/Fire/index.d.ts +2 -2
  19. package/Fire/index.js +2 -2
  20. package/GLTFDracoLoader.d.ts +71 -71
  21. package/GLTFDracoLoader.js +266 -266
  22. package/GLTFDracoLoader.ts +1 -1
  23. package/Grass/Grass.d.ts +8 -8
  24. package/Grass/Grass.js +38 -38
  25. package/Grass/index.d.ts +2 -2
  26. package/Grass/index.js +2 -2
  27. package/Ground1/Ground1.d.ts +10 -10
  28. package/Ground1/Ground1.js +71 -71
  29. package/Ground1/index.d.ts +1 -1
  30. package/Ground1/index.js +1 -1
  31. package/MeshBase.d.ts +36 -36
  32. package/MeshBase.js +247 -247
  33. package/Mirror.d.ts +25 -25
  34. package/Mirror.js +143 -143
  35. package/MktMesh/MktMesh.d.ts +301 -291
  36. package/MktMesh/MktMesh.js +1064 -1045
  37. package/MktMesh/MktMesh.ts +32 -11
  38. package/MktMesh/assetUrl.d.ts +59 -35
  39. package/MktMesh/assetUrl.js +154 -69
  40. package/MktMesh/assetUrl.ts +155 -69
  41. package/MktMesh/mktMappingShader.d.ts +12 -12
  42. package/MktMesh/mktMappingShader.js +138 -138
  43. package/MktMesh/mktMappingShader.ts +139 -139
  44. package/MktMesh/mktShader.d.ts +263 -263
  45. package/MktMesh/mktShader.js +1236 -1236
  46. package/MktMesh/mktShader.ts +1344 -1344
  47. package/MktMesh/panoBitmapTexture.d.ts +25 -25
  48. package/MktMesh/panoBitmapTexture.js +52 -52
  49. package/MktMesh/panoBitmapTexture.ts +54 -54
  50. package/MktMesh/panoSlotBind.d.ts +29 -29
  51. package/MktMesh/panoSlotBind.js +106 -106
  52. package/MktMesh/panoSlotBind.ts +153 -153
  53. package/MktMesh/panoSlotRoster.d.ts +22 -22
  54. package/MktMesh/panoSlotRoster.js +169 -169
  55. package/MktMesh/panoSlotRoster.ts +168 -168
  56. package/MktMesh/panoTileStitcher.d.ts +119 -119
  57. package/MktMesh/panoTileStitcher.js +529 -529
  58. package/MktMesh/panoTileStitcher.ts +616 -616
  59. package/MktMesh/types.d.ts +75 -75
  60. package/MktMesh/types.js +1 -1
  61. package/MktMesh/wallCapTessellate.d.ts +33 -33
  62. package/MktMesh/wallCapTessellate.js +97 -97
  63. package/MktMesh/wallCapTessellate.ts +68 -68
  64. package/PipeMesh/PathGeometry.d.ts +44 -44
  65. package/PipeMesh/PathGeometry.js +307 -307
  66. package/PipeMesh/PathPoint.d.ts +17 -17
  67. package/PipeMesh/PathPoint.js +31 -31
  68. package/PipeMesh/PathPointList.d.ts +31 -31
  69. package/PipeMesh/PathPointList.js +209 -209
  70. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  71. package/PipeMesh/PathTubeGeometry.js +130 -130
  72. package/PipeMesh/PipeMesh.d.ts +25 -25
  73. package/PipeMesh/PipeMesh.js +63 -63
  74. package/PipeMesh/index.d.ts +1 -1
  75. package/PipeMesh/index.js +1 -1
  76. package/PlaneModal1/PlaneModal1.d.ts +34 -34
  77. package/PlaneModal1/PlaneModal1.js +109 -109
  78. package/PlaneModal1/index.d.ts +1 -1
  79. package/PlaneModal1/index.js +1 -1
  80. package/PlaneText.d.ts +27 -27
  81. package/PlaneText.js +35 -35
  82. package/PlaneWithHtml.d.ts +13 -13
  83. package/PlaneWithHtml.js +64 -64
  84. package/PointLight/PointLight.d.ts +13 -13
  85. package/PointLight/PointLight.js +64 -64
  86. package/PointLight/index.d.ts +1 -1
  87. package/PointLight/index.js +1 -1
  88. package/Sky/Sky.d.ts +9 -9
  89. package/Sky/Sky.js +22 -22
  90. package/Sky/index.d.ts +1 -1
  91. package/Sky/index.js +1 -1
  92. package/SpotLight.d.ts +6 -6
  93. package/SpotLight.js +72 -72
  94. package/SpriteModal1/SpriteModal1.d.ts +33 -33
  95. package/SpriteModal1/SpriteModal1.js +116 -116
  96. package/SpriteModal1/index.d.ts +1 -1
  97. package/SpriteModal1/index.js +1 -1
  98. package/SpriteText.d.ts +46 -46
  99. package/SpriteText.js +176 -176
  100. package/SpriteText2.d.ts +48 -48
  101. package/SpriteText2.js +223 -223
  102. package/SpriteText3.d.ts +86 -86
  103. package/SpriteText3.js +517 -517
  104. package/SpriteText3.ts +599 -599
  105. package/SpriteWithHtml.d.ts +9 -9
  106. package/SpriteWithHtml.js +44 -44
  107. package/Text3D/Text3D.d.ts +20 -20
  108. package/Text3D/Text3D.js +58 -58
  109. package/Text3D/fontController.d.ts +60 -60
  110. package/Text3D/fontController.js +166 -166
  111. package/Text3D/index.d.ts +1 -1
  112. package/Text3D/index.js +1 -1
  113. package/ThreeBase.d.ts +248 -248
  114. package/ThreeBase.js +1202 -1202
  115. package/ThreeBase.ts +1314 -1314
  116. package/ThreeMouse.d.ts +53 -54
  117. package/ThreeMouse.js +453 -453
  118. package/Transition.d.ts +45 -45
  119. package/Transition.js +209 -209
  120. package/Water/Water.d.ts +5 -5
  121. package/Water/Water.js +44 -44
  122. package/Water/index.d.ts +2 -2
  123. package/Water/index.js +2 -2
  124. package/WaterFlow/FloatingDuck.d.ts +88 -88
  125. package/WaterFlow/FloatingDuck.js +307 -307
  126. package/WaterFlow/FloatingDuck.ts +445 -445
  127. package/WaterFlow/GPUFluidSimulation.d.ts +56 -56
  128. package/WaterFlow/GPUFluidSimulation.js +781 -781
  129. package/WaterFlow/GPUFluidSimulation.ts +827 -827
  130. package/WaterFlow/ReservoirSystem.d.ts +106 -106
  131. package/WaterFlow/ReservoirSystem.js +611 -611
  132. package/WaterFlow/ReservoirSystem.ts +754 -754
  133. package/WaterFlow/SPHField.d.ts +35 -35
  134. package/WaterFlow/SPHField.js +203 -203
  135. package/WaterFlow/SPHField.ts +228 -228
  136. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -69
  137. package/WaterFlow/ScreenSpaceFluidPass.js +728 -728
  138. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -789
  139. package/WaterFlow/ShallowWaterSurface.d.ts +45 -45
  140. package/WaterFlow/ShallowWaterSurface.js +438 -438
  141. package/WaterFlow/ShallowWaterSurface.ts +510 -510
  142. package/WaterFlow/WaterFlow.d.ts +87 -87
  143. package/WaterFlow/WaterFlow.js +171 -171
  144. package/WaterFlow/WaterFlow.ts +219 -219
  145. package/WaterFlow/types.d.ts +161 -161
  146. package/WaterFlow/types.js +1 -1
  147. package/WaterFlow/types.ts +189 -189
  148. package/WaterFlow/waterFlowPass.d.ts +3 -3
  149. package/WaterFlow/waterFlowPass.js +20 -20
  150. package/WaterFlow/waterFlowPass.ts +40 -40
  151. package/collisionOBB.d.ts +42 -42
  152. package/collisionOBB.js +112 -112
  153. package/collisionOBB.ts +108 -108
  154. package/declare.d.ts +25 -25
  155. package/index.d.ts +1 -1
  156. package/index.js +1 -1
  157. package/instanced/InstancedBatch.d.ts +57 -57
  158. package/instanced/InstancedBatch.js +348 -348
  159. package/instanced/InstancedBatch.ts +406 -406
  160. package/instanced/InstancedChild.d.ts +174 -174
  161. package/instanced/InstancedChild.js +264 -264
  162. package/instanced/InstancedChild.ts +327 -327
  163. package/instanced/InstancedGroup.d.ts +43 -43
  164. package/instanced/InstancedGroup.js +126 -126
  165. package/instanced/InstancedGroup.ts +137 -137
  166. package/instanced/InstancedMeshBase.d.ts +38 -38
  167. package/instanced/InstancedMeshBase.js +282 -282
  168. package/instanced/InstancedTemplate.d.ts +46 -46
  169. package/instanced/InstancedTemplate.js +125 -125
  170. package/keyControls.d.ts +29 -30
  171. package/keyControls.js +101 -101
  172. package/lensflare/LensFlare.d.ts +39 -39
  173. package/lensflare/LensFlare.js +284 -284
  174. package/package.json +6 -1
  175. package/pass/bloom_pass.d.ts +2 -2
  176. package/pass/bloom_pass.js +174 -174
  177. package/pass/cover_pass.d.ts +2 -2
  178. package/pass/cover_pass.js +108 -108
  179. package/pass/debugDepthState.d.ts +20 -20
  180. package/pass/debugDepthState.js +9 -9
  181. package/pass/debugDepthState.ts +22 -22
  182. package/pass/depthTestBias.d.ts +12 -12
  183. package/pass/depthTestBias.js +12 -12
  184. package/pass/depthTestBias.ts +12 -12
  185. package/pass/effectUserData.d.ts +27 -27
  186. package/pass/effectUserData.js +1 -1
  187. package/pass/effectUserData.ts +46 -46
  188. package/pass/frosted_glass_pass.d.ts +2 -2
  189. package/pass/frosted_glass_pass.js +150 -150
  190. package/pass/fsQuad.d.ts +9 -9
  191. package/pass/fsQuad.js +28 -28
  192. package/pass/glow_pass.d.ts +2 -2
  193. package/pass/glow_pass.js +350 -350
  194. package/pass/screen_bloom_pass.d.ts +11 -11
  195. package/pass/screen_bloom_pass.js +136 -136
  196. package/pass/ssao_pass.d.ts +88 -88
  197. package/pass/ssao_pass.js +799 -799
  198. package/pass/ssao_pass.ts +918 -918
  199. package/pass/stroke_pass.d.ts +2 -2
  200. package/pass/stroke_pass.js +267 -267
  201. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  202. package/shaders/FXAAShaderMaterial.js +20 -20
  203. package/shaders/baseDepthVisShader.d.ts +11 -11
  204. package/shaders/baseDepthVisShader.js +36 -36
  205. package/shaders/baseDepthVisShader.ts +36 -36
  206. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  207. package/shaders/baseTextureToRenderShader.js +19 -19
  208. package/shaders/copyShader.d.ts +3 -3
  209. package/shaders/copyShader.js +13 -13
  210. package/shaders/debugDepthCompareShader.d.ts +14 -14
  211. package/shaders/debugDepthCompareShader.js +49 -49
  212. package/shaders/debugDepthCompareShader.ts +50 -50
  213. package/shaders/debugDepthJudgeShader.d.ts +18 -18
  214. package/shaders/debugDepthJudgeShader.js +52 -52
  215. package/shaders/debugDepthJudgeShader.ts +54 -54
  216. package/shaders/debugExcludedShader.d.ts +13 -13
  217. package/shaders/debugExcludedShader.js +46 -46
  218. package/shaders/debugExcludedShader.ts +48 -48
  219. package/spriteTextBg.d.ts +1 -1
  220. package/spriteTextBg.js +20 -20
  221. package/spriteTextBg.ts +20 -20
  222. package/threeLoader.d.ts +35 -35
  223. package/threeLoader.js +518 -518
  224. package/threeUtils.d.ts +53 -53
  225. package/threeUtils.js +329 -329
  226. package/types.d.ts +2 -2
  227. package/types.js +6 -6
@@ -1,1248 +1,1248 @@
1
- /*
2
- * @Author: xiaosihan
3
- * @Date: 2026-08-25
4
- * @Description: MktMesh(墨卡托模型)的自定义 ShaderMaterial。
5
- *
6
- * 思路(与脚手架 render3DGoods 的 panoramaMercatorMaterial 同源):
7
- * 1. 顶点着色器:vProjPos = uProjInvMatrix * modelMatrix * im * position。
8
- * 墙体几何在构建时已把点位写入世界坐标、MktMesh 自身保持单位变换,故无需实例矩阵;
9
- * 纹理朝向的旋转由片元着色器乘 mat3(textureMatrix)(纹理矩阵左上 3x3 旋转分量)完成。
10
- * ⚠️ **取样方向的两个操作数,必须都对 MktMesh 的整体变换免疫**(皇上旨 2026-09-19):
11
- * textureMatrix 是**世界矩阵**(见下方片元 uniform 注释),属独立于 MktMesh 的外部数据,
12
- * 本来就不随 MktMesh 移动/旋转/缩放;而墙顶点经 modelMatrix 升到世界坐标,**会**跟着变 ——
13
- * 一个动、一个不动,方向就变了 => **墙上的纹理跟着滑动**(皇上报的正是此症)。
14
- * 故引入 uProjInvMatrix(默认单位阵):MktMesh 自己写 inverse(matrixWorld),把墙顶点也**拉回
15
- * 与整体变换无关的几何局部坐标**,两个操作数于是双双免疫 => 方向恒定 => 纹理稳稳贴在墙上。
16
- * 其余网格(编辑器地板/立方体、预览立方体)保持单位阵 => 行为与改造前逐字节一致。
17
- * ⚠️ 注意 M ≠ I 时 vProjPos(局部)与 textureMatrix(世界)并不在同一空间 —— 这是**刻意**的:
18
- * 皇上要的是「纹理贴在墙上不动」,达成条件就是「两个操作数都不随 M 变」,而非「同处一空间」。
19
- * 千万别"好心"再去把 textureMatrix 也乘一遍 inverse(matrixWorld),那会让纹理重新动起来。
20
- * 2. 片元着色器:方向向量 dir = normalize(vProjPos - 纹理矩阵位置),即「以纹理矩阵的位置为投影中心」;
21
- * 再乘纹理矩阵的旋转分量(mat3(textureMatrix)),得到纹理局部方向;
22
- * 最后按**等距柱状**(equirectangular)把方向换算成全景纹理 UV 采样。
23
- * ⚠️ 类名叫 MktMesh(墨卡托)是历史包袱,**取样口径不是墨卡托**:全景图是等距柱状图、
24
- * 像素行正比于纬度,故 v 必须线性于 lat(理由见片元着色器 v 换算处的说明)。
25
- * 3. 色彩空间 = **只有一条路**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
26
- * 全景纹理(瓦片 / 整图 / 拼合槽 RT)一律标 `colorSpace = SRGBColorSpace` ⇒ WebGL2 按 SRGB8_ALPHA8 上传,
27
- * `texture2D` 时**硬件已经做过一次 sRGB→linear 解码**,片元直接把采样值当线性值用、**不再手动 pow 解码**。
28
- * 原 `uColorSpace` uniform + 那段内联 pow 分支(sRGB/Linear 两档)已整条撤除 ⇒ 两遍都解 = 二次解码压暗。
29
- * toneMapped=false 跳过 Reinhard;末端 <colorspace_fragment> / 终处理只编码一次 ⇒ 一进一出,与源图逐字节还原。
30
- *
31
- * 兼容注意:为兼容 Windows / ANGLE 的 GLSL 编译器,本文件刻意不使用全局 const 常量与自定义函数,
32
- * 所有常量均以字面量内联书写;ShaderMaterial 为 GLSL ES 1.00,用 texture2D / gl_FragColor。
33
- */
34
- import { Color, DataTexture, DoubleSide, Matrix4, ShaderMaterial, SRGBColorSpace, Vector3, Vector4 } from "three";
35
- /**
36
- * 🔬 瓦片象限诊断开关(2026-09-21)。
37
- * 背景:皇上为「全景 2×2 瓦片上下方向」连调六轮,当时把「位置」与「块内朝向」绑在一起调
38
- * (tileRow 随 v 动),2×2=4 种组合**四档全试过仍不合预期** ⇒ 病灶必不在开关取值内,
39
- * 继续靠「交换 / 翻转 / 颠倒」这类语义词猜测只会无限循环,故改用**可视化归因**。
40
- *
41
- * ✅ **后续定论(2026-09-22 写死)**:四档轮转当时调不动,根因不在开关取值,而在**基础 v 映射口径**
42
- * (切片线性、取样墨卡托,见下方 🎛️ 段 ①)—— 口径错配是**连续**偏差,任何离散档位都凑不回来。
43
- * 现口径已统一为等距柱状线性,皇上验收「不交换 / 不翻转」那态即正确 ⇒ **方向参数整条撤除、写死**
44
- * (见 🎛️ 段)。本诊断开关保留,用于日后再次核对「哪块瓦片落在哪个象限」。
45
- *
46
- * 用法:页面 URL 带 `?panoTileDebug=1` 时,两张槽纹理**不再采样纹理**,各自输出一个纯色 ——
47
- * ⭐ 10-06 晚起只有左右两槽(竖着不再分块):红=左半幅(槽0) 绿=右半幅(槽1)
48
- * (旧 4 象限版是 红=tiles[0] 绿=tiles[1] 蓝=tiles[2] 黄=tiles[3],已随四槽一并作废)
49
- * 皇上只要说出「画面上下左右各是什么颜色」,即可一次反推出真实映射。
50
- *
51
- * ⚠️ 以**字面量 0.0/1.0** 内联进 GLSL:正常态整段被编译器消除,线上正常路径零开销、零行为差异。
52
- */
53
- /**
54
- * 诊断开关取值:URL 的 `?name=1` —— ⚠️ **search 与 hash 两处都认**。
55
- * 皇上的地址长这样:`https://www.3dkeshihua.com/three-base/#/mktMeshEdit?nocache=xxx`,
56
- * 查询串挂在 `#` **后面**(那是路由的 hash,`location.search` 是空串)⇒ 只查 search 的话开关永远按不下去。
57
- */
58
- function panoDebugOn(name) {
59
- try {
60
- if (typeof location === "undefined")
61
- return false;
62
- const re = new RegExp(`[?&]${name}=1\\b`);
63
- return re.test(location.search || "") || re.test(location.hash || "");
64
- }
65
- catch {
66
- return false;
67
- }
68
- }
69
- const TILE_DEBUG_GLSL = panoDebugOn("panoTileDebug") ? "1.0" : "0.0";
70
- /**
71
- * 🔬 归属诊断配色(皇上旨 2026-09-25「你为每个全景球设置不同的颜色, 然后把全景瓦片映射在墙面上时, 叠加该全景图的颜色,
72
- * 这样我就能看清楚整个墙面是不是叠加了同一个颜色 —— 若是同一个颜色, 说明整面都映射了同一张瓦片, 根本没按最近取」)。
73
- *
74
- * 用法:页面 URL 带 `?panoTint=1`(`#` 前后都认,见 `panoDebugOn`)⇒
75
- * 片元算完最终颜色后,**这块面按「它归哪个景点」染成该景点的固定色**(红/绿/蓝/黄;色相由景点决定,
76
- * 强度见下面的 `PANO_TINT_MIX`),
77
- * 同时应用层把**全景点标记**染成同一色板(图例,方便对「哪块色是哪个球」)⇒
78
- * 这一片墙/地板是什么色,它就是那个球投的;整墙一个色 = 只投了一张,两色交界 = 归属分界线。
79
- * ⚠️ 口径历经五版:① 「采样色 × 固定色」→ 叠在亮图上近白,皇上「完全看不到效果」;② 逐槽把 `linK` 整张换成纯色
80
- * → 墙对了,但地板渐隐成灰的那些地方压根没全景、于是也没色 ⇒ 皇上「颜色叠加怎么没有了」;
81
- * ③(皇上 09-25 口谕「就在最后算出来的颜色再叠加上颜色,不用想那么复杂」)⇒ 只在 **main 末尾对最终颜色**上一次色,
82
- * 归属判据与取图那条逐字一致,但地板盘外也照样上色,一眼看得出整间房怎么分家;
83
- * ④(皇上 09-25 近旨「地板有叠加色了,但墙上没有;每个全景点的叠加色都是红色,我想要让他们的颜色不一样」)
84
- * ⇒ 两处同一个根因:**色相按「纹理槽」给,而槽 ≠ 景点**。①「仅映射选中的那一个」模式下墙走的是另一份
85
- * **单全景材质**(`createMktShaderMaterial`,压根没有染色段)⇒ 墙当然没色;② 同一模式下地板只把选中的那张
86
- * 喂进 **0 号槽** ⇒ 不管选第几个点都涂 0 号的红。修法:色相改为**运行时按景点序号**给(`uPanoTintColors[slot]`
87
- * / 单全景材质 `uPanoTint`,两根 uniform 都只在开态存在),单全景材质末尾也补上同一条上色;色板抬成
88
- * **前 20 个景点各一色**(见 `panoTintHex`);
89
- * ⑤(皇上 09-26 晚「颜色叠加中, 叠加的颜色比例少一点」)⇒ ③④ 那两版把最终颜色**整块换成了色相**(只剩明暗),
90
- * 底下那张全景照片完全读不出来 ⇒ 改成 `mix(原色, 染色, PANO_TINT_MIX)`,两份材质共用这一个常数。
91
- *
92
- * ⚠️ 关掉时(默认)**一行 GLSL 都不生成**(JS 侧三元,不是 uniform 分支)⇒ 线上正常路径零开销、零行为差异。
93
- * ⚠️ 与 `panoTileDebug` 互不影响:那个看「哪一块瓦片落在哪个象限」,这个看「这一片墙归哪个全景」。
94
- */
95
- const PANO_TINT_ON = panoDebugOn("panoTint");
96
- /** 诊断配色是否开启(应用层据此把全景点标记染成同色板,两边对得上) */
97
- export const MKT_PANO_TINT_ON = PANO_TINT_ON;
98
- /**
99
- * 🔬 诊断色的**叠加强度**(0~1):`mix(原画面色, 染色, 本值)`。1 = 整块换成色相(09-25 初版,墙上只剩明暗,
100
- * 照片本身看不清);皇上旨 2026-09-26 晚「叠加的颜色比例少一点」⇒ 取 **0.5**:色相仍是主色、一眼分得出归属,
101
- * 但底下那张全景图的明暗与颜色能同时读出来。两份材质(融合 / 单全景)**共用这一个值**。
102
- */
103
- const PANO_TINT_MIX = 0.5;
104
- /**
105
- * 🔬 **距离明暗诊断图**(皇上旨 2026-09-26 晚「你计一下每个点距离最近的一个全景点的距离, 然后距离越小颜色就是白色,
106
- * 越远颜色就是黑色」)。
107
- *
108
- * 用法:分两条路,**各自独立**,谁也不靠谁:
109
- * · **编辑器(MktMeshEdit)= 建材质时显式传 `panoDist: mktPanoDistEditorOn()`** ⇒ 默认关、地址里写 `panoDist=1` 才开
110
- * (皇上旨 2026-09-27 晚「把颜色换成纹理,全景的纹理」—— 灰阶诊断让位给真投影,诊断图按需开)。
111
- * · **库侧(发布页 / 预览弹窗)= 不传 ⇒ 读 URL 的 `PANO_DIST_ON`,默认关** ⇒ 正常渲染路径零影响。
112
- * 开了之后片元算完最终颜色,按**这面这点到「它实际用的那张全景」的投影中心的水平距离**涂明暗:
113
- * **贴到球脚下 = 白,远到房间最远角 = 黑**。
114
- * 与归属配色的分工:`panoTint` 回答「这块归哪个球」(分家线),`panoDist` 回答「这点离自己的球到底有多远」——
115
- * 后者能把「整墙一个色」那面墙上的**连续渐变**照出来,距离判据是不是真在起作用一眼可辨(恒白/恒黑才是坏的)。
116
- * ⚠️ 距离口径与取图那条**同一个**:水平(xz)距离、候选 = 存活槽、门优先兜底不限门 ⇒ 白色区域就是那一张覆盖到的地方。
117
- * ⚠️ 黑端(多远算全黑)= 应用层每次喂的 `uPanoDistMax`,取**墙体上最远处到自己最近景点的那段水平距离**
118
- * (`_panoDistRange()`,皇上那间约 12m)⇒「黑」就是这间房可能出现的最大距离,跨面可比(不是每面各自归一化)。
119
- * ⛔ 别拿地板尺寸当尺子:默认地板盘 `FLOOR_SIZE_DEFAULT = 80`(远超房间),对角线 113m 会把整面墙的灰阶压平
120
- * (09-26 实测横扫三个点亮度 222/223/224,等于没有渐变)。
121
- * ⚠️ 明暗曲线 = **线性**(`1 − d/D`,皇上旨 2026-09-27「要,改成线性渐变」)。09-26 那版 `pow(…, 2.2)` 的动机是
122
- * 「着色器里是线性色,不补感知伽马会中间偏亮」—— 代价实测更糟:6m 处灰阶只剩 0.22 ⇒ 渐变几乎全压在景点脚下,
123
- * 他数不出圆。线性后同一点 0.50 中灰,整间房的远近一眼读得出。
124
- * ⚠️ 强度**不沿用** `PANO_TINT_MIX` 而是自己的 `PANO_DIST_MIX = 1`(整块换成灰阶):皇上要的是「近=白、远=黑」这张**距离图**本身,
125
- * 09-26 实测按 0.5 混 ⇒ 一面墙横扫过去只有 135→151(19 级)的差别,肉眼仍是「一片灰」;归属配色那条 0.5 的旨意针对的是
126
- * 「色叠加把照片盖住了」,与本图无关(要看照片本来颜色就把 `?panoDist` 关掉)。
127
- * ⚠️ 两道诊断**同时开**时本图后上色 ⇒ 明暗图盖住色相(要同时看归属与距离请分开截两张)。
128
- * ⚠️ 开关口径历经三版:① 初版「默认关、URL `?panoDist=1` 才开」⇒ 皇上第二天地址栏里只剩 `panoTint=1`,
129
- * 看到的又是实拍照片,误判「代码被改回去了」(09-26 实测线上包里 panoDist 一段俱在,纯为开关没跟上)。
130
- * ② 皇上同日晚旨「继续改成 距离越近越白、越远越黑的模式」⇒ 改成**编辑器这一页默认开**:写法是在 renderer
131
- * 模块顶层调 `mktPanoDistEditorDefaultOn()` 翻全局开关。09-26 实测线上打印当场暴露它是**半张图**:
132
- * 「墙 / 地板 / 家具三份材质的片元着色器不是同一份」—— 立方体那份是 `static material = create…()`,
133
- * 在**模块求值期**就建好了,永远早于任何应用层模块体里的语句 ⇒ 墙/地板黑了白了,立方体仍是实拍。
134
- * ③ 现行(本次定论):**开关不再靠语句先后**,而是建材质时把这一份材质要不要这张图**显式说清楚**
135
- * (融合工厂的 `panoDist` 选项)。⇒ 编辑器三处建材质点同传一个纯函数,预览/发布页不传 ⇒ 天然关。
136
- * ④ 09-27 晚皇上旨「把颜色换成纹理,全景的纹理」⇒ 编辑器这一页翻回**默认关**(`mktPanoDistEditorOn()`
137
- * = `panoDebugOn("panoDist")`),灰阶诊断按需 `?panoDist=1` 再看。口径机制不变,只翻默认值。
138
- * ⛔ 不许再回到「模块顶层翻全局变量」那一路:那份 GLSL 是建材质时一次性拼出来的,谁先谁后取决于 import 顺序,
139
- * 改一次 import 就悄悄漏一面(且 `统一性检查` 会当场报不是同一份源码)。
140
- */
141
- const PANO_DIST_ON = panoDebugOn("panoDist");
142
- /**
143
- * 🔬 **编辑器这一页**该不该有近白远黑图:**默认关**(皇上旨 2026-09-27 晚「把颜色换成纹理,全景的纹理」——
144
- * 灰阶诊断盖住了真投影,默认让位给全景纹理;要看诊断图 URL 显式写 `panoDist=1`,`#` 前后都认)。
145
- * ⚠️ 这是个**纯查询**(不改任何全局状态):应用层每建一份材质调一次,传进 `createMktFloorBlendShaderMaterial({ panoDist })`。
146
- * ⛔ 不要图省事改成「调一次翻全局」—— 静态字段上的材质在模块求值期就建好了,翻不到它(见上面第 ② 版事故)。
147
- * ⚠️ URL 解析只留库侧这一处(与 `panoDebugOn` 同族口径),⛔ 应用层不许再各解一遍。
148
- */
149
- export function mktPanoDistEditorOn() {
150
- return panoDebugOn("panoDist");
151
- }
152
- /**
153
- * 🔬 距离明暗图的**叠加强度**(0~1):`mix(原画面色, 灰阶, 本值)`。取 **1 = 整块换成灰阶**(本图就是纯距离图),
154
- * ⛔ 不与归属配色的 `PANO_TINT_MIX`(0.5)共用 —— 共用那版实测一面墙只有 19 级亮度差,等于看不出渐变。
155
- * ⚠️ 拼进 GLSL 时必须走 `.toFixed(1)`:TS 里的 `1` 会被原样写成整数 `1` ⇒ **GLSL ES 报 `mix` 无匹配重载、整份片元编译失败**
156
- * (09-26 踩过,画面直接空掉),着色器里任何浮点参数都得带小数点。
157
- */
158
- const PANO_DIST_MIX = 1;
159
- /**
160
- * 🔬 距离明暗诊断那一段 GLSL(**两份材质共用这一份**,与 `PANO_TINT_MIX` 同理:两处各写一遍必然分叉)。
161
- * `colorVar` = 最终颜色变量,`distExpr` = 这点到「最近那个全景点」的**水平**距离(米,与取图同口径)。
162
- * 判不到任何候选(场景里一个全景点都没有)时 `distExpr` ≥ 1e8 ⇒ 整段跳过,露本来颜色。
163
- * ⚠️ 曲线 = **线性**(皇上旨 2026-09-27「要,改成线性渐变」):旧的 `pow(…, 2.2)` 实测把 6m 处压到灰阶 0.22
164
- * ⇒ 「圆」只剩景点脚下一小块(09-26 他报「只能看到一个白色中心」的第二层原因)。线性后 6m = 0.50 中灰。
165
- * ⚠️ 代价照实报:纯白(灰阶≥235)仍然只覆盖景点脚下 D×(1−235/255) ≈ **0.078×D** 的半径(尺子 12m ⇒ 0.94m),
166
- * 铺开的是**渐变**而不是白心本身。
167
- */
168
- function panoDistGlsl(colorVar, distExpr) {
169
- return /* glsl */ `{
170
- float distNear = ${distExpr};
171
- if (distNear < 1e8) {
172
- float distGray = 1.0 - clamp(distNear / max(uPanoDistMax, 0.001), 0.0, 1.0);
173
- ${colorVar} = mix(${colorVar}, vec3(distGray), ${PANO_DIST_MIX.toFixed(1)});
174
- }
175
- }`;
176
- }
177
- /**
178
- * 🔬 距离明暗图的「求最近景点」那一段 —— **真循环**版(皇上旨 2026-09-27 晚「那就更不对了,我一开始的需求就是
179
- * 把 4 个全景点的坐标传给 shader,按最近取用」+ 看过代码后点头要这版)。
180
- * 语义与他原话逐字对齐:每个像素遍历 `uPanoPoints[0..N-1]`,只留最小水平距离。
181
- * ⚠️ 09-27 晚起取图与明暗图**同把尺**(皇上「取用距离最近的那个全景点的瓦片纹理」定版):取图那条的夹角门
182
- * 已整链删除 ⇒ 两边都是「纯水平距离最近」,圆与图永远一致。⛔ 本函数只动颜色,不动取图。
183
- * ⚠️ 动态下标只碰普通 uniform 数组(`vec4[]`),不碰 sampler ⇒ ANGLE 那条「sampler 必须常量下标」的限制管不到这里。
184
- */
185
- function panoDistLoopGlsl(colorVar, maxPano) {
186
- return /* glsl */ `{
187
- float distNear = 1e9;
188
- for (int i = 0; i < ${maxPano}; i++) {
189
- vec4 p = uPanoPoints[i];
190
- if (p.w > 0.5) distNear = min(distNear, distance(vProjPos.xz, p.xz));
191
- }
192
- if (distNear < 1e8) {
193
- float distGray = 1.0 - clamp(distNear / max(uPanoDistMax, 0.001), 0.0, 1.0);
194
- ${colorVar} = mix(${colorVar}, vec3(distGray), ${PANO_DIST_MIX.toFixed(1)});
195
- }
196
- }`;
197
- }
198
- /** 第 5~24 个景点的色板(sRGB 十六进制,两两可分辨):**球色与墙面染色的色相同源**,勿在两处各写一份。
199
- * 前 20 个各一色、互不相同;第 21 个起重复末尾那 4 色(深色底 + 中间色,任意两色不撞)。 */
200
- const PANO_TINT_SEQ_HEX = [
201
- 0xff4747, 0x47ff47, 0x477bff, 0xffff47, 0xff47ff, 0x47ffff, 0xff8c1a, 0x8c1aff, 0x1aff8c, 0xff1a8c,
202
- 0x1a8cff, 0x8cff1a, 0xff47b4, 0x47ffb4, 0xb447ff, 0xb4ff47, 0xffb447, 0x47b4ff, 0x2acfff, 0xff477b,
203
- ];
204
- /** 末尾 4 色(红/绿/蓝/黄):景点数 > 20 时循环取用 */
205
- export const MKT_PANO_TINT_HEX = [0xff4747, 0x47e05a, 0x5a8cff, 0xffe14a];
206
- /** 「第 i 个**景点**」的诊断色(按景点序号,不是槽位):0~19 各一色,>=20 循环末尾 4 色 */
207
- export function panoTintHex(i) {
208
- const n = Number.isFinite(i) && i >= 0 ? Math.floor(i) : 0;
209
- return n < PANO_TINT_SEQ_HEX.length ? PANO_TINT_SEQ_HEX[n] : MKT_PANO_TINT_HEX[n % MKT_PANO_TINT_HEX.length];
210
- }
211
- /** 景点诊断色的**线性空间**分量(three 的 Color 按 sRGB 输入即转线性)⇒ 写进着色器与球色都走这一个口径 */
212
- function panoTintLinear(i) {
213
- const c = new Color().setHex(panoTintHex(i), SRGBColorSpace);
214
- return [c.r, c.g, c.b];
215
- }
216
- /**
217
- * 写入诊断色 uniform(**仅 `?panoTint=1` 时这些 uniform 才存在**,关态本函数直接空转 ⇒ 线上零开销)。
218
- *
219
- * 为什么色相改走 uniform、不再编译期按槽位写死(皇上旨 2026-09-25「每个全景点的叠加色都是红色,我想要让他们的颜色不一样」):
220
- * 槽位不等于景点。融合材质下「第 i 槽 = 第 i 个点」(09-25 去相机化定论)时才恰好相等,
221
- * 而「仅映射选中的那一个」模式下地板永远只喂**选中那一张到 0 号槽** ⇒ 按槽写死 = 无论选第几个点都涂红,
222
- * 且那个模式下的墙/家具是另一份单全景材质(压根没有染色段)⇒ 墙一点色都没有。两处同一个错。
223
- * 故应用层在**每次往某个槽写全景时**顺手写「这个槽里躺的是第几个景点」的颜色:
224
- * - 融合材质:`uPanoTintColors[slot]`(vec3 数组)
225
- * - 单全景材质:`uPanoTint`(vec3,整面恒为选中的那一个,slot 恒 0)
226
- * 材质没有这些 uniform(关态 / 旧材质 / 误传)时静默跳过,不抛错。
227
- */
228
- export function setMktPanoTint(material, index, slot = 0) {
229
- if (!PANO_TINT_ON || !material || !material.uniforms)
230
- return;
231
- const [r, g, b] = panoTintLinear(index);
232
- const arr = material.uniforms.uPanoTintColors?.value;
233
- if (arr && arr.length > slot && arr[slot] && typeof arr[slot].set === "function") {
234
- arr[slot].set(r, g, b);
235
- return;
236
- }
237
- const v = material.uniforms.uPanoTint?.value;
238
- if (v && typeof v.set === "function")
239
- v.set(r, g, b);
240
- }
241
- /**
242
- * 写入距离明暗诊断图的**黑端距离**(米):材质上没有这根 uniform(那份材质建时就没开这张图 / 误传)时静默跳过。
243
- * ⚠️ 判据只看**材质自己有没有** `uPanoDistMax`,⛔ 不再读全局 `PANO_DIST_ON`:编辑器那三份材质是显式传 `panoDist`
244
- * 建出来的(全局开关可能仍是关 —— 发布页/预览走的就是全局),拿全局判会把编辑器该喂的尺子饿掉。
245
- * 传的是「多远算全黑」= 全局一把尺(应用层现场量),⛔ 不要按「每个槽各自」给 —— 跨面可比才读得出远近。
246
- */
247
- export function setMktPanoDistRange(material, meters) {
248
- if (!material || !material.uniforms)
249
- return;
250
- const u = material.uniforms.uPanoDistMax;
251
- if (u && Number.isFinite(meters) && meters > 0)
252
- u.value = meters;
253
- }
254
- /**
255
- * 写入距离明暗诊断图的**圆心清单**(皇上旨 2026-09-27「shader 里面有四个全景点的坐标, 每个像素点都判断距离 4 个全景点
256
- * 的距离中最小的那个值, 然后用这个值来设置颜色」):一条 = 一个全景点的世界矩阵(只取平移列),空位传 null。
257
- * ⛔ 不要拿 `weights` 当这份清单:`weights[i]` 说的是「第 i 格的**瓦片绑好了没**」(没绑好给 0 是防止拿 1×1 占位图
258
- * 刷一片素灰,09-25 线上抓到的病灶),而本数组说的是「第 i 格**有没有一个点**」。
259
- * 两者在「景点已放置、图还在下载」那段时间里就是不一样 ⇒ 明暗图按点数画圆,取图仍按绑好的那几张。
260
- * ⚠️ 材质上没有这根 uniform(那份材质建时没开这张图 / 误传)时静默跳过,与 `setMktPanoDistRange` 同口径。
261
- */
262
- export function setMktPanoPoints(material, matrices) {
263
- if (!material || !material.uniforms)
264
- return;
265
- const u = material.uniforms.uPanoPoints?.value;
266
- if (!Array.isArray(u))
267
- return;
268
- for (let i = 0; i < u.length; i++) {
269
- const m = matrices[i];
270
- const e = m?.elements;
271
- if (!e) {
272
- u[i].set(0, 0, 0, 0);
273
- continue;
274
- }
275
- const x = e[12];
276
- const y = e[13];
277
- const z = e[14];
278
- u[i].set(x, y, z, Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 1 : 0);
279
- }
280
- }
281
- /**
282
- * 🎛️ 全景上下方向 = **口径写死:等距柱状线性 v + 不交换 + 不翻转**(2026-09-22 皇上定论)。
283
- *
284
- * 皇上验收「不交换 / 不翻转」那态后口谕:「完全正确,就按照这样的方式写死在代码里面,
285
- * 后续也不用再通过参数来调整了」⇒ 三个方向 uniform(`uPanoSwapRow` 上下交换 /
286
- * `uPanoFlipTile` 上下翻转 / `uPanoVertOff` 垂直偏移)连同 `setMktPanoDims`、
287
- * store 三字段与其 setter、序列化、四处材质广播、预览 `setPanoDims`、面板两个 Select
288
- * **整条链路一并删除**(不是「留着不用」——半截链路最容易在日后被误接回去)。
289
- * ⚠️ 若真要再参数化:从 git 找回这一条完整链路,**别在半路上重新发明一遍**
290
- * —— 历次「调了没反应」有一半就是重复实现导致两处口径分叉。
291
- * ⚠️ 唯一**保留**的是 `uPanoTiled`(见下 🧩 段):它不是可调参数,而是「这几个槽装的是拼合槽纹理
292
- * 还是一张整图」的**数据形状判定**,删了就退回「把整图的一块拉伸铺满球面」(10-06 晚起是 1/2 拉伸)。
293
- *
294
- * 为什么这里值这么多注释(三轮参数化 + 一次撤除,皇上从 09-21 调到现在)—— 历次失败的**真正根因**:
295
- * ① **v 映射口径与切片口径不一致** —— 切片是把原图**线性等分**(等距柱状图:像素行 ∝ 纬度;当时 2×2,
296
- * 2026-10-06 起 8×8、当夜再改 8×4 ⇒ 「线性」这条结论与切片数无关),着色器却按**墨卡托** `log(tan(45+lat/2))` 算 v。
297
- * 赤道处恰好相等,越偏离越错:
298
- * 墙面仰角 +10° 采到原图 +5.0°(**线性口径下应恒等于 +10°**)、−45°(地板)采到 −25.2°。
299
- * **这个偏差是连续的 ⇒ 任何离散档位组合都凑不回来**,所以「四档全试遍仍不合预期」。
300
- * ⇒ 结论只有一条:**v 必须线性于纬度**(`0.5 + lat/PI`),这一条已写死,别再改回墨卡托。
301
- * ② 两档开关在**错口径下**几乎不可能调对 —— 旧墨卡托 v 把它们和口径误差叠在一起:
302
- * lat+10° 在四态下分别显示原图 +5.0° / **−85.0°** / −5.0° / **+85.0°**(后两态=三脚架与天花板正中)。
303
- * 09-22 一度把口径修正后的两档按原语义恢复上面板(皇上当时明确要「上下交换 + 上下翻转」两个开关,
304
- * 臣此前自作主张删控件换连续「垂直偏移」属越权改需求),**修好口径后皇上验收第一档即正确**,遂改判写死。
305
- * ③ **「上下翻转」在错口径下必然看起来没反应** —— 墙面只覆盖贴赤道约 9% 高度的窄带,
306
- * 而 `v = 1 - v` 是**关于赤道镜像这条窄带自身**,翻完仍落在同一条带里,
307
- * 净效果仅「±5° 对调」≈ 画面几乎不动。
308
- *
309
- * 🧩 **第二个根因(与方向参数无关,故本次写死后依然保留)**:
310
- * 默认场景与旧存档的 `textures[].tiles` **只有 1 条地址**(当年上传切片功能之后才有的字段),
311
- * 喂料处回退成「各槽同为 `url`」(10-06 晚起是 2 槽:`[url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
312
- * ① 各槽同图 ⇒ 选哪一格都一样(当时看到的「交换零效果」症状,写死后该症状无从出现);
313
- * ② 更要命的是仍按「瓦片内 UV」`localU/localV` 去采整图 ⇒ 整图被按 1/2 拉伸铺满整个球面(旧版是 1/4):
314
- * 墙面仰角 +10° 采到原图 **−70°(脚底/三脚架)** —— 这才是「全景映射在模型上的位置不太对」的另一半,
315
- * 且它是**结构性**错,比 ① 的墨卡托连续偏差更严重(臣前几轮只修了墨卡托那一半,所以皇上仍觉得不对)。
316
- * 修法 = `uPanoTiled`(单全景标量 / 融合每点数组):0 时把瓦片内 UV **还原成整图 UV**
317
- * `panoU = u`、`panoV = v`(口径写死后这就是恒等式:横向 `0.5(2u)=u` / `0.5+0.5(2u−1)=u`;
318
- * 纵向 10-06 晚起 localV 本就等于 v ⇒ 两个方向都退化成恒等式)。
319
- * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~32 条」,10-06 晚旨只支持 8×4=32 张、不足涂灰),且每个回退喂料点都要写标记,
320
- * 漏一处该对象就悄悄退回 1/2 拉伸。默认值取 1(已切片的真实上传数据行为不变)。
321
- * 护栏:`test/panoVMapping.test.js`。
322
- * 🔄 2026-09-22 追加(皇上口谕):本标记现由**用户可选的「全景纹理形态」`PanoTexMode`**(tiled/whole)与数据
323
- * 形状共同决定 —— 见下方 `PanoTexMode` / `mktPanoTexUrls` / `mktPanoTiledFlag` 三者的说明。
324
- * 喂地址与写标记**必须走同一形态参数**,这是本段唯一的铁律。
325
- *
326
- * ⛔ **血案铁律(2026-09-21 10:39,方向参数删了它也照样成立;⭐ 2026-10-06 晚起**已作废**,仅存档)**:
327
- * 旧口径(2×2 / 8×8 四槽)下 `tileRow` 必须**随取样用的 v 现算**(上半球取上排、下半球取下排),
328
- * 绝不能塌缩成「与 v 无关的单一常量」—— 那样上下半球会取到同一排(天顶与天底同一张图),画面崩坏。
329
- * ⚠️ **作废说明**:10-06 晚皇上旨「每个全景纹理由**左右 2 个 RT** 组成、原图只切 8×4=32 片」
330
- * ⇒ 竖着**不再分块**,一个槽就装下上下 180°,`tileRow` 这个变量**已整条删除**。
331
- * ⛔ 谁把它加回来 = 又变回 4 槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽;加回来前先答「第 3、4 张槽从哪来」。
332
- *
333
- * ⚠️ 历史教训(2026-09-21 线上曾报 GLSL 编译错):曾用 `(${ROW_COND} == 1) ? 1 : 0` 内联表达式,
334
- * 因 GLSL/C 里 `==` 优先级高于 `?:`,拼接后被解析为 `(v>=0.5) ? 1 : (0==1)` ⇒ int 与 bool 不匹配
335
- * ⇒ 整个片元着色器编译失败(画面全黑)。**故模板里只允许纯字面量,任何运算都在 JS 侧算完。**
336
- */
337
- const vertexShader = /* glsl */ `
338
- // common 提供 isPerspectiveMatrix 等辅助函数,logdepthbuf_vertex 依赖它
339
- #include <common>
340
- // 对数深度缓冲:ThreeBase 的 WebGLRenderer 开启了 logarithmicDepthBuffer,
341
- // 内置材质自动注入,但自定义 ShaderMaterial 不会,必须手动 include,
342
- // 否则 MktMesh 的深度与场景其他物体不一致,会被深度测试整块丢弃(表现为不显示但无编译报错)。
343
- #include <logdepthbuf_pars_vertex>
344
-
345
- // 投影空间逆矩阵:把墙顶点从「世界坐标」拉回「与 MktMesh 整体变换无关的几何局部坐标」。
346
- // ⚠️ 需求铁律(皇上旨 2026-09-19):「墙的矩阵和全景的矩阵是相对的,无论 MktMesh 整体怎么
347
- // 移动/旋转/缩放,墙上的纹理都保持不变」。textureMatrix 是世界矩阵、不随 MktMesh 动,
348
- // 故墙顶点也必须摆脱 modelMatrix 的影响 —— 乘上 inverse(matrixWorld) 正好抵消。
349
- // 默认**单位阵**(= 直接用世界坐标,行为与改造前逐字节一致),只由 MktMesh / MktDevMesh
350
- // 自己按 matrixWorld 写入;故编辑器地板 / 立方体、预览立方体这些「与 MktMesh 各自独立」
351
- // 的网格完全不受影响。
352
- uniform mat4 uProjInvMatrix;
353
-
354
- varying vec3 vProjPos;
355
- #ifdef USE_INSTANCING_COLOR
356
- varying vec3 vInstanceColor;
357
- #endif
358
-
359
- void main() {
360
- // 模型/实例局部坐标:普通 Mesh(墙/地板)无实例矩阵;InstancedMesh(家具立方体)额外乘 instanceMatrix。
361
- // 与融合着色器同理,靠 three.js 自动注入的 USE_INSTANCING 宏在一份代码里同时服务两类网格。
362
- #ifdef USE_INSTANCING
363
- mat4 im = instanceMatrix;
364
- #else
365
- mat4 im = mat4(1.0);
366
- #endif
367
- vec4 modelPos = im * vec4(position, 1.0);
368
- // 取样坐标:先按 modelMatrix 升到世界坐标,再乘 uProjInvMatrix 抵消掉 MktMesh 的整体变换。
369
- // 对 MktMesh / MktDevMesh 自身,uProjInvMatrix = inverse(matrixWorld),两者相消 => 只剩几何局部坐标,
370
- // 于是 MktMesh 整体怎么变换,取样方向都不变(纹理稳稳贴在墙上)。
371
- vProjPos = (uProjInvMatrix * modelMatrix * modelPos).xyz;
372
-
373
- #ifdef USE_INSTANCING_COLOR
374
- vInstanceColor = instanceColor;
375
- #endif
376
-
377
- gl_Position = projectionMatrix * modelViewMatrix * modelPos;
378
-
379
- #include <logdepthbuf_vertex>
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2026-08-25
4
+ * @Description: MktMesh(墨卡托模型)的自定义 ShaderMaterial。
5
+ *
6
+ * 思路(与脚手架 render3DGoods 的 panoramaMercatorMaterial 同源):
7
+ * 1. 顶点着色器:vProjPos = uProjInvMatrix * modelMatrix * im * position。
8
+ * 墙体几何在构建时已把点位写入世界坐标、MktMesh 自身保持单位变换,故无需实例矩阵;
9
+ * 纹理朝向的旋转由片元着色器乘 mat3(textureMatrix)(纹理矩阵左上 3x3 旋转分量)完成。
10
+ * ⚠️ **取样方向的两个操作数,必须都对 MktMesh 的整体变换免疫**(皇上旨 2026-09-19):
11
+ * textureMatrix 是**世界矩阵**(见下方片元 uniform 注释),属独立于 MktMesh 的外部数据,
12
+ * 本来就不随 MktMesh 移动/旋转/缩放;而墙顶点经 modelMatrix 升到世界坐标,**会**跟着变 ——
13
+ * 一个动、一个不动,方向就变了 => **墙上的纹理跟着滑动**(皇上报的正是此症)。
14
+ * 故引入 uProjInvMatrix(默认单位阵):MktMesh 自己写 inverse(matrixWorld),把墙顶点也**拉回
15
+ * 与整体变换无关的几何局部坐标**,两个操作数于是双双免疫 => 方向恒定 => 纹理稳稳贴在墙上。
16
+ * 其余网格(编辑器地板/立方体、预览立方体)保持单位阵 => 行为与改造前逐字节一致。
17
+ * ⚠️ 注意 M ≠ I 时 vProjPos(局部)与 textureMatrix(世界)并不在同一空间 —— 这是**刻意**的:
18
+ * 皇上要的是「纹理贴在墙上不动」,达成条件就是「两个操作数都不随 M 变」,而非「同处一空间」。
19
+ * 千万别"好心"再去把 textureMatrix 也乘一遍 inverse(matrixWorld),那会让纹理重新动起来。
20
+ * 2. 片元着色器:方向向量 dir = normalize(vProjPos - 纹理矩阵位置),即「以纹理矩阵的位置为投影中心」;
21
+ * 再乘纹理矩阵的旋转分量(mat3(textureMatrix)),得到纹理局部方向;
22
+ * 最后按**等距柱状**(equirectangular)把方向换算成全景纹理 UV 采样。
23
+ * ⚠️ 类名叫 MktMesh(墨卡托)是历史包袱,**取样口径不是墨卡托**:全景图是等距柱状图、
24
+ * 像素行正比于纬度,故 v 必须线性于 lat(理由见片元着色器 v 换算处的说明)。
25
+ * 3. 色彩空间 = **只有一条路**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
26
+ * 全景纹理(瓦片 / 整图 / 拼合槽 RT)一律标 `colorSpace = SRGBColorSpace` ⇒ WebGL2 按 SRGB8_ALPHA8 上传,
27
+ * `texture2D` 时**硬件已经做过一次 sRGB→linear 解码**,片元直接把采样值当线性值用、**不再手动 pow 解码**。
28
+ * 原 `uColorSpace` uniform + 那段内联 pow 分支(sRGB/Linear 两档)已整条撤除 ⇒ 两遍都解 = 二次解码压暗。
29
+ * toneMapped=false 跳过 Reinhard;末端 <colorspace_fragment> / 终处理只编码一次 ⇒ 一进一出,与源图逐字节还原。
30
+ *
31
+ * 兼容注意:为兼容 Windows / ANGLE 的 GLSL 编译器,本文件刻意不使用全局 const 常量与自定义函数,
32
+ * 所有常量均以字面量内联书写;ShaderMaterial 为 GLSL ES 1.00,用 texture2D / gl_FragColor。
33
+ */
34
+ import { Color, DataTexture, DoubleSide, Matrix4, ShaderMaterial, SRGBColorSpace, Vector3, Vector4 } from "three";
35
+ /**
36
+ * 🔬 瓦片象限诊断开关(2026-09-21)。
37
+ * 背景:皇上为「全景 2×2 瓦片上下方向」连调六轮,当时把「位置」与「块内朝向」绑在一起调
38
+ * (tileRow 随 v 动),2×2=4 种组合**四档全试过仍不合预期** ⇒ 病灶必不在开关取值内,
39
+ * 继续靠「交换 / 翻转 / 颠倒」这类语义词猜测只会无限循环,故改用**可视化归因**。
40
+ *
41
+ * ✅ **后续定论(2026-09-22 写死)**:四档轮转当时调不动,根因不在开关取值,而在**基础 v 映射口径**
42
+ * (切片线性、取样墨卡托,见下方 🎛️ 段 ①)—— 口径错配是**连续**偏差,任何离散档位都凑不回来。
43
+ * 现口径已统一为等距柱状线性,皇上验收「不交换 / 不翻转」那态即正确 ⇒ **方向参数整条撤除、写死**
44
+ * (见 🎛️ 段)。本诊断开关保留,用于日后再次核对「哪块瓦片落在哪个象限」。
45
+ *
46
+ * 用法:页面 URL 带 `?panoTileDebug=1` 时,两张槽纹理**不再采样纹理**,各自输出一个纯色 ——
47
+ * ⭐ 10-06 晚起只有左右两槽(竖着不再分块):红=左半幅(槽0) 绿=右半幅(槽1)
48
+ * (旧 4 象限版是 红=tiles[0] 绿=tiles[1] 蓝=tiles[2] 黄=tiles[3],已随四槽一并作废)
49
+ * 皇上只要说出「画面上下左右各是什么颜色」,即可一次反推出真实映射。
50
+ *
51
+ * ⚠️ 以**字面量 0.0/1.0** 内联进 GLSL:正常态整段被编译器消除,线上正常路径零开销、零行为差异。
52
+ */
53
+ /**
54
+ * 诊断开关取值:URL 的 `?name=1` —— ⚠️ **search 与 hash 两处都认**。
55
+ * 皇上的地址长这样:`https://www.3dkeshihua.com/three-base/#/mktMeshEdit?nocache=xxx`,
56
+ * 查询串挂在 `#` **后面**(那是路由的 hash,`location.search` 是空串)⇒ 只查 search 的话开关永远按不下去。
57
+ */
58
+ function panoDebugOn(name) {
59
+ try {
60
+ if (typeof location === "undefined")
61
+ return false;
62
+ const re = new RegExp(`[?&]${name}=1\\b`);
63
+ return re.test(location.search || "") || re.test(location.hash || "");
380
64
  }
381
- `;
382
- /**
383
- * 多全景地面融合着色器的顶点着色器(**地板 / 墙体 / 立方体 三者共用同一份**)。
384
- * three.js 渲染 InstancedMesh 时会自动定义 USE_INSTANCING 宏并注入 instanceMatrix / instanceColor,
385
- * 同一份代码因此同时服务两类网格:
386
- * - 普通 Mesh(墙/地板):无实例矩阵,im 退化为单位阵;
387
- * - InstancedMesh(家具立方体,InstancedChild 合批):im = instanceMatrix,各实例世界变换在其中。
388
- * 故「地板/墙体/立方体」共用这一个着色器,无需为立方体另写实例化版(消除原先的 instanced 分支)。
389
- * instanceColor(逐实例染色,选中高亮)同样用 USE_INSTANCING_COLOR 宏在顶点→片段间传递。
390
- */
391
- // ⭐ 下面这一组「切片 / 拼合槽」几何常量**必须排在 `fragmentShader` 之前**:单全景那份片元是模块级
392
- // 常量字符串,里面要用 `${RT_STITCH_SLOT_COUNT}` 声明 sampler 数组长度(TS 的块级作用域禁止先用后声明)。
393
- /** ⭐ 32 瓦片拼合(2026-10-06 晚皇上旨:一张全景切 **横 8 × 竖 4 = 32 片**,每 **4×4 = 16 片**离屏拼成
394
- * 1 张 **4096×4096 正方形**槽纹理,**左右 2 槽**组成完整全景;上一版是同日早些时候的 8×8 = 64 片 / 4 槽
395
- * 2×2 象限,再上一版是 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
396
- * 虚拟键不是真 URL,纹理由 `PanoTileStitcher` 用 shader 逐格画进 `WebGLRenderTarget` 提供,
397
- * 消费方(基类 `_ensurePanoTiles` / 编辑器 `_peekTileAt`)见前缀即走拼合器,⛔ 不得当 URL 下载。
398
- * ⚠️ 键协议 `rt:<batch>#<slot>` 与 `uPanoTiled` 这套对外接口**一字未改** —— 变的只有「每批几条地址」
399
- * 和「槽怎么分」;`slot` 的取值范围从 0..3 缩到 0..1,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
400
- * ⚠️ 旧存档不兼容(皇上 2026-10-06 拍板):存档里 64 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
401
- export const RT_STITCH_KEY_PREFIX = "rt:";
402
- /** 原图**横向**(经度方向 u)切几片(旨「横 8」) */
403
- export const RT_STITCH_COLS = 8;
404
- /** 原图**纵向**(纬度方向 v)切几片(旨「竖 4」)⇒ 竖着不再分块,一个 RT 就装下整幅的上下 180° */
405
- export const RT_STITCH_ROWS = 4;
406
- /** 一个批次(= 一个全景点)的瓦片总数 = 8 × 4 = 32;`tiles` 存档数组的硬上限 */
407
- export const RT_STITCH_TILES_PER_BATCH = RT_STITCH_COLS * RT_STITCH_ROWS;
408
- /** 一个拼合槽每边几格(旨「每个 RT 由 16 个全景瓦片组成」⇒ 4×4 = 16)*/
409
- export const RT_STITCH_SLOT_GRID = 4;
410
- /** 一个拼合槽的格数 = 4² = 16(画满 16 格 = 这一槽齐活) */
411
- export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_GRID * RT_STITCH_SLOT_GRID;
412
- /** 拼合槽个数 = **左右 2 槽**(旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 一个全景点只占 **2 个 sampler**。
413
- * ⛔ 别以为 32 片就要 32 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16 = 2 槽 × 8 景点,
414
- * 这一条是硬顶,也是「必须拼进 RT」而不是「直接喂瓦片」的根本原因。
415
- * ⭐ 分块口径:槽 0 = **左半幅**(列 0-3、行 0-3)、槽 1 = **右半幅**(列 4-7、行 0-3),
416
- * 即「只按 u 分左右,v 不再分上下」⇒ 每槽覆盖 **横向 180° × 纵向 180°**,是原图左/右半个**正方形**
417
- * ⇒ 槽纹理必须跟着变正方形(见下 `RT_STITCH_SLOT_W/H`),否则纵向被压 2 倍。 */
418
- export const RT_STITCH_SLOT_COUNT = 2;
419
- /** 单槽像素尺寸 **4096×4096 正方形**(皇上 2026-10-06 拍板保画质):2 槽 = 有效分辨率 8192×4096。
420
- * ⚠️ 为什么必须变正方形:每槽覆盖的原图区域从「横 180° × 竖 90°(2:1)」变成「横 180° × 竖 180°(1:1)」,
421
- * RT 若仍是 4096×2048 ⇒ 纵向缩放比是横向的 2 倍、**纵向分辨率腰斩**,直接踩「不降质」红线。
422
- * 每格 = 1024×1024;一片瓦片的原图边长 = 横 W/8、竖 H/4 ⇒ 横向缩放比 = (W/8)/1024 = 1.4533(11904 宽原图),
423
- * 与 64 片那一版**逐字相同** ⇒ 本次改版**不涨也不降画质**;变的是同屏全景数 4 → 8、
424
- * 单全景下载片数 64 → 32(减半)、单全景 RT 显存 128MB(不变,只是从 4 张 32MB 变 2 张 64MB)。
425
- * ⚠️ 红线(「千万不要降低质量」):这两个数与「原图切几片」无关,⛔ 不许当成省显存的旋钮去缩。 */
426
- export const RT_STITCH_SLOT_W = 4096;
427
- export const RT_STITCH_SLOT_H = 4096;
428
- export const floorVertexShader = /* glsl */ `
429
- #include <common>
430
- #include <logdepthbuf_pars_vertex>
431
-
432
- // 投影空间逆矩阵:见单全景顶点着色器处的同一段说明(皇上旨 2026-09-19)。
433
- // 默认保持单位阵(地板 / 立方体行为与改造前完全一致);编辑器墙(MktDevMesh)与发布态 MktMesh
434
- // 一样每帧写入 inverse(matrixWorld),故墙上的纹理同样不随整体变换漂移。
435
- uniform mat4 uProjInvMatrix;
436
-
437
- // 全景序号映射模式:本三角面归属的全局全景序号(来自 geometry 的 aPanoIndex;无此 attribute 的对象恒为 0,仅 mode=1 时读取)
438
- attribute float aPanoIndex;
439
- varying float vPanoIndex;
440
-
441
- varying vec3 vProjPos;
442
- // 取图「正面夹角」判据用(皇上旨 2026-09-27 深夜):几何法线,变换到与 vProjPos **同一空间**
443
- // (uProjInvMatrix * modelMatrix * instanceMatrix 的旋转部分),片元里与「点→全景」向量点积算夹角。
444
- // ⛔ normal 是 three.js 自动注入的标准 attribute(墙=computeVertexNormals 逐面法线 / 封盖=ShapeGeometry /
445
- // 地板=PlaneGeometry / 家具=BoxGeometry),此处无需自己声明。
446
- varying vec3 vWNormal;
447
- #ifdef USE_INSTANCING_COLOR
448
- varying vec3 vInstanceColor;
449
- #endif
450
-
451
- void main() {
452
- // 实例矩阵:InstancedMesh 自动定义 USE_INSTANCING 并提供 instanceMatrix;
453
- // 普通 Mesh(墙/地板)无实例矩阵,退化为单位阵 —— 一份着色器同时服务墙/地板/立方体。
454
- #ifdef USE_INSTANCING
455
- mat4 im = instanceMatrix;
456
- #else
457
- mat4 im = mat4(1.0);
458
- #endif
459
- // 模型/实例局部坐标;立方体实例的变换在 instanceMatrix,墙/地板在 modelMatrix
460
- vec4 modelPos = im * vec4(position, 1.0);
461
- vProjPos = (uProjInvMatrix * modelMatrix * modelPos).xyz;
462
- // 法线与 vProjPos 走同一条矩阵链(mat3 取旋转/缩放部分,片元里 normalize 抵消缩放)
463
- vWNormal = mat3(uProjInvMatrix * modelMatrix * im) * normal;
464
- // 全景序号映射模式:把本三角面的归属序号传给片元(geometry 无 aPanoIndex 的对象恒为 0,仅 mode=1 时读取)
465
- vPanoIndex = aPanoIndex;
466
-
467
- #ifdef USE_INSTANCING_COLOR
468
- vInstanceColor = instanceColor;
469
- #endif
470
-
471
- gl_Position = projectionMatrix * modelViewMatrix * modelPos;
472
-
473
- #include <logdepthbuf_vertex>
65
+ catch {
66
+ return false;
474
67
  }
475
- `;
476
- const fragmentShader = /* glsl */ `
477
- // 对数深度缓冲(与顶点着色器配套,保证 MktMesh 深度与场景一致)
478
- #include <logdepthbuf_pars_fragment>
479
-
480
- // 一个全景点 = 2 张**拼合槽纹理**(原图切 8×4=32 片,每 4×4=16 片拼成一张槽,10-06 晚皇上旨)。
481
- // panoramas[0..1] = [左半幅, 右半幅] 两个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
482
- // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(tileIdx 命中的分支内 sampler 下标为字面量)。
483
- uniform sampler2D panoramas[${RT_STITCH_SLOT_COUNT}];
484
- uniform mat4 textureMatrix; // 纹理世界矩阵:位置(第4列 xyz)=投影中心,旋转(左上3x3)=纹理朝向
485
- uniform float uPanoramaVisible; // 全景投射显隐:1=显示全景,0=仅显示中性素色(编辑态切换/投射显隐开关用)
486
- // 色彩空间**只有一条路**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
487
- // 原 uniform float uColorSpace; + 片元里那段 sRGB→linear 的 pow 分支已整条撤除,勿加回(原因见下方采样处)。
488
- // 全景上下方向**已写死**(等距柱状线性 v + 不交换 + 不翻转,2026-09-22 皇上定论,理由见文件头 🎛️ 段):
489
- // 曾有 uPanoSwapRow / uPanoFlipTile / uPanoVertOff 三个可调 uniform,现已整条撤除,勿加回。
490
- // ⚠️ uPanoTiled = panoramas[0..1] 装的是「拼合好的左右两张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
491
- // 由「用户选的纹理形态 PanoTexMode + 该点有没有切过片」共同决定(见下方 mktPanoTiledFlag)。
492
- // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左半幅拉伸到整个
493
- // 球面(墙面仰角 +10° 采到原图 −70° 的地板)—— 2026-09-22「全景位置不对」的第二真因,见文件头 🧩 段。
494
- uniform float uPanoTiled;${PANO_TINT_ON ? `
495
- // 🔬 归属诊断色(仅 ?panoTint=1 时存在):本材质当前投的就是「选中的那一个」全景,
496
- // 故由应用层把**那个景点**的固定色写进来(见 setMktPanoTint)⇒ 墙面/立方体上的色与球色同相。
497
- uniform vec3 uPanoTint;` : ""}${PANO_DIST_ON ? `
498
- // 🔬 距离明暗诊断(仅 ?panoDist=1 时存在):「多远算全黑」的米数,由应用层写(见 setMktPanoDistRange)
499
- uniform float uPanoDistMax;` : ""}
500
-
501
- // 取样坐标(已由顶点着色器的 uProjInvMatrix 抵消掉 MktMesh 的整体变换,与 textureMatrix 一样对变换免疫)
502
- varying vec3 vProjPos;
503
- // 实例染色(仅 InstancedMesh 带 instanceColor 时启用):默认白=全景真实色,选中黄=高亮染色
504
- #ifdef USE_INSTANCING_COLOR
505
- varying vec3 vInstanceColor;
506
- #endif
507
-
508
- void main() {
509
- // 投影中心 = 纹理矩阵的位置
510
- vec3 texPos = textureMatrix[3].xyz;
511
- // 纹理朝向 = 纹理矩阵的旋转分量(mat4 -> mat3 取左上 3x3)
512
- mat3 texRot = mat3(textureMatrix);
513
-
514
- // 从纹理位置指向该片元的方向向量(即「以纹理矩阵位置为中心」)。
515
- vec3 dir = normalize(vProjPos - texPos);
516
- dir = texRot * dir;
517
-
518
- // 经度 [-PI, PI] -> u ∈ [0,1]
519
- float lon = atan(dir.z, dir.x);
520
- float u = lon * 0.15915494309189535 + 0.5; // 1/(2*PI)
521
-
522
- // 纬度 [-PI/2, PI/2]。⚠️ **等距柱状(equirectangular)口径**:全景图的像素行正比于纬度,
523
- // 故 v 必须**线性于 lat**,±90° 落在 1/0(线性在极点不发散,因此不需要截断纬度)。
524
- // 此处曾按墨卡托 log(tan(45+lat/2)) 换算 —— 与切片口径不一致,导致墙面仰角 +10° 只采到
525
- // 原图 +5.0°、且越偏离赤道偏得越多。这才是「全景映射位置不对」的根因(2026-09-22 定位后写死)。
526
- float lat = asin(clamp(dir.y, -1.0, 1.0));
527
- float v = 0.5 + lat * 0.3183098861837907; // 1/PI
528
-
529
- // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
530
- // ⭐ 2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:**竖着不再分块** ⇒ 槽只随 **u** 走,
531
- // v 方向一槽装下整幅上下 180°(这也是槽纹理必须改成 4096² 正方形的原因,见 RT_STITCH_SLOT_W/H)。
532
- // ⛔ 别把 tileRow 加回来:加回来 = 又变成 2×2 四槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽。
533
- int tileIdx = (u >= 0.5) ? 1 : 0;
534
- // 槽内局部 UV(连续、无 fract 边界跳变):横向按左右减半;
535
- // ⛔ 纵向**不再减半**(槽就是全高)⇒ localV 恒等于 v。写成 2v / 2v-1 = 纵向分辨率白丢一半。
536
- float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
537
- float localV = v;
538
-
539
- // 🧩 uPanoTiled == 0(两槽装的是**同一张完整等距柱状图**,没有拼合槽):
540
- // 必须还原成**全图 UV** 再采样,也就是直接用 (u, v) ——
541
- // 横向还原式 (半==左 ? 0.0 : 0.5) + 0.5*localU 代入 localU 后恒等于 u;纵向 localV 本就等于 v,
542
- // **两个方向都退化成恒等式**(方向已写死:无交换、无翻转、无偏移)。
543
- // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左半幅铺满整个球面。
544
- float panoU = (uPanoTiled > 0.5) ? localU : u;
545
- float panoV = (uPanoTiled > 0.5) ? localV : v;
546
-
547
- // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
548
- if (${TILE_DEBUG_GLSL} > 0.5) {
549
- vec3 dbgCol;
550
- if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
551
- else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
552
- gl_FragColor = vec4(dbgCol, 1.0);
553
- #include <colorspace_fragment>
554
- #include <logdepthbuf_fragment>
555
- return;
556
- }
557
-
558
- // ⭐ 采样出来的**已经是线性值**:全景纹理(瓦片 / 整图 / 拼合槽 RT)一律标 colorSpace = SRGBColorSpace
559
- // ⇒ WebGL2 按 SRGB8_ALPHA8 上传,texture2D 时硬件已做过一次 sRGB→linear 解码。
560
- // 这里因此**不再手动解码**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
561
- // 末端的 <colorspace_fragment> / 终处理再编码一次 ⇒ 一进一出,屏幕上与源图逐字节还原。
562
- // ⛔ 别加回那段 pow():它在硬件已解码之上再解一遍 = 二次解码压暗(中灰 #808080 掉到 #373737、#333333 掉到 #080808)。
563
- vec3 projCol;
564
- if (tileIdx == 0) projCol = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
565
- else projCol = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
566
-
567
- // 全景投射显隐:关闭时(仅墙体/地板,回落中性素色,不投射全景);默认 1 完全显示
568
- if (uPanoramaVisible < 0.5) {
569
- projCol = vec3(0.78, 0.78, 0.80);
68
+ }
69
+ const TILE_DEBUG_GLSL = panoDebugOn("panoTileDebug") ? "1.0" : "0.0";
70
+ /**
71
+ * 🔬 归属诊断配色(皇上旨 2026-09-25「你为每个全景球设置不同的颜色, 然后把全景瓦片映射在墙面上时, 叠加该全景图的颜色,
72
+ * 这样我就能看清楚整个墙面是不是叠加了同一个颜色 —— 若是同一个颜色, 说明整面都映射了同一张瓦片, 根本没按最近取」)。
73
+ *
74
+ * 用法:页面 URL 带 `?panoTint=1`(`#` 前后都认,见 `panoDebugOn`)⇒
75
+ * 片元算完最终颜色后,**这块面按「它归哪个景点」染成该景点的固定色**(红/绿/蓝/黄;色相由景点决定,
76
+ * 强度见下面的 `PANO_TINT_MIX`),
77
+ * 同时应用层把**全景点标记**染成同一色板(图例,方便对「哪块色是哪个球」)⇒
78
+ * 这一片墙/地板是什么色,它就是那个球投的;整墙一个色 = 只投了一张,两色交界 = 归属分界线。
79
+ * ⚠️ 口径历经五版:① 「采样色 × 固定色」→ 叠在亮图上近白,皇上「完全看不到效果」;② 逐槽把 `linK` 整张换成纯色
80
+ * → 墙对了,但地板渐隐成灰的那些地方压根没全景、于是也没色 ⇒ 皇上「颜色叠加怎么没有了」;
81
+ * ③(皇上 09-25 口谕「就在最后算出来的颜色再叠加上颜色,不用想那么复杂」)⇒ 只在 **main 末尾对最终颜色**上一次色,
82
+ * 归属判据与取图那条逐字一致,但地板盘外也照样上色,一眼看得出整间房怎么分家;
83
+ * ④(皇上 09-25 近旨「地板有叠加色了,但墙上没有;每个全景点的叠加色都是红色,我想要让他们的颜色不一样」)
84
+ * ⇒ 两处同一个根因:**色相按「纹理槽」给,而槽 ≠ 景点**。①「仅映射选中的那一个」模式下墙走的是另一份
85
+ * **单全景材质**(`createMktShaderMaterial`,压根没有染色段)⇒ 墙当然没色;② 同一模式下地板只把选中的那张
86
+ * 喂进 **0 号槽** ⇒ 不管选第几个点都涂 0 号的红。修法:色相改为**运行时按景点序号**给(`uPanoTintColors[slot]`
87
+ * / 单全景材质 `uPanoTint`,两根 uniform 都只在开态存在),单全景材质末尾也补上同一条上色;色板抬成
88
+ * **前 20 个景点各一色**(见 `panoTintHex`);
89
+ * ⑤(皇上 09-26 晚「颜色叠加中, 叠加的颜色比例少一点」)⇒ ③④ 那两版把最终颜色**整块换成了色相**(只剩明暗),
90
+ * 底下那张全景照片完全读不出来 ⇒ 改成 `mix(原色, 染色, PANO_TINT_MIX)`,两份材质共用这一个常数。
91
+ *
92
+ * ⚠️ 关掉时(默认)**一行 GLSL 都不生成**(JS 侧三元,不是 uniform 分支)⇒ 线上正常路径零开销、零行为差异。
93
+ * ⚠️ 与 `panoTileDebug` 互不影响:那个看「哪一块瓦片落在哪个象限」,这个看「这一片墙归哪个全景」。
94
+ */
95
+ const PANO_TINT_ON = panoDebugOn("panoTint");
96
+ /** 诊断配色是否开启(应用层据此把全景点标记染成同色板,两边对得上) */
97
+ export const MKT_PANO_TINT_ON = PANO_TINT_ON;
98
+ /**
99
+ * 🔬 诊断色的**叠加强度**(0~1):`mix(原画面色, 染色, 本值)`。1 = 整块换成色相(09-25 初版,墙上只剩明暗,
100
+ * 照片本身看不清);皇上旨 2026-09-26 晚「叠加的颜色比例少一点」⇒ 取 **0.5**:色相仍是主色、一眼分得出归属,
101
+ * 但底下那张全景图的明暗与颜色能同时读出来。两份材质(融合 / 单全景)**共用这一个值**。
102
+ */
103
+ const PANO_TINT_MIX = 0.5;
104
+ /**
105
+ * 🔬 **距离明暗诊断图**(皇上旨 2026-09-26 晚「你计一下每个点距离最近的一个全景点的距离, 然后距离越小颜色就是白色,
106
+ * 越远颜色就是黑色」)。
107
+ *
108
+ * 用法:分两条路,**各自独立**,谁也不靠谁:
109
+ * · **编辑器(MktMeshEdit)= 建材质时显式传 `panoDist: mktPanoDistEditorOn()`** ⇒ 默认关、地址里写 `panoDist=1` 才开
110
+ * (皇上旨 2026-09-27 晚「把颜色换成纹理,全景的纹理」—— 灰阶诊断让位给真投影,诊断图按需开)。
111
+ * · **库侧(发布页 / 预览弹窗)= 不传 ⇒ 读 URL 的 `PANO_DIST_ON`,默认关** ⇒ 正常渲染路径零影响。
112
+ * 开了之后片元算完最终颜色,按**这面这点到「它实际用的那张全景」的投影中心的水平距离**涂明暗:
113
+ * **贴到球脚下 = 白,远到房间最远角 = 黑**。
114
+ * 与归属配色的分工:`panoTint` 回答「这块归哪个球」(分家线),`panoDist` 回答「这点离自己的球到底有多远」——
115
+ * 后者能把「整墙一个色」那面墙上的**连续渐变**照出来,距离判据是不是真在起作用一眼可辨(恒白/恒黑才是坏的)。
116
+ * ⚠️ 距离口径与取图那条**同一个**:水平(xz)距离、候选 = 存活槽、门优先兜底不限门 ⇒ 白色区域就是那一张覆盖到的地方。
117
+ * ⚠️ 黑端(多远算全黑)= 应用层每次喂的 `uPanoDistMax`,取**墙体上最远处到自己最近景点的那段水平距离**
118
+ * (`_panoDistRange()`,皇上那间约 12m)⇒「黑」就是这间房可能出现的最大距离,跨面可比(不是每面各自归一化)。
119
+ * ⛔ 别拿地板尺寸当尺子:默认地板盘 `FLOOR_SIZE_DEFAULT = 80`(远超房间),对角线 113m 会把整面墙的灰阶压平
120
+ * (09-26 实测横扫三个点亮度 222/223/224,等于没有渐变)。
121
+ * ⚠️ 明暗曲线 = **线性**(`1 − d/D`,皇上旨 2026-09-27「要,改成线性渐变」)。09-26 那版 `pow(…, 2.2)` 的动机是
122
+ * 「着色器里是线性色,不补感知伽马会中间偏亮」—— 代价实测更糟:6m 处灰阶只剩 0.22 ⇒ 渐变几乎全压在景点脚下,
123
+ * 他数不出圆。线性后同一点 0.50 中灰,整间房的远近一眼读得出。
124
+ * ⚠️ 强度**不沿用** `PANO_TINT_MIX` 而是自己的 `PANO_DIST_MIX = 1`(整块换成灰阶):皇上要的是「近=白、远=黑」这张**距离图**本身,
125
+ * 09-26 实测按 0.5 混 ⇒ 一面墙横扫过去只有 135→151(19 级)的差别,肉眼仍是「一片灰」;归属配色那条 0.5 的旨意针对的是
126
+ * 「色叠加把照片盖住了」,与本图无关(要看照片本来颜色就把 `?panoDist` 关掉)。
127
+ * ⚠️ 两道诊断**同时开**时本图后上色 ⇒ 明暗图盖住色相(要同时看归属与距离请分开截两张)。
128
+ * ⚠️ 开关口径历经三版:① 初版「默认关、URL `?panoDist=1` 才开」⇒ 皇上第二天地址栏里只剩 `panoTint=1`,
129
+ * 看到的又是实拍照片,误判「代码被改回去了」(09-26 实测线上包里 panoDist 一段俱在,纯为开关没跟上)。
130
+ * ② 皇上同日晚旨「继续改成 距离越近越白、越远越黑的模式」⇒ 改成**编辑器这一页默认开**:写法是在 renderer
131
+ * 模块顶层调 `mktPanoDistEditorDefaultOn()` 翻全局开关。09-26 实测线上打印当场暴露它是**半张图**:
132
+ * 「墙 / 地板 / 家具三份材质的片元着色器不是同一份」—— 立方体那份是 `static material = create…()`,
133
+ * 在**模块求值期**就建好了,永远早于任何应用层模块体里的语句 ⇒ 墙/地板黑了白了,立方体仍是实拍。
134
+ * ③ 现行(本次定论):**开关不再靠语句先后**,而是建材质时把这一份材质要不要这张图**显式说清楚**
135
+ * (融合工厂的 `panoDist` 选项)。⇒ 编辑器三处建材质点同传一个纯函数,预览/发布页不传 ⇒ 天然关。
136
+ * ④ 09-27 晚皇上旨「把颜色换成纹理,全景的纹理」⇒ 编辑器这一页翻回**默认关**(`mktPanoDistEditorOn()`
137
+ * = `panoDebugOn("panoDist")`),灰阶诊断按需 `?panoDist=1` 再看。口径机制不变,只翻默认值。
138
+ * ⛔ 不许再回到「模块顶层翻全局变量」那一路:那份 GLSL 是建材质时一次性拼出来的,谁先谁后取决于 import 顺序,
139
+ * 改一次 import 就悄悄漏一面(且 `统一性检查` 会当场报不是同一份源码)。
140
+ */
141
+ const PANO_DIST_ON = panoDebugOn("panoDist");
142
+ /**
143
+ * 🔬 **编辑器这一页**该不该有近白远黑图:**默认关**(皇上旨 2026-09-27 晚「把颜色换成纹理,全景的纹理」——
144
+ * 灰阶诊断盖住了真投影,默认让位给全景纹理;要看诊断图 URL 显式写 `panoDist=1`,`#` 前后都认)。
145
+ * ⚠️ 这是个**纯查询**(不改任何全局状态):应用层每建一份材质调一次,传进 `createMktFloorBlendShaderMaterial({ panoDist })`。
146
+ * ⛔ 不要图省事改成「调一次翻全局」—— 静态字段上的材质在模块求值期就建好了,翻不到它(见上面第 ② 版事故)。
147
+ * ⚠️ URL 解析只留库侧这一处(与 `panoDebugOn` 同族口径),⛔ 应用层不许再各解一遍。
148
+ */
149
+ export function mktPanoDistEditorOn() {
150
+ return panoDebugOn("panoDist");
151
+ }
152
+ /**
153
+ * 🔬 距离明暗图的**叠加强度**(0~1):`mix(原画面色, 灰阶, 本值)`。取 **1 = 整块换成灰阶**(本图就是纯距离图),
154
+ * ⛔ 不与归属配色的 `PANO_TINT_MIX`(0.5)共用 —— 共用那版实测一面墙只有 19 级亮度差,等于看不出渐变。
155
+ * ⚠️ 拼进 GLSL 时必须走 `.toFixed(1)`:TS 里的 `1` 会被原样写成整数 `1` ⇒ **GLSL ES 报 `mix` 无匹配重载、整份片元编译失败**
156
+ * (09-26 踩过,画面直接空掉),着色器里任何浮点参数都得带小数点。
157
+ */
158
+ const PANO_DIST_MIX = 1;
159
+ /**
160
+ * 🔬 距离明暗诊断那一段 GLSL(**两份材质共用这一份**,与 `PANO_TINT_MIX` 同理:两处各写一遍必然分叉)。
161
+ * `colorVar` = 最终颜色变量,`distExpr` = 这点到「最近那个全景点」的**水平**距离(米,与取图同口径)。
162
+ * 判不到任何候选(场景里一个全景点都没有)时 `distExpr` ≥ 1e8 ⇒ 整段跳过,露本来颜色。
163
+ * ⚠️ 曲线 = **线性**(皇上旨 2026-09-27「要,改成线性渐变」):旧的 `pow(…, 2.2)` 实测把 6m 处压到灰阶 0.22
164
+ * ⇒ 「圆」只剩景点脚下一小块(09-26 他报「只能看到一个白色中心」的第二层原因)。线性后 6m = 0.50 中灰。
165
+ * ⚠️ 代价照实报:纯白(灰阶≥235)仍然只覆盖景点脚下 D×(1−235/255) ≈ **0.078×D** 的半径(尺子 12m ⇒ 0.94m),
166
+ * 铺开的是**渐变**而不是白心本身。
167
+ */
168
+ function panoDistGlsl(colorVar, distExpr) {
169
+ return /* glsl */ `{
170
+ float distNear = ${distExpr};
171
+ if (distNear < 1e8) {
172
+ float distGray = 1.0 - clamp(distNear / max(uPanoDistMax, 0.001), 0.0, 1.0);
173
+ ${colorVar} = mix(${colorVar}, vec3(distGray), ${PANO_DIST_MIX.toFixed(1)});
174
+ }
175
+ }`;
176
+ }
177
+ /**
178
+ * 🔬 距离明暗图的「求最近景点」那一段 —— **真循环**版(皇上旨 2026-09-27 晚「那就更不对了,我一开始的需求就是
179
+ * 把 4 个全景点的坐标传给 shader,按最近取用」+ 看过代码后点头要这版)。
180
+ * 语义与他原话逐字对齐:每个像素遍历 `uPanoPoints[0..N-1]`,只留最小水平距离。
181
+ * ⚠️ 09-27 晚起取图与明暗图**同把尺**(皇上「取用距离最近的那个全景点的瓦片纹理」定版):取图那条的夹角门
182
+ * 已整链删除 ⇒ 两边都是「纯水平距离最近」,圆与图永远一致。⛔ 本函数只动颜色,不动取图。
183
+ * ⚠️ 动态下标只碰普通 uniform 数组(`vec4[]`),不碰 sampler ⇒ ANGLE 那条「sampler 必须常量下标」的限制管不到这里。
184
+ */
185
+ function panoDistLoopGlsl(colorVar, maxPano) {
186
+ return /* glsl */ `{
187
+ float distNear = 1e9;
188
+ for (int i = 0; i < ${maxPano}; i++) {
189
+ vec4 p = uPanoPoints[i];
190
+ if (p.w > 0.5) distNear = min(distNear, distance(vProjPos.xz, p.xz));
191
+ }
192
+ if (distNear < 1e8) {
193
+ float distGray = 1.0 - clamp(distNear / max(uPanoDistMax, 0.001), 0.0, 1.0);
194
+ ${colorVar} = mix(${colorVar}, vec3(distGray), ${PANO_DIST_MIX.toFixed(1)});
195
+ }
196
+ }`;
197
+ }
198
+ /** 第 5~24 个景点的色板(sRGB 十六进制,两两可分辨):**球色与墙面染色的色相同源**,勿在两处各写一份。
199
+ * 前 20 个各一色、互不相同;第 21 个起重复末尾那 4 色(深色底 + 中间色,任意两色不撞)。 */
200
+ const PANO_TINT_SEQ_HEX = [
201
+ 0xff4747, 0x47ff47, 0x477bff, 0xffff47, 0xff47ff, 0x47ffff, 0xff8c1a, 0x8c1aff, 0x1aff8c, 0xff1a8c,
202
+ 0x1a8cff, 0x8cff1a, 0xff47b4, 0x47ffb4, 0xb447ff, 0xb4ff47, 0xffb447, 0x47b4ff, 0x2acfff, 0xff477b,
203
+ ];
204
+ /** 末尾 4 色(红/绿/蓝/黄):景点数 > 20 时循环取用 */
205
+ export const MKT_PANO_TINT_HEX = [0xff4747, 0x47e05a, 0x5a8cff, 0xffe14a];
206
+ /** 「第 i 个**景点**」的诊断色(按景点序号,不是槽位):0~19 各一色,>=20 循环末尾 4 色 */
207
+ export function panoTintHex(i) {
208
+ const n = Number.isFinite(i) && i >= 0 ? Math.floor(i) : 0;
209
+ return n < PANO_TINT_SEQ_HEX.length ? PANO_TINT_SEQ_HEX[n] : MKT_PANO_TINT_HEX[n % MKT_PANO_TINT_HEX.length];
210
+ }
211
+ /** 景点诊断色的**线性空间**分量(three 的 Color 按 sRGB 输入即转线性)⇒ 写进着色器与球色都走这一个口径 */
212
+ function panoTintLinear(i) {
213
+ const c = new Color().setHex(panoTintHex(i), SRGBColorSpace);
214
+ return [c.r, c.g, c.b];
215
+ }
216
+ /**
217
+ * 写入诊断色 uniform(**仅 `?panoTint=1` 时这些 uniform 才存在**,关态本函数直接空转 ⇒ 线上零开销)。
218
+ *
219
+ * 为什么色相改走 uniform、不再编译期按槽位写死(皇上旨 2026-09-25「每个全景点的叠加色都是红色,我想要让他们的颜色不一样」):
220
+ * 槽位不等于景点。融合材质下「第 i 槽 = 第 i 个点」(09-25 去相机化定论)时才恰好相等,
221
+ * 而「仅映射选中的那一个」模式下地板永远只喂**选中那一张到 0 号槽** ⇒ 按槽写死 = 无论选第几个点都涂红,
222
+ * 且那个模式下的墙/家具是另一份单全景材质(压根没有染色段)⇒ 墙一点色都没有。两处同一个错。
223
+ * 故应用层在**每次往某个槽写全景时**顺手写「这个槽里躺的是第几个景点」的颜色:
224
+ * - 融合材质:`uPanoTintColors[slot]`(vec3 数组)
225
+ * - 单全景材质:`uPanoTint`(vec3,整面恒为选中的那一个,slot 恒 0)
226
+ * 材质没有这些 uniform(关态 / 旧材质 / 误传)时静默跳过,不抛错。
227
+ */
228
+ export function setMktPanoTint(material, index, slot = 0) {
229
+ if (!PANO_TINT_ON || !material || !material.uniforms)
230
+ return;
231
+ const [r, g, b] = panoTintLinear(index);
232
+ const arr = material.uniforms.uPanoTintColors?.value;
233
+ if (arr && arr.length > slot && arr[slot] && typeof arr[slot].set === "function") {
234
+ arr[slot].set(r, g, b);
235
+ return;
236
+ }
237
+ const v = material.uniforms.uPanoTint?.value;
238
+ if (v && typeof v.set === "function")
239
+ v.set(r, g, b);
240
+ }
241
+ /**
242
+ * 写入距离明暗诊断图的**黑端距离**(米):材质上没有这根 uniform(那份材质建时就没开这张图 / 误传)时静默跳过。
243
+ * ⚠️ 判据只看**材质自己有没有** `uPanoDistMax`,⛔ 不再读全局 `PANO_DIST_ON`:编辑器那三份材质是显式传 `panoDist`
244
+ * 建出来的(全局开关可能仍是关 —— 发布页/预览走的就是全局),拿全局判会把编辑器该喂的尺子饿掉。
245
+ * 传的是「多远算全黑」= 全局一把尺(应用层现场量),⛔ 不要按「每个槽各自」给 —— 跨面可比才读得出远近。
246
+ */
247
+ export function setMktPanoDistRange(material, meters) {
248
+ if (!material || !material.uniforms)
249
+ return;
250
+ const u = material.uniforms.uPanoDistMax;
251
+ if (u && Number.isFinite(meters) && meters > 0)
252
+ u.value = meters;
253
+ }
254
+ /**
255
+ * 写入距离明暗诊断图的**圆心清单**(皇上旨 2026-09-27「shader 里面有四个全景点的坐标, 每个像素点都判断距离 4 个全景点
256
+ * 的距离中最小的那个值, 然后用这个值来设置颜色」):一条 = 一个全景点的世界矩阵(只取平移列),空位传 null。
257
+ * ⛔ 不要拿 `weights` 当这份清单:`weights[i]` 说的是「第 i 格的**瓦片绑好了没**」(没绑好给 0 是防止拿 1×1 占位图
258
+ * 刷一片素灰,09-25 线上抓到的病灶),而本数组说的是「第 i 格**有没有一个点**」。
259
+ * 两者在「景点已放置、图还在下载」那段时间里就是不一样 ⇒ 明暗图按点数画圆,取图仍按绑好的那几张。
260
+ * ⚠️ 材质上没有这根 uniform(那份材质建时没开这张图 / 误传)时静默跳过,与 `setMktPanoDistRange` 同口径。
261
+ */
262
+ export function setMktPanoPoints(material, matrices) {
263
+ if (!material || !material.uniforms)
264
+ return;
265
+ const u = material.uniforms.uPanoPoints?.value;
266
+ if (!Array.isArray(u))
267
+ return;
268
+ for (let i = 0; i < u.length; i++) {
269
+ const m = matrices[i];
270
+ const e = m?.elements;
271
+ if (!e) {
272
+ u[i].set(0, 0, 0, 0);
273
+ continue;
570
274
  }
571
-
572
- // 实例染色:默认白=原全景色;选中黄(家具 InstancedMesh 写入 instanceColor)染黄高亮。
573
- #ifdef USE_INSTANCING_COLOR
574
- projCol *= vInstanceColor;
575
- #endif
576
- ${PANO_TINT_ON ? `
577
- // 🔬 归属诊断配色(?panoTint=1,皇上旨 2026-09-25):**单全景材质也要上色**。
578
- // 这一支是「仅映射选中的那一个」映射方式下的墙/家具材质,它没有「哪个景点投到这块面」的
579
- // 分界概念(整面恒为选中的那一张),故色相由应用层按**选中的景点序号**写进 uPanoTint(见 setMktPanoTint)
580
- // ⇒ 选 1 号球整面绿、选 2 号整面蓝,与球色对得上。
581
- // ⚠️ 色相交给标记色、明暗留原画面(乘色叠在亮图上近白,已被打回过);强度只取 PANO_TINT_MIX(皇上 09-26「比例少一点」)。
582
- {
583
- float tintLum = dot(projCol, vec3(0.2126, 0.7152, 0.0722));
584
- vec3 tintTo = uPanoTint * (0.55 + 0.45 * clamp(tintLum * 3.0, 0.0, 1.0));
585
- projCol = mix(projCol, tintTo, ${PANO_TINT_MIX});
586
- }` : ""}${PANO_DIST_ON ? `
587
- // 🔬 距离明暗诊断(?panoDist=1):这一支恒投「选中的那一个」⇒ 距离就是到它那点的水平距离。
588
- ${panoDistGlsl("projCol", "distance(vProjPos.xz, texPos.xz)")}` : ""}
589
-
590
- gl_FragColor = vec4(projCol, 1.0);
591
-
592
- // 输出颜色空间编码:渲染到线性 RT(主编辑器后处理链)时 passthrough,渲染到 sRGB 屏幕(预览弹窗)时自动编码 sRGB
593
- #include <colorspace_fragment>
594
-
595
- #include <logdepthbuf_fragment>
275
+ const x = e[12];
276
+ const y = e[13];
277
+ const z = e[14];
278
+ u[i].set(x, y, z, Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 1 : 0);
596
279
  }
597
- `;
598
- /** 1×1 白色占位纹理:panorama 在尚未激活任何纹理前使用,避免空纹理采样报错 */
599
- export function createPlaceholderTexture() {
600
- const tex = new DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
601
- tex.needsUpdate = true;
602
- return tex;
603
- }
604
- /** 形态合法值(序列化 / 反序列化单一来源,勿在别处另立字面量) */
605
- export function isPanoTexMode(v) {
606
- return v === "tiled" || v === "whole";
607
- }
608
- /** 缺失格涂灰(09-29 晚旨「不足张数缺的地方涂灰,一眼看出数据不全」)。
609
- * 取 #767676 ≈ mapping 模式 `uFaceMissingColor`(线性 0.18)的 sRGB 值,两处观感一致。
610
- * 10-06 起拼合器只有一份(canvas 那一份奉旨删除)⇒ 常量搬到本文件当唯一来源。 */
611
- export const MISSING_TILE_GRAY = "#767676";
612
- /** 接缝余量(**原图像素**,每片向四边各多切这么多;贴原图边那侧裁到剩多少算多少)。
613
- *
614
- * 10-06 槽内改成 4×4 = 16 格 ⇒ 一张槽内部的接缝从旧版的 1 条十字变成 **3 横 3 竖**,这一条是随之而来的画质护栏:
615
- * ① 上传时每片**向四边多切 M 像素**(`rtStitchTileRects`);
616
- * ② 拼合时**只采中间那块**(`rtStitchTileInset` 算出的 UV 内缩窗口),多切的那一圈**不画进格子**。
617
- *
618
- * 为什么这样就没了细亮/细暗线:缝的病根是「双线性 / mipmap 在瓦片边界外拿不到邻居像素,只能复制自己的
619
- * 边缘像素」(就是 `PanoTexMode` 文件头 ② 那条)。多带的 M 像素就是**真的邻居像素**,插值不再靠复制。
620
- *
621
- * M 取 8 够不够:一格 1024×1024px、一片原图 1488×1488px(横 W/8、竖 H/4)⇒ 两个方向都是 1.45× 缩小,
622
- * 三线性 mip 的 footprint 半径最大约
623
- * `2 × 1.45 ≈ 3` texel ⇒ 8px 有 ≈2.7 倍余量;再小一档(4px)也够用,取 8 是给「原图比 8192 窄 ⇒ 格子
624
- * 反而在放大」那类点留边界。**代价**:每片文件比「正好 1/8」宽 16px、高 16px(≈ +4% 面积),⛔ 不是降质。 */
625
- export const RT_STITCH_SEAM_MARGIN_PX = 8;
626
- /** 32 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
627
- * 09-29 晚旨「不足张数缺的地方涂灰」后批次里允许有空串占位 ⇒ 取首个非空,防首张缺失时键为空串互撞) */
628
- export function rtStitchBatchOf(tiles) {
629
- return tiles.find((u) => u) || "";
630
- }
631
- /** 某一拼合槽的虚拟键:`rt:<batch>#<slot>` */
632
- export function rtStitchKey(batch, slot) {
633
- return `${RT_STITCH_KEY_PREFIX}${batch}#${slot}`;
634
- }
635
- /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 片 / 64 片 / 现行 32 片**同一个前缀**,键本身不含片数信息 */
636
- export function isRtStitchKey(key) {
637
- return key.startsWith(RT_STITCH_KEY_PREFIX);
638
- }
639
- /** 解析虚拟键 → { batch, slot };非 rt 键返回 null */
640
- export function rtStitchKeyParts(key) {
641
- if (!isRtStitchKey(key))
642
- return null;
643
- const body = key.slice(RT_STITCH_KEY_PREFIX.length);
644
- const i = body.lastIndexOf("#");
645
- if (i < 0)
646
- return null;
647
- return { batch: body.slice(0, i), slot: Number(body.slice(i + 1)) };
648
- }
649
- /** 槽 slot 用哪 16 张瓦片(原图 **8 列 × 4 行 行优先**下标 `r*8+c`;**槽内格序 = 4×4 行优先、图像左上起**):
650
- * 槽 0 = 图像**左半幅**(行 0-3 × 列 0-3)→ `[0,1,2,3, 8,9,10,11, 16,17,18,19, 24,25,26,27]`;
651
- * 槽 1 = **右半幅**(行 0-3 × 列 4-7)→ `[4,5,6,7, 12,13,14,15, 20,21,22,23, 28,29,30,31]`。
652
- * ⭐ 竖着**不再分块**(皇上旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 两个槽**都是全高**,只按 u 分左右,
653
- * 故槽纹理也必须跟着变正方形(见 `RT_STITCH_SLOT_W/H` 那条注释,⛔ 别只改这里漏了那边)。
654
- * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 32 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
655
- * 对着手写死的字面量逐个数断言 —— 改了网格常数而护栏没跟着改,当场红。 */
656
- export const RT_STITCH_SLOT_TILES = (() => {
657
- // 横向几个槽(COLS/SLOT_GRID = 8/4 = 2);纵向恒 1 个槽(ROWS/SLOT_GRID = 4/4 = 1 ⇒ sr 恒 0 = 全高)
658
- const slotCols = RT_STITCH_COLS / RT_STITCH_SLOT_GRID;
659
- const slots = [];
660
- for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
661
- const sr = Math.floor(slot / slotCols) * RT_STITCH_SLOT_GRID;
662
- const sc = (slot % slotCols) * RT_STITCH_SLOT_GRID;
663
- const ids = [];
664
- for (let r = 0; r < RT_STITCH_SLOT_GRID; r++) {
665
- for (let c = 0; c < RT_STITCH_SLOT_GRID; c++)
666
- ids.push((sr + r) * RT_STITCH_COLS + (sc + c));
667
- }
668
- slots.push(ids);
669
- }
670
- return slots;
671
- })();
672
- /** 槽内第 cell 格在**图像方向**的位置 `[列, 行]`(0 起,与 `RT_STITCH_SLOT_TILES` 的格序同一口径)。
673
- * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。 */
674
- export function rtStitchCellPos(cell) {
675
- return [cell % RT_STITCH_SLOT_GRID, Math.floor(cell / RT_STITCH_SLOT_GRID)];
676
- }
677
- /** 网格取整:第 `i` 条分界线在原图 `n` 像素上的位置(`round` ⇒ 余数摊到各格,
678
- * ⛔ 不必再像旧上传代码那样特判「末行末列吸收余数」)。
679
- * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):10-06 晚起两个方向
680
- * **不再相等**(8×4),必须显式传,⛔ 不许再拿一个「每边片数」两边共用 —— 竖着按 8 切 = 纵向切错。 */
681
- function gridLine(i, n, steps) {
682
- return Math.round((i * n) / steps);
683
- }
684
- /** 32 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*8+c` 与 `RT_STITCH_SLOT_TILES` 同源;
685
- * ⭐ 10-06 晚旨「横 8 × 竖 4 = 32」:`r` 走 `RT_STITCH_ROWS`(4)、`c` 走 `RT_STITCH_COLS`(8),不再相等)。
686
- *
687
- * ⭐ **上传侧(`panoramaUpload`)与拼合侧(`PanoTileStitcher`)必须共用这一份几何**:
688
- * 上传多切多少、拼合就内缩多少;两边各算一次就是「缝没治掉、反而把画面挤歪」。
689
- * 拼合侧拿不到原图尺寸(它只有 32 个 URL 和 32 张已解码纹理),所以它用 `rtStitchTileInset`
690
- * —— 那条只需「下标 + 该瓦片自己的像素尺寸」,两者等价的前提是「多切量 = 常数 M、只有贴边那几片被裁」,
691
- * 而「贴不贴边」纯由下标决定 ⇒ 护栏 BE 组用同一批 (W,H) 把两条算路对表钉死。 */
692
- export function rtStitchTileRects(w, h) {
693
- const out = [];
694
- for (let r = 0; r < RT_STITCH_ROWS; r++) {
695
- for (let c = 0; c < RT_STITCH_COLS; c++) {
696
- const x0 = gridLine(c, w, RT_STITCH_COLS);
697
- const x1 = gridLine(c + 1, w, RT_STITCH_COLS);
698
- const y0 = gridLine(r, h, RT_STITCH_ROWS);
699
- const y1 = gridLine(r + 1, h, RT_STITCH_ROWS);
700
- const ml = Math.min(RT_STITCH_SEAM_MARGIN_PX, x0);
701
- const mr = Math.min(RT_STITCH_SEAM_MARGIN_PX, w - x1);
702
- const mt = Math.min(RT_STITCH_SEAM_MARGIN_PX, y0);
703
- const mb = Math.min(RT_STITCH_SEAM_MARGIN_PX, h - y1);
704
- out.push({ sx: x0 - ml, sy: y0 - mt, sw: x1 + mr - (x0 - ml), sh: y1 + mb - (y0 - mt), ml, mt, mr, mb });
705
- }
280
+ }
281
+ /**
282
+ * 🎛️ 全景上下方向 = **口径写死:等距柱状线性 v + 不交换 + 不翻转**(2026-09-22 皇上定论)。
283
+ *
284
+ * 皇上验收「不交换 / 不翻转」那态后口谕:「完全正确,就按照这样的方式写死在代码里面,
285
+ * 后续也不用再通过参数来调整了」⇒ 三个方向 uniform(`uPanoSwapRow` 上下交换 /
286
+ * `uPanoFlipTile` 上下翻转 / `uPanoVertOff` 垂直偏移)连同 `setMktPanoDims`、
287
+ * store 三字段与其 setter、序列化、四处材质广播、预览 `setPanoDims`、面板两个 Select
288
+ * **整条链路一并删除**(不是「留着不用」——半截链路最容易在日后被误接回去)。
289
+ * ⚠️ 若真要再参数化:从 git 找回这一条完整链路,**别在半路上重新发明一遍**
290
+ * —— 历次「调了没反应」有一半就是重复实现导致两处口径分叉。
291
+ * ⚠️ 唯一**保留**的是 `uPanoTiled`(见下 🧩 段):它不是可调参数,而是「这几个槽装的是拼合槽纹理
292
+ * 还是一张整图」的**数据形状判定**,删了就退回「把整图的一块拉伸铺满球面」(10-06 晚起是 1/2 拉伸)。
293
+ *
294
+ * 为什么这里值这么多注释(三轮参数化 + 一次撤除,皇上从 09-21 调到现在)—— 历次失败的**真正根因**:
295
+ * ① **v 映射口径与切片口径不一致** —— 切片是把原图**线性等分**(等距柱状图:像素行 ∝ 纬度;当时 2×2,
296
+ * 2026-10-06 起 8×8、当夜再改 8×4 ⇒ 「线性」这条结论与切片数无关),着色器却按**墨卡托** `log(tan(45+lat/2))` 算 v。
297
+ * 赤道处恰好相等,越偏离越错:
298
+ * 墙面仰角 +10° 采到原图 +5.0°(**线性口径下应恒等于 +10°**)、−45°(地板)采到 −25.2°。
299
+ * **这个偏差是连续的 ⇒ 任何离散档位组合都凑不回来**,所以「四档全试遍仍不合预期」。
300
+ * ⇒ 结论只有一条:**v 必须线性于纬度**(`0.5 + lat/PI`),这一条已写死,别再改回墨卡托。
301
+ * ② 两档开关在**错口径下**几乎不可能调对 —— 旧墨卡托 v 把它们和口径误差叠在一起:
302
+ * lat+10° 在四态下分别显示原图 +5.0° / **−85.0°** / −5.0° / **+85.0°**(后两态=三脚架与天花板正中)。
303
+ * 09-22 一度把口径修正后的两档按原语义恢复上面板(皇上当时明确要「上下交换 + 上下翻转」两个开关,
304
+ * 臣此前自作主张删控件换连续「垂直偏移」属越权改需求),**修好口径后皇上验收第一档即正确**,遂改判写死。
305
+ * ③ **「上下翻转」在错口径下必然看起来没反应** —— 墙面只覆盖贴赤道约 9% 高度的窄带,
306
+ * 而 `v = 1 - v` 是**关于赤道镜像这条窄带自身**,翻完仍落在同一条带里,
307
+ * 净效果仅「±5° 对调」≈ 画面几乎不动。
308
+ *
309
+ * 🧩 **第二个根因(与方向参数无关,故本次写死后依然保留)**:
310
+ * 默认场景与旧存档的 `textures[].tiles` **只有 1 条地址**(当年上传切片功能之后才有的字段),
311
+ * 喂料处回退成「各槽同为 `url`」(10-06 晚起是 2 槽:`[url, url]`)⇒ **各 sampler 装的是同一张完整等距柱状图**。于是:
312
+ * ① 各槽同图 ⇒ 选哪一格都一样(当时看到的「交换零效果」症状,写死后该症状无从出现);
313
+ * ② 更要命的是仍按「瓦片内 UV」`localU/localV` 去采整图 ⇒ 整图被按 1/2 拉伸铺满整个球面(旧版是 1/4):
314
+ * 墙面仰角 +10° 采到原图 **−70°(脚底/三脚架)** —— 这才是「全景映射在模型上的位置不太对」的另一半,
315
+ * 且它是**结构性**错,比 ① 的墨卡托连续偏差更严重(臣前几轮只修了墨卡托那一半,所以皇上仍觉得不对)。
316
+ * 修法 = `uPanoTiled`(单全景标量 / 融合每点数组):0 时把瓦片内 UV **还原成整图 UV**
317
+ * `panoU = u`、`panoV = v`(口径写死后这就是恒等式:横向 `0.5(2u)=u` / `0.5+0.5(2u−1)=u`;
318
+ * 纵向 10-06 晚起 localV 本就等于 v ⇒ 两个方向都退化成恒等式)。
319
+ * ⚠️ 判据与回退式**必须同源**(`mktPanoTiledFlag` = 「tiled 形态 且 tiles 有任意 1~32 条」,10-06 晚旨只支持 8×4=32 张、不足涂灰),且每个回退喂料点都要写标记,
320
+ * 漏一处该对象就悄悄退回 1/2 拉伸。默认值取 1(已切片的真实上传数据行为不变)。
321
+ * 护栏:`test/panoVMapping.test.js`。
322
+ * 🔄 2026-09-22 追加(皇上口谕):本标记现由**用户可选的「全景纹理形态」`PanoTexMode`**(tiled/whole)与数据
323
+ * 形状共同决定 —— 见下方 `PanoTexMode` / `mktPanoTexUrls` / `mktPanoTiledFlag` 三者的说明。
324
+ * 喂地址与写标记**必须走同一形态参数**,这是本段唯一的铁律。
325
+ *
326
+ * ⛔ **血案铁律(2026-09-21 10:39,方向参数删了它也照样成立;⭐ 2026-10-06 晚起**已作废**,仅存档)**:
327
+ * 旧口径(2×2 / 8×8 四槽)下 `tileRow` 必须**随取样用的 v 现算**(上半球取上排、下半球取下排),
328
+ * 绝不能塌缩成「与 v 无关的单一常量」—— 那样上下半球会取到同一排(天顶与天底同一张图),画面崩坏。
329
+ * ⚠️ **作废说明**:10-06 晚皇上旨「每个全景纹理由**左右 2 个 RT** 组成、原图只切 8×4=32 片」
330
+ * ⇒ 竖着**不再分块**,一个槽就装下上下 180°,`tileRow` 这个变量**已整条删除**。
331
+ * ⛔ 谁把它加回来 = 又变回 4 槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽;加回来前先答「第 3、4 张槽从哪来」。
332
+ *
333
+ * ⚠️ 历史教训(2026-09-21 线上曾报 GLSL 编译错):曾用 `(${ROW_COND} == 1) ? 1 : 0` 内联表达式,
334
+ * 因 GLSL/C 里 `==` 优先级高于 `?:`,拼接后被解析为 `(v>=0.5) ? 1 : (0==1)` ⇒ int 与 bool 不匹配
335
+ * ⇒ 整个片元着色器编译失败(画面全黑)。**故模板里只允许纯字面量,任何运算都在 JS 侧算完。**
336
+ */
337
+ const vertexShader = /* glsl */ `
338
+ // common 提供 isPerspectiveMatrix 等辅助函数,logdepthbuf_vertex 依赖它
339
+ #include <common>
340
+ // 对数深度缓冲:ThreeBase 的 WebGLRenderer 开启了 logarithmicDepthBuffer,
341
+ // 内置材质自动注入,但自定义 ShaderMaterial 不会,必须手动 include,
342
+ // 否则 MktMesh 的深度与场景其他物体不一致,会被深度测试整块丢弃(表现为不显示但无编译报错)。
343
+ #include <logdepthbuf_pars_vertex>
344
+
345
+ // 投影空间逆矩阵:把墙顶点从「世界坐标」拉回「与 MktMesh 整体变换无关的几何局部坐标」。
346
+ // ⚠️ 需求铁律(皇上旨 2026-09-19):「墙的矩阵和全景的矩阵是相对的,无论 MktMesh 整体怎么
347
+ // 移动/旋转/缩放,墙上的纹理都保持不变」。textureMatrix 是世界矩阵、不随 MktMesh 动,
348
+ // 故墙顶点也必须摆脱 modelMatrix 的影响 —— 乘上 inverse(matrixWorld) 正好抵消。
349
+ // 默认**单位阵**(= 直接用世界坐标,行为与改造前逐字节一致),只由 MktMesh / MktDevMesh
350
+ // 自己按 matrixWorld 写入;故编辑器地板 / 立方体、预览立方体这些「与 MktMesh 各自独立」
351
+ // 的网格完全不受影响。
352
+ uniform mat4 uProjInvMatrix;
353
+
354
+ varying vec3 vProjPos;
355
+ #ifdef USE_INSTANCING_COLOR
356
+ varying vec3 vInstanceColor;
357
+ #endif
358
+
359
+ void main() {
360
+ // 模型/实例局部坐标:普通 Mesh(墙/地板)无实例矩阵;InstancedMesh(家具立方体)额外乘 instanceMatrix。
361
+ // 与融合着色器同理,靠 three.js 自动注入的 USE_INSTANCING 宏在一份代码里同时服务两类网格。
362
+ #ifdef USE_INSTANCING
363
+ mat4 im = instanceMatrix;
364
+ #else
365
+ mat4 im = mat4(1.0);
366
+ #endif
367
+ vec4 modelPos = im * vec4(position, 1.0);
368
+ // 取样坐标:先按 modelMatrix 升到世界坐标,再乘 uProjInvMatrix 抵消掉 MktMesh 的整体变换。
369
+ // 对 MktMesh / MktDevMesh 自身,uProjInvMatrix = inverse(matrixWorld),两者相消 => 只剩几何局部坐标,
370
+ // 于是 MktMesh 整体怎么变换,取样方向都不变(纹理稳稳贴在墙上)。
371
+ vProjPos = (uProjInvMatrix * modelMatrix * modelPos).xyz;
372
+
373
+ #ifdef USE_INSTANCING_COLOR
374
+ vInstanceColor = instanceColor;
375
+ #endif
376
+
377
+ gl_Position = projectionMatrix * modelViewMatrix * modelPos;
378
+
379
+ #include <logdepthbuf_vertex>
706
380
  }
707
- return out;
708
- }
709
- /** 这片瓦片有没有贴原图边界(8×4 ⇒ 32 片里四周那 20 片至少贴一条边;贴边的**那一侧**多切量被裁成 0)。
710
- * 拼合侧靠它 + 瓦片自己的像素尺寸还原内缩量 ⇒ 不必知道原图多大。
711
- * ⚠️ 下标拆分必须按 **列数 COLS=8**(`i = r*8+c`),⛔ 不是 ROWS。 */
712
- export function rtStitchTileEdgeClip(i) {
713
- const r = Math.floor(i / RT_STITCH_COLS);
714
- const c = i % RT_STITCH_COLS;
715
- return {
716
- left: c === 0,
717
- right: c === RT_STITCH_COLS - 1,
718
- top: r === 0,
719
- bottom: r === RT_STITCH_ROWS - 1,
720
- };
721
- }
722
- /** 拼合时的采样窗口 = 该瓦片纹理的 UV 里「**只留基础格、丢掉多切那一圈**」的那一块,
723
- * 返回 `[u0, v0, u1, v1]`(片元里 `mix(vec2(u0,v0), vec2(u1,v1), vUv)`)。
724
- *
725
- * `tw/th` = 该瓦片纹理的**实际**像素宽高。内缩比例 = `M / sw`(多切那 M 像素占整片的比例),
726
- * 与「文件是不是正好等于标称切片区」无关 ⇒ 编码差 1px 也不会露出邻居的格子。
727
- * 贴原图边那一侧内缩 0:那里压根没多切,且那条边界就是全景自己的边(`ClampToEdge` 照旧)。
728
- *
729
- * ⚠️ v 方向口径(与 `panoBitmapTexture` 绑死):瓦片走 `imageOrientation:"flipY"` 预翻 + `texture.flipY=false`
730
- * ⇒ 纹理 **v=0 = 图像下边**,所以图像顶部余量 `mt` 落在 `v1`、底部 `mb` 落在 `v0`。搞反 = 上下颠倒。 */
731
- export function rtStitchTileInset(i, tw, th) {
732
- const e = rtStitchTileEdgeClip(i);
733
- const m = RT_STITCH_SEAM_MARGIN_PX;
734
- const u0 = e.left ? 0 : Math.min(0.5, m / tw);
735
- const u1 = e.right ? 1 : 1 - Math.min(0.5, m / tw);
736
- const v0 = e.bottom ? 0 : Math.min(0.5, m / th);
737
- const v1 = e.top ? 1 : 1 - Math.min(0.5, m / th);
738
- return [u0, v0, u1, v1];
739
- }
740
- /**
741
- * 「这 `RT_STITCH_SLOT_COUNT`(2) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
742
- * 只改标记不改地址(或反之)= 用整图 UV 去采一张瓦片 / 用瓦片内 UV 去采整图,画面立刻错乱。
743
- * 非 tiled 态(或该点压根没切过片)一律回退成「各槽同为一张整图」。
744
- * ⭐ 2026-10-06 晚旨「只支持 32 张(8×4)」:只要有瓦片(1~32 条)一律走拼合器(rt: 虚拟键),
745
- * 不足 32 张的位置由拼合器**涂灰**(一眼看出数据不全);8×8=64 张与 4×4=16 张那两版**都不做兼容**
746
- * (皇上「无须兼容正常使用,有异常我会重传」⇒ 旧存档的 64 条地址超张数被判废、重传即可)。
747
- * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 2 个 rtStitchKey —— 槽数再变会漏改)。
748
- */
749
- export function mktPanoTexUrls(url, tiles, mode = "tiled") {
750
- if (mode === "tiled" && tiles && tiles.length > 0) {
751
- const batch = rtStitchBatchOf(tiles);
752
- return Array.from({ length: RT_STITCH_SLOT_COUNT }, (_, s) => rtStitchKey(batch, s));
381
+ `;
382
+ /**
383
+ * 多全景地面融合着色器的顶点着色器(**地板 / 墙体 / 立方体 三者共用同一份**)。
384
+ * three.js 渲染 InstancedMesh 时会自动定义 USE_INSTANCING 宏并注入 instanceMatrix / instanceColor,
385
+ * 同一份代码因此同时服务两类网格:
386
+ * - 普通 Mesh(墙/地板):无实例矩阵,im 退化为单位阵;
387
+ * - InstancedMesh(家具立方体,InstancedChild 合批):im = instanceMatrix,各实例世界变换在其中。
388
+ * 故「地板/墙体/立方体」共用这一个着色器,无需为立方体另写实例化版(消除原先的 instanced 分支)。
389
+ * instanceColor(逐实例染色,选中高亮)同样用 USE_INSTANCING_COLOR 宏在顶点→片段间传递。
390
+ */
391
+ // ⭐ 下面这一组「切片 / 拼合槽」几何常量**必须排在 `fragmentShader` 之前**:单全景那份片元是模块级
392
+ // 常量字符串,里面要用 `${RT_STITCH_SLOT_COUNT}` 声明 sampler 数组长度(TS 的块级作用域禁止先用后声明)。
393
+ /** ⭐ 32 瓦片拼合(2026-10-06 晚皇上旨:一张全景切 **横 8 × 竖 4 = 32 片**,每 **4×4 = 16 片**离屏拼成
394
+ * 1 张 **4096×4096 正方形**槽纹理,**左右 2 槽**组成完整全景;上一版是同日早些时候的 8×8 = 64 片 / 4 槽
395
+ * 2×2 象限,再上一版是 09-29 旨的 4×4 = 16 片)的「槽虚拟地址」前缀。
396
+ * 虚拟键不是真 URL,纹理由 `PanoTileStitcher` 用 shader 逐格画进 `WebGLRenderTarget` 提供,
397
+ * 消费方(基类 `_ensurePanoTiles` / 编辑器 `_peekTileAt`)见前缀即走拼合器,⛔ 不得当 URL 下载。
398
+ * ⚠️ 键协议 `rt:<batch>#<slot>` 与 `uPanoTiled` 这套对外接口**一字未改** —— 变的只有「每批几条地址」
399
+ * 和「槽怎么分」;`slot` 的取值范围从 0..3 缩到 0..1,消费方一律用 `RT_STITCH_SLOT_COUNT` 取,不许写死。
400
+ * ⚠️ 旧存档不兼容(皇上 2026-10-06 拍板):存档里 64 条瓦片地址会因张数超限被判废 ⇒ **老项目需重新上传**。 */
401
+ export const RT_STITCH_KEY_PREFIX = "rt:";
402
+ /** 原图**横向**(经度方向 u)切几片(旨「横 8」) */
403
+ export const RT_STITCH_COLS = 8;
404
+ /** 原图**纵向**(纬度方向 v)切几片(旨「竖 4」)⇒ 竖着不再分块,一个 RT 就装下整幅的上下 180° */
405
+ export const RT_STITCH_ROWS = 4;
406
+ /** 一个批次(= 一个全景点)的瓦片总数 = 8 × 4 = 32;`tiles` 存档数组的硬上限 */
407
+ export const RT_STITCH_TILES_PER_BATCH = RT_STITCH_COLS * RT_STITCH_ROWS;
408
+ /** 一个拼合槽每边几格(旨「每个 RT 由 16 个全景瓦片组成」⇒ 4×4 = 16)*/
409
+ export const RT_STITCH_SLOT_GRID = 4;
410
+ /** 一个拼合槽的格数 = 4² = 16(画满 16 格 = 这一槽齐活) */
411
+ export const RT_STITCH_CELLS_PER_SLOT = RT_STITCH_SLOT_GRID * RT_STITCH_SLOT_GRID;
412
+ /** 拼合槽个数 = **左右 2 槽**(旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 一个全景点只占 **2 个 sampler**。
413
+ * ⛔ 别以为 32 片就要 32 个 sampler:皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16 = 2 槽 × 8 景点,
414
+ * 这一条是硬顶,也是「必须拼进 RT」而不是「直接喂瓦片」的根本原因。
415
+ * ⭐ 分块口径:槽 0 = **左半幅**(列 0-3、行 0-3)、槽 1 = **右半幅**(列 4-7、行 0-3),
416
+ * 即「只按 u 分左右,v 不再分上下」⇒ 每槽覆盖 **横向 180° × 纵向 180°**,是原图左/右半个**正方形**
417
+ * ⇒ 槽纹理必须跟着变正方形(见下 `RT_STITCH_SLOT_W/H`),否则纵向被压 2 倍。 */
418
+ export const RT_STITCH_SLOT_COUNT = 2;
419
+ /** 单槽像素尺寸 **4096×4096 正方形**(皇上 2026-10-06 拍板保画质):2 槽 = 有效分辨率 8192×4096。
420
+ * ⚠️ 为什么必须变正方形:每槽覆盖的原图区域从「横 180° × 竖 90°(2:1)」变成「横 180° × 竖 180°(1:1)」,
421
+ * RT 若仍是 4096×2048 ⇒ 纵向缩放比是横向的 2 倍、**纵向分辨率腰斩**,直接踩「不降质」红线。
422
+ * 每格 = 1024×1024;一片瓦片的原图边长 = 横 W/8、竖 H/4 ⇒ 横向缩放比 = (W/8)/1024 = 1.4533(11904 宽原图),
423
+ * 与 64 片那一版**逐字相同** ⇒ 本次改版**不涨也不降画质**;变的是同屏全景数 4 → 8、
424
+ * 单全景下载片数 64 → 32(减半)、单全景 RT 显存 128MB(不变,只是从 4 张 32MB 变 2 张 64MB)。
425
+ * ⚠️ 红线(「千万不要降低质量」):这两个数与「原图切几片」无关,⛔ 不许当成省显存的旋钮去缩。 */
426
+ export const RT_STITCH_SLOT_W = 4096;
427
+ export const RT_STITCH_SLOT_H = 4096;
428
+ export const floorVertexShader = /* glsl */ `
429
+ #include <common>
430
+ #include <logdepthbuf_pars_vertex>
431
+
432
+ // 投影空间逆矩阵:见单全景顶点着色器处的同一段说明(皇上旨 2026-09-19)。
433
+ // 默认保持单位阵(地板 / 立方体行为与改造前完全一致);编辑器墙(MktDevMesh)与发布态 MktMesh
434
+ // 一样每帧写入 inverse(matrixWorld),故墙上的纹理同样不随整体变换漂移。
435
+ uniform mat4 uProjInvMatrix;
436
+
437
+ // 全景序号映射模式:本三角面归属的全局全景序号(来自 geometry 的 aPanoIndex;无此 attribute 的对象恒为 0,仅 mode=1 时读取)
438
+ attribute float aPanoIndex;
439
+ varying float vPanoIndex;
440
+
441
+ varying vec3 vProjPos;
442
+ // 取图「正面夹角」判据用(皇上旨 2026-09-27 深夜):几何法线,变换到与 vProjPos **同一空间**
443
+ // (uProjInvMatrix * modelMatrix * instanceMatrix 的旋转部分),片元里与「点→全景」向量点积算夹角。
444
+ // ⛔ normal 是 three.js 自动注入的标准 attribute(墙=computeVertexNormals 逐面法线 / 封盖=ShapeGeometry /
445
+ // 地板=PlaneGeometry / 家具=BoxGeometry),此处无需自己声明。
446
+ varying vec3 vWNormal;
447
+ #ifdef USE_INSTANCING_COLOR
448
+ varying vec3 vInstanceColor;
449
+ #endif
450
+
451
+ void main() {
452
+ // 实例矩阵:InstancedMesh 自动定义 USE_INSTANCING 并提供 instanceMatrix;
453
+ // 普通 Mesh(墙/地板)无实例矩阵,退化为单位阵 —— 一份着色器同时服务墙/地板/立方体。
454
+ #ifdef USE_INSTANCING
455
+ mat4 im = instanceMatrix;
456
+ #else
457
+ mat4 im = mat4(1.0);
458
+ #endif
459
+ // 模型/实例局部坐标;立方体实例的变换在 instanceMatrix,墙/地板在 modelMatrix
460
+ vec4 modelPos = im * vec4(position, 1.0);
461
+ vProjPos = (uProjInvMatrix * modelMatrix * modelPos).xyz;
462
+ // 法线与 vProjPos 走同一条矩阵链(mat3 取旋转/缩放部分,片元里 normalize 抵消缩放)
463
+ vWNormal = mat3(uProjInvMatrix * modelMatrix * im) * normal;
464
+ // 全景序号映射模式:把本三角面的归属序号传给片元(geometry 无 aPanoIndex 的对象恒为 0,仅 mode=1 时读取)
465
+ vPanoIndex = aPanoIndex;
466
+
467
+ #ifdef USE_INSTANCING_COLOR
468
+ vInstanceColor = instanceColor;
469
+ #endif
470
+
471
+ gl_Position = projectionMatrix * modelViewMatrix * modelPos;
472
+
473
+ #include <logdepthbuf_vertex>
753
474
  }
754
- return Array.from({ length: RT_STITCH_SLOT_COUNT }, () => url);
755
- }
756
- /**
757
- * 「这 2 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
758
- * `tiles` 有 **任意 1~32 条**地址(不足 32 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
759
- * 否则各槽同为一张整图(旧数据 / 种子图 / 用户切整片)。
760
- */
761
- export function mktPanoTiledFlag(tiles, mode = "tiled") {
762
- return mode === "tiled" && tiles && tiles.length > 0 ? 1 : 0;
763
- }
764
- /**
765
- * 写入 uPanoTiled(2026-09-22「交换 / 不交换完全一样」的修复)。
766
- *
767
- * 单全景材质该 uniform 是标量;融合材质是**每点一个**的数组(不同点来源可能不同),故带 slot。
768
- * 材质没有该 uniform(旧材质 / 误传)时静默跳过,不抛错。
769
- */
770
- export function setMktPanoTiled(material, tiled, slot = 0) {
771
- if (!material || !material.uniforms)
772
- return;
773
- const u = material.uniforms.uPanoTiled;
774
- if (!u)
775
- return;
776
- if (Array.isArray(u.value) || ArrayBuffer.isView(u.value)) {
777
- const arr = u.value;
778
- if (slot >= 0 && slot < arr.length)
779
- arr[slot] = tiled;
780
- }
781
- else {
782
- u.value = tiled;
783
- }
784
- }
785
- /**
786
- * 创建 MktMesh 专用的「实例化 + 等距柱状投影全景」着色器材质。
787
- * panorama / textureMatrix 由 MktMesh 在运行时按「最近纹理」动态设置。
788
- */
789
- export function createMktShaderMaterial() {
790
- return new ShaderMaterial({
791
- uniforms: {
792
- // 一个全景点恒占 **2 个 sampler**(2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」):
793
- // panoramas[0..1] = 2 张**离屏拼合槽纹理**([左半幅, 右半幅],每槽由 `RT_STITCH_CELLS_PER_SLOT`
794
- // 片瓦片拼成),片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
795
- // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 2 张)—— 槽数再变这里会漏改。
796
- panoramas: {
797
- value: Array.from({ length: RT_STITCH_SLOT_COUNT }, () => createPlaceholderTexture()),
798
- },
799
- textureMatrix: { value: new Matrix4() },
800
- uPanoramaVisible: { value: 1 },
801
- // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 连同着色器里那段 pow 分支已整条撤除
802
- // 全景上下方向**已写死**,无 uniform(见文件头 🎛️ 段)。
803
- // 1 = panoramas[0..1] 是 2 张拼合槽纹理(左/右半幅);0 = 两槽同为一张完整等距柱状图(改用全图 UV)。
804
- // 由喂纹理处按「用户选的 PanoTexMode + 数据来源」写(setMktPanoTiled);默认 1 与改造前行为逐字一致。
805
- uPanoTiled: { value: 1 },
806
- // 投影空间逆矩阵:默认单位阵 = 直接用世界坐标(与改造前行为逐字节一致)。
807
- // 仅由 MktMesh 自身按 matrixWorld 写入,使墙的投影与 MktMesh 整体变换解耦(皇上旨 2026-09-19)。
808
- uProjInvMatrix: { value: new Matrix4() },
809
- // 🔬 归属诊断色(线性空间):**只在 ?panoTint=1 时创建**(JS 侧三元,关时连 uniform 都不存在)
810
- // ⇒ 「仅映射选中的那一个」那套墙/家具材质也带上该景点的固定色,见 setMktPanoTint。
811
- ...(PANO_TINT_ON ? { uPanoTint: { value: new Vector3(1, 1, 1) } } : {}),
812
- // 🔬 距离明暗诊断的「黑端米数」:**只在 ?panoDist=1 时创建**;默认 16 ≈ 常见房间对角线
813
- // (应用层每次喂投影时按 floorSize 写实际值,见 setMktPanoDistRange;万一没喂也不会整图黑)
814
- ...(PANO_DIST_ON ? { uPanoDistMax: { value: 16 } } : {}),
815
- },
816
- vertexShader,
817
- fragmentShader,
818
- // 双面渲染(DoubleSide):09-29 皇上旨「3 个 shader 的 side 参数需要保持一致」——
819
- // 四个全景 shader 工厂(单全景 / blend / mapping / selected)统一 DoubleSide,
820
- // 墙零厚度两面都要见投影,家具/地板与墙两面行为一致(旧 FrontSide「正/反面立方体互不可见」
821
- // 的设计早被编辑器三模式切换的 DoubleSide 三件套冲掉,留着只是初始态不一致的祸根)。
822
- side: DoubleSide,
823
- // 面片一律不透明(每实例透明度已移除):走不透明队列,写深度且免去按距离排序开销
824
- transparent: false,
825
- // 跳过 ThreeBase 的 ReinhardToneMapping,避免压暗全景颜色(色彩空间口径见文件头 ③:硬件解码一次、片元不再手动 pow)
826
- toneMapped: false,
827
- });
828
- }
829
- /**
830
- * 多全景融合着色器:把「最近最多 maxPano 个全景」以数组传入,每个片元对这些张各自做等距柱状投射,
831
- * 再取**唯一一条**口径(皇上旨 2026-09-25 晚「都要一起改」→ 2026-09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
832
- * - **候选里只取水平距离最近的那一张,满权重**(地板 / 墙 / 家具立方体**同一份、同一条**,
833
- * 旧的「地板按覆盖度加权融合、交界渐隐」那一路已按旨删除 ⇒ 面上不再有渐隐带,也不再有中央露灰)。
834
- * - ⚠️ 09-24~09-25 的「夹角门(点在面正面侧才合格)+ 全不合格兜底」已按 09-27 晚旨**整链删除**:
835
- * 它是「明暗圆对、纹理不对」的第二来源(墙背后更近的点被门否掉,纹理落到正面侧的远槽);
836
- * 纯距离口径下墙背后的全景也会投过来,皇上知情选定。
837
- * - 候选池按「槽里有没有全景」筛,**不按覆盖半径(c>0)筛**,否则两个景点距离一远就变成
838
- * 「各占一片互不相干的 territory、交界处整条露灰底」,距离判据压根轮不到(实测 bothCandidates=0)。
839
- * - ⚠️ 代价(已向皇上报过并获批):`uFloorCoreRadius` / `uFloorFadeRange` 两根覆盖半径旋钮**从此对任何面都不参与取图**。
840
- * 控件与 uniform 暂未删(未奉旨),删与不删等皇上下结论。
841
- * - panoramas[i] : 第 i 张全景纹理
842
- * - textureMatrices[i]: 第 i 张全景的世界矩阵(位置=投影中心,旋转=朝向)
843
- * - weights[i] : 第 i 张是否活着(camW,0 = 该槽未绑全景 ⇒ 不进候选池)
844
- */
845
- /**
846
- * 生成多全景片元着色器(地板 / 墙 / 家具共用)。
847
- * ⚠️ 关键约束:Windows/ANGLE(D3D) 后端**禁止用循环变量/非常量索引 sampler 数组**(报错
848
- * "array index for samplers must be constant integral expressions"),故这里在 JS 侧把循环
849
- * 完全展开成「字面量下标」的若干代码段(panoramas[0]…panoramas[N-1]),所有数组访问均为常量下标。
850
- * 未参与融合的槽位权重=0,被 if(w>0.0) 跳过(常量分支,零开销)。
851
- */
852
- function buildFloorBlendFragment(maxPano, panoDistOn) {
853
- // 每个全景点占 **2** 张拼合槽纹理(32 片每 16 片拼一张:左半幅 / 右半幅)⇒ 8 × 2 = 16 = sampler 硬顶
854
- const tileCount = maxPano * RT_STITCH_SLOT_COUNT;
855
- let slotCalc = ""; // 每槽各自算出:颜色 linK / 水平距离 dK / 夹角 angK(acos+背面+180°)/ 分值 scoreK = d−ang
856
- let slotReduceLive = ""; // 每槽参与「score 最小值」归约(候选池 = 槽里活着的那几张,score<1e8 即活)
857
- let slotOne = ""; // 每槽对「只取 score 最小那一张」的贡献(唯一取图口径,地板/墙/家具共用)
858
- let slotOwner = ""; // 🔬 每槽对「归属诊断色」的贡献(仅 ?panoTint=1 时生成,正常态为空串)
859
- for (let k = 0; k < maxPano; k++) {
860
- const base = k * RT_STITCH_SLOT_COUNT; // 本点占 2 个连续纹理槽:base=k*2 .. base+1(顺序 [左半幅, 右半幅])
861
- slotCalc += `
862
- // ── 点槽 ${k}(纹理 ${base}..${base + 1})──
863
- vec3 lin${k} = vec3(0.0);
864
- float d${k} = 1e9; // 本面这点到本槽全景中心的**水平**距离(score 被减数)
865
- // float camD${k} —— 14:24 旨曾加「全景中心到相机距离」项,15:07 旨去掉(连声明一并去,注释存档)
866
- float cosA${k} = -1.0; // 「点→全景」向量 与 正面法线 的夹角余弦(acos 后 = 夹角弧度)
867
- float ang${k} = 1e9; // 夹角(acos 后 0..180°,背面 +180° ⇒ 270°..360°),score 的减数
868
- float score${k} = 1e9; // ⭐主判据(皇上 09-28 15:07 旨七修):score = 顶点与全景的距离 − 夹角,取**最小**者定纹理
869
- // ── 旧原理存档(注释保留不删)──
870
- // 09-28 14:24 旨「score = 顶点距离 + 相机距离 − 夹角」:camD 项已整条撤(见下方槽内注释行)
871
- // 09-28 00:44 旨「score = 距离 × 夹角²,取积最小」:见下方槽内注释行
872
- // 09-28 01:56 旨「按夹角从小到大排序取最小、打平比水平距离」:bestAng/bestDAtAng 两段归约(已撤,git 可查)
873
- {
874
- float camW = weights[${k}]; // 该全景的「锚点距离权重」(JS 按**名册那条水平距离**反比算,未归一化;⭐ 10-07 第二十一道旨后那把尺量的端点 = 控制器中心点,未注入时才是相机位置)
875
- if (camW > 0.0) {
876
- mat4 tm = textureMatrices[${k}];
877
- vec3 texPos = tm[3].xyz; // 投影中心 = 纹理矩阵位置
878
- mat3 texRot = mat3(tm); // 纹理朝向 = 旋转分量
879
- vec3 dir = normalize(vProjPos - texPos);
880
- dir = texRot * dir; // 转到纹理局部方向
881
- float lon = atan(dir.z, dir.x);
882
- float u = lon * 0.15915494309189535 + 0.5; // 经度 -> u
883
- float lat = asin(clamp(dir.y, -1.0, 1.0));
884
- // 等距柱状线性 v(= 全图 UV),与单全景分支**逐字同口径**,理由见单全景分支说明
885
- float v = 0.5 + lat * 0.3183098861837907; // 1/PI
886
- // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
887
- // ⭐ 10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:竖着不再分块 ⇒ 槽只随 u 走,与单全景分支逐字同口径
888
- int tileIdx = (u >= 0.5) ? 1 : 0;
889
- float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
890
- // ⛔ 纵向不再减半(槽就是全高)⇒ localV 恒等于 v;写成 2v/2v-1 = 纵向分辨率白丢一半
891
- float localV = v;
892
- // 🧩 该点这 2 槽装的是整图(用户切了 whole 形态,或该点压根没切过片)⇒ 还原成全图 UV 再采样,与单全景分支逐字同口径
893
- // (方向已写死,故横向还原式恒等于 u、纵向 localV 本就等于 v,见单全景分支推导)
894
- float panoU = (uPanoTiled[${k}] > 0.5) ? localU : u;
895
- float panoV = (uPanoTiled[${k}] > 0.5) ? localV : v;
896
- // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组:base 为本点连槽起点(JS 常量 k*RT_STITCH_SLOT_COUNT),
897
- // 以下 2 个采样下标均为「base + 字面量 0/1」的常量表达式,编译安全。
898
- // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
899
- if (${TILE_DEBUG_GLSL} > 0.5) {
900
- vec3 dbgCol;
901
- if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
902
- else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
903
- gl_FragColor = vec4(dbgCol, 1.0);
904
- #include <colorspace_fragment>
905
- #include <logdepthbuf_fragment>
906
- return;
907
- }
908
-
909
- // ⭐ 采样即线性(与单全景分支同一条口径,10-07 旨「色彩空间统一使用 liner」):纹理都标了
910
- // colorSpace = SRGBColorSpace ⇒ 硬件上传成 SRGB8_ALPHA8、texture2D 已解码一次;
911
- // 这里**不再手动 pow 解码**(那遍是二次解码 = 整体压暗),末端只编码一次 ⇒ 与源图逐字节还原。
912
- if (tileIdx == 0) lin${k} = texture2D(panoramas[${base}], vec2(panoU, panoV)).rgb;
913
- else lin${k} = texture2D(panoramas[${base + 1}], vec2(panoU, panoV)).rgb;
914
- // ⭐ 09-28 15:07 旨定版(七修):score = **顶点到全景的距离 − 夹角**,取最小者。
915
- // 夹角 = acos(cosA)(0..180°),cosA = dot(normalize(全景中心 − 本点), N)(N 已按 gl_FrontFacing 翻到观察面)。
916
- // 背面一刀 +180°(00:26 旨「背面的夹角该大于 180 度」)保留。
917
- // 14:24 那版曾加「全景与相机的距离 camD」,15:07 旨「把全景到相机的那个逻辑去掉」,注释保留不删。
918
- vec3 toPano${k} = texPos - vProjPos;
919
- float lenP${k} = length(toPano${k});
920
- cosA${k} = (lenP${k} > 1e-4) ? dot(toPano${k} / lenP${k}, N) : 1.0;
921
- d${k} = distance(vProjPos.xz, texPos.xz);
922
- // camD${k} = distance(cameraPosition, texPos); // ⛌ 14:24 旨「全景与相机距离」项,15:07 旨去掉,注释保留不删
923
- ang${k} = acos(clamp(cosA${k}, -1.0, 1.0));
924
- if (cosA${k} < 0.0) ang${k} += 3.14159265; // 背面 +180°(π)
925
- score${k} = d${k} - ang${k}; // ⭐ 15:07 旨:距离 − 角度,取最小
926
- // score${k} = d${k} + camD${k} - ang${k}; // ⛌ 旧原理(14:24 旨:两距离之和 − 角度),注释保留不删
927
- // score${k} = d${k} * ang${k} * ang${k}; // ⛌ 旧原理(00:44 旨:距离 × 夹角² 取积最小),注释保留不删
928
- }
929
- }`;
930
- // ⭐ 归约(皇上 09-28 15:07 旨七修「顶点距离 − 夹角」):bestScore = 各活槽分值最小值。
931
- // score == bestScore 是同值浮点精确比较(min 存的就是同一份 bit 值,== 可靠,与历代口径同一技巧)。
932
- slotReduceLive += `
933
- if (score${k} < 1e8) {
934
- bestScore = min(bestScore, score${k});
935
- }`;
936
- // ⭐ 胜者(皇上 09-28 15:07 旨七修):score = 顶点距离 − 角度 最小的那张。
937
- slotOne += `
938
- if (score${k} < 1e8 && score${k} == bestScore) {
939
- wsum += 1.0; acc += lin${k}; cover += 1.0;
940
- }`;
941
- // 🔬 归属诊断(皇上旨 2026-09-25「就在最后算出来的颜色上再叠颜色,不用想那么复杂」):
942
- // 这里只**记账**(这点归哪个景点),染色动作留到 main 末尾对最终颜色做一次。
943
- // 归属判据与 slotOne **逐字同一条**(距离−角度最小),末了统一「打平归编号小的那个」。
944
- if (PANO_TINT_ON) {
945
- slotOwner += `
946
- if (score${k} < 1e8 && score${k} == bestScore) {
947
- // ⚠️ tintCnt == 0.0 这道闸 = **打平归编号小的那个**:两张一样近的那条分界带(实测两景点连线的中垂面与墙交线那一带)
948
- // 会同时命中两个槽,平均出来是「红+绿=橄榄」—— 那不是没上色,但肉眼读不出归属,故只记第一个。
949
- if (tintCnt == 0.0) { tintAcc += uPanoTintColors[${k}]; tintCnt += 1.0; }
950
- }`;
475
+ `;
476
+ const fragmentShader = /* glsl */ `
477
+ // 对数深度缓冲(与顶点着色器配套,保证 MktMesh 深度与场景一致)
478
+ #include <logdepthbuf_pars_fragment>
479
+
480
+ // 一个全景点 = 2 张**拼合槽纹理**(原图切 8×4=32 片,每 4×4=16 片拼成一张槽,10-06 晚皇上旨)。
481
+ // panoramas[0..1] = [左半幅, 右半幅] 两个 RT;片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
482
+ // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组,故槽选择用「常量下标 if 链」(tileIdx 命中的分支内 sampler 下标为字面量)。
483
+ uniform sampler2D panoramas[${RT_STITCH_SLOT_COUNT}];
484
+ uniform mat4 textureMatrix; // 纹理世界矩阵:位置(第4列 xyz)=投影中心,旋转(左上3x3)=纹理朝向
485
+ uniform float uPanoramaVisible; // 全景投射显隐:1=显示全景,0=仅显示中性素色(编辑态切换/投射显隐开关用)
486
+ // 色彩空间**只有一条路**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
487
+ // 原 uniform float uColorSpace; + 片元里那段 sRGB→linear 的 pow 分支已整条撤除,勿加回(原因见下方采样处)。
488
+ // 全景上下方向**已写死**(等距柱状线性 v + 不交换 + 不翻转,2026-09-22 皇上定论,理由见文件头 🎛️ 段):
489
+ // 曾有 uPanoSwapRow / uPanoFlipTile / uPanoVertOff 三个可调 uniform,现已整条撤除,勿加回。
490
+ // ⚠️ uPanoTiled = panoramas[0..1] 装的是「拼合好的左右两张槽纹理」(1) 还是「同一张完整等距柱状图」(0)。
491
+ // 由「用户选的纹理形态 PanoTexMode + 该点有没有切过片」共同决定(见下方 mktPanoTiledFlag)。
492
+ // 0 时必须改用**全图 UV** 取样:拿瓦片内的 localU/localV 去采整图 = 把整图的左半幅拉伸到整个
493
+ // 球面(墙面仰角 +10° 采到原图 −70° 的地板)—— 2026-09-22「全景位置不对」的第二真因,见文件头 🧩 段。
494
+ uniform float uPanoTiled;${PANO_TINT_ON ? `
495
+ // 🔬 归属诊断色(仅 ?panoTint=1 时存在):本材质当前投的就是「选中的那一个」全景,
496
+ // 故由应用层把**那个景点**的固定色写进来(见 setMktPanoTint)⇒ 墙面/立方体上的色与球色同相。
497
+ uniform vec3 uPanoTint;` : ""}${PANO_DIST_ON ? `
498
+ // 🔬 距离明暗诊断(仅 ?panoDist=1 时存在):「多远算全黑」的米数,由应用层写(见 setMktPanoDistRange)
499
+ uniform float uPanoDistMax;` : ""}
500
+
501
+ // 取样坐标(已由顶点着色器的 uProjInvMatrix 抵消掉 MktMesh 的整体变换,与 textureMatrix 一样对变换免疫)
502
+ varying vec3 vProjPos;
503
+ // 实例染色(仅 InstancedMesh 带 instanceColor 时启用):默认白=全景真实色,选中黄=高亮染色
504
+ #ifdef USE_INSTANCING_COLOR
505
+ varying vec3 vInstanceColor;
506
+ #endif
507
+
508
+ void main() {
509
+ // 投影中心 = 纹理矩阵的位置
510
+ vec3 texPos = textureMatrix[3].xyz;
511
+ // 纹理朝向 = 纹理矩阵的旋转分量(mat4 -> mat3 取左上 3x3)
512
+ mat3 texRot = mat3(textureMatrix);
513
+
514
+ // 从纹理位置指向该片元的方向向量(即「以纹理矩阵位置为中心」)。
515
+ vec3 dir = normalize(vProjPos - texPos);
516
+ dir = texRot * dir;
517
+
518
+ // 经度 [-PI, PI] -> u ∈ [0,1]
519
+ float lon = atan(dir.z, dir.x);
520
+ float u = lon * 0.15915494309189535 + 0.5; // 1/(2*PI)
521
+
522
+ // 纬度 [-PI/2, PI/2]。⚠️ **等距柱状(equirectangular)口径**:全景图的像素行正比于纬度,
523
+ // 故 v 必须**线性于 lat**,±90° 落在 1/0(线性在极点不发散,因此不需要截断纬度)。
524
+ // 此处曾按墨卡托 log(tan(45+lat/2)) 换算 —— 与切片口径不一致,导致墙面仰角 +10° 只采到
525
+ // 原图 +5.0°、且越偏离赤道偏得越多。这才是「全景映射位置不对」的根因(2026-09-22 定位后写死)。
526
+ float lat = asin(clamp(dir.y, -1.0, 1.0));
527
+ float v = 0.5 + lat * 0.3183098861837907; // 1/PI
528
+
529
+ // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
530
+ // ⭐ 2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:**竖着不再分块** ⇒ 槽只随 **u** 走,
531
+ // v 方向一槽装下整幅上下 180°(这也是槽纹理必须改成 4096² 正方形的原因,见 RT_STITCH_SLOT_W/H)。
532
+ // ⛔ 别把 tileRow 加回来:加回来 = 又变成 2×2 四槽,而 sampler 只有 2 个 ⇒ 读到未定义的槽。
533
+ int tileIdx = (u >= 0.5) ? 1 : 0;
534
+ // 槽内局部 UV(连续、无 fract 边界跳变):横向按左右减半;
535
+ // ⛔ 纵向**不再减半**(槽就是全高)⇒ localV 恒等于 v。写成 2v / 2v-1 = 纵向分辨率白丢一半。
536
+ float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
537
+ float localV = v;
538
+
539
+ // 🧩 uPanoTiled == 0(两槽装的是**同一张完整等距柱状图**,没有拼合槽):
540
+ // 必须还原成**全图 UV** 再采样,也就是直接用 (u, v) ——
541
+ // 横向还原式 (半==左 ? 0.0 : 0.5) + 0.5*localU 代入 localU 后恒等于 u;纵向 localV 本就等于 v,
542
+ // **两个方向都退化成恒等式**(方向已写死:无交换、无翻转、无偏移)。
543
+ // ⛔ 切勿删掉本分支图省事:直接拿 localU/localV 去采整图 = 把整图的左半幅铺满整个球面。
544
+ float panoU = (uPanoTiled > 0.5) ? localU : u;
545
+ float panoV = (uPanoTiled > 0.5) ? localV : v;
546
+
547
+ // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
548
+ if (${TILE_DEBUG_GLSL} > 0.5) {
549
+ vec3 dbgCol;
550
+ if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
551
+ else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
552
+ gl_FragColor = vec4(dbgCol, 1.0);
553
+ #include <colorspace_fragment>
554
+ #include <logdepthbuf_fragment>
555
+ return;
556
+ }
557
+
558
+ // ⭐ 采样出来的**已经是线性值**:全景纹理(瓦片 / 整图 / 拼合槽 RT)一律标 colorSpace = SRGBColorSpace
559
+ // ⇒ WebGL2 按 SRGB8_ALPHA8 上传,texture2D 时硬件已做过一次 sRGB→linear 解码。
560
+ // 这里因此**不再手动解码**(皇上旨 2026-10-07「mktmesh 里面的纹理色彩空间统一使用 liner」):
561
+ // 末端的 <colorspace_fragment> / 终处理再编码一次 ⇒ 一进一出,屏幕上与源图逐字节还原。
562
+ // ⛔ 别加回那段 pow():它在硬件已解码之上再解一遍 = 二次解码压暗(中灰 #808080 掉到 #373737、#333333 掉到 #080808)。
563
+ vec3 projCol;
564
+ if (tileIdx == 0) projCol = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
565
+ else projCol = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
566
+
567
+ // 全景投射显隐:关闭时(仅墙体/地板,回落中性素色,不投射全景);默认 1 完全显示
568
+ if (uPanoramaVisible < 0.5) {
569
+ projCol = vec3(0.78, 0.78, 0.80);
951
570
  }
952
- // 🔬 距离明暗图**不在这儿逐槽拼**了(皇上旨 2026-09-27 晚:要一句真循环遍历 4 个坐标)⇒ 见 `panoDistLoopGlsl`。
571
+
572
+ // 实例染色:默认白=原全景色;选中黄(家具 InstancedMesh 写入 instanceColor)染黄高亮。
573
+ #ifdef USE_INSTANCING_COLOR
574
+ projCol *= vInstanceColor;
575
+ #endif
576
+ ${PANO_TINT_ON ? `
577
+ // 🔬 归属诊断配色(?panoTint=1,皇上旨 2026-09-25):**单全景材质也要上色**。
578
+ // 这一支是「仅映射选中的那一个」映射方式下的墙/家具材质,它没有「哪个景点投到这块面」的
579
+ // 分界概念(整面恒为选中的那一张),故色相由应用层按**选中的景点序号**写进 uPanoTint(见 setMktPanoTint)
580
+ // ⇒ 选 1 号球整面绿、选 2 号整面蓝,与球色对得上。
581
+ // ⚠️ 色相交给标记色、明暗留原画面(乘色叠在亮图上近白,已被打回过);强度只取 PANO_TINT_MIX(皇上 09-26「比例少一点」)。
582
+ {
583
+ float tintLum = dot(projCol, vec3(0.2126, 0.7152, 0.0722));
584
+ vec3 tintTo = uPanoTint * (0.55 + 0.45 * clamp(tintLum * 3.0, 0.0, 1.0));
585
+ projCol = mix(projCol, tintTo, ${PANO_TINT_MIX});
586
+ }` : ""}${PANO_DIST_ON ? `
587
+ // 🔬 距离明暗诊断(?panoDist=1):这一支恒投「选中的那一个」⇒ 距离就是到它那点的水平距离。
588
+ ${panoDistGlsl("projCol", "distance(vProjPos.xz, texPos.xz)")}` : ""}
589
+
590
+ gl_FragColor = vec4(projCol, 1.0);
591
+
592
+ // 输出颜色空间编码:渲染到线性 RT(主编辑器后处理链)时 passthrough,渲染到 sRGB 屏幕(预览弹窗)时自动编码 sRGB
593
+ #include <colorspace_fragment>
594
+
595
+ #include <logdepthbuf_fragment>
953
596
  }
954
- return /* glsl */ `
955
- #include <logdepthbuf_pars_fragment>
956
-
957
- uniform sampler2D panoramas[${tileCount}];${PANO_TINT_ON ? `
958
- // 🔬 归属诊断色(线性空间,**仅 ?panoTint=1 时存在**):第 i 个纹理槽里躺的那个景点的固定色,
959
- // 由应用层每次换绑时写入(见 setMktPanoTint)。⚠️ 刻意不按槽位编译期写死:
960
- // 「仅映射选中的那一个」模式下地板永远只喂 0 号槽,按槽写死 = 选第几个点都涂同一个红(皇上 09-25 反馈)。
961
- uniform vec3 uPanoTintColors[${maxPano}];` : ""}${panoDistOn ? `
962
- // 🔬 距离明暗诊断(这份材质建时 panoDist 选项为真才存在):「多远算全黑」的米数,由应用层每次喂投影时写
963
- // (见 setMktPanoDistRange)⇒ 全屋同一把尺子,跨面才比得出远近。
964
- uniform float uPanoDistMax;
965
- // 🔬 距离明暗诊断的**圆心清单**(皇上旨 2026-09-27「shader 里面有四个全景点的坐标, 每个像素判断距离 4 个点里最小的
966
- // 那个值, 再用这个值设置颜色」):xyz = 该点的世界坐标,w = 1 ⇒ 这一格**真有一个全景点**。
967
- // ⛔ 与 weights 的分工(两者刻意不同,别合并):weights 管「这格的**图**能不能取」(瓦片没绑好就 0,防止拿
968
- // 1×1 占位图刷一片素灰),uPanoPoints 管「这格**有没有点**」⇒ 圆按景点个数画,图按传到位的那几张取。
969
- // ⚠️ 09-27 晚起明暗那段**只读这一个数组**(纯水平距离最近)⇒ 与取图同把尺(夹角门已废);坐标与上面
970
- // textureMatrices[k][3].xyz 同源同值(同一批矩阵喂进来,见 setMktPanoPoints),不是第二套数据。
971
- uniform vec4 uPanoPoints[${maxPano}];` : ""}
972
- uniform mat4 textureMatrices[${maxPano}];
973
- uniform float weights[${maxPano}]; // 锚点距离权重 camW(JS 由名册那条水平距离写入,未归一化;着色器内与空间因子相乘后整体归一化)
974
- uniform vec3 uFloorBaseColor; // 地板底色(线性空间,深灰):只在「一个候选都没有」(maxPano 张全景全未绑定)时露出来
975
- // ⚠️ 下面两根覆盖半径旋钮**自 09-25 晚「都要一起改」起不再参与取图**(覆盖度融合那一路已删,每个像素纯按最近一张)。
976
- // 控件与 uniform 暂留(未奉旨删控件),值仍由应用层写入,但着色器不读 ⇒ 调它们没有视觉变化。
977
- uniform float uFloorCoreRadius;
978
- uniform float uFloorFadeRange;
979
- uniform float uFloorOpacity; // 地板全景投影「整体透明度」(0~1,全局对所有全景一起生效):仅作用于全景图映射的可见度,越小全景越淡、越透出地板灰底;地板灰色本身不变
980
- // 全景投射显隐:1=显示全景(投射到墙/地板),0=仅显示素色(墙体回落中性色,地板恒为 1 不受影响)
981
- uniform float uPanoramaVisible;
982
- // 色彩空间只有一条路(10-07 旨「统一使用 liner」):原 uniform float uColorSpace; + 那段 sRGB pow 解码分支已整条撤除,
983
- // 采样值即线性(纹理标了 SRGBColorSpace ⇒ 硬件解过一次),末端 <colorspace_fragment> 只编码一次。
984
- // ⭐ 取图方式只有一条(皇上旨 2026-09-25 晚「都要一起改」→ 09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
985
- // 地板 / 墙 / 家具**同一份着色器、同一条判据** —— 候选里只取水平距离最近那**一张**,满权重。
986
- // 旧的 uPanoPickNearest(0=地板覆盖度融合 / 1=模型表面取最近)已随融合那一路一起删除;
987
- // 09-24~09-25 的夹角门 + 兜底也已按 09-27 晚旨整链删除(它是「明暗圆对、纹理不对」的第二来源)。
988
- // ⚠️ 候选池是「槽里活着的全景」(weights>0,瓦片绑好了的),**不受覆盖半径筛掉**。
989
- // 全景上下方向**已写死**,无 uniform(与单全景材质同口径,见文件头 🎛️ 段)。
990
- // 每个全景点**各自**的「这 2 槽是瓦片(1)还是同一张整图(0)」—— 融合时不同点可能来源不同(上传的有瓦片、
991
- // 旧数据/种子图没有),故按点存数组;为 0 的点走全图 UV,理由见单全景分支同注释。
992
- uniform float uPanoTiled[${maxPano}];
993
-
994
- // 取样坐标(已由顶点着色器的 uProjInvMatrix 抵消掉 MktMesh 的整体变换,与 textureMatrices 一样对变换免疫)
995
- varying vec3 vProjPos;
996
- // 全景序号映射模式:本三角面归属的全局全景序号(顶点着色器从 geometry 的 aPanoIndex 传来;无此 attribute 的对象恒为 0)
997
- varying float vPanoIndex;
998
- // 几何法线(顶点着色器按同一条矩阵链变换到与 vProjPos 同一空间):取图「正面夹角」判据用
999
- varying vec3 vWNormal;
1000
- // 实例染色(仅当 InstancedMesh 带 instanceColor 时启用):默认白=全景真实色,选中黄=高亮染色
1001
- #ifdef USE_INSTANCING_COLOR
1002
- varying vec3 vInstanceColor;
1003
- #endif
1004
-
1005
- void main() {
1006
- // ⭐ 正面法线 N(分值公式里「夹角」的基准,皇上 09-28 凌晨旨「距离 × 夹角」):几何法线归一化后,
1007
- // 按 gl_FrontFacing 翻到**观察者看的这一面**(墙零厚度 DoubleSide,两面各自以看到的这面算夹角——
1008
- // 同一面墙从两侧看,分值不同是应该的)。地板/封盖/立方体同理。
1009
- vec3 N = normalize(vWNormal);
1010
- if (!gl_FrontFacing) N = -N;
1011
- ${PANO_TINT_ON ? ` // 🔬 归属诊断配色(?panoTint=1):本面这点归哪个景点(tintAcc 累加其固定色,tintCnt 计数)
1012
- vec3 tintAcc = vec3(0.0);
1013
- float tintCnt = 0.0;` : ""}
1014
- ${slotCalc}
1015
- // ⭐ 取图口径(距离+夹角融合,钦定公式):score = 顶点距离 − 角度,取最小的那**一张**(地板/家具/编辑态墙同一份 shader)。
1016
- // (原「uFaceMappingMode 0/1 运行时分支」已按皇上旨 2026-09-29「三模式三 shader」拆出:
1017
- // 序号直取在 createMktFloorMappingShaderMaterial、仅显示选中在 createMktFloorSelectedShaderMaterial。)
1018
- vec3 acc = vec3(0.0);
1019
- float wsum = 0.0;
1020
- float cover = 0.0;
1021
- // 「取图胜者」归约(皇上 09-28 15:07 旨七修定版:score = 顶点距离 − 角度,取最小)
1022
- float bestScore = 1e9;
1023
- ${slotReduceLive}
1024
- ${PANO_TINT_ON ? " // 🔬 先记账:这点按「距离减夹角最小」归哪个景点(与下面 slotOne 的取图判据同一条)\n" + slotOwner : ""}
1025
- // 一面只显一张:score 最小的那张(不论多远,绝不留空面)
1026
- ${slotOne}
1027
- if (wsum > 0.0) acc /= wsum; // 按总权重归一化(总混合值=1)
1028
- // 覆盖度:有候选即 1(09-25 晚起不再有「渐隐/露灰」的中间态),只在「一个候选都没有」时 = 0
1029
- // ⇒ 露出地板/墙体本来的底色。
1030
- float coverage = clamp(cover, 0.0, 1.0);
1031
- // 整体透明度仅作用于全景图映射:把覆盖度按 uFloorOpacity 缩放后再混合,
1032
- // 灰底始终不透明(alpha=1),opacity 越小全景越淡、越透出地板灰色;地板灰色本身不变。
1033
- float coverageEff = clamp(coverage * uFloorOpacity, 0.0, 1.0);
1034
- vec3 col = mix(uFloorBaseColor, acc, coverageEff);
1035
- // 全景投射显隐:关闭时(仅墙体,地板恒为 1)回落中性素色,不投射全景
1036
- if (uPanoramaVisible < 0.5) {
1037
- col = vec3(0.78, 0.78, 0.80);
597
+ `;
598
+ /** 1×1 白色占位纹理:panorama 在尚未激活任何纹理前使用,避免空纹理采样报错 */
599
+ export function createPlaceholderTexture() {
600
+ const tex = new DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
601
+ tex.needsUpdate = true;
602
+ return tex;
603
+ }
604
+ /** 形态合法值(序列化 / 反序列化单一来源,勿在别处另立字面量) */
605
+ export function isPanoTexMode(v) {
606
+ return v === "tiled" || v === "whole";
607
+ }
608
+ /** 缺失格涂灰(09-29 晚旨「不足张数缺的地方涂灰,一眼看出数据不全」)。
609
+ * 取 #767676 ≈ mapping 模式 `uFaceMissingColor`(线性 0.18)的 sRGB 值,两处观感一致。
610
+ * 10-06 起拼合器只有一份(canvas 那一份奉旨删除)⇒ 常量搬到本文件当唯一来源。 */
611
+ export const MISSING_TILE_GRAY = "#767676";
612
+ /** 接缝余量(**原图像素**,每片向四边各多切这么多;贴原图边那侧裁到剩多少算多少)。
613
+ *
614
+ * 10-06 槽内改成 4×4 = 16 格 ⇒ 一张槽内部的接缝从旧版的 1 条十字变成 **3 横 3 竖**,这一条是随之而来的画质护栏:
615
+ * ① 上传时每片**向四边多切 M 像素**(`rtStitchTileRects`);
616
+ * ② 拼合时**只采中间那块**(`rtStitchTileInset` 算出的 UV 内缩窗口),多切的那一圈**不画进格子**。
617
+ *
618
+ * 为什么这样就没了细亮/细暗线:缝的病根是「双线性 / mipmap 在瓦片边界外拿不到邻居像素,只能复制自己的
619
+ * 边缘像素」(就是 `PanoTexMode` 文件头 ② 那条)。多带的 M 像素就是**真的邻居像素**,插值不再靠复制。
620
+ *
621
+ * M 取 8 够不够:一格 1024×1024px、一片原图 1488×1488px(横 W/8、竖 H/4)⇒ 两个方向都是 1.45× 缩小,
622
+ * 三线性 mip 的 footprint 半径最大约
623
+ * `2 × 1.45 ≈ 3` texel ⇒ 8px 有 ≈2.7 倍余量;再小一档(4px)也够用,取 8 是给「原图比 8192 窄 ⇒ 格子
624
+ * 反而在放大」那类点留边界。**代价**:每片文件比「正好 1/8」宽 16px、高 16px(≈ +4% 面积),⛔ 不是降质。 */
625
+ export const RT_STITCH_SEAM_MARGIN_PX = 8;
626
+ /** 32 瓦片批次的标识 = 第一条**非空**瓦片地址(同批上传 URL 前缀一致,足以唯一;
627
+ * 09-29 晚旨「不足张数缺的地方涂灰」后批次里允许有空串占位 ⇒ 取首个非空,防首张缺失时键为空串互撞) */
628
+ export function rtStitchBatchOf(tiles) {
629
+ return tiles.find((u) => u) || "";
630
+ }
631
+ /** 某一拼合槽的虚拟键:`rt:<batch>#<slot>` */
632
+ export function rtStitchKey(batch, slot) {
633
+ return `${RT_STITCH_KEY_PREFIX}${batch}#${slot}`;
634
+ }
635
+ /** 是否拼合槽(`rt:<batch>#<slot>`)的虚拟键 —— 16 片 / 64 片 / 现行 32 片**同一个前缀**,键本身不含片数信息 */
636
+ export function isRtStitchKey(key) {
637
+ return key.startsWith(RT_STITCH_KEY_PREFIX);
638
+ }
639
+ /** 解析虚拟键 → { batch, slot };非 rt 键返回 null */
640
+ export function rtStitchKeyParts(key) {
641
+ if (!isRtStitchKey(key))
642
+ return null;
643
+ const body = key.slice(RT_STITCH_KEY_PREFIX.length);
644
+ const i = body.lastIndexOf("#");
645
+ if (i < 0)
646
+ return null;
647
+ return { batch: body.slice(0, i), slot: Number(body.slice(i + 1)) };
648
+ }
649
+ /** 槽 slot 用哪 16 张瓦片(原图 **8 列 × 4 行 行优先**下标 `r*8+c`;**槽内格序 = 4×4 行优先、图像左上起**):
650
+ * 槽 0 = 图像**左半幅**(行 0-3 × 列 0-3)→ `[0,1,2,3, 8,9,10,11, 16,17,18,19, 24,25,26,27]`;
651
+ * 槽 1 = **右半幅**(行 0-3 × 列 4-7)→ `[4,5,6,7, 12,13,14,15, 20,21,22,23, 28,29,30,31]`。
652
+ * ⭐ 竖着**不再分块**(皇上旨「每个全景纹理由左右 2 个 RT 组成」)⇒ 两个槽**都是全高**,只按 u 分左右,
653
+ * 故槽纹理也必须跟着变正方形(见 `RT_STITCH_SLOT_W/H` 那条注释,⛔ 别只改这里漏了那边)。
654
+ * ⚠️ 表是**从上面那几个网格常数生成**的(⛔ 手抄 32 个数):正确性由护栏 `test/mktmesh.test.js` BE 组
655
+ * 对着手写死的字面量逐个数断言 —— 改了网格常数而护栏没跟着改,当场红。 */
656
+ export const RT_STITCH_SLOT_TILES = (() => {
657
+ // 横向几个槽(COLS/SLOT_GRID = 8/4 = 2);纵向恒 1 个槽(ROWS/SLOT_GRID = 4/4 = 1 ⇒ sr 恒 0 = 全高)
658
+ const slotCols = RT_STITCH_COLS / RT_STITCH_SLOT_GRID;
659
+ const slots = [];
660
+ for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
661
+ const sr = Math.floor(slot / slotCols) * RT_STITCH_SLOT_GRID;
662
+ const sc = (slot % slotCols) * RT_STITCH_SLOT_GRID;
663
+ const ids = [];
664
+ for (let r = 0; r < RT_STITCH_SLOT_GRID; r++) {
665
+ for (let c = 0; c < RT_STITCH_SLOT_GRID; c++)
666
+ ids.push((sr + r) * RT_STITCH_COLS + (sc + c));
1038
667
  }
1039
- // 实例染色:默认白=原全景色;选中黄(FurnitureObject.highlight 写入 instanceColor)染黄高亮。
1040
- // 普通 Mesh(墙/地板)无 instanceColor,USE_INSTANCING_COLOR 不定义,此分支被编译剔除。
1041
- #ifdef USE_INSTANCING_COLOR
1042
- col *= vInstanceColor;
1043
- #endif
1044
- ${PANO_TINT_ON ? ` // 🔬 归属诊断配色(皇上旨 2026-09-25「就在最后算出来的颜色再叠加上颜色,不用想那么复杂」):
1045
- // 整段只在 ?panoTint=1 时生成,默认关 ⇒ 正常态一行都没有。色相全交给所属景点,只保留明暗,
1046
- // 所以亮部/暗部仍看得出是原照片的纹理,但一眼能分「这块面归哪个景点」。
1047
- // ⚠️ 与上一版(逐槽染 linK)的区别:这里染的是**最终颜色** ⇒ 即使那一片压根没有候选(一个全景都没绑上、
1048
- // 露底色)之外的任何区域也照样按归属上色;归属判据与取图判据同一条 = 「存活口径最近一张」。
1049
- // ⚠️ 刻意不用「采样色 × 淡色」:那种叠法叠在亮图上几乎发白,肉眼看不出(09-25 已被打回过一次)。
1050
- // ⚠️ 强度 = PANO_TINT_MIX(皇上旨 09-26 晚「叠加的颜色比例少一点」):原来整块换成色相 ⇒ 只剩明暗、
1051
- // 底下一张全景图完全读不出来;现在按 mix(原色, 染色, 本值) 叠,色相仍占主但照片看得见了。
1052
- if (tintCnt > 0.0) {
1053
- vec3 tintHue = tintAcc / tintCnt;
1054
- float tintLum = dot(col, vec3(0.2126, 0.7152, 0.0722));
1055
- vec3 tintTo = tintHue * (0.55 + 0.45 * clamp(tintLum * 3.0, 0.0, 1.0));
1056
- col = mix(col, tintTo, ${PANO_TINT_MIX});
1057
- }` : ""}${panoDistOn ? `
1058
- // 🔬 距离明暗诊断(皇上旨 09-26 晚「距离越小越白、越远越黑」+ 09-27 早「按我有 4 个全景点来写代码」+
1059
- // 09-27 晚「我一开始的需求就是要把 4 个全景点的坐标传给 shader,按最近取用」):
1060
- // **一句真循环**遍历 uPanoPoints[0..${maxPano - 1}],每个像素只留「到最近那个景点」的水平距离。
1061
- // ⚠️ 纯水平距离最近(09-27 晚定版后与取图**同把尺**,夹角门已废)⇒ 墙背后的景点也往这面墙上画圆,
1062
- // 一个面能看满 N 个渐变圆;代价 = 明暗图不分身前身后(与取图口径一致)。
1063
- ${panoDistLoopGlsl("col", maxPano)}` : ""}
1064
- gl_FragColor = vec4(col, 1.0);
1065
- // 输出颜色空间编码:渲染到线性 RT(主编辑器后处理链)时 passthrough,渲染到 sRGB 屏幕(预览弹窗)时自动编码 sRGB
1066
- #include <colorspace_fragment>
1067
- #include <logdepthbuf_fragment>
668
+ slots.push(ids);
1068
669
  }
1069
- `;
1070
- }
1071
- /** 融合材质的全景点数**上限**:每点占 **2** 张拼合槽纹理(32 片 8×4 → 左右 2 槽,每槽 4×4 格)⇒ **8 × 2 = 16 个 sampler**,再多样本编译不过。
1072
- * ⭐ 2026-10-06 晚皇上旨「一次能够展示 8 个全景纹理」:靠的就是「每点从 4 槽降到 2 槽」——
1073
- * sampler 总数 16 是硬件硬顶(皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16,一直没变),
1074
- * 原来 16 ÷ 4 槽 = 4 个景点,现在 16 ÷ 2 槽 = **8 个景点**,画质与单全景显存**分毫未动**。 */
1075
- export const MKT_PANO_MAX_SLOTS = 8;
1076
- /**
1077
- * 创建「多全景融合」着色器材质:每帧由外部喂入最近 maxPano 个全景点(⭐ 10-06 晚旨后每点 2 张拼合槽纹理)。
1078
- * 同一份着色器同时服务普通 Mesh(墙/地板)与 InstancedMesh(家具立方体):
1079
- * 实例化与否由 three.js 按对象类型自动注入 USE_INSTANCING 宏决定,无需外部传 instanced 开关。
1080
- * 数组 uniform 在创建时即按 maxPano 长度初始化(占位纹理 + 单位矩阵 + 权重 0),运行时由渲染器每帧改写对应槽位。
1081
- * ⚠️ 取图口径只有一条(皇上旨 2026-09-25 晚「都要一起改」→ 09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
1082
- * 地板 / 墙 / 家具**全部**按「水平距离最近的一张」取图(09-24~09-25 的夹角门已整链删除),
1083
- * 不再有「地板按覆盖度融合」这条路,故也**没有** `pickNearest` 选项(旧的 0/1 分流 uniform 已删)。
1084
- */
1085
- export function createMktFloorBlendShaderMaterial(options = {}) {
1086
- // 2026-09-20:每个全景点占 N 张纹理槽,故「最多融合点数」maxPano 与纹理上限挂钩:
1087
- // maxPano × N 张纹理须 ≤ 16(shader sampler 上限)。
1088
- // 2026-09-24 皇上旨「同一时间要准备 4 个全景的瓦片纹理,一共刚好 16 个」(当时 N=4 ⇒ 上限 4)。
1089
- // ⭐ 2026-10-06 晚旨「一次能够展示 8 个全景纹理」:N 从 4 降到 **2**(每个全景只占左右两张 RT)
1090
- // ⇒ 16 ÷ 2 = **上限 8 个景点**,默认就取满 8。画质 / 单全景显存**分毫未动**。
1091
- // ⚠️ 10-06 切 32 片之后这两个槽各是「16 片拼出来的一张 4096² RT」,sampler 总数仍是 16(硬顶)。
1092
- // ⚠️ 所有喂同一批数据的材质必须同档:某处写成 3 而喂料方按 8 喂,会往 panos[6..15] 越界写、着色器读到 undefined sampler。
1093
- const maxPano = Math.min(options.maxPano ?? MKT_PANO_MAX_SLOTS, MKT_PANO_MAX_SLOTS);
1094
- // 🔬 这一份材质是否带「近白远黑」图:显式传了听他的(编辑器),没传读 URL(发布页/预览默认关)。
1095
- const panoDistOn = options.panoDist ?? PANO_DIST_ON;
1096
- const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 总纹理槽数 = 点数 × 每点槽数(8 × 2 = 16 = sampler 硬顶)
1097
- const panoramas = [];
1098
- const matrices = [];
1099
- const weights = new Float32Array(maxPano); // 默认全 0(未参与)
1100
- for (let i = 0; i < tileCount; i++) {
1101
- panoramas.push(createPlaceholderTexture());
1102
- }
1103
- for (let i = 0; i < maxPano; i++) {
1104
- matrices.push(new Matrix4());
1105
- }
1106
- return new ShaderMaterial({
1107
- uniforms: {
1108
- panoramas: { value: panoramas },
1109
- textureMatrices: { value: matrices },
1110
- weights: { value: weights },
1111
- // 地板底色(线性空间深灰,约 sRGB 0.27):远处露出此色
1112
- uFloorBaseColor: { value: new Vector3(0.06, 0.06, 0.06) },
1113
- // 🔬 归属诊断色(每槽一个 vec3 = 该槽里那个景点的固定色):**只在 ?panoTint=1 时创建**,
1114
- // 关态连 uniform 都不存在(JS 侧三元)⇒ 线上正常路径零开销。默认白 = 未被应用层写过。
1115
- ...(PANO_TINT_ON
1116
- ? { uPanoTintColors: { value: Array.from({ length: maxPano }, () => new Vector3(1, 1, 1)) } }
1117
- : {}),
1118
- // 🔬 距离明暗诊断的「黑端米数」:**这份材质 panoDist 为真才创建**(默认 16 ≈ 常见房间对角线,
1119
- // 应用层每次喂投影时按现场量出的尺子写实际值,见 setMktPanoDistRange);
1120
- // uPanoPoints = 同一张图的**圆心清单**(每格一个 vec4,w=1 表示这格有个全景点,见 setMktPanoPoints)
1121
- ...(panoDistOn ? {
1122
- uPanoDistMax: { value: 16 },
1123
- uPanoPoints: { value: Array.from({ length: maxPano }, () => new Vector4(0, 0, 0, 0)) },
1124
- } : {}),
1125
- // ⚠️ 下面两根覆盖半径:着色器自 09-25 晚「都要一起改」起**不再读它们**(覆盖度融合那一路已删),
1126
- // uniform 与应用层写入暂留 ⇒ 面板上那两根旋钮调了没有视觉变化,删不删等皇上旨意。
1127
- uFloorCoreRadius: { value: 4 },
1128
- uFloorFadeRange: { value: 4 },
1129
- // 整体透明度(0~1):仅缩放全景图映射的可见度(覆盖度×此值),地板灰底不变;全局对所有全景一起生效;1=全景完全可见
1130
- uFloorOpacity: { value: 1 },
1131
- // 全景投射显隐:1=显示全景,0=仅显示素色(墙体回落中性色,地板恒为 1)
1132
- uPanoramaVisible: { value: 1 },
1133
- // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 已随那段 pow 分支整条撤除
1134
- // 全景上下方向**已写死**,无 uniform(见文件头 🎛️ 段)。
1135
- // 每点各自的「这 2 槽是瓦片(1)/同一张整图(0)」,由喂料处按「PanoTexMode + 真实 tiles」写(setMktPanoTiled);
1136
- // 默认全 1 = 维持改造前的瓦片口径(真实上传的图都是四瓦片)。
1137
- uPanoTiled: { value: new Float32Array(maxPano).fill(1) },
1138
- // 投影空间逆矩阵:默认单位阵(= 世界坐标,行为与改造前一致)。
1139
- // 融合材质默认**永不**写入它 —— 编辑器地板/立方体、预览立方体因此完全不受 MktMesh 变换影响。
1140
- uProjInvMatrix: { value: new Matrix4() },
1141
- },
1142
- vertexShader: floorVertexShader,
1143
- fragmentShader: buildFloorBlendFragment(maxPano, panoDistOn),
1144
- side: DoubleSide,
1145
- transparent: false,
1146
- toneMapped: false,
1147
- });
1148
- }
1149
- /**
1150
- * 「仅显示选中全景」片元(皇上旨 2026-09-29「三模式三 shader」之三):
1151
- * 整面恒显示 **0 号点槽**那一张全景 —— 不看距离、不看夹角、不看序号,槽里是谁就投谁
1152
- * (喂料侧保证 0 号槽 = 当前选中的全景、其余槽留空)。
1153
- * uniform 形状与 blend 同档 ⇒ 喂料复用 feedBlendProjection 槽位指派路零改动;
1154
- * GLSL 只读 0 号点槽,其余槽是声明了没人采的死重(开销可忽略)。
1155
- * 槽没绑好(weights[0]≤0,选中全景瓦片还在路上 / 没选中任何全景)→ 露 uFloorBaseColor 深灰底。
1156
- */
1157
- function buildFloorSelectedFragment(maxPano) {
1158
- const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 形状与 blend 同档(喂料共用)
1159
- return /* glsl */ `
1160
- #include <logdepthbuf_pars_fragment>
1161
- uniform sampler2D panoramas[${tileCount}]; // 形状与 blend 同档(喂料共用);本 shader 只采 0 号点槽(纹理 0..1)
1162
- uniform mat4 textureMatrices[${maxPano}];
1163
- uniform float weights[${maxPano}]; // 0 号槽 > 0 = 选中全景的瓦片已绑好
1164
- uniform float uPanoTiled[${maxPano}];
1165
- uniform vec3 uFloorBaseColor; // 槽没绑好时露出的深灰底(线性空间,约 sRGB 0.27)
1166
- uniform float uFloorOpacity; // 整体透明度(与 blend 同口径:只缩放全景可见度,底色不变)
1167
- // 色彩空间只有一条路(10-07 旨「统一使用 liner」):原 uniform float uColorSpace; + 那段 pow 分支已整条撤除(与 blend 同口径)
1168
- uniform float uPanoramaVisible;
1169
- varying vec3 vProjPos;
1170
- #ifdef USE_INSTANCING_COLOR
1171
- varying vec3 vInstanceColor;
1172
- #endif
1173
- void main() {
1174
- // ── 0 号点槽(纹理 0..1,即左右那 2 张拼合槽):选中的全景独占(采样段与 blend 逐字同口径)──
1175
- vec3 lin0 = vec3(0.0);
1176
- if (weights[0] > 0.0) {
1177
- mat4 tm = textureMatrices[0];
1178
- vec3 texPos = tm[3].xyz;
1179
- mat3 texRot = mat3(tm);
1180
- vec3 dir = normalize(vProjPos - texPos);
1181
- dir = texRot * dir;
1182
- float lon = atan(dir.z, dir.x);
1183
- float u = lon * 0.15915494309189535 + 0.5; // 经度 -> u
1184
- float lat = asin(clamp(dir.y, -1.0, 1.0));
1185
- float v = 0.5 + lat * 0.3183098861837907; // 1/PI(等距柱状线性 v,与 blend 逐字同口径)
1186
- // ⭐ 10-06 晚旨:竖着不再分块 ⇒ 槽只随 u 走(左右两半:[左半幅=0, 右半幅=1]),与 blend 逐字同口径
1187
- int tileIdx = (u >= 0.5) ? 1 : 0;
1188
- float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
1189
- // ⛔ 纵向不再减半(槽就是全高)⇒ localV 恒等于 v
1190
- float localV = v;
1191
- float panoU = (uPanoTiled[0] > 0.5) ? localU : u;
1192
- float panoV = (uPanoTiled[0] > 0.5) ? localV : v;
1193
- // ⭐ 采样即线性(与单全景 / blend 那两路同一条口径,10-07 旨「色彩空间统一使用 liner」):纹理都标了
1194
- // colorSpace = SRGBColorSpace ⇒ 硬件上传成 SRGB8_ALPHA8、texture2D 已解码一次;
1195
- // 这里**不再手动 pow 解码**(那遍是二次解码 = 整体压暗),末端只编码一次 ⇒ 与源图逐字节还原。
1196
- if (tileIdx == 0) lin0 = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
1197
- else lin0 = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
670
+ return slots;
671
+ })();
672
+ /** 槽内第 cell 格在**图像方向**的位置 `[列, 行]`(0 起,与 `RT_STITCH_SLOT_TILES` 的格序同一口径)。
673
+ * ⚠️ 这是「图像方向」(行 0 = 图像最上),GL 的 y 向上 ⇒ 画进 RT 时 y 要翻(拼合器里那句 `1 - (2*r+1)/n`)。 */
674
+ export function rtStitchCellPos(cell) {
675
+ return [cell % RT_STITCH_SLOT_GRID, Math.floor(cell / RT_STITCH_SLOT_GRID)];
676
+ }
677
+ /** 网格取整:第 `i` 条分界线在原图 `n` 像素上的位置(`round` ⇒ 余数摊到各格,
678
+ * ⛔ 不必再像旧上传代码那样特判「末行末列吸收余数」)。
679
+ * ⚠️ `steps` = **该方向**切几片(横 `RT_STITCH_COLS` / 竖 `RT_STITCH_ROWS`):10-06 晚起两个方向
680
+ * **不再相等**(8×4),必须显式传,⛔ 不许再拿一个「每边片数」两边共用 —— 竖着按 8 切 = 纵向切错。 */
681
+ function gridLine(i, n, steps) {
682
+ return Math.round((i * n) / steps);
683
+ }
684
+ /** 32 片瓦片的切图矩形(行优先、图像左上起,下标 `i = r*8+c` 与 `RT_STITCH_SLOT_TILES` 同源;
685
+ * ⭐ 10-06 晚旨「横 8 × 竖 4 = 32」:`r` 走 `RT_STITCH_ROWS`(4)、`c` 走 `RT_STITCH_COLS`(8),不再相等)。
686
+ *
687
+ * ⭐ **上传侧(`panoramaUpload`)与拼合侧(`PanoTileStitcher`)必须共用这一份几何**:
688
+ * 上传多切多少、拼合就内缩多少;两边各算一次就是「缝没治掉、反而把画面挤歪」。
689
+ * 拼合侧拿不到原图尺寸(它只有 32 个 URL 和 32 张已解码纹理),所以它用 `rtStitchTileInset`
690
+ * —— 那条只需「下标 + 该瓦片自己的像素尺寸」,两者等价的前提是「多切量 = 常数 M、只有贴边那几片被裁」,
691
+ * 而「贴不贴边」纯由下标决定 ⇒ 护栏 BE 组用同一批 (W,H) 把两条算路对表钉死。 */
692
+ export function rtStitchTileRects(w, h) {
693
+ const out = [];
694
+ for (let r = 0; r < RT_STITCH_ROWS; r++) {
695
+ for (let c = 0; c < RT_STITCH_COLS; c++) {
696
+ const x0 = gridLine(c, w, RT_STITCH_COLS);
697
+ const x1 = gridLine(c + 1, w, RT_STITCH_COLS);
698
+ const y0 = gridLine(r, h, RT_STITCH_ROWS);
699
+ const y1 = gridLine(r + 1, h, RT_STITCH_ROWS);
700
+ const ml = Math.min(RT_STITCH_SEAM_MARGIN_PX, x0);
701
+ const mr = Math.min(RT_STITCH_SEAM_MARGIN_PX, w - x1);
702
+ const mt = Math.min(RT_STITCH_SEAM_MARGIN_PX, y0);
703
+ const mb = Math.min(RT_STITCH_SEAM_MARGIN_PX, h - y1);
704
+ out.push({ sx: x0 - ml, sy: y0 - mt, sw: x1 + mr - (x0 - ml), sh: y1 + mb - (y0 - mt), ml, mt, mr, mb });
705
+ }
706
+ }
707
+ return out;
708
+ }
709
+ /** 这片瓦片有没有贴原图边界(8×4 ⇒ 32 片里四周那 20 片至少贴一条边;贴边的**那一侧**多切量被裁成 0)。
710
+ * 拼合侧靠它 + 瓦片自己的像素尺寸还原内缩量 ⇒ 不必知道原图多大。
711
+ * ⚠️ 下标拆分必须按 **列数 COLS=8**(`i = r*8+c`),⛔ 不是 ROWS。 */
712
+ export function rtStitchTileEdgeClip(i) {
713
+ const r = Math.floor(i / RT_STITCH_COLS);
714
+ const c = i % RT_STITCH_COLS;
715
+ return {
716
+ left: c === 0,
717
+ right: c === RT_STITCH_COLS - 1,
718
+ top: r === 0,
719
+ bottom: r === RT_STITCH_ROWS - 1,
720
+ };
721
+ }
722
+ /** 拼合时的采样窗口 = 该瓦片纹理的 UV 里「**只留基础格、丢掉多切那一圈**」的那一块,
723
+ * 返回 `[u0, v0, u1, v1]`(片元里 `mix(vec2(u0,v0), vec2(u1,v1), vUv)`)。
724
+ *
725
+ * `tw/th` = 该瓦片纹理的**实际**像素宽高。内缩比例 = `M / sw`(多切那 M 像素占整片的比例),
726
+ * 与「文件是不是正好等于标称切片区」无关 ⇒ 编码差 1px 也不会露出邻居的格子。
727
+ * 贴原图边那一侧内缩 0:那里压根没多切,且那条边界就是全景自己的边(`ClampToEdge` 照旧)。
728
+ *
729
+ * ⚠️ v 方向口径(与 `panoBitmapTexture` 绑死):瓦片走 `imageOrientation:"flipY"` 预翻 + `texture.flipY=false`
730
+ * ⇒ 纹理 **v=0 = 图像下边**,所以图像顶部余量 `mt` 落在 `v1`、底部 `mb` 落在 `v0`。搞反 = 上下颠倒。 */
731
+ export function rtStitchTileInset(i, tw, th) {
732
+ const e = rtStitchTileEdgeClip(i);
733
+ const m = RT_STITCH_SEAM_MARGIN_PX;
734
+ const u0 = e.left ? 0 : Math.min(0.5, m / tw);
735
+ const u1 = e.right ? 1 : 1 - Math.min(0.5, m / tw);
736
+ const v0 = e.bottom ? 0 : Math.min(0.5, m / th);
737
+ const v1 = e.top ? 1 : 1 - Math.min(0.5, m / th);
738
+ return [u0, v0, u1, v1];
739
+ }
740
+ /**
741
+ * 「这 `RT_STITCH_SLOT_COUNT`(2) 个纹理槽到底喂哪几个地址」。⚠️ **必须**与 `mktPanoTiledFlag` 同源、同一时机调用:
742
+ * 只改标记不改地址(或反之)= 用整图 UV 去采一张瓦片 / 用瓦片内 UV 去采整图,画面立刻错乱。
743
+ * 非 tiled 态(或该点压根没切过片)一律回退成「各槽同为一张整图」。
744
+ * ⭐ 2026-10-06 晚旨「只支持 32 张(8×4)」:只要有瓦片(1~32 条)一律走拼合器(rt: 虚拟键),
745
+ * 不足 32 张的位置由拼合器**涂灰**(一眼看出数据不全);8×8=64 张与 4×4=16 张那两版**都不做兼容**
746
+ * (皇上「无须兼容正常使用,有异常我会重传」⇒ 旧存档的 64 条地址超张数被判废、重传即可)。
747
+ * ⚠️ 返回长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 2 个 rtStitchKey —— 槽数再变会漏改)。
748
+ */
749
+ export function mktPanoTexUrls(url, tiles, mode = "tiled") {
750
+ if (mode === "tiled" && tiles && tiles.length > 0) {
751
+ const batch = rtStitchBatchOf(tiles);
752
+ return Array.from({ length: RT_STITCH_SLOT_COUNT }, (_, s) => rtStitchKey(batch, s));
753
+ }
754
+ return Array.from({ length: RT_STITCH_SLOT_COUNT }, () => url);
755
+ }
756
+ /**
757
+ * 「这 2 个纹理槽是不是真瓦片」的判据 —— **必须**与喂纹理处的回退式(`mktPanoTexUrls`)同源:
758
+ * `tiles` 有 **任意 1~32 条**地址(不足 32 = 数据不全,缺的格子拼合时涂灰)**且**当前形态选的是 tiled = 是瓦片;
759
+ * 否则各槽同为一张整图(旧数据 / 种子图 / 用户切整片)。
760
+ */
761
+ export function mktPanoTiledFlag(tiles, mode = "tiled") {
762
+ return mode === "tiled" && tiles && tiles.length > 0 ? 1 : 0;
763
+ }
764
+ /**
765
+ * 写入 uPanoTiled(2026-09-22「交换 / 不交换完全一样」的修复)。
766
+ *
767
+ * 单全景材质该 uniform 是标量;融合材质是**每点一个**的数组(不同点来源可能不同),故带 slot。
768
+ * 材质没有该 uniform(旧材质 / 误传)时静默跳过,不抛错。
769
+ */
770
+ export function setMktPanoTiled(material, tiled, slot = 0) {
771
+ if (!material || !material.uniforms)
772
+ return;
773
+ const u = material.uniforms.uPanoTiled;
774
+ if (!u)
775
+ return;
776
+ if (Array.isArray(u.value) || ArrayBuffer.isView(u.value)) {
777
+ const arr = u.value;
778
+ if (slot >= 0 && slot < arr.length)
779
+ arr[slot] = tiled;
780
+ }
781
+ else {
782
+ u.value = tiled;
783
+ }
784
+ }
785
+ /**
786
+ * 创建 MktMesh 专用的「实例化 + 等距柱状投影全景」着色器材质。
787
+ * panorama / textureMatrix 由 MktMesh 在运行时按「最近纹理」动态设置。
788
+ */
789
+ export function createMktShaderMaterial() {
790
+ return new ShaderMaterial({
791
+ uniforms: {
792
+ // 一个全景点恒占 **2 个 sampler**(2026-10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」):
793
+ // panoramas[0..1] = 2 张**离屏拼合槽纹理**([左半幅, 右半幅],每槽由 `RT_STITCH_CELLS_PER_SLOT`
794
+ // 片瓦片拼成),片元按全景 UV 的 **u** 挑对应槽采样(v 不再分块)。
795
+ // ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(不许手写 2 张)—— 槽数再变这里会漏改。
796
+ panoramas: {
797
+ value: Array.from({ length: RT_STITCH_SLOT_COUNT }, () => createPlaceholderTexture()),
798
+ },
799
+ textureMatrix: { value: new Matrix4() },
800
+ uPanoramaVisible: { value: 1 }, // 全景投射显隐:1=显示全景,0=仅显示中性素色(编辑态切换/投射显隐开关用)
801
+ // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 连同着色器里那段 pow 分支已整条撤除
802
+ // 全景上下方向**已写死**,无 uniform(见文件头 🎛️ 段)。
803
+ // 1 = panoramas[0..1] 是 2 张拼合槽纹理(左/右半幅);0 = 两槽同为一张完整等距柱状图(改用全图 UV)。
804
+ // 由喂纹理处按「用户选的 PanoTexMode + 数据来源」写(setMktPanoTiled);默认 1 与改造前行为逐字一致。
805
+ uPanoTiled: { value: 1 },
806
+ // 投影空间逆矩阵:默认单位阵 = 直接用世界坐标(与改造前行为逐字节一致)。
807
+ // 仅由 MktMesh 自身按 matrixWorld 写入,使墙的投影与 MktMesh 整体变换解耦(皇上旨 2026-09-19)。
808
+ uProjInvMatrix: { value: new Matrix4() },
809
+ // 🔬 归属诊断色(线性空间):**只在 ?panoTint=1 时创建**(JS 侧三元,关时连 uniform 都不存在)
810
+ // ⇒ 「仅映射选中的那一个」那套墙/家具材质也带上该景点的固定色,见 setMktPanoTint。
811
+ ...(PANO_TINT_ON ? { uPanoTint: { value: new Vector3(1, 1, 1) } } : {}),
812
+ // 🔬 距离明暗诊断的「黑端米数」:**只在 ?panoDist=1 时创建**;默认 16 ≈ 常见房间对角线
813
+ // (应用层每次喂投影时按 floorSize 写实际值,见 setMktPanoDistRange;万一没喂也不会整图黑)
814
+ ...(PANO_DIST_ON ? { uPanoDistMax: { value: 16 } } : {}),
815
+ },
816
+ vertexShader,
817
+ fragmentShader,
818
+ // 双面渲染(DoubleSide):09-29 皇上旨「3 个 shader 的 side 参数需要保持一致」——
819
+ // 四个全景 shader 工厂(单全景 / blend / mapping / selected)统一 DoubleSide,
820
+ // 墙零厚度两面都要见投影,家具/地板与墙两面行为一致(旧 FrontSide「正/反面立方体互不可见」
821
+ // 的设计早被编辑器三模式切换的 DoubleSide 三件套冲掉,留着只是初始态不一致的祸根)。
822
+ side: DoubleSide,
823
+ // 面片一律不透明(每实例透明度已移除):走不透明队列,写深度且免去按距离排序开销
824
+ transparent: false,
825
+ // 跳过 ThreeBase 的 ReinhardToneMapping,避免压暗全景颜色(色彩空间口径见文件头 ③:硬件解码一次、片元不再手动 pow)
826
+ toneMapped: false,
827
+ });
828
+ }
829
+ /**
830
+ * 多全景融合着色器:把「最近最多 maxPano 个全景」以数组传入,每个片元对这些张各自做等距柱状投射,
831
+ * 再取**唯一一条**口径(皇上旨 2026-09-25 晚「都要一起改」→ 2026-09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
832
+ * - **候选里只取水平距离最近的那一张,满权重**(地板 / 墙 / 家具立方体**同一份、同一条**,
833
+ * 旧的「地板按覆盖度加权融合、交界渐隐」那一路已按旨删除 ⇒ 面上不再有渐隐带,也不再有中央露灰)。
834
+ * - ⚠️ 09-24~09-25 的「夹角门(点在面正面侧才合格)+ 全不合格兜底」已按 09-27 晚旨**整链删除**:
835
+ * 它是「明暗圆对、纹理不对」的第二来源(墙背后更近的点被门否掉,纹理落到正面侧的远槽);
836
+ * 纯距离口径下墙背后的全景也会投过来,皇上知情选定。
837
+ * - 候选池按「槽里有没有全景」筛,**不按覆盖半径(c>0)筛**,否则两个景点距离一远就变成
838
+ * 「各占一片互不相干的 territory、交界处整条露灰底」,距离判据压根轮不到(实测 bothCandidates=0)。
839
+ * - ⚠️ 代价(已向皇上报过并获批):`uFloorCoreRadius` / `uFloorFadeRange` 两根覆盖半径旋钮**从此对任何面都不参与取图**。
840
+ * 控件与 uniform 暂未删(未奉旨),删与不删等皇上下结论。
841
+ * - panoramas[i] : 第 i 张全景纹理
842
+ * - textureMatrices[i]: 第 i 张全景的世界矩阵(位置=投影中心,旋转=朝向)
843
+ * - weights[i] : 第 i 张是否活着(camW,0 = 该槽未绑全景 ⇒ 不进候选池)
844
+ */
845
+ /**
846
+ * 生成多全景片元着色器(地板 / 墙 / 家具共用)。
847
+ * ⚠️ 关键约束:Windows/ANGLE(D3D) 后端**禁止用循环变量/非常量索引 sampler 数组**(报错
848
+ * "array index for samplers must be constant integral expressions"),故这里在 JS 侧把循环
849
+ * 完全展开成「字面量下标」的若干代码段(panoramas[0]…panoramas[N-1]),所有数组访问均为常量下标。
850
+ * 未参与融合的槽位权重=0,被 if(w>0.0) 跳过(常量分支,零开销)。
851
+ */
852
+ function buildFloorBlendFragment(maxPano, panoDistOn) {
853
+ // 每个全景点占 **2** 张拼合槽纹理(32 片每 16 片拼一张:左半幅 / 右半幅)⇒ 8 × 2 = 16 = sampler 硬顶
854
+ const tileCount = maxPano * RT_STITCH_SLOT_COUNT;
855
+ let slotCalc = ""; // 每槽各自算出:颜色 linK / 水平距离 dK / 夹角 angK(acos+背面+180°)/ 分值 scoreK = d−ang
856
+ let slotReduceLive = ""; // 每槽参与「score 最小值」归约(候选池 = 槽里活着的那几张,score<1e8 即活)
857
+ let slotOne = ""; // 每槽对「只取 score 最小那一张」的贡献(唯一取图口径,地板/墙/家具共用)
858
+ let slotOwner = ""; // 🔬 每槽对「归属诊断色」的贡献(仅 ?panoTint=1 时生成,正常态为空串)
859
+ for (let k = 0; k < maxPano; k++) {
860
+ const base = k * RT_STITCH_SLOT_COUNT; // 本点占 2 个连续纹理槽:base=k*2 .. base+1(顺序 [左半幅, 右半幅])
861
+ slotCalc += `
862
+ // ── 点槽 ${k}(纹理 ${base}..${base + 1})──
863
+ vec3 lin${k} = vec3(0.0);
864
+ float d${k} = 1e9; // 本面这点到本槽全景中心的**水平**距离(score 被减数)
865
+ // float camD${k} —— 14:24 旨曾加「全景中心到相机距离」项,15:07 旨去掉(连声明一并去,注释存档)
866
+ float cosA${k} = -1.0; // 「点→全景」向量 与 正面法线 的夹角余弦(acos 后 = 夹角弧度)
867
+ float ang${k} = 1e9; // 夹角(acos 后 0..180°,背面 +180° ⇒ 270°..360°),score 的减数
868
+ float score${k} = 1e9; // ⭐主判据(皇上 09-28 15:07 旨七修):score = 顶点与全景的距离 − 夹角,取**最小**者定纹理
869
+ // ── 旧原理存档(注释保留不删)──
870
+ // 09-28 14:24 旨「score = 顶点距离 + 相机距离 − 夹角」:camD 项已整条撤(见下方槽内注释行)
871
+ // 09-28 00:44 旨「score = 距离 × 夹角²,取积最小」:见下方槽内注释行
872
+ // 09-28 01:56 旨「按夹角从小到大排序取最小、打平比水平距离」:bestAng/bestDAtAng 两段归约(已撤,git 可查)
873
+ {
874
+ float camW = weights[${k}]; // 该全景的「锚点距离权重」(JS 按**名册那条水平距离**反比算,未归一化;⭐ 10-07 第二十一道旨后那把尺量的端点 = 控制器中心点,未注入时才是相机位置)
875
+ if (camW > 0.0) {
876
+ mat4 tm = textureMatrices[${k}];
877
+ vec3 texPos = tm[3].xyz; // 投影中心 = 纹理矩阵位置
878
+ mat3 texRot = mat3(tm); // 纹理朝向 = 旋转分量
879
+ vec3 dir = normalize(vProjPos - texPos);
880
+ dir = texRot * dir; // 转到纹理局部方向
881
+ float lon = atan(dir.z, dir.x);
882
+ float u = lon * 0.15915494309189535 + 0.5; // 经度 -> u
883
+ float lat = asin(clamp(dir.y, -1.0, 1.0));
884
+ // 等距柱状线性 v(= 全图 UV),与单全景分支**逐字同口径**,理由见单全景分支说明
885
+ float v = 0.5 + lat * 0.3183098861837907; // 1/PI
886
+ // 左右两半 -> 拼合槽下标 [左半幅=0, 右半幅=1](每槽 = 8×4 网格里 4×4=16 片拼成,见 RT_STITCH_SLOT_TILES)
887
+ // ⭐ 10-06 晚旨「每个全景纹理由左右 2 个 RT 组成」:竖着不再分块 ⇒ 槽只随 u 走,与单全景分支逐字同口径
888
+ int tileIdx = (u >= 0.5) ? 1 : 0;
889
+ float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
890
+ // ⛔ 纵向不再减半(槽就是全高)⇒ localV 恒等于 v;写成 2v/2v-1 = 纵向分辨率白丢一半
891
+ float localV = v;
892
+ // 🧩 该点这 2 槽装的是整图(用户切了 whole 形态,或该点压根没切过片)⇒ 还原成全图 UV 再采样,与单全景分支逐字同口径
893
+ // (方向已写死,故横向还原式恒等于 u、纵向 localV 本就等于 v,见单全景分支推导)
894
+ float panoU = (uPanoTiled[${k}] > 0.5) ? localU : u;
895
+ float panoV = (uPanoTiled[${k}] > 0.5) ? localV : v;
896
+ // ⚠️ ANGLE(D3D) 禁止动态索引 sampler 数组:base 为本点连槽起点(JS 常量 k*RT_STITCH_SLOT_COUNT),
897
+ // 以下 2 个采样下标均为「base + 字面量 0/1」的常量表达式,编译安全。
898
+ // 🔬 诊断直通(见文件头 TILE_DEBUG_GLSL 说明;正常态内联为 0.0,整段被编译器消除)
899
+ if (${TILE_DEBUG_GLSL} > 0.5) {
900
+ vec3 dbgCol;
901
+ if (tileIdx == 0) dbgCol = vec3(1.0, 0.0, 0.0); // 红 = 左半幅槽 0
902
+ else dbgCol = vec3(0.0, 1.0, 0.0); // 绿 = 右半幅槽 1
903
+ gl_FragColor = vec4(dbgCol, 1.0);
904
+ #include <colorspace_fragment>
905
+ #include <logdepthbuf_fragment>
906
+ return;
907
+ }
908
+
909
+ // ⭐ 采样即线性(与单全景分支同一条口径,10-07 旨「色彩空间统一使用 liner」):纹理都标了
910
+ // colorSpace = SRGBColorSpace ⇒ 硬件上传成 SRGB8_ALPHA8、texture2D 已解码一次;
911
+ // 这里**不再手动 pow 解码**(那遍是二次解码 = 整体压暗),末端只编码一次 ⇒ 与源图逐字节还原。
912
+ if (tileIdx == 0) lin${k} = texture2D(panoramas[${base}], vec2(panoU, panoV)).rgb;
913
+ else lin${k} = texture2D(panoramas[${base + 1}], vec2(panoU, panoV)).rgb;
914
+ // ⭐ 09-28 15:07 旨定版(七修):score = **顶点到全景的距离 − 夹角**,取最小者。
915
+ // 夹角 = acos(cosA)(0..180°),cosA = dot(normalize(全景中心 − 本点), N)(N 已按 gl_FrontFacing 翻到观察面)。
916
+ // 背面一刀 +180°(00:26 旨「背面的夹角该大于 180 度」)保留。
917
+ // 14:24 那版曾加「全景与相机的距离 camD」,15:07 旨「把全景到相机的那个逻辑去掉」,注释保留不删。
918
+ vec3 toPano${k} = texPos - vProjPos;
919
+ float lenP${k} = length(toPano${k});
920
+ cosA${k} = (lenP${k} > 1e-4) ? dot(toPano${k} / lenP${k}, N) : 1.0;
921
+ d${k} = distance(vProjPos.xz, texPos.xz);
922
+ // camD${k} = distance(cameraPosition, texPos); // ⛌ 14:24 旨「全景与相机距离」项,15:07 旨去掉,注释保留不删
923
+ ang${k} = acos(clamp(cosA${k}, -1.0, 1.0));
924
+ if (cosA${k} < 0.0) ang${k} += 3.14159265; // 背面 +180°(π)
925
+ score${k} = d${k} - ang${k}; // ⭐ 15:07 旨:距离 − 角度,取最小
926
+ // score${k} = d${k} + camD${k} - ang${k}; // ⛌ 旧原理(14:24 旨:两距离之和 − 角度),注释保留不删
927
+ // score${k} = d${k} * ang${k} * ang${k}; // ⛌ 旧原理(00:44 旨:距离 × 夹角² 取积最小),注释保留不删
928
+ }
929
+ }`;
930
+ // ⭐ 归约(皇上 09-28 15:07 旨七修「顶点距离 − 夹角」):bestScore = 各活槽分值最小值。
931
+ // score == bestScore 是同值浮点精确比较(min 存的就是同一份 bit 值,== 可靠,与历代口径同一技巧)。
932
+ slotReduceLive += `
933
+ if (score${k} < 1e8) {
934
+ bestScore = min(bestScore, score${k});
935
+ }`;
936
+ // ⭐ 胜者(皇上 09-28 15:07 旨七修):score = 顶点距离 − 角度 最小的那张。
937
+ slotOne += `
938
+ if (score${k} < 1e8 && score${k} == bestScore) {
939
+ wsum += 1.0; acc += lin${k}; cover += 1.0;
940
+ }`;
941
+ // 🔬 归属诊断(皇上旨 2026-09-25「就在最后算出来的颜色上再叠颜色,不用想那么复杂」):
942
+ // 这里只**记账**(这点归哪个景点),染色动作留到 main 末尾对最终颜色做一次。
943
+ // 归属判据与 slotOne **逐字同一条**(距离−角度最小),末了统一「打平归编号小的那个」。
944
+ if (PANO_TINT_ON) {
945
+ slotOwner += `
946
+ if (score${k} < 1e8 && score${k} == bestScore) {
947
+ // ⚠️ tintCnt == 0.0 这道闸 = **打平归编号小的那个**:两张一样近的那条分界带(实测两景点连线的中垂面与墙交线那一带)
948
+ // 会同时命中两个槽,平均出来是「红+绿=橄榄」—— 那不是没上色,但肉眼读不出归属,故只记第一个。
949
+ if (tintCnt == 0.0) { tintAcc += uPanoTintColors[${k}]; tintCnt += 1.0; }
950
+ }`;
1198
951
  }
1199
- float coverage = weights[0] > 0.0 ? 1.0 : 0.0;
1200
- float coverageEff = clamp(coverage * uFloorOpacity, 0.0, 1.0);
1201
- vec3 col = mix(uFloorBaseColor, lin0, coverageEff);
1202
- // 全景投射显隐:关闭时回落中性素色(与 blend 一致)
1203
- if (uPanoramaVisible < 0.5) col = vec3(0.78, 0.78, 0.80);
1204
- #ifdef USE_INSTANCING_COLOR
1205
- col *= vInstanceColor;
1206
- #endif
1207
- gl_FragColor = vec4(col, 1.0);
1208
- #include <colorspace_fragment>
1209
- #include <logdepthbuf_fragment>
952
+ // 🔬 距离明暗图**不在这儿逐槽拼**了(皇上旨 2026-09-27 晚:要一句真循环遍历 4 个坐标)⇒ 见 `panoDistLoopGlsl`。
1210
953
  }
1211
- `;
1212
- }
1213
- /**
1214
- * 创建「仅显示选中全景」着色器材质(皇上旨 2026-09-29「三模式三 shader」之三):仅编辑器使用。
1215
- * uniform 形状与 blend 工厂同档 ⇒ 喂料复用 feedBlendProjection 槽位指派路零改动;
1216
- * 本 shader 只读 0 号点槽(GLSL 写死),uFloorCoreRadius/uFloorFadeRange 喂料会写但不读(静默忽略)。
1217
- */
1218
- export function createMktFloorSelectedShaderMaterial(options = {}) {
1219
- const maxPano = Math.min(options.maxPano ?? MKT_PANO_MAX_SLOTS, MKT_PANO_MAX_SLOTS);
1220
- const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 与 blend / mapping 同档(8 点 × 2 槽 = 16)
1221
- const panoramas = [];
1222
- const matrices = [];
1223
- for (let i = 0; i < tileCount; i++)
1224
- panoramas.push(createPlaceholderTexture());
1225
- for (let i = 0; i < maxPano; i++)
1226
- matrices.push(new Matrix4());
1227
- return new ShaderMaterial({
1228
- uniforms: {
1229
- panoramas: { value: panoramas },
1230
- textureMatrices: { value: matrices },
1231
- weights: { value: new Float32Array(maxPano) },
1232
- uPanoTiled: { value: new Float32Array(maxPano).fill(1) },
1233
- uFloorBaseColor: { value: new Vector3(0.06, 0.06, 0.06) },
1234
- // 喂料统一入口会写;本 shader 不读 uFloorCoreRadius/uFloorFadeRange(留 JS 项 = 喂料零分支)
1235
- uFloorCoreRadius: { value: 4 },
1236
- uFloorFadeRange: { value: 4 },
1237
- uFloorOpacity: { value: 1 },
1238
- uPanoramaVisible: { value: 1 },
1239
- // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 已随那段 pow 分支整条撤除
1240
- uProjInvMatrix: { value: new Matrix4() },
1241
- },
1242
- vertexShader: floorVertexShader,
1243
- fragmentShader: buildFloorSelectedFragment(maxPano),
1244
- side: DoubleSide,
1245
- transparent: false,
1246
- toneMapped: false,
1247
- });
1248
- }
954
+ return /* glsl */ `
955
+ #include <logdepthbuf_pars_fragment>
956
+
957
+ uniform sampler2D panoramas[${tileCount}];${PANO_TINT_ON ? `
958
+ // 🔬 归属诊断色(线性空间,**仅 ?panoTint=1 时存在**):第 i 个纹理槽里躺的那个景点的固定色,
959
+ // 由应用层每次换绑时写入(见 setMktPanoTint)。⚠️ 刻意不按槽位编译期写死:
960
+ // 「仅映射选中的那一个」模式下地板永远只喂 0 号槽,按槽写死 = 选第几个点都涂同一个红(皇上 09-25 反馈)。
961
+ uniform vec3 uPanoTintColors[${maxPano}];` : ""}${panoDistOn ? `
962
+ // 🔬 距离明暗诊断(这份材质建时 panoDist 选项为真才存在):「多远算全黑」的米数,由应用层每次喂投影时写
963
+ // (见 setMktPanoDistRange)⇒ 全屋同一把尺子,跨面才比得出远近。
964
+ uniform float uPanoDistMax;
965
+ // 🔬 距离明暗诊断的**圆心清单**(皇上旨 2026-09-27「shader 里面有四个全景点的坐标, 每个像素判断距离 4 个点里最小的
966
+ // 那个值, 再用这个值设置颜色」):xyz = 该点的世界坐标,w = 1 ⇒ 这一格**真有一个全景点**。
967
+ // ⛔ 与 weights 的分工(两者刻意不同,别合并):weights 管「这格的**图**能不能取」(瓦片没绑好就 0,防止拿
968
+ // 1×1 占位图刷一片素灰),uPanoPoints 管「这格**有没有点**」⇒ 圆按景点个数画,图按传到位的那几张取。
969
+ // ⚠️ 09-27 晚起明暗那段**只读这一个数组**(纯水平距离最近)⇒ 与取图同把尺(夹角门已废);坐标与上面
970
+ // textureMatrices[k][3].xyz 同源同值(同一批矩阵喂进来,见 setMktPanoPoints),不是第二套数据。
971
+ uniform vec4 uPanoPoints[${maxPano}];` : ""}
972
+ uniform mat4 textureMatrices[${maxPano}];
973
+ uniform float weights[${maxPano}]; // 锚点距离权重 camW(JS 由名册那条水平距离写入,未归一化;着色器内与空间因子相乘后整体归一化)
974
+ uniform vec3 uFloorBaseColor; // 地板底色(线性空间,深灰):只在「一个候选都没有」(maxPano 张全景全未绑定)时露出来
975
+ // ⚠️ 下面两根覆盖半径旋钮**自 09-25 晚「都要一起改」起不再参与取图**(覆盖度融合那一路已删,每个像素纯按最近一张)。
976
+ // 控件与 uniform 暂留(未奉旨删控件),值仍由应用层写入,但着色器不读 ⇒ 调它们没有视觉变化。
977
+ uniform float uFloorCoreRadius;
978
+ uniform float uFloorFadeRange;
979
+ uniform float uFloorOpacity; // 地板全景投影「整体透明度」(0~1,全局对所有全景一起生效):仅作用于全景图映射的可见度,越小全景越淡、越透出地板灰底;地板灰色本身不变
980
+ // 全景投射显隐:1=显示全景(投射到墙/地板),0=仅显示素色(墙体回落中性色,地板恒为 1 不受影响)
981
+ uniform float uPanoramaVisible;
982
+ // 色彩空间只有一条路(10-07 旨「统一使用 liner」):原 uniform float uColorSpace; + 那段 sRGB pow 解码分支已整条撤除,
983
+ // 采样值即线性(纹理标了 SRGBColorSpace ⇒ 硬件解过一次),末端 <colorspace_fragment> 只编码一次。
984
+ // ⭐ 取图方式只有一条(皇上旨 2026-09-25 晚「都要一起改」→ 09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
985
+ // 地板 / 墙 / 家具**同一份着色器、同一条判据** —— 候选里只取水平距离最近那**一张**,满权重。
986
+ // 旧的 uPanoPickNearest(0=地板覆盖度融合 / 1=模型表面取最近)已随融合那一路一起删除;
987
+ // 09-24~09-25 的夹角门 + 兜底也已按 09-27 晚旨整链删除(它是「明暗圆对、纹理不对」的第二来源)。
988
+ // ⚠️ 候选池是「槽里活着的全景」(weights>0,瓦片绑好了的),**不受覆盖半径筛掉**。
989
+ // 全景上下方向**已写死**,无 uniform(与单全景材质同口径,见文件头 🎛️ 段)。
990
+ // 每个全景点**各自**的「这 2 槽是瓦片(1)还是同一张整图(0)」—— 融合时不同点可能来源不同(上传的有瓦片、
991
+ // 旧数据/种子图没有),故按点存数组;为 0 的点走全图 UV,理由见单全景分支同注释。
992
+ uniform float uPanoTiled[${maxPano}];
993
+
994
+ // 取样坐标(已由顶点着色器的 uProjInvMatrix 抵消掉 MktMesh 的整体变换,与 textureMatrices 一样对变换免疫)
995
+ varying vec3 vProjPos;
996
+ // 全景序号映射模式:本三角面归属的全局全景序号(顶点着色器从 geometry 的 aPanoIndex 传来;无此 attribute 的对象恒为 0)
997
+ varying float vPanoIndex;
998
+ // 几何法线(顶点着色器按同一条矩阵链变换到与 vProjPos 同一空间):取图「正面夹角」判据用
999
+ varying vec3 vWNormal;
1000
+ // 实例染色(仅当 InstancedMesh 带 instanceColor 时启用):默认白=全景真实色,选中黄=高亮染色
1001
+ #ifdef USE_INSTANCING_COLOR
1002
+ varying vec3 vInstanceColor;
1003
+ #endif
1004
+
1005
+ void main() {
1006
+ // ⭐ 正面法线 N(分值公式里「夹角」的基准,皇上 09-28 凌晨旨「距离 × 夹角」):几何法线归一化后,
1007
+ // 按 gl_FrontFacing 翻到**观察者看的这一面**(墙零厚度 DoubleSide,两面各自以看到的这面算夹角——
1008
+ // 同一面墙从两侧看,分值不同是应该的)。地板/封盖/立方体同理。
1009
+ vec3 N = normalize(vWNormal);
1010
+ if (!gl_FrontFacing) N = -N;
1011
+ ${PANO_TINT_ON ? ` // 🔬 归属诊断配色(?panoTint=1):本面这点归哪个景点(tintAcc 累加其固定色,tintCnt 计数)
1012
+ vec3 tintAcc = vec3(0.0);
1013
+ float tintCnt = 0.0;` : ""}
1014
+ ${slotCalc}
1015
+ // ⭐ 取图口径(距离+夹角融合,钦定公式):score = 顶点距离 − 角度,取最小的那**一张**(地板/家具/编辑态墙同一份 shader)。
1016
+ // (原「uFaceMappingMode 0/1 运行时分支」已按皇上旨 2026-09-29「三模式三 shader」拆出:
1017
+ // 序号直取在 createMktFloorMappingShaderMaterial、仅显示选中在 createMktFloorSelectedShaderMaterial。)
1018
+ vec3 acc = vec3(0.0);
1019
+ float wsum = 0.0;
1020
+ float cover = 0.0;
1021
+ // 「取图胜者」归约(皇上 09-28 15:07 旨七修定版:score = 顶点距离 − 角度,取最小)
1022
+ float bestScore = 1e9;
1023
+ ${slotReduceLive}
1024
+ ${PANO_TINT_ON ? " // 🔬 先记账:这点按「距离减夹角最小」归哪个景点(与下面 slotOne 的取图判据同一条)\n" + slotOwner : ""}
1025
+ // 一面只显一张:score 最小的那张(不论多远,绝不留空面)
1026
+ ${slotOne}
1027
+ if (wsum > 0.0) acc /= wsum; // 按总权重归一化(总混合值=1)
1028
+ // 覆盖度:有候选即 1(09-25 晚起不再有「渐隐/露灰」的中间态),只在「一个候选都没有」时 = 0
1029
+ // ⇒ 露出地板/墙体本来的底色。
1030
+ float coverage = clamp(cover, 0.0, 1.0);
1031
+ // 整体透明度仅作用于全景图映射:把覆盖度按 uFloorOpacity 缩放后再混合,
1032
+ // 灰底始终不透明(alpha=1),opacity 越小全景越淡、越透出地板灰色;地板灰色本身不变。
1033
+ float coverageEff = clamp(coverage * uFloorOpacity, 0.0, 1.0);
1034
+ vec3 col = mix(uFloorBaseColor, acc, coverageEff);
1035
+ // 全景投射显隐:关闭时(仅墙体,地板恒为 1)回落中性素色,不投射全景
1036
+ if (uPanoramaVisible < 0.5) {
1037
+ col = vec3(0.78, 0.78, 0.80);
1038
+ }
1039
+ // 实例染色:默认白=原全景色;选中黄(FurnitureObject.highlight 写入 instanceColor)染黄高亮。
1040
+ // 普通 Mesh(墙/地板)无 instanceColor,USE_INSTANCING_COLOR 不定义,此分支被编译剔除。
1041
+ #ifdef USE_INSTANCING_COLOR
1042
+ col *= vInstanceColor;
1043
+ #endif
1044
+ ${PANO_TINT_ON ? ` // 🔬 归属诊断配色(皇上旨 2026-09-25「就在最后算出来的颜色再叠加上颜色,不用想那么复杂」):
1045
+ // 整段只在 ?panoTint=1 时生成,默认关 ⇒ 正常态一行都没有。色相全交给所属景点,只保留明暗,
1046
+ // 所以亮部/暗部仍看得出是原照片的纹理,但一眼能分「这块面归哪个景点」。
1047
+ // ⚠️ 与上一版(逐槽染 linK)的区别:这里染的是**最终颜色** ⇒ 即使那一片压根没有候选(一个全景都没绑上、
1048
+ // 露底色)之外的任何区域也照样按归属上色;归属判据与取图判据同一条 = 「存活口径最近一张」。
1049
+ // ⚠️ 刻意不用「采样色 × 淡色」:那种叠法叠在亮图上几乎发白,肉眼看不出(09-25 已被打回过一次)。
1050
+ // ⚠️ 强度 = PANO_TINT_MIX(皇上旨 09-26 晚「叠加的颜色比例少一点」):原来整块换成色相 ⇒ 只剩明暗、
1051
+ // 底下一张全景图完全读不出来;现在按 mix(原色, 染色, 本值) 叠,色相仍占主但照片看得见了。
1052
+ if (tintCnt > 0.0) {
1053
+ vec3 tintHue = tintAcc / tintCnt;
1054
+ float tintLum = dot(col, vec3(0.2126, 0.7152, 0.0722));
1055
+ vec3 tintTo = tintHue * (0.55 + 0.45 * clamp(tintLum * 3.0, 0.0, 1.0));
1056
+ col = mix(col, tintTo, ${PANO_TINT_MIX});
1057
+ }` : ""}${panoDistOn ? `
1058
+ // 🔬 距离明暗诊断(皇上旨 09-26 晚「距离越小越白、越远越黑」+ 09-27 早「按我有 4 个全景点来写代码」+
1059
+ // 09-27 晚「我一开始的需求就是要把 4 个全景点的坐标传给 shader,按最近取用」):
1060
+ // **一句真循环**遍历 uPanoPoints[0..${maxPano - 1}],每个像素只留「到最近那个景点」的水平距离。
1061
+ // ⚠️ 纯水平距离最近(09-27 晚定版后与取图**同把尺**,夹角门已废)⇒ 墙背后的景点也往这面墙上画圆,
1062
+ // 一个面能看满 N 个渐变圆;代价 = 明暗图不分身前身后(与取图口径一致)。
1063
+ ${panoDistLoopGlsl("col", maxPano)}` : ""}
1064
+ gl_FragColor = vec4(col, 1.0);
1065
+ // 输出颜色空间编码:渲染到线性 RT(主编辑器后处理链)时 passthrough,渲染到 sRGB 屏幕(预览弹窗)时自动编码 sRGB
1066
+ #include <colorspace_fragment>
1067
+ #include <logdepthbuf_fragment>
1068
+ }
1069
+ `;
1070
+ }
1071
+ /** 融合材质的全景点数**上限**:每点占 **2** 张拼合槽纹理(32 片 8×4 → 左右 2 槽,每槽 4×4 格)⇒ **8 × 2 = 16 个 sampler**,再多样本编译不过。
1072
+ * ⭐ 2026-10-06 晚皇上旨「一次能够展示 8 个全景纹理」:靠的就是「每点从 4 槽降到 2 槽」——
1073
+ * sampler 总数 16 是硬件硬顶(皇上本机 `MAX_TEXTURE_IMAGE_UNITS` 实测 16,一直没变),
1074
+ * 原来 16 ÷ 4 槽 = 4 个景点,现在 16 ÷ 2 槽 = **8 个景点**,画质与单全景显存**分毫未动**。 */
1075
+ export const MKT_PANO_MAX_SLOTS = 8;
1076
+ /**
1077
+ * 创建「多全景融合」着色器材质:每帧由外部喂入最近 maxPano 个全景点(⭐ 10-06 晚旨后每点 2 张拼合槽纹理)。
1078
+ * 同一份着色器同时服务普通 Mesh(墙/地板)与 InstancedMesh(家具立方体):
1079
+ * 实例化与否由 three.js 按对象类型自动注入 USE_INSTANCING 宏决定,无需外部传 instanced 开关。
1080
+ * 数组 uniform 在创建时即按 maxPano 长度初始化(占位纹理 + 单位矩阵 + 权重 0),运行时由渲染器每帧改写对应槽位。
1081
+ * ⚠️ 取图口径只有一条(皇上旨 2026-09-25 晚「都要一起改」→ 09-27 晚「取用距离最近的那个全景点的瓦片纹理」定版):
1082
+ * 地板 / 墙 / 家具**全部**按「水平距离最近的一张」取图(09-24~09-25 的夹角门已整链删除),
1083
+ * 不再有「地板按覆盖度融合」这条路,故也**没有** `pickNearest` 选项(旧的 0/1 分流 uniform 已删)。
1084
+ */
1085
+ export function createMktFloorBlendShaderMaterial(options = {}) {
1086
+ // 2026-09-20:每个全景点占 N 张纹理槽,故「最多融合点数」maxPano 与纹理上限挂钩:
1087
+ // maxPano × N 张纹理须 ≤ 16(shader sampler 上限)。
1088
+ // 2026-09-24 皇上旨「同一时间要准备 4 个全景的瓦片纹理,一共刚好 16 个」(当时 N=4 ⇒ 上限 4)。
1089
+ // ⭐ 2026-10-06 晚旨「一次能够展示 8 个全景纹理」:N 从 4 降到 **2**(每个全景只占左右两张 RT)
1090
+ // ⇒ 16 ÷ 2 = **上限 8 个景点**,默认就取满 8。画质 / 单全景显存**分毫未动**。
1091
+ // ⚠️ 10-06 切 32 片之后这两个槽各是「16 片拼出来的一张 4096² RT」,sampler 总数仍是 16(硬顶)。
1092
+ // ⚠️ 所有喂同一批数据的材质必须同档:某处写成 3 而喂料方按 8 喂,会往 panos[6..15] 越界写、着色器读到 undefined sampler。
1093
+ const maxPano = Math.min(options.maxPano ?? MKT_PANO_MAX_SLOTS, MKT_PANO_MAX_SLOTS);
1094
+ // 🔬 这一份材质是否带「近白远黑」图:显式传了听他的(编辑器),没传读 URL(发布页/预览默认关)。
1095
+ const panoDistOn = options.panoDist ?? PANO_DIST_ON;
1096
+ const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 总纹理槽数 = 点数 × 每点槽数(8 × 2 = 16 = sampler 硬顶)
1097
+ const panoramas = [];
1098
+ const matrices = [];
1099
+ const weights = new Float32Array(maxPano); // 默认全 0(未参与)
1100
+ for (let i = 0; i < tileCount; i++) {
1101
+ panoramas.push(createPlaceholderTexture());
1102
+ }
1103
+ for (let i = 0; i < maxPano; i++) {
1104
+ matrices.push(new Matrix4());
1105
+ }
1106
+ return new ShaderMaterial({
1107
+ uniforms: {
1108
+ panoramas: { value: panoramas },
1109
+ textureMatrices: { value: matrices },
1110
+ weights: { value: weights },
1111
+ // 地板底色(线性空间深灰,约 sRGB 0.27):远处露出此色
1112
+ uFloorBaseColor: { value: new Vector3(0.06, 0.06, 0.06) },
1113
+ // 🔬 归属诊断色(每槽一个 vec3 = 该槽里那个景点的固定色):**只在 ?panoTint=1 时创建**,
1114
+ // 关态连 uniform 都不存在(JS 侧三元)⇒ 线上正常路径零开销。默认白 = 未被应用层写过。
1115
+ ...(PANO_TINT_ON
1116
+ ? { uPanoTintColors: { value: Array.from({ length: maxPano }, () => new Vector3(1, 1, 1)) } }
1117
+ : {}),
1118
+ // 🔬 距离明暗诊断的「黑端米数」:**这份材质 panoDist 为真才创建**(默认 16 ≈ 常见房间对角线,
1119
+ // 应用层每次喂投影时按现场量出的尺子写实际值,见 setMktPanoDistRange);
1120
+ // uPanoPoints = 同一张图的**圆心清单**(每格一个 vec4,w=1 表示这格有个全景点,见 setMktPanoPoints)
1121
+ ...(panoDistOn ? {
1122
+ uPanoDistMax: { value: 16 },
1123
+ uPanoPoints: { value: Array.from({ length: maxPano }, () => new Vector4(0, 0, 0, 0)) },
1124
+ } : {}),
1125
+ // ⚠️ 下面两根覆盖半径:着色器自 09-25 晚「都要一起改」起**不再读它们**(覆盖度融合那一路已删),
1126
+ // uniform 与应用层写入暂留 ⇒ 面板上那两根旋钮调了没有视觉变化,删不删等皇上旨意。
1127
+ uFloorCoreRadius: { value: 4 },
1128
+ uFloorFadeRange: { value: 4 },
1129
+ // 整体透明度(0~1):仅缩放全景图映射的可见度(覆盖度×此值),地板灰底不变;全局对所有全景一起生效;1=全景完全可见
1130
+ uFloorOpacity: { value: 1 },
1131
+ // 全景投射显隐:1=显示全景,0=仅显示素色(墙体回落中性色,地板恒为 1)
1132
+ uPanoramaVisible: { value: 1 },
1133
+ // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 已随那段 pow 分支整条撤除
1134
+ // 全景上下方向**已写死**,无 uniform(见文件头 🎛️ 段)。
1135
+ // 每点各自的「这 2 槽是瓦片(1)/同一张整图(0)」,由喂料处按「PanoTexMode + 真实 tiles」写(setMktPanoTiled);
1136
+ // 默认全 1 = 维持改造前的瓦片口径(真实上传的图都是四瓦片)。
1137
+ uPanoTiled: { value: new Float32Array(maxPano).fill(1) },
1138
+ // 投影空间逆矩阵:默认单位阵(= 世界坐标,行为与改造前一致)。
1139
+ // 融合材质默认**永不**写入它 —— 编辑器地板/立方体、预览立方体因此完全不受 MktMesh 变换影响。
1140
+ uProjInvMatrix: { value: new Matrix4() },
1141
+ },
1142
+ vertexShader: floorVertexShader,
1143
+ fragmentShader: buildFloorBlendFragment(maxPano, panoDistOn),
1144
+ side: DoubleSide,
1145
+ transparent: false,
1146
+ toneMapped: false,
1147
+ });
1148
+ }
1149
+ /**
1150
+ * 「仅显示选中全景」片元(皇上旨 2026-09-29「三模式三 shader」之三):
1151
+ * 整面恒显示 **0 号点槽**那一张全景 —— 不看距离、不看夹角、不看序号,槽里是谁就投谁
1152
+ * (喂料侧保证 0 号槽 = 当前选中的全景、其余槽留空)。
1153
+ * uniform 形状与 blend 同档 ⇒ 喂料复用 feedBlendProjection 槽位指派路零改动;
1154
+ * GLSL 只读 0 号点槽,其余槽是声明了没人采的死重(开销可忽略)。
1155
+ * 槽没绑好(weights[0]≤0,选中全景瓦片还在路上 / 没选中任何全景)→ 露 uFloorBaseColor 深灰底。
1156
+ */
1157
+ function buildFloorSelectedFragment(maxPano) {
1158
+ const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 形状与 blend 同档(喂料共用)
1159
+ return /* glsl */ `
1160
+ #include <logdepthbuf_pars_fragment>
1161
+ uniform sampler2D panoramas[${tileCount}]; // 形状与 blend 同档(喂料共用);本 shader 只采 0 号点槽(纹理 0..1)
1162
+ uniform mat4 textureMatrices[${maxPano}];
1163
+ uniform float weights[${maxPano}]; // 0 号槽 > 0 = 选中全景的瓦片已绑好
1164
+ uniform float uPanoTiled[${maxPano}];
1165
+ uniform vec3 uFloorBaseColor; // 槽没绑好时露出的深灰底(线性空间,约 sRGB 0.27)
1166
+ uniform float uFloorOpacity; // 整体透明度(与 blend 同口径:只缩放全景可见度,底色不变)
1167
+ // 色彩空间只有一条路(10-07 旨「统一使用 liner」):原 uniform float uColorSpace; + 那段 pow 分支已整条撤除(与 blend 同口径)
1168
+ uniform float uPanoramaVisible;
1169
+ varying vec3 vProjPos;
1170
+ #ifdef USE_INSTANCING_COLOR
1171
+ varying vec3 vInstanceColor;
1172
+ #endif
1173
+ void main() {
1174
+ // ── 0 号点槽(纹理 0..1,即左右那 2 张拼合槽):选中的全景独占(采样段与 blend 逐字同口径)──
1175
+ vec3 lin0 = vec3(0.0);
1176
+ if (weights[0] > 0.0) {
1177
+ mat4 tm = textureMatrices[0];
1178
+ vec3 texPos = tm[3].xyz;
1179
+ mat3 texRot = mat3(tm);
1180
+ vec3 dir = normalize(vProjPos - texPos);
1181
+ dir = texRot * dir;
1182
+ float lon = atan(dir.z, dir.x);
1183
+ float u = lon * 0.15915494309189535 + 0.5; // 经度 -> u
1184
+ float lat = asin(clamp(dir.y, -1.0, 1.0));
1185
+ float v = 0.5 + lat * 0.3183098861837907; // 1/PI(等距柱状线性 v,与 blend 逐字同口径)
1186
+ // ⭐ 10-06 晚旨:竖着不再分块 ⇒ 槽只随 u 走(左右两半:[左半幅=0, 右半幅=1]),与 blend 逐字同口径
1187
+ int tileIdx = (u >= 0.5) ? 1 : 0;
1188
+ float localU = (u < 0.5) ? (u * 2.0) : (u * 2.0 - 1.0);
1189
+ // ⛔ 纵向不再减半(槽就是全高)⇒ localV 恒等于 v
1190
+ float localV = v;
1191
+ float panoU = (uPanoTiled[0] > 0.5) ? localU : u;
1192
+ float panoV = (uPanoTiled[0] > 0.5) ? localV : v;
1193
+ // ⭐ 采样即线性(与单全景 / blend 那两路同一条口径,10-07 旨「色彩空间统一使用 liner」):纹理都标了
1194
+ // colorSpace = SRGBColorSpace ⇒ 硬件上传成 SRGB8_ALPHA8、texture2D 已解码一次;
1195
+ // 这里**不再手动 pow 解码**(那遍是二次解码 = 整体压暗),末端只编码一次 ⇒ 与源图逐字节还原。
1196
+ if (tileIdx == 0) lin0 = texture2D(panoramas[0], vec2(panoU, panoV)).rgb;
1197
+ else lin0 = texture2D(panoramas[1], vec2(panoU, panoV)).rgb;
1198
+ }
1199
+ float coverage = weights[0] > 0.0 ? 1.0 : 0.0;
1200
+ float coverageEff = clamp(coverage * uFloorOpacity, 0.0, 1.0);
1201
+ vec3 col = mix(uFloorBaseColor, lin0, coverageEff);
1202
+ // 全景投射显隐:关闭时回落中性素色(与 blend 一致)
1203
+ if (uPanoramaVisible < 0.5) col = vec3(0.78, 0.78, 0.80);
1204
+ #ifdef USE_INSTANCING_COLOR
1205
+ col *= vInstanceColor;
1206
+ #endif
1207
+ gl_FragColor = vec4(col, 1.0);
1208
+ #include <colorspace_fragment>
1209
+ #include <logdepthbuf_fragment>
1210
+ }
1211
+ `;
1212
+ }
1213
+ /**
1214
+ * 创建「仅显示选中全景」着色器材质(皇上旨 2026-09-29「三模式三 shader」之三):仅编辑器使用。
1215
+ * uniform 形状与 blend 工厂同档 ⇒ 喂料复用 feedBlendProjection 槽位指派路零改动;
1216
+ * 本 shader 只读 0 号点槽(GLSL 写死),uFloorCoreRadius/uFloorFadeRange 喂料会写但不读(静默忽略)。
1217
+ */
1218
+ export function createMktFloorSelectedShaderMaterial(options = {}) {
1219
+ const maxPano = Math.min(options.maxPano ?? MKT_PANO_MAX_SLOTS, MKT_PANO_MAX_SLOTS);
1220
+ const tileCount = maxPano * RT_STITCH_SLOT_COUNT; // 与 blend / mapping 同档(8 点 × 2 槽 = 16)
1221
+ const panoramas = [];
1222
+ const matrices = [];
1223
+ for (let i = 0; i < tileCount; i++)
1224
+ panoramas.push(createPlaceholderTexture());
1225
+ for (let i = 0; i < maxPano; i++)
1226
+ matrices.push(new Matrix4());
1227
+ return new ShaderMaterial({
1228
+ uniforms: {
1229
+ panoramas: { value: panoramas },
1230
+ textureMatrices: { value: matrices },
1231
+ weights: { value: new Float32Array(maxPano) },
1232
+ uPanoTiled: { value: new Float32Array(maxPano).fill(1) },
1233
+ uFloorBaseColor: { value: new Vector3(0.06, 0.06, 0.06) },
1234
+ // 喂料统一入口会写;本 shader 不读 uFloorCoreRadius/uFloorFadeRange(留 JS 项 = 喂料零分支)
1235
+ uFloorCoreRadius: { value: 4 },
1236
+ uFloorFadeRange: { value: 4 },
1237
+ uFloorOpacity: { value: 1 },
1238
+ uPanoramaVisible: { value: 1 },
1239
+ // 色彩空间只有一条路(10-07 旨「统一使用 liner」)⇒ 原 `uColorSpace: { value: 0 }` 已随那段 pow 分支整条撤除
1240
+ uProjInvMatrix: { value: new Matrix4() },
1241
+ },
1242
+ vertexShader: floorVertexShader,
1243
+ fragmentShader: buildFloorSelectedFragment(maxPano),
1244
+ side: DoubleSide,
1245
+ transparent: false,
1246
+ toneMapped: false,
1247
+ });
1248
+ }