three-base 2.0.195 → 2.0.197
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 +93 -0
- package/MktMesh/MktMesh.js +276 -1
- package/MktMesh/MktMesh.ts +278 -0
- package/MktMesh/modelSlot.js +3 -0
- package/MktMesh/modelSlot.ts +3 -0
- package/MktMesh/panoBitmapTexture.js +5 -1
- package/MktMesh/panoBitmapTexture.ts +6 -1
- package/MktMesh/panoTileStitcher.d.ts +15 -7
- package/MktMesh/panoTileStitcher.js +39 -10
- package/MktMesh/panoTileStitcher.ts +48 -15
- package/package.json +1 -1
- package/threeLoader.d.ts +14 -0
- package/threeLoader.js +48 -3
- package/threeLoader.ts +45 -3
package/EventMap.d.ts
CHANGED
|
@@ -20,6 +20,22 @@ export interface BeforeRenderEvent {
|
|
|
20
20
|
export interface AfterRenderEvent {
|
|
21
21
|
type: "afterRender";
|
|
22
22
|
}
|
|
23
|
+
export interface LoadErrorEvent {
|
|
24
|
+
type: "loadError";
|
|
25
|
+
url: string;
|
|
26
|
+
/** asset = 走 manager 计数的素材(全景整图/切片、平面瓦片、模型内嵌贴图);model = 模型槽那颗 GLB 整体没出来 */
|
|
27
|
+
kind: "asset" | "model";
|
|
28
|
+
/** 仅 kind="model":那一颗模型槽的名字,页面据此点名是哪一颗没出来 */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** 全站累计失败地址数(与 manager 的 itemsTotal/itemsLoaded 同口径:累加、永不清零) */
|
|
31
|
+
failed: number;
|
|
32
|
+
/** 这个地址是不是第一次失败(同地址的后续重试仍发事件,但 isNew=false、不计数) */
|
|
33
|
+
isNew: boolean;
|
|
34
|
+
/** 服务端回的状态码(只在真的拿到过回复时有:全景/平面瓦片的 fetch 非 2xx、GLB 下载失败);网络层失败为 undefined */
|
|
35
|
+
status?: number;
|
|
36
|
+
/** 服务端**明说这个地址没有**(404 / 403)⇒ 页面据此把引用它的那条数据整条删掉;断网一类没有回复的失败不算 */
|
|
37
|
+
missing: boolean;
|
|
38
|
+
}
|
|
23
39
|
export interface RAFEvent {
|
|
24
40
|
type: "requestAnimationFrame";
|
|
25
41
|
timeTemp: number;
|
|
@@ -60,6 +76,7 @@ export interface ThreeLoaderEventMap extends Object3DEventMap {
|
|
|
60
76
|
"loadding": LoadingEvent;
|
|
61
77
|
"onLoad": OnLoadEvent;
|
|
62
78
|
"progress": LoaderProgressEvent;
|
|
79
|
+
"loadError": LoadErrorEvent;
|
|
63
80
|
}
|
|
64
81
|
export interface ThreeBaseEventTypeMap extends Object3DEventMap {
|
|
65
82
|
"loadding": LoadingEvent;
|
package/EventMap.ts
CHANGED
|
@@ -38,6 +38,24 @@ export interface LoaderProgressEvent {
|
|
|
38
38
|
}
|
|
39
39
|
export interface BeforeRenderEvent { type: "beforeRender"; }
|
|
40
40
|
export interface AfterRenderEvent { type: "afterRender"; }
|
|
41
|
+
// 资源加载失败:`DefaultLoadingManager.onError`(素材那一路)与模型槽取回失败(GLB 那一路)都汇到这一个出口。
|
|
42
|
+
// `url` 同一地址只计一次数(全景/平面瓦片失败会每帧重试 ⇒ 不去重会把失败数刷成无穷大)。
|
|
43
|
+
export interface LoadErrorEvent {
|
|
44
|
+
type: "loadError";
|
|
45
|
+
url: string;
|
|
46
|
+
/** asset = 走 manager 计数的素材(全景整图/切片、平面瓦片、模型内嵌贴图);model = 模型槽那颗 GLB 整体没出来 */
|
|
47
|
+
kind: "asset" | "model";
|
|
48
|
+
/** 仅 kind="model":那一颗模型槽的名字,页面据此点名是哪一颗没出来 */
|
|
49
|
+
label?: string;
|
|
50
|
+
/** 全站累计失败地址数(与 manager 的 itemsTotal/itemsLoaded 同口径:累加、永不清零) */
|
|
51
|
+
failed: number;
|
|
52
|
+
/** 这个地址是不是第一次失败(同地址的后续重试仍发事件,但 isNew=false、不计数) */
|
|
53
|
+
isNew: boolean;
|
|
54
|
+
/** 服务端回的状态码(只在真的拿到过回复时有:全景/平面瓦片的 fetch 非 2xx、GLB 下载失败);网络层失败为 undefined */
|
|
55
|
+
status?: number;
|
|
56
|
+
/** 服务端**明说这个地址没有**(404 / 403)⇒ 页面据此把引用它的那条数据整条删掉;断网一类没有回复的失败不算 */
|
|
57
|
+
missing: boolean;
|
|
58
|
+
}
|
|
41
59
|
|
|
42
60
|
export interface RAFEvent {
|
|
43
61
|
type: "requestAnimationFrame";
|
|
@@ -85,6 +103,7 @@ export interface ThreeLoaderEventMap extends Object3DEventMap {
|
|
|
85
103
|
"loadding": LoadingEvent;
|
|
86
104
|
"onLoad": OnLoadEvent;
|
|
87
105
|
"progress": LoaderProgressEvent;
|
|
106
|
+
"loadError": LoadErrorEvent;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
export interface ThreeBaseEventTypeMap extends Object3DEventMap {
|
package/MktMesh/MktMesh.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BufferGeometry, Camera, Matrix4, Mesh, ShaderMaterial, Texture, Vector3 } from "three";
|
|
2
|
+
import type { WebGLRenderer } from "three";
|
|
2
3
|
import { type PanoTexMode } from "./mktShader";
|
|
3
4
|
import { type PanoStitcher } from "./panoTileStitcher";
|
|
4
5
|
import type { MktMeshData, MktTextureInfo, WallPoint, WallSide } from "./types";
|
|
@@ -161,6 +162,22 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
161
162
|
* 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
|
|
162
163
|
*/
|
|
163
164
|
private _revealPending;
|
|
165
|
+
/**
|
|
166
|
+
* 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
|
|
167
|
+
* null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
|
|
168
|
+
*/
|
|
169
|
+
private _warmItems;
|
|
170
|
+
/** 已收过的去重键(`几何 × 材质数组 × 各材质的 map`)⇒ 跨轮次只补收新出现的组合,不重复编同一条 GL 路径。
|
|
171
|
+
* 自然收尾时**故意留着**(晚到的内容补排时靠它跳过旧档);只有 `stopGpuPrewarm`/`dispose` 把它归 null = 摘牌。 */
|
|
172
|
+
private _warmSeen;
|
|
173
|
+
/** 预热用的小场景:只挂当前这一件的替身 + 宿主灯光的克隆,`fog`/`environment` 照抄(都进 program 缓存键) */
|
|
174
|
+
private _warmScene;
|
|
175
|
+
/** 此刻挂在预热小场景里的那一颗替身(一帧只画一件 ⇒ 换件时先把上一颗摘下来) */
|
|
176
|
+
private _warmActive;
|
|
177
|
+
/** 预热小场景里的那几盏灯 = 宿主灯的克隆体(各持一份 `shadow` ⇒ 收尾时释放的是它们自己的阴影图,碰不到宿主的) */
|
|
178
|
+
private _warmLights;
|
|
179
|
+
/** 预热落点:1×1 渲染目标(只有像素不要,要的是「这条 GL 路径确实走完了」),随预热结束/dispose 释放 */
|
|
180
|
+
private _warmRt;
|
|
164
181
|
constructor(opts?: {
|
|
165
182
|
wallHeight?: number;
|
|
166
183
|
wallClosed?: boolean;
|
|
@@ -398,6 +415,82 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
|
398
415
|
* 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
|
|
399
416
|
*/
|
|
400
417
|
tickProgressiveReveal(): void;
|
|
418
|
+
/**
|
|
419
|
+
* 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
|
|
420
|
+
*
|
|
421
|
+
* 为什么必须在隐藏期做:`visible = false` 时 three 的遍历第一句就 `if (object.visible === false) return`
|
|
422
|
+
* (与上面 `tickProgressiveReveal` 注释同一颗雷)⇒ 墙根本进不了渲染列表,那份大 shader 从来没进过
|
|
423
|
+
* three 的 program 缓存。进厅那一帧才第一次画它 = 编译 + 链接 + 顶点缓冲上传全压在那一帧,
|
|
424
|
+
* 这就是「像有很多资源要载入 GPU」的另一半(前一半「16 张 6144² 渲染目标」已在拼合器里治掉)。
|
|
425
|
+
*
|
|
426
|
+
* ⛔ 不用 `renderer.compile()` / `compileAsync()`:那两个吃**整个场景**、一次把里面全部 program 编完,
|
|
427
|
+
* 没地方按帧配额(`compileAsync` 的完成回调还会在同一帧落一坨主线程开销)。这里的做法是
|
|
428
|
+
* 「拿同一个材质对象 + 同一个几何对象真画一次,画进 1×1 的渲染目标」⇒ 走的就是进厅那一条 GL 路径,
|
|
429
|
+
* 编出来的正是同一份 program(three 的缓存按 `材质 × 几何属性形态 × 场景光照/雾` 计键),
|
|
430
|
+
* 顶点缓冲也真上了 GPU;那 1×1 的像素直接扔掉,不占显存、不污染画面。
|
|
431
|
+
*
|
|
432
|
+
* ⚠️ 2026-11-01 第二版:清单从「只收不带灯」放宽到**全收**,替身从「自造一颗裸 Mesh」换成「原对象自己克隆一颗」。
|
|
433
|
+
* 量出来的账(本地 dev,进厅三连帧 100/250/234 ms):进厅那几帧里 89 份几何第一次上传(`bufferData` ×72)、
|
|
434
|
+
* 504 次 `drawElements`、6 份 program 现编 —— 第一版一道都没挡住:展厅里的展品(`ModelSlot` 那些 GLB 件,
|
|
435
|
+
* Lambert/Standard 带灯材质)既没上传也没编,因为它们既不是 ShaderMaterial 也不是 MeshBasicMaterial。
|
|
436
|
+
* 带灯材质的 program 缓存键里有「方向光/点光/聚光数量 + 阴影档 + 环境图」
|
|
437
|
+
* (`three.module.js:20901-20913`:`numDirLights`…`numDirLightShadows`、`shadowMapType`,另有 `envMap`/`fog` 两个位)
|
|
438
|
+
* ⇒ 预热那间小屋子把宿主的灯**克隆一份**摆进去、`fog`/`environment` 照抄,否则编出来是另一份 define 组 = 白编。
|
|
439
|
+
* 替身必须是**克隆**不是裸 Mesh:`instancing`/`skinning`/`morphTargets` 也各占缓存键一个位
|
|
440
|
+
* (`getProgramCacheKeyBooleans` 里那段 `_programLayers`)⇒ 拿裸 Mesh 画实例化材质编的是非实例化那一份。
|
|
441
|
+
* 克隆的灯只活在预热期,收尾时连各自的阴影图一起释放(`light.clone()` 给的是 `shadow.clone()` 的新份,
|
|
442
|
+
* 实测不与宿主共享 ⇒ 不会误释放宿主那张);阴影边长压到 `GPU_WARM_SHADOW_MAP_SIZE`,理由写在那颗常量上。
|
|
443
|
+
*
|
|
444
|
+
* ⚠️ 一次性清单不等于收工:隐藏期里展厅的内容**还没长齐**(GLB 逐帧各挂一颗、平面的 4 格瓦片逐张就绪逐张绑),
|
|
445
|
+
* 所以补收有两只手 —— ① 排空那一拍再扫一遍;② 内容落位那两个落点各调一次 `_topUpGpuPrewarm()`
|
|
446
|
+
* (GLB 挂进子树、平面绑上瓦片)。去重键 = `几何 id × 材质 id 串 × 各材质的 map id`(平面再带绑定戳记)
|
|
447
|
+
* ⇒ 补收只针对「没见过的那一档」,同一件不会被画第二遍。
|
|
448
|
+
*
|
|
449
|
+
* @param renderer 宿主那一份渲染器(隐藏期 `onBeforeRender` 不触发,库里拿不到 ⇒ 必须由宿主交进来;
|
|
450
|
+
* 站侧的落点就是 `FloorMktBase.preloadFirstPano(renderer)`,它本来就是隐藏期的入口)
|
|
451
|
+
*/
|
|
452
|
+
startGpuPrewarm(renderer: WebGLRenderer): void;
|
|
453
|
+
/** 停止隐藏期预热(宿主进入展厅时调):剩下的清单直接作废并**摘牌** —— 亮出来那一帧本来就要编要传,不必再抢额度;摘牌后晚到的内容也不再来补排。 */
|
|
454
|
+
stopGpuPrewarm(): void;
|
|
455
|
+
/**
|
|
456
|
+
* 内容后落位时补排预热(GLB 挂进子树那一拍、平面绑上瓦片纹理那一拍各调一次)。
|
|
457
|
+
*
|
|
458
|
+
* 为什么必须有这一颗:预热是隐藏期跑的,而隐藏期里展厅的内容**还没长齐** —— GLB 十几只各走一条异步下载链、
|
|
459
|
+
* 逐帧只挂一颗,平面那 4 格图也是逐张就绪逐张绑。一次性清单在几拍之内就排空了 ⇒ 晚到的那一族一件都没被画过,
|
|
460
|
+
* 进厅那一帧照样现编现传(这就是第一版「预热跑了、人还是卡了一下」的根)。
|
|
461
|
+
*
|
|
462
|
+
* 只在隐藏期响应:`_warmSeen` 归 null = 宿主已叫停(进厅)⇒ 晚到的东西交给「逐帧点亮」那一拍自己编,⛔ 别再抢额度。
|
|
463
|
+
*/
|
|
464
|
+
private _topUpGpuPrewarm;
|
|
465
|
+
/**
|
|
466
|
+
* 扫一遍本对象的子树,把「还没收过的那一档」压进清单。
|
|
467
|
+
* @returns 这次新收了几个(0 = 确实没有新东西 ⇒ 调用方据此收尾,⛔ 不是每帧空转)
|
|
468
|
+
*/
|
|
469
|
+
private _collectWarmItems;
|
|
470
|
+
/** 排下一件:先垫一个空函数占一格(全局队列每 tick 只跑一项 ⇒ 真活落在下一个 tick),与全景上传、平面绑定同一手法。 */
|
|
471
|
+
private _scheduleGpuWarmStep;
|
|
472
|
+
/** 队列回调的全部内容:预热**一件**(一次 1×1 绘制)。清单空了/已被摘牌就收尾。 */
|
|
473
|
+
private _runGpuWarmStep;
|
|
474
|
+
/**
|
|
475
|
+
* 造(并记住)预热那间小屋子:只建一次。
|
|
476
|
+
* `fog` / `environment` 照抄、宿主的灯**逐盏克隆**摆进来 —— 这三样都进 program 缓存键,
|
|
477
|
+
* 少一样就是编出一份进厅用不上的 program。
|
|
478
|
+
*/
|
|
479
|
+
private _ensureWarmScene;
|
|
480
|
+
/** 沿 `parent` 往上找到的第一个 Scene = 宿主场景(本站是 `largeScreenRender` 那一棵)。 */
|
|
481
|
+
private _findHostScene;
|
|
482
|
+
/**
|
|
483
|
+
* 收尾(跑完 / 进厅 / 宿主叫停 / `dispose` 四条路都汇到这里):清单作废 + 临时资源拆掉。
|
|
484
|
+
*
|
|
485
|
+
* ⚠️ **不动 `_warmSeen`**:自然跑完那一路后面还会有内容落位(GLB、后绑的瓦片),补排时全靠这张表跳过已经画过的旧档
|
|
486
|
+
* —— 清掉它等于把同一批东西再画一遍。真正的「摘牌」只有 `stopGpuPrewarm`/`dispose`,它们先把这颗归 null。
|
|
487
|
+
*/
|
|
488
|
+
private _finishGpuPrewarm;
|
|
489
|
+
/**
|
|
490
|
+
* 拆掉预热那套临时资源(1×1 渲染目标 + 克隆的灯 + 小场景)。
|
|
491
|
+
* 替身只是从小屋子里摘出来 —— 它的 `geometry`/`material` 都是宿主那一份对象 ⇒ ⛔ 在此 dispose,会把展厅要用的资源释放掉。
|
|
492
|
+
*/
|
|
493
|
+
private _releaseWarmHarness;
|
|
401
494
|
/** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
|
|
402
495
|
* @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
|
|
403
496
|
exportData(projName?: string): MktMeshData;
|
package/MktMesh/MktMesh.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* 说明:所有实例共享同一份 material(单一 panorama uniform),
|
|
24
24
|
* 同一时刻只激活名册 0 号槽(中心)那一个纹理(全局共享单激活方案)。
|
|
25
25
|
*/
|
|
26
|
-
import { BufferGeometry, BackSide, Float32BufferAttribute, Matrix4, Mesh, Shape, ShapeGeometry, SRGBColorSpace, Vector3, Vector4, } from "three";
|
|
26
|
+
import { BufferGeometry, BackSide, Float32BufferAttribute, Matrix4, OrthographicCamera, Scene, Mesh, Shape, ShapeGeometry, SRGBColorSpace, Vector3, Vector4, WebGLRenderTarget, } from "three";
|
|
27
27
|
import { createPlaceholderTexture, isPanoTexMode, mktPanoTiledFlag, mktPanoTexUrls, isRtStitchKey, rtStitchKeyParts, RT_STITCH_KEY_PREFIX, RT_STITCH_SLOT_COUNT } from "./mktShader";
|
|
28
28
|
import { MKT_PANO_MAX_SLOTS } from "./mktMappingShader";
|
|
29
29
|
import { createMktFloorMappingShaderMaterial } from "./mktMappingShader";
|
|
@@ -54,6 +54,20 @@ import { ImagePlaneTileLoader } from "./imageTileLoader";
|
|
|
54
54
|
/** 载入 JSON 时**优先**加载几个全景的素材(2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」)。
|
|
55
55
|
* 口径 = 存档 `textures` 数组的前 n 项;其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。 */
|
|
56
56
|
const PANO_IMPORT_PRIORITY = 2;
|
|
57
|
+
/** 隐藏期预热那一次绘制用的镜头(模块级共用:它只是个「有合法的投影矩阵可读」的载体,永不动 ⇒ ⛔ 不必每实例造一份)。 */
|
|
58
|
+
const GPU_WARM_CAMERA = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
|
59
|
+
/**
|
|
60
|
+
* 预热小屋子里那几盏克隆灯的**阴影图边长**(宿主一般是 4096²)。
|
|
61
|
+
* 压小不是降质量:这张图只在预热那间小屋子里用、像素当场扔掉,宿主自己那张阴影图归宿主每帧照画。
|
|
62
|
+
* 判据 = three 的 program 缓存键里只有 `shadowMapType` 与「几盏灯投影」这两档,**没有** `mapSize`
|
|
63
|
+
* (`three.module.js:20913` 一段 `numDirLightShadows / shadowMapType`,`mapSize` 只是 uniform)
|
|
64
|
+
* ⇒ 256² 编出来的 depth program 与 4096² 那份是同一个。
|
|
65
|
+
* 不压小的代价:预热每跑一件就等于替宿主重画一张 4096² 深度图(`shadow.autoUpdate` 默认开)
|
|
66
|
+
* ⇒ 隐藏期那圈自由视角跟着掉帧,那正是「卡」本身,⛔ 不能拿它换。
|
|
67
|
+
*/
|
|
68
|
+
const GPU_WARM_SHADOW_MAP_SIZE = 256;
|
|
69
|
+
/** 「同一件」判据里要看的那张主图 id:ShaderMaterial 那一族没有 `map` 这一档 ⇒ 恒 0;图片平面绑上图才换一档。 */
|
|
70
|
+
const warmMapId = (m) => m.map?.id ?? 0;
|
|
57
71
|
export default class MktMesh extends Mesh {
|
|
58
72
|
/** 纹理列表(每个纹理的 url + 世界矩阵) */
|
|
59
73
|
textures = [];
|
|
@@ -221,6 +235,22 @@ export default class MktMesh extends Mesh {
|
|
|
221
235
|
* 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
|
|
222
236
|
*/
|
|
223
237
|
_revealPending = [];
|
|
238
|
+
/**
|
|
239
|
+
* 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
|
|
240
|
+
* null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
|
|
241
|
+
*/
|
|
242
|
+
_warmItems = null;
|
|
243
|
+
/** 已收过的去重键(`几何 × 材质数组 × 各材质的 map`)⇒ 跨轮次只补收新出现的组合,不重复编同一条 GL 路径。
|
|
244
|
+
* 自然收尾时**故意留着**(晚到的内容补排时靠它跳过旧档);只有 `stopGpuPrewarm`/`dispose` 把它归 null = 摘牌。 */
|
|
245
|
+
_warmSeen = null;
|
|
246
|
+
/** 预热用的小场景:只挂当前这一件的替身 + 宿主灯光的克隆,`fog`/`environment` 照抄(都进 program 缓存键) */
|
|
247
|
+
_warmScene = null;
|
|
248
|
+
/** 此刻挂在预热小场景里的那一颗替身(一帧只画一件 ⇒ 换件时先把上一颗摘下来) */
|
|
249
|
+
_warmActive = null;
|
|
250
|
+
/** 预热小场景里的那几盏灯 = 宿主灯的克隆体(各持一份 `shadow` ⇒ 收尾时释放的是它们自己的阴影图,碰不到宿主的) */
|
|
251
|
+
_warmLights = null;
|
|
252
|
+
/** 预热落点:1×1 渲染目标(只有像素不要,要的是「这条 GL 路径确实走完了」),随预热结束/dispose 释放 */
|
|
253
|
+
_warmRt = null;
|
|
224
254
|
constructor(opts = {}) {
|
|
225
255
|
// 发布/预览态墙:**固定**用「全景序号直取」着色器(皇上旨 2026-09-29:mktmesh 就只有按照 attribute
|
|
226
256
|
// 配置来显示全景这一种固定的 shader——每帧由 feedFaceMapping 按 panoFaceMapping 把被引用的全景
|
|
@@ -1190,6 +1220,8 @@ export default class MktMesh extends Mesh {
|
|
|
1190
1220
|
return;
|
|
1191
1221
|
if (ms.mountStaged() && onEachLoad)
|
|
1192
1222
|
onEachLoad();
|
|
1223
|
+
// 挂进来的那一族此刻才在子树里 ⇒ 补排预热(清单已排空时,这一颗是重新开工的唯一触发点)
|
|
1224
|
+
this._topUpGpuPrewarm();
|
|
1193
1225
|
}
|
|
1194
1226
|
/**
|
|
1195
1227
|
* 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
|
|
@@ -1251,6 +1283,9 @@ export default class MktMesh extends Mesh {
|
|
|
1251
1283
|
}
|
|
1252
1284
|
}
|
|
1253
1285
|
ip.setTileTextures(texs, insets);
|
|
1286
|
+
// 瓦片是一张一张就绪的 ⇒ 每绑上一张,这一颗的「待画纹理」就换一档(去重键带绑定戳记)⇒ 补排一次,
|
|
1287
|
+
// 让上传发生在隐藏期而不是进厅那一帧。
|
|
1288
|
+
this._topUpGpuPrewarm();
|
|
1254
1289
|
if (pending) {
|
|
1255
1290
|
this._queueImagePlaneBind(ip);
|
|
1256
1291
|
return;
|
|
@@ -1276,6 +1311,243 @@ export default class MktMesh extends Mesh {
|
|
|
1276
1311
|
return;
|
|
1277
1312
|
}
|
|
1278
1313
|
}
|
|
1314
|
+
// ─────────────────── 隐藏期 GPU 预热(2026-11-01 需求:进厅那一下不要再现编着色器) ───────────────────
|
|
1315
|
+
/**
|
|
1316
|
+
* 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
|
|
1317
|
+
*
|
|
1318
|
+
* 为什么必须在隐藏期做:`visible = false` 时 three 的遍历第一句就 `if (object.visible === false) return`
|
|
1319
|
+
* (与上面 `tickProgressiveReveal` 注释同一颗雷)⇒ 墙根本进不了渲染列表,那份大 shader 从来没进过
|
|
1320
|
+
* three 的 program 缓存。进厅那一帧才第一次画它 = 编译 + 链接 + 顶点缓冲上传全压在那一帧,
|
|
1321
|
+
* 这就是「像有很多资源要载入 GPU」的另一半(前一半「16 张 6144² 渲染目标」已在拼合器里治掉)。
|
|
1322
|
+
*
|
|
1323
|
+
* ⛔ 不用 `renderer.compile()` / `compileAsync()`:那两个吃**整个场景**、一次把里面全部 program 编完,
|
|
1324
|
+
* 没地方按帧配额(`compileAsync` 的完成回调还会在同一帧落一坨主线程开销)。这里的做法是
|
|
1325
|
+
* 「拿同一个材质对象 + 同一个几何对象真画一次,画进 1×1 的渲染目标」⇒ 走的就是进厅那一条 GL 路径,
|
|
1326
|
+
* 编出来的正是同一份 program(three 的缓存按 `材质 × 几何属性形态 × 场景光照/雾` 计键),
|
|
1327
|
+
* 顶点缓冲也真上了 GPU;那 1×1 的像素直接扔掉,不占显存、不污染画面。
|
|
1328
|
+
*
|
|
1329
|
+
* ⚠️ 2026-11-01 第二版:清单从「只收不带灯」放宽到**全收**,替身从「自造一颗裸 Mesh」换成「原对象自己克隆一颗」。
|
|
1330
|
+
* 量出来的账(本地 dev,进厅三连帧 100/250/234 ms):进厅那几帧里 89 份几何第一次上传(`bufferData` ×72)、
|
|
1331
|
+
* 504 次 `drawElements`、6 份 program 现编 —— 第一版一道都没挡住:展厅里的展品(`ModelSlot` 那些 GLB 件,
|
|
1332
|
+
* Lambert/Standard 带灯材质)既没上传也没编,因为它们既不是 ShaderMaterial 也不是 MeshBasicMaterial。
|
|
1333
|
+
* 带灯材质的 program 缓存键里有「方向光/点光/聚光数量 + 阴影档 + 环境图」
|
|
1334
|
+
* (`three.module.js:20901-20913`:`numDirLights`…`numDirLightShadows`、`shadowMapType`,另有 `envMap`/`fog` 两个位)
|
|
1335
|
+
* ⇒ 预热那间小屋子把宿主的灯**克隆一份**摆进去、`fog`/`environment` 照抄,否则编出来是另一份 define 组 = 白编。
|
|
1336
|
+
* 替身必须是**克隆**不是裸 Mesh:`instancing`/`skinning`/`morphTargets` 也各占缓存键一个位
|
|
1337
|
+
* (`getProgramCacheKeyBooleans` 里那段 `_programLayers`)⇒ 拿裸 Mesh 画实例化材质编的是非实例化那一份。
|
|
1338
|
+
* 克隆的灯只活在预热期,收尾时连各自的阴影图一起释放(`light.clone()` 给的是 `shadow.clone()` 的新份,
|
|
1339
|
+
* 实测不与宿主共享 ⇒ 不会误释放宿主那张);阴影边长压到 `GPU_WARM_SHADOW_MAP_SIZE`,理由写在那颗常量上。
|
|
1340
|
+
*
|
|
1341
|
+
* ⚠️ 一次性清单不等于收工:隐藏期里展厅的内容**还没长齐**(GLB 逐帧各挂一颗、平面的 4 格瓦片逐张就绪逐张绑),
|
|
1342
|
+
* 所以补收有两只手 —— ① 排空那一拍再扫一遍;② 内容落位那两个落点各调一次 `_topUpGpuPrewarm()`
|
|
1343
|
+
* (GLB 挂进子树、平面绑上瓦片)。去重键 = `几何 id × 材质 id 串 × 各材质的 map id`(平面再带绑定戳记)
|
|
1344
|
+
* ⇒ 补收只针对「没见过的那一档」,同一件不会被画第二遍。
|
|
1345
|
+
*
|
|
1346
|
+
* @param renderer 宿主那一份渲染器(隐藏期 `onBeforeRender` 不触发,库里拿不到 ⇒ 必须由宿主交进来;
|
|
1347
|
+
* 站侧的落点就是 `FloorMktBase.preloadFirstPano(renderer)`,它本来就是隐藏期的入口)
|
|
1348
|
+
*/
|
|
1349
|
+
startGpuPrewarm(renderer) {
|
|
1350
|
+
if (!renderer)
|
|
1351
|
+
return;
|
|
1352
|
+
// 顺手记下渲染器:全景瓦片那道「每 tick 一张 initTexture」的闸在隐藏期也就能落下去了
|
|
1353
|
+
//(原先 `_glRenderer` 只在 `onBeforeRender` 里捕获 ⇒ 隐藏期排上的队全走「three 自己绑」的降级路)。
|
|
1354
|
+
this._glRenderer = renderer;
|
|
1355
|
+
if (this._warmItems)
|
|
1356
|
+
return; // 已在预热 ⇒ ⛔ 不重建清单(重复点会打断还没跑的那几件)
|
|
1357
|
+
this._warmItems = [];
|
|
1358
|
+
this._warmSeen = new Set();
|
|
1359
|
+
this._collectWarmItems();
|
|
1360
|
+
this._scheduleGpuWarmStep(renderer);
|
|
1361
|
+
}
|
|
1362
|
+
/** 停止隐藏期预热(宿主进入展厅时调):剩下的清单直接作废并**摘牌** —— 亮出来那一帧本来就要编要传,不必再抢额度;摘牌后晚到的内容也不再来补排。 */
|
|
1363
|
+
stopGpuPrewarm() {
|
|
1364
|
+
this._warmSeen = null; // 这颗就是「摘牌」本身:`_topUpGpuPrewarm` 只认它
|
|
1365
|
+
this._finishGpuPrewarm();
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* 内容后落位时补排预热(GLB 挂进子树那一拍、平面绑上瓦片纹理那一拍各调一次)。
|
|
1369
|
+
*
|
|
1370
|
+
* 为什么必须有这一颗:预热是隐藏期跑的,而隐藏期里展厅的内容**还没长齐** —— GLB 十几只各走一条异步下载链、
|
|
1371
|
+
* 逐帧只挂一颗,平面那 4 格图也是逐张就绪逐张绑。一次性清单在几拍之内就排空了 ⇒ 晚到的那一族一件都没被画过,
|
|
1372
|
+
* 进厅那一帧照样现编现传(这就是第一版「预热跑了、人还是卡了一下」的根)。
|
|
1373
|
+
*
|
|
1374
|
+
* 只在隐藏期响应:`_warmSeen` 归 null = 宿主已叫停(进厅)⇒ 晚到的东西交给「逐帧点亮」那一拍自己编,⛔ 别再抢额度。
|
|
1375
|
+
*/
|
|
1376
|
+
_topUpGpuPrewarm() {
|
|
1377
|
+
if (!this._warmSeen || this.visible)
|
|
1378
|
+
return;
|
|
1379
|
+
if (!this._warmItems) {
|
|
1380
|
+
this._warmItems = []; // 上一轮已经收尾 ⇒ 重新开工(去重表还在 ⇒ 只补新出现的那几档,旧的不会重画)
|
|
1381
|
+
this._scheduleGpuWarmStep(this._glRenderer); // `_warmSeen` 非空 ⇒ `startGpuPrewarm` 早已把渲染器交进来
|
|
1382
|
+
}
|
|
1383
|
+
this._collectWarmItems();
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* 扫一遍本对象的子树,把「还没收过的那一档」压进清单。
|
|
1387
|
+
* @returns 这次新收了几个(0 = 确实没有新东西 ⇒ 调用方据此收尾,⛔ 不是每帧空转)
|
|
1388
|
+
*/
|
|
1389
|
+
_collectWarmItems() {
|
|
1390
|
+
const items = this._warmItems;
|
|
1391
|
+
const seen = this._warmSeen;
|
|
1392
|
+
if (!items || !seen)
|
|
1393
|
+
return 0;
|
|
1394
|
+
let added = 0;
|
|
1395
|
+
// ⚠️ 遍历**不看 `visible`**(与 `_ensureWarmScene` 里那趟剪枝的灯正好相反):隐藏期里展品的槽、图片的层都是
|
|
1396
|
+
// 先压着不亮的(`setModels` 那一路进来就是 `visible = false`),要挡它们就等于把这次要治的那一族又漏回去。
|
|
1397
|
+
this.traverse((o) => {
|
|
1398
|
+
const mesh = o;
|
|
1399
|
+
if (!mesh.isMesh)
|
|
1400
|
+
return;
|
|
1401
|
+
const g = mesh.geometry;
|
|
1402
|
+
if (!g)
|
|
1403
|
+
return;
|
|
1404
|
+
const mats = (Array.isArray(mesh.material) ? mesh.material : [mesh.material]);
|
|
1405
|
+
if (!mats.length || mats.some((m) => !m || !m.visible))
|
|
1406
|
+
return; // 材质自己就不画 ⇒ 编了也用不上
|
|
1407
|
+
// 「同一件」= 同一份几何 × 同一串材质 × 同一串图。后两截都要进键,因为 program 缓存键里 `USE_MAP` 那一档
|
|
1408
|
+
// 看的是材质真绑没绑;而图片平面那一族的 4 格瓦片走的是 `_uTiles` uniform 数组(`material.map` 恒 null),
|
|
1409
|
+
// 绑上第 1 张那一刻才是「这条 GL 路径第一次真画」⇒ 拿它自带的绑定戳记当尺,图一换就补收一次。
|
|
1410
|
+
const key = `${g.id}|${mats.map((m) => m.id).join("+")}|${mats.map(warmMapId).join("+")}` +
|
|
1411
|
+
(o instanceof ImagePlane ? `|${o.tilesBindingStamp()}` : "");
|
|
1412
|
+
if (seen.has(key))
|
|
1413
|
+
return;
|
|
1414
|
+
seen.add(key);
|
|
1415
|
+
items.push({ src: o, m: mesh.material, g });
|
|
1416
|
+
added++;
|
|
1417
|
+
});
|
|
1418
|
+
return added;
|
|
1419
|
+
}
|
|
1420
|
+
/** 排下一件:先垫一个空函数占一格(全局队列每 tick 只跑一项 ⇒ 真活落在下一个 tick),与全景上传、平面绑定同一手法。 */
|
|
1421
|
+
_scheduleGpuWarmStep(renderer) {
|
|
1422
|
+
threeUtils.addWork(() => { });
|
|
1423
|
+
threeUtils.addWork(() => this._runGpuWarmStep(renderer));
|
|
1424
|
+
}
|
|
1425
|
+
/** 队列回调的全部内容:预热**一件**(一次 1×1 绘制)。清单空了/已被摘牌就收尾。 */
|
|
1426
|
+
_runGpuWarmStep(renderer) {
|
|
1427
|
+
const items = this._warmItems;
|
|
1428
|
+
if (!items)
|
|
1429
|
+
return; // `stopGpuPrewarm`/`dispose` 已摘牌 ⇒ 本项作废
|
|
1430
|
+
if (this.visible) { // 已经亮起来了(进厅)⇒ 不再和真实那帧抢额度
|
|
1431
|
+
this._finishGpuPrewarm();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (!items.length && this._collectWarmItems() === 0) { // 排空了:先补扫(GLB、后绑的图都是晚到的),真没有才收尾
|
|
1435
|
+
this._finishGpuPrewarm();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const it = items.shift();
|
|
1439
|
+
const scene = this._ensureWarmScene();
|
|
1440
|
+
let proxy = it.p;
|
|
1441
|
+
if (!proxy) {
|
|
1442
|
+
// 替身 = **原对象自己克隆一颗**(⛔ 自造裸 `Mesh`):`instancing`/`skinning`/`morphTargets` 都进缓存键,
|
|
1443
|
+
// 裸 Mesh 画实例化材质编的是非实例化那一份 = 白编。克隆体与宿主共享同一份 `geometry`/`material` 对象
|
|
1444
|
+
// ⇒ 编的是同一个 program、传的是同一个顶点缓冲。子树不要(`recursive = false`):一件只画一颗。
|
|
1445
|
+
proxy = it.p = it.src.clone(false);
|
|
1446
|
+
proxy.visible = true; // 宿主那颗此刻多半还是 `visible = false` ⇒ 替身得进得了 `projectObject`
|
|
1447
|
+
proxy.frustumCulled = false; // 墙几何是世界坐标、体量又比这 2×2 的取景框大得多 ⇒ 别按包围球剔除
|
|
1448
|
+
}
|
|
1449
|
+
if (it.src.isInstancedMesh) {
|
|
1450
|
+
const src = it.src;
|
|
1451
|
+
const inst = proxy;
|
|
1452
|
+
// `clone()` 里的 `instanceMatrix.copy(...)` 是**快照**(另起一份 Float32Array)⇒ 改回指宿主那一份:
|
|
1453
|
+
// 上传的就是进厅要用的那个 buffer,晚到的落位改动也一起带上
|
|
1454
|
+
inst.instanceMatrix = src.instanceMatrix;
|
|
1455
|
+
inst.count = src.count;
|
|
1456
|
+
}
|
|
1457
|
+
if (this._warmActive && this._warmActive !== proxy)
|
|
1458
|
+
scene.remove(this._warmActive); // 一帧只画这一件
|
|
1459
|
+
if (proxy.parent !== scene)
|
|
1460
|
+
scene.add(proxy);
|
|
1461
|
+
this._warmActive = proxy;
|
|
1462
|
+
const prevTarget = renderer.getRenderTarget();
|
|
1463
|
+
renderer.setRenderTarget(this._warmRt);
|
|
1464
|
+
renderer.render(scene, GPU_WARM_CAMERA);
|
|
1465
|
+
renderer.setRenderTarget(prevTarget);
|
|
1466
|
+
this._scheduleGpuWarmStep(renderer);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 造(并记住)预热那间小屋子:只建一次。
|
|
1470
|
+
* `fog` / `environment` 照抄、宿主的灯**逐盏克隆**摆进来 —— 这三样都进 program 缓存键,
|
|
1471
|
+
* 少一样就是编出一份进厅用不上的 program。
|
|
1472
|
+
*/
|
|
1473
|
+
_ensureWarmScene() {
|
|
1474
|
+
if (this._warmScene)
|
|
1475
|
+
return this._warmScene;
|
|
1476
|
+
const scene = new Scene();
|
|
1477
|
+
const lights = [];
|
|
1478
|
+
const host = this._findHostScene();
|
|
1479
|
+
if (host) {
|
|
1480
|
+
scene.fog = host.fog; // `USE_FOG` 进键
|
|
1481
|
+
scene.environment = host.environment; // `envMap` 进键(Standard 那族吃场景环境图)
|
|
1482
|
+
const walk = (o) => {
|
|
1483
|
+
if (!o.visible)
|
|
1484
|
+
return; // 与 three 的 `projectObject` 同一道闸:此刻不计数的灯,进厅那份 define 组里也没有
|
|
1485
|
+
const light = o;
|
|
1486
|
+
if (light.isLight) {
|
|
1487
|
+
const clone = light.clone(true);
|
|
1488
|
+
// `light.clone()` 里 `shadow = source.shadow.clone()` ⇒ 克隆体自带一份**新的** shadow
|
|
1489
|
+
// (边长、bias、投影相机范围都照抄,实测不与宿主共享),所以这里只管把它压小;
|
|
1490
|
+
// 压小的理由与「为什么压了还编得出同一份 program」都写在 `GPU_WARM_SHADOW_MAP_SIZE` 上。
|
|
1491
|
+
const shadow = clone.shadow;
|
|
1492
|
+
if (shadow?.mapSize)
|
|
1493
|
+
shadow.mapSize.set(GPU_WARM_SHADOW_MAP_SIZE, GPU_WARM_SHADOW_MAP_SIZE);
|
|
1494
|
+
// target 同样是新克隆的一份(位置一起带过来)⇒ 挂进这间屋子,`updateMatrixWorld` 才走得到它、
|
|
1495
|
+
// 光照方向才与宿主一致。⛔ 不许把宿主那颗挂过来:`Object3D.add` 会先从原父节点 `remove`,
|
|
1496
|
+
// 楼外那圈自由视角的阴影当场就被摘歪了。
|
|
1497
|
+
const target = clone.target;
|
|
1498
|
+
if (target)
|
|
1499
|
+
scene.add(target);
|
|
1500
|
+
scene.add(clone);
|
|
1501
|
+
lights.push(clone);
|
|
1502
|
+
}
|
|
1503
|
+
for (const child of o.children)
|
|
1504
|
+
walk(child);
|
|
1505
|
+
};
|
|
1506
|
+
walk(host);
|
|
1507
|
+
}
|
|
1508
|
+
this._warmLights = lights;
|
|
1509
|
+
this._warmScene = scene;
|
|
1510
|
+
if (!this._warmRt)
|
|
1511
|
+
this._warmRt = new WebGLRenderTarget(1, 1);
|
|
1512
|
+
return scene;
|
|
1513
|
+
}
|
|
1514
|
+
/** 沿 `parent` 往上找到的第一个 Scene = 宿主场景(本站是 `largeScreenRender` 那一棵)。 */
|
|
1515
|
+
_findHostScene() {
|
|
1516
|
+
for (let p = this.parent; p; p = p.parent) {
|
|
1517
|
+
if (p.isScene)
|
|
1518
|
+
return p;
|
|
1519
|
+
}
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* 收尾(跑完 / 进厅 / 宿主叫停 / `dispose` 四条路都汇到这里):清单作废 + 临时资源拆掉。
|
|
1524
|
+
*
|
|
1525
|
+
* ⚠️ **不动 `_warmSeen`**:自然跑完那一路后面还会有内容落位(GLB、后绑的瓦片),补排时全靠这张表跳过已经画过的旧档
|
|
1526
|
+
* —— 清掉它等于把同一批东西再画一遍。真正的「摘牌」只有 `stopGpuPrewarm`/`dispose`,它们先把这颗归 null。
|
|
1527
|
+
*/
|
|
1528
|
+
_finishGpuPrewarm() {
|
|
1529
|
+
this._warmItems = null;
|
|
1530
|
+
this._releaseWarmHarness();
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* 拆掉预热那套临时资源(1×1 渲染目标 + 克隆的灯 + 小场景)。
|
|
1534
|
+
* 替身只是从小屋子里摘出来 —— 它的 `geometry`/`material` 都是宿主那一份对象 ⇒ ⛔ 在此 dispose,会把展厅要用的资源释放掉。
|
|
1535
|
+
*/
|
|
1536
|
+
_releaseWarmHarness() {
|
|
1537
|
+
if (this._warmRt)
|
|
1538
|
+
this._warmRt.dispose();
|
|
1539
|
+
this._warmRt = null;
|
|
1540
|
+
for (const light of this._warmLights || []) {
|
|
1541
|
+
const shadow = light.shadow;
|
|
1542
|
+
if (shadow?.dispose)
|
|
1543
|
+
shadow.dispose(); // 放掉的是克隆体自己那张阴影图(`clone()` 给的是新 shadow)
|
|
1544
|
+
}
|
|
1545
|
+
this._warmLights = null;
|
|
1546
|
+
if (this._warmScene)
|
|
1547
|
+
this._warmScene.clear(); // 只摘孩子(替身、克隆灯、灯的 target)
|
|
1548
|
+
this._warmScene = null;
|
|
1549
|
+
this._warmActive = null;
|
|
1550
|
+
}
|
|
1279
1551
|
/** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
|
|
1280
1552
|
* @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
|
|
1281
1553
|
exportData(projName) {
|
|
@@ -1442,6 +1714,9 @@ export default class MktMesh extends Mesh {
|
|
|
1442
1714
|
this._imgBindDone.clear();
|
|
1443
1715
|
this._modelMountQueued.clear();
|
|
1444
1716
|
this._revealPending = [];
|
|
1717
|
+
// 隐藏期预热:清单、去重表与小屋子那套临时资源随本对象一起退场(那张 1×1 渲染目标是本类**唯一**自建的 RT)。
|
|
1718
|
+
// 走 `stopGpuPrewarm` 而不是 `_finishGpuPrewarm`:本对象都要没了,去重表再留着就是泄漏。
|
|
1719
|
+
this.stopGpuPrewarm();
|
|
1445
1720
|
this._stitcher.dispose();
|
|
1446
1721
|
this._glRenderer = null;
|
|
1447
1722
|
if (this._placeholder)
|