UNPKG

@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
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