Files

194 lines
8.0 KiB
JavaScript

"use strict";
// Loading game meshes into a GL3D.View: the .msh, its material script (Material/<mesh>.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 "<op> <src> <dst>" (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 <name>1.dds … <name>(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/<path>) 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;