three-base 2.0.188 → 2.0.192

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.
@@ -107,6 +107,12 @@ export class ModelSlot extends Group {
107
107
  _staged = null;
108
108
  /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
109
109
  _loadToken = 0;
110
+ /**
111
+ * 内容是否已落定:真模型挂进了 `_contentRoot`,或这一颗压根没在等什么(空地址 / 取回失败 ⇒ 停在占位小立方体)。
112
+ * ⚠️ 落定**不等于**已经画进 GPU —— 顶点缓冲与首次 program 编译发生在「亮出来的那一帧」,
113
+ * 所以逐帧限流必须闸在点亮这一下(`MktMesh.tickProgressiveReveal`),闸在挂载那一下拦不住 GPU 侧的账。
114
+ */
115
+ _loaded = false;
110
116
  _disposed = false;
111
117
  constructor(name) {
112
118
  super();
@@ -126,6 +132,8 @@ export class ModelSlot extends Group {
126
132
  get naturalSize() { return this._naturalSize; }
127
133
  /** 是否已经挂上真模型(false = 仍是占位小立方体) */
128
134
  get hasModel() { return this._model !== null; }
135
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
136
+ get loaded() { return this._loaded; }
129
137
  /** 改尺寸档(米,最长边):立刻重缩内容 */
130
138
  setSize(size) {
131
139
  this._size = size > 0 ? size : this._size;
@@ -161,13 +169,25 @@ export class ModelSlot extends Group {
161
169
  async loadModel(url) {
162
170
  this._url = url;
163
171
  const token = ++this._loadToken;
172
+ this._loaded = false;
164
173
  this._detachModel();
165
174
  // 先立刻退回占位小立方体:GLB 在路上那段窗口(慢网/大模型)⛔ 不许让这一槽凭空消失
166
175
  // (取回失败那条支路本来也是停在占位上,两段时间里画面一致)
167
176
  this._layout();
168
- if (!url)
177
+ if (!url) {
178
+ // 空地址 = 这一颗压根没在等什么 ⇒ 当场落定(占位小立方体就是它的最终内容)
179
+ this._loaded = true;
169
180
  return true;
170
- const obj = await threeLoader.loadModel(url);
181
+ }
182
+ // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
183
+ // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
184
+ let obj;
185
+ try {
186
+ obj = await threeLoader.loadModel(url);
187
+ }
188
+ catch {
189
+ obj = undefined;
190
+ }
171
191
  // 迟到回调:这一槽已经改指向别的地址、或已经删除 ⇒ 当场释放,别留一份没人认领的显存
172
192
  if (this._disposed || token !== this._loadToken) {
173
193
  if (obj)
@@ -175,6 +195,7 @@ export class ModelSlot extends Group {
175
195
  return false;
176
196
  }
177
197
  if (!obj) {
198
+ this._loaded = true;
178
199
  this._layout();
179
200
  return false;
180
201
  }
@@ -189,6 +210,7 @@ export class ModelSlot extends Group {
189
210
  this._staged = null;
190
211
  this._model = obj;
191
212
  this._contentRoot.add(obj);
213
+ this._loaded = true;
192
214
  this._layout();
193
215
  return true;
194
216
  }
@@ -220,6 +242,8 @@ export class ModelSlot extends Group {
220
242
  this._size = d.size > 0 ? d.size : MODEL_SLOT_DEFAULT_SIZE;
221
243
  this._naturalSize = d.naturalSize;
222
244
  this._url = d.url || "";
245
+ // 落定判据跟着地址走:空地址 = 没在等什么(占位方块就是最终内容);有地址 = 等 GLB 那一拍
246
+ this._loaded = !this._url;
223
247
  if (!this._url)
224
248
  this._detachModel();
225
249
  this._layout();
@@ -137,6 +137,12 @@ export class ModelSlot extends Group {
137
137
  private _staged: Object3D | null = null;
138
138
  /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
139
139
  private _loadToken = 0;
140
+ /**
141
+ * 内容是否已落定:真模型挂进了 `_contentRoot`,或这一颗压根没在等什么(空地址 / 取回失败 ⇒ 停在占位小立方体)。
142
+ * ⚠️ 落定**不等于**已经画进 GPU —— 顶点缓冲与首次 program 编译发生在「亮出来的那一帧」,
143
+ * 所以逐帧限流必须闸在点亮这一下(`MktMesh.tickProgressiveReveal`),闸在挂载那一下拦不住 GPU 侧的账。
144
+ */
145
+ private _loaded = false;
140
146
  private _disposed = false;
141
147
 
142
148
  constructor(name?: string) {
@@ -164,6 +170,8 @@ export class ModelSlot extends Group {
164
170
  get naturalSize(): { x: number; y: number; z: number } | undefined { return this._naturalSize; }
165
171
  /** 是否已经挂上真模型(false = 仍是占位小立方体) */
166
172
  get hasModel(): boolean { return this._model !== null; }
173
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
174
+ get loaded(): boolean { return this._loaded; }
167
175
 
168
176
  /** 改尺寸档(米,最长边):立刻重缩内容 */
169
177
  setSize(size: number): void {
@@ -201,18 +209,31 @@ export class ModelSlot extends Group {
201
209
  async loadModel(url: string): Promise<boolean> {
202
210
  this._url = url;
203
211
  const token = ++this._loadToken;
212
+ this._loaded = false;
204
213
  this._detachModel();
205
214
  // 先立刻退回占位小立方体:GLB 在路上那段窗口(慢网/大模型)⛔ 不许让这一槽凭空消失
206
215
  // (取回失败那条支路本来也是停在占位上,两段时间里画面一致)
207
216
  this._layout();
208
- if (!url) return true;
209
- const obj = await threeLoader.loadModel(url);
217
+ if (!url) {
218
+ // 空地址 = 这一颗压根没在等什么 ⇒ 当场落定(占位小立方体就是它的最终内容)
219
+ this._loaded = true;
220
+ return true;
221
+ }
222
+ // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
223
+ // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
224
+ let obj: Object3D | undefined;
225
+ try {
226
+ obj = await threeLoader.loadModel(url);
227
+ } catch {
228
+ obj = undefined;
229
+ }
210
230
  // 迟到回调:这一槽已经改指向别的地址、或已经删除 ⇒ 当场释放,别留一份没人认领的显存
211
231
  if (this._disposed || token !== this._loadToken) {
212
232
  if (obj) disposeSubtree(obj);
213
233
  return false;
214
234
  }
215
235
  if (!obj) {
236
+ this._loaded = true;
216
237
  this._layout();
217
238
  return false;
218
239
  }
@@ -227,6 +248,7 @@ export class ModelSlot extends Group {
227
248
  this._staged = null;
228
249
  this._model = obj;
229
250
  this._contentRoot.add(obj);
251
+ this._loaded = true;
230
252
  this._layout();
231
253
  return true;
232
254
  }
@@ -257,6 +279,8 @@ export class ModelSlot extends Group {
257
279
  this._size = d.size > 0 ? d.size : MODEL_SLOT_DEFAULT_SIZE;
258
280
  this._naturalSize = d.naturalSize;
259
281
  this._url = d.url || "";
282
+ // 落定判据跟着地址走:空地址 = 没在等什么(占位方块就是最终内容);有地址 = 等 GLB 那一拍
283
+ this._loaded = !this._url;
260
284
  if (!this._url) this._detachModel();
261
285
  this._layout();
262
286
  }
@@ -6,7 +6,7 @@ import { type TileLayout } from "./mktShader";
6
6
  */
7
7
  export interface PanoStitcher {
8
8
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
9
- * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 6×3 / 2 槽 / 6144×6144,10-26 需求);
9
+ * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
10
10
  * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
11
11
  ensure(urls: string[], layout?: TileLayout): void;
12
12
  /**
@@ -59,7 +59,7 @@ export declare class PanoTileStitcher implements PanoStitcher {
59
59
  private _camera;
60
60
  private _mat;
61
61
  /** 几何是 2×2(铺满正交相机的 [-1,1]²),每格按 `1/slotCellCols × 1/slotCellRows` 缩放再挪过去
62
- * ⇒ 全景一格 = 槽的 1/3(横 1 竖 3,10-23 旨),图片平面一格 = 1/4(横 2 竖 2) */
62
+ * ⇒ 全景一格 = 槽的 1/4 宽 × 1/4 高(横 4 竖 4,2026-10-27 口径),图片平面一格 = 1/4(横 2 竖 2) */
63
63
  private _quad;
64
64
  /** 缺格那一路 `uMap` 也要有个合法纹理可绑(1×1,从不真采样) */
65
65
  private _blank;
@@ -67,10 +67,12 @@ export declare class PanoTileStitcher implements PanoStitcher {
67
67
  private _rtTextures;
68
68
  constructor();
69
69
  /** 确保批次已建组、加载已发起。幂等:同批已在册 ⇒ 直接返回(一片瓦片终身只下载、只解码一次)。
70
- * ⭐ 09-29「不足的涂灰」+ 10-26「6×3 = 18 片」:1~18 条皆可入组,内部补空串占位到 18;
70
+ * ⭐ 09-29「不足的涂灰」+ 2026-10-27「8×4 = 32 片」:1~32 条皆可入组,内部补空串占位到 32;
71
71
  * 空串 / 死链的格子拼合时涂灰(一眼看出该全景数据不全)。
72
- * ⛔ 旧版 16 条(4×4)、32 条(8×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
73
- * 超 18 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
72
+ * ⛔ 旧版 18 条(6×3)、16 条(4×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
73
+ * 超 32 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
74
+ * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
75
+ * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
74
76
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
75
77
  ensure(urls: string[], layout?: TileLayout): void;
76
78
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
@@ -121,7 +123,7 @@ export declare class PanoTileStitcher implements PanoStitcher {
121
123
  * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
122
124
  private _uploadTile;
123
125
  /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(每槽 `slotCellCols × slotCellRows` 格里的一格,
124
- * 10-26 起全景是「横 3 × 竖 3」= 9 格、图片平面是 2×2 = 4 格)。
126
+ * 2026-10-27 起全景是「横 4 × 竖 4」= 16 格、图片平面是 2×2 = 4 格)。
125
127
  * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 cellsPerSlot 帧**,
126
128
  * 清一次就把前面画好的格子抹了 ⇒ 槽纹理只剩最后一格。清屏交给我们自己(见 `_clearRt`)。 */
127
129
  private _stitchCell;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * @Description: 18 瓦片全景的「离屏拼合器」= **shader 逐格画进 WebGLRenderTarget**(本项目唯一一份拼合实现)。
2
+ * @Description: 32 瓦片全景的「离屏拼合器」= **shader 逐格画进 WebGLRenderTarget**(本项目唯一一份拼合实现)。
3
3
  *
4
4
  * 三道旨意叠成现在这一份:
5
5
  * ① 2026-09-29:一张全景切多片(每片所有 GPU 都装得下),运行时离屏拼回若干张固定尺寸的「槽纹理」,
@@ -28,9 +28,16 @@
28
28
  * ⇒ 改 **6×3=18 片 / 左右 2 槽**,每槽 = 原图半幅(横 3 格 × 竖 3 格)= **6144×6144**、与原图 1:1。
29
29
  * ⭐ 净效果:单全景有效分辨率与显存**一字未动**(仍是 75.5Mpx / ≈384MB),sampler = 点数 × 2 ⇒
30
30
  * **同屏全景点数 5 → 8**(8×2=16 正好用满硬顶)。旧存档 9 条地址**不兼容**,用户重传即可。
31
+ * ⑥ 2026-10-27(需求,逐字):「目前全景的 2个RT 是 3*3=9张的对吧, 我想要改成之前的 4*4=16张的逻辑, 数据你不用管,
32
+ * 我会重新上传, 你把代码改正确就行」
33
+ * ⇒ 改 **8×4=32 片 / 左右 2 槽**,每槽 = 原图半幅(横 4 格 × 竖 4 格 = 16 格)= **6144×6144**、与原图 1:1。
34
+ * ⭐ 净效果:单全景有效分辨率与显存**一字未动**(有效分辨率仍是 **12288×6144**、与 ⑤ 那版 18 片**逐像素同量**;
35
+ * 显存仍 ≈384MB 含 mipmap),sampler 账不变(每点 2 张 × 同屏 8 点 = 16 = 本机硬顶)⇒ 变的只有「切几刀」:
36
+ * 每格像素 2048×2048 → **1536×1536**、一组待画格 18 → **32**。
37
+ * ⚠️ 旧存档 18 / 9 / 64 / 16 条地址**不兼容**(张数不符被判废)⇒ 用户「数据你不用管,我会重新上传」。
31
38
  *
32
39
  * ⚠️ **仍然有 canvas,但只在上传切图那一步**(`src/views/MktMeshEdit/panoramaUpload.ts` 用 2D 画布把原图
33
- * 切成 18 个 JPEG 传 OSS)—— 那是**离线、一次性、产出的是数据**;运行时这条路上一个 canvas 都没有。
40
+ * 切成 32 个 JPEG 传 OSS)—— 那是**离线、一次性、产出的是数据**;运行时这条路上一个 canvas 都没有。
34
41
  *
35
42
  * ── 每帧的三道工序(旨②那两句话逐条落地,顺序固定)────────────────────────────────
36
43
  * 1. `_pumpDownloads()`:补发缺图的下载(每帧 ≤2 张、在册 ≤8、已解码待上传 ≤8)⇒ ⛔ 一批几十条 XHR 一起炸。
@@ -40,49 +47,51 @@
40
47
  * 到 GPU 里面去」)。这一趟**只上传、不画** ⇒ 上传与绘制永不落在同一拍(09-29 那句「两个大头别在同一
41
48
  * 帧」的延续),而且第 2 步天然只能画「上一帧及更早」传上去的图 ⇒ 每帧 = 1 上传 + 1 绘制(稳态)。
42
49
  *
43
- * ⚠️ 于是「一帧一片」的帧数随网格走:16 片 → 32 片 → 9 片 → **18 片/组 = 18 帧**(30ms/帧 ⇒ 约 0.55s,
44
- * 10-26 需求)。每格像素 = `RT_STITCH_SLOT_W/3 × RT_STITCH_SLOT_H/3` = **2048×2048**(原图切出来那片多大就多大,一比一)。
45
- * 且因为下面那条「先喂 shader」的旨意,第一格画完墙上就有东西了,不必等 18 帧。
50
+ * ⚠️ 于是「一帧一片」的帧数随网格走:16 片 → 32 片 → 9 片 → 18 片 → **32 片/组 = 32 帧**(30ms/帧 ⇒ 约 1.0s,
51
+ * 2026-10-27 需求)。每格像素 = `RT_STITCH_SLOT_W/4 × RT_STITCH_SLOT_H/4` = **1536×1536**(原图切出来那片多大就多大,一比一)。
52
+ * 且因为下面那条「先喂 shader」的旨意,第一格画完墙上就有东西了,不必等 32 帧。
46
53
  *
47
54
  * ⭐ 部分就绪就先喂(同日旨「1. 需要先喂给 shader」):`peek()` 不再等一个槽的 `cellsPerSlot` 格画满,
48
55
  * **画过 ≥1 格就交出去**。为了保证交出去的那张 RT 里绝无未定义像素,RT 建好后的第一件事是
49
56
  * 整张涂成缺失灰(`_clearRt`)⇒ 观感 = 该象限从左上角起逐格显影,缺数据的格子一直是灰的。
50
57
  *
51
58
  * ── 画质四条(⛔ 改任何一条都是改画质,先请旨)──────────────────────────────
52
- * 1. **尺寸 / 格子几何**:槽 `RT_STITCH_SLOT_W/H` = **6144×6144**、每槽「横 3 格 × 竖 3 格」、原图**横 6 × 竖 3** 片,
59
+ * 1. **尺寸 / 格子几何**:槽 `RT_STITCH_SLOT_W/H` = **6144×6144**、每槽「横 4 格 × 竖 4 格」、原图**横 8 × 竖 4** 片,
53
60
  * 全在 `./mktShader`(单一来源)。⭐ 10-23 旨后格子尺寸 = 那片瓦片的尺寸 ⇒ **一比一**:11968×5984 那类原图
54
- * 切 6×3 得 ~1995×1995 的片,铺进 2048×2048 的格子几乎不缩(缩放比 1.03,与上一版同一档)
55
- * ⇒ 单全景有效分辨率 33.5Mpx → **75.5Mpx**,是**升**画质,与「千万不要降低质量」红线同向。
61
+ * 切 8×4 得 ~1496×1496 的片,铺进 1536×1536 的格子几乎不缩(缩放比 1.03,与上一版同一档)
62
+ * ⇒ 单全景有效分辨率 33.5Mpx → **75.5Mpx**(= 12288×6144,本次与上一版**逐像素同量**),是**升**画质那一档定下的,
63
+ * 与「千万不要降低质量」红线同向。
56
64
  * ⛔ 不许拿槽尺寸当省显存的旋钮:现口径每槽覆盖**横 180° × 竖 180°**(原图左/右半幅),
57
- * 宽高比 1:1 与「半幅(3 列 × 3 行)」一致;上一版一槽装一整列时是 2:3,两版各自都对,⛔ 别混着抄。
65
+ * 宽高比 1:1 与「半幅(4 列 × 4 行)」一致;更早按列分槽那一版一槽装一整列时是 2:3,两版各自都对,⛔ 别混着抄。
58
66
  * 2. **色彩**:瓦片纹理 `colorSpace = SRGBColorSpace` ⇒ WebGL2 内部格式 `SRGB8_ALPHA8`,采样时硬件解码成
59
67
  * linear;RT 纹理同样标 sRGB ⇒ 写入时硬件再编码回 sRGB。一进一出**逐字节还原**。
60
68
  * ⚠️ 另一半证据在 three 里:写进非 XR 的 render target 时片元末尾的 `linearToOutputTexel` 恒按
61
69
  * LinearSRGB(= 不转换),编码完全交给硬件 ⇒ ⛔ 我们的片元绝不 `#include <colorspace_fragment>`,
62
70
  * 也不带 tone mapping 的 chunk(那两处都会再转一遍 ⇒ 画面发白)。
63
71
  * 3. **摩尔纹**:两层都开 mipmap ——
64
- * ① 瓦片纹理 `generateMipmaps` + `LinearMipmapLinearFilter`:一片原图(~1995×1995)铺进 2048×2048
72
+ * ① 瓦片纹理 `generateMipmaps` + `LinearMipmapLinearFilter`:一片原图(~1496×1496)铺进 1536×1536
65
73
  * 的格子是**一比一**,但墙面在远处仍会缩到格子以下 ⇒ ⛔ 单点双线性会采出锯齿,mipmap 照旧开
66
74
  * (WebGL2 对 NPOT 也能生成 mipmap,three r163 起只支持 WebGL2)。
67
75
  * ② 槽 RT 标 `generateMipmaps`:three 每次渲染进 RT 后自己 `glGenerateMipmap`(`WebGLRenderer.js`
68
76
  * `updateRenderTargetMipmap`)⇒ 与当年 CanvasTexture 上传后生成 mipmap 同一口径。
69
77
  * ⚠️ 代价:每格一次(旧版每格也是一次),都在 GPU 上、不占主线程。
70
- * 4. **接缝**:槽内「横 3 × 竖 3」⇒ 一张槽内部的格子线是 **2 横 2 竖**(上一版一槽一整列时只有 2 横 0 竖);
71
- * 槽与槽之间另有 u=1/2 **一道**跨纹理竖缝,那道治不了(见 `PanoTexMode` 说明②;上一版是两道)。治法 = 上传时每片向四边**多切**
78
+ * 4. **接缝**:槽内「横 4 × 竖 4」⇒ 一张槽内部的格子线是 **3 横 3 竖**(上一版 3×3 是 2 横 2 竖;更早一槽一整列时只有 2 横 0 竖);
79
+ * 槽与槽之间另有 u=1/2 **一道**跨纹理竖缝,那道治不了(见 `PanoTexMode` 说明②;更早按列 3 槽那版才是两道)。治法 = 上传时每片向四边**多切**
72
80
  * `RT_STITCH_SEAM_MARGIN_PX` 像素、拼合时**只采中间那块**(`uSrcRect`)⇒ 边界插值拿得到真邻居像素,
73
81
  * 不再复制自己的边缘。⛔ 这条不许「顺手简化」成直接铺满格子:几何会挤歪,缝也还在。
74
82
  * 详见 `./mktShader` 里那三个 `rtStitch*` 纯函数 —— **上传侧与拼合侧必须共用同一份几何**。
75
83
  *
76
- * 资源账(⚠️ 如实记;10-26 需求改 6×3=18 片 / 左右 2 槽 / 6144×6144 后重算):
84
+ * 资源账(⚠️ 如实记;2026-10-27 需求改 8×4=32 片 / 左右 2 槽 / 6144×6144 后重算 —— 槽尺寸一字未动 ⇒ 显存那一栏不变):
77
85
  * - 常驻显存:2 槽 × (6144×**6144**×4B + mipmap≈33%) ≈ **384MB/全景**(不含 mipmap 288MB —— 与上一版**一字不差**)
78
86
  * —— 更早那版 8×4=32 片 / 2 槽 4096² 只有 **170MB/全景** ⇒ 单全景 **2.26 倍**,代价来自「槽与原图一比一」那次**升分辨率**,
79
87
  * 不是浪费:33.5Mpx → 75.5Mpx 的有效像素全在 RT 里。
80
- * ⚠️ 组**运行中不淘汰**(见文末那条需求)⇒ 13 张全景全常驻 ≈ **4.9GB**(上一版 2.2GB)。同屏景点数虽回到 8,
88
+ * ⚠️ 组**运行中不淘汰**(见文末那条需求)⇒ 13 张全景全常驻 ≈ **4.9GB**(上一版同数 —— 单全景显存一字未动)。同屏景点数 8,
81
89
  * 那只省 sampler 绑定、**不省显存**。⚠️ 这是这一轮最大的风险项,已另记待用户裁决(见「RT 不回收」)。
82
- * - 上传总量:同一张原图切完 ⇒ **总像素与上一版逐字节同量**,片数 9 → 18、每片 4096×2048 → 2048×2048(单片像素减半)
83
- * ⇒ 下载发起数比 9 片那版**翻倍**(仍比 32 片那版少 14 条)、单请求变小。
90
+ * - 上传总量:同一张原图切完 ⇒ **总像素与上一版逐字节同量**,片数 18 → 32、每片 2048×2048 → 1536×1536(单片像素减到 ≈0.56 倍)
91
+ * ⇒ 下载发起数比 18 片那版**多 14 条**(与 10-06 那版 32 片同数)、单请求更小。
84
92
  * - 瓦片纹理画完即弃 ⇒ 同时在册的只有「下载中 + 已解码待上传」那些,上限 8+8 片;
85
- * ⚠️ 每片解码位图 2048×2048×4B = **16MB**(上一版每片 4096×2048 = 32MB)⇒ 峰值 CPU 侧 ≈ **256MB**(原 512MB)。
93
+ * ⚠️ 每片解码位图 1536×1536×4B = **9MB**(上一版每片 2048×2048 = 16MB)⇒ 闸内在制品峰值 CPU 侧 ≈ **144MB**(原 256MB);
94
+ * 一组 32 片若全解码则是 ≈**288MB**(上一版 18 片 × 16MB = 288MB —— **同一量**)。
86
95
  *
87
96
  * ⚠️ RT 是 GL 上下文私有资源:`pump(renderer)` 换 renderer 时整组作废(`_switchRenderer`),下一帧消费方
88
97
  * 照旧的 `ensure()` 重建(瓦片走浏览器 HTTP 缓存重新下载)。现况是**每个持有者一份拼合器 + 一个 renderer**
@@ -103,11 +112,11 @@ import { MISSING_TILE_GRAY, rtStitchBatchOf, rtStitchCellPos, rtStitchTileInset,
103
112
  * ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
104
113
  */
105
114
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
106
- /** 每帧最多发起几张下载(现行一批 18 片,⛔ 不加以限制就是一帧甩 18 条 XHR + 18 次 400 万像素解码) */
115
+ /** 每帧最多发起几张下载(现行一批 32 片,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次 ≈236 万像素解码) */
107
116
  const PANO_STITCH_DOWNLOADS_PER_FRAME = 2;
108
117
  /** 「下载中」的片数上限(跨所有批次) */
109
118
  const PANO_STITCH_MAX_INFLIGHT = 8;
110
- /** 「已解码、还没上传进 GPU」的片数上限 ⇒ 也是 CPU 侧位图的峰值内存闸(≈8 × 4.6MB) */
119
+ /** 「已解码、还没上传进 GPU」的片数上限 ⇒ 也是 CPU 侧位图的峰值内存闸(≈8 × 9MB,每片 1536×1536×4B) */
111
120
  const PANO_STITCH_MAX_DECODED_AHEAD = 8;
112
121
  /** 瓦片纹理的实际像素宽高(`loadPanoTexture` 交回来的是 `ImageBitmap`,尺寸就是切片区)。
113
122
  * 内缩比例按**实际尺寸**算 ⇒ 编码差 1px 也不会露出邻居格子的像素。拿不到尺寸时退回下面那个兜底边长。 */
@@ -192,7 +201,7 @@ export class PanoTileStitcher {
192
201
  blending: NoBlending,
193
202
  });
194
203
  /** 几何是 2×2(铺满正交相机的 [-1,1]²),每格按 `1/slotCellCols × 1/slotCellRows` 缩放再挪过去
195
- * ⇒ 全景一格 = 槽的 1/3(横 1 竖 3,10-23 旨),图片平面一格 = 1/4(横 2 竖 2) */
204
+ * ⇒ 全景一格 = 槽的 1/4 宽 × 1/4 高(横 4 竖 4,2026-10-27 口径),图片平面一格 = 1/4(横 2 竖 2) */
196
205
  _quad = new Mesh(new PlaneGeometry(2, 2), this._mat);
197
206
  /** 缺格那一路 `uMap` 也要有个合法纹理可绑(1×1,从不真采样) */
198
207
  _blank = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
@@ -204,10 +213,12 @@ export class PanoTileStitcher {
204
213
  this._blank.needsUpdate = true;
205
214
  }
206
215
  /** 确保批次已建组、加载已发起。幂等:同批已在册 ⇒ 直接返回(一片瓦片终身只下载、只解码一次)。
207
- * ⭐ 09-29「不足的涂灰」+ 10-26「6×3 = 18 片」:1~18 条皆可入组,内部补空串占位到 18;
216
+ * ⭐ 09-29「不足的涂灰」+ 2026-10-27「8×4 = 32 片」:1~32 条皆可入组,内部补空串占位到 32;
208
217
  * 空串 / 死链的格子拼合时涂灰(一眼看出该全景数据不全)。
209
- * ⛔ 旧版 16 条(4×4)、32 条(8×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
210
- * 超 18 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
218
+ * ⛔ 旧版 18 条(6×3)、16 条(4×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
219
+ * 超 32 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
220
+ * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
221
+ * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
211
222
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
212
223
  ensure(urls, layout) {
213
224
  const L = layout ?? PANO_TILE_LAYOUT;
@@ -442,14 +453,14 @@ export class PanoTileStitcher {
442
453
  }
443
454
  // ───────────────────────── 内部:单格绘制 ─────────────────────────
444
455
  /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(每槽 `slotCellCols × slotCellRows` 格里的一格,
445
- * 10-26 起全景是「横 3 × 竖 3」= 9 格、图片平面是 2×2 = 4 格)。
456
+ * 2026-10-27 起全景是「横 4 × 竖 4」= 16 格、图片平面是 2×2 = 4 格)。
446
457
  * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 cellsPerSlot 帧**,
447
458
  * 清一次就把前面画好的格子抹了 ⇒ 槽纹理只剩最后一格。清屏交给我们自己(见 `_clearRt`)。 */
448
459
  _stitchCell(renderer, g, slot, cell, tex) {
449
460
  const rt = this._ensureRt(renderer, g, slot);
450
461
  const tileIdx = g.slotTiles[slot][cell];
451
462
  const [cc, rr] = rtStitchCellPos(cell, g.layout.slotCellCols);
452
- // ⚠️ 横竖**分轴**缩放/摆位(旧的单一 `slotGrid` 方形假设整条作废):10-26 起全景一槽 = 原图半幅 ⇒ 3 横 3 竖,图片平面那档 2 横 2 竖
463
+ // ⚠️ 横竖**分轴**缩放/摆位(旧的单一 `slotGrid` 方形假设整条作废):现行全景一槽 = 原图半幅 ⇒ 4 横 4 竖,图片平面那档 2 横 2 竖
453
464
  const cw = g.layout.slotCellCols;
454
465
  const ch = g.layout.slotCellRows;
455
466
  this._quad.scale.set(1 / cw, 1 / ch, 1);
@@ -1,5 +1,5 @@
1
1
  /*
2
- * @Description: 18 瓦片全景的「离屏拼合器」= **shader 逐格画进 WebGLRenderTarget**(本项目唯一一份拼合实现)。
2
+ * @Description: 32 瓦片全景的「离屏拼合器」= **shader 逐格画进 WebGLRenderTarget**(本项目唯一一份拼合实现)。
3
3
  *
4
4
  * 三道旨意叠成现在这一份:
5
5
  * ① 2026-09-29:一张全景切多片(每片所有 GPU 都装得下),运行时离屏拼回若干张固定尺寸的「槽纹理」,
@@ -28,9 +28,16 @@
28
28
  * ⇒ 改 **6×3=18 片 / 左右 2 槽**,每槽 = 原图半幅(横 3 格 × 竖 3 格)= **6144×6144**、与原图 1:1。
29
29
  * ⭐ 净效果:单全景有效分辨率与显存**一字未动**(仍是 75.5Mpx / ≈384MB),sampler = 点数 × 2 ⇒
30
30
  * **同屏全景点数 5 → 8**(8×2=16 正好用满硬顶)。旧存档 9 条地址**不兼容**,用户重传即可。
31
+ * ⑥ 2026-10-27(需求,逐字):「目前全景的 2个RT 是 3*3=9张的对吧, 我想要改成之前的 4*4=16张的逻辑, 数据你不用管,
32
+ * 我会重新上传, 你把代码改正确就行」
33
+ * ⇒ 改 **8×4=32 片 / 左右 2 槽**,每槽 = 原图半幅(横 4 格 × 竖 4 格 = 16 格)= **6144×6144**、与原图 1:1。
34
+ * ⭐ 净效果:单全景有效分辨率与显存**一字未动**(有效分辨率仍是 **12288×6144**、与 ⑤ 那版 18 片**逐像素同量**;
35
+ * 显存仍 ≈384MB 含 mipmap),sampler 账不变(每点 2 张 × 同屏 8 点 = 16 = 本机硬顶)⇒ 变的只有「切几刀」:
36
+ * 每格像素 2048×2048 → **1536×1536**、一组待画格 18 → **32**。
37
+ * ⚠️ 旧存档 18 / 9 / 64 / 16 条地址**不兼容**(张数不符被判废)⇒ 用户「数据你不用管,我会重新上传」。
31
38
  *
32
39
  * ⚠️ **仍然有 canvas,但只在上传切图那一步**(`src/views/MktMeshEdit/panoramaUpload.ts` 用 2D 画布把原图
33
- * 切成 18 个 JPEG 传 OSS)—— 那是**离线、一次性、产出的是数据**;运行时这条路上一个 canvas 都没有。
40
+ * 切成 32 个 JPEG 传 OSS)—— 那是**离线、一次性、产出的是数据**;运行时这条路上一个 canvas 都没有。
34
41
  *
35
42
  * ── 每帧的三道工序(旨②那两句话逐条落地,顺序固定)────────────────────────────────
36
43
  * 1. `_pumpDownloads()`:补发缺图的下载(每帧 ≤2 张、在册 ≤8、已解码待上传 ≤8)⇒ ⛔ 一批几十条 XHR 一起炸。
@@ -40,49 +47,51 @@
40
47
  * 到 GPU 里面去」)。这一趟**只上传、不画** ⇒ 上传与绘制永不落在同一拍(09-29 那句「两个大头别在同一
41
48
  * 帧」的延续),而且第 2 步天然只能画「上一帧及更早」传上去的图 ⇒ 每帧 = 1 上传 + 1 绘制(稳态)。
42
49
  *
43
- * ⚠️ 于是「一帧一片」的帧数随网格走:16 片 → 32 片 → 9 片 → **18 片/组 = 18 帧**(30ms/帧 ⇒ 约 0.55s,
44
- * 10-26 需求)。每格像素 = `RT_STITCH_SLOT_W/3 × RT_STITCH_SLOT_H/3` = **2048×2048**(原图切出来那片多大就多大,一比一)。
45
- * 且因为下面那条「先喂 shader」的旨意,第一格画完墙上就有东西了,不必等 18 帧。
50
+ * ⚠️ 于是「一帧一片」的帧数随网格走:16 片 → 32 片 → 9 片 → 18 片 → **32 片/组 = 32 帧**(30ms/帧 ⇒ 约 1.0s,
51
+ * 2026-10-27 需求)。每格像素 = `RT_STITCH_SLOT_W/4 × RT_STITCH_SLOT_H/4` = **1536×1536**(原图切出来那片多大就多大,一比一)。
52
+ * 且因为下面那条「先喂 shader」的旨意,第一格画完墙上就有东西了,不必等 32 帧。
46
53
  *
47
54
  * ⭐ 部分就绪就先喂(同日旨「1. 需要先喂给 shader」):`peek()` 不再等一个槽的 `cellsPerSlot` 格画满,
48
55
  * **画过 ≥1 格就交出去**。为了保证交出去的那张 RT 里绝无未定义像素,RT 建好后的第一件事是
49
56
  * 整张涂成缺失灰(`_clearRt`)⇒ 观感 = 该象限从左上角起逐格显影,缺数据的格子一直是灰的。
50
57
  *
51
58
  * ── 画质四条(⛔ 改任何一条都是改画质,先请旨)──────────────────────────────
52
- * 1. **尺寸 / 格子几何**:槽 `RT_STITCH_SLOT_W/H` = **6144×6144**、每槽「横 3 格 × 竖 3 格」、原图**横 6 × 竖 3** 片,
59
+ * 1. **尺寸 / 格子几何**:槽 `RT_STITCH_SLOT_W/H` = **6144×6144**、每槽「横 4 格 × 竖 4 格」、原图**横 8 × 竖 4** 片,
53
60
  * 全在 `./mktShader`(单一来源)。⭐ 10-23 旨后格子尺寸 = 那片瓦片的尺寸 ⇒ **一比一**:11968×5984 那类原图
54
- * 切 6×3 得 ~1995×1995 的片,铺进 2048×2048 的格子几乎不缩(缩放比 1.03,与上一版同一档)
55
- * ⇒ 单全景有效分辨率 33.5Mpx → **75.5Mpx**,是**升**画质,与「千万不要降低质量」红线同向。
61
+ * 切 8×4 得 ~1496×1496 的片,铺进 1536×1536 的格子几乎不缩(缩放比 1.03,与上一版同一档)
62
+ * ⇒ 单全景有效分辨率 33.5Mpx → **75.5Mpx**(= 12288×6144,本次与上一版**逐像素同量**),是**升**画质那一档定下的,
63
+ * 与「千万不要降低质量」红线同向。
56
64
  * ⛔ 不许拿槽尺寸当省显存的旋钮:现口径每槽覆盖**横 180° × 竖 180°**(原图左/右半幅),
57
- * 宽高比 1:1 与「半幅(3 列 × 3 行)」一致;上一版一槽装一整列时是 2:3,两版各自都对,⛔ 别混着抄。
65
+ * 宽高比 1:1 与「半幅(4 列 × 4 行)」一致;更早按列分槽那一版一槽装一整列时是 2:3,两版各自都对,⛔ 别混着抄。
58
66
  * 2. **色彩**:瓦片纹理 `colorSpace = SRGBColorSpace` ⇒ WebGL2 内部格式 `SRGB8_ALPHA8`,采样时硬件解码成
59
67
  * linear;RT 纹理同样标 sRGB ⇒ 写入时硬件再编码回 sRGB。一进一出**逐字节还原**。
60
68
  * ⚠️ 另一半证据在 three 里:写进非 XR 的 render target 时片元末尾的 `linearToOutputTexel` 恒按
61
69
  * LinearSRGB(= 不转换),编码完全交给硬件 ⇒ ⛔ 我们的片元绝不 `#include <colorspace_fragment>`,
62
70
  * 也不带 tone mapping 的 chunk(那两处都会再转一遍 ⇒ 画面发白)。
63
71
  * 3. **摩尔纹**:两层都开 mipmap ——
64
- * ① 瓦片纹理 `generateMipmaps` + `LinearMipmapLinearFilter`:一片原图(~1995×1995)铺进 2048×2048
72
+ * ① 瓦片纹理 `generateMipmaps` + `LinearMipmapLinearFilter`:一片原图(~1496×1496)铺进 1536×1536
65
73
  * 的格子是**一比一**,但墙面在远处仍会缩到格子以下 ⇒ ⛔ 单点双线性会采出锯齿,mipmap 照旧开
66
74
  * (WebGL2 对 NPOT 也能生成 mipmap,three r163 起只支持 WebGL2)。
67
75
  * ② 槽 RT 标 `generateMipmaps`:three 每次渲染进 RT 后自己 `glGenerateMipmap`(`WebGLRenderer.js`
68
76
  * `updateRenderTargetMipmap`)⇒ 与当年 CanvasTexture 上传后生成 mipmap 同一口径。
69
77
  * ⚠️ 代价:每格一次(旧版每格也是一次),都在 GPU 上、不占主线程。
70
- * 4. **接缝**:槽内「横 3 × 竖 3」⇒ 一张槽内部的格子线是 **2 横 2 竖**(上一版一槽一整列时只有 2 横 0 竖);
71
- * 槽与槽之间另有 u=1/2 **一道**跨纹理竖缝,那道治不了(见 `PanoTexMode` 说明②;上一版是两道)。治法 = 上传时每片向四边**多切**
78
+ * 4. **接缝**:槽内「横 4 × 竖 4」⇒ 一张槽内部的格子线是 **3 横 3 竖**(上一版 3×3 是 2 横 2 竖;更早一槽一整列时只有 2 横 0 竖);
79
+ * 槽与槽之间另有 u=1/2 **一道**跨纹理竖缝,那道治不了(见 `PanoTexMode` 说明②;更早按列 3 槽那版才是两道)。治法 = 上传时每片向四边**多切**
72
80
  * `RT_STITCH_SEAM_MARGIN_PX` 像素、拼合时**只采中间那块**(`uSrcRect`)⇒ 边界插值拿得到真邻居像素,
73
81
  * 不再复制自己的边缘。⛔ 这条不许「顺手简化」成直接铺满格子:几何会挤歪,缝也还在。
74
82
  * 详见 `./mktShader` 里那三个 `rtStitch*` 纯函数 —— **上传侧与拼合侧必须共用同一份几何**。
75
83
  *
76
- * 资源账(⚠️ 如实记;10-26 需求改 6×3=18 片 / 左右 2 槽 / 6144×6144 后重算):
84
+ * 资源账(⚠️ 如实记;2026-10-27 需求改 8×4=32 片 / 左右 2 槽 / 6144×6144 后重算 —— 槽尺寸一字未动 ⇒ 显存那一栏不变):
77
85
  * - 常驻显存:2 槽 × (6144×**6144**×4B + mipmap≈33%) ≈ **384MB/全景**(不含 mipmap 288MB —— 与上一版**一字不差**)
78
86
  * —— 更早那版 8×4=32 片 / 2 槽 4096² 只有 **170MB/全景** ⇒ 单全景 **2.26 倍**,代价来自「槽与原图一比一」那次**升分辨率**,
79
87
  * 不是浪费:33.5Mpx → 75.5Mpx 的有效像素全在 RT 里。
80
- * ⚠️ 组**运行中不淘汰**(见文末那条需求)⇒ 13 张全景全常驻 ≈ **4.9GB**(上一版 2.2GB)。同屏景点数虽回到 8,
88
+ * ⚠️ 组**运行中不淘汰**(见文末那条需求)⇒ 13 张全景全常驻 ≈ **4.9GB**(上一版同数 —— 单全景显存一字未动)。同屏景点数 8,
81
89
  * 那只省 sampler 绑定、**不省显存**。⚠️ 这是这一轮最大的风险项,已另记待用户裁决(见「RT 不回收」)。
82
- * - 上传总量:同一张原图切完 ⇒ **总像素与上一版逐字节同量**,片数 9 → 18、每片 4096×2048 → 2048×2048(单片像素减半)
83
- * ⇒ 下载发起数比 9 片那版**翻倍**(仍比 32 片那版少 14 条)、单请求变小。
90
+ * - 上传总量:同一张原图切完 ⇒ **总像素与上一版逐字节同量**,片数 18 → 32、每片 2048×2048 → 1536×1536(单片像素减到 ≈0.56 倍)
91
+ * ⇒ 下载发起数比 18 片那版**多 14 条**(与 10-06 那版 32 片同数)、单请求更小。
84
92
  * - 瓦片纹理画完即弃 ⇒ 同时在册的只有「下载中 + 已解码待上传」那些,上限 8+8 片;
85
- * ⚠️ 每片解码位图 2048×2048×4B = **16MB**(上一版每片 4096×2048 = 32MB)⇒ 峰值 CPU 侧 ≈ **256MB**(原 512MB)。
93
+ * ⚠️ 每片解码位图 1536×1536×4B = **9MB**(上一版每片 2048×2048 = 16MB)⇒ 闸内在制品峰值 CPU 侧 ≈ **144MB**(原 256MB);
94
+ * 一组 32 片若全解码则是 ≈**288MB**(上一版 18 片 × 16MB = 288MB —— **同一量**)。
86
95
  *
87
96
  * ⚠️ RT 是 GL 上下文私有资源:`pump(renderer)` 换 renderer 时整组作废(`_switchRenderer`),下一帧消费方
88
97
  * 照旧的 `ensure()` 重建(瓦片走浏览器 HTTP 缓存重新下载)。现况是**每个持有者一份拼合器 + 一个 renderer**
@@ -135,13 +144,13 @@ import {
135
144
  */
136
145
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
137
146
 
138
- /** 每帧最多发起几张下载(现行一批 18 片,⛔ 不加以限制就是一帧甩 18 条 XHR + 18 次 400 万像素解码) */
147
+ /** 每帧最多发起几张下载(现行一批 32 片,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次 ≈236 万像素解码) */
139
148
  const PANO_STITCH_DOWNLOADS_PER_FRAME = 2;
140
149
 
141
150
  /** 「下载中」的片数上限(跨所有批次) */
142
151
  const PANO_STITCH_MAX_INFLIGHT = 8;
143
152
 
144
- /** 「已解码、还没上传进 GPU」的片数上限 ⇒ 也是 CPU 侧位图的峰值内存闸(≈8 × 4.6MB) */
153
+ /** 「已解码、还没上传进 GPU」的片数上限 ⇒ 也是 CPU 侧位图的峰值内存闸(≈8 × 9MB,每片 1536×1536×4B) */
145
154
  const PANO_STITCH_MAX_DECODED_AHEAD = 8;
146
155
 
147
156
  /** 瓦片纹理的实际像素宽高(`loadPanoTexture` 交回来的是 `ImageBitmap`,尺寸就是切片区)。
@@ -228,7 +237,7 @@ interface StitchGroup {
228
237
  */
229
238
  export interface PanoStitcher {
230
239
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
231
- * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 6×3 / 2 槽 / 6144×6144,10-26 需求);
240
+ * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
232
241
  * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
233
242
  ensure(urls: string[], layout?: TileLayout): void;
234
243
  /**
@@ -296,7 +305,7 @@ private _uploadQueued: Set<TileRec> = new Set();
296
305
  blending: NoBlending,
297
306
  });
298
307
  /** 几何是 2×2(铺满正交相机的 [-1,1]²),每格按 `1/slotCellCols × 1/slotCellRows` 缩放再挪过去
299
- * ⇒ 全景一格 = 槽的 1/3(横 1 竖 3,10-23 旨),图片平面一格 = 1/4(横 2 竖 2) */
308
+ * ⇒ 全景一格 = 槽的 1/4 宽 × 1/4 高(横 4 竖 4,2026-10-27 口径),图片平面一格 = 1/4(横 2 竖 2) */
300
309
  private _quad: Mesh<PlaneGeometry, ShaderMaterial> = new Mesh(new PlaneGeometry(2, 2), this._mat);
301
310
  /** 缺格那一路 `uMap` 也要有个合法纹理可绑(1×1,从不真采样) */
302
311
  private _blank: DataTexture = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
@@ -310,10 +319,12 @@ private _uploadQueued: Set<TileRec> = new Set();
310
319
  }
311
320
 
312
321
  /** 确保批次已建组、加载已发起。幂等:同批已在册 ⇒ 直接返回(一片瓦片终身只下载、只解码一次)。
313
- * ⭐ 09-29「不足的涂灰」+ 10-26「6×3 = 18 片」:1~18 条皆可入组,内部补空串占位到 18;
322
+ * ⭐ 09-29「不足的涂灰」+ 2026-10-27「8×4 = 32 片」:1~32 条皆可入组,内部补空串占位到 32;
314
323
  * 空串 / 死链的格子拼合时涂灰(一眼看出该全景数据不全)。
315
- * ⛔ 旧版 16 条(4×4)、32 条(8×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
316
- * 超 18 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
324
+ * ⛔ 旧版 18 条(6×3)、16 条(4×4)、64 条(8×8)与 9 条(3×3)的数据**都不做兼容**(用户「无须兼容正常使用, 有异常我会重传」):
325
+ * 超 32 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
326
+ * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
327
+ * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
317
328
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
318
329
  ensure(urls: string[], layout?: TileLayout): void {
319
330
  const L = layout ?? PANO_TILE_LAYOUT;
@@ -533,14 +544,14 @@ private _uploadQueued: Set<TileRec> = new Set();
533
544
  // ───────────────────────── 内部:单格绘制 ─────────────────────────
534
545
 
535
546
  /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(每槽 `slotCellCols × slotCellRows` 格里的一格,
536
- * 10-26 起全景是「横 3 × 竖 3」= 9 格、图片平面是 2×2 = 4 格)。
547
+ * 2026-10-27 起全景是「横 4 × 竖 4」= 16 格、图片平面是 2×2 = 4 格)。
537
548
  * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 cellsPerSlot 帧**,
538
549
  * 清一次就把前面画好的格子抹了 ⇒ 槽纹理只剩最后一格。清屏交给我们自己(见 `_clearRt`)。 */
539
550
  private _stitchCell(renderer: WebGLRenderer, g: StitchGroup, slot: number, cell: number, tex: Texture | null): void {
540
551
  const rt = this._ensureRt(renderer, g, slot);
541
552
  const tileIdx = g.slotTiles[slot][cell];
542
553
  const [cc, rr] = rtStitchCellPos(cell, g.layout.slotCellCols);
543
- // ⚠️ 横竖**分轴**缩放/摆位(旧的单一 `slotGrid` 方形假设整条作废):10-26 起全景一槽 = 原图半幅 ⇒ 3 横 3 竖,图片平面那档 2 横 2 竖
554
+ // ⚠️ 横竖**分轴**缩放/摆位(旧的单一 `slotGrid` 方形假设整条作废):现行全景一槽 = 原图半幅 ⇒ 4 横 4 竖,图片平面那档 2 横 2 竖
544
555
  const cw = g.layout.slotCellCols;
545
556
  const ch = g.layout.slotCellRows;
546
557
  this._quad.scale.set(1 / cw, 1 / ch, 1);
@@ -25,7 +25,8 @@ export interface MktTextureInfo {
25
25
  * ⭐ 皇上旨 2026-10-06 晚(逐字)「我希望一次能够展示 8 个全景纹理, 每个全景纹理由左右 2 个 RT 组成,
26
26
  * 每个 RT 由 16 个全景瓦片组成, 所以, 全景图在上传的时候, 只需要拆分成 横 8 * 竖 4 = 32 个瓦片图就可以了」
27
27
  * ⇒ **现行唯一形态 = 32 张(横 8 × 竖 4)**:行优先从图像左上开始,运行时由
28
- * `PanoTileStitcher` 用 shader 逐格画进 **2 张 4096×4096** `WebGLRenderTarget`(左半幅 / 右半幅,每槽 4×4 格),
28
+ * `PanoTileStitcher` 用 shader 逐格画进 **2 张 6144×6144** `WebGLRenderTarget`(左半幅 / 右半幅,每槽 4×4 格;
29
+ * 槽尺寸是 10-26 那版定的,2026-10-27 回到 32 片切档时一字未动),
29
30
  * 2 槽组成完整全景再喂 shader。⚠️ 同一天早些时候那道「8×8 = 64 片 / 4 张 4096×2048」**已作废**。
30
31
  * ⚠️ 每片向四边**多带** `RT_STITCH_SEAM_MARGIN_PX` 像素的邻居余量(治内部接缝;采样窗口由
31
32
  * `rtStitchTileInset` 还原),所以片宽 ≠ 原图正好 1/8、片高 ≠ 正好 1/4。
package/MktMesh/types.ts CHANGED
@@ -33,7 +33,8 @@ export interface MktTextureInfo {
33
33
  * ⭐ 皇上旨 2026-10-06 晚(逐字)「我希望一次能够展示 8 个全景纹理, 每个全景纹理由左右 2 个 RT 组成,
34
34
  * 每个 RT 由 16 个全景瓦片组成, 所以, 全景图在上传的时候, 只需要拆分成 横 8 * 竖 4 = 32 个瓦片图就可以了」
35
35
  * ⇒ **现行唯一形态 = 32 张(横 8 × 竖 4)**:行优先从图像左上开始,运行时由
36
- * `PanoTileStitcher` 用 shader 逐格画进 **2 张 4096×4096** `WebGLRenderTarget`(左半幅 / 右半幅,每槽 4×4 格),
36
+ * `PanoTileStitcher` 用 shader 逐格画进 **2 张 6144×6144** `WebGLRenderTarget`(左半幅 / 右半幅,每槽 4×4 格;
37
+ * 槽尺寸是 10-26 那版定的,2026-10-27 回到 32 片切档时一字未动),
37
38
  * 2 槽组成完整全景再喂 shader。⚠️ 同一天早些时候那道「8×8 = 64 片 / 4 张 4096×2048」**已作废**。
38
39
  * ⚠️ 每片向四边**多带** `RT_STITCH_SEAM_MARGIN_PX` 像素的邻居余量(治内部接缝;采样窗口由
39
40
  * `rtStitchTileInset` 还原),所以片宽 ≠ 原图正好 1/8、片高 ≠ 正好 1/4。
package/ThreeBase.d.ts CHANGED
@@ -44,6 +44,8 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
44
44
  maxPixelRatio: number;
45
45
  renderScale: number;
46
46
  private _resolution;
47
+ /** baseRenderTarget 之外**全部** RT 的倍率(深度 / 法线 / 各效果 mask / 屏幕合成);1 = 不超采样 */
48
+ auxResolution: number;
47
49
  get resolution(): number;
48
50
  set resolution(value: number);
49
51
  /**
@@ -127,7 +129,8 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
127
129
  setContainer(div: HTMLDivElement | null): Promise<void>;
128
130
  setSize(width: number, height: number): void;
129
131
  /**
130
- * 按「容器尺寸 × effectiveRatio × resolution」重建所有渲染目标尺寸。
132
+ * 按「容器尺寸 × effectiveRatio」重建所有渲染目标尺寸:base 再乘 resolution,
133
+ * 其余(深度 / 法线 / mask / 屏幕合成)再乘 auxResolution。
131
134
  * 由 setSize()(容器尺寸变化)与 resolution setter(运行时调倍率)共用 ——
132
135
  * 关键:resolution 是运行时可调的性能/质量旋钮,改完必须立刻重建 RT,
133
136
  * 否则倍率变了而 RT 尺寸没变,画面毫无变化(历史坑:"调分辨率没感觉")。