three-base 2.0.137 → 2.0.142
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.
- package/DirectionalLight.d.ts +6 -6
- package/DirectionalLight.js +68 -68
- package/DirectionalLight.ts +70 -0
- package/EventMap.d.ts +103 -0
- package/EventMap.js +17 -0
- package/EventMap.ts +139 -0
- package/EventMesh.d.ts +18 -18
- package/EventMesh.js +59 -59
- package/EventMesh.ts +60 -0
- package/ExtrudeMesh.d.ts +112 -112
- package/ExtrudeMesh.js +342 -366
- package/ExtrudeMesh.ts +553 -0
- package/FPScontrol.d.ts +511 -78
- package/FPScontrol.js +1799 -617
- package/FPScontrol.ts +1855 -0
- package/Fire/Fire.d.ts +28 -28
- package/Fire/Fire.js +80 -79
- package/Fire/Fire.ts +89 -0
- package/Fire/{FireGromtry.d.ts → FireGeometry.d.ts} +9 -9
- package/Fire/{FireGromtry.js → FireGeometry.js} +40 -40
- package/Fire/FireGeometry.ts +40 -0
- package/Fire/FireMaterial.d.ts +28 -28
- package/Fire/FireMaterial.js +32 -32
- package/Fire/FireMaterial.ts +89 -0
- package/Fire/index.d.ts +2 -1
- package/Fire/index.js +2 -1
- package/Fire/index.ts +2 -0
- package/GLTFDracoLoader.d.ts +71 -0
- package/GLTFDracoLoader.js +266 -0
- package/GLTFDracoLoader.ts +430 -0
- package/Grass/Grass.d.ts +8 -8
- package/Grass/Grass.js +38 -38
- package/Grass/Grass.ts +38 -0
- package/Grass/index.d.ts +2 -1
- package/Grass/index.js +2 -1
- package/Grass/index.ts +2 -0
- package/Ground1/Ground1.d.ts +10 -11
- package/Ground1/Ground1.js +95 -91
- package/Ground1/Ground1.ts +114 -0
- package/Ground1/index.d.ts +1 -1
- package/Ground1/index.js +1 -1
- package/Ground1/index.ts +1 -0
- package/MeshBase.d.ts +36 -35
- package/MeshBase.js +247 -223
- package/MeshBase.ts +253 -0
- package/Mirror.d.ts +25 -25
- package/Mirror.js +143 -139
- package/Mirror.ts +203 -0
- package/MktMesh/MktMesh.d.ts +291 -0
- package/MktMesh/MktMesh.js +1045 -0
- package/MktMesh/MktMesh.ts +1075 -0
- package/MktMesh/assetUrl.d.ts +35 -0
- package/MktMesh/assetUrl.js +69 -0
- package/MktMesh/assetUrl.ts +69 -0
- package/MktMesh/mktMappingShader.d.ts +12 -0
- package/MktMesh/mktMappingShader.js +139 -0
- package/MktMesh/mktMappingShader.ts +139 -0
- package/MktMesh/mktShader.d.ts +263 -0
- package/MktMesh/mktShader.js +1248 -0
- package/MktMesh/mktShader.ts +1344 -0
- package/MktMesh/panoBitmapTexture.d.ts +25 -0
- package/MktMesh/panoBitmapTexture.js +52 -0
- package/MktMesh/panoBitmapTexture.ts +54 -0
- package/MktMesh/panoSlotBind.d.ts +29 -0
- package/MktMesh/panoSlotBind.js +106 -0
- package/MktMesh/panoSlotBind.ts +153 -0
- package/MktMesh/panoSlotRoster.d.ts +22 -0
- package/MktMesh/panoSlotRoster.js +169 -0
- package/MktMesh/panoSlotRoster.ts +168 -0
- package/MktMesh/panoTileStitcher.d.ts +119 -0
- package/MktMesh/panoTileStitcher.js +533 -0
- package/MktMesh/panoTileStitcher.ts +616 -0
- package/MktMesh/types.d.ts +75 -0
- package/MktMesh/types.js +1 -0
- package/MktMesh/types.ts +87 -0
- package/MktMesh/wallCapTessellate.d.ts +33 -0
- package/MktMesh/wallCapTessellate.js +97 -0
- package/MktMesh/wallCapTessellate.ts +68 -0
- package/PipeMesh/PathGeometry.d.ts +44 -44
- package/PipeMesh/PathGeometry.js +307 -300
- package/PipeMesh/PathGeometry.ts +343 -0
- package/PipeMesh/PathPoint.d.ts +17 -17
- package/PipeMesh/PathPoint.js +31 -31
- package/PipeMesh/PathPoint.ts +36 -0
- package/PipeMesh/PathPointList.d.ts +31 -31
- package/PipeMesh/PathPointList.js +209 -208
- package/PipeMesh/PathPointList.ts +227 -0
- package/PipeMesh/PathTubeGeometry.d.ts +25 -25
- package/PipeMesh/PathTubeGeometry.js +130 -130
- package/PipeMesh/PathTubeGeometry.ts +146 -0
- package/PipeMesh/PipeMesh.d.ts +25 -25
- package/PipeMesh/PipeMesh.js +63 -63
- package/PipeMesh/PipeMesh.ts +76 -0
- package/PipeMesh/index.d.ts +1 -1
- package/PipeMesh/index.js +1 -1
- package/PipeMesh/index.ts +1 -0
- package/PlaneModal1/PlaneModal1.d.ts +34 -31
- package/PlaneModal1/PlaneModal1.js +109 -105
- package/PlaneModal1/PlaneModal1.ts +138 -0
- package/PlaneModal1/index.d.ts +1 -1
- package/PlaneModal1/index.js +1 -1
- package/PlaneModal1/index.ts +1 -0
- package/PlaneText.d.ts +27 -39
- package/PlaneText.js +35 -30
- package/PlaneText.ts +55 -0
- package/PlaneWithHtml.d.ts +13 -11
- package/PlaneWithHtml.js +64 -63
- package/PlaneWithHtml.ts +70 -0
- package/PointLight/PointLight.d.ts +13 -14
- package/PointLight/PointLight.js +64 -70
- package/PointLight/PointLight.ts +67 -0
- package/PointLight/index.d.ts +1 -1
- package/PointLight/index.js +1 -1
- package/PointLight/index.ts +1 -0
- package/README.md +110 -79
- package/Sky/Sky.d.ts +9 -9
- package/Sky/Sky.js +22 -22
- package/Sky/Sky.ts +22 -0
- package/Sky/index.d.ts +1 -1
- package/Sky/index.js +1 -1
- package/Sky/index.ts +1 -0
- package/SpotLight.d.ts +6 -6
- package/SpotLight.js +72 -72
- package/SpotLight.ts +74 -0
- package/SpriteModal1/SpriteModal1.d.ts +33 -30
- package/SpriteModal1/SpriteModal1.js +116 -110
- package/SpriteModal1/SpriteModal1.ts +146 -0
- package/SpriteModal1/index.d.ts +1 -1
- package/SpriteModal1/index.js +1 -1
- package/SpriteModal1/index.ts +1 -0
- package/SpriteText.d.ts +46 -45
- package/SpriteText.js +176 -163
- package/SpriteText.ts +205 -0
- package/SpriteText2.d.ts +48 -47
- package/SpriteText2.js +223 -198
- package/SpriteText2.ts +258 -0
- package/SpriteText3.d.ts +86 -0
- package/SpriteText3.js +519 -0
- package/SpriteText3.ts +599 -0
- package/SpriteWithHtml.d.ts +9 -7
- package/SpriteWithHtml.js +44 -44
- package/SpriteWithHtml.ts +50 -0
- package/Text3D/Text3D.d.ts +20 -20
- package/Text3D/Text3D.js +58 -62
- package/Text3D/Text3D.ts +77 -0
- package/Text3D/fontController.d.ts +60 -59
- package/Text3D/fontController.js +166 -199
- package/Text3D/fontController.ts +209 -0
- package/Text3D/index.d.ts +1 -1
- package/Text3D/index.js +1 -1
- package/Text3D/index.ts +1 -0
- package/ThreeBase.d.ts +248 -111
- package/ThreeBase.js +1202 -656
- package/ThreeBase.ts +1314 -0
- package/ThreeMouse.d.ts +54 -40
- package/ThreeMouse.js +453 -407
- package/ThreeMouse.ts +472 -0
- package/Transition.d.ts +45 -40
- package/Transition.js +209 -210
- package/Transition.ts +242 -0
- package/Water/Water.d.ts +5 -5
- package/Water/Water.js +44 -26
- package/Water/Water.ts +45 -0
- package/Water/index.d.ts +2 -0
- package/Water/index.js +2 -0
- package/Water/index.ts +2 -0
- package/WaterFlow/FloatingDuck.d.ts +88 -0
- package/WaterFlow/FloatingDuck.js +307 -0
- package/WaterFlow/FloatingDuck.ts +445 -0
- package/WaterFlow/GPUFluidSimulation.d.ts +56 -0
- package/WaterFlow/GPUFluidSimulation.js +785 -0
- package/WaterFlow/GPUFluidSimulation.ts +827 -0
- package/WaterFlow/ReservoirSystem.d.ts +106 -0
- package/WaterFlow/ReservoirSystem.js +621 -0
- package/WaterFlow/ReservoirSystem.ts +754 -0
- package/WaterFlow/SPHField.d.ts +35 -0
- package/WaterFlow/SPHField.js +204 -0
- package/WaterFlow/SPHField.ts +228 -0
- package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -0
- package/WaterFlow/ScreenSpaceFluidPass.js +736 -0
- package/WaterFlow/ScreenSpaceFluidPass.ts +789 -0
- package/WaterFlow/ShallowWaterSurface.d.ts +45 -0
- package/WaterFlow/ShallowWaterSurface.js +441 -0
- package/WaterFlow/ShallowWaterSurface.ts +510 -0
- package/WaterFlow/WaterFlow.d.ts +87 -0
- package/WaterFlow/WaterFlow.js +171 -0
- package/WaterFlow/WaterFlow.ts +219 -0
- package/WaterFlow/types.d.ts +161 -0
- package/WaterFlow/types.js +1 -0
- package/WaterFlow/types.ts +189 -0
- package/WaterFlow/waterFlowPass.d.ts +3 -0
- package/WaterFlow/waterFlowPass.js +20 -0
- package/WaterFlow/waterFlowPass.ts +40 -0
- package/collisionOBB.d.ts +42 -0
- package/collisionOBB.js +112 -0
- package/collisionOBB.ts +108 -0
- package/declare.d.ts +23 -165
- package/index.d.ts +1 -1
- package/index.js +1 -2
- package/index.ts +1 -0
- package/instanced/InstancedBatch.d.ts +57 -0
- package/instanced/InstancedBatch.js +348 -0
- package/instanced/InstancedBatch.ts +406 -0
- package/instanced/InstancedChild.d.ts +174 -0
- package/instanced/InstancedChild.js +264 -0
- package/instanced/InstancedChild.ts +327 -0
- package/instanced/InstancedGroup.d.ts +43 -0
- package/instanced/InstancedGroup.js +126 -0
- package/instanced/InstancedGroup.ts +137 -0
- package/{InstancedMeshBase.d.ts → instanced/InstancedMeshBase.d.ts} +38 -39
- package/{InstancedMeshBase.js → instanced/InstancedMeshBase.js} +282 -293
- package/instanced/InstancedMeshBase.ts +292 -0
- package/instanced/InstancedTemplate.d.ts +46 -0
- package/instanced/InstancedTemplate.js +125 -0
- package/instanced/InstancedTemplate.ts +188 -0
- package/keyControls.d.ts +30 -0
- package/{keyContorls.js → keyControls.js} +101 -98
- package/keyControls.ts +107 -0
- package/lensflare/LensFlare.d.ts +39 -0
- package/lensflare/LensFlare.js +373 -0
- package/lensflare/LensFlare.ts +448 -0
- package/lensflare/lensflare0.png +0 -0
- package/lensflare/lensflare3.png +0 -0
- package/lensflare/lensflare_custom.png +0 -0
- package/lensflare/lensflare_hex_ghost.png +0 -0
- package/lensflare/lensflare_hex_main.png +0 -0
- package/package.json +99 -25
- package/pass/bloom_pass.d.ts +2 -2
- package/pass/bloom_pass.js +188 -101
- package/pass/bloom_pass.ts +243 -0
- package/pass/cover_pass.d.ts +2 -2
- package/pass/cover_pass.js +122 -99
- package/pass/cover_pass.ts +182 -0
- package/pass/debugDepthState.d.ts +20 -0
- package/pass/debugDepthState.js +9 -0
- package/pass/debugDepthState.ts +22 -0
- package/pass/depthTestBias.d.ts +12 -0
- package/pass/depthTestBias.js +12 -0
- package/pass/depthTestBias.ts +12 -0
- package/pass/effectUserData.d.ts +27 -0
- package/pass/effectUserData.js +1 -0
- package/pass/effectUserData.ts +46 -0
- package/pass/frosted_glass_pass.d.ts +2 -2
- package/pass/frosted_glass_pass.js +167 -186
- package/pass/frosted_glass_pass.ts +288 -0
- package/pass/fsQuad.d.ts +9 -9
- package/pass/fsQuad.js +28 -28
- package/pass/fsQuad.ts +34 -0
- package/pass/glow_pass.d.ts +2 -2
- package/pass/glow_pass.js +449 -233
- package/pass/glow_pass.ts +654 -0
- package/pass/screen_bloom_pass.d.ts +11 -0
- package/pass/screen_bloom_pass.js +235 -0
- package/pass/screen_bloom_pass.ts +263 -0
- package/pass/ssao_pass.d.ts +88 -13
- package/pass/ssao_pass.js +802 -366
- package/pass/ssao_pass.ts +918 -0
- package/pass/stroke_pass.d.ts +2 -2
- package/pass/stroke_pass.js +329 -258
- package/pass/stroke_pass.ts +638 -0
- package/shaders/FXAAShaderMaterial.d.ts +3 -3
- package/shaders/FXAAShaderMaterial.js +23 -66
- package/shaders/FXAAShaderMaterial.ts +201 -0
- package/shaders/baseDepthVisShader.d.ts +11 -0
- package/shaders/baseDepthVisShader.js +36 -0
- package/shaders/baseDepthVisShader.ts +36 -0
- package/shaders/baseTextureToRenderShader.d.ts +2 -2
- package/shaders/baseTextureToRenderShader.js +55 -60
- package/shaders/baseTextureToRenderShader.ts +232 -0
- package/shaders/copyShader.d.ts +3 -3
- package/shaders/copyShader.js +13 -14
- package/shaders/copyShader.ts +32 -0
- package/shaders/debugDepthCompareShader.d.ts +14 -0
- package/shaders/debugDepthCompareShader.js +49 -0
- package/shaders/debugDepthCompareShader.ts +50 -0
- package/shaders/debugDepthJudgeShader.d.ts +18 -0
- package/shaders/debugDepthJudgeShader.js +52 -0
- package/shaders/debugDepthJudgeShader.ts +54 -0
- package/shaders/debugExcludedShader.d.ts +13 -0
- package/shaders/debugExcludedShader.js +46 -0
- package/shaders/debugExcludedShader.ts +48 -0
- package/spriteTextBg.d.ts +1 -0
- package/spriteTextBg.js +20 -0
- package/spriteTextBg.ts +20 -0
- package/threeLoader.d.ts +35 -34
- package/threeLoader.js +518 -527
- package/threeLoader.ts +525 -0
- package/threeUtils.d.ts +53 -52
- package/threeUtils.js +329 -330
- package/threeUtils.ts +373 -0
- package/types.d.ts +2 -0
- package/types.js +6 -0
- package/types.ts +8 -0
- package/ApngTexture.d.ts +0 -13
- package/ApngTexture.js +0 -61
- package/ApngTextureManage.d.ts +0 -18
- package/ApngTextureManage.js +0 -87
- package/KeyFrameAnimation.d.ts +0 -25
- package/KeyFrameAnimation.js +0 -78
- package/keyContorls.d.ts +0 -25
- package/lib/draco/draco_decoder.js +0 -34
- package/lib/draco/draco_decoder.wasm +0 -0
- package/lib/draco/draco_wasm_wrapper.js +0 -117
- package/pass/emissive_ao_pass.d.ts +0 -13
- package/pass/emissive_ao_pass.js +0 -370
- package/shaders.d.ts +0 -5
- package/shaders.js +0 -93
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author: xiaosihan
|
|
3
|
+
* @Date: 2026-08-25
|
|
4
|
+
* @Description: MktMesh —— 墨卡托模型(普通 Mesh + 墨卡托投影全景 + 由点位挤出生成的墙体)。
|
|
5
|
+
*
|
|
6
|
+
* 定位:继承 THREE.Mesh(普通网格,不再用 InstancedMesh)。
|
|
7
|
+
* 墙面几何由一组「地面折线点位」(wallPoints,XZ 平面)通过挤出生成:
|
|
8
|
+
* - 墙体侧面:复用 three-base 的 ExtrudeMesh(shapePath = [[0,0],[0,墙高]] 沿点位挤出一道幕墙);
|
|
9
|
+
* - 墙体上/下两个多边形封盖:用 three.js 自带的 ShapeGeometry 由点位三角化生成。
|
|
10
|
+
* 墨卡托投影全景纹理(panorama)实时映射到墙面上:每帧按槽位名册(中心 ± 邻居)决定 shader 那 8 格装哪 8 张。
|
|
11
|
+
*
|
|
12
|
+
* 核心特性:
|
|
13
|
+
* 1. 墙面 = 普通 Mesh 几何(世界坐标),MktMesh 自身保持单位变换;
|
|
14
|
+
* 2. 纹理列表 textures:每个纹理含 url + 世界矩阵(位置=墨卡托投影中心,旋转=朝向);
|
|
15
|
+
* 3. 默认每帧(onBeforeRender)按「离**名册锚点**最近的水平距离(XZ)」算出**槽位名册**(中心 ± 邻居,见
|
|
16
|
+
* `panoSlotRoster.ts`;皇上旨 2026-10-03 起预览/发布与编辑器共用同一份算法 ⇒ 两边装同一批全景)。
|
|
17
|
+
* ⭐ 皇上旨 2026-10-07 第二十一道旨:锚点 = 外部控制器的中心点(`setRosterCenter` 注入),没注入才回落相机位置;
|
|
18
|
+
* 中心变化或名册里有空格被补上人时加载新瓦片 → 设置 shader 的 8 个全景槽 panorama + textureMatrix;
|
|
19
|
+
* 4. exportData / importData 支持导入导出(墙点位 + 纹理数组)。
|
|
20
|
+
* 5. ⚠️ 铁律:激活纹理(墙面上正在绘制的全景)的旋转/位置变化,必须立即同步到
|
|
21
|
+
* shader 的 textureMatrix uniform(见 setTexture),否则平面纹理不跟着全景球转。
|
|
22
|
+
*
|
|
23
|
+
* 说明:所有实例共享同一份 material(单一 panorama uniform),
|
|
24
|
+
* 同一时刻只激活名册 0 号槽(中心)那一个纹理(全局共享单激活方案)。
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
BufferAttribute,
|
|
28
|
+
BufferGeometry,
|
|
29
|
+
Camera,
|
|
30
|
+
BackSide,
|
|
31
|
+
Float32BufferAttribute,
|
|
32
|
+
Material,
|
|
33
|
+
Matrix4,
|
|
34
|
+
Side,
|
|
35
|
+
Mesh,
|
|
36
|
+
Shape,
|
|
37
|
+
ShapeGeometry,
|
|
38
|
+
ShaderMaterial,
|
|
39
|
+
SRGBColorSpace,
|
|
40
|
+
Texture,
|
|
41
|
+
Vector3,
|
|
42
|
+
} from "three";
|
|
43
|
+
import type { WebGLRenderer } from "three";
|
|
44
|
+
import { createPlaceholderTexture, MKT_PANO_MAX_SLOTS, isPanoTexMode, mktPanoTiledFlag, mktPanoTexUrls, isRtStitchKey, rtStitchKeyParts, RT_STITCH_KEY_PREFIX, RT_STITCH_SLOT_COUNT, type PanoTexMode } from "./mktShader";
|
|
45
|
+
import { createMktFloorMappingShaderMaterial } from "./mktMappingShader";
|
|
46
|
+
// ⭐ 皇上旨 2026-10-06:拼合 = shader 逐格画进 WebGLRenderTarget,**一份实现**(canvas 2D 那一份连同
|
|
47
|
+
// `?panoStitch=canvas` 开关整条删除)
|
|
48
|
+
import { PanoTileStitcher, type PanoStitcher } from "./panoTileStitcher";
|
|
49
|
+
import { panoSlotRosterStep, PANO_SLOT_SWAP_MARGIN_M } from "./panoSlotRoster";
|
|
50
|
+
// ⭐ 甲(皇上旨 2026-10-05「你试试甲」):整图/瓦片的下载与解码一律走这里(解码在浏览器工作线程,
|
|
51
|
+
// 不再让上传那一拍的 `texImage2D(<img>)` 替我们在主线程同步解 270MB)。编辑态同一份实现。
|
|
52
|
+
import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
|
|
53
|
+
// ⭐ 皇上旨 2026-10-03「整套搬进库(彻底同源)」:「一个点槽的左右 2 张拼合槽纹理怎么换绑」只有这一份策略实现,
|
|
54
|
+
// 编辑态 `mktMeshEditRenderer._bindSlotTiles` 与库侧都只「委托一行」+ 注入各自取图口(⛔ 不许有第二份)。
|
|
55
|
+
import { panoSlotBind, panoSlotTileKeys } from "./panoSlotBind";
|
|
56
|
+
import threeUtils from "../threeUtils";
|
|
57
|
+
import ExtrudeMesh from "../ExtrudeMesh";
|
|
58
|
+
import type { MktMeshData, MktTextureInfo, WallPoint, WallSide } from "./types";
|
|
59
|
+
|
|
60
|
+
// ⭐ 皇上旨 2026-10-07「上下封口的三角面能不能不要那么长 / 三角面再细分一下」:封盖三角化之后
|
|
61
|
+
// 再按**最长边二分**切细(算法本体在 `./wallCapTessellate`,零依赖纯函数 ⇒ 护栏能单独真跑)。
|
|
62
|
+
import { subdivideCapTriangles, WALL_CAP_MAX_EDGE_M } from "./wallCapTessellate";
|
|
63
|
+
// ⭐ 皇上旨 2026-10-08「json 里面的图片的地址,把域名去掉,只保留相对的根路径」:
|
|
64
|
+
// 导出时剥域名(`exportData`)、导入时补回来(`importData`)——**各只有这一处**,⛔ 不许在别处另剥/另补。
|
|
65
|
+
import { stripAssetHost, absAssetUrl } from "./assetUrl";
|
|
66
|
+
|
|
67
|
+
export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
68
|
+
/** 纹理列表(每个纹理的 url + 世界矩阵) */
|
|
69
|
+
textures: MktTextureInfo[] = [];
|
|
70
|
+
/** 当前激活(正在绘制)的纹理索引,-1 表示尚未激活。⭐ 10-03 起该值 = 槽位名册的**中心**(`_slotPanos[0]`),不是「每帧纯最近那张」 */
|
|
71
|
+
activeIndex: number = -1;
|
|
72
|
+
|
|
73
|
+
/** 墙体地面折线点位(XZ 平面,世界坐标) */
|
|
74
|
+
wallPoints: WallPoint[] = [];
|
|
75
|
+
/** 墙体高度(沿 +Y 挤出) */
|
|
76
|
+
wallHeight: number = 3;
|
|
77
|
+
/** 墙体折线是否闭合(首尾相连成环) */
|
|
78
|
+
wallClosed: boolean = true;
|
|
79
|
+
/** 墙体正反面显示(字段保留兼容存档;09-29 晚旨后无论何值材质一律 BackSide,见 _wallSideToThree) */
|
|
80
|
+
wallSide: WallSide = "double";
|
|
81
|
+
/** 是否跳过墙体上/下多边形封盖(true = 只留侧面幕墙)。⚠️ 2026-09-26 核实:本库与编辑态**都取默认 false**
|
|
82
|
+
* (旧注释写「编辑态 MktDevMesh 设为 true」是错的 —— MktDevMesh 明确保留封盖,与发布态一致,
|
|
83
|
+
* 见其类头注释)。封盖与侧面**同一份几何、同一份材质** ⇒ 正反面(material.side)天然同步,
|
|
84
|
+
* 没有「封盖不跟墙体正反面」的第二套口径(这正是皇上 09-26 第①条要查的点)。 */
|
|
85
|
+
skipWallCaps: boolean = false;
|
|
86
|
+
/**
|
|
87
|
+
* ⭐ 皇上旨 2026-10-07「上下封口的三角面能不能不要那么长 / 三角面再细分一下,不要那么大」:
|
|
88
|
+
* 上/下封盖在耳切三角化之后,再按**最长边二分**切到每条边都 ≤ 本值(米)。粒度越细 ⇒
|
|
89
|
+
* 封盖上「一个三角面 = 一张全景」的区块越小,序号映射 / 取图在天花板和地面上分得越准
|
|
90
|
+
* (原先一个耳切大三角能横跨整个房间 ⇒ 整片天花板只能是同一张全景)。
|
|
91
|
+
* ⛔ 0 / 负数 = 不细分(= 10-07 之前的形状)。
|
|
92
|
+
* ⚠️ 改这个值会**改变面数** ⇒ 存档里的逐面序号按「块」继承(见 `importData` 那句 warn)。
|
|
93
|
+
*/
|
|
94
|
+
wallCapMaxEdgeM: number = WALL_CAP_MAX_EDGE_M;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 全景「序号直接取」映射:每个三角面(faceIndex)→ 被指派的「全景全局序号」(-1=未设)。
|
|
98
|
+
* 序号直接取由着色器读取 geometry 的 aPanoIndex;与 panoIndices 槽数组配合取对应全景纹理。
|
|
99
|
+
* 长度 = 几何三角面数,随 rebuildWallGeometry 按位置重算(保留旧映射,见下方注释)。
|
|
100
|
+
* 该数据随工程存档持久化(MktMeshData.panoFaceMapping),编辑器/预览/发布三端同源。
|
|
101
|
+
*/
|
|
102
|
+
panoFaceMapping: number[] = [];
|
|
103
|
+
/**
|
|
104
|
+
* 全景点坐标数组(每全景 3 个数 = 中心点 xyz,世界坐标)。
|
|
105
|
+
* ⭐ 2026-09-30 旨:全景点数据是**独立的纯几何数据**,与纹理图片是否加载/解码无关;
|
|
106
|
+
* 智能序号只认这份,不认 textures。由 setTextures/addTexture/removeTexture/setTexture(矩阵变更) 派生(_syncPanoCenters)。
|
|
107
|
+
* 本数组是「每全景 xyz」的**源**;序号映射最终烘焙成几何属性 `aPanoCenter`(每三角面9数,与 position 对齐),见 _syncPanoCenterAttr。
|
|
108
|
+
* ⚠️⚠️ 2026-10-01 血案修复:本字段改为**读取时实时派生**(getter),不再依赖「谁记得调 _syncPanoCenters」。
|
|
109
|
+
* 起因:`MktDevMesh.setTextures` 覆写了基类(编辑态不激活单全景),直接 `this.textures = textures` 而**不调
|
|
110
|
+
* super** ⇒ 派生点整条被绕过,`panoCenters` 恒为空 ⇒ 映射模式下点「智能序号」在 centerCount===0 处静默 return,
|
|
111
|
+
* 墙上全空且无任何提示。派生点式缓存在有子类覆写时就必然漏,**改为实时派生才是根治**(读一次 O(全景数),
|
|
112
|
+
* 调用方只有智能序号与 _syncPanoCenterAttr,都不是每帧路径)。
|
|
113
|
+
*/
|
|
114
|
+
get panoCenters(): number[] {
|
|
115
|
+
this._syncPanoCenters();
|
|
116
|
+
return this._panoCentersCache;
|
|
117
|
+
}
|
|
118
|
+
/** 派生结果的落点(勿直接读写,统一走 panoCenters getter / _syncPanoCenters) */
|
|
119
|
+
private _panoCentersCache: number[] = [];
|
|
120
|
+
/** 旧几何的「面中心 XZ→{y, 序号}」索引,用于改墙/改高后按位置保留全景序号映射(皇上旨 2026-09-28 决策②)。 */
|
|
121
|
+
private _panoXZMap: Map<string, { y: number; idx: number }[]> = new Map();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* ⭐ 槽位名册(皇上旨 2026-10-03「库侧也一并改成中心 ± 邻居」):每帧由 `updateTexture` 走
|
|
125
|
+
* `panoSlotRosterStep` 算出,**0 号槽 = 中心**(⭐ 10-07 第二十一道旨:离**控制器中心点** XZ 最近那张,
|
|
126
|
+
* 带 `PANO_SLOT_SWAP_MARGIN_M` 死区;锚点见 `_rosterCenter`),
|
|
127
|
+
* 1..7 = 中心在全景列表里的序号一层层往外扩的邻居(⭐ 10-07 旨「尽可能填满 8 个全景位置」⇒ 越界的序号不占格、
|
|
128
|
+
* 继续往有货那侧扩 ⇒ 填几格 = min(项目全景总数, 8),只有全景总数不够时才有空格)。
|
|
129
|
+
* ⇒ 与编辑态 `mktMeshEditRenderer._slotPanos` **同一份算法、同一个常数**(预览看到的那几张 = 编辑器里的那几张,均为 8 个全景槽)。
|
|
130
|
+
* 存**对象引用**不存下标(删点会让下标整体前移,引用不会认错人)。
|
|
131
|
+
* 消费方 = `feedFaceMapping`(墙 8 格);⭐ 10-03 旨② 后预览立方体与墙**共用同一份材质**,
|
|
132
|
+
* 立方体几何无 `aPanoIndex` ⇒ `vPanoIndex ≡ 0` ⇒ 它显示的就是 0 号槽那张(墙与立方体天然同源,
|
|
133
|
+
* 原来那条「把中心那几张抄给立方体单全景材质」的 `currentTiles` 旁路已整条删除)。
|
|
134
|
+
*/
|
|
135
|
+
private _slotPanos: (MktTextureInfo | undefined)[] = [];
|
|
136
|
+
/**
|
|
137
|
+
* ⭐ 名册那把尺子的**锚点**(皇上旨 2026-10-07 第二十一道旨:「判断使用哪些全景球的判断条件,
|
|
138
|
+
* 从距离相机的距离改成判断距离控制器的中心点的距离」)。这里存外部控制器的中心点
|
|
139
|
+
* (轨道控制器 = `controls.target`,也就是画面正中央那个注视点),由消费方经 `setRosterCenter()` 注入。
|
|
140
|
+
* · 只读 XZ ⇒ 与名册历代那把「水平距离」尺同一把(09-28 旨),高度差不进尺子。
|
|
141
|
+
* · `null` ⇒ 回落 `camera.position`(老口径):发布页那一路没有控制器引用,不注入就一切照旧。
|
|
142
|
+
* · 存的是**引用**(OrbitControls 原地改它那个 Vector3)⇒ 消费方设一次即可,不必每帧重设。
|
|
143
|
+
* · ⚠️ 全景点矩阵本就是**世界矩阵**,锚点也取世界坐标;⛔ 别再乘一遍 `matrixWorld`(同 `updateTexture` 里那句提醒)。
|
|
144
|
+
*/
|
|
145
|
+
private _rosterCenter: Vector3 | null = null;
|
|
146
|
+
/**
|
|
147
|
+
* ⭐ 皇上旨 2026-10-03「整套搬进库(彻底同源)」:库侧不再自养「按全景序号取整槽、齐活才写」那一路,
|
|
148
|
+
* 改成与编辑态**同一份换绑策略**(`./panoSlotBind`)+ 同一口径的三张表:
|
|
149
|
+
* 地址→纹理缓存、已上传 GPU 集合、待上传排队集合。治的两个病:
|
|
150
|
+
* ① 换景点闪一帧灰(库侧原先没有「未就绪整槽沿用上一批」这层兜底);
|
|
151
|
+
* ② 图与投影矩阵来自**不同批**(原先矩阵无条件 copy,纹理却可能还在路上)。
|
|
152
|
+
* 地址 → 已解码纹理;值为 `null` = 该地址正在加载(占位 ⇒ 同一地址不会被问两次就下两遍)。
|
|
153
|
+
* ⚠️ 皇上旨 2026-10-02:运行中**不淘汰**任何一张,只在 `dispose()` 整批释放。
|
|
154
|
+
*/
|
|
155
|
+
private _texCache: Map<string, Texture | null> = new Map();
|
|
156
|
+
/** 已上传 GPU 的纹理:没进这张表的**不许**绑进 shader(首绑现场 `texImage2D` 就是那一下卡顿,皇上旨 09-29「两个大头别在同一帧」) */
|
|
157
|
+
private _texUploaded: WeakSet<Texture> = new WeakSet();
|
|
158
|
+
/** 已排进全局队列、等着「只干上传那一句」的纹理(`addWork` 按闭包判重判不出来 ⇒ 必须自备集合) */
|
|
159
|
+
private _uploadQueued: Set<Texture> = new Set();
|
|
160
|
+
/** 加载地址真的拼了 ossSize 缩放参数的条目 ⇒ 换档只重载这些(真瓦片一律原图,不进此集) */
|
|
161
|
+
private _ossSizeDepKeys: Set<string> = new Set();
|
|
162
|
+
/** 渲染器:由 `onBeforeRender` 每帧捕获,预上传走它的 `initTexture`;没捕获到就退回 three 的自绑路径(不报错) */
|
|
163
|
+
private _glRenderer: WebGLRenderer | null = null;
|
|
164
|
+
/** ⭐ 32 瓦片(横 8 × 竖 4 切片)离屏拼合器:左右 2 槽纹理由它逐格拼出(每帧一张新纹理进 GPU + 已进 GPU 的逐格画)。
|
|
165
|
+
* 编辑器渲染器经 `panoStitcher` 复用同一实例(同一份槽纹理缓存,⛔ 不另建第二份显存)。
|
|
166
|
+
* 皇上旨 2026-10-06「不用 canvas 2D 的 drawImage 拼合, 用一个 shader 一个一个渲染到 RT 上面去」
|
|
167
|
+
* +「canvas 是什么意思, 按照我的想法, 不应该有 canvas 才对啊」⇒ **只此一份实现**:旧的 canvas 2D
|
|
168
|
+
* 那一份连同 `?panoStitch=canvas` 对照开关整条删除。⚠️ RT 是 GL 上下文私有资源 ⇒ 见拼合器 `_switchRenderer`。 */
|
|
169
|
+
private _stitcher: PanoStitcher = new PanoTileStitcher();
|
|
170
|
+
/** 拼合器只读访问(编辑器渲染器共用;⛔ 不另建第二实例) */
|
|
171
|
+
get panoStitcher(): PanoStitcher {
|
|
172
|
+
return this._stitcher;
|
|
173
|
+
}
|
|
174
|
+
/** 融合材质未绑纹理时的占位(避免空纹理采样报错;深灰兜底由着色器 uFaceMissingColor 负责) */
|
|
175
|
+
protected _placeholder!: Texture;
|
|
176
|
+
// 纹理下载/解码走 `loadPanoTexture`(甲,2026-10-05),结果按**地址**进 `_texCache`,运行中不淘汰、只随 `dispose()` 释放。
|
|
177
|
+
// ⛔ 别换回 `TextureLoader`:它只等 `img.onload`(不解码),解码会被拽到上传那一拍的 `texImage2D(<img>)` 上同步做完。
|
|
178
|
+
/** 纹理 OSS 缩放尺寸(1024 / 2048 / 空=原图),由外部 setOssSize 注入;默认 1024 保持旧行为。
|
|
179
|
+
* 编辑器渲染器与预览渲染器各自把 store.ossSize 推下来,使全景球/预览按该尺寸加载。 */
|
|
180
|
+
private _ossSize: "1024" | "2048" | "" = "1024";
|
|
181
|
+
/** 全景纹理形态:tiled=左右 2 槽各装一张**拼合槽纹理**(该点 32 片 横 8 × 竖 4 → 2 张 4096×4096,10-06 晚旨)
|
|
182
|
+
* / whole=2 槽同为一张完整等距柱状原图。
|
|
183
|
+
* 由外部 setPanoTexMode 注入(与 _ossSize 同一套注入路径),默认 tiled 保持改造前行为。 */
|
|
184
|
+
private _panoTexMode: PanoTexMode = "tiled";
|
|
185
|
+
/** 当前全景纹理形态(只读)。消费方(预览立方体等自持材质的路径)据此算 `mktPanoTiledFlag`,
|
|
186
|
+
* 必须与基类喂纹理处用同一个值,否则标记与地址不同源 ⇒ 整图被按 1/2 横向铺满球面(10-06 晚起纵向不分块,四槽时代是 1/4)。 */
|
|
187
|
+
get panoTexMode(): PanoTexMode {
|
|
188
|
+
return this._panoTexMode;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 复用临时对象(避免每帧 new,降低 GC 压力)
|
|
192
|
+
private _tmpV3: Vector3 = new Vector3();
|
|
193
|
+
|
|
194
|
+
constructor(opts: { wallHeight?: number; wallClosed?: boolean } = {}) {
|
|
195
|
+
// 发布/预览态墙:**固定**用「全景序号直取」着色器(皇上旨 2026-09-29:mktmesh 就只有按照 attribute
|
|
196
|
+
// 配置来显示全景这一种固定的 shader——每帧由 feedFaceMapping 按 panoFaceMapping 把被引用的全景
|
|
197
|
+
// 喂进 8 个全景槽,按三角面 aPanoIndex 取对应纹理;原 blend+uFaceMappingMode 运行时分支已拆出独立 shader)。
|
|
198
|
+
const material = createMktFloorMappingShaderMaterial({ maxPano: MKT_PANO_MAX_SLOTS });
|
|
199
|
+
const placeholder = createPlaceholderTexture();
|
|
200
|
+
super(new BufferGeometry(), material);
|
|
201
|
+
this._placeholder = placeholder;
|
|
202
|
+
// ⭐ 2026-09-26 彻查「正反面」:材质 face 朝向**只有一个写入口** = setWallSide()(内部走 _wallSideToThree)。
|
|
203
|
+
// 这里过去写死 `material.side = DoubleSide`,与那条映射并列出第二份「谁决定正面」的写法;
|
|
204
|
+
// 改成构造末尾用自身字段回灌:值仍是默认 "double" ⇒ **当时**画面逐字不变,但侧面上不再有两处口径。
|
|
205
|
+
// ⚠️ 那句「画面逐字不变」只在那一天成立:09-29 晚旨之后 `_wallSideToThree` 三档全落 BackSide ⇒
|
|
206
|
+
// 这一行回灌落地的**就是**反面(不是双面)。皇上旨 2026-10-07「mktmesh 的 shader 和 mktdevmesh 的映射 shader 保持一致」
|
|
207
|
+
// 之后预览也删了自己那句手写 DoubleSide 覆盖 ⇒ 「朝向只有一个写入口」这条对库侧/编辑态/预览三处同时成立。
|
|
208
|
+
// (必须在 super() 之后调用 —— this.wallSide 是类字段,早于 super() 读不到。)
|
|
209
|
+
this.setWallSide(this.wallSide);
|
|
210
|
+
// 保险:禁止视锥剔除,避免 boundingSphere 边界问题导致 GPU 跳过绘制
|
|
211
|
+
this.frustumCulled = false;
|
|
212
|
+
if (opts.wallHeight !== undefined) this.wallHeight = opts.wallHeight;
|
|
213
|
+
if (opts.wallClosed !== undefined) this.wallClosed = opts.wallClosed;
|
|
214
|
+
// 每帧渲染前:① 同步「投影空间逆矩阵」(让墙的投影与 MktMesh 整体变换解耦);
|
|
215
|
+
// ② 按**名册锚点**(⭐ 10-07 第二十一道旨:控制器中心点,未注入时才是相机位置)的 XZ 距离算**槽位名册**(中心 ± 邻居),中心换人时更新 `activeIndex`;
|
|
216
|
+
// ③ 发布/预览态墙按全景序号直接取喂食,8 格 = 名册那 8 张(皇上旨 2026-09-28 决策③ + 10-03「库侧同口径」)
|
|
217
|
+
this.onBeforeRender = (renderer, _scene, camera) => {
|
|
218
|
+
this._glRenderer = renderer;
|
|
219
|
+
this.syncProjInvMatrix();
|
|
220
|
+
this.updateTexture(camera as Camera);
|
|
221
|
+
this.feedFaceMapping();
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ───────────────────────── 墙体几何(由点位挤出) ─────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 设置墙体地面折线点位,并重建墙体几何。
|
|
229
|
+
* @param points 地面折线点位(XZ 平面,世界坐标),至少 2 个点才能生成墙体
|
|
230
|
+
* @param opts.closed 是否闭合(首尾相连成环),默认沿用当前 wallClosed
|
|
231
|
+
* @param opts.height 墙高(沿 +Y 挤出),默认沿用当前 wallHeight
|
|
232
|
+
*/
|
|
233
|
+
setWallPoints(points: WallPoint[], opts: { closed?: boolean; height?: number } = {}): void {
|
|
234
|
+
// ⚠️ 必须保留每个点的 y(离地高度),否则「墙点 Y 高度可调」会丢值
|
|
235
|
+
this.wallPoints = points.map((p) => ({ x: p.x, y: p.y ?? 0, z: p.z }));
|
|
236
|
+
if (opts.closed !== undefined) this.wallClosed = opts.closed;
|
|
237
|
+
if (opts.height !== undefined) this.wallHeight = opts.height;
|
|
238
|
+
this.rebuildWallGeometry();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 读取墙点位(副本,含 y,调用方可自行修改再回设) */
|
|
242
|
+
getWallPoints(): WallPoint[] {
|
|
243
|
+
return this.wallPoints.map((p) => ({ x: p.x, y: p.y, z: p.z }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 墙正面/反面枚举 → three.js Side 常量。
|
|
248
|
+
* ⭐ 2026-09-29 晚旨「shader 设反面显示、不要双面」:三档**全部**落到 BackSide(对应历史上
|
|
249
|
+
* 的 wallSide="back" 档 —— 零厚度幕墙只画**朝房间内的那一面**,从房间外看穿过去、看不到内部全景)。
|
|
250
|
+
* API 形参与 wallSide 字段保留(存档/调用方兼容)。⚠️ 地板/立方体**不由这里控制**:编辑态地板恒走「仅显示选中」工厂(DoubleSide);
|
|
251
|
+
* 立方体在 mapping 档与墙**共用同一份材质实例**(皇上旨 2026-10-02),⇒ 朝向跟着墙 = BackSide,
|
|
252
|
+
* 另两档(blend / selected)另建一份 mapping 并显式拉回 DoubleSide(见渲染器 `_applyDisplayMode`;预览自 10-07 旨起与编辑态同口径)。
|
|
253
|
+
* ⚠️ 若实测房间内反而看不到墙(几何法线朝向与预期相反),把这里的 BackSide 换成 FrontSide 即可(一行)。
|
|
254
|
+
*/
|
|
255
|
+
private _wallSideToThree(_side: WallSide): Side {
|
|
256
|
+
return BackSide;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 设置墙体正反面显示(**材质 side 的唯一写入口**;参数保留兼容,当前无论传什么都渲染**反面**,见 _wallSideToThree) */
|
|
260
|
+
setWallSide(side: WallSide): void {
|
|
261
|
+
this.wallSide = side;
|
|
262
|
+
(this.material as Material).side = this._wallSideToThree(side);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 由 wallPoints / wallHeight / wallClosed 重建墙体几何:侧面(ExtrudeMesh) + 上下封盖(ShapeGeometry) */
|
|
266
|
+
private rebuildWallGeometry(): void {
|
|
267
|
+
const pts = this.wallPoints;
|
|
268
|
+
const height = this.wallHeight;
|
|
269
|
+
const positions: number[] = [];
|
|
270
|
+
|
|
271
|
+
// ── 全景序号映射:改墙/改高后按位置保留(皇上旨 2026-09-28 决策②)──
|
|
272
|
+
// 捕获旧几何面中心 + 旧映射(this.geometry 此刻仍是旧几何,尚未 dispose)。
|
|
273
|
+
// ⭐ 10-07 封盖细分之后:新面中心**多半不再落在旧面中心那几个 XZ 上**(细分是在三角形内部加了一堆点)
|
|
274
|
+
// ⇒ 只认「XZ 键精确相等」的话,封盖那一整片序号会被丢光 ⇒ 这里多留一份**旧三角形的 XZ 顶点**,
|
|
275
|
+
// 新面中心改用「它落在哪个旧三角形里」来认祖(点在三角形内),认不到才退回精确键那一路。
|
|
276
|
+
const oldPosAttr = this.geometry.getAttribute("position") as BufferAttribute | undefined;
|
|
277
|
+
const oldFaceCount = oldPosAttr ? Math.floor(oldPosAttr.count / 3) : 0;
|
|
278
|
+
const oldCentroids: { x: number; y: number; z: number }[] = new Array(oldFaceCount);
|
|
279
|
+
const oldTris: { ax: number; az: number; bx: number; bz: number; cx: number; cz: number; y: number; idx: number }[] = [];
|
|
280
|
+
const oldMapping = this.panoFaceMapping;
|
|
281
|
+
for (let f = 0; f < oldFaceCount; f++) {
|
|
282
|
+
const a = 3 * f, b = 3 * f + 1, c = 3 * f + 2;
|
|
283
|
+
const ax = oldPosAttr!.getX(a), ay = oldPosAttr!.getY(a), az = oldPosAttr!.getZ(a);
|
|
284
|
+
const bx = oldPosAttr!.getX(b), by = oldPosAttr!.getY(b), bz = oldPosAttr!.getZ(b);
|
|
285
|
+
const cx = oldPosAttr!.getX(c), cy = oldPosAttr!.getY(c), cz = oldPosAttr!.getZ(c);
|
|
286
|
+
oldCentroids[f] = { x: (ax + bx + cx) / 3, y: (ay + by + cy) / 3, z: (az + bz + cz) / 3 };
|
|
287
|
+
oldTris.push({ ax, az, bx, bz, cx, cz, y: (ay + by + cy) / 3, idx: oldMapping[f] ?? -1 });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (pts.length >= 2) {
|
|
291
|
+
// —— 墙体侧面:复用 ExtrudeMesh,shapePath=[[0,0],[0,height]] 沿地面折线挤出一道幕墙 ——
|
|
292
|
+
// 路径点的 vector3[1] = p.y,即墙基离地高度;ExtrudeMesh 把整个截面抬到该高度再向上挤出 height,
|
|
293
|
+
// 故底面落在 p.y、顶面落在 p.y + height(支点位高低错落 / 阶梯墙基)。
|
|
294
|
+
const paths = pts.map((p, i) => ({
|
|
295
|
+
vector3: [p.x, p.y, p.z] as [number, number, number],
|
|
296
|
+
radius: 0,
|
|
297
|
+
segments: 0,
|
|
298
|
+
scale: { x: 1, y: 1 },
|
|
299
|
+
name: `w${i}`,
|
|
300
|
+
}));
|
|
301
|
+
const tmp = new ExtrudeMesh(
|
|
302
|
+
{ paths, shapePath: [[0, 0], [0, height]], closed: this.wallClosed },
|
|
303
|
+
undefined as unknown as Material
|
|
304
|
+
);
|
|
305
|
+
const sidePos = tmp.geometry.getAttribute("position");
|
|
306
|
+
for (let i = 0; i < sidePos.count; i++) {
|
|
307
|
+
positions.push(sidePos.getX(i), sidePos.getY(i), sidePos.getZ(i));
|
|
308
|
+
}
|
|
309
|
+
tmp.geometry.dispose();
|
|
310
|
+
|
|
311
|
+
// —— 墙体上/下两个多边形封盖:用 three.js 自带的 ShapeGeometry 由点位三角化 ——
|
|
312
|
+
// ⚠️ 2026-09-26 更正:此前这句注释写的是「编辑态只要幕墙、不要封顶」——**与事实相反**,勿再照它理解。
|
|
313
|
+
// `skipWallCaps` 在库侧与编辑态 `MktDevMesh` 都保持默认 false —— 封盖与侧面**推进同一份 geometry、
|
|
314
|
+
// 共用同一份 material**(见下方 positions.push 与 `this.geometry = geo`),故「墙体正反面」参数一改,
|
|
315
|
+
// 封盖与侧面同帧跟着翻,不存在第二处需要同步的 side。本开关只留给「只要幕墙不要封顶」的外部调用方,
|
|
316
|
+
// 本项目内**无人置 true**。
|
|
317
|
+
if (!this.skipWallCaps) {
|
|
318
|
+
// Shape 用 2D 坐标(x, z);rotateX(90°) 把 (x,y,0)→(x,0,y),故 shape 的 (px,pz) 落回世界 (px,0,pz),与侧面点位一致。
|
|
319
|
+
// 每个封盖顶点按 (x,z) 反查对应点位的 y:下封盖 y=p.y,上封盖 y=p.y+height,使封盖随墙基高低错落。
|
|
320
|
+
const shape = new Shape();
|
|
321
|
+
pts.forEach((p, i) => {
|
|
322
|
+
if (i === 0) shape.moveTo(p.x, p.z);
|
|
323
|
+
else shape.lineTo(p.x, p.z);
|
|
324
|
+
});
|
|
325
|
+
if (this.wallClosed) shape.closePath();
|
|
326
|
+
const cap = new ShapeGeometry(shape);
|
|
327
|
+
cap.rotateX(Math.PI / 2);
|
|
328
|
+
const capNI = cap.toNonIndexed();
|
|
329
|
+
const capPos = capNI.getAttribute("position");
|
|
330
|
+
// 坐标→Y 映射:**三角化后的顶点都是多边形点的副本**(没有插值点)⇒ 可按 (x,z) 精确落回点位高度。
|
|
331
|
+
// ⚠️ 细分切出来的**内部点**不在多边形点的位置上 ⇒ 这张表查不到(会打成 0 ⇒ 封盖塌下去),
|
|
332
|
+
// 它们的 y 由下面的细分函数按两端平均插值(落在原平面上)。
|
|
333
|
+
const yByXZ = new Map<string, number>();
|
|
334
|
+
for (const p of pts) yByXZ.set(`${p.x},${p.z}`, p.y);
|
|
335
|
+
const yOf = (x: number, z: number): number => {
|
|
336
|
+
const v = yByXZ.get(`${x},${z}`);
|
|
337
|
+
return v === undefined ? 0 : v;
|
|
338
|
+
};
|
|
339
|
+
// ⭐ 10-07 旨「封盖三角面不要那么长 / 再细分一下」:先把封盖三角化结果摊成扁平三角形
|
|
340
|
+
// (每个顶点带上它那处墙基的 y),再按**最长边二分**切到每条边都不长于 `wallCapMaxEdgeM`
|
|
341
|
+
// (0 / 负数 = 不细分)。细分只在封盖平面内加顶点 ⇒ **形状一点不变,只是面变小**。
|
|
342
|
+
const rawCap: number[] = [];
|
|
343
|
+
for (let i = 0; i < capPos.count; i++) {
|
|
344
|
+
const x = capPos.getX(i);
|
|
345
|
+
const z = capPos.getZ(i);
|
|
346
|
+
rawCap.push(x, yOf(x, z), z);
|
|
347
|
+
}
|
|
348
|
+
const capTris = subdivideCapTriangles(rawCap, this.wallCapMaxEdgeM);
|
|
349
|
+
// 下封盖(level=0)→ y=p.y;上封盖(level=height)→ y=p.y+height
|
|
350
|
+
for (let lv = 0; lv <= 1; lv++) {
|
|
351
|
+
const base = lv === 0 ? 0 : height;
|
|
352
|
+
if (lv === 0) {
|
|
353
|
+
// 下封盖:顶点顺序与 shape 一致,法线朝下(指向地面外侧)
|
|
354
|
+
for (let i = 0; i < capTris.length; i += 9) {
|
|
355
|
+
positions.push(capTris[i], capTris[i + 1] + base, capTris[i + 2]);
|
|
356
|
+
positions.push(capTris[i + 3], capTris[i + 4] + base, capTris[i + 5]);
|
|
357
|
+
positions.push(capTris[i + 6], capTris[i + 7] + base, capTris[i + 8]);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// 上封盖:每个三角形的**顶点顺序倒过来** ⇒ 绕序翻转 ⇒ 法线由朝下翻为朝上(朝天/朝外,
|
|
361
|
+
// 与「盒子封盖朝外」一致;单面(front/back)模式下从外部俯视可见、内部仰视不可见)。
|
|
362
|
+
for (let i = 0; i < capTris.length; i += 9) {
|
|
363
|
+
positions.push(capTris[i + 6], capTris[i + 7] + base, capTris[i + 8]);
|
|
364
|
+
positions.push(capTris[i + 3], capTris[i + 4] + base, capTris[i + 5]);
|
|
365
|
+
positions.push(capTris[i], capTris[i + 1] + base, capTris[i + 2]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
capNI.dispose();
|
|
370
|
+
cap.dispose();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const geo = new BufferGeometry();
|
|
375
|
+
geo.setAttribute("position", new Float32BufferAttribute(positions, 3));
|
|
376
|
+
// ⭐ 必须算法线(皇上旨 2026-09-24「模型面上映射要先判全景点方向与面法线夹角 <90°」):
|
|
377
|
+
// 本几何是非索引三角形汤(侧面 + 上下封盖),computeVertexNormals 因此给出**逐面**法线,
|
|
378
|
+
// 正好就是「面的法线」。缺它着色器读到 (0,0,0) ⇒ 点积恒 0 ⇒ 夹角门永远筛不到候选(会走兜底,不崩但白改)。
|
|
379
|
+
geo.computeVertexNormals();
|
|
380
|
+
|
|
381
|
+
// ── 按位置重算全景序号映射(保留旧指派,决策②)──
|
|
382
|
+
const faceCount = Math.floor(positions.length / 9);
|
|
383
|
+
const newMapping: number[] = new Array(faceCount).fill(-1);
|
|
384
|
+
const rk = (v: number): number => Math.round(v * 1000) / 1000; // 1mm 量化做 XZ 键
|
|
385
|
+
// 旧 XZ 索引:同一 XZ 可能对应上下两个封盖(y 不同),故一个键挂一个 {y,idx} 列表
|
|
386
|
+
const oldMap = new Map<string, { y: number; idx: number }[]>();
|
|
387
|
+
for (let f = 0; f < oldFaceCount; f++) {
|
|
388
|
+
const c = oldCentroids[f];
|
|
389
|
+
const key = `${rk(c.x)},${rk(c.z)}`;
|
|
390
|
+
const arr = oldMap.get(key) ?? [];
|
|
391
|
+
arr.push({ y: c.y, idx: oldMapping[f] ?? -1 });
|
|
392
|
+
oldMap.set(key, arr);
|
|
393
|
+
}
|
|
394
|
+
// ⭐ 10-07 封盖细分后**新加的一层**:新面中心「落在哪个旧三角形里」就继承那一片的序号。
|
|
395
|
+
// 只在上面那道精确 XZ 键**没命中**时才走(命中 = 老口径,一个字没改)。
|
|
396
|
+
// ⚠️ 非加不可的理由:封盖被切成几百上千个小面后,新面中心几乎不可能正好等于某个旧面中心
|
|
397
|
+
// ⇒ 没有这一层,改墙高 / 改墙点之后封盖的序号会整片变 -1(得重跑一次「智能序号」才看得见图)。
|
|
398
|
+
const insideBudget = oldTris.length * faceCount <= 4000000; // 兜底闸:面数大到离谱就跳过这层,别把改墙那一下卡死
|
|
399
|
+
const side2 = (ax: number, az: number, bx: number, bz: number, px: number, pz: number): number =>
|
|
400
|
+
(bx - ax) * (pz - az) - (bz - az) * (px - ax);
|
|
401
|
+
/** 点在 XZ 平面上的三角形内(叉积同号)判据:命中多个(上下封盖同 XZ)时取 y 最近那一片 */
|
|
402
|
+
const pickInside = (px: number, pz: number, cy: number): number => {
|
|
403
|
+
let best = -1;
|
|
404
|
+
let bestD = Infinity;
|
|
405
|
+
for (let i = 0; i < oldTris.length; i++) {
|
|
406
|
+
const t = oldTris[i];
|
|
407
|
+
const d1 = side2(t.ax, t.az, t.bx, t.bz, px, pz);
|
|
408
|
+
const d2 = side2(t.bx, t.bz, t.cx, t.cz, px, pz);
|
|
409
|
+
const d3 = side2(t.cx, t.cz, t.ax, t.az, px, pz);
|
|
410
|
+
// 三个叉积里有正有负 ⇒ 点在三角形外(全 0 = 退化成一条线的面,也算命中,交给 y 差挑)
|
|
411
|
+
if ((d1 > 0 || d2 > 0 || d3 > 0) && (d1 < 0 || d2 < 0 || d3 < 0)) continue;
|
|
412
|
+
const d = Math.abs(t.y - cy);
|
|
413
|
+
if (d < bestD) { bestD = d; best = t.idx; }
|
|
414
|
+
}
|
|
415
|
+
return best;
|
|
416
|
+
};
|
|
417
|
+
for (let f = 0; f < faceCount; f++) {
|
|
418
|
+
const a = 9 * f;
|
|
419
|
+
const cx = (positions[a] + positions[a + 3] + positions[a + 6]) / 3;
|
|
420
|
+
const cz = (positions[a + 2] + positions[a + 5] + positions[a + 8]) / 3;
|
|
421
|
+
const cy = (positions[a + 1] + positions[a + 4] + positions[a + 7]) / 3;
|
|
422
|
+
const cand = oldMap.get(`${rk(cx)},${rk(cz)}`);
|
|
423
|
+
if (cand && cand.length) {
|
|
424
|
+
// 同 XZ 多个候选(上/下封盖)时取 y 最近者,避免把底盖映射错配到顶盖
|
|
425
|
+
let best = cand[0];
|
|
426
|
+
let bestD = Math.abs(best.y - cy);
|
|
427
|
+
for (let i = 1; i < cand.length; i++) {
|
|
428
|
+
const d = Math.abs(cand[i].y - cy);
|
|
429
|
+
if (d < bestD) { bestD = d; best = cand[i]; }
|
|
430
|
+
}
|
|
431
|
+
newMapping[f] = best.idx;
|
|
432
|
+
} else if (insideBudget && oldTris.length) {
|
|
433
|
+
const v = pickInside(cx, cz, cy);
|
|
434
|
+
if (v >= 0) newMapping[f] = v; // -1 = 那一片本来就没指派,保持未指派
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.panoFaceMapping = newMapping;
|
|
438
|
+
// 重建 XZ 索引(供下次改墙保留)
|
|
439
|
+
const xzMap = new Map<string, { y: number; idx: number }[]>();
|
|
440
|
+
for (let f = 0; f < faceCount; f++) {
|
|
441
|
+
const a = 9 * f;
|
|
442
|
+
const cx = (positions[a] + positions[a + 3] + positions[a + 6]) / 3;
|
|
443
|
+
const cz = (positions[a + 2] + positions[a + 5] + positions[a + 8]) / 3;
|
|
444
|
+
const cy = (positions[a + 1] + positions[a + 4] + positions[a + 7]) / 3;
|
|
445
|
+
const key = `${rk(cx)},${rk(cz)}`;
|
|
446
|
+
const arr = xzMap.get(key) ?? [];
|
|
447
|
+
arr.push({ y: cy, idx: newMapping[f] });
|
|
448
|
+
xzMap.set(key, arr);
|
|
449
|
+
}
|
|
450
|
+
this._panoXZMap = xzMap;
|
|
451
|
+
// aPanoIndex 几何属性(每三角面 3 顶点同值 = 该面归属的全景全局序号;默认 -1 = 未设)
|
|
452
|
+
const panoIdxAttr = new Float32Array(faceCount * 3);
|
|
453
|
+
for (let f = 0; f < faceCount; f++) {
|
|
454
|
+
const v = newMapping[f] ?? -1;
|
|
455
|
+
panoIdxAttr[3 * f] = v;
|
|
456
|
+
panoIdxAttr[3 * f + 1] = v;
|
|
457
|
+
panoIdxAttr[3 * f + 2] = v;
|
|
458
|
+
}
|
|
459
|
+
geo.setAttribute("aPanoIndex", new Float32BufferAttribute(panoIdxAttr, 1));
|
|
460
|
+
|
|
461
|
+
this.geometry.dispose();
|
|
462
|
+
this.geometry = geo;
|
|
463
|
+
this._syncPanoCenterAttr(); // ⭐ 2026-09-30:同步 aPanoCenter(每三角面9数,与 position 对齐)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 设置某个三角面(faceIndex)归属的全景全局序号;-1=清除(序号模式下显示深灰兜底色)。
|
|
468
|
+
* 更新映射数组 + 几何 aPanoIndex 属性(仅该面 3 顶点),并同步 XZ 索引供后续改墙保留。
|
|
469
|
+
*/
|
|
470
|
+
setFacePanoIndex(faceIndex: number, globalIndex: number): void {
|
|
471
|
+
if (faceIndex < 0 || faceIndex >= this.panoFaceMapping.length) return;
|
|
472
|
+
this.panoFaceMapping[faceIndex] = globalIndex;
|
|
473
|
+
const attr = this.geometry.getAttribute("aPanoIndex") as Float32BufferAttribute | undefined;
|
|
474
|
+
if (attr && attr.count === this.panoFaceMapping.length * 3) {
|
|
475
|
+
attr.setXYZ(faceIndex * 3, globalIndex, globalIndex, globalIndex);
|
|
476
|
+
attr.needsUpdate = true;
|
|
477
|
+
// ⭐ 2026-09-30:同步该面 aPanoCenter(每三角面9数,与 position 对齐)
|
|
478
|
+
const cAttr = this.geometry.getAttribute("aPanoCenter") as Float32BufferAttribute | undefined;
|
|
479
|
+
if (cAttr && cAttr.count === this.panoFaceMapping.length * 3) {
|
|
480
|
+
const nPano = this.panoCenters.length / 3;
|
|
481
|
+
let cx = 0, cy = 0, cz = 0;
|
|
482
|
+
if (globalIndex >= 0 && globalIndex < nPano) {
|
|
483
|
+
cx = this.panoCenters[globalIndex * 3];
|
|
484
|
+
cy = this.panoCenters[globalIndex * 3 + 1];
|
|
485
|
+
cz = this.panoCenters[globalIndex * 3 + 2];
|
|
486
|
+
}
|
|
487
|
+
for (let v = 0; v < 3; v++) cAttr.setXYZ(faceIndex * 3 + v, cx, cy, cz);
|
|
488
|
+
cAttr.needsUpdate = true;
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// 属性缺失/长度不符(极少):整面重建(按位置保留会把它当成 -1,但此处是显式指定,可接受)
|
|
492
|
+
this.rebuildWallGeometry();
|
|
493
|
+
}
|
|
494
|
+
this._rebuildXZMap();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** 重建 XZ 索引(映射变化后调用,保证下次改墙按位置保留) */
|
|
498
|
+
private _rebuildXZMap(): void {
|
|
499
|
+
const pos = this.geometry.getAttribute("position") as BufferAttribute | undefined;
|
|
500
|
+
const faceCount = pos ? Math.floor(pos.count / 3) : 0;
|
|
501
|
+
const rk = (v: number): number => Math.round(v * 1000) / 1000;
|
|
502
|
+
const m = new Map<string, { y: number; idx: number }[]>();
|
|
503
|
+
for (let f = 0; f < faceCount; f++) {
|
|
504
|
+
const a = 9 * f;
|
|
505
|
+
const cx = (pos!.getX(a) + pos!.getX(a + 3) + pos!.getX(a + 6)) / 3;
|
|
506
|
+
const cz = (pos!.getZ(a) + pos!.getZ(a + 3) + pos!.getZ(a + 8)) / 3;
|
|
507
|
+
const cy = (pos!.getY(a) + pos!.getY(a + 3) + pos!.getY(a + 7)) / 3;
|
|
508
|
+
const key = `${rk(cx)},${rk(cz)}`;
|
|
509
|
+
const arr = m.get(key) ?? [];
|
|
510
|
+
arr.push({ y: cy, idx: this.panoFaceMapping[f] ?? -1 });
|
|
511
|
+
m.set(key, arr);
|
|
512
|
+
}
|
|
513
|
+
this._panoXZMap = m;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** 三角面总数(= 几何顶点数 / 3,因非索引三角形汤) */
|
|
517
|
+
getFaceCount(): number {
|
|
518
|
+
const pos = this.geometry.getAttribute("position") as BufferAttribute | undefined;
|
|
519
|
+
return pos ? Math.floor(pos.count / 3) : 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 返回被三角面引用的全景全局序号集合(去重),按引用面数降序、最多 maxSlots 个。
|
|
524
|
+
* ⚠️ 2026-10-03 起**不在喂料路上**(墙那 8 格改由槽位名册决定,见 `feedFaceMapping`);保留为公共查询 API
|
|
525
|
+
* (外部若要按「哪几张全景被指派得最多」统计仍可用),⛔ 不许再拿它决定 shader 装哪几张。
|
|
526
|
+
*/
|
|
527
|
+
getReferencedPanoIndices(maxSlots: number): number[] {
|
|
528
|
+
const cnt = new Map<number, number>();
|
|
529
|
+
for (const idx of this.panoFaceMapping) {
|
|
530
|
+
if (idx >= 0) cnt.set(idx, (cnt.get(idx) ?? 0) + 1);
|
|
531
|
+
}
|
|
532
|
+
const sorted = [...cnt.entries()].sort((a, b) => b[1] - a[1]).map((e) => e[0]);
|
|
533
|
+
return sorted.slice(0, maxSlots);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** 仅重建 aPanoIndex 几何属性(映射数据变化、几何未变时调用,避免整面重建) */
|
|
537
|
+
rebuildPanoIndexAttribute(): void {
|
|
538
|
+
const faceCount = this.panoFaceMapping.length;
|
|
539
|
+
const attr = this.geometry.getAttribute("aPanoIndex") as Float32BufferAttribute | undefined;
|
|
540
|
+
if (attr && attr.count === faceCount * 3) {
|
|
541
|
+
for (let f = 0; f < faceCount; f++) {
|
|
542
|
+
const v = this.panoFaceMapping[f] ?? -1;
|
|
543
|
+
attr.setXYZ(f * 3, v, v, v);
|
|
544
|
+
}
|
|
545
|
+
attr.needsUpdate = true;
|
|
546
|
+
} else {
|
|
547
|
+
this.rebuildWallGeometry();
|
|
548
|
+
}
|
|
549
|
+
this._rebuildXZMap();
|
|
550
|
+
this._syncPanoCenterAttr(); // ⭐ 2026-09-30:同步 aPanoCenter(每三角面9数,与 position 对齐)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ───────────────────────── 纹理列表管理 ─────────────────────────
|
|
554
|
+
|
|
555
|
+
/** 从 textures 各矩阵平移分量(elements[12,13,14],即墨卡托投影中心)派生 panoCenters(每全景 xyz)。
|
|
556
|
+
* 全景点位置与纹理图片加载解耦,纯几何;智能序号只依赖这份数据。 */
|
|
557
|
+
protected _syncPanoCenters(): void {
|
|
558
|
+
const out: number[] = new Array(this.textures.length * 3);
|
|
559
|
+
for (let i = 0; i < this.textures.length; i++) {
|
|
560
|
+
const e = this.textures[i].matrix.elements;
|
|
561
|
+
out[i * 3] = e[12];
|
|
562
|
+
out[i * 3 + 1] = e[13];
|
|
563
|
+
out[i * 3 + 2] = e[14];
|
|
564
|
+
}
|
|
565
|
+
// ⚠️ 写缓存字段而非 this.panoCenters —— 后者是 getter,会递归回本方法
|
|
566
|
+
this._panoCentersCache = out;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 写 aPanoCenter 几何属性:每三角面 9 数(该面归属全景的中心 xyz,复制到 3 顶点),
|
|
571
|
+
* itemSize=3、count = position.count,与 position **完全对齐**
|
|
572
|
+
* (皇上旨 2026-09-30 第③点:「每个三角面 9 个数一组,全景也按 9 数一组、和顶点数组长度对齐」)。
|
|
573
|
+
* 源 = panoCenters(每全景 xyz);面未映射(gi<0 或越界)填 0。
|
|
574
|
+
* rebuildWallGeometry / rebuildPanoIndexAttribute / setFacePanoIndex 都会调它保持同步。
|
|
575
|
+
*/
|
|
576
|
+
private _syncPanoCenterAttr(): void {
|
|
577
|
+
const faceCount = this.panoFaceMapping.length;
|
|
578
|
+
const posAttr = this.geometry.getAttribute("position") as BufferAttribute | undefined;
|
|
579
|
+
if (!posAttr || posAttr.count !== faceCount * 3) return; // 几何与映射不一致先跳过
|
|
580
|
+
const centers = this.panoCenters;
|
|
581
|
+
const nPano = centers.length / 3;
|
|
582
|
+
let attr = this.geometry.getAttribute("aPanoCenter") as Float32BufferAttribute | undefined;
|
|
583
|
+
if (!attr || attr.count !== faceCount * 3) {
|
|
584
|
+
attr = new Float32BufferAttribute(new Float32Array(faceCount * 9), 3);
|
|
585
|
+
this.geometry.setAttribute("aPanoCenter", attr);
|
|
586
|
+
}
|
|
587
|
+
const arr = attr.array as Float32Array;
|
|
588
|
+
for (let f = 0; f < faceCount; f++) {
|
|
589
|
+
const gi = this.panoFaceMapping[f] ?? -1;
|
|
590
|
+
let cx = 0, cy = 0, cz = 0;
|
|
591
|
+
if (gi >= 0 && gi < nPano) {
|
|
592
|
+
cx = centers[gi * 3]; cy = centers[gi * 3 + 1]; cz = centers[gi * 3 + 2];
|
|
593
|
+
}
|
|
594
|
+
const a = 9 * f; // 每三角面 9 数,与 position 同布局
|
|
595
|
+
for (let v = 0; v < 3; v++) {
|
|
596
|
+
const o = a + v * 3;
|
|
597
|
+
arr[o] = cx; arr[o + 1] = cy; arr[o + 2] = cz;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
attr.needsUpdate = true;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** 设置纹理列表并激活第一个(或最近的)纹理 */
|
|
604
|
+
setTextures(textures: MktTextureInfo[]): void {
|
|
605
|
+
// ⭐ 皇上旨 2026-10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」⇒ 换工程**不**清 `_texCache`
|
|
606
|
+
// (旧项目的瓦片留到本对象 dispose 再释放;清它等于下次切回来重走一遍下载 + 解码 = 卡顿源头)。
|
|
607
|
+
// 要清的只有**名册**:里面躺的是上一批 `MktTextureInfo` 对象引用,留着会让下一帧拿已删景点的 URL 去喂槽。
|
|
608
|
+
this._slotPanos = [];
|
|
609
|
+
this.textures = textures;
|
|
610
|
+
this.activeIndex = -1;
|
|
611
|
+
this._syncPanoCenters(); // ⭐ 2026-09-30:派生全景点坐标(与纹理解耦)
|
|
612
|
+
if (this.textures.length > 0) {
|
|
613
|
+
this.activateTexture(0);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** 追加一个纹理 */
|
|
618
|
+
addTexture(info: MktTextureInfo): void {
|
|
619
|
+
this.textures.push(info);
|
|
620
|
+
this._syncPanoCenters();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** 删除一个纹理;若删除的是当前激活纹理则回退,索引随之前移 */
|
|
624
|
+
removeTexture(index: number): void {
|
|
625
|
+
if (index < 0 || index >= this.textures.length) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
this.textures.splice(index, 1);
|
|
629
|
+
this._syncPanoCenters();
|
|
630
|
+
// ⭐ 10-03 旨⑤:缓存键是**地址**不是「全景序号」⇒ 删点导致序号整体前移**不需要**清缓存
|
|
631
|
+
// (旧写法 `_invalidateTileCache()` 是因为按序号缓存、不清就把旧地址挂在新序号上);
|
|
632
|
+
// 名册存对象引用 ⇒ 被删的那个对象下一帧自然从名册里掉出去(`panoSlotRosterStep` 只认 dists 里还有的人)。
|
|
633
|
+
if (this.activeIndex === index) {
|
|
634
|
+
this.activeIndex = -1;
|
|
635
|
+
} else if (this.activeIndex > index) {
|
|
636
|
+
this.activeIndex--;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* 更新单个纹理的 url / 名称 / 矩阵;若改的是「当前激活纹理」则同步生效。
|
|
642
|
+
*
|
|
643
|
+
* ⚠️ 需求铁律(防止回归):墙面绘制的是「当前激活纹理」,其朝向由 shader 的 textureMatrix uniform 决定。
|
|
644
|
+
* 所以只要激活纹理的位置/旋转变了,必须**立即**把新矩阵 copy 进 textureMatrix uniform——
|
|
645
|
+
* 否则用户旋转/移动全景球时,平面纹理不会跟着一起转。
|
|
646
|
+
* 另:改旋转不要触发纹理网络 reload,只有 url 真正变化才重载。
|
|
647
|
+
*/
|
|
648
|
+
setTexture(index: number, opts: { url?: string; name?: string; matrix?: Matrix4; tiles?: string[] }): void {
|
|
649
|
+
if (index < 0 || index >= this.textures.length) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const cur = this.textures[index];
|
|
653
|
+
if (opts.url !== undefined) {
|
|
654
|
+
cur.url = opts.url;
|
|
655
|
+
}
|
|
656
|
+
// 名称只是标识,不参与渲染,改它**不**触发纹理 reload(见下方 opts.url 分支)
|
|
657
|
+
if (opts.name !== undefined) {
|
|
658
|
+
cur.name = opts.name;
|
|
659
|
+
}
|
|
660
|
+
if (opts.tiles !== undefined) {
|
|
661
|
+
cur.tiles = opts.tiles;
|
|
662
|
+
}
|
|
663
|
+
if (opts.matrix !== undefined) {
|
|
664
|
+
cur.matrix.copy(opts.matrix);
|
|
665
|
+
this._syncPanoCenters(); // ⭐ 全景位置/旋转一变,全景点坐标同步派生
|
|
666
|
+
}
|
|
667
|
+
if (index === this.activeIndex) {
|
|
668
|
+
// 关键:激活纹理的矩阵一变,立刻同步到 shader uniform(墙面纹理才会跟着转)。
|
|
669
|
+
// ⚠️ 基类材质现改为融合形态(textureMatrices 数组),无单数 textureMatrix;
|
|
670
|
+
// 单全景形态用 textureMatrix、融合形态用 textureMatrices[0],两者都接住。
|
|
671
|
+
const tm = (this.material.uniforms.textureMatrix?.value ??
|
|
672
|
+
this.material.uniforms.textureMatrices?.value?.[0]) as Matrix4 | undefined;
|
|
673
|
+
if (tm) tm.copy(cur.matrix);
|
|
674
|
+
// ⭐ 10-03 旨⑤:改地址/瓦片**不需要**在这里手工失效重载 —— 缓存按地址建键,新 URL 天然是新条目,
|
|
675
|
+
// 下一帧 `feedFaceMapping` 见「账本里的地址 ≠ 目标地址」就走整槽换人(判不齐则沿用上一批),
|
|
676
|
+
// 跨形态(32 片拼合 ↔ 整片)那一次由 binder 的整档判齐支负责。旧写法(清序号缓存 + 重新 activate)已整条删除。
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* 把 inverse(matrixWorld) 写进当前材质的 uProjInvMatrix(每帧一次)—— 「投影与自身变换解耦」的唯一写入点。
|
|
682
|
+
*
|
|
683
|
+
* ⚠️ 需求铁律(皇上旨 2026-09-19):「墙的矩阵和全景的矩阵是相对的,无论 MktMesh 模型整体
|
|
684
|
+
* 怎么移动/旋转/缩放,墙上的纹理都保持不变」。
|
|
685
|
+
* 达成条件是「取样方向的两个操作数都对 MktMesh 的整体变换免疫」:
|
|
686
|
+
* · 全景点矩阵(textureMatrix)是**世界矩阵**、属独立于 MktMesh 的外部数据 → 本就不随它动;
|
|
687
|
+
* · 墙顶点经 modelMatrix 升到世界坐标 → **会**随它动 → 一个动一个不动,方向即变,纹理在墙上滑动。
|
|
688
|
+
* 故此处把 inverse(matrixWorld) 交给着色器,把墙顶点也拉回「与整体变换无关的几何局部坐标」,
|
|
689
|
+
* 两个操作数双双免疫,方向恒定,纹理稳稳贴在墙上。
|
|
690
|
+
* ⚠️ M ≠ I 时 vProjPos(局部)与 textureMatrix(世界)并不在同一空间 —— 这是**刻意**的:
|
|
691
|
+
* 皇上要的是「纹理贴在墙上不动」,条件是「两个操作数都不随 M 变」,而非「同处一个空间」。
|
|
692
|
+
* 千万别"好心"再去把 textureMatrix 也乘一遍 inverse(matrixWorld),那会让纹理重新动起来。
|
|
693
|
+
*
|
|
694
|
+
* 每帧无条件重算(一次 4x4 求逆,代价可忽略):用户拖预览变换面板时 matrixWorld 每帧都在变,
|
|
695
|
+
* 靠「变更时通知」容易漏,无条件同步最稳。材质被替换(如编辑态 blend↔single 重建)时按当前材质重新取
|
|
696
|
+
* uniform,故 **MktDevMesh 也必须每帧调用本方法**(它同样会重建材质,见该类 onBeforeRender 注释)。
|
|
697
|
+
* 材质没有该 uniform(旧材质/第三方材质)时静默跳过 —— 退化为「世界坐标」的旧行为。
|
|
698
|
+
*/
|
|
699
|
+
syncProjInvMatrix(): void {
|
|
700
|
+
const mat = this.material as ShaderMaterial;
|
|
701
|
+
const u = mat && mat.uniforms ? mat.uniforms.uProjInvMatrix : undefined;
|
|
702
|
+
if (!u) return;
|
|
703
|
+
(u.value as Matrix4).copy(this.matrixWorld).invert();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ───────────────────────── 每帧最近纹理计算 ─────────────────────────
|
|
707
|
+
|
|
708
|
+
/** 每帧算「离**名册锚点**最近」的水平距离(XZ)⇒ 走**槽位名册**(中心 ± 邻居)⇒ 名册中心变化时切换激活纹理。
|
|
709
|
+
* ⭐ 皇上旨 2026-10-07 第二十一道旨:锚点 = 外部控制器的中心点(`setRosterCenter` 注入的轨道注视点);
|
|
710
|
+
* 没注入(如发布页那一路)才回落相机位置。 */
|
|
711
|
+
updateTexture(camera: Camera): void {
|
|
712
|
+
const n = this.textures.length;
|
|
713
|
+
if (n === 0) {
|
|
714
|
+
this._slotPanos = [];
|
|
715
|
+
this.activeIndex = -1;
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const ref = this._rosterCenter ?? camera.position;
|
|
719
|
+
const dists: { t: MktTextureInfo; d: number }[] = [];
|
|
720
|
+
for (let i = 0; i < n; i++) {
|
|
721
|
+
const e = this.textures[i].matrix.elements;
|
|
722
|
+
this._tmpV3.set(e[12], e[13], e[14]);
|
|
723
|
+
// ⚠️ 全景点矩阵**本身就是世界矩阵**(见上方 textures 字段注释),位置已在世界空间,
|
|
724
|
+
// 可直接与锚点的世界坐标比距离 —— 这里**绝不能再乘 matrixWorld**:那是双重变换,
|
|
725
|
+
// MktMesh 一有整体变换就会算错「离锚点最近的是哪张」(选错全景)。
|
|
726
|
+
// (2026-09-19 曾误以为它记在模型空间而多乘了一次,趁 MktDevMesh 对齐时一并纠正。)
|
|
727
|
+
// ⭐ 09-28 09:33 旨:挑图与编辑器取图同一把「**水平距离**」尺(XZ 平面,不含高度差)。
|
|
728
|
+
// const d = this._tmpV3.distanceTo(camera.position); // 旧原理:三维直线距离——全景点同高(y 恒 1.6)时与本式排序等价,注释保留备查
|
|
729
|
+
// ⭐ 10-07 第二十一道旨:尺子的另一端从相机位置换成 `ref`(控制器中心点;未注入时它就是相机位置)。
|
|
730
|
+
dists.push({ t: this.textures[i], d: Math.hypot(this._tmpV3.x - ref.x, this._tmpV3.z - ref.z) });
|
|
731
|
+
}
|
|
732
|
+
// ⭐ 皇上旨 2026-10-03「1.可以」:预览/发布态的 8 格**也**按「中心 ± 邻居」名册挑(算法与死区常数
|
|
733
|
+
// 都在 ./panoSlotRoster,编辑态那份读的是同一处 ⇒ 预览与编辑器逐字同一批全景);
|
|
734
|
+
// ⛔ 不再按「引用面数取前 4」(getReferencedPanoIndices 那套静态名单只留作公共 API,不进喂料路)。
|
|
735
|
+
this._slotPanos = panoSlotRosterStep(this._slotPanos, dists, MKT_PANO_MAX_SLOTS, PANO_SLOT_SWAP_MARGIN_M);
|
|
736
|
+
// ⭐ 10-03 旨②:中心那张不再要「抄给立方体」(立方体已与墙共用同一份材质),但**仍**留 `activeIndex`
|
|
737
|
+
// 作为「名册中心有没有换人」的闩锁(下面的比较 + 外部只读),换人才重新激活。
|
|
738
|
+
const center = this._slotPanos[0];
|
|
739
|
+
const target = center ? this.textures.indexOf(center) : -1;
|
|
740
|
+
if (target >= 0 && target !== this.activeIndex) {
|
|
741
|
+
this.activateTexture(target);
|
|
742
|
+
}
|
|
743
|
+
// 每帧**只**驱动一次拼合器(皇上旨 09-29「一帧里别同时处理很多纹理」+ 10-06「每帧只载入一张新纹理到 GPU」
|
|
744
|
+
// ⇒ 拼合器内部分两段:每帧最多 1 次新纹理上传,画进 RT 只认已在显存里的那几格):
|
|
745
|
+
// 下面的 `feedFaceMapping` 与各槽 `ensureTileTexture` 一律只 peek,不在这里重复 pump。
|
|
746
|
+
// ⭐ 10-06:RT 要在 renderer 上才画得进去 —— `_glRenderer` 由 `onBeforeRender` 在**这一句之前**捕获
|
|
747
|
+
// (构造后头一帧还没捕获到就传 null:那一帧什么都不画,下一帧自然补上)。
|
|
748
|
+
this._stitcher.pump(this._glRenderer);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** 给纹理地址追加 OSS 图片缩放参数(1024 / 2048 / 空=不缩放)。
|
|
752
|
+
* 尺寸由外部 setOssSize 控制(默认 1024,与编辑器渲染器 resolveOssUrl 语义一致);
|
|
753
|
+
* 预览/发布库经 setOssSize 注入,使全景球按所选尺寸加载。
|
|
754
|
+
* ⚠️ `original=true`(这一槽喂的是**真瓦片地址**,即 rt 虚拟键那一路)时**一律走原图**、不拼尺寸参数
|
|
755
|
+
* (皇上旨 2026-09-23「切换到4片瓦片的时候, 就不需要设置oss 尺寸参数了, 默认使用原图」)。
|
|
756
|
+
* 判据与喂地址、写 `uPanoTiled` 同源(`mktPanoTiledFlag`),别另算一套。 */
|
|
757
|
+
private resolveOssUrl(url: string, original = false): string {
|
|
758
|
+
// ⭐ 32 瓦片拼合的虚拟键不是真 URL(纹理由 PanoTileStitcher 提供),原样放行、绝不拼 OSS 参数
|
|
759
|
+
if (!url || url.startsWith(RT_STITCH_KEY_PREFIX) || url.includes("x-oss-process")) {
|
|
760
|
+
return url;
|
|
761
|
+
}
|
|
762
|
+
if (original) return url;
|
|
763
|
+
const size = this._ossSize;
|
|
764
|
+
if (!size) return url;
|
|
765
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
766
|
+
return `${url}${sep}x-oss-process=image/resize,m_lfit,w_${size}`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** ⭐ 皇上旨 2026-10-07 第二十一道旨:把「用哪些全景球」那把尺子的锚点从相机位置换成**控制器中心点**。
|
|
770
|
+
* 消费方(预览弹窗)把轨道控制器的 `target` 交进来即可;传 `null` = 回落相机位置(老口径,发布页照旧)。
|
|
771
|
+
* 存引用不存副本 ⇒ 轨道控制器原地改那个 Vector3,设一次就一直跟着走。 */
|
|
772
|
+
setRosterCenter(center: Vector3 | null): void {
|
|
773
|
+
this._rosterCenter = center;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** 设置纹理 OSS 缩放尺寸(1024 / 2048 / 空=原图);变化即按新尺寸重载当前激活纹理。
|
|
777
|
+
* 编辑器 MktDevMesh 关闭了 activateTexture(activeIndex=-1),此处为 no-op;预览/发布库用基类激活,会真正按新尺寸重载。 */
|
|
778
|
+
setOssSize(size: "1024" | "2048" | ""): void {
|
|
779
|
+
const next = size === "1024" || size === "2048" || size === "" ? size : "1024";
|
|
780
|
+
if (next === this._ossSize) return;
|
|
781
|
+
this._ossSize = next;
|
|
782
|
+
this._reloadActiveTexture();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** ossSize 换档后重载「加载地址拼了尺寸参数」的那批条目(与编辑态 `_reloadFloorTextures` 同口径:
|
|
786
|
+
* dispose + 删条目,下一帧 `_peekTileAt` 自然按新档重拉)。
|
|
787
|
+
* ⚠️ 真瓦片(现行 32 片/全景)一律走原图 ⇒ 不在重载范围;整图档才受尺寸档影响(皇上旨 09-23)。 */
|
|
788
|
+
private _reloadActiveTexture(): void {
|
|
789
|
+
if (this._ossSizeDepKeys.size === 0) return;
|
|
790
|
+
for (const key of this._ossSizeDepKeys) {
|
|
791
|
+
releasePanoTexture(this._texCache.get(key)); // 换档丢掉的这张要连位图一起关(见 `releasePanoTexture`)
|
|
792
|
+
this._texCache.delete(key);
|
|
793
|
+
}
|
|
794
|
+
this._ossSizeDepKeys.clear();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ───────────── 全景瓦片缓存(按地址 + 每 tick 一张上传;与编辑态同一份策略) ─────────────
|
|
798
|
+
|
|
799
|
+
/** 换绑一个点槽的左右 2 张拼合槽纹理:策略全在 `./panoSlotBind`(皇上旨 10-03「整套搬进库」⇒ 编辑器与库**同一份**),
|
|
800
|
+
* 这里只把本类的取图口与占位纹理注入进去。返回 true = 图与点位配好了(调用方才写投影矩阵)。 */
|
|
801
|
+
private _bindSlotTiles(
|
|
802
|
+
mat: ShaderMaterial, panos: Texture[], base: number, tiles: string[], tiled: boolean, slot: number
|
|
803
|
+
): boolean {
|
|
804
|
+
return panoSlotBind(mat, panos, base, tiles, tiled, slot, {
|
|
805
|
+
peek: (key, original) => this._peekTileAt(key, original),
|
|
806
|
+
// 跨形态(瓦片 ↔ 整片)那一次要「整槽同进同退」:库里没有过渡档那一层,逐张 peek 攒成整槽即可。
|
|
807
|
+
// ⛔ 长度由 `RT_STITCH_SLOT_COUNT` 派生(10-06 晚旨后是 2 张,⛔ 别写死 4 个下标)
|
|
808
|
+
peekSlot: (t, original) =>
|
|
809
|
+
Array.from({ length: RT_STITCH_SLOT_COUNT }, (_, k) => this._peekTileAt(t[k], original)),
|
|
810
|
+
placeholder: this._placeholder,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** 本材质「panoramas[i] 这一格认的是哪张瓦片地址」——逐张换绑的账本(⚠️ 账本实体在 `./panoSlotBind`,
|
|
815
|
+
* 与编辑态共一本,按材质实例分键 ⇒ 两处材质不同实例、天然不串味)。 */
|
|
816
|
+
private _tileKeys(mat: ShaderMaterial, len: number): string[] {
|
|
817
|
+
return panoSlotTileKeys(mat, len);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** 按缓存键要一张**已经设进 shader** 的纹理:没有就当场发起下载(异步代码一律不进全局队列 —— 皇上旨 2026-09-25),
|
|
821
|
+
* 解码完还没上传的排一个「只干上传那一句」的队列项;两关都过了才返回给它去绑。
|
|
822
|
+
* ⭐ `rt:` 虚拟键(32 瓦片拼合槽)走拼合器 peek;⛔ 不在这里 pump(每帧驱动点唯一,见 `updateTexture`,
|
|
823
|
+
* 否则一帧画好几格 = 把「每帧只载入一张新纹理」那道闸从后门拆了)。 */
|
|
824
|
+
private _peekTileAt(key: string, original: boolean): Texture | null {
|
|
825
|
+
if (isRtStitchKey(key)) {
|
|
826
|
+
const p = rtStitchKeyParts(key);
|
|
827
|
+
if (!p) return null;
|
|
828
|
+
const tex = this._stitcher.peek(p.batch, p.slot);
|
|
829
|
+
if (!tex) return null;
|
|
830
|
+
// ⭐ 皇上旨 09-29「两个大头别在同一帧」:普通瓦片不许在 render 首绑现场 texSubImage2D ⇒ 走
|
|
831
|
+
// 「每 tick 一张」预上传队列;没排到的帧返回 null,自动推迟绑定 ⇒ render 里从此没有首绑上传。
|
|
832
|
+
// ⭐ 皇上旨 10-06 换成 RT 那一份后这一闸**自动跳过**:RT 的附件纹理本来就在显存里,
|
|
833
|
+
// 压根没有「上传」那一下(⛔ 也别拿 `initTexture` 去喂它 —— 那是 three 自己管的渲染目标)。
|
|
834
|
+
if (!this._stitcher.resident(tex) && !this._texUploaded.has(tex)) {
|
|
835
|
+
this._commitTileToShader(tex);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
return tex;
|
|
839
|
+
}
|
|
840
|
+
const tex = this._texCache.get(key);
|
|
841
|
+
if (!tex) {
|
|
842
|
+
// 未开始或加载中:直接发起(缓存里以 null 占位 ⇒ 同一地址不会被问两次就下两遍)。
|
|
843
|
+
// 发起只是把 XHR 丢出去(微秒级),主线程成本可忽略;真正贵的解码/上传各自有闸。
|
|
844
|
+
this.ensureTileTexture(key, original);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
if (!this._texUploaded.has(tex)) {
|
|
848
|
+
this._commitTileToShader(tex); // 已解码但还没上 GPU:排它自己的那一个 tick,这一帧不绑
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
return tex;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** 真正发起一张瓦片纹理的加载(`key` = 原始地址;rt 虚拟键由 `_peekTileAt` 上半段拦走,不会到这里)。
|
|
855
|
+
* ⚠️ 皇上旨 2026-09-28「加载瓦片的时候, 瓦片纹理不需要添加 oss 的参数」⇒ 真瓦片(`original=true`,现行 32 片/全景)走原图,
|
|
856
|
+
* 整图档才按 ossSize 档缩(`resolveOssUrl`)。 */
|
|
857
|
+
private ensureTileTexture(key: string, original = false): Texture | null {
|
|
858
|
+
const cached = this._texCache.get(key);
|
|
859
|
+
if (cached) return cached;
|
|
860
|
+
if (cached === null) return null; // 加载中
|
|
861
|
+
this._texCache.set(key, null); // 标记加载中
|
|
862
|
+
const loadUrl = this.resolveOssUrl(key, original);
|
|
863
|
+
if (loadUrl !== key) this._ossSizeDepKeys.add(key);
|
|
864
|
+
loadPanoTexture(loadUrl).then(
|
|
865
|
+
(tex) => {
|
|
866
|
+
tex.colorSpace = SRGBColorSpace;
|
|
867
|
+
// 该键可能已被 `_reloadActiveTexture`(换档)删掉并重新发起 ⇒ 按键写回即可:地址就是本条目的身份,
|
|
868
|
+
// 不存在「过期回调写脏另一个地址」的问题(与编辑态 `_floorTexCache` 同一口径)。
|
|
869
|
+
this._texCache.set(key, tex);
|
|
870
|
+
// 解码已在浏览器工作线程做完(甲)⇒ 这里只排它**自己那一个**队列项(只含「上传 GPU」那一句):
|
|
871
|
+
// 一批里几张同时解完 ⇒ 各自排各自的 tick 逐张落地,上传那一拍不再顺带同步解码。
|
|
872
|
+
this._commitTileToShader(tex);
|
|
873
|
+
},
|
|
874
|
+
() => {
|
|
875
|
+
// 单张失败:摘掉「加载中」占位 ⇒ 下一帧同键判定仍是未就绪,自动重试(旧库路径本来就会重试,口径不变)。
|
|
876
|
+
if (this._texCache.get(key) === null) this._texCache.delete(key);
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** ⭐ 把一张「已经加载解码好」的纹理排进全局队列,排到的那个 tick 只做一件事:把它上传到 GPU。
|
|
883
|
+
* 皇上旨 2026-09-25 原文:「队列里面的代码就是 把加载好的纹理 设置给 shader 的 这一句代码」——
|
|
884
|
+
* 所以下载、淘汰、绑定判断统统不许出现在回调里。
|
|
885
|
+
* ⚠️ 去重闸门必须是自己的 `_uploadQueued`:`addWork` 用 `workList.includes(work)` 判重,而每次都是新闭包 ⇒ 判不出来。
|
|
886
|
+
* ⭐ 皇上旨 2026-10-05:「你在调用 addWork 的时候,再调一次 addWork 传入一个空函数,这样真正需要执行的方法就会隔一帧执行」
|
|
887
|
+
* ⇒ 真活前面先垫一个空函数占一格(全局队列每 tick 只跑一项 ⇒ 多一格 = 晚一帧),与编辑态 `_commitTileToShader` 同一口径。
|
|
888
|
+
* ⚠️ 空函数**必须内联每次现造**:`addWork` 按引用判重,共用一个常量引用时第二张起空格进不了队列 ⇒ 改动静默失效。 */
|
|
889
|
+
private _commitTileToShader(tex: Texture): void {
|
|
890
|
+
if (this._texUploaded.has(tex) || this._uploadQueued.has(tex)) return;
|
|
891
|
+
this._uploadQueued.add(tex);
|
|
892
|
+
threeUtils.addWork(() => {}); // 占一格(什么都不干),把下面那一项推到下一个 tick
|
|
893
|
+
threeUtils.addWork(() => this._uploadTileTexture(tex));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** 队列回调的全部内容:把这一张纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的),每 tick 一张。 */
|
|
897
|
+
private _uploadTileTexture(tex: Texture): void {
|
|
898
|
+
// 排上队之后已被清掉(那张 dispose 过了)⇒ 本项作废:再上传会把显存凭空建回来且无人回收。
|
|
899
|
+
if (!this._uploadQueued.delete(tex)) return;
|
|
900
|
+
try {
|
|
901
|
+
// `_glRenderer` 还没被 `onBeforeRender` 捕获到(构造后尚未渲染过一帧)⇒ 跳过预上传,
|
|
902
|
+
// 交给 three 在绑定那一帧自己上传 —— 那是改造前的老路径,不会报错,只是那一张没享受「错峰」。
|
|
903
|
+
if (this._glRenderer) this._glRenderer.initTexture(tex);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
console.warn("[MktMesh] 瓦片预上传失败,交给常规渲染路径自行上传:", e);
|
|
906
|
+
}
|
|
907
|
+
// ⚠️ 成败都记「已处理」。不记 ⇒ 这张永远进不了 `_texUploaded` ⇒ 那一格永远换不上新图,
|
|
908
|
+
// 比「卡一下」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
|
|
909
|
+
this._texUploaded.add(tex);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* 序号直接取模式喂食(皇上旨 2026-09-28 决策③:MktMesh 基类只有这一种模式)。
|
|
914
|
+
* ⭐ 皇上旨 2026-10-03「1.可以」:shader 各槽装的是**槽位名册**那 `MKT_PANO_MAX_SLOTS` 张(`_slotPanos`,
|
|
915
|
+
* 中心 ± 邻居,与编辑态 `mktMeshEditRenderer.feedFaceMapping` 同一份算法、同一个死区常数)
|
|
916
|
+
* ⇒ 预览/发布与编辑器同一屏装同一批全景。⛔ 不再按「被引用面数取前 4」(那是 09-28 的静态名单,
|
|
917
|
+
* 与相机无关 ⇒ 走近的景点进不了槽,正是他这次报「预览里 shader 不对」的根因)。
|
|
918
|
+
* 片元按 aPanoIndex 在 panoIndices 里找对应 slot 取纹理;指派的那张不在当前名册里 ⇒ 该面回落 `uFaceMissingColor` 深灰,
|
|
919
|
+
* 把**名册锚点**挪近那张景点后自动出图(⭐ 10-07 第二十一道旨:预览轨道模式 = 控制器中心点,第一人称/未注入 = 相机位置;与编辑态同一份已知代价)。
|
|
920
|
+
*/
|
|
921
|
+
feedFaceMapping(): void {
|
|
922
|
+
this._stitcher.pump(this._glRenderer); // 每帧驱动拼合器(≤1 片/帧;下方逐槽只 peek 不驱动)
|
|
923
|
+
const mat = this.material as ShaderMaterial;
|
|
924
|
+
const u = mat.uniforms;
|
|
925
|
+
if (!u.panoIndices) return; // 非融合材质(防御)
|
|
926
|
+
u.uPanoramaVisible.value = 1;
|
|
927
|
+
// 复用统一喂料:填 panoramas[]/textureMatrices[]/weights[] + uPanoramaVisible
|
|
928
|
+
// (材质本身就是序号直取 shader,恒按 panoIndices 选槽取图——原 uFaceMappingMode=1 翻转已随拆分删除;
|
|
929
|
+
// ⚠️ 本函数**从来没写过** uColorSpace:那句旧注释是骗人的。色彩空间 2026-10-07 已整条退路——
|
|
930
|
+
// 纹理恒标 SRGBColorSpace 由硬件解码一次,shader 不再手动 pow,故连 uniform 都不存在了。)
|
|
931
|
+
const maxPano = (u.panoIndices.value as Float32Array).length;
|
|
932
|
+
const panos = u.panoramas.value as Texture[];
|
|
933
|
+
const mats = u.textureMatrices.value as Matrix4[];
|
|
934
|
+
const ws = u.weights.value as Float32Array;
|
|
935
|
+
const idxU = u.panoIndices.value as Float32Array;
|
|
936
|
+
const keys = this._tileKeys(mat, panos.length);
|
|
937
|
+
for (let s = 0; s < maxPano; s++) {
|
|
938
|
+
const base = s * RT_STITCH_SLOT_COUNT; // ⛔ 不是 s*4:10-06 晚旨后一个全景只占左右 2 槽
|
|
939
|
+
const info = this._slotPanos[s]; // 名册没这一格(项目全景总数不够 / 首帧未算)⇒ 走下面的清空支
|
|
940
|
+
if (!info) {
|
|
941
|
+
for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) panos[base + k] = this._placeholder;
|
|
942
|
+
mats[s].identity();
|
|
943
|
+
ws[s] = 0;
|
|
944
|
+
idxU[s] = -1;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
idxU[s] = this.textures.indexOf(info);
|
|
948
|
+
// 各槽喂什么地址 = 「用户选的形态 + 该点有没有切过片」共同决定(whole 或无 tiles ⇒ 各槽同为一张整图)
|
|
949
|
+
const tiles = mktPanoTexUrls(info.url, info.tiles, this._panoTexMode);
|
|
950
|
+
// ⚠️ 「是不是真瓦片」只算一次:喂地址(走不走 OSS 尺寸档)与写 uPanoTiled 共用同一个值
|
|
951
|
+
const tiled = mktPanoTiledFlag(info.tiles, this._panoTexMode) === 1;
|
|
952
|
+
// ⭐ 10-06 晚旨「只支持 32 张(8×4)」:任意 1~32 条瓦片整组交拼合器(不足 32 张的格子涂灰);
|
|
953
|
+
// 这 2 槽是虚拟键,这里只负责「每帧把真 URL 喂给拼合器」(虚拟键本身不含完整地址)。
|
|
954
|
+
if (tiled && info.tiles && info.tiles.length > 0) this._stitcher.ensure(info.tiles);
|
|
955
|
+
// ⭐ 皇上旨 2026-10-03「整套搬进库」:换绑成败由**共用的那份策略**判定(未就绪 ⇒ 整槽沿用上一批)。
|
|
956
|
+
// 没换成就**矩阵也别动** ⇒ 旧图永远配旧矩阵(原来矩阵无条件 copy = 新矩阵配旧图,正是②的错配)。
|
|
957
|
+
const bound = this._bindSlotTiles(mat, panos, base, tiles, tiled, s);
|
|
958
|
+
if (bound) mats[s].copy(info.matrix);
|
|
959
|
+
// 左上格当整槽的代表。⚠️ 判据必须**同时**看账本:`keys[base]` 为空串 ⇒ 这座槽**从没成功绑过**,
|
|
960
|
+
// 此刻 `panos[base]` 里躺的是占位/工厂默认 1×1,而矩阵还是单位阵 —— 给它权重等于拿「世界原点」
|
|
961
|
+
// 当投影中心投一片素色。已绑过的槽(账本非空)换人没判齐时权重照给 ⇒ 沿用上一批,不整体退出。
|
|
962
|
+
const hasReal = !!keys[base] && !!panos[base] && panos[base] !== this._placeholder;
|
|
963
|
+
// 库侧单点不混合 ⇒ 权重恒 1(编辑态那份是距离反比 camW,这里没有「多张混出一个面」的事)
|
|
964
|
+
ws[s] = hasReal ? 1 : 0;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** 设置全景纹理形态(tiled=8×4 三十二片走拼合器 / whole=一整片原图);非法值回落 tiled。
|
|
969
|
+
* ⭐ 10-03 旨⑤:这里**不再**手工重载纹理 —— 形态一变目标地址就变了(瓦片 URL ↔ 整图 URL),
|
|
970
|
+
* 下一帧 `feedFaceMapping` 里共用的那份 binder 见「槽内现有形态 ≠ 目标形态」自动走整档判齐、一次换绑并同帧翻标记。
|
|
971
|
+
* 编辑器 MktDevMesh 走的是自己那路(同一个 binder、同一份策略),本方法对它只是记下形态。 */
|
|
972
|
+
setPanoTexMode(mode: PanoTexMode): void {
|
|
973
|
+
const next = isPanoTexMode(mode) ? mode : "tiled";
|
|
974
|
+
if (next === this._panoTexMode) return;
|
|
975
|
+
this._panoTexMode = next;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* 记下「当前激活(名册 0 号槽 = 中心)是第几个全景」。
|
|
980
|
+
* ⭐ 皇上旨 2026-10-03 旨②:预览立方体已与墙**共用同一份材质**,原来那句「把中心那几张抄给立方体单全景材质」
|
|
981
|
+
* 的 `currentTiles` 旁路整条删除 ⇒ 本方法不再加载瓦片、不再写 `uPanoTiled`(形态标记归 binder 逐槽写,
|
|
982
|
+
* 地址/标记/分辨率三者同源的判据在 `feedFaceMapping`)。留着它是因为 `updateTexture` 用它做「中心换人」的闩锁,
|
|
983
|
+
* 且它是发布态的公共 API(外部只读 `activeIndex` 靠这里维护)。
|
|
984
|
+
*/
|
|
985
|
+
activateTexture(index: number): void {
|
|
986
|
+
if (index < 0 || index >= this.textures.length) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.activeIndex = index;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ───────────────────────── 导入 / 导出 ─────────────────────────
|
|
993
|
+
|
|
994
|
+
/** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵) */
|
|
995
|
+
exportData(): MktMeshData {
|
|
996
|
+
return {
|
|
997
|
+
wallPoints: this.wallPoints.map((p) => ({ x: p.x, y: p.y, z: p.z })),
|
|
998
|
+
wallHeight: this.wallHeight,
|
|
999
|
+
wallClosed: this.wallClosed,
|
|
1000
|
+
wallSide: this.wallSide,
|
|
1001
|
+
// 全景序号映射:每个三角面归属的全景全局序号(与 wallPoints 同几何拓扑绑定,随存档走)
|
|
1002
|
+
panoFaceMapping: this.panoFaceMapping.slice(),
|
|
1003
|
+
textures: this.textures.map((t) => ({
|
|
1004
|
+
// ⭐ 10-08 旨:存档里只落**根相对路径**(域名在读 JavaScript 之外也用不上,还锁死环境)。
|
|
1005
|
+
// `stripAssetHost` 幂等 ⇒ 内存里已经是相对的不会出问题,旧存档的绝对地址也不会反复剥。
|
|
1006
|
+
url: stripAssetHost(t.url),
|
|
1007
|
+
name: t.name ?? "",
|
|
1008
|
+
// 2026-09-20:导出 4 瓦片(顺序 [左上, 右上, 左下, 右下]);旧数据无 tiles 时导空数组
|
|
1009
|
+
tiles: (t.tiles && t.tiles.length ? t.tiles.map(stripAssetHost) : []) as string[],
|
|
1010
|
+
matrix: t.matrix.toArray(),
|
|
1011
|
+
})),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** 导入数据:重建墙体几何与纹理列表,并激活第一个纹理 */
|
|
1016
|
+
importData(data: MktMeshData): void {
|
|
1017
|
+
this.setWallPoints(data.wallPoints ?? [], {
|
|
1018
|
+
closed: data.wallClosed,
|
|
1019
|
+
height: data.wallHeight,
|
|
1020
|
+
});
|
|
1021
|
+
// ⭐ 还原全景序号映射**放在 setTextures 之后**:rebuildPanoIndexAttribute 会顺手烘焙 aPanoCenter
|
|
1022
|
+
// (源 = panoCenters,由 textures 实时派生),排在 setTextures 前面 = 拿旧/空纹理列表算圆心 ⇒ 属性里全是 0。
|
|
1023
|
+
this.setWallSide(data.wallSide ?? "double");
|
|
1024
|
+
this.setTextures(
|
|
1025
|
+
(data.textures ?? []).map((t) => {
|
|
1026
|
+
// ⭐ 10-08 旨的成对另一半:存档落的是根相对路径 ⇒ 进内存前在这里**唯一一处**补回域名,
|
|
1027
|
+
// 下游(拼合器 / 预览 / UI `<img>`)拿到的仍是绝对地址 ⇒ 它们一行都不用改。
|
|
1028
|
+
// `absAssetUrl` 幂等 ⇒ 旧存档里已是绝对地址的(= 升级前存的)不会被拼成 `https://host/https://host/...`
|
|
1029
|
+
const urlAbs = absAssetUrl(t.url as string);
|
|
1030
|
+
return {
|
|
1031
|
+
url: urlAbs,
|
|
1032
|
+
name: t.name ?? "",
|
|
1033
|
+
// 2026-09-20:导入瓦片;缺省回退 [url](旧存档整图占 1 格)——10-06 晚旨「只支持 32 张(横 8 × 竖 4)」后
|
|
1034
|
+
// 该点走拼合器:整图缩在 1/32 格 + 其余 31 格涂灰,数据不全一眼可见(重传 32 片切片即修复)
|
|
1035
|
+
tiles: t.tiles && t.tiles.length ? t.tiles.map(absAssetUrl) : [urlAbs],
|
|
1036
|
+
matrix: new Matrix4().fromArray(t.matrix),
|
|
1037
|
+
};
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
1040
|
+
// setWallPoints 已按位置重算过映射(旧为空 → 全 -1),此处用存档里的逐面指派覆盖。
|
|
1041
|
+
// 几何拓扑由同一份 wallPoints 重建,面索引与导出时一致 ⇒ 逐面写入即可对齐。
|
|
1042
|
+
// ⭐ 皇上旨 2026-10-03「补」:长度不符**不再整份丢弃**——按较小长度逐面灌回(存档多出的面丢掉,
|
|
1043
|
+
// 墙变长多出来的那些面保持 setWallPoints 按位置继承到的值),并把两个数 warn 出来(不静默吞)。
|
|
1044
|
+
const imported = (data.panoFaceMapping && data.panoFaceMapping.length ? data.panoFaceMapping : []) as number[];
|
|
1045
|
+
const faceCount = this.panoFaceMapping.length;
|
|
1046
|
+
if (imported.length && imported.length !== faceCount) {
|
|
1047
|
+
console.warn(`[MktMesh] 存档序号映射面数 ${imported.length} ≠ 当前墙体面数 ${faceCount},按较小者逐面还原(差值来自墙点/墙高变化)`);
|
|
1048
|
+
}
|
|
1049
|
+
const restore = Math.min(imported.length, faceCount);
|
|
1050
|
+
if (restore > 0) {
|
|
1051
|
+
for (let f = 0; f < restore; f++) this.panoFaceMapping[f] = imported[f];
|
|
1052
|
+
this.rebuildPanoIndexAttribute();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ───────────────────────── 释放 ─────────────────────────
|
|
1057
|
+
|
|
1058
|
+
/** 释放 GPU 资源:瓦片缓存(**只有这里**清,皇上旨 2026-10-02)、拼合器、几何、材质 */
|
|
1059
|
+
dispose(): this {
|
|
1060
|
+
// ⚠️ rt 虚拟键从不进 `_texCache`(拼合槽纹理由 `_stitcher` 自己持有并释放),这里清的都是本类下载的 Texture。
|
|
1061
|
+
// ⭐ 甲(2026-10-05):`releasePanoTexture` 除 `dispose()` 外还要 `ImageBitmap.close()` —— 位图的原生内存
|
|
1062
|
+
// 不像 `<img>` 能等 GC,一张 270MB 挂着不关,切几轮全景就把进程吃爆。
|
|
1063
|
+
for (const tex of this._texCache.values()) releasePanoTexture(tex);
|
|
1064
|
+
this._texCache.clear();
|
|
1065
|
+
this._texUploaded = new WeakSet(); // WeakSet 无 clear ⇒ 整只换掉
|
|
1066
|
+
this._uploadQueued.clear(); // 排队项即便回调仍触发,也会因 `!this._uploadQueued.delete(tex)` 早退
|
|
1067
|
+
this._ossSizeDepKeys.clear();
|
|
1068
|
+
this._stitcher.dispose();
|
|
1069
|
+
this._glRenderer = null;
|
|
1070
|
+
if (this._placeholder) this._placeholder.dispose();
|
|
1071
|
+
this.geometry.dispose();
|
|
1072
|
+
this.material.dispose();
|
|
1073
|
+
return this;
|
|
1074
|
+
}
|
|
1075
|
+
}
|