// // 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; var texWidth, texHeight, alphaMul = 1; 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; 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; } 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); gl.bindBuffer(gl.ARRAY_BUFFER, pos); gl.bufferData(gl.ARRAY_BUFFER, posArray, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, tx); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vecTx), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, col); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vecCol), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, mat); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vecMat), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, norm); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vecNorm), gl.STATIC_DRAW); modelBuffer.strips.push({ posArray: posArray, vPos: pos, vTx: tx, vCol: col, vMat: mat, vNorm: norm, verts: vecPos.length/3, mode: modes[vecMode] }) } function pushVector() { if (vecMode == 1 && vecNum%4 == 3) { //quads - special case vecPos = vecPos.concat(vecPos.slice(vecPos.length-9, vecPos.length-6)).concat(vecPos.slice(vecPos.length-3)); vecNorm = vecNorm.concat(vecNorm.slice(vecNorm.length-9, vecNorm.length-6)).concat(vecNorm.slice(vecNorm.length-3)); vecTx = vecTx.concat(vecTx.slice(vecTx.length-6, vecTx.length-4)).concat(vecTx.slice(vecTx.length-2)); vecCol = vecCol.concat(vecCol.slice(vecCol.length-12, vecCol.length-8)).concat(vecCol.slice(vecCol.length-4)); vecMat = vecMat.concat(vecMat.slice(vecMat.length-3, vecMat.length-2)).concat(vecMat.slice(vecMat.length-1)); } if (optimiseTriangles && (vecMode > 1) && (vecNum > 2)) { //convert tri strips to individual triangles so we get one buffer per polygon vecPos = vecPos.concat(vecPos.slice(vecPos.length-6)); vecNorm = vecNorm.concat(vecNorm.slice(vecNorm.length-6)); vecTx = vecTx.concat(vecTx.slice(vecTx.length-4)); vecCol = vecCol.concat(vecCol.slice(vecCol.length-8)); vecMat = vecMat.concat(vecMat.slice(vecMat.length-2)); } 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; 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.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) { this.nitroShader = shaders[1]; var shader = shaders[1]; gl.useProgram(shader); gl.uniformMatrix4fv(shader.shadowMatUniform, false, sMat); gl.uniformMatrix4fv(shader.farShadowMatUniform, false, fsMat); gl.uniform1f(shader.shadOffUniform, 0.00005+((mobile)?0.0005:0)); gl.uniform1f(shader.farShadOffUniform, 0.0005); 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.unsetShadowMode = function() { this.nitroShader = shaders[0]; 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; 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, remap) { var bmd = bmd; this.bmd = bmd; var thisObj = this; var loadedTex; var texCanvas; var tex; var texAnim; 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.getCollisionModel = getCollisionModel; modelBuffers = [] this.modelBuffers = modelBuffers; var matBuf = []; for (var i=0; i>anim.frameStep.scaleS)%anim.scaleS.length], anim.scaleT[(texFrame>>anim.frameStep.scaleT)%anim.scaleT.length]]); 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 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); drawModelBuffer(modelBuffers[modelind][polyind], gl, shader); } 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.create(); var lastStackID = 0; for (var i=0; i