411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
|
//
|
||
|
// nsbmd.js
|
||
|
//--------------------
|
||
|
// Reads NSBMD models and any texture data within them.
|
||
|
// by RHY3756547
|
||
|
//
|
||
|
// includes: gl-matrix.js (glMatrix 2.0)
|
||
|
// /formats/nitro.js
|
||
|
// /formats/nsbtx.js
|
||
|
//
|
||
|
|
||
|
window.nsbmd = function(input) {
|
||
|
|
||
|
var mainOff, modelData, texPalOff, materials;
|
||
|
var mainObj = this;
|
||
|
|
||
|
if (input != null) {
|
||
|
load(input);
|
||
|
}
|
||
|
this.load = load;
|
||
|
|
||
|
|
||
|
function load(input) {
|
||
|
mainObj.hasBillboards = false;
|
||
|
var view = new DataView(input);
|
||
|
var header = null;
|
||
|
var offset = 0;
|
||
|
var tex;
|
||
|
|
||
|
//nitro 3d header
|
||
|
header = nitro.readHeader(view);
|
||
|
if (header.stamp != "BMD0") throw "NSBMD invalid. Expected BMD0, found "+header.stamp;
|
||
|
if (header.numSections > 2) throw "NSBMD invalid. Too many sections - should have 2 maximum.";
|
||
|
if (header.numSections == 2) tex = new nsbtx(input.slice(header.sectionOffsets[1]), true, true);
|
||
|
offset = header.sectionOffsets[0];
|
||
|
//end nitro
|
||
|
|
||
|
mainOff = offset;
|
||
|
|
||
|
var stamp = readChar(view, offset+0x0)+readChar(view, offset+0x1)+readChar(view, offset+0x2)+readChar(view, offset+0x3);
|
||
|
if (stamp != "MDL0") throw "NSBMD invalid. Expected MDL0, found "+stamp;
|
||
|
|
||
|
mainObj.tex = tex;
|
||
|
|
||
|
modelData = nitro.read3dInfo(view, mainOff+8, modelInfoHandler);
|
||
|
mainObj.modelData = modelData;
|
||
|
}
|
||
|
|
||
|
function modelInfoHandler(view, offset) {
|
||
|
var mdlOff = view.getUint32(offset, true);
|
||
|
|
||
|
var off = mainOff+mdlOff;
|
||
|
var obj = readModelData(view, off);
|
||
|
obj.nextoff = offset+4;
|
||
|
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
function readModelData(view, offset) {
|
||
|
var head = {}
|
||
|
head.blockSize = view.getUint32(offset, true);
|
||
|
head.bonesOffset = offset+view.getUint32(offset+4, true);
|
||
|
head.materialsOffset = offset+view.getUint32(offset+8, true);
|
||
|
head.polyStartOffset = offset+view.getUint32(offset+0xC, true);
|
||
|
head.polyEndOffset = offset+view.getUint32(offset+0x10, true);
|
||
|
|
||
|
head.numObjects = view.getUint8(offset+0x17);
|
||
|
head.numMaterials = view.getUint8(offset+0x18);
|
||
|
head.numPolys = view.getUint8(offset+0x19);
|
||
|
head.maxStack = view.getUint8(offset+0x1A);
|
||
|
|
||
|
head.scale = view.getInt32(offset+0x1C, true)/4096;
|
||
|
|
||
|
head.numVerts = view.getUint16(offset+0x24, true);
|
||
|
head.numSurfaces = view.getUint16(offset+0x26, true);
|
||
|
head.numTriangles = view.getUint16(offset+0x28, true);
|
||
|
head.numQuads = view.getUint16(offset+0x2A, true);
|
||
|
|
||
|
head.bboxX = view.getInt16(offset+0x2C, true)/4096;
|
||
|
head.bboxY = view.getInt16(offset+0x2E, true)/4096;
|
||
|
head.bboxZ = view.getInt16(offset+0x30, true)/4096;
|
||
|
head.bboxWidth = view.getInt16(offset+0x32, true)/4096;
|
||
|
head.bboxHeight = view.getInt16(offset+0x34, true)/4096;
|
||
|
head.bboxDepth = view.getInt16(offset+0x36, true)/4096;
|
||
|
//head.runtimeData = view.getUint64(offset+0x38, true);
|
||
|
texPalOff = head.materialsOffset; //leak into local scope so it can be used by tex and pal bindings
|
||
|
|
||
|
var objects = nitro.read3dInfo(view, offset+0x40, objInfoHandler);
|
||
|
var polys = nitro.read3dInfo(view, head.polyStartOffset, polyInfoHandler);
|
||
|
|
||
|
materials = nitro.read3dInfo(view, head.materialsOffset+4, matInfoHandler);
|
||
|
|
||
|
var tex = nitro.read3dInfo(view, head.materialsOffset+view.getUint16(head.materialsOffset, true), texInfoHandler);
|
||
|
var palt = nitro.read3dInfo(view, head.materialsOffset+view.getUint16(head.materialsOffset+2, true), palInfoHandler);
|
||
|
|
||
|
var commands = parseBones(head.bonesOffset, view, polys, materials, objects, head.maxStack);
|
||
|
|
||
|
return {head: head, objects: objects, polys: polys, materials: materials, tex:tex, palt:palt, commands:commands}
|
||
|
}
|
||
|
|
||
|
function parseBones(offset, view, polys, materials, objects, maxStack) {
|
||
|
var last;
|
||
|
var commands = [];
|
||
|
|
||
|
var freeStack = maxStack;
|
||
|
var forceID=null;
|
||
|
var lastMat = null;
|
||
|
|
||
|
while (offset<texPalOff) { //bones
|
||
|
last = view.getUint8(offset++);
|
||
|
console.log("bone cmd: 0x"+last.toString(16));
|
||
|
switch (last) {
|
||
|
case 0x06: //bind object transforms to parent. bone exists but is not placed in the stack
|
||
|
var obj = view.getUint8(offset++);
|
||
|
var parent = view.getUint8(offset++);
|
||
|
var zero = view.getUint8(offset++);
|
||
|
|
||
|
var object = objects.objectData[obj];
|
||
|
object.parent = parent;
|
||
|
|
||
|
commands.push({obj:obj, parent:parent, stackID:freeStack++});
|
||
|
break;
|
||
|
case 0x26:
|
||
|
case 0x46: //placed in the stack at stack id
|
||
|
var obj = view.getUint8(offset++);
|
||
|
var parent = view.getUint8(offset++);
|
||
|
var zero = view.getUint8(offset++);
|
||
|
var stackID = view.getUint8(offset++);
|
||
|
|
||
|
var object = objects.objectData[obj];
|
||
|
object.parent = parent;
|
||
|
|
||
|
commands.push({obj:obj, parent:parent, stackID:((last == 0x26)?stackID:freeStack++), restoreID:((last == 0x46)?stackID:null)});
|
||
|
break;
|
||
|
case 0x66: //has ability to restore to another stack id. no idea how this works
|
||
|
var obj = view.getUint8(offset++);
|
||
|
var parent = view.getUint8(offset++);
|
||
|
var zero = view.getUint8(offset++);
|
||
|
var stackID = view.getUint8(offset++);
|
||
|
var restoreID = view.getUint8(offset++);
|
||
|
|
||
|
var object = objects.objectData[obj];
|
||
|
object.parent = parent;
|
||
|
|
||
|
commands.push({obj:obj, parent:parent, stackID:stackID, restoreID:restoreID});
|
||
|
break;
|
||
|
case 0x04:
|
||
|
case 0x24:
|
||
|
case 0x44: //bind material to polygon: matID, 5, polyID
|
||
|
var mat = view.getUint8(offset++);
|
||
|
lastMat = mat;
|
||
|
var five = view.getUint8(offset++); //???
|
||
|
var poly = view.getUint8(offset++);
|
||
|
polys.objectData[poly].stackID = (forceID == null)?(commands[commands.length-1].stackID):forceID;
|
||
|
polys.objectData[poly].mat = mat;
|
||
|
break;
|
||
|
case 1:
|
||
|
break;
|
||
|
case 2: //node visibility (maybe to implement this set matrix to 0)
|
||
|
|
||
|
var node = view.getUint8(offset++);
|
||
|
var vis = view.getUint8(offset++);
|
||
|
objects.objectData[node].vis = vis;
|
||
|
console.log("visibility thing "+node);
|
||
|
if (node > 10) debugger;
|
||
|
break;
|
||
|
case 3: //stack id for poly (wit)
|
||
|
forceID = view.getUint8(offset++);
|
||
|
console.log("stackid is "+forceID);
|
||
|
case 0:
|
||
|
break;
|
||
|
case 5:
|
||
|
//i don't... what??
|
||
|
//holy shp!
|
||
|
var poly = view.getUint8(offset++);
|
||
|
polys.objectData[poly].stackID = (stackID == null)?(commands[commands.length-1].forceID):forceID;
|
||
|
polys.objectData[poly].mat = lastMat;
|
||
|
|
||
|
break;
|
||
|
case 7:
|
||
|
//sets object to be billboard
|
||
|
var obj = view.getUint8(offset++);
|
||
|
objects.objectData[obj].billboardMode = 1;
|
||
|
mainObj.hasBillboards = true;
|
||
|
break;
|
||
|
case 8:
|
||
|
//sets object to be billboard around only y axis. (xz remain unchanged)
|
||
|
var obj = view.getUint8(offset++);
|
||
|
objects.objectData[obj].billboardMode = 2;
|
||
|
mainObj.hasBillboards = true;
|
||
|
break;
|
||
|
case 0x0b:
|
||
|
break; //begin, not quite sure what of. doesn't seem to change anything
|
||
|
case 0x2b:
|
||
|
break; //end
|
||
|
default:
|
||
|
console.log("bone transform unknown: "+last);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
//if (window.throwWhatever) debugger;
|
||
|
return commands;
|
||
|
}
|
||
|
|
||
|
function matInfoHandler(view, off, base) {
|
||
|
var offset = texPalOff + view.getUint32(off, true);
|
||
|
|
||
|
var rel = 0;
|
||
|
/*while (rel < 40) {
|
||
|
var flags = view.getUint16(offset+rel, true);
|
||
|
if ((flags&15)==15) console.log("rel at "+rel);
|
||
|
rel += 2;
|
||
|
}*/
|
||
|
|
||
|
var polyAttrib = view.getUint16(offset+12, true);
|
||
|
console.log(polyAttrib);
|
||
|
|
||
|
var flags = view.getUint16(offset+22, true); //other info in here is specular data etc.
|
||
|
|
||
|
//scale starts at 44;
|
||
|
|
||
|
var mat;
|
||
|
offset += 44;
|
||
|
switch ((flags>>14) & 0x03) { //texture scaling mode
|
||
|
case 0:
|
||
|
mat = mat3.create(); //no scale
|
||
|
break;
|
||
|
case 1:
|
||
|
mat = mat3.create();
|
||
|
mat3.scale(mat, mat, [view.getInt32(offset, true)/4096, view.getInt32(offset+4, true)/4096]);
|
||
|
//mat3.translate(mat, mat, [-anim.translateS[(texFrame>>anim.frameStep.translateS)%anim.translateS.length], anim.translateT[(texFrame>>anim.frameStep.translateT)%anim.translateT.length]]) //for some mystery reason I need to negate the S translation
|
||
|
|
||
|
break;
|
||
|
case 2:
|
||
|
case 3:
|
||
|
mat = mat3.create(); //custom tex mat
|
||
|
alert("custom");
|
||
|
for (var i=0; i<16; i++) {
|
||
|
mat[i] = view.getInt32(offset, true)/4096;
|
||
|
offset += 4;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var cullMode = ((polyAttrib>>6)&3);
|
||
|
|
||
|
var alpha = ((polyAttrib>>16)&31)/31;
|
||
|
if (alpha == 0) alpha = 1;
|
||
|
|
||
|
return {
|
||
|
height: 8 << ((flags>>7)&7),
|
||
|
width: 8 << ((flags>>4)&7),
|
||
|
repeatX: flags&1,
|
||
|
repeatY: (flags>>1)&1,
|
||
|
flipX: (flags>>2)&1,
|
||
|
flipY: (flags>>3)&1,
|
||
|
texMat: mat,
|
||
|
alpha: alpha,
|
||
|
cullMode: cullMode,
|
||
|
nextoff: off + 4
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function texInfoHandler(view, off, base, ind) {
|
||
|
var oDat = texPalOff+view.getUint16(off, true); //contains offset to array of materials to bind to
|
||
|
var num = view.getUint8(off+2, true);
|
||
|
var mats = [];
|
||
|
for (var i=0; i<num; i++) {
|
||
|
var mat = view.getUint8(oDat++);
|
||
|
materials.objectData[mat].tex = ind; //bind to this material
|
||
|
mats.push(mat);
|
||
|
}
|
||
|
return {
|
||
|
mats: mats,
|
||
|
nextoff: off + 4
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function palInfoHandler(view, off, base, ind) {
|
||
|
var oDat = texPalOff+view.getUint16(off, true); //contains offset to array of materials to bind to
|
||
|
var num = view.getUint8(off+2, true);
|
||
|
var mats = [];
|
||
|
for (var i=0; i<num; i++) {
|
||
|
var mat = view.getUint8(oDat++);
|
||
|
materials.objectData[mat].pal = ind; //bind to this material
|
||
|
mats.push(mat);
|
||
|
}
|
||
|
return {
|
||
|
mats: mats,
|
||
|
nextoff: off + 4
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function polyInfoHandler(view, off, base) {
|
||
|
var offset = base + view.getUint32(off, true);
|
||
|
var dlStart = offset+view.getUint32(offset+8, true);
|
||
|
var displayList = view.buffer.slice(dlStart, dlStart+view.getUint32(offset+0xC, true))
|
||
|
return {
|
||
|
nextoff: off + 4,
|
||
|
disp: displayList
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function objInfoHandler(view, off, base) {
|
||
|
var offset = base + view.getUint32(off, true);
|
||
|
|
||
|
var flag = view.getUint16(offset, true); //flag format nnnn psrt
|
||
|
var rotTerm1 = view.getInt16(offset+0x2, true)/4096; //first term of rotate mat if present
|
||
|
var translate = vec3.create();
|
||
|
if (!(flag&1)) { //translate (t) flag is 0
|
||
|
translate[0] = view.getInt32(offset+0x4, true)/4096;
|
||
|
translate[1] = view.getInt32(offset+0x8, true)/4096;
|
||
|
translate[2] = view.getInt32(offset+0xC, true)/4096;
|
||
|
offset += 0xC;
|
||
|
}
|
||
|
var pivot;
|
||
|
var A, B, neg, mode;
|
||
|
if (flag&8) { //pivot exists
|
||
|
pivot = new Float32Array([0,0,0,0,0,0,0,0,0]);
|
||
|
mode = (flag>>4)&15;
|
||
|
neg = (flag>>8)&15;
|
||
|
A = view.getInt16(offset+0x4, true)/4096;
|
||
|
B = view.getInt16(offset+0x6, true)/4096;
|
||
|
|
||
|
pivot[mode] = (neg&1)?-1:1;
|
||
|
var horiz = mode%3;
|
||
|
var vert = Math.floor(mode/3)
|
||
|
var left = (horiz==0)?1:0; var top = ((vert==0)?1:0)*3;
|
||
|
var right = (horiz==2)?1:2; var btm = ((vert==2)?1:2)*3;
|
||
|
pivot[left+top] = A;
|
||
|
pivot[right+top] = B;
|
||
|
pivot[left+btm] = (neg&2)?-B:B;
|
||
|
pivot[right+btm] = (neg&4)?-A:A;
|
||
|
|
||
|
offset += 4;
|
||
|
} else {
|
||
|
pivot = mat3.create()
|
||
|
}
|
||
|
var scale = vec3.create();
|
||
|
if (!(flag&4)) {
|
||
|
scale[0] = view.getInt32(offset+0x4, true)/4096;
|
||
|
scale[1] = view.getInt32(offset+0x8, true)/4096;
|
||
|
scale[2] = view.getInt32(offset+0xC, true)/4096;
|
||
|
offset += 0xC;
|
||
|
} else {
|
||
|
scale[0] = 1;
|
||
|
scale[1] = 1;
|
||
|
scale[2] = 1;
|
||
|
}
|
||
|
if ((!(flag&8)) && (!(flag&2))) { //rotate matrix, replaces pivot
|
||
|
pivot[0] = rotTerm1;
|
||
|
pivot[1] = view.getInt16(offset+0x4, true)/4096;
|
||
|
pivot[2] = view.getInt16(offset+0x6, true)/4096;
|
||
|
pivot[3] = view.getInt16(offset+0x8, true)/4096;
|
||
|
pivot[4] = view.getInt16(offset+0xA, true)/4096;
|
||
|
pivot[5] = view.getInt16(offset+0xC, true)/4096;
|
||
|
pivot[6] = view.getInt16(offset+0xE, true)/4096;
|
||
|
pivot[7] = view.getInt16(offset+0x10, true)/4096;
|
||
|
pivot[8] = view.getInt16(offset+0x12, true)/4096;
|
||
|
offset += 16;
|
||
|
}
|
||
|
var mat = mat4.create();
|
||
|
mat4.translate(mat, mat, translate);
|
||
|
mat4.multiply(mat, mat, mat4FromMat3(pivot));
|
||
|
mat4.scale(mat, mat, scale);
|
||
|
return {
|
||
|
translate: translate,
|
||
|
pivot: pivot,
|
||
|
|
||
|
pA: A,
|
||
|
pB: B,
|
||
|
pMode: mode,
|
||
|
pNeg: neg,
|
||
|
|
||
|
scale: scale,
|
||
|
flag: flag,
|
||
|
mat: mat,
|
||
|
billboardMode: 0,
|
||
|
nextoff: off + 4
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function mat4FromMat3(mat) {
|
||
|
dest = mat4.create();
|
||
|
|
||
|
dest[0] = mat[0];
|
||
|
dest[1] = mat[1];
|
||
|
dest[2] = mat[2];
|
||
|
dest[3] = 0;
|
||
|
|
||
|
dest[4] = mat[3];
|
||
|
dest[5] = mat[4];
|
||
|
dest[6] = mat[5];
|
||
|
dest[7] = 0;
|
||
|
|
||
|
dest[8] = mat[6];
|
||
|
dest[9] = mat[7];
|
||
|
dest[10] = mat[8];
|
||
|
dest[11] = 0;
|
||
|
|
||
|
dest[12] = 0;
|
||
|
dest[13] = 0;
|
||
|
dest[14] = 0;
|
||
|
dest[15] = 1;
|
||
|
|
||
|
return dest;
|
||
|
}
|
||
|
|
||
|
function readChar(view, offset) {
|
||
|
return String.fromCharCode(view.getUint8(offset));
|
||
|
}
|
||
|
}
|