Commit cc7b6acb by xhw

loadNewModel

parent afc33b83
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
import * as THREE from "./js/three.module.js"; import * as THREE from "./js/three.module.js";
import { SparkRenderer, SplatMesh } from "./js/spark.module.js"; import { SparkRenderer, SplatMesh } from "./js/spark.module.js";
import { OrbitControls } from "./js/OrbitControls.js"; import { OrbitControls } from "./js/OrbitControls.js";
import { ensureSparkCompatibleSpz } from "./spz-v4-adapter.js";
export class SparkSplatViewer { export class SparkSplatViewer {
/** /**
...@@ -62,6 +63,7 @@ export class SparkSplatViewer { ...@@ -62,6 +63,7 @@ export class SparkSplatViewer {
autoRotateIdleDelay: options.autoRotateIdleDelay ?? 2000, autoRotateIdleDelay: options.autoRotateIdleDelay ?? 2000,
// 回调函数 // 回调函数
onBeforeLoad: options.onBeforeLoad ?? null,
onLoad: options.onLoad ?? null, onLoad: options.onLoad ?? null,
onProgress: options.onProgress ?? null, onProgress: options.onProgress ?? null,
onError: options.onError ?? null, onError: options.onError ?? null,
...@@ -403,7 +405,13 @@ export class SparkSplatViewer { ...@@ -403,7 +405,13 @@ export class SparkSplatViewer {
* @param {Function} loadOptions.onComplete - 加载完成回调 * @param {Function} loadOptions.onComplete - 加载完成回调
* @param {Function} loadOptions.onError - 加载失败回调 * @param {Function} loadOptions.onError - 加载失败回调
* @param {Function} loadOptions.onProgress - 加载进度回调 * @param {Function} loadOptions.onProgress - 加载进度回调
* @returns {SplatMesh} 新创建的 SplatMesh 实例 * @returns {Promise<SplatMesh>} 新创建的 SplatMesh 实例
*
* 格式兼容说明:
* 内置 SparkJS 只支持 gzip 封装的 SPZ v1~v3;
* 后端产出的 NGSP / ZSTD SPZ v4 会先在本方法内下载、解压重打包为
* gzip SPZ v3(见 spz-v4-adapter.js),再交给 SparkJS 解码。
* 非 SPZ 格式(.ply/.splat/.sog 等)仍走 SparkJS 原生 url 加载路径。
*/ */
loadModel(url, loadOptions = {}) { loadModel(url, loadOptions = {}) {
const { const {
...@@ -412,17 +420,99 @@ export class SparkSplatViewer { ...@@ -412,17 +420,99 @@ export class SparkSplatViewer {
desktopZoom = this.options.desktopZoom, desktopZoom = this.options.desktopZoom,
mobileZoom = this.options.mobileZoom, mobileZoom = this.options.mobileZoom,
tabletZoom = this.options.tabletZoom, tabletZoom = this.options.tabletZoom,
onBeforeLoad = null,
onComplete = null, onComplete = null,
onError = null, onError = null,
onProgress = null, onProgress = null,
} = loadOptions; } = loadOptions;
// 0. 加载开始前回调(可在此显示 loading 遮罩,避免白屏)
if (onBeforeLoad) onBeforeLoad();
if (this.options.onBeforeLoad) this.options.onBeforeLoad();
// 1. 移除旧模型 // 1. 移除旧模型
this._disposeCurrentModel(); this._disposeCurrentModel();
// 2. 创建新模型 // 2. .spz 文件先自行下载并做格式适配;其他格式走 SparkJS 原路径
const isSpz = /\.spz($|[?#])/i.test(url);
if (isSpz) {
return this._loadSpzWithCompat(url, {
autoFit, zoomDuration, desktopZoom, mobileZoom, tabletZoom,
onComplete, onError, onProgress,
});
}
return this._createSplatMesh({ url }, {
autoFit, zoomDuration, desktopZoom, mobileZoom, tabletZoom,
onComplete, onError, onProgress,
});
}
/**
* 下载 .spz → 检测格式 → v4 转 v3 → fileBytes 喂给 SplatMesh
* @private
*/
async _loadSpzWithCompat(url, loadOptions) {
const { onBeforeLoad, onError, onProgress } = loadOptions;
try {
// 自行下载以便检查魔数(同时汇报进度)
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const total = Number(response.headers.get("Content-Length")) || 0;
const reader = response.body.getReader();
const chunks = [];
let loaded = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
const event = { lengthComputable: total > 0, loaded, total };
if (onProgress) onProgress(event);
if (this.options.onProgress) this.options.onProgress(event);
}
let fileBytes = new Uint8Array(loaded);
let offset = 0;
for (const c of chunks) {
fileBytes.set(c, offset);
offset += c.length;
}
// 格式适配:NGSP v4 (zstd) → gzip v3;gzip v1~v3 原样直通
fileBytes = await ensureSparkCompatibleSpz(fileBytes);
return this._createSplatMesh({ fileBytes }, loadOptions);
} catch (err) {
console.error(`[SparkSplatViewer] 模型加载失败: ${url}`, err);
if (onError) onError(err);
if (this.options.onError) this.options.onError(err);
return null;
}
}
/**
* 创建 SplatMesh 并接入场景(原 loadModel 的创建逻辑)
* @param {Object} source - { url } 或 { fileBytes }
* @private
*/
_createSplatMesh(source, loadOptions) {
const {
autoFit,
zoomDuration = this.options.zoomDuration,
desktopZoom = this.options.desktopZoom,
mobileZoom = this.options.mobileZoom,
tabletZoom = this.options.tabletZoom,
onBeforeLoad = null,
onComplete = null,
onError = null,
onProgress = null,
} = loadOptions;
const url = source.url ?? "(fileBytes)";
// 创建新模型
const newMesh = new SplatMesh({ const newMesh = new SplatMesh({
url: url, ...source,
onLoad: (mesh) => { onLoad: (mesh) => {
console.log(`[SparkSplatViewer] 模型加载完成: ${url}`); console.log(`[SparkSplatViewer] 模型加载完成: ${url}`);
...@@ -553,6 +643,100 @@ export class SparkSplatViewer { ...@@ -553,6 +643,100 @@ export class SparkSplatViewer {
}; };
} }
// ═══════════════════════════════════════════════════
// 自动旋转控制方法(公共API)
// ═══════════════════════════════════════════════════
/**
* 设置自动旋转开关
* @param {boolean} enabled - true 开启, false 关闭
*/
setAutoRotate(enabled) {
this.options.autoRotate = !!enabled;
if (enabled) {
this._startAutoRotateFromCurrent();
} else {
this._isAutoRotating = false;
}
console.log(`[SparkSplatViewer] 自动旋转已${enabled ? '开启' : '关闭'}`);
}
/**
* 获取当前自动旋转状态
* @returns {boolean}
*/
getAutoRotate() {
return this.options.autoRotate;
}
/**
* 设置自动旋转速度
* @param {number} speed - 速度值(推荐 0.1 ~ 1.0)
*/
setAutoRotateSpeed(speed) {
this.options.autoRotateSpeed = Math.max(0, parseFloat(speed) || 0);
console.log(`[SparkSplatViewer] 自动旋转速度已设为: ${this.options.autoRotateSpeed}`);
}
/**
* 获取当前自动旋转速度
* @returns {number}
*/
getAutoRotateSpeed() {
return this.options.autoRotateSpeed;
}
/**
* 设置空闲延迟时间(用户停止交互后多久恢复自动旋转)
* @param {number} delayMs - 延迟时间(毫秒)
*/
setAutoRotateIdleDelay(delayMs) {
this.options.autoRotateIdleDelay = Math.max(0, parseInt(delayMs) || 0);
console.log(`[SparkSplatViewer] 空闲延迟已设为: ${this.options.autoRotateIdleDelay}ms`);
}
/**
* 获取当前空闲延迟时间
* @returns {number}
*/
getAutoRotateIdleDelay() {
return this.options.autoRotateIdleDelay;
}
/**
* 立即暂停自动旋转(不影响配置,只是临时暂停)
* 用户交互时会自动调用,也可手动调用
*/
pauseAutoRotate() {
this._isAutoRotating = false;
if (this._idleTimer) {
clearTimeout(this._idleTimer);
this._idleTimer = null;
}
console.log('[SparkSplatViewer] 自动旋转已暂停');
}
/**
* 恢复自动旋转(如果配置为开启状态)
*/
resumeAutoRotate() {
if (this.options.autoRotate && !this._isUserInteracting) {
this._startAutoRotateFromCurrent();
console.log('[SparkSplatViewer] 自动旋转已恢复');
}
}
/**
* 切换自动旋转状态(开/关)
* @returns {boolean} 切换后的状态
*/
toggleAutoRotate() {
const newState = !this.options.autoRotate;
this.setAutoRotate(newState);
return newState;
}
/** 销毁组件 */ /** 销毁组件 */
destroy() { destroy() {
this.isDestroyed = true; this.isDestroyed = true;
......
/**
* Spz4Transcoder - SPZ v4 (NGSP / ZSTD 六流) → 旧版 v3 (gzip 单流) 纯前端转码器
*
* 背景:
* SPZ v4 文件头为 32 字节明文(NGSP magic),数据部分改为 6 条独立的
* ZSTD 压缩流(位置/透明度/颜色/缩放/旋转/球谐),SparkJS 等基于旧版
* gzip 单流的加载器无法直接读取(会报 Unsupported SPZ version: 4)。
*
* 原理:
* v4 各属性的逐点编码与 v3 完全一致(24bit 定点坐标、8bit log 缩放、
* smallest-three 10bit 四元数、8bit alpha/颜色/球谐),因此无需逐点解码,
* 只需把 6 条流 ZSTD 解压后按旧版顺序重新拼接,套上 16 字节 v3 头再
* gzip,即可被任何旧版 spz 加载器读取。
*
* 用法:
* import { probeSpzFormat, transcodeSpz4ToV3 } from "./Spz4Transcoder.js";
* const format = probeSpzFormat(headBytes); // "v4" | "legacy" | "unknown"
* const v3Bytes = await transcodeSpz4ToV3(fileBytes); // gzip 后的旧版 .spz
*/
import { decompress as zstdDecompress } from "./js/fzstd.module.js";
export const SPZ_MAGIC = 0x5053474e; // "NGSP"
const GZIP_MAGIC_0 = 0x1f;
const GZIP_MAGIC_1 = 0x8b;
// v4 六条流的官方固定顺序(spz 源码 kAllSplatAttributes)
const V4_STREAM_ORDER = ["positions", "alphas", "colors", "scales", "rotations", "sh"];
// 旧版 gzip 单流内的属性拼接顺序
const LEGACY_PAYLOAD_ORDER = ["positions", "scales", "rotations", "alphas", "colors", "sh"];
/** 每个 SH 阶数对应的系数维度(每通道) */
const dimForDegree = (d) => [0, 3, 8, 15, 24][d] ?? 0;
/** v4 各属性每点字节数 */
function bytesPerPoint(attr, shDegree) {
switch (attr) {
case "positions": return 9; // 3 × 24bit 定点
case "alphas": return 1;
case "colors": return 3;
case "scales": return 3;
case "rotations": return 4; // smallest-three 10bit
case "sh": return dimForDegree(shDegree) * 3;
default: return 0;
}
}
/**
* 探测 spz 文件格式(只需前 4 字节)
* @param {Uint8Array} head - 文件头部字节(至少 4 字节)
* @returns {"v4"|"legacy"|"unknown"}
*/
export function probeSpzFormat(head) {
if (!head || head.length < 4) return "unknown";
// "NGSP" 明文 magic → v4
if (head[0] === 0x4e && head[1] === 0x47 && head[2] === 0x53 && head[3] === 0x50) {
return "v4";
}
// gzip magic → 旧版 v1/v2/v3
if (head[0] === GZIP_MAGIC_0 && head[1] === GZIP_MAGIC_1) {
return "legacy";
}
return "unknown";
}
/**
* 解析 v4 的 32 字节明文头
* @param {Uint8Array} bytes - 至少包含前 32 字节的文件数据
*/
export function parseSpz4Header(bytes) {
if (bytes.length < 32) throw new Error("[Spz4] 文件太小,不足 32 字节头");
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const magic = dv.getUint32(0, true);
if (magic !== SPZ_MAGIC) throw new Error("[Spz4] magic 不匹配,不是 NGSP 文件");
const header = {
magic,
version: dv.getUint32(4, true),
numPoints: dv.getUint32(8, true),
shDegree: dv.getUint8(12),
fractionalBits: dv.getUint8(13),
flags: dv.getUint8(14),
numStreams: dv.getUint8(15),
tocByteOffset: dv.getUint32(16, true),
};
header.antialiased = (header.flags & 0x1) !== 0;
header.hasExtensions = (header.flags & 0x2) !== 0;
if (header.version !== 4) throw new Error(`[Spz4] 不支持的版本: ${header.version}`);
if (header.shDegree > 4) throw new Error(`[Spz4] 非法 SH 阶数: ${header.shDegree}`);
return header;
}
/**
* 把 v4 文件转码为旧版 v3(gzip 单流)字节
* @param {Uint8Array} bytes - 完整 v4 文件内容
* @param {Object} [options]
* @param {Function} [options.onStage] - 阶段回调 (stage: string, ratio: 0~1)
* @returns {Promise<Uint8Array>} gzip 压缩后的旧版 .spz 字节
*/
export async function transcodeSpz4ToV3(bytes, options = {}) {
const { onStage = null } = options;
const header = parseSpz4Header(bytes);
const { numPoints, numStreams, tocByteOffset } = header;
let { shDegree } = header;
if (header.hasExtensions) {
console.warn("[Spz4] 文件含厂商扩展(如相机边界元数据),转码后将丢弃");
}
console.log(
`[Spz4] 头信息: ${numPoints} 点, SH${shDegree}, fractionalBits=${header.fractionalBits}, ` +
`${numStreams} 条流, TOC@${tocByteOffset}`
);
// ── 1. 读 TOC:每条流 [compressedSize u64, uncompressedSize u64] ──
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const toc = [];
for (let i = 0; i < numStreams; i++) {
const off = tocByteOffset + i * 16;
toc.push({
compressed: Number(dv.getBigUint64(off, true)),
uncompressed: Number(dv.getBigUint64(off + 8, true)),
});
}
// ── 2. 逐条 ZSTD 解压 ──
let streamStart = tocByteOffset + numStreams * 16;
const decoded = {}; // attr -> Uint8Array
for (let i = 0; i < numStreams; i++) {
const attr = V4_STREAM_ORDER[i] ?? `stream${i}`;
const { compressed, uncompressed } = toc[i];
const src = bytes.subarray(streamStart, streamStart + compressed);
streamStart += compressed;
if (uncompressed === 0) {
decoded[attr] = new Uint8Array(0);
} else {
decoded[attr] = zstdDecompress(src, new Uint8Array(uncompressed));
}
// 校验解压大小(仅对六个已知属性)
if (i < 6) {
const expected = numPoints * bytesPerPoint(attr, shDegree);
if (decoded[attr].length !== expected) {
throw new Error(
`[Spz4] ${attr} 流大小异常: 期望 ${expected},实际 ${decoded[attr].length}`
);
}
}
if (onStage) onStage(`decompress:${attr}`, (i + 1) / (numStreams + 1));
}
// ── 3. SH4 → SH3 截断(旧版加载器最高只认 SH3) ──
// v4 SH 布局为「系数在外、通道在内」,前 45 字节正好是 1~3 阶全部系数
let outShDegree = shDegree;
if (shDegree === 4) {
const src = decoded.sh;
const dst = new Uint8Array(numPoints * 15 * 3);
for (let p = 0; p < numPoints; p++) {
dst.set(src.subarray(p * 72, p * 72 + 45), p * 45);
}
decoded.sh = dst;
outShDegree = 3;
console.warn("[Spz4] SH4 已截断为 SH3(旧版加载器上限),视角相关光照细节略有损失");
}
// ── 4. 拼旧版 v3:16 字节头 + 按旧顺序拼接各属性 ──
const parts = LEGACY_PAYLOAD_ORDER.map((attr) => decoded[attr] ?? new Uint8Array(0));
const payloadSize = parts.reduce((n, p) => n + p.length, 0);
const out = new Uint8Array(16 + payloadSize);
const odv = new DataView(out.buffer);
odv.setUint32(0, SPZ_MAGIC, true); // magic
odv.setUint32(4, 3, true); // version = 3
odv.setUint32(8, numPoints, true); // numPoints
odv.setUint8(12, outShDegree); // shDegree
odv.setUint8(13, header.fractionalBits);// fractionalBits
odv.setUint8(14, header.flags & 0x1); // flags(只保留 antialiased 位)
odv.setUint8(15, 0); // reserved
let offset = 16;
for (const part of parts) {
out.set(part, offset);
offset += part.length;
}
if (onStage) onStage("assemble", 0.95);
// ── 5. gzip 压缩 ──
const gz = await gzipCompress(out);
console.log(
`[Spz4] 转码完成: v4 ${(bytes.length / 1024 / 1024).toFixed(2)}MB → ` +
`v3 ${(gz.length / 1024 / 1024).toFixed(2)}MB`
);
if (onStage) onStage("gzip", 1);
return gz;
}
/** 用浏览器原生 CompressionStream 做 gzip */
async function gzipCompress(bytes) {
if (typeof CompressionStream === "undefined") {
throw new Error("[Spz4] 当前浏览器不支持 CompressionStream,无法 gzip 压缩");
}
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
export default { probeSpzFormat, parseSpz4Header, transcodeSpz4ToV3 };
/**
* Bundled by jsDelivr using Rollup v4.62.2 and esbuild v0.28.1.
* Original file: /npm/fzstd@0.1.1/esm/index.mjs
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var gr=ArrayBuffer,F=Uint8Array,_=Uint16Array,zr=Int16Array,s=Int32Array,t=function(r,e,i){if(F.prototype.slice)return F.prototype.slice.call(r,e,i);(e==null||e<0)&&(e=0),(i==null||i>r.length)&&(i=r.length);var n=new F(i-e);return n.set(r.subarray(e,i)),n},N=function(r,e,i,n){if(F.prototype.fill)return F.prototype.fill.call(r,e,i,n);for((i==null||i<0)&&(i=0),(n==null||n>r.length)&&(n=r.length);i<n;++i)r[i]=e;return r},fr=function(r,e,i,n){if(F.prototype.copyWithin)return F.prototype.copyWithin.call(r,e,i,n);for((i==null||i<0)&&(i=0),(n==null||n>r.length)&&(n=r.length);i<n;)r[e++]=r[i++]},pr={InvalidData:0,WindowSizeTooLarge:1,InvalidBlockType:2,FSEAccuracyTooHigh:3,DistanceTooFarBack:4,UnexpectedEOF:5},Er=["invalid zstd data","window size too large (>2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],z=function(r,e,i){var n=new Error(e||Er[r]);if(n.code=r,Error.captureStackTrace&&Error.captureStackTrace(n,z),!i)throw n;return n},yr=function(r,e,i){for(var n=0,a=0;n<i;++n)a|=r[e++]<<(n<<3);return a},Ar=function(r,e){return(r[e]|r[e+1]<<8|r[e+2]<<16|r[e+3]<<24)>>>0},or=function(r,e){var i=r[0]|r[1]<<8|r[2]<<16;if(i==3126568&&r[3]==253){var n=r[4],a=n>>5&1,h=n>>2&1,v=n&3,l=n>>6;n&8&&z(0);var w=6-a,p=v==3?4:v,S=yr(r,w,p);w+=p;var g=l?1<<l:a,E=yr(r,w,g)+(l==1&&256),T=E;if(!a){var I=1<<10+(r[5]>>3);T=I+(I>>3)*(r[5]&7)}T>2145386496&&z(1);var o=new F((e==1?E||T:e?0:T)+12);return o[0]=1,o[4]=4,o[8]=8,{b:w+g,y:0,l:0,d:S,w:e&&e!=1?e:o.subarray(12),e:T,o:new s(o.buffer,0,3),u:E,c:h,m:Math.min(131072,T)}}else if((i>>4|r[3]<<20)==25481893)return Ar(r,4)+8;z(0)},Z=function(r){for(var e=0;1<<e<=r;++e);return e-1},P=function(r,e,i){var n=(e<<3)+4,a=(r[e]&15)+5;a>i&&z(3);for(var h=1<<a,v=h,l=-1,w=-1,p=-1,S=h,g=new gr(512+(h<<2)),E=new zr(g,0,256),T=new _(g,0,256),I=new _(g,512,h),o=512+(h<<1),A=new F(g,o,h),c=new F(g,o+h);l<255&&v>0;){var m=Z(v+1),B=n>>3,H=(1<<m+1)-1,D=(r[B]|r[B+1]<<8|r[B+2]<<16)>>(n&7)&H,f=(1<<m)-1,x=H-v-1,W=D&f;if(W<x?(n+=m,D=W):(n+=m+1,D>f&&(D-=x)),E[++l]=--D,D==-1?(v+=D,A[--S]=l):v-=D,!D)do{var j=n>>3;w=(r[j]|r[j+1]<<8)>>(n&7)&3,n+=2,l+=w}while(w==3)}(l>255||v)&&z(0);for(var M=0,q=(h>>1)+(h>>3)+3,C=h-1,O=0;O<=l;++O){var u=E[O];if(u<1){T[O]=-u;continue}for(p=0;p<u;++p){A[M]=O;do M=M+q&C;while(M>=S)}}for(M&&z(0),p=0;p<h;++p){var y=T[A[p]]++,U=c[p]=a-Z(y);I[p]=(y<<U)-h}return[n+7>>3,{b:a,s:A,n:c,t:I}]},Fr=function(r,e){var i=0,n=-1,a=new F(292),h=r[e],v=a.subarray(0,256),l=a.subarray(256,268),w=new _(a.buffer,268);if(h<128){var p=P(r,e+1,6),S=p[0],g=p[1];e+=h;var E=S<<3,T=r[e];T||z(0);for(var I=0,o=0,A=g.b,c=A,m=(++e<<3)-8+Z(T);m-=A,!(m<E);){var B=m>>3;if(I+=(r[B]|r[B+1]<<8)>>(m&7)&(1<<A)-1,v[++n]=g.s[I],m-=c,m<E)break;B=m>>3,o+=(r[B]|r[B+1]<<8)>>(m&7)&(1<<c)-1,v[++n]=g.s[o],A=g.n[I],I=g.t[I],c=g.n[o],o=g.t[o]}++n>255&&z(0)}else{for(n=h-127;i<n;i+=2){var H=r[++e];v[i]=H>>4,v[i+1]=H&15}++e}var D=0;for(i=0;i<n;++i){var f=v[i];f>11&&z(0),D+=f&&1<<f-1}var x=Z(D)+1,W=1<<x,j=W-D;for(j&j-1&&z(0),v[n++]=Z(j)+1,i=0;i<n;++i){var f=v[i];++l[v[i]=f&&x+1-f]}var M=new F(W<<1),q=M.subarray(0,W),C=M.subarray(W);for(w[x]=0,i=x;i>0;--i){var O=w[i];N(C,i,O,w[i-1]=O+l[i]*(1<<x-i))}for(w[0]!=W&&z(0),i=0;i<n;++i){var u=v[i];if(u){var y=w[u];N(q,i,y,w[u]=y+(1<<x-u))}}return[e,{n:C,b:x,s:q}]},Tr=P(new F([81,16,99,140,49,198,24,99,12,33,196,24,99,102,102,134,70,146,4]),0,6)[1],Br=P(new F([33,20,196,24,99,140,33,132,16,66,8,33,132,16,66,8,33,68,68,68,68,68,68,68,68,36,9]),0,6)[1],Dr=P(new F([32,132,16,66,102,70,68,68,68,68,36,73,2]),0,5)[1],lr=function(r,e){for(var i=r.length,n=new s(i),a=0;a<i;++a)n[a]=e,e+=1<<r[a];return n},d=new F(new s([0,0,0,0,16843009,50528770,134678020,202050057,269422093]).buffer,0,36),Sr=lr(d,0),rr=new F(new s([0,0,0,0,0,0,0,0,16843009,50528770,117769220,185207048,252579084,16]).buffer,0,53),mr=lr(rr,3),Q=function(r,e,i){var n=r.length,a=e.length,h=r[n-1],v=(1<<i.b)-1,l=-i.b;h||z(0);for(var w=0,p=i.b,S=(n<<3)-8+Z(h)-p,g=-1;S>l&&g<a;){var E=S>>3,T=(r[E]|r[E+1]<<8|r[E+2]<<16)>>(S&7);w=(w<<p|T)&v,e[++g]=i.s[w],S-=p=i.n[w]}(S!=l||g+1!=a)&&z(0)},Ir=function(r,e,i){var n=6,a=e.length,h=a+3>>2,v=h<<1,l=h+v;Q(r.subarray(n,n+=r[0]|r[1]<<8),e.subarray(0,h),i),Q(r.subarray(n,n+=r[2]|r[3]<<8),e.subarray(h,v),i),Q(r.subarray(n,n+=r[4]|r[5]<<8),e.subarray(v,l),i),Q(r.subarray(n),e.subarray(l),i)},wr=function(r,e,i){var n,a=e.b,h=r[a],v=h>>1&3;e.l=h&1;var l=h>>3|r[a+1]<<5|r[a+2]<<13,w=(a+=3)+l;if(v==1)return a>=r.length?void 0:(e.b=a+1,i?(N(i,r[a],e.y,e.y+=l),i):N(new F(l),r[a]));if(!(w>r.length)){if(v==0)return e.b=w,i?(i.set(r.subarray(a,w),e.y),e.y+=l,i):t(r,a,w);if(v==2){var p=r[a],S=p&3,g=p>>2&3,E=p>>4,T=0,I=0;S<2?g&1?E|=r[++a]<<4|(g&2&&r[++a]<<12):E=p>>3:(I=g,g<2?(E|=(r[++a]&63)<<4,T=r[a]>>6|r[++a]<<2):g==2?(E|=r[++a]<<4|(r[++a]&3)<<12,T=r[a]>>2|r[++a]<<6):(E|=r[++a]<<4|(r[++a]&63)<<12,T=r[a]>>6|r[++a]<<2|r[++a]<<10)),++a;var o=i?i.subarray(e.y,e.y+e.m):new F(e.m),A=o.length-E;if(S==0)o.set(r.subarray(a,a+=E),A);else if(S==1)N(o,r[a++],A);else{var c=e.h;if(S==2){var m=Fr(r,a);T+=a-(a=m[0]),e.h=c=m[1]}else c||z(0);(I?Ir:Q)(r.subarray(a,a+=T),o.subarray(A),c)}var B=r[a++];if(B){B==255?B=(r[a++]|r[a++]<<8)+32512:B>127&&(B=B-128<<8|r[a++]);var H=r[a++];H&3&&z(0);for(var D=[Br,Dr,Tr],f=2;f>-1;--f){var x=H>>(f<<1)+2&3;if(x==1){var W=new F([0,0,r[a++]]);D[f]={s:W.subarray(2,3),n:W.subarray(0,1),t:new _(W.buffer,0,1),b:0}}else x==2?(n=P(r,a,9-(f&1)),a=n[0],D[f]=n[1]):x==3&&(e.t||z(0),D[f]=e.t[f])}var j=e.t=D,M=j[0],q=j[1],C=j[2],O=r[w-1];O||z(0);var u=(w<<3)-8+Z(O)-C.b,y=u>>3,U=0,R=(r[y]|r[y+1]<<8)>>(u&7)&(1<<C.b)-1;y=(u-=q.b)>>3;var V=(r[y]|r[y+1]<<8)>>(u&7)&(1<<q.b)-1;y=(u-=M.b)>>3;var X=(r[y]|r[y+1]<<8)>>(u&7)&(1<<M.b)-1;for(++B;--B;){var k=C.s[R],ir=C.n[R],b=M.s[X],ar=M.n[X],nr=q.s[V],vr=q.n[V];y=(u-=nr)>>3;var hr=1<<nr,L=hr+((r[y]|r[y+1]<<8|r[y+2]<<16|r[y+3]<<24)>>>(u&7)&hr-1);y=(u-=rr[b])>>3;var G=mr[b]+((r[y]|r[y+1]<<8|r[y+2]<<16)>>(u&7)&(1<<rr[b])-1);y=(u-=d[k])>>3;var Y=Sr[k]+((r[y]|r[y+1]<<8|r[y+2]<<16)>>(u&7)&(1<<d[k])-1);if(y=(u-=ir)>>3,R=C.t[R]+((r[y]|r[y+1]<<8)>>(u&7)&(1<<ir)-1),y=(u-=ar)>>3,X=M.t[X]+((r[y]|r[y+1]<<8)>>(u&7)&(1<<ar)-1),y=(u-=vr)>>3,V=q.t[V]+((r[y]|r[y+1]<<8)>>(u&7)&(1<<vr)-1),L>3)e.o[2]=e.o[1],e.o[1]=e.o[0],e.o[0]=L-=3;else{var $=L-(Y!=0);$?(L=$==3?e.o[0]-1:e.o[$],$>1&&(e.o[2]=e.o[1]),e.o[1]=e.o[0],e.o[0]=L):L=e.o[0]}for(var f=0;f<Y;++f)o[U+f]=o[A+f];U+=Y,A+=Y;var J=U-L;if(J<0){var K=-J,ur=e.e+J;K>G&&(K=G);for(var f=0;f<K;++f)o[U+f]=e.w[ur+f];U+=K,G-=K,J=0}for(var f=0;f<G;++f)o[U+f]=o[J+f];U+=G}if(U!=A)for(;A<o.length;)o[U++]=o[A++];else U=o.length;i?e.y+=U:o=t(o,0,U)}else if(i){if(e.y+=E,A)for(var f=0;f<E;++f)o[f]=o[A+f]}else A&&(o=t(o,A));return e.b=w,o}z(2)}},er=function(r,e){if(r.length==1)return r[0];for(var i=new F(e),n=0,a=0;n<r.length;++n){var h=r[n];i.set(h,a),a+=h.length}return i};function xr(r,e){for(var i=[],n=+!e,a=0,h=0;r.length;){var v=or(r,n||e);if(typeof v=="object"){for(n?(e=null,v.w.length==v.u&&(i.push(e=v.w),h+=v.u)):(i.push(e),v.e=0);!v.l;){var l=wr(r,v,e);l||z(5),e?v.e=v.y:(i.push(l),h+=l.length,fr(v.w,0,l.length),v.w.set(l,v.w.length-l.length))}a=v.b+v.c*4}else a=v;r=r.subarray(a)}return er(i,h)}var Mr=(function(){function r(e){this.ondata=e,this.c=[],this.l=0,this.z=0}return r.prototype.push=function(e,i){if(typeof this.s=="number"){var n=Math.min(e.length,this.s);e=e.subarray(n),this.s-=n}var a=e.length,h=a+this.l;if(!this.s){if(i){if(!h){this.ondata(new F(0),!0);return}h<5&&z(5)}else if(h<18){this.c.push(e),this.l=h;return}if(this.l&&(this.c.push(e),e=er(this.c,h),this.c=[],this.l=0),typeof(this.s=or(e))=="number")return this.push(e,i)}if(typeof this.s!="number"){if(h<(this.z||3)){i&&z(5),this.c.push(e),this.l=h;return}if(this.l&&(this.c.push(e),e=er(this.c,h),this.c=[],this.l=0),!this.z&&h<(this.z=e[this.s.b]&2?4:3+(e[this.s.b]>>3|e[this.s.b+1]<<5|e[this.s.b+2]<<13))){i&&z(5),this.c.push(e),this.l=h;return}else this.z=0;for(;;){var v=wr(e,this.s);if(v)this.ondata(v,!1),fr(this.s.w,0,v.length),this.s.w.set(v,this.s.w.length-v.length);else{i&&z(5);var l=e.subarray(this.s.b);this.s.b=0,this.c.push(l),this.l+=l.length;return}if(this.s.l){var w=e.subarray(this.s.b);this.s=this.s.c*4,this.push(w,i);return}}}else i&&z(5)},r})();export{Mr as Decompress,pr as ZstdErrorCode,xr as decompress};
//# sourceMappingURL=/sm/4759345dbd148e971d15658881e9db85797ba5d3d73bc9e6fe58086a51d9e8e3.map
\ No newline at end of file
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
</style> </style>
<base target="_blank"> <base target="_blank">
<base target="_blank"> <base target="_blank">
<base target="_blank">
<base target="_blank">
<base target="_blank">
<base target="_blank">
</head> </head>
<body> <body>
<div id="viewer-container"></div> <div id="viewer-container"></div>
...@@ -78,7 +82,7 @@ ...@@ -78,7 +82,7 @@
/** /**
* 加载新模型并替换当前模型 * 加载新模型并替换当前模型
* @param {string} url - 模型文件路径 (.spz, .ply, .splat, .ksplat, .sog, .rad 等) * @param {string} url - 模型文件路径 (.spz 支持 gzip v1~v3 和 NGSP/ZSTD v4;另支持 .ply, .splat, .ksplat, .sog, .rad 等)
* @param {Object} options - 可选配置 * @param {Object} options - 可选配置
*/ */
window.loadNewModel = (url, options = {}) => { window.loadNewModel = (url, options = {}) => {
...@@ -88,43 +92,98 @@ ...@@ -88,43 +92,98 @@
desktopZoom: options.desktopZoom ?? 2.2, // 桌面端缩放 desktopZoom: options.desktopZoom ?? 2.2, // 桌面端缩放
mobileZoom: options.mobileZoom ?? 0.55, // 移动端缩放 mobileZoom: options.mobileZoom ?? 0.55, // 移动端缩放
tabletZoom: options.tabletZoom ?? 1.0, // 平板端缩放 tabletZoom: options.tabletZoom ?? 1.0, // 平板端缩放
onBeforeLoad: options.onBeforeLoad, // 加载开始前回调(可显示 loading 避免白屏)
onComplete: options.onComplete, // 加载完成回调 onComplete: options.onComplete, // 加载完成回调
onError: options.onError, // 加载失败回调 onError: options.onError, // 加载失败回调
onProgress: options.onProgress, // 进度回调 onProgress: options.onProgress, // 进度回调
},()=>{});
};
window.loadNewModel = (url, options = {}, callback = null) => {
// 简写形式: loadNewModel(url, (mesh) => { ... })
if (typeof options === "function") {
callback = options;
options = {};
}
return new Promise((resolve, reject) => {
viewer.loadModel(url, {
autoFit: options.autoFit !== false,
zoomDuration: options.zoomDuration ?? 500,
desktopZoom: options.desktopZoom ?? 2.2,
mobileZoom: options.mobileZoom ?? 0.55,
tabletZoom: options.tabletZoom ?? 1.0,
onProgress: options.onProgress,
onComplete: (mesh) => {
if (options.onComplete) options.onComplete(mesh);
if (callback) callback(mesh);
resolve(mesh);
},
onError: (err) => {
if (options.onError) options.onError(err);
reject(err);
},
});
}); });
}; };
// 使用示例: // 使用示例:
// loadNewModel("./new-model.spz"); // loadNewModel("./new-model.spz");
// loadNewModel("./new-model.spz", { autoFit: true, desktopZoom: 3.0 }); // loadNewModel("./new-model.spz", { autoFit: true, desktopZoom: 3.0 });
//
// 避免白屏:在 onBeforeLoad 中显示 loading 遮罩
// loadNewModel("./new-model.spz", {
// onBeforeLoad: () => { document.getElementById('loading').style.display = 'flex'; },
// onComplete: () => { document.getElementById('loading').style.display = 'none'; },
// onError: () => { document.getElementById('loading').style.display = 'none'; },
// });
// ═══════════════════════════════════════════════════
// 自动旋转控制方法(供外部调用)
// ═══════════════════════════════════════════════════
/**
* 开启/关闭自动旋转
* @param {boolean} enabled - true 开启, false 关闭
*/
window.setAutoRotate = (enabled) => {
viewer.setAutoRotate(enabled);
};
/**
* 获取当前自动旋转状态
* @returns {boolean}
*/
window.getAutoRotate = () => {
return viewer.getAutoRotate();
};
/**
* 设置自动旋转速度
* @param {number} speed - 速度值(推荐 0.1 ~ 1.0)
*/
window.setAutoRotateSpeed = (speed) => {
viewer.setAutoRotateSpeed(speed);
};
/**
* 获取当前自动旋转速度
* @returns {number}
*/
window.getAutoRotateSpeed = () => {
return viewer.getAutoRotateSpeed();
};
/**
* 设置空闲延迟时间
* @param {number} delayMs - 延迟时间(毫秒)
*/
window.setAutoRotateIdleDelay = (delayMs) => {
viewer.setAutoRotateIdleDelay(delayMs);
};
/**
* 获取当前空闲延迟时间
* @returns {number}
*/
window.getAutoRotateIdleDelay = () => {
return viewer.getAutoRotateIdleDelay();
};
/**
* 立即暂停自动旋转
*/
window.pauseAutoRotate = () => {
viewer.pauseAutoRotate();
};
/**
* 恢复自动旋转
*/
window.resumeAutoRotate = () => {
viewer.resumeAutoRotate();
};
/**
* 切换自动旋转状态
* @returns {boolean} 切换后的状态
*/
window.toggleAutoRotate = () => {
return viewer.toggleAutoRotate();
};
// 其他可用方法: // 其他可用方法:
// viewer.resetCamera(); // 重置相机 // viewer.resetCamera(); // 重置相机
...@@ -132,6 +191,14 @@ ...@@ -132,6 +191,14 @@
// viewer.getCameraInfo(); // 获取相机信息 // viewer.getCameraInfo(); // 获取相机信息
// viewer.animateZoomTo(2.0, 800); // 平滑缩放到指定值 // viewer.animateZoomTo(2.0, 800); // 平滑缩放到指定值
// viewer.destroy(); // 销毁组件 // viewer.destroy(); // 销毁组件
//
// 自动旋转控制:
// setAutoRotate(true/false); // 开启/关闭自动旋转
// toggleAutoRotate(); // 切换自动旋转状态
// setAutoRotateSpeed(0.5); // 设置旋转速度
// setAutoRotateIdleDelay(3000); // 设置空闲延迟(ms)
// pauseAutoRotate(); // 暂停自动旋转
// resumeAutoRotate(); // 恢复自动旋转
// 获取模型包围盒 // 获取模型包围盒
// const bounds = viewer.getModelBounds(); // const bounds = viewer.getModelBounds();
......
// Some numerical data is initialized as -1 even when it doesn't need initialization to help the JIT infer types
// aliases for shorter compressed code (most minifers don't do this)
var ab = ArrayBuffer, u8 = Uint8Array, u16 = Uint16Array, i16 = Int16Array, u32 = Uint32Array, i32 = Int32Array;
var slc = function (v, s, e) {
if (u8.prototype.slice)
return u8.prototype.slice.call(v, s, e);
if (s == null || s < 0)
s = 0;
if (e == null || e > v.length)
e = v.length;
var n = new u8(e - s);
n.set(v.subarray(s, e));
return n;
};
var fill = function (v, n, s, e) {
if (u8.prototype.fill)
return u8.prototype.fill.call(v, n, s, e);
if (s == null || s < 0)
s = 0;
if (e == null || e > v.length)
e = v.length;
for (; s < e; ++s)
v[s] = n;
return v;
};
var cpw = function (v, t, s, e) {
if (u8.prototype.copyWithin)
return u8.prototype.copyWithin.call(v, t, s, e);
if (s == null || s < 0)
s = 0;
if (e == null || e > v.length)
e = v.length;
while (s < e) {
v[t++] = v[s++];
}
};
/**
* Codes for errors generated within this library
*/
export var ZstdErrorCode = {
InvalidData: 0,
WindowSizeTooLarge: 1,
InvalidBlockType: 2,
FSEAccuracyTooHigh: 3,
DistanceTooFarBack: 4,
UnexpectedEOF: 5
};
// error codes
var ec = [
'invalid zstd data',
'window size too large (>2046MB)',
'invalid block type',
'FSE accuracy too high',
'match distance too far back',
'unexpected EOF'
];
var err = function (ind, msg, nt) {
var e = new Error(msg || ec[ind]);
e.code = ind;
if (Error.captureStackTrace)
Error.captureStackTrace(e, err);
if (!nt)
throw e;
return e;
};
var rb = function (d, b, n) {
var i = 0, o = 0;
for (; i < n; ++i)
o |= d[b++] << (i << 3);
return o;
};
var b4 = function (d, b) { return (d[b] | (d[b + 1] << 8) | (d[b + 2] << 16) | (d[b + 3] << 24)) >>> 0; };
// read Zstandard frame header
var rzfh = function (dat, w) {
var n3 = dat[0] | (dat[1] << 8) | (dat[2] << 16);
if (n3 == 0x2FB528 && dat[3] == 253) {
// Zstandard
var flg = dat[4];
// single segment checksum dict flag frame content flag
var ss = (flg >> 5) & 1, cc = (flg >> 2) & 1, df = flg & 3, fcf = flg >> 6;
if (flg & 8)
err(0);
// byte
var bt = 6 - ss;
// dict bytes
var db = df == 3 ? 4 : df;
// dictionary id
var di = rb(dat, bt, db);
bt += db;
// frame size bytes
var fsb = fcf ? (1 << fcf) : ss;
// frame source size
var fss = rb(dat, bt, fsb) + ((fcf == 1) && 256);
// window size
var ws = fss;
if (!ss) {
// window descriptor
var wb = 1 << (10 + (dat[5] >> 3));
ws = wb + (wb >> 3) * (dat[5] & 7);
}
if (ws > 2145386496)
err(1);
var buf = new u8((w == 1 ? (fss || ws) : w ? 0 : ws) + 12);
buf[0] = 1, buf[4] = 4, buf[8] = 8;
return {
b: bt + fsb,
y: 0,
l: 0,
d: di,
w: (w && w != 1) ? w : buf.subarray(12),
e: ws,
o: new i32(buf.buffer, 0, 3),
u: fss,
c: cc,
m: Math.min(131072, ws)
};
}
else if (((n3 >> 4) | (dat[3] << 20)) == 0x184D2A5) {
// skippable
return b4(dat, 4) + 8;
}
err(0);
};
// most significant bit for nonzero
var msb = function (val) {
var bits = 0;
for (; (1 << bits) <= val; ++bits)
;
return bits - 1;
};
// read finite state entropy
var rfse = function (dat, bt, mal) {
// table pos
var tpos = (bt << 3) + 4;
// accuracy log
var al = (dat[bt] & 15) + 5;
if (al > mal)
err(3);
// size
var sz = 1 << al;
// probabilities symbols repeat index high threshold
var probs = sz, sym = -1, re = -1, i = -1, ht = sz;
// optimization: single allocation is much faster
var buf = new ab(512 + (sz << 2));
var freq = new i16(buf, 0, 256);
// same view as freq
var dstate = new u16(buf, 0, 256);
var nstate = new u16(buf, 512, sz);
var bb1 = 512 + (sz << 1);
var syms = new u8(buf, bb1, sz);
var nbits = new u8(buf, bb1 + sz);
while (sym < 255 && probs > 0) {
var bits = msb(probs + 1);
var cbt = tpos >> 3;
// mask
var msk = (1 << (bits + 1)) - 1;
var val = ((dat[cbt] | (dat[cbt + 1] << 8) | (dat[cbt + 2] << 16)) >> (tpos & 7)) & msk;
// mask (1 fewer bit)
var msk1fb = (1 << bits) - 1;
// max small value
var msv = msk - probs - 1;
// small value
var sval = val & msk1fb;
if (sval < msv)
tpos += bits, val = sval;
else {
tpos += bits + 1;
if (val > msk1fb)
val -= msv;
}
freq[++sym] = --val;
if (val == -1) {
probs += val;
syms[--ht] = sym;
}
else
probs -= val;
if (!val) {
do {
// repeat byte
var rbt = tpos >> 3;
re = ((dat[rbt] | (dat[rbt + 1] << 8)) >> (tpos & 7)) & 3;
tpos += 2;
sym += re;
} while (re == 3);
}
}
if (sym > 255 || probs)
err(0);
var sympos = 0;
// sym step (coprime with sz - formula from zstd source)
var sstep = (sz >> 1) + (sz >> 3) + 3;
// sym mask
var smask = sz - 1;
for (var s = 0; s <= sym; ++s) {
var sf = freq[s];
if (sf < 1) {
dstate[s] = -sf;
continue;
}
// This is split into two loops in zstd to avoid branching, but as JS is higher-level that is unnecessary
for (i = 0; i < sf; ++i) {
syms[sympos] = s;
do {
sympos = (sympos + sstep) & smask;
} while (sympos >= ht);
}
}
// After spreading symbols, should be zero again
if (sympos)
err(0);
for (i = 0; i < sz; ++i) {
// next state
var ns = dstate[syms[i]]++;
// num bits
var nb = nbits[i] = al - msb(ns);
nstate[i] = (ns << nb) - sz;
}
return [(tpos + 7) >> 3, {
b: al,
s: syms,
n: nbits,
t: nstate
}];
};
// read huffman
var rhu = function (dat, bt) {
// index weight count
var i = 0, wc = -1;
// buffer header byte
var buf = new u8(292), hb = dat[bt];
// huffman weights
var hw = buf.subarray(0, 256);
// rank count
var rc = buf.subarray(256, 268);
// rank index
var ri = new u16(buf.buffer, 268);
// NOTE: at this point bt is 1 less than expected
if (hb < 128) {
// end byte, fse decode table
var _a = rfse(dat, bt + 1, 6), ebt = _a[0], fdt = _a[1];
bt += hb;
var epos = ebt << 3;
// last byte
var lb = dat[bt];
if (!lb)
err(0);
// state1 state2 state1 bits state2 bits
var st1 = 0, st2 = 0, btr1 = fdt.b, btr2 = btr1;
// fse pos
// pre-increment to account for original deficit of 1
var fpos = (++bt << 3) - 8 + msb(lb);
for (;;) {
fpos -= btr1;
if (fpos < epos)
break;
var cbt = fpos >> 3;
st1 += ((dat[cbt] | (dat[cbt + 1] << 8)) >> (fpos & 7)) & ((1 << btr1) - 1);
hw[++wc] = fdt.s[st1];
fpos -= btr2;
if (fpos < epos)
break;
cbt = fpos >> 3;
st2 += ((dat[cbt] | (dat[cbt + 1] << 8)) >> (fpos & 7)) & ((1 << btr2) - 1);
hw[++wc] = fdt.s[st2];
btr1 = fdt.n[st1];
st1 = fdt.t[st1];
btr2 = fdt.n[st2];
st2 = fdt.t[st2];
}
if (++wc > 255)
err(0);
}
else {
wc = hb - 127;
for (; i < wc; i += 2) {
var byte = dat[++bt];
hw[i] = byte >> 4;
hw[i + 1] = byte & 15;
}
++bt;
}
// weight exponential sum
var wes = 0;
for (i = 0; i < wc; ++i) {
var wt = hw[i];
// bits must be at most 11, same as weight
if (wt > 11)
err(0);
wes += wt && (1 << (wt - 1));
}
// max bits
var mb = msb(wes) + 1;
// table size
var ts = 1 << mb;
// remaining sum
var rem = ts - wes;
// must be power of 2
if (rem & (rem - 1))
err(0);
hw[wc++] = msb(rem) + 1;
for (i = 0; i < wc; ++i) {
var wt = hw[i];
++rc[hw[i] = wt && (mb + 1 - wt)];
}
// huf buf
var hbuf = new u8(ts << 1);
// symbols num bits
var syms = hbuf.subarray(0, ts), nb = hbuf.subarray(ts);
ri[mb] = 0;
for (i = mb; i > 0; --i) {
var pv = ri[i];
fill(nb, i, pv, ri[i - 1] = pv + rc[i] * (1 << (mb - i)));
}
if (ri[0] != ts)
err(0);
for (i = 0; i < wc; ++i) {
var bits = hw[i];
if (bits) {
var code = ri[bits];
fill(syms, i, code, ri[bits] = code + (1 << (mb - bits)));
}
}
return [bt, {
n: nb,
b: mb,
s: syms
}];
};
// Tables generated using this:
// https://gist.github.com/101arrowz/a979452d4355992cbf8f257cbffc9edd
// default literal length table
var dllt = /*#__PURE__*/ rfse(/*#__PURE__*/ new u8([
81, 16, 99, 140, 49, 198, 24, 99, 12, 33, 196, 24, 99, 102, 102, 134, 70, 146, 4
]), 0, 6)[1];
// default match length table
var dmlt = /*#__PURE__*/ rfse(/*#__PURE__*/ new u8([
33, 20, 196, 24, 99, 140, 33, 132, 16, 66, 8, 33, 132, 16, 66, 8, 33, 68, 68, 68, 68, 68, 68, 68, 68, 36, 9
]), 0, 6)[1];
// default offset code table
var doct = /*#__PURE__ */ rfse(/*#__PURE__*/ new u8([
32, 132, 16, 66, 102, 70, 68, 68, 68, 68, 36, 73, 2
]), 0, 5)[1];
// bits to baseline
var b2bl = function (b, s) {
var len = b.length, bl = new i32(len);
for (var i = 0; i < len; ++i) {
bl[i] = s;
s += 1 << b[i];
}
return bl;
};
// literal length bits
var llb = /*#__PURE__ */ new u8(( /*#__PURE__ */new i32([
0, 0, 0, 0, 16843009, 50528770, 134678020, 202050057, 269422093
])).buffer, 0, 36);
// literal length baseline
var llbl = /*#__PURE__ */ b2bl(llb, 0);
// match length bits
var mlb = /*#__PURE__ */ new u8(( /*#__PURE__ */new i32([
0, 0, 0, 0, 0, 0, 0, 0, 16843009, 50528770, 117769220, 185207048, 252579084, 16
])).buffer, 0, 53);
// match length baseline
var mlbl = /*#__PURE__ */ b2bl(mlb, 3);
// decode huffman stream
var dhu = function (dat, out, hu) {
var len = dat.length, ss = out.length, lb = dat[len - 1], msk = (1 << hu.b) - 1, eb = -hu.b;
if (!lb)
err(0);
var st = 0, btr = hu.b, pos = (len << 3) - 8 + msb(lb) - btr, i = -1;
for (; pos > eb && i < ss;) {
var cbt = pos >> 3;
var val = (dat[cbt] | (dat[cbt + 1] << 8) | (dat[cbt + 2] << 16)) >> (pos & 7);
st = ((st << btr) | val) & msk;
out[++i] = hu.s[st];
pos -= (btr = hu.n[st]);
}
if (pos != eb || i + 1 != ss)
err(0);
};
// decode huffman stream 4x
// TODO: use workers to parallelize
var dhu4 = function (dat, out, hu) {
var bt = 6;
var ss = out.length, sz1 = (ss + 3) >> 2, sz2 = sz1 << 1, sz3 = sz1 + sz2;
dhu(dat.subarray(bt, bt += dat[0] | (dat[1] << 8)), out.subarray(0, sz1), hu);
dhu(dat.subarray(bt, bt += dat[2] | (dat[3] << 8)), out.subarray(sz1, sz2), hu);
dhu(dat.subarray(bt, bt += dat[4] | (dat[5] << 8)), out.subarray(sz2, sz3), hu);
dhu(dat.subarray(bt), out.subarray(sz3), hu);
};
// read Zstandard block
var rzb = function (dat, st, out) {
var _a;
var bt = st.b;
// byte 0 block type
var b0 = dat[bt], btype = (b0 >> 1) & 3;
st.l = b0 & 1;
var sz = (b0 >> 3) | (dat[bt + 1] << 5) | (dat[bt + 2] << 13);
// end byte for block
var ebt = (bt += 3) + sz;
if (btype == 1) {
if (bt >= dat.length)
return;
st.b = bt + 1;
if (out) {
fill(out, dat[bt], st.y, st.y += sz);
return out;
}
return fill(new u8(sz), dat[bt]);
}
if (ebt > dat.length)
return;
if (btype == 0) {
st.b = ebt;
if (out) {
out.set(dat.subarray(bt, ebt), st.y);
st.y += sz;
return out;
}
return slc(dat, bt, ebt);
}
if (btype == 2) {
// byte 3 lit btype size format
var b3 = dat[bt], lbt = b3 & 3, sf = (b3 >> 2) & 3;
// lit src size lit cmp sz 4 streams
var lss = b3 >> 4, lcs = 0, s4 = 0;
if (lbt < 2) {
if (sf & 1)
lss |= (dat[++bt] << 4) | ((sf & 2) && (dat[++bt] << 12));
else
lss = b3 >> 3;
}
else {
s4 = sf;
if (sf < 2)
lss |= ((dat[++bt] & 63) << 4), lcs = (dat[bt] >> 6) | (dat[++bt] << 2);
else if (sf == 2)
lss |= (dat[++bt] << 4) | ((dat[++bt] & 3) << 12), lcs = (dat[bt] >> 2) | (dat[++bt] << 6);
else
lss |= (dat[++bt] << 4) | ((dat[++bt] & 63) << 12), lcs = (dat[bt] >> 6) | (dat[++bt] << 2) | (dat[++bt] << 10);
}
++bt;
// add literals to end - can never overlap with backreferences because unused literals always appended
var buf = out ? out.subarray(st.y, st.y + st.m) : new u8(st.m);
// starting point for literals
var spl = buf.length - lss;
if (lbt == 0)
buf.set(dat.subarray(bt, bt += lss), spl);
else if (lbt == 1)
fill(buf, dat[bt++], spl);
else {
// huffman table
var hu = st.h;
if (lbt == 2) {
var hud = rhu(dat, bt);
// subtract description length
lcs += bt - (bt = hud[0]);
st.h = hu = hud[1];
}
else if (!hu)
err(0);
(s4 ? dhu4 : dhu)(dat.subarray(bt, bt += lcs), buf.subarray(spl), hu);
}
// num sequences
var ns = dat[bt++];
if (ns) {
if (ns == 255)
ns = (dat[bt++] | (dat[bt++] << 8)) + 0x7F00;
else if (ns > 127)
ns = ((ns - 128) << 8) | dat[bt++];
// symbol compression modes
var scm = dat[bt++];
if (scm & 3)
err(0);
var dts = [dmlt, doct, dllt];
for (var i = 2; i > -1; --i) {
var md = (scm >> ((i << 1) + 2)) & 3;
if (md == 1) {
// rle buf
var rbuf = new u8([0, 0, dat[bt++]]);
dts[i] = {
s: rbuf.subarray(2, 3),
n: rbuf.subarray(0, 1),
t: new u16(rbuf.buffer, 0, 1),
b: 0
};
}
else if (md == 2) {
// accuracy log 8 for offsets, 9 for others
_a = rfse(dat, bt, 9 - (i & 1)), bt = _a[0], dts[i] = _a[1];
}
else if (md == 3) {
if (!st.t)
err(0);
dts[i] = st.t[i];
}
}
var _b = st.t = dts, mlt = _b[0], oct = _b[1], llt = _b[2];
var lb = dat[ebt - 1];
if (!lb)
err(0);
var spos = (ebt << 3) - 8 + msb(lb) - llt.b, cbt = spos >> 3, oubt = 0;
var lst = ((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << llt.b) - 1);
cbt = (spos -= oct.b) >> 3;
var ost = ((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << oct.b) - 1);
cbt = (spos -= mlt.b) >> 3;
var mst = ((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << mlt.b) - 1);
for (++ns; --ns;) {
var llc = llt.s[lst];
var lbtr = llt.n[lst];
var mlc = mlt.s[mst];
var mbtr = mlt.n[mst];
var ofc = oct.s[ost];
var obtr = oct.n[ost];
cbt = (spos -= ofc) >> 3;
var ofp = 1 << ofc;
var off = ofp + (((dat[cbt] | (dat[cbt + 1] << 8) | (dat[cbt + 2] << 16) | (dat[cbt + 3] << 24)) >>> (spos & 7)) & (ofp - 1));
cbt = (spos -= mlb[mlc]) >> 3;
var ml = mlbl[mlc] + (((dat[cbt] | (dat[cbt + 1] << 8) | (dat[cbt + 2] << 16)) >> (spos & 7)) & ((1 << mlb[mlc]) - 1));
cbt = (spos -= llb[llc]) >> 3;
var ll = llbl[llc] + (((dat[cbt] | (dat[cbt + 1] << 8) | (dat[cbt + 2] << 16)) >> (spos & 7)) & ((1 << llb[llc]) - 1));
cbt = (spos -= lbtr) >> 3;
lst = llt.t[lst] + (((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << lbtr) - 1));
cbt = (spos -= mbtr) >> 3;
mst = mlt.t[mst] + (((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << mbtr) - 1));
cbt = (spos -= obtr) >> 3;
ost = oct.t[ost] + (((dat[cbt] | (dat[cbt + 1] << 8)) >> (spos & 7)) & ((1 << obtr) - 1));
if (off > 3) {
st.o[2] = st.o[1];
st.o[1] = st.o[0];
st.o[0] = off -= 3;
}
else {
var idx = off - (ll != 0);
if (idx) {
off = idx == 3 ? st.o[0] - 1 : st.o[idx];
if (idx > 1)
st.o[2] = st.o[1];
st.o[1] = st.o[0];
st.o[0] = off;
}
else
off = st.o[0];
}
for (var i = 0; i < ll; ++i) {
buf[oubt + i] = buf[spl + i];
}
oubt += ll, spl += ll;
var stin = oubt - off;
if (stin < 0) {
var len = -stin;
var bs = st.e + stin;
if (len > ml)
len = ml;
for (var i = 0; i < len; ++i) {
buf[oubt + i] = st.w[bs + i];
}
oubt += len, ml -= len, stin = 0;
}
for (var i = 0; i < ml; ++i) {
buf[oubt + i] = buf[stin + i];
}
oubt += ml;
}
if (oubt != spl) {
while (spl < buf.length) {
buf[oubt++] = buf[spl++];
}
}
else
oubt = buf.length;
if (out)
st.y += oubt;
else
buf = slc(buf, 0, oubt);
}
else if (out) {
st.y += lss;
if (spl) {
for (var i = 0; i < lss; ++i) {
buf[i] = buf[spl + i];
}
}
}
else if (spl)
buf = slc(buf, spl);
st.b = ebt;
return buf;
}
err(2);
};
// concat
var cct = function (bufs, ol) {
if (bufs.length == 1)
return bufs[0];
var buf = new u8(ol);
for (var i = 0, b = 0; i < bufs.length; ++i) {
var chk = bufs[i];
buf.set(chk, b);
b += chk.length;
}
return buf;
};
/**
* Decompresses Zstandard data
* @param dat The input data
* @param buf The output buffer. If unspecified, the function will allocate
* exactly enough memory to fit the decompressed data. If your
* data has multiple frames and you know the output size, specifying
* it will yield better performance.
* @returns The decompressed data
*/
export function decompress(dat, buf) {
var bufs = [], nb = +!buf;
var bt = 0, ol = 0;
for (; dat.length;) {
var st = rzfh(dat, nb || buf);
if (typeof st == 'object') {
if (nb) {
buf = null;
if (st.w.length == st.u) {
bufs.push(buf = st.w);
ol += st.u;
}
}
else {
bufs.push(buf);
st.e = 0;
}
for (; !st.l;) {
var blk = rzb(dat, st, buf);
if (!blk)
err(5);
if (buf)
st.e = st.y;
else {
bufs.push(blk);
ol += blk.length;
cpw(st.w, 0, blk.length);
st.w.set(blk, st.w.length - blk.length);
}
}
bt = st.b + (st.c * 4);
}
else
bt = st;
dat = dat.subarray(bt);
}
return cct(bufs, ol);
}
/**
* Decompressor for Zstandard streamed data
*/
var Decompress = /*#__PURE__*/ (function () {
/**
* Creates a Zstandard decompressor
* @param ondata The handler for stream data
*/
function Decompress(ondata) {
this.ondata = ondata;
this.c = [];
this.l = 0;
this.z = 0;
}
/**
* Pushes data to be decompressed
* @param chunk The chunk of data to push
* @param final Whether or not this is the last chunk in the stream
*/
Decompress.prototype.push = function (chunk, final) {
if (typeof this.s == 'number') {
var sub = Math.min(chunk.length, this.s);
chunk = chunk.subarray(sub);
this.s -= sub;
}
var sl = chunk.length;
var ncs = sl + this.l;
if (!this.s) {
if (final) {
if (!ncs) {
this.ondata(new u8(0), true);
return;
}
// min for frame + one block
if (ncs < 5)
err(5);
}
else if (ncs < 18) {
this.c.push(chunk);
this.l = ncs;
return;
}
if (this.l) {
this.c.push(chunk);
chunk = cct(this.c, ncs);
this.c = [];
this.l = 0;
}
if (typeof (this.s = rzfh(chunk)) == 'number')
return this.push(chunk, final);
}
if (typeof this.s != 'number') {
if (ncs < (this.z || 3)) {
if (final)
err(5);
this.c.push(chunk);
this.l = ncs;
return;
}
if (this.l) {
this.c.push(chunk);
chunk = cct(this.c, ncs);
this.c = [];
this.l = 0;
}
if (!this.z && ncs < (this.z = (chunk[this.s.b] & 2) ? 4 : 3 + ((chunk[this.s.b] >> 3) | (chunk[this.s.b + 1] << 5) | (chunk[this.s.b + 2] << 13)))) {
if (final)
err(5);
this.c.push(chunk);
this.l = ncs;
return;
}
else
this.z = 0;
for (;;) {
var blk = rzb(chunk, this.s);
if (!blk) {
if (final)
err(5);
var adc = chunk.subarray(this.s.b);
this.s.b = 0;
this.c.push(adc), this.l += adc.length;
return;
}
else {
this.ondata(blk, false);
cpw(this.s.w, 0, blk.length);
this.s.w.set(blk, this.s.w.length - blk.length);
}
if (this.s.l) {
var rest = chunk.subarray(this.s.b);
this.s = this.s.c * 4;
this.push(rest, final);
return;
}
}
}
else if (final)
err(5);
};
return Decompress;
}());
export { Decompress };
/**
* spz-v4-adapter.js
* SPZ v4 (NGSP / ZSTD) → SPZ v3 (gzip) 前端转换适配器
*
* 背景:
* SparkJS (spark.module.js) 内置的 SpzReader 只支持 gzip 单流的 SPZ v1~v3,
* 遇到后端新产出的 NGSP v4(明文 32 字节头 + TOC + 多路 ZSTD 流)会抛
* "Invalid gzip header"。
*
* 本适配器在浏览器端把 v4 文件解包、按 v3 布局重组并用 gzip 重新压缩,
* 再交给 SparkJS 原有解码路径,无需改动 spark.module.js。
*
* v4 文件布局(依据 nianticlabs/spz src/cc/load-spz.cc):
* Bytes 0–31 NgspFileHeader(明文,小端)
* Bytes 32..(tbo-1) 扩展区(flags & 0x2 时存在,本适配器跳过)
* Bytes tbo..(tbo+N*16-1) TOC:N × [compressedSize u64, uncompressedSize u64]
* 之后 N 路独立 ZSTD 流
* 流顺序固定为:positions, alphas, colors, scales, rotations, sh
* (长度为 0 的流会被跳过,例如 shDegree=0 时没有 sh 流)
*
* v3 负载布局:16 字节头 + 上述流按相同顺序直接拼接,整体 gzip。
*
* 依赖:fzstd(纯 JS zstd 解码器,本地文件 ./fzstd.esm.js)
* 浏览器要求:支持 CompressionStream("gzip")(Chrome/Edge 80+,Firefox 113+,Safari 16.4+)
*/
import { decompress as zstdDecompress } from "./fzstd.esm.js";
const NGSP_MAGIC = 0x5053474e; // "NGSP"
const V4_HEADER_SIZE = 32;
const TOC_ENTRY_SIZE = 16;
// 各 SH 阶数对应的每点系数向量数(vec3 数)
const SH_DIM_FOR_DEGREE = [0, 3, 8, 15, 24];
/** 是否为 gzip 文件(SPZ v1~v3) */
export function isGzipSpz(bytes) {
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
}
/** 是否为 NGSP v4 文件 */
export function isNgspV4(bytes) {
if (bytes.length < V4_HEADER_SIZE) return false;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
return view.getUint32(0, true) === NGSP_MAGIC && view.getUint32(4, true) >= 4;
}
/**
* 解析 v4 头部
* @returns {{numPoints:number, shDegree:number, fractionalBits:number, flags:number, numStreams:number, tocByteOffset:number}}
*/
export function parseNgspHeader(bytes) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
if (view.getUint32(0, true) !== NGSP_MAGIC) {
throw new Error("[spz-v4-adapter] 不是 NGSP 文件(magic 不匹配)");
}
const version = view.getUint32(4, true);
if (version < 4) {
throw new Error(`[spz-v4-adapter] NGSP 版本 ${version} 不是 v4,请走 gzip 路径`);
}
return {
version,
numPoints: view.getUint32(8, true),
shDegree: view.getUint8(12),
fractionalBits: view.getUint8(13),
flags: view.getUint8(14),
numStreams: view.getUint8(15),
tocByteOffset: view.getUint32(16, true),
};
}
/**
* 读取 TOC 并逐路 zstd 解压
* @returns {Uint8Array[]} 解压后的流,顺序为 positions/alphas/colors/scales/rotations/sh
*/
function decompressStreams(bytes, header) {
const { numStreams, tocByteOffset } = header;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
if (tocByteOffset < V4_HEADER_SIZE || tocByteOffset + numStreams * TOC_ENTRY_SIZE > bytes.length) {
throw new Error("[spz-v4-adapter] TOC 偏移越界,文件损坏");
}
// 每路流预期的解压后大小(与官方加载器 checkSizes 一致,用于校验)
const n = header.numPoints;
const shDim = SH_DIM_FOR_DEGREE[header.shDegree] ?? 0;
const expectedSizes = [n * 9, n * 1, n * 3, n * 3, n * 4, n * shDim * 3].filter((s) => s > 0);
if (expectedSizes.length !== numStreams) {
throw new Error(
`[spz-v4-adapter] 流数量不匹配:头部声明 ${numStreams} 路,按 shDegree=${header.shDegree} 预期 ${expectedSizes.length} 路`
);
}
const streams = new Array(numStreams);
let offset = tocByteOffset + numStreams * TOC_ENTRY_SIZE;
for (let i = 0; i < numStreams; i++) {
const e = tocByteOffset + i * TOC_ENTRY_SIZE;
// u64 小端;实际文件不会超 4GB,取低 32 位并校验高位
const compressedSize = view.getUint32(e, true) + view.getUint32(e + 4, true) * 4294967296;
const uncompressedSize = view.getUint32(e + 8, true) + view.getUint32(e + 12, true) * 4294967296;
if (uncompressedSize !== expectedSizes[i]) {
throw new Error(
`[spz-v4-adapter] 第 ${i} 路流大小不匹配:文件声明 ${uncompressedSize},预期 ${expectedSizes[i]}`
);
}
if (offset + compressedSize > bytes.length) {
throw new Error(`[spz-v4-adapter] 第 ${i} 路流超出文件末尾,文件损坏`);
}
streams[i] = zstdDecompress(bytes.subarray(offset, offset + compressedSize));
if (streams[i].length !== uncompressedSize) {
throw new Error(`[spz-v4-adapter] 第 ${i} 路流解压后大小异常`);
}
offset += compressedSize;
}
return streams;
}
/**
* SH degree 4 → 3 截断:v4 每点 sh 占 24*3=72 字节,v3 最多 15*3=45 字节,
* 取每点前 45 字节(低阶系数排前面),高阶光泽细节会丢失,但模型可正常显示。
*/
function truncateSh4To3(shStream, numPoints) {
const out = new Uint8Array(numPoints * 45);
for (let i = 0; i < numPoints; i++) {
out.set(shStream.subarray(i * 72, i * 72 + 45), i * 45);
}
return out;
}
/** 用浏览器原生 CompressionStream 做 gzip 压缩 */
async function gzipCompress(bytes) {
if (typeof CompressionStream === "undefined") {
throw new Error("[spz-v4-adapter] 当前浏览器不支持 CompressionStream,无法重打包 gzip");
}
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
/**
* 核心转换:NGSP v4 (zstd) 字节 → SPZ v3 (gzip) 字节
* @param {Uint8Array} bytes - v4 文件完整字节
* @returns {Promise<Uint8Array>} 可直接喂给 SparkJS SplatMesh(fileBytes) 的 gzip SPZ 字节
*/
export async function convertNgspV4ToGzipSpz(bytes) {
const header = parseNgspHeader(bytes);
const streams = decompressStreams(bytes, header);
let shDegree = header.shDegree;
// SparkJS 只支持 SH degree ≤ 3,degree 4 截断处理
if (shDegree === 4) {
streams[5] = truncateSh4To3(streams[5], header.numPoints);
shDegree = 3;
console.warn("[spz-v4-adapter] SH degree 4 已截断为 3(SparkJS 上限),会损失少量高光细节");
}
// 拼 v3 负载:16 字节头 + 各流顺序拼接
const totalSize = 16 + streams.reduce((acc, s) => acc + s.length, 0);
const payload = new Uint8Array(totalSize);
const view = new DataView(payload.buffer);
view.setUint32(0, NGSP_MAGIC, true);
view.setUint32(4, 3, true); // version = 3
view.setUint32(8, header.numPoints, true);
view.setUint8(12, shDegree);
view.setUint8(13, header.fractionalBits);
view.setUint8(14, header.flags & 0x1); // 只保留 antialiased 位,去掉扩展标志
view.setUint8(15, 0);
let offset = 16;
for (const s of streams) {
payload.set(s, offset);
offset += s.length;
}
return gzipCompress(payload);
}
/**
* 统一入口:给任意 SPZ 字节,返回 SparkJS 可读的 gzip SPZ 字节
* @param {Uint8Array} bytes
* @param {{onConvertStart?:Function, onConvertDone?:Function}} [hooks]
* @returns {Promise<Uint8Array>}
*/
export async function ensureSparkCompatibleSpz(bytes, hooks = {}) {
if (isGzipSpz(bytes)) return bytes; // v1~v3,原样返回
if (isNgspV4(bytes)) {
console.log("[spz-v4-adapter] 检测到 NGSP v4 (ZSTD),开始转换…");
hooks.onConvertStart?.();
const t0 = performance.now();
const out = await convertNgspV4ToGzipSpz(bytes);
console.log(
`[spz-v4-adapter] 转换完成:${(bytes.length / 1024).toFixed(0)}KB → ${(out.length / 1024).toFixed(0)}KB,` +
`耗时 ${(performance.now() - t0).toFixed(0)}ms`
);
hooks.onConvertDone?.(out);
return out;
}
throw new Error("[spz-v4-adapter] 无法识别的 SPZ 格式(既不是 gzip v1~v3,也不是 NGSP v4)");
}
File added
File added
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment