"use strict"; // Loading game meshes into a GL3D.View: the .msh, its material script (Material/.txt, one // material per sub-mesh), the textures (decoded here, uploaded per view) and model configs. // Shared by the model viewer and the map editor; each keeps its own kit (one texture cache per // WebGL context). const MeshKit = (() => { let seq = 0; function create(view, getSrc, options = {}) { const tex = new Map(); // texture name → Promise<{ tex, img, w, h, source } | { error }> function texture(name) { const key = name.toLowerCase(); if (!tex.has(key)) { tex.set( key, (async () => { const src = getSrc(); if (!src) return { error: "no source" }; const t = await call("mdl:texture", src, name); if (!t) return { error: "not found" }; let img; if (t.ext === "dds") img = TexDecode.dds(t.bytes); else if (t.ext === "tga") img = TexDecode.tga(t.bytes); else img = await createImageBitmap(new Blob([t.bytes]), { premultiplyAlpha: "none" }); const prepared = options.prepareTexture ? options.prepareTexture({ img, ext: t.ext, name }) : img; return { tex: view.texture(prepared), img: prepared, w: prepared.width, h: prepared.height, source: t.source, ext: t.ext }; })().catch((err) => ({ error: err.message })), ); } return tex.get(key); } async function readNamed(name, folder, strict = false) { const src = getSrc(); if (!src) return null; return call("mdl:read", src, name, folder, strict); } /** * scene_blend " " (D3D style: add / subtract / rev_subtract / min / max with * zero, one, src_color, inv_src_color, src_alpha, inv_src_alpha, dest_alpha, inv_dest_alpha, * dest_color, inv_dest_color) → { op, src, dst }, or null for none / opaque. */ function parseBlend(s) { const t = (s || "").trim().toLowerCase().split(/\s+/).filter(Boolean); if (!t.length) return null; // ioMaterialSerializer::ParseSceneBlend delegates named modes to // ioPass::SetSceneBlending. Keep its factors, especially additive alpha. if (t.length === 1) { const named = { no_alpha: null, src_alpha: { op: "add", src: "src_alpha", dst: "inv_src_alpha" }, color_key: { op: "add", src: "src_alpha", dst: "inv_src_alpha" }, alpha_blend: { op: "add", src: "src_alpha", dst: "inv_src_alpha" }, add: { op: "add", src: "src_alpha", dst: "one" }, screen: { op: "add", src: "inv_dest_color", dst: "one" }, lighten: { op: "max", src: "one", dst: "one" }, darken: { op: "min", src: "one", dst: "one" }, rev_subtract: { op: "rev_subtract", src: "src_alpha", dst: "one" }, subtract: { op: "subtract", src: "src_alpha", dst: "one" }, }; if (Object.hasOwn(named, t[0])) return named[t[0]]; } if (t[0] === "modulate") return { op: "add", src: "dest_color", dst: "zero" }; const [op, src, dst] = t.length >= 3 ? t : ["add", ...t]; if (src === "one" && dst === "zero") return null; return { op, src: src.replace(/s$/, ""), dst: (dst || "zero").replace(/colors$/, "color") }; } /** Texture, UV set, lightmap and blending of a sub-mesh from its material. */ function groupLook(d, data) { const set = (n) => (n === 1 && data.uv1 ? 1 : 0); const blendFunc = parseBlend(d?.blend); return { uvSet: set(d?.uvSet), texName: d?.texture || null, lmName: d?.lightmap || null, lmSet: set(d?.lightmapSet), lmAddDiffuse: !!d?.lmAddDiffuse, diffuseColor: d?.diffuseColor || [1, 1, 1], blend: !!blendFunc, blendFunc, texAnim: d?.anim || null, lmAnim: d?.lightmapAnim || null, alphaCut: d?.alphaReject ? 0.5 : 0, tex: null, lmTex: null, texInfo: null, }; } /** Reads the material script, builds the draw groups and puts the mesh in the view. → m */ async function loadMesh(hit, name, place = {}) { const data = LSF.parseMsh(hit.bytes); const m = { id: ++seq, name, data, where: hit.where, source: hit.source, visible: true, dirty: false, mat: null, groups: [] }; const mh = await readNamed(`${name}.txt`, "material", true).catch(() => null); if (mh) { const text = LSF.decode(mh.bytes); m.mat = { text, raw: mh.bytes, where: mh.where, source: mh.source, mats: LSF.parseMaterial(text), dirty: false }; } const subs = data.subs.length ? data.subs : [{ indexStart: 0, faceCount: data.indices.length / 3 }]; m.groups = subs.map((sub, i) => { const start = Math.min(sub.indexStart, data.indices.length); return Object.assign({ start, count: Math.min(sub.faceCount * 3, data.indices.length - start) }, groupLook(LSF.diffuseOf(m.mat?.mats[i]), data)); }); if (data.boneNames?.length) { m.skinPos = new Float32Array(data.pos.length); m.skinNrm = data.normal ? new Float32Array(data.normal.length) : null; } view.addMesh(m.id, { pos: data.pos, normal: data.normal, uv0: data.uv0, uv1: data.uv1, indices: data.indices, groups: m.groups }, place); return m; } async function loadTextures(m) { await Promise.all( m.groups.map(async (g, gi) => { if (g.texName) { const t = await texture(g.texName); g.texInfo = t; if (t.tex) view.setGroupTexture(m.id, gi, t.tex); // anim_texture N: frames 1.dds … (N-1).dds after the base one const n = g.texAnim?.frames?.count || 0; if (n > 1) { const dot = g.texName.lastIndexOf("."); const stem = dot > 0 ? g.texName.slice(0, dot) : g.texName; const ext = dot > 0 ? g.texName.slice(dot) : ""; const frames = [t.tex || null]; for (let i = 1; i < n; i++) frames.push((await texture(`${stem}${i}${ext}`)).tex || null); g.frameTex = frames; } } if (g.lmName) { const t = await texture(g.lmName); if (t.tex) view.setGroupTexture(m.id, gi, t.tex, "lmTex"); } }), ); } /** Re-reads a mesh's draw groups after its material text changed. */ function applyMaterial(m) { m.groups.forEach((g, i) => { Object.assign(g, groupLook(LSF.diffuseOf(m.mat.mats[i]), m.data)); view.setGroupTexture(m.id, i, null); view.setGroupTexture(m.id, i, null, "lmTex"); }); return loadTextures(m); } /** A model config (resource/model/) by its path, e.g. "staticmodel/prisoner.txt". */ async function readModel(path) { const src = getSrc(); if (!src) return null; const hit = await call("mdl:readPath", src, `model/${path.replace(/\\/g, "/").replace(/^\/+/, "")}`); return hit ? { hit, cfg: LSF.parseModelCfg(LSF.decode(hit.bytes)) } : null; } /** Texture animations of a mesh at time t (s); t = null puts them back to rest. → true when something animates */ function animate(m, t) { let any = false; for (const g of m.groups) { if (g.texAnim) { any = true; if (t === null) { g.texM = null; g.colorMul = 1; g.emissive = 0; if (g.frameTex?.[0]) g.tex = g.frameTex[0]; } else { const r = LSF.texAnimAt(g.texAnim, t); g.texM = r.m; g.colorMul = r.colorMul; g.emissive = r.emissive; if (g.frameTex?.[r.frame]) g.tex = g.frameTex[r.frame]; } } if (g.lmAnim) { any = true; g.lmM = t === null ? null : LSF.texAnimAt(g.lmAnim, t).m; } } return any; } return { texture, readNamed, groupLook, loadMesh, loadTextures, applyMaterial, readModel, animate, clearTextures: () => tex.clear() }; } return { create }; })(); window.MeshKit = MeshKit;