@babylonjs/viewer
Version:
The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.
560 lines (555 loc) • 19.7 kB
JavaScript
import { j as U8, T as ThrowLiteError, i as TU, ad as acquireTexture, aJ as U8C, k as getOrCreateSampler, ai as U32, aj as resolveAccessor, ak as DV, F as F32 } from './index-By0tcgYN.esm.js';
let _table = null;
function getTable() {
if (_table) {
return _table;
}
const t = /* @__PURE__ */ new Map();
_table = t;
function add(gl, gpuFormat, feature, blockW, blockH, blockBytes) {
t.set(gl, { gpuFormat, feature, blockW, blockH, blockBytes });
}
const BC = "texture-compression-bc";
const ETC = "texture-compression-etc2";
const ASTC = "texture-compression-astc";
add(33776, "bc1-rgba-unorm", BC, 4, 4, 8);
add(33777, "bc1-rgba-unorm", BC, 4, 4, 8);
add(33778, "bc2-rgba-unorm", BC, 4, 4, 16);
add(33779, "bc3-rgba-unorm", BC, 4, 4, 16);
add(36283, "bc4-r-unorm", BC, 4, 4, 8);
add(36284, "bc4-r-snorm", BC, 4, 4, 8);
add(36285, "bc5-rg-unorm", BC, 4, 4, 16);
add(36286, "bc5-rg-snorm", BC, 4, 4, 16);
add(36495, "bc6h-rgb-ufloat", BC, 4, 4, 16);
add(36494, "bc6h-rgb-float", BC, 4, 4, 16);
add(36492, "bc7-rgba-unorm", BC, 4, 4, 16);
add(36493, "bc7-rgba-unorm-srgb", BC, 4, 4, 16);
add(37488, "eac-r11unorm", ETC, 4, 4, 8);
add(37489, "eac-r11snorm", ETC, 4, 4, 8);
add(37490, "eac-rg11unorm", ETC, 4, 4, 16);
add(37491, "eac-rg11snorm", ETC, 4, 4, 16);
add(37492, "etc2-rgb8unorm", ETC, 4, 4, 8);
add(37493, "etc2-rgb8unorm-srgb", ETC, 4, 4, 8);
add(37494, "etc2-rgb8a1unorm", ETC, 4, 4, 8);
add(37495, "etc2-rgb8a1unorm-srgb", ETC, 4, 4, 8);
add(37496, "etc2-rgba8unorm", ETC, 4, 4, 16);
add(37497, "etc2-rgba8unorm-srgb", ETC, 4, 4, 16);
const ASTC_BLOCKS = [
[4, 4],
[5, 4],
[5, 5],
[6, 5],
[6, 6],
[8, 5],
[8, 6],
[8, 8],
[10, 5],
[10, 6],
[10, 8],
[10, 10],
[12, 10],
[12, 12]
];
for (let i = 0; i < ASTC_BLOCKS.length; i++) {
const [w, h] = ASTC_BLOCKS[i];
const tag = `${w}x${h}`;
add(37808 + i, `astc-${tag}-unorm`, ASTC, w, h, 16);
add(37840 + i, `astc-${tag}-unorm-srgb`, ASTC, w, h, 16);
}
return t;
}
function getCompressedFormat(glInternalFormat) {
return getTable().get(glInternalFormat);
}
let _ktx2DecoderUrl = "https://cdn.babylonjs.com/babylon.ktx2Decoder.js";
let _ktx2DecoderPromise = null;
const GL_RGBA8 = 32856;
const GL_R8 = 33321;
const GL_RG8 = 33323;
const RGBA_CAPS = { astc: false, bptc: false, s3tc: false, pvrtc: false, etc2: false, etc1: false };
function deviceKtx2Caps(engine) {
const f = engine._device.features;
const bc = f.has("texture-compression-bc");
const etc2 = f.has("texture-compression-etc2");
return {
astc: f.has("texture-compression-astc"),
bptc: bc,
// BC6H/BC7
s3tc: bc,
// BC1/BC2/BC3
pvrtc: false,
// unsupported on WebGPU
etc2,
etc1: etc2
// ETC1 content transcodes on ETC2-capable GPUs
};
}
function loadKtx2Decoder() {
if (_ktx2DecoderPromise) {
return _ktx2DecoderPromise;
}
_ktx2DecoderPromise = new Promise((resolve, reject) => {
const w = globalThis;
const init = () => {
const mod = w.KTX2DECODER;
if (!mod) {
reject(new Error("KTX2: decoder global KTX2DECODER not found after script load"));
return;
}
mod.MSCTranscoder.UseFromWorkerThread = false;
mod.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
resolve(new mod.KTX2Decoder());
};
if (w.KTX2DECODER) {
init();
return;
}
const script = document.createElement("script");
script.src = _ktx2DecoderUrl;
script.async = true;
script.onload = init;
script.onerror = () => reject(new Error(`KTX2: failed to load ${script.src}`));
document.head.appendChild(script);
});
_ktx2DecoderPromise.catch(() => {
_ktx2DecoderPromise = null;
});
return _ktx2DecoderPromise;
}
function srgbFormat(format) {
switch (format) {
case "rgba8unorm":
return "rgba8unorm-srgb";
case "bc1-rgba-unorm":
return "bc1-rgba-unorm-srgb";
case "bc2-rgba-unorm":
return "bc2-rgba-unorm-srgb";
case "bc3-rgba-unorm":
return "bc3-rgba-unorm-srgb";
case "bc7-rgba-unorm":
return "bc7-rgba-unorm-srgb";
case "etc2-rgb8unorm":
return "etc2-rgb8unorm-srgb";
case "etc2-rgb8a1unorm":
return "etc2-rgb8a1unorm-srgb";
case "etc2-rgba8unorm":
return "etc2-rgba8unorm-srgb";
case "astc-4x4-unorm":
return "astc-4x4-unorm-srgb";
case "astc-5x4-unorm":
return "astc-5x4-unorm-srgb";
case "astc-5x5-unorm":
return "astc-5x5-unorm-srgb";
case "astc-6x5-unorm":
return "astc-6x5-unorm-srgb";
case "astc-6x6-unorm":
return "astc-6x6-unorm-srgb";
case "astc-8x5-unorm":
return "astc-8x5-unorm-srgb";
case "astc-8x6-unorm":
return "astc-8x6-unorm-srgb";
case "astc-8x8-unorm":
return "astc-8x8-unorm-srgb";
case "astc-10x5-unorm":
return "astc-10x5-unorm-srgb";
case "astc-10x6-unorm":
return "astc-10x6-unorm-srgb";
case "astc-10x8-unorm":
return "astc-10x8-unorm-srgb";
case "astc-10x10-unorm":
return "astc-10x10-unorm-srgb";
case "astc-12x10-unorm":
return "astc-12x10-unorm-srgb";
case "astc-12x12-unorm":
return "astc-12x12-unorm-srgb";
default:
return format;
}
}
function uncompressedInfo(glFormat) {
switch (glFormat) {
case GL_RGBA8:
return { format: "rgba8unorm", bytesPerPixel: 4 };
case GL_R8:
return { format: "r8unorm", bytesPerPixel: 1 };
case GL_RG8:
return { format: "rg8unorm", bytesPerPixel: 2 };
default:
return null;
}
}
function validateDecoded(decoded) {
if (decoded.errors) {
ThrowLiteError(404, decoded.errors);
}
if (!decoded.mipmaps.length) {
ThrowLiteError(405);
}
for (let i = 0; i < decoded.mipmaps.length; i++) {
if (!decoded.mipmaps[i]?.data) {
ThrowLiteError(406, i);
}
}
return decoded.mipmaps;
}
function makeSampler(engine, mipCount) {
return getOrCreateSampler(engine, {
addressModeU: "repeat",
addressModeV: "repeat",
minFilter: "linear",
magFilter: "linear",
mipmapFilter: mipCount > 1 ? "linear" : "nearest",
maxAnisotropy: mipCount > 1 ? 4 : 1
});
}
function uploadCompressed(engine, mips, format, sRGB) {
if (!engine._device.features.has(format.feature)) {
ThrowLiteError(407, format.feature);
}
const width = mips[0].width;
const height = mips[0].height;
const texture = engine._device.createTexture({
size: { width, height },
format: sRGB ? srgbFormat(format.gpuFormat) : format.gpuFormat,
mipLevelCount: mips.length,
usage: TU.TEXTURE_BINDING | TU.COPY_DST
});
for (let level = 0; level < mips.length; level++) {
const mip = mips[level];
const blocksPerRow = Math.ceil(mip.width / format.blockW);
const rowBytes = blocksPerRow * format.blockBytes;
const copyW = blocksPerRow * format.blockW;
const copyH = Math.ceil(mip.height / format.blockH) * format.blockH;
engine._device.queue.writeTexture({ texture, mipLevel: level }, mip.data, { bytesPerRow: rowBytes }, { width: copyW, height: copyH });
}
const tex2d = { texture, view: texture.createView(), sampler: makeSampler(engine, mips.length), width, height, invertY: true };
acquireTexture(tex2d);
return tex2d;
}
function uploadUncompressed(engine, mips, info, sRGB) {
const width = mips[0].width;
const height = mips[0].height;
const texture = engine._device.createTexture({
size: { width, height },
format: sRGB ? srgbFormat(info.format) : info.format,
mipLevelCount: mips.length,
usage: TU.TEXTURE_BINDING | TU.COPY_DST
});
for (let level = 0; level < mips.length; level++) {
const mip = mips[level];
const expected = mip.width * mip.height * info.bytesPerPixel;
if (mip.data.length !== expected) {
ThrowLiteError(408, level, mip.data.length, expected);
}
engine._device.queue.writeTexture(
{ texture, mipLevel: level },
mip.data,
{ bytesPerRow: mip.width * info.bytesPerPixel },
{ width: mip.width, height: mip.height }
);
}
const tex2d = { texture, view: texture.createView(), sampler: makeSampler(engine, mips.length), width, height, invertY: true };
acquireTexture(tex2d);
return tex2d;
}
async function uploadKtx2Texture2D(engine, buffer, sRGB) {
const decoder = await loadKtx2Decoder();
const decoded = await decoder.decode(new U8(buffer), deviceKtx2Caps(engine));
const mips = validateDecoded(decoded);
const compressed = getCompressedFormat(decoded.transcodedFormat);
if (compressed) {
return uploadCompressed(engine, mips, compressed, sRGB);
}
const uncompressed = uncompressedInfo(decoded.transcodedFormat);
if (uncompressed) {
return uploadUncompressed(engine, mips, uncompressed, sRGB);
}
ThrowLiteError(409, decoded.transcodedFormat.toString(16));
}
async function decodeKtx2ImageBitmapFromBuffer(buffer) {
const decoder = await loadKtx2Decoder();
const decoded = await decoder.decode(new U8(buffer), RGBA_CAPS, { forceRGBA: true });
const mip0 = validateDecoded(decoded)[0];
if (mip0.data.length !== mip0.width * mip0.height * 4) {
ThrowLiteError(411);
}
const pixels = new U8C(mip0.data.length);
pixels.set(mip0.data);
return createImageBitmap(new ImageData(pixels, mip0.width, mip0.height));
}
const NAME = "KHR_texture_basisu";
const FLOAT = 5126;
const COMPONENT_BYTES = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
const TYPE_SIZES = {
SCALAR: 1,
VEC2: 2,
VEC3: 3,
VEC4: 4,
MAT2: 4,
MAT3: 9,
MAT4: 16
};
const BASISU_MATERIAL_DATA = "__basisuMaterialData";
function basisSourceIndex(tex) {
const source = tex?.extensions?.KHR_texture_basisu?.source;
return typeof source === "number" ? source : null;
}
function textureIndex(texInfo) {
const index = texInfo?.index;
return typeof index === "number" ? index : null;
}
function textureUsesBasisu(json, texInfo) {
const index = textureIndex(texInfo);
return index !== null && basisSourceIndex(json.textures?.[index]) !== null;
}
function stripBasisuTexture(json, owner, slot, data) {
if (!textureUsesBasisu(json, owner?.[slot])) {
return false;
}
data[slot] = owner[slot];
delete owner[slot];
return true;
}
function prepareBasisuMaterials(json, binChunk, baseUrl) {
for (const mat of json.materials ?? []) {
const data = { json, binChunk, baseUrl };
const pbr = mat.pbrMetallicRoughness ?? {};
let hasBasisu = stripBasisuTexture(json, pbr, "baseColorTexture", data);
hasBasisu = stripBasisuTexture(json, pbr, "metallicRoughnessTexture", data) || hasBasisu;
hasBasisu = stripBasisuTexture(json, mat, "normalTexture", data) || hasBasisu;
hasBasisu = stripBasisuTexture(json, mat, "occlusionTexture", data) || hasBasisu;
hasBasisu = stripBasisuTexture(json, mat, "emissiveTexture", data) || hasBasisu;
const spec = mat.extensions?.KHR_materials_specular;
if (spec) {
hasBasisu = stripBasisuTexture(json, spec, "specularTexture", data) || hasBasisu;
hasBasisu = stripBasisuTexture(json, spec, "specularColorTexture", data) || hasBasisu;
}
if (hasBasisu) {
Object.defineProperty(mat, BASISU_MATERIAL_DATA, { value: data });
}
}
}
async function resolveImageBuffer(ctx, imageIdx) {
const image = ctx.json.images?.[imageIdx];
if (!image) {
ThrowLiteError(78, NAME, imageIdx);
}
if (image.bufferView !== void 0) {
const bv = ctx.json.bufferViews?.[image.bufferView];
if (!bv) {
ThrowLiteError(79, NAME, image.bufferView);
}
const offset = ctx.binChunk.byteOffset + (bv.byteOffset ?? 0);
const copy = new U8(bv.byteLength);
copy.set(new U8(ctx.binChunk.buffer, offset, bv.byteLength));
return copy.buffer;
}
if (image.uri) {
const url = new URL(image.uri, ctx.baseUrl + "x").href;
const response = await fetch(url);
if (!response.ok) {
ThrowLiteError(80, NAME, response.status, response.statusText);
}
return response.arrayBuffer();
}
ThrowLiteError(81, NAME);
}
async function loadBasisuBitmap(data, texInfo) {
const index = textureIndex(texInfo);
if (index === null) {
return null;
}
const source = basisSourceIndex(data.json.textures?.[index]);
if (source === null) {
return null;
}
data.bitmaps ??= /* @__PURE__ */ new Map();
let bitmap = data.bitmaps.get(index);
if (!bitmap) {
bitmap = resolveImageBuffer(data, source).then(decodeKtx2ImageBitmapFromBuffer);
data.bitmaps.set(index, bitmap);
}
return bitmap;
}
async function uploadBasisuTexture(data, ctx, texInfo, sRGB) {
const index = textureIndex(texInfo);
if (index === null) {
return void 0;
}
data.textures ??= /* @__PURE__ */ new Map();
const key = `${index}:${sRGB ? 1 : 0}`;
let tex = data.textures.get(key);
if (!tex) {
const source = basisSourceIndex(data.json.textures?.[index]);
if (source === null) {
return void 0;
}
tex = await uploadKtx2Texture2D(ctx._engine, await resolveImageBuffer(data, source), sRGB);
data.textures.set(key, tex);
}
return tex;
}
async function compositeOrm(mr, occ) {
const w = mr.width;
const h = mr.height;
const c1 = new OffscreenCanvas(w, h);
const x1 = c1.getContext("2d");
x1.drawImage(mr, 0, 0, w, h);
const d1 = x1.getImageData(0, 0, w, h);
const c2 = new OffscreenCanvas(w, h);
const x2 = c2.getContext("2d");
x2.drawImage(occ, 0, 0, w, h);
const d2 = x2.getImageData(0, 0, w, h);
for (let j = 0; j < d1.data.length; j += 4) {
d1.data[j] = d2.data[j];
}
x1.putImageData(d1, 0, 0);
return createImageBitmap(c1);
}
async function uploadOrmTexture(data, ctx) {
const mrInfo = data.metallicRoughnessTexture;
const occInfo = data.occlusionTexture;
const mrIndex = textureIndex(mrInfo);
const occIndex = textureIndex(occInfo);
if (mrIndex === null && occIndex === null) {
return void 0;
}
if (mrIndex === null || occIndex === null || mrIndex === occIndex) {
return uploadBasisuTexture(data, ctx, mrInfo ?? occInfo, false);
}
data.textures ??= /* @__PURE__ */ new Map();
const key = `orm:${mrIndex}:${occIndex}`;
let tex = data.textures.get(key);
if (!tex) {
const [mr, occ] = await Promise.all([loadBasisuBitmap(data, mrInfo), loadBasisuBitmap(data, occInfo)]);
if (!mr || !occ) {
return void 0;
}
tex = ctx._uploadImage(await compositeOrm(mr, occ), false);
data.textures.set(key, tex);
}
return tex;
}
function readComponent(view, offset, componentType, normalized) {
switch (componentType) {
case FLOAT:
return view.getFloat32(offset, true);
case 5125: {
const v = view.getUint32(offset, true);
return normalized ? v / 4294967295 : v;
}
case 5123: {
const v = view.getUint16(offset, true);
return normalized ? v / 65535 : v;
}
case 5122: {
const v = view.getInt16(offset, true);
return normalized ? Math.max(v / 32767, -1) : v;
}
case 5121: {
const v = view.getUint8(offset);
return normalized ? v / 255 : v;
}
case 5120: {
const v = view.getInt8(offset);
return normalized ? Math.max(v / 127, -1) : v;
}
default:
ThrowLiteError(82, NAME, componentType);
}
}
function readStridedFloat(json, binChunk, accessorIdx) {
const accessor = json.accessors[accessorIdx];
const bufferView = json.bufferViews[accessor.bufferView];
const componentType = accessor.componentType;
const compBytes = COMPONENT_BYTES[componentType];
if (!compBytes) {
ThrowLiteError(83, NAME, accessorIdx, componentType);
}
const componentCount = TYPE_SIZES[accessor.type] ?? 1;
const elementBytes = componentCount * compBytes;
const byteStride = bufferView.byteStride ?? elementBytes;
if (byteStride < elementBytes) {
ThrowLiteError(84, NAME, byteStride, accessorIdx);
}
const normalized = accessor.normalized === true;
const baseOffset = binChunk.byteOffset + (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
const view = new DV(binChunk.buffer);
const out = new F32(accessor.count * componentCount);
for (let i = 0, o = 0; i < accessor.count; i++) {
const src = baseOffset + i * byteStride;
for (let c = 0; c < componentCount; c++, o++) {
out[o] = readComponent(view, src + c * compBytes, componentType, normalized);
}
}
return out;
}
const ext = {
id: NAME,
async preMesh(json, binChunk, baseUrl) {
const gltf = json;
prepareBasisuMaterials(gltf, binChunk, baseUrl);
const decoded = /* @__PURE__ */ new Map();
for (const mesh of gltf.meshes ?? []) {
for (const primitive of mesh.primitives ?? []) {
const attrs = primitive.attributes ?? {};
const strided = Object.keys(attrs).some((name) => gltf.bufferViews?.[gltf.accessors?.[attrs[name]]?.bufferView]?.byteStride !== void 0);
if (!strided) {
continue;
}
const attributes = /* @__PURE__ */ new Map();
for (const name of Object.keys(attrs)) {
const accessorIdx = attrs[name];
const accessor = gltf.accessors[accessorIdx];
if (gltf.bufferViews?.[accessor.bufferView]?.byteStride !== void 0) {
attributes.set(name, readStridedFloat(gltf, binChunk, accessorIdx));
}
}
const posAcc = gltf.accessors[attrs.POSITION];
const idx = primitive.indices === void 0 ? new U32(0) : new U32(resolveAccessor(gltf, binChunk, primitive.indices)._data);
decoded.set(primitive, {
_attributes: attributes,
_indices: idx,
_vertexCount: posAcc.count,
_indexCount: idx.length
});
}
}
return decoded;
},
async applyMaterial(mat, ctx) {
const data = mat._rawMatDef?.[BASISU_MATERIAL_DATA];
if (!data) {
return null;
}
const [baseColorTexture, ormTexture, normalTexture, emissiveTexture, specularTexture, specularColorTexture] = await Promise.all([
uploadBasisuTexture(data, ctx, data.baseColorTexture, true),
uploadOrmTexture(data, ctx),
uploadBasisuTexture(data, ctx, data.normalTexture, false),
uploadBasisuTexture(data, ctx, data.emissiveTexture, true),
uploadBasisuTexture(data, ctx, data.specularTexture, false),
// specularColorTexture → reflectanceTexture: load LINEAR so the GPU does not
// sRGB-decode on sample; the reflectance shader applies its own pow(2.2)
// (BJS toLinearSpace parity). sRGB-format here would gamma-decode twice.
uploadBasisuTexture(data, ctx, data.specularColorTexture, false)
]);
const out = {
...baseColorTexture ? { baseColorTexture } : void 0,
...ormTexture ? {
ormTexture,
...data.metallicRoughnessTexture ? { metallicFactor: mat._metallicFactor, roughnessFactor: mat._roughnessFactor } : void 0,
...data.occlusionTexture ? { occlusionStrength: 1, occlusionTexCoord: data.occlusionTexture.texCoord ?? 0 } : void 0
} : void 0,
...normalTexture ? { normalTexture, normalTextureScale: data.normalTexture?.scale ?? 1 } : void 0,
...emissiveTexture ? { emissiveTexture } : void 0,
...specularTexture ? { metallicReflectanceTexture: specularTexture, useOnlyMetallicFromMetallicReflectanceTexture: true } : void 0,
...specularColorTexture ? { reflectanceTexture: specularColorTexture } : void 0
};
if (!out.baseColorTexture && !out.ormTexture && !out.normalTexture && !out.emissiveTexture && !out.metallicReflectanceTexture && !out.reflectanceTexture) {
return null;
}
return out;
}
};
export { ext as default };
//# sourceMappingURL=gltf-ext-basisu-DHhPqw5C.esm.js.map