three-base 2.0.196 → 2.0.198
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/EventMap.d.ts +17 -0
- package/EventMap.ts +19 -0
- package/MktMesh/MktMesh.d.ts +83 -19
- package/MktMesh/MktMesh.js +247 -79
- package/MktMesh/MktMesh.ts +247 -75
- package/MktMesh/imagePlane.d.ts +14 -0
- package/MktMesh/imagePlane.js +79 -3
- package/MktMesh/imagePlane.ts +79 -2
- package/MktMesh/modelSlot.d.ts +2 -0
- package/MktMesh/modelSlot.js +15 -0
- package/MktMesh/modelSlot.ts +15 -0
- package/MktMesh/panoBitmapTexture.js +5 -1
- package/MktMesh/panoBitmapTexture.ts +6 -1
- package/ThreeBase.js +1 -10
- package/ThreeBase.ts +1 -9
- package/package.json +1 -1
- package/pass/debugDepthState.d.ts +4 -8
- package/pass/debugDepthState.js +0 -4
- package/pass/debugDepthState.ts +4 -10
- package/pass/stroke_pass.js +8 -90
- package/pass/stroke_pass.ts +8 -94
- package/threeLoader.d.ts +14 -0
- package/threeLoader.js +48 -3
- package/threeLoader.ts +45 -3
package/MktMesh/MktMesh.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
Camera,
|
|
30
30
|
BackSide,
|
|
31
31
|
Float32BufferAttribute,
|
|
32
|
+
InstancedMesh,
|
|
33
|
+
Light,
|
|
32
34
|
Material,
|
|
33
35
|
Matrix4,
|
|
34
36
|
Object3D,
|
|
@@ -73,16 +75,35 @@ import { stripAssetHost, absAssetUrl } from "./assetUrl";
|
|
|
73
75
|
// 原先各住在本站两个渲染器里一份 ⇒ 下游直接用包里的 `MktMesh` 时,存档里的 `images`/`models` 两键没人读。
|
|
74
76
|
// 入库之后它们挂在 `MktMesh` **自己的子树**下,与墙同一棵变换 ⇒ 预览那三根 transform 滑杆搬得动整份展品。
|
|
75
77
|
import { ImagePlane, type ImagePlaneData } from "./imagePlane";
|
|
76
|
-
import { ModelSlot, type ModelSlotData } from "./modelSlot";
|
|
78
|
+
import { ModelSlot, glbFileName, type ModelSlotData } from "./modelSlot";
|
|
77
79
|
import { ImagePlaneTileLoader } from "./imageTileLoader";
|
|
78
80
|
|
|
79
|
-
/** 载入 JSON
|
|
80
|
-
* 口径 = 存档 `textures` 数组的前 n
|
|
81
|
-
|
|
81
|
+
/** 载入 JSON 时**优先**加载几个全景的素材的**默认档**(数值只写这一处)。
|
|
82
|
+
* 口径 = 存档 `textures` 数组的前 n 项(n = -1 视为整份全加载);其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。
|
|
83
|
+
* 2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」立的是 2;
|
|
84
|
+
* 2026-11-05 需求逐字「现在要默认把全景全部都要默认加载, 现在需要通过配置来设置, 提前加载多少个全景 -1 就是全加载 0就是不加载 1就是加载个 2就提前加载2个 一次类推, 默认加载前4个」⇒ 换成 4,并让这一档可被实例覆写。 */
|
|
85
|
+
const PANO_IMPORT_PRIORITY_DEFAULT = 4;
|
|
82
86
|
|
|
83
87
|
/** 隐藏期预热那一次绘制用的镜头(模块级共用:它只是个「有合法的投影矩阵可读」的载体,永不动 ⇒ ⛔ 不必每实例造一份)。 */
|
|
84
88
|
const GPU_WARM_CAMERA = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* 预热小屋子里那几盏克隆灯的**阴影图边长**(宿主一般是 4096²)。
|
|
92
|
+
* 压小不是降质量:这张图只在预热那间小屋子里用、像素当场扔掉,宿主自己那张阴影图归宿主每帧照画。
|
|
93
|
+
* 判据 = three 的 program 缓存键里只有 `shadowMapType` 与「几盏灯投影」这两档,**没有** `mapSize`
|
|
94
|
+
* (`three.module.js:20913` 一段 `numDirLightShadows / shadowMapType`,`mapSize` 只是 uniform)
|
|
95
|
+
* ⇒ 256² 编出来的 depth program 与 4096² 那份是同一个。
|
|
96
|
+
* 不压小的代价:预热每跑一件就等于替宿主重画一张 4096² 深度图(`shadow.autoUpdate` 默认开)
|
|
97
|
+
* ⇒ 隐藏期那圈自由视角跟着掉帧,那正是「卡」本身,⛔ 不能拿它换。
|
|
98
|
+
*/
|
|
99
|
+
const GPU_WARM_SHADOW_MAP_SIZE = 256;
|
|
100
|
+
|
|
101
|
+
/** 只取预热要动的那两颗字段(`LightShadow` 的类型名不在这页的导入清单里,结构类型够用且不必扩面)。 */
|
|
102
|
+
type WarmShadow = { mapSize?: { set(w: number, h: number): unknown }; dispose?(): void };
|
|
103
|
+
|
|
104
|
+
/** 「同一件」判据里要看的那张主图 id:ShaderMaterial 那一族没有 `map` 这一档 ⇒ 恒 0;图片平面绑上图才换一档。 */
|
|
105
|
+
const warmMapId = (m: Material): number => (m as Material & { map?: Texture | null }).map?.id ?? 0;
|
|
106
|
+
|
|
86
107
|
export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
87
108
|
/** 纹理列表(每个纹理的 url + 世界矩阵) */
|
|
88
109
|
textures: MktTextureInfo[] = [];
|
|
@@ -212,6 +233,15 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
212
233
|
return this._panoTexMode;
|
|
213
234
|
}
|
|
214
235
|
|
|
236
|
+
/** 优先加载的全景个数(存档 `textures` 的前 n 项):**-1 = 全部、0 = 一个都不提前、n = 前 n 个**(2026-11-05 需求)。
|
|
237
|
+
* 默认取 `PANO_IMPORT_PRIORITY_DEFAULT`(=4);宿主想换档在渲染循环跑起来之前 `setPanoPriorityCount(-1)` 一行即可
|
|
238
|
+
* (与 `_ossSize`、`_panoTexMode` 同一套注入路径 ⇒ 编辑器与预览各自的配置各自推下来)。 */
|
|
239
|
+
private _panoPriorityCount: number = PANO_IMPORT_PRIORITY_DEFAULT;
|
|
240
|
+
/** 当前优先加载个数(只读,回显用)。 */
|
|
241
|
+
get panoPriorityCount(): number {
|
|
242
|
+
return this._panoPriorityCount;
|
|
243
|
+
}
|
|
244
|
+
|
|
215
245
|
// 复用临时对象(避免每帧 new,降低 GC 压力)
|
|
216
246
|
private _tmpV3: Vector3 = new Vector3();
|
|
217
247
|
|
|
@@ -257,14 +287,19 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
257
287
|
private _revealPending: Array<ImagePlane | ModelSlot> = [];
|
|
258
288
|
|
|
259
289
|
/**
|
|
260
|
-
* 隐藏期 GPU 预热的在册清单(元素 =
|
|
290
|
+
* 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
|
|
261
291
|
* null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
|
|
262
292
|
*/
|
|
263
|
-
private _warmItems: Array<{ m: Material; g: BufferGeometry }> | null = null;
|
|
264
|
-
/**
|
|
293
|
+
private _warmItems: Array<{ src: Object3D; m: Material | Material[]; g: BufferGeometry; p?: Object3D }> | null = null;
|
|
294
|
+
/** 已收过的去重键(`几何 × 材质数组 × 各材质的 map`)⇒ 跨轮次只补收新出现的组合,不重复编同一条 GL 路径。
|
|
295
|
+
* 自然收尾时**故意留着**(晚到的内容补排时靠它跳过旧档);只有 `stopGpuPrewarm`/`dispose` 把它归 null = 摘牌。 */
|
|
296
|
+
private _warmSeen: Set<string> | null = null;
|
|
297
|
+
/** 预热用的小场景:只挂当前这一件的替身 + 宿主灯光的克隆,`fog`/`environment` 照抄(都进 program 缓存键) */
|
|
265
298
|
private _warmScene: Scene | null = null;
|
|
266
|
-
/**
|
|
267
|
-
private
|
|
299
|
+
/** 此刻挂在预热小场景里的那一颗替身(一帧只画一件 ⇒ 换件时先把上一颗摘下来) */
|
|
300
|
+
private _warmActive: Object3D | null = null;
|
|
301
|
+
/** 预热小场景里的那几盏灯 = 宿主灯的克隆体(各持一份 `shadow` ⇒ 收尾时释放的是它们自己的阴影图,碰不到宿主的) */
|
|
302
|
+
private _warmLights: Light[] | null = null;
|
|
268
303
|
/** 预热落点:1×1 渲染目标(只有像素不要,要的是「这条 GL 路径确实走完了」),随预热结束/dispose 释放 */
|
|
269
304
|
private _warmRt: WebGLRenderTarget | null = null;
|
|
270
305
|
|
|
@@ -299,6 +334,7 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
299
334
|
// `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
|
|
300
335
|
// `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
|
|
301
336
|
// 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
|
|
337
|
+
// 2026-11-05 需求:这一批点几颗改由 `panoPriorityCount` 定(默认 4,-1 = 存档全部)⇒ 本句不动。
|
|
302
338
|
this.prefetchHeadPanoramas();
|
|
303
339
|
this.feedFaceMapping();
|
|
304
340
|
this.tickProgressiveReveal();
|
|
@@ -1084,6 +1120,18 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1084
1120
|
this._panoTexMode = next;
|
|
1085
1121
|
}
|
|
1086
1122
|
|
|
1123
|
+
/** 设置优先加载的全景个数:-1 = 全部 / 0 = 不提前 / n = 存档前 n 个(2026-11-05 需求)。
|
|
1124
|
+
* 收口:负数(含 -2、NaN 一类)一律归到 -1「全加载」,正数向下取整;非有限值回落默认档。
|
|
1125
|
+
* 改档⛔ 需要重新 `importData` —— 名册每帧重排,下一帧 `prefetchHeadPanoramas` 直接用新数点名
|
|
1126
|
+
* (`prioritize` 幂等 ⇒ 已在队列里的那批不重复投喂,也不会把已出队的旧批再塞回去)。 */
|
|
1127
|
+
setPanoPriorityCount(count: number): void {
|
|
1128
|
+
if (!Number.isFinite(count)) this._panoPriorityCount = PANO_IMPORT_PRIORITY_DEFAULT;
|
|
1129
|
+
else {
|
|
1130
|
+
const n = Math.floor(count);
|
|
1131
|
+
this._panoPriorityCount = n < 0 ? -1 : n;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1087
1135
|
/**
|
|
1088
1136
|
* 记下「当前激活(名册 0 号槽 = 中心)是第几个全景」。
|
|
1089
1137
|
* ⭐ 皇上旨 2026-10-03 旨②:预览立方体已与墙**共用同一份材质**,原来那句「把中心那几张抄给立方体单全景材质」
|
|
@@ -1213,6 +1261,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1213
1261
|
ms.dispose();
|
|
1214
1262
|
}
|
|
1215
1263
|
this.models = [];
|
|
1264
|
+
// 用户 2026-11-04「日志少打印点吧」⇒ 这一路(进厅 / 切项目 / 预览的整层重建)几十颗⛔ 不再一颗一行:
|
|
1265
|
+
// 名单先攒着,循环结束打**一条**(文件名口径与 `setModelUrl` 那一记共用 `glbFileName`)⇒ 一颗不落、一行到位。
|
|
1266
|
+
const glbNames: string[] = [];
|
|
1216
1267
|
for (const d of datas) {
|
|
1217
1268
|
const ms = new ModelSlot(d.name);
|
|
1218
1269
|
ms.applyData(d);
|
|
@@ -1224,9 +1275,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1224
1275
|
//(⚠️ 与旧的「GLB 在路上时这一槽露出占位方块」不同:整层重建这一路一颗都不露,直到轮到自己亮)
|
|
1225
1276
|
ms.visible = false;
|
|
1226
1277
|
this._revealPending.push(ms);
|
|
1278
|
+
glbNames.push(glbFileName(d.url));
|
|
1227
1279
|
void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
|
|
1228
1280
|
}
|
|
1229
1281
|
}
|
|
1282
|
+
if (glbNames.length > 0) console.log(`[MktMesh] 整层重建加载 ${glbNames.length} 个 glb:${glbNames.join("、")}`);
|
|
1230
1283
|
}
|
|
1231
1284
|
|
|
1232
1285
|
/** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
|
|
@@ -1243,6 +1296,8 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1243
1296
|
// 槽已退场(换项目 / 删除):待挂那棵由 `ModelSlot.dispose` → `_detachModel` 当场释放,这里不挂
|
|
1244
1297
|
if (!this.models.includes(ms)) return;
|
|
1245
1298
|
if (ms.mountStaged() && onEachLoad) onEachLoad();
|
|
1299
|
+
// 挂进来的那一族此刻才在子树里 ⇒ 补排预热(清单已排空时,这一颗是重新开工的唯一触发点)
|
|
1300
|
+
this._topUpGpuPrewarm();
|
|
1246
1301
|
}
|
|
1247
1302
|
|
|
1248
1303
|
/**
|
|
@@ -1300,6 +1355,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1300
1355
|
}
|
|
1301
1356
|
}
|
|
1302
1357
|
ip.setTileTextures(texs, insets);
|
|
1358
|
+
// 瓦片是一张一张就绪的 ⇒ 每绑上一张,这一颗的「待画纹理」就换一档(去重键带绑定戳记)⇒ 补排一次,
|
|
1359
|
+
// 让上传发生在隐藏期而不是进厅那一帧。
|
|
1360
|
+
this._topUpGpuPrewarm();
|
|
1303
1361
|
if (pending) {
|
|
1304
1362
|
this._queueImagePlaneBind(ip);
|
|
1305
1363
|
return;
|
|
@@ -1329,7 +1387,7 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1329
1387
|
// ─────────────────── 隐藏期 GPU 预热(2026-11-01 需求:进厅那一下不要再现编着色器) ───────────────────
|
|
1330
1388
|
|
|
1331
1389
|
/**
|
|
1332
|
-
*
|
|
1390
|
+
* 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
|
|
1333
1391
|
*
|
|
1334
1392
|
* 为什么必须在隐藏期做:`visible = false` 时 three 的遍历第一句就 `if (object.visible === false) return`
|
|
1335
1393
|
* (与上面 `tickProgressiveReveal` 注释同一颗雷)⇒ 墙根本进不了渲染列表,那份大 shader 从来没进过
|
|
@@ -1342,10 +1400,22 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1342
1400
|
* 编出来的正是同一份 program(three 的缓存按 `材质 × 几何属性形态 × 场景光照/雾` 计键),
|
|
1343
1401
|
* 顶点缓冲也真上了 GPU;那 1×1 的像素直接扔掉,不占显存、不污染画面。
|
|
1344
1402
|
*
|
|
1345
|
-
* ⚠️
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1403
|
+
* ⚠️ 2026-11-01 第二版:清单从「只收不带灯」放宽到**全收**,替身从「自造一颗裸 Mesh」换成「原对象自己克隆一颗」。
|
|
1404
|
+
* 量出来的账(本地 dev,进厅三连帧 100/250/234 ms):进厅那几帧里 89 份几何第一次上传(`bufferData` ×72)、
|
|
1405
|
+
* 504 次 `drawElements`、6 份 program 现编 —— 第一版一道都没挡住:展厅里的展品(`ModelSlot` 那些 GLB 件,
|
|
1406
|
+
* Lambert/Standard 带灯材质)既没上传也没编,因为它们既不是 ShaderMaterial 也不是 MeshBasicMaterial。
|
|
1407
|
+
* 带灯材质的 program 缓存键里有「方向光/点光/聚光数量 + 阴影档 + 环境图」
|
|
1408
|
+
* (`three.module.js:20901-20913`:`numDirLights`…`numDirLightShadows`、`shadowMapType`,另有 `envMap`/`fog` 两个位)
|
|
1409
|
+
* ⇒ 预热那间小屋子把宿主的灯**克隆一份**摆进去、`fog`/`environment` 照抄,否则编出来是另一份 define 组 = 白编。
|
|
1410
|
+
* 替身必须是**克隆**不是裸 Mesh:`instancing`/`skinning`/`morphTargets` 也各占缓存键一个位
|
|
1411
|
+
* (`getProgramCacheKeyBooleans` 里那段 `_programLayers`)⇒ 拿裸 Mesh 画实例化材质编的是非实例化那一份。
|
|
1412
|
+
* 克隆的灯只活在预热期,收尾时连各自的阴影图一起释放(`light.clone()` 给的是 `shadow.clone()` 的新份,
|
|
1413
|
+
* 实测不与宿主共享 ⇒ 不会误释放宿主那张);阴影边长压到 `GPU_WARM_SHADOW_MAP_SIZE`,理由写在那颗常量上。
|
|
1414
|
+
*
|
|
1415
|
+
* ⚠️ 一次性清单不等于收工:隐藏期里展厅的内容**还没长齐**(GLB 逐帧各挂一颗、平面的 4 格瓦片逐张就绪逐张绑),
|
|
1416
|
+
* 所以补收有两只手 —— ① 排空那一拍再扫一遍;② 内容落位那两个落点各调一次 `_topUpGpuPrewarm()`
|
|
1417
|
+
* (GLB 挂进子树、平面绑上瓦片)。去重键 = `几何 id × 材质 id 串 × 各材质的 map id`(平面再带绑定戳记)
|
|
1418
|
+
* ⇒ 补收只针对「没见过的那一档」,同一件不会被画第二遍。
|
|
1349
1419
|
*
|
|
1350
1420
|
* @param renderer 宿主那一份渲染器(隐藏期 `onBeforeRender` 不触发,库里拿不到 ⇒ 必须由宿主交进来;
|
|
1351
1421
|
* 站侧的落点就是 `FloorMktBase.preloadFirstPano(renderer)`,它本来就是隐藏期的入口)
|
|
@@ -1356,35 +1426,65 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1356
1426
|
//(原先 `_glRenderer` 只在 `onBeforeRender` 里捕获 ⇒ 隐藏期排上的队全走「three 自己绑」的降级路)。
|
|
1357
1427
|
this._glRenderer = renderer;
|
|
1358
1428
|
if (this._warmItems) return; // 已在预热 ⇒ ⛔ 不重建清单(重复点会打断还没跑的那几件)
|
|
1429
|
+
this._warmItems = [];
|
|
1430
|
+
this._warmSeen = new Set<string>();
|
|
1431
|
+
this._collectWarmItems();
|
|
1432
|
+
this._scheduleGpuWarmStep(renderer);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/** 停止隐藏期预热(宿主进入展厅时调):剩下的清单直接作废并**摘牌** —— 亮出来那一帧本来就要编要传,不必再抢额度;摘牌后晚到的内容也不再来补排。 */
|
|
1436
|
+
stopGpuPrewarm(): void {
|
|
1437
|
+
this._warmSeen = null; // 这颗就是「摘牌」本身:`_topUpGpuPrewarm` 只认它
|
|
1438
|
+
this._finishGpuPrewarm();
|
|
1439
|
+
}
|
|
1359
1440
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1441
|
+
/**
|
|
1442
|
+
* 内容后落位时补排预热(GLB 挂进子树那一拍、平面绑上瓦片纹理那一拍各调一次)。
|
|
1443
|
+
*
|
|
1444
|
+
* 为什么必须有这一颗:预热是隐藏期跑的,而隐藏期里展厅的内容**还没长齐** —— GLB 十几只各走一条异步下载链、
|
|
1445
|
+
* 逐帧只挂一颗,平面那 4 格图也是逐张就绪逐张绑。一次性清单在几拍之内就排空了 ⇒ 晚到的那一族一件都没被画过,
|
|
1446
|
+
* 进厅那一帧照样现编现传(这就是第一版「预热跑了、人还是卡了一下」的根)。
|
|
1447
|
+
*
|
|
1448
|
+
* 只在隐藏期响应:`_warmSeen` 归 null = 宿主已叫停(进厅)⇒ 晚到的东西交给「逐帧点亮」那一拍自己编,⛔ 别再抢额度。
|
|
1449
|
+
*/
|
|
1450
|
+
private _topUpGpuPrewarm(): void {
|
|
1451
|
+
if (!this._warmSeen || this.visible) return;
|
|
1452
|
+
if (!this._warmItems) {
|
|
1453
|
+
this._warmItems = []; // 上一轮已经收尾 ⇒ 重新开工(去重表还在 ⇒ 只补新出现的那几档,旧的不会重画)
|
|
1454
|
+
this._scheduleGpuWarmStep(this._glRenderer!); // `_warmSeen` 非空 ⇒ `startGpuPrewarm` 早已把渲染器交进来
|
|
1455
|
+
}
|
|
1456
|
+
this._collectWarmItems();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* 扫一遍本对象的子树,把「还没收过的那一档」压进清单。
|
|
1461
|
+
* @returns 这次新收了几个(0 = 确实没有新东西 ⇒ 调用方据此收尾,⛔ 不是每帧空转)
|
|
1462
|
+
*/
|
|
1463
|
+
private _collectWarmItems(): number {
|
|
1464
|
+
const items = this._warmItems;
|
|
1465
|
+
const seen = this._warmSeen;
|
|
1466
|
+
if (!items || !seen) return 0;
|
|
1467
|
+
let added = 0;
|
|
1468
|
+
// ⚠️ 遍历**不看 `visible`**(与 `_ensureWarmScene` 里那趟剪枝的灯正好相反):隐藏期里展品的槽、图片的层都是
|
|
1469
|
+
// 先压着不亮的(`setModels` 那一路进来就是 `visible = false`),要挡它们就等于把这次要治的那一族又漏回去。
|
|
1362
1470
|
this.traverse((o) => {
|
|
1363
1471
|
const mesh = o as Mesh;
|
|
1364
1472
|
if (!mesh.isMesh) return;
|
|
1365
|
-
const m = mesh.material as Material;
|
|
1366
1473
|
const g = mesh.geometry as BufferGeometry;
|
|
1367
|
-
if (!
|
|
1368
|
-
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if (seen.has(key)) return; // 同一份材质 + 同一份几何 = 同一个 program,画一次就够
|
|
1474
|
+
if (!g) return;
|
|
1475
|
+
const mats = (Array.isArray(mesh.material) ? mesh.material : [mesh.material]) as Material[];
|
|
1476
|
+
if (!mats.length || mats.some((m) => !m || !m.visible)) return; // 材质自己就不画 ⇒ 编了也用不上
|
|
1477
|
+
// 「同一件」= 同一份几何 × 同一串材质 × 同一串图。后两截都要进键,因为 program 缓存键里 `USE_MAP` 那一档
|
|
1478
|
+
// 看的是材质真绑没绑;而图片平面那一族的 4 格瓦片走的是 `_uTiles` uniform 数组(`material.map` 恒 null),
|
|
1479
|
+
// 绑上第 1 张那一刻才是「这条 GL 路径第一次真画」⇒ 拿它自带的绑定戳记当尺,图一换就补收一次。
|
|
1480
|
+
const key = `${g.id}|${mats.map((m) => m.id).join("+")}|${mats.map(warmMapId).join("+")}` +
|
|
1481
|
+
(o instanceof ImagePlane ? `|${(o as ImagePlane).tilesBindingStamp()}` : "");
|
|
1482
|
+
if (seen.has(key)) return;
|
|
1377
1483
|
seen.add(key);
|
|
1378
|
-
items.push({ m, g });
|
|
1484
|
+
items.push({ src: o, m: mesh.material as Material | Material[], g });
|
|
1485
|
+
added++;
|
|
1379
1486
|
});
|
|
1380
|
-
|
|
1381
|
-
this._scheduleGpuWarmStep(renderer);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
/** 停止隐藏期预热(宿主进入展厅时调):剩下的清单直接作废 —— 亮出来那一帧本来就要编要传,不必再抢额度。 */
|
|
1385
|
-
stopGpuPrewarm(): void {
|
|
1386
|
-
this._warmItems = null;
|
|
1387
|
-
this._releaseWarmHarness();
|
|
1487
|
+
return added;
|
|
1388
1488
|
}
|
|
1389
1489
|
|
|
1390
1490
|
/** 排下一件:先垫一个空函数占一格(全局队列每 tick 只跑一项 ⇒ 真活落在下一个 tick),与全景上传、平面绑定同一手法。 */
|
|
@@ -1398,50 +1498,117 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1398
1498
|
const items = this._warmItems;
|
|
1399
1499
|
if (!items) return; // `stopGpuPrewarm`/`dispose` 已摘牌 ⇒ 本项作废
|
|
1400
1500
|
if (this.visible) { // 已经亮起来了(进厅)⇒ 不再和真实那帧抢额度
|
|
1401
|
-
this.
|
|
1402
|
-
this._releaseWarmHarness();
|
|
1501
|
+
this._finishGpuPrewarm();
|
|
1403
1502
|
return;
|
|
1404
1503
|
}
|
|
1405
|
-
if (!items.length) {
|
|
1406
|
-
this.
|
|
1407
|
-
this._releaseWarmHarness();
|
|
1504
|
+
if (!items.length && this._collectWarmItems() === 0) { // 排空了:先补扫(GLB、后绑的图都是晚到的),真没有才收尾
|
|
1505
|
+
this._finishGpuPrewarm();
|
|
1408
1506
|
return;
|
|
1409
1507
|
}
|
|
1410
1508
|
const it = items.shift()!;
|
|
1411
|
-
|
|
1412
|
-
let
|
|
1413
|
-
if (!proxy
|
|
1414
|
-
|
|
1415
|
-
//
|
|
1416
|
-
//
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if ((p as Scene).isScene) { host = p as Scene; break; }
|
|
1420
|
-
}
|
|
1421
|
-
scene.fog = host ? host.fog : null;
|
|
1422
|
-
proxy = new Mesh();
|
|
1509
|
+
const scene = this._ensureWarmScene();
|
|
1510
|
+
let proxy = it.p;
|
|
1511
|
+
if (!proxy) {
|
|
1512
|
+
// 替身 = **原对象自己克隆一颗**(⛔ 自造裸 `Mesh`):`instancing`/`skinning`/`morphTargets` 都进缓存键,
|
|
1513
|
+
// 裸 Mesh 画实例化材质编的是非实例化那一份 = 白编。克隆体与宿主共享同一份 `geometry`/`material` 对象
|
|
1514
|
+
// ⇒ 编的是同一个 program、传的是同一个顶点缓冲。子树不要(`recursive = false`):一件只画一颗。
|
|
1515
|
+
proxy = it.p = it.src.clone(false);
|
|
1516
|
+
proxy.visible = true; // 宿主那颗此刻多半还是 `visible = false` ⇒ 替身得进得了 `projectObject`
|
|
1423
1517
|
proxy.frustumCulled = false; // 墙几何是世界坐标、体量又比这 2×2 的取景框大得多 ⇒ 别按包围球剔除
|
|
1424
|
-
scene.add(proxy);
|
|
1425
|
-
this._warmScene = scene;
|
|
1426
|
-
this._warmProxy = proxy;
|
|
1427
1518
|
}
|
|
1428
|
-
if (
|
|
1429
|
-
|
|
1430
|
-
|
|
1519
|
+
if ((it.src as InstancedMesh).isInstancedMesh) {
|
|
1520
|
+
const src = it.src as InstancedMesh;
|
|
1521
|
+
const inst = proxy as InstancedMesh;
|
|
1522
|
+
// `clone()` 里的 `instanceMatrix.copy(...)` 是**快照**(另起一份 Float32Array)⇒ 改回指宿主那一份:
|
|
1523
|
+
// 上传的就是进厅要用的那个 buffer,晚到的落位改动也一起带上
|
|
1524
|
+
inst.instanceMatrix = src.instanceMatrix;
|
|
1525
|
+
inst.count = src.count;
|
|
1526
|
+
}
|
|
1527
|
+
if (this._warmActive && this._warmActive !== proxy) scene.remove(this._warmActive); // 一帧只画这一件
|
|
1528
|
+
if (proxy.parent !== scene) scene.add(proxy);
|
|
1529
|
+
this._warmActive = proxy;
|
|
1431
1530
|
const prevTarget = renderer.getRenderTarget();
|
|
1432
|
-
renderer.setRenderTarget(this._warmRt);
|
|
1531
|
+
renderer.setRenderTarget(this._warmRt!);
|
|
1433
1532
|
renderer.render(scene, GPU_WARM_CAMERA);
|
|
1434
1533
|
renderer.setRenderTarget(prevTarget);
|
|
1435
1534
|
this._scheduleGpuWarmStep(renderer);
|
|
1436
1535
|
}
|
|
1437
1536
|
|
|
1438
|
-
/**
|
|
1537
|
+
/**
|
|
1538
|
+
* 造(并记住)预热那间小屋子:只建一次。
|
|
1539
|
+
* `fog` / `environment` 照抄、宿主的灯**逐盏克隆**摆进来 —— 这三样都进 program 缓存键,
|
|
1540
|
+
* 少一样就是编出一份进厅用不上的 program。
|
|
1541
|
+
*/
|
|
1542
|
+
private _ensureWarmScene(): Scene {
|
|
1543
|
+
if (this._warmScene) return this._warmScene;
|
|
1544
|
+
const scene = new Scene();
|
|
1545
|
+
const lights: Light[] = [];
|
|
1546
|
+
const host = this._findHostScene();
|
|
1547
|
+
if (host) {
|
|
1548
|
+
scene.fog = host.fog; // `USE_FOG` 进键
|
|
1549
|
+
scene.environment = host.environment; // `envMap` 进键(Standard 那族吃场景环境图)
|
|
1550
|
+
const walk = (o: Object3D) => {
|
|
1551
|
+
if (!o.visible) return; // 与 three 的 `projectObject` 同一道闸:此刻不计数的灯,进厅那份 define 组里也没有
|
|
1552
|
+
const light = o as Light;
|
|
1553
|
+
if (light.isLight) {
|
|
1554
|
+
const clone = light.clone(true) as Light;
|
|
1555
|
+
// `light.clone()` 里 `shadow = source.shadow.clone()` ⇒ 克隆体自带一份**新的** shadow
|
|
1556
|
+
// (边长、bias、投影相机范围都照抄,实测不与宿主共享),所以这里只管把它压小;
|
|
1557
|
+
// 压小的理由与「为什么压了还编得出同一份 program」都写在 `GPU_WARM_SHADOW_MAP_SIZE` 上。
|
|
1558
|
+
const shadow = (clone as unknown as { shadow?: WarmShadow }).shadow;
|
|
1559
|
+
if (shadow?.mapSize) shadow.mapSize.set(GPU_WARM_SHADOW_MAP_SIZE, GPU_WARM_SHADOW_MAP_SIZE);
|
|
1560
|
+
// target 同样是新克隆的一份(位置一起带过来)⇒ 挂进这间屋子,`updateMatrixWorld` 才走得到它、
|
|
1561
|
+
// 光照方向才与宿主一致。⛔ 不许把宿主那颗挂过来:`Object3D.add` 会先从原父节点 `remove`,
|
|
1562
|
+
// 楼外那圈自由视角的阴影当场就被摘歪了。
|
|
1563
|
+
const target = (clone as unknown as { target?: Object3D }).target;
|
|
1564
|
+
if (target) scene.add(target);
|
|
1565
|
+
scene.add(clone);
|
|
1566
|
+
lights.push(clone);
|
|
1567
|
+
}
|
|
1568
|
+
for (const child of o.children) walk(child);
|
|
1569
|
+
};
|
|
1570
|
+
walk(host);
|
|
1571
|
+
}
|
|
1572
|
+
this._warmLights = lights;
|
|
1573
|
+
this._warmScene = scene;
|
|
1574
|
+
if (!this._warmRt) this._warmRt = new WebGLRenderTarget(1, 1);
|
|
1575
|
+
return scene;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/** 沿 `parent` 往上找到的第一个 Scene = 宿主场景(本站是 `largeScreenRender` 那一棵)。 */
|
|
1579
|
+
private _findHostScene(): Scene | null {
|
|
1580
|
+
for (let p: Object3D | null = this.parent; p; p = p.parent) {
|
|
1581
|
+
if ((p as Scene).isScene) return p as Scene;
|
|
1582
|
+
}
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* 收尾(跑完 / 进厅 / 宿主叫停 / `dispose` 四条路都汇到这里):清单作废 + 临时资源拆掉。
|
|
1588
|
+
*
|
|
1589
|
+
* ⚠️ **不动 `_warmSeen`**:自然跑完那一路后面还会有内容落位(GLB、后绑的瓦片),补排时全靠这张表跳过已经画过的旧档
|
|
1590
|
+
* —— 清掉它等于把同一批东西再画一遍。真正的「摘牌」只有 `stopGpuPrewarm`/`dispose`,它们先把这颗归 null。
|
|
1591
|
+
*/
|
|
1592
|
+
private _finishGpuPrewarm(): void {
|
|
1593
|
+
this._warmItems = null;
|
|
1594
|
+
this._releaseWarmHarness();
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* 拆掉预热那套临时资源(1×1 渲染目标 + 克隆的灯 + 小场景)。
|
|
1599
|
+
* 替身只是从小屋子里摘出来 —— 它的 `geometry`/`material` 都是宿主那一份对象 ⇒ ⛔ 在此 dispose,会把展厅要用的资源释放掉。
|
|
1600
|
+
*/
|
|
1439
1601
|
private _releaseWarmHarness(): void {
|
|
1440
1602
|
if (this._warmRt) this._warmRt.dispose();
|
|
1441
1603
|
this._warmRt = null;
|
|
1442
|
-
|
|
1604
|
+
for (const light of this._warmLights || []) {
|
|
1605
|
+
const shadow = (light as unknown as { shadow?: WarmShadow }).shadow;
|
|
1606
|
+
if (shadow?.dispose) shadow.dispose(); // 放掉的是克隆体自己那张阴影图(`clone()` 给的是新 shadow)
|
|
1607
|
+
}
|
|
1608
|
+
this._warmLights = null;
|
|
1609
|
+
if (this._warmScene) this._warmScene.clear(); // 只摘孩子(替身、克隆灯、灯的 target)
|
|
1443
1610
|
this._warmScene = null;
|
|
1444
|
-
this.
|
|
1611
|
+
this._warmActive = null;
|
|
1445
1612
|
}
|
|
1446
1613
|
|
|
1447
1614
|
/** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
|
|
@@ -1545,11 +1712,12 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1545
1712
|
}
|
|
1546
1713
|
|
|
1547
1714
|
/**
|
|
1548
|
-
* 把**存档里前 `
|
|
1715
|
+
* 把**存档里前 `panoPriorityCount` 个全景**(-1 = 整份全加载)按存档顺序登记进拼合器的**加载任务队列**
|
|
1549
1716
|
*(2026-10-29 需求「优先加载前 2 个全景素材」+ 2026-10-30 需求逐字:「给 mktmesh 内部加一个加载任务队列,
|
|
1550
1717
|
* 把模型和平面图排在最前面,然后按照顺序,依次把前 2 个全景的顺序把瓦片加载任务添加到任务队列里面,
|
|
1551
1718
|
* 第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;
|
|
1552
|
-
*
|
|
1719
|
+
* 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」
|
|
1720
|
+
* + 2026-11-05 需求「提前加载多少个全景改由配置决定,-1 全加载 / 0 不加载 / n 前 n 个,默认 4」)。
|
|
1553
1721
|
* 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
|
|
1554
1722
|
*(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
|
|
1555
1723
|
*
|
|
@@ -1558,27 +1726,31 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1558
1726
|
* (GLB 逐只 `loadModel` 不 throttle;平面那 4 片一次性 `_imgTileLoader.preload`),
|
|
1559
1727
|
* 而全景瓦片最早也要等下一帧的 `pump` 才发第一片 ⇒ 发起次序天然是「模型 → 平面图 → 全景」,
|
|
1560
1728
|
* ⛔ 这里不再把它们塞进队列重复管一遍(塞了就是第二把尺)。
|
|
1561
|
-
* ② 本方法把存档 0
|
|
1729
|
+
* ② 本方法把存档 0 号起这 n 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
|
|
1562
1730
|
* 0 号 32 格全落定(画进 RT,或该格本就缺地址/判死)才出队、1 号才开始发第一片。
|
|
1563
1731
|
* 比 10-29 那版(只是把两批挪到在册顺序前面、额度仍被两批分摊)更硬:**第 1 颗独享额度,最快出全图**。
|
|
1732
|
+
* n = -1(全加载)时这一条变成「整份存档按存档顺序串行」⇒ 距离名册插不到前面,出清速度取决于总片数。
|
|
1564
1733
|
* ③ 名册按距离点到的其他全景走 `feedFaceMapping` 里那句 `ensure(t.tiles)`(不带队列)⇒ 只入册、
|
|
1565
1734
|
* 排在队列后面等,队列空了才按在册顺序补料(现行口径不变)。
|
|
1566
1735
|
* ⚠️⚠️ 为什么每帧点而⛔ 只在 `importData` 里点一记:预览每开一次都换渲染器,`_switchRenderer` 把在册组
|
|
1567
1736
|
* 整份作废(队列里的键也在同一刻失效);只在载入那次点名等于白点。本方法点在 `pump` 之后、名册逐槽
|
|
1568
|
-
* `ensure` 之前 ⇒
|
|
1737
|
+
* `ensure` 之前 ⇒ 每帧把这几记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以存档次序恒不变)。
|
|
1569
1738
|
* ⚠️ 代价如实记:
|
|
1570
|
-
* · 队列非空时**其他全景一片都不发** ⇒
|
|
1739
|
+
* · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前 n 颗的那面墙会多灰一会儿。
|
|
1571
1740
|
* · 一颗「永远下不完」的瓦片(下载失败还没累计到判死)会压住队列;判死阈值是解码失败 3 次
|
|
1572
|
-
* (`_dead`),退避重试期间队首不落定 ⇒
|
|
1741
|
+
* (`_dead`),退避重试期间队首不落定 ⇒ 后面几颗跟着延后。真实数据里缺片是「地址为空/404」,
|
|
1573
1742
|
* 404 走三次即判死、那一格拼合涂灰 ⇒ 队列照样出清。
|
|
1574
|
-
* ·
|
|
1575
|
-
* (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)——
|
|
1743
|
+
* · 这批全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
|
|
1744
|
+
* (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这些景点迟早要建;
|
|
1745
|
+
* ⚠️ n = -1(全加载)时这一条按存档全景总数放大,显存与在途请求都跟着涨。
|
|
1576
1746
|
* ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
|
|
1577
1747
|
* 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
|
|
1578
1748
|
* 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
|
|
1579
1749
|
*/
|
|
1580
1750
|
prefetchHeadPanoramas(): void {
|
|
1581
|
-
const
|
|
1751
|
+
const total = this.textures.length;
|
|
1752
|
+
// -1 = 整份;0 = 一个都不提前(这一句直接空转,其余全景仍按距离名册排队 = 回到 10-29 之前的行为)
|
|
1753
|
+
const n = this._panoPriorityCount < 0 ? total : Math.min(this._panoPriorityCount, total);
|
|
1582
1754
|
// 正着点:0 号先入队 ⇒ 队首是它,落定后才轮到 1 号(`prioritize` 对已在队列里的键不挪位置)
|
|
1583
1755
|
for (let i = 0; i < n; i++) {
|
|
1584
1756
|
const info = this.textures[i];
|
|
@@ -1617,9 +1789,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
1617
1789
|
this._imgBindDone.clear();
|
|
1618
1790
|
this._modelMountQueued.clear();
|
|
1619
1791
|
this._revealPending = [];
|
|
1620
|
-
//
|
|
1621
|
-
|
|
1622
|
-
this.
|
|
1792
|
+
// 隐藏期预热:清单、去重表与小屋子那套临时资源随本对象一起退场(那张 1×1 渲染目标是本类**唯一**自建的 RT)。
|
|
1793
|
+
// 走 `stopGpuPrewarm` 而不是 `_finishGpuPrewarm`:本对象都要没了,去重表再留着就是泄漏。
|
|
1794
|
+
this.stopGpuPrewarm();
|
|
1623
1795
|
this._stitcher.dispose();
|
|
1624
1796
|
this._glRenderer = null;
|
|
1625
1797
|
if (this._placeholder) this._placeholder.dispose();
|
package/MktMesh/imagePlane.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export declare class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
|
|
|
39
39
|
private _data;
|
|
40
40
|
/** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
|
|
41
41
|
private _loaded;
|
|
42
|
+
/** 此刻是不是编辑器的选中态(决定上不上黄、边框亮不亮) */
|
|
43
|
+
private _selected;
|
|
44
|
+
/** 选中边框:懒建(预览/发布那一路从不选中 ⇒ 一颗都不建) */
|
|
45
|
+
private _outline;
|
|
42
46
|
constructor(name?: string);
|
|
43
47
|
get imageWidth(): number;
|
|
44
48
|
get imageHeight(): number;
|
|
@@ -57,6 +61,16 @@ export declare class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
|
|
|
57
61
|
tilesBindingStamp(): string;
|
|
58
62
|
highlight(): void;
|
|
59
63
|
restoreColor(): void;
|
|
64
|
+
/**
|
|
65
|
+
* 选中态的三样东西一起算(颜色 + 边框),一处规则:
|
|
66
|
+
* - 黄只在**还没上传图**那一档:片元里贴图是乘进 diffuse 的(`diffuseColor *= imgTexel`),有图还套黄 ⇒ 整张图发黄。
|
|
67
|
+
* - 边框在有图/没图两档都亮:没图那四格是整格 `discard`(一个像素都不落),只靠黄等于没有反馈,看不见也找不着。
|
|
68
|
+
*/
|
|
69
|
+
private _syncSelection;
|
|
70
|
+
/** 选中边框懒建:预览/发布那一路从不选中 ⇒ 一颗 LineSegments2 都不建(与模型槽那圈 `BoxLine2` 同口径) */
|
|
71
|
+
private _ensureOutline;
|
|
72
|
+
/** 四条边按当前几何的宽高铺在平面那一平面上(局部 z=0,中心为原点) */
|
|
73
|
+
private _layoutOutline;
|
|
60
74
|
toData(): ImagePlaneData;
|
|
61
75
|
applyData(d: ImagePlaneData): void;
|
|
62
76
|
dispose(): void;
|