// // nitroRender.js //-------------------- // Provides an interface with which NSBMD models can be drawn to a fst canvas. // by RHY3756547 // // includes: gl-matrix.js (glMatrix 2.0) // /formats/nitro.js --passive requirement from other nitro formats // /formats/nsbmd.js // /formats/nsbta.js // /formats/nsbtx.js // window.nitroRender = new function() { var gl, frag, vert, nitroShader; var cVec, color, texCoord, norm; var vecMode, vecPos, vecNorm, vecTx, vecCol, vecNum, vecMat, curMat, stripAlt; var texWidth, texHeight, alphaMul = 1; var t = this; this.cullModes = []; this.billboardID = 0; //incrememts every time billboards need to be updated. cycles &0xFFFFFF to avoid issues this.lastMatStack = null; //used to check if we need to send the matStack again. will be used with a versioning system in future. this.last = {}; //obj: the last vertex buffers drawn var optimiseTriangles = true; //improves draw performance by >10x on most models. var modelBuffer; var shaders = []; this.renderDispList = renderDispList; this.setAlpha = setAlpha; this.getViewWidth = getViewWidth; this.getViewHeight = getViewHeight; this.flagShadow = false; this.forceFlatNormals = false; //generate flat normals for this mesh. Used for course model for better shadows. var parameters = { 0: 0, 0x10:1, 0x11:0, 0x12:1, 0x13:1, 0x14:1, 0x15:0, 0x16:16, 0x17:12, 0x18:16, 0x19:12, 0x1A:9, 0x1B:3, 0x1C:3, //matrix commands 0x20:1, 0x21:1, 0x22:1, 0x23:2, 0x24:1, 0x25:1, 0x26:1, 0x27:1, 0x28:1, 0x29:1, 0x2A:1, 0x2B:1, //vertex commands 0x30:1, 0x31:1, 0x32:1, 0x33:1, 0x34:32, //material param 0x40:1, 0x41:0, //begin or end vertices 0x50:1, //swap buffers 0x60:1, //viewport 0x70:3, 0x71:2, 0x72:1 //tests } var instructions = {}; instructions[0x14] = function(view, off) { //restore to matrix, used constantly for bone transforms curMat = view.getUint8(off); } instructions[0x20] = function(view, off) { //color var dat = view.getUint16(off,true); color[0] = (dat&31)/31; color[1] = ((dat>>5)&31)/31; color[2] = ((dat>>10)&31)/31; } instructions[0x21] = function(view, off) { //normal var dat = view.getUint32(off, true); norm[0] = tenBitSign(dat); norm[1] = tenBitSign(dat>>10); norm[2] = tenBitSign(dat>>20); } instructions[0x22] = function(view, off) { //texcoord texCoord[0] = (view.getInt16(off, true)/16)/texWidth; texCoord[1] = (view.getInt16(off+2, true)/16)/texHeight; } instructions[0x23] = function(view, off) { //xyz 16 bit cVec[0] = view.getInt16(off, true)/4096; cVec[1] = view.getInt16(off+2, true)/4096; cVec[2] = view.getInt16(off+4, true)/4096; pushVector(); } instructions[0x24] = function(view, off) { //xyz 10 bit var dat = view.getUint32(off, true); cVec[0] = tenBitSign(dat); cVec[1] = tenBitSign(dat>>10); cVec[2] = tenBitSign(dat>>20); pushVector(); } instructions[0x25] = function(view, off) { //xy 16 bit cVec[0] = view.getInt16(off, true)/4096; cVec[1] = view.getInt16(off+2, true)/4096; pushVector(); } instructions[0x26] = function(view, off) { //xz 16 bit cVec[0] = view.getInt16(off, true)/4096; cVec[2] = view.getInt16(off+2, true)/4096; pushVector(); } instructions[0x27] = function(view, off) { //yz 16 bit cVec[1] = view.getInt16(off, true)/4096; cVec[2] = view.getInt16(off+2, true)/4096; pushVector(); } instructions[0x28] = function(view, off) { //xyz 10 bit relative var dat = view.getUint32(off, true); cVec[0] += relativeSign(dat); cVec[1] += relativeSign(dat>>10); cVec[2] += relativeSign(dat>>20); pushVector(); } instructions[0x40] = function(view, off) { //begin vtx var dat = view.getUint32(off, true); vecMode = dat; if (!optimiseTriangles) { vecPos = []; vecNorm = []; vecTx = []; vecCol = []; vecMat = []; } vecNum = 0; stripAlt = 0; } instructions[0x41] = function(view, off) { //end vtx if (!optimiseTriangles) pushStrip(); } function setAlpha(alpha) { //for fading specific things out or whatever alphaMul = alpha; } function getViewWidth(){ return gl.viewportWidth; } function getViewHeight() { return gl.viewportHeight; } function pushStrip() { //push the last group of triangles to the buffer. Should do this on matrix change... details fourthcoming var modes = (optimiseTriangles)?[gl.TRIANGLES, gl.TRIANGLES, gl.TRIANGLES, gl.TRIANGLES]:[gl.TRIANGLES, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_STRIP]; var pos = gl.createBuffer(); var col = gl.createBuffer(); var tx = gl.createBuffer(); var mat = gl.createBuffer(); var norm = gl.createBuffer(); var posArray = new Float32Array(vecPos); if (t.forceFlatNormals && modes[vecMode] == gl.TRIANGLES) { //calculate new flat normals for each triangle for (var i=0; i 1) && (vecNum > 2)) { //convert tri strips to individual triangles so we get one buffer per polygon var b = vecMat.length - (((stripAlt % 2) == 0)?1:3); var s2 = vecMat.length - (((stripAlt % 2) == 0)?2:1); vecPos = vecPos.concat(vecPos.slice(b*3, b*3+3)).concat(vecPos.slice(s2*3, s2*3+3)); vecNorm = vecNorm.concat(vecNorm.slice(b*3, b*3+3)).concat(vecNorm.slice(s2*3, s2*3+3)); vecTx = vecTx.concat(vecTx.slice(b*2, b*2+2)).concat(vecTx.slice(s2*2, s2*2+2)); vecCol = vecCol.concat(vecCol.slice(b*4, b*4+4)).concat(vecCol.slice(s2*4, s2*4+4)); vecMat = vecMat.concat(vecMat.slice(b, b+1)).concat(vecMat.slice(s2, s2+1)); stripAlt++; } vecNum++; vecPos = vecPos.concat(cVec); vecTx = vecTx.concat(texCoord); vecCol = vecCol.concat(color); vecNorm = vecNorm.concat(norm); vecMat.push(curMat); } function tenBitSign(val) { val &= 1023; if (val & 512) return (val-1024)/64; else return val/64; } function relativeSign(val) { val &= 1023; if (val & 512) return (val-1024)/4096; else return val/4096; } this.init = function(ctx) { gl = ctx; this.gl = gl; this.billboardMat = mat4.create(); this.yBillboardMat = mat4.create(); shaders = nitroShaders.compileShaders(gl); this.nitroShader = shaders[0]; this.cullModes = [gl.FRONT_AND_BACK, gl.FRONT, gl.BACK]; } this.prepareShader = function() { //prepares the shader so no redundant calls have to be made. Should be called upon every program change. gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); //gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); this.last = {}; gl.activeTexture(gl.TEXTURE0); gl.uniform1i(this.nitroShader.samplerUniform, 0); } this.setShadowMode = function(sTex, fsTex, sMat, fsMat, dir) { this.nitroShader = shaders[1]; var shader = shaders[1]; gl.useProgram(shader); vec3.normalize(dir, dir); gl.uniform3fv(shader.lightDirUniform, dir); gl.uniformMatrix4fv(shader.shadowMatUniform, false, sMat); gl.uniformMatrix4fv(shader.farShadowMatUniform, false, fsMat); gl.uniform1f(shader.lightIntensityUniform, 0.3); this.resetShadOff(); this.setNormalFlip(1); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, sTex); gl.uniform1i(shader.lightSamplerUniform, 1); gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, fsTex); gl.uniform1i(shader.farLightSamplerUniform, 2); this.setColMult([1, 1, 1, 1]); this.prepareShader(); } this.setLightIntensities = function(intensity, shadIntensity) { if (intensity == null) intensity = 0.3; if (shadIntensity == null) shadIntensity = 1; var shader = this.nitroShader; gl.useProgram(this.nitroShader); gl.uniform1f(shader.lightIntensityUniform, intensity); gl.uniform1f(shader.shadLightenUniform, 1-shadIntensity); } this.setShadBias = function(bias) { var shader = this.nitroShader; gl.useProgram(this.nitroShader); gl.uniform1f(shader.shadOffUniform, bias); gl.uniform1f(shader.farShadOffUniform, bias); } this.setNormalFlip = function(flip) { var shader = this.nitroShader; gl.useProgram(this.nitroShader); gl.uniform1f(shader.normalFlipUniform, flip); } this.resetShadOff = function() { var shader = this.nitroShader; gl.useProgram(this.nitroShader); gl.uniform1f(shader.shadOffUniform, 0.0005+((mobile)?0.0005:0)); gl.uniform1f(shader.farShadOffUniform, 0.0020); } this.unsetShadowMode = function() { this.nitroShader = shaders[0]; gl.useProgram(this.nitroShader); this.setColMult([1, 1, 1, 1]); this.prepareShader(); } var paused = false; this.pauseShadowMode = function() { this.nitroShader = shaders[0]; if (this.nitroShader == shaders[1]) paused = true; gl.useProgram(this.nitroShader); this.setColMult([1, 1, 1, 1]); this.prepareShader(); } this.unpauseShadowMode = function() { if (!paused) return; this.nitroShader = shaders[1]; gl.useProgram(this.nitroShader); this.setColMult([1, 1, 1, 1]); this.prepareShader(); } this.setColMult = function(color) { gl.useProgram(this.nitroShader); gl.uniform4fv(this.nitroShader.colMultUniform, color); } this.updateBillboards = function(view) { this.billboardID = (this.billboardID+1)%0xFFFFFF; var nv = mat4.clone(view); nv[12] = 0; nv[13] = 0; nv[14] = 0; //nullify translation var nv2 = mat4.clone(nv); this.billboardMat = mat4.invert(nv, nv); nv2[4] = 0; nv2[5] = 1; //do not invert y axis view nv2[6] = 0; this.yBillboardMat = mat4.invert(nv2, nv2); } function renderDispList(disp, tex, startStack) { //renders the display list to a form of vertex buffer. The idea is that NSBTA and NSBCA can still be applied to the buffer at little performance cost. (rather than recompiling the model) modelBuffer = { strips: [] /* strip entry format: vPos: glBuffer, vTx: glBuffer, vCol: glBuffer, verts: int count of vertices, mode: (eg. gl.TRIANGLES, gl.TRIANGLESTRIP) mat: transformation matrix to apply. unused atm as matrix functions are unimplemented */ } //the nitroModel will store this and use it for rendering instead of the display list in future. curMat = startStack; //start on root bone var shader = nitroRender.nitroShader; var gl = nitroRender.gl; var off=0; var view = new DataView(disp); texWidth = tex.width; texHeight = tex.height; cVec = [0,0,0]; norm = [0,1,0]; texCoord = [0,0]; color = [1,1,1,alphaMul]; //todo: polygon attributes vecMode = 0; vecNum = 0; stripAlt = 0; vecPos = []; vecNorm = []; vecTx = []; vecCol = []; vecMat = []; while (off < disp.byteLength) { var ioff = off; off += 4; for (var i=0; i<4; i++) { var inst = view.getUint8(ioff++); if (instructions[inst] != null) { instructions[inst](view, off); } else { if (inst != 0) alert("invalid instruction 0x"+(inst.toString(16))); } var temp = parameters[inst]; off += (temp == null)?0:temp*4; } } if (optimiseTriangles) pushStrip(); return modelBuffer; } }; function nitroModel(bmd, btx) { var bmd = bmd; this.bmd = bmd; var thisObj = this; var loadedTex; var texCanvas; var tex; var texAnim; var texPAnim; var texFrame; var modelBuffers; var collisionModel = []; var matBufEmpty = new Float32Array(31*16); var temp = mat4.create(); var off=0; for (var i=0; i<31; i++) { matBufEmpty.set(temp, off); off += 16; } temp = null; var texMap = { tex:{}, pal:{} }; //var matStack; this.draw = draw; this.drawPoly = externDrawPoly; this.drawModel = externDrawModel; this.getBoundingCollisionModel = getBoundingCollisionModel; this.getCollisionModel = getCollisionModel; this.baseMat = mat4.create(); modelBuffers = [] this.modelBuffers = modelBuffers; var matBuf = []; for (var i=0; i max[j]) max[j] = tri[j]; } } //create the bounding box out = [ { //top Vertices: [[max[0], max[1], max[2]], [max[0], max[1], min[2]], [min[0], max[1], min[2]]], Normal: [0, 1, 0] }, { Vertices: [[min[0], max[1], min[2]], [min[0], max[1], max[2]], [max[0], max[1], max[2]]], Normal: [0, 1, 0] }, { //bottom Vertices: [[min[0], min[1], min[2]], [max[0], min[1], min[2]], [max[0], min[1], max[2]] ], Normal: [0, -1, 0] }, { Vertices: [[max[0], min[1], max[2]], [min[0], min[1], max[2]], [min[0], min[1], min[2]] ], Normal: [0, -1, 0] }, { //back (farthest z) Vertices: [[max[0], max[1], max[2]], [max[0], min[1], max[2]], [min[0], min[1], max[2]]], Normal: [0, 0, 1] }, { Vertices: [[min[0], min[1], max[2]], [min[0], max[1], max[2]], [max[0], max[1], max[2]]], Normal: [0, 0, 1] }, { //front (closest z) Vertices: [[min[0], min[1], min[2]], [max[0], min[1], min[2]], [max[0], max[1], min[2]]], Normal: [0, 0, -1] }, { Vertices: [[max[0], max[1], min[2]], [min[0], max[1], min[2]], [min[0], min[1], min[2]]], Normal: [0, 0, -1] }, { //right (pos x) Vertices: [[max[0], max[1], max[2]], [max[0], min[1], max[2]], [max[0], min[1], min[2]]], Normal: [1, 0, 0] }, { Vertices: [[max[0], min[1], min[2]], [max[0], max[1], min[2]], [max[0], max[1], max[2]]], Normal: [1, 0, 0] }, { //left (neg x) Vertices: [[-max[0], min[1], min[2]], [-max[0], min[1], max[2]], [-max[0], max[1], max[2]]], Normal: [-1, 0, 0] }, { Vertices: [[-max[0], max[1], max[2]], [-max[0], max[1], min[2]], [-max[0], min[1], min[2]]], Normal: [-1, 0, 0] }, ] out.push() return {dat:out, scale:model.head.scale}; } function getCollisionModel(modelind, polyind, colType) { //simple func to get collision model for a model. used when I'm too lazy to define my own... REQUIRES TRI MODE ACTIVE! if (collisionModel[modelind] == null) collisionModel[modelind] = []; if (collisionModel[modelind][polyind] != null) return collisionModel[modelind][polyind]; var model = bmd.modelData.objectData[modelind]; var poly = model.polys.objectData[polyind]; if (modelBuffers[modelind][polyind] == null) modelBuffers[modelind][polyind] = nitroRender.renderDispList(poly.disp, tex[poly.mat], (poly.stackID == null)?model.lastStackID:poly.stackID); var tris = modelBuffers[modelind][polyind].strips[0].posArray; var out = []; var tC = tris.length/9; var off = 0; for (var i=0; i=0; i--) { if (offFrame >= anim.frames[i].time) { loadMatTex(model.materials.objectData[pmat], btx == null ? bmd.tex : btx, anim.frames[i]); /* tex[pmat] = cacheTex(btx == null ? bmd.tex : btx, anim.frames[i].tex, anim.frames[i].mat, model.materials.objectData[pmat]); */ break; } } } } if (nitroRender.last.tex != tex[pmat]) { gl.bindTexture(gl.TEXTURE_2D, tex[pmat]); //load up material texture nitroRender.last.tex = tex[pmat]; } var material = model.materials.objectData[pmat]; nitroRender.setAlpha(material.alpha); if (texAnim != null) { //generate and send texture matrix from data var matname = model.materials.names[pmat]; //attach tex anim to mat with same name var anims = texAnim.animData.objectData[modelind].data; var animNum = anims.names.indexOf(matname); if (animNum != -1) { //we got a match! it's wonderful :') var anim = anims.objectData[animNum]; var mat = matAtFrame(texFrame, anim); gl.uniformMatrix3fv(shader.texMatrixUniform, false, mat); } else { gl.uniformMatrix3fv(shader.texMatrixUniform, false, material.texMat); } } else gl.uniformMatrix3fv(shader.texMatrixUniform, false, material.texMat); if (modelBuffers[modelind][polyind] == null) modelBuffers[modelind][polyind] = nitroRender.renderDispList(poly.disp, tex[poly.mat], (poly.stackID == null)?model.lastStackID:poly.stackID); if (material.cullMode < 3) { gl.enable(gl.CULL_FACE); gl.cullFace(nitroRender.cullModes[material.cullMode]); } else { if (nitroRender.forceFlatNormals) { //dual side lighting model, course render mode essentially gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); drawModelBuffer(modelBuffers[modelind][polyind], gl, shader); nitroRender.setNormalFlip(-1); gl.cullFace(gl.FRONT); drawModelBuffer(modelBuffers[modelind][polyind], gl, shader); nitroRender.setNormalFlip(1); return; } gl.disable(gl.CULL_FACE); } drawModelBuffer(modelBuffers[modelind][polyind], gl, shader); } function frameLerp(frame, step, values) { if (values.length == 1) return values[0]; var i = (frame / (1 << step)) % 1; var len = values.length if (step > 0) len -= 1; var frame1 = (frame>>step)%len; var from = values[frame1]; var to = values[frame1+1] || values[frame1]; return to * i + from * (1-i); } function matAtFrame(frame, anim) { var mat = mat3.create(); //material texture mat is ignored mat3.scale(mat, mat, [frameLerp(frame, anim.frameStep.scaleS, anim.scaleS), frameLerp(frame, anim.frameStep.scaleT, anim.scaleT)]); mat3.translate(mat, mat, [-frameLerp(frame, anim.frameStep.translateS, anim.translateS), frameLerp(frame, anim.frameStep.translateT, anim.translateT)]); return mat; } function generateMatrixStack(model, targ) { //this generates a matrix stack with the default bones. use nitroAnimator to pass custom matrix stacks using nsbca animations. var matrices = []; var objs = model.objects.objectData; var cmds = model.commands; var curMat = mat4.clone(thisObj.baseMat); var lastStackID = 0; var highestUsed = -1; for (var i=0; i highestUsed) highestUsed = lastStackID; } else { matrices[lastStackID] = mat4.clone(curMat); } } model.lastStackID = lastStackID; var scale = [model.head.scale, model.head.scale, model.head.scale]; targ.set(matBufEmpty); var off=0; for (var i=0; i<=highestUsed; i++) { if (matrices[i] != null) { mat4.scale(matrices[i], matrices[i], scale); targ.set(matrices[i], off); } off += 16; } return targ; } function drawModelBuffer(buf, gl, shader) { for (var i=0; i