/* * Javascript/Canvas Textured 3D Renderer v0.3.1 * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com * This software is free to use for non-commercial purposes. For anything else, please contact the author. * This is a version modified by Stefano Gioffre'. */ Canvas3D.Mesh = function() { this._oPosition = new Canvas3D.Vec3(0,0,0); this._oRotation = new Canvas3D.Vec3(0,0,0); this._aVertices = []; // vertex positions in object space this._aGlobalVertices = []; // vertices translated to global space this._aFaces = []; this._aNormals = []; this._aMaterials = []; this._bDirty = true; this._bVisible = true; this._iForcedZ = -1; this._bHideWhenRotating = false; this._oDefaultColor = {r:155,g:155,b:155}; this._oDefaultMaterial = {}; this._iSize = 1; this._fScale = 1; this._bFill = true; // render filled triangles this._bWire = false; // render wireframe this._bShading = true; // shade/light filled triangles this._bBackfaceCull = true; // only draw triangles facing the camera this._bZSort = true; // sort triangles back-to-front this._bExpandClipPath = true; // expand clip path by 1px, to minimize gaps this._bTexture = false; // render textured triangles (must enable bFill as well) this._bTextureShading = false; // render shading on textured triangles (must enable bShading as well) // sometimes the exported normals from Max are messed up, or they're imported wrong or whatever, I don't know. // We can recalculate them after loading. this._bCalcNormals = true; // only allow textures if canvas is available var oCanvas = document.createElement("canvas"); this._bCanTexture = false; this._bCanTextureUV = false; if (oCanvas.getContext && oCanvas.getContext("2d")) { this._bCanTexture = true; if (oCanvas.getContext("2d").getImageData) { this._bCanTextureUV = true; } } }; // parse the mesh data // the mesh data (vertices, faces, texture coordinates, materials) are read from a JSON object structure // and copied into local arrays // normals are recalculated, if enabled. Canvas3D.Mesh.prototype.setMeshData = function(oMeshData, oScene) { this._oMeshData = oMeshData; this._aVertices = []; this._aFaces = []; this._aNormals = []; this._aMaterials = []; var oPos = this._oPosition; var me = this; if (this._oMeshData.mat) { for (var m=0;m= iSrcWidth) iSrcX = iSrcWidth-1; if (iSrcY >= iSrcHeight) iSrcY = iSrcHeight-1; var iDstPixOffset = iDstYOffset + iDstX*4; var iSrcPixOffset = (iSrcY*iSrcWidth + iSrcX)*4; aDstData[iDstPixOffset] = aSrcData[iSrcPixOffset]; aDstData[iDstPixOffset+1] = aSrcData[iSrcPixOffset+1]; aDstData[iDstPixOffset+2] = aSrcData[iSrcPixOffset+2]; aDstData[iDstPixOffset+3] = oMat.texalpha ? aSrcData[iSrcPixOffset+3] : 255; } while (--x); iOffsetX++; iDstXOffset++; } while (--y); iTexOffsetX += iTexRes; } while (--f); oDstCtx.putImageData(oDstDataObj, 0, 0); oDstCtx.fillRect(0,0,0,0); // Opera doesn't update until we draw something? oMat.facecanvas = oTexCanvas; } var fncZSort = function(a, b) { return a.transcenter.z - b.transcenter.z; } // math and misc shortcuts var sin = Math.sin; var cos = Math.cos; var asin = Math.asin; var acos = Math.acos; var pow = Math.pow; var sqrt = Math.sqrt; var fRadDeg = 180 / Math.PI; var fDegRad = Math.PI / 180; var fDegRad45 = fDegRad*45; var fDegRad90 = fDegRad*90; var fDegRad180 = fDegRad*180; var fSqrt2Div2 = sqrt(2) / 2; // this functions draws a textured (and shaded) triangle on the canvas // this is done by rotating/scaling a triangular section in place, setting up a clipping path and drawing the image. // if UV coords are enabled, only the correct part of the triangle-strip texture is drawn // if not, the entire texture is drawn for each face // some of the code used for skewing the image was inspired by the AS function found here: // http://www.senocular.com/flash/actionscript.php?file=ActionScript_1.0/Prototypes/MovieClip/skew.as Canvas3D.Mesh.prototype._drawTextureTriangle = function(oContext, oMat, oPoint1, oPoint2, oPoint3, iOffsetX, iOffsetY, fShade, oNormal, iIdx) { if (!oMat.image) { return; } if (!oMat.image.complete) { return; } var oMatImage = oMat.image; if (!oMatImage.canvas) { // first time around, we paint the texture image to a canvas // drawing the triangle later on is slightly faster using another canvas object rather than an image object // this should be moved to someplace else var iTexWidth = 50; var iTexHeight = 50; var oTextureCanvas = document.createElement("canvas"); oTextureCanvas.width = iTexWidth; oTextureCanvas.height = iTexHeight; oTextureCanvas.style.width = iTexWidth + "px"; oTextureCanvas.style.height = iTexHeight + "px"; var oTexCtx = oTextureCanvas.getContext("2d"); oTexCtx.drawImage(oMatImage, 0, 0, iTexWidth, iTexHeight); oMatImage.canvas = oTextureCanvas; } var x1 = oPoint1.x; var y1 = oPoint1.y; var x2 = oPoint2.x; var y2 = oPoint2.y; var x3 = oPoint3.x; var y3 = oPoint3.y; // trig to calc the angle we need to rotate in order get our texturetriangle in place var dx = x3 - x2; var dy = y3 - y2; var a = sqrt((dx*dx + dy*dy)); dx = x3 - x1; dy = y3 - y1; var b = sqrt((dx*dx + dy*dy)); dx = x2 - x1; dy = y2 - y1; var c = sqrt((dx*dx + dy*dy)); var aa = a*a, bb = b*b, cc = c*c; var fCosB = (aa + cc - bb) / (2*a*c); var fAngleB = acos(fCosB); if (isNaN(fAngleB)) return; var fCosC = (aa + bb - cc) / (2*a*b); var fAngleC = acos(fCosC); if (isNaN(fAngleC)) return; if ((fAngleB + fAngleC) == 0) return; var fSkewX = -(fDegRad90 - (fAngleB + fAngleC)); var fTriRotation = -(asin((y2 - y1) / c)); if (x2 > x1) { // rotate the other way around if triangle is flipped fTriRotation = fDegRad180 - fTriRotation; } if (fSkewX == fDegRad90) fSkewX = fDegRad*89.99; if (fSkewX == -fDegRad90) fSkewX = -fDegRad*89.99; var fCosX = cos(fSkewX); var fRotation = fDegRad45 + fSkewX * 0.5; var fDiv = 1 / (sin(fRotation) * fSqrt2Div2); var fScaleX = fCosX * fDiv; var fScaleY = (sin(fSkewX) + 1) * fDiv; // setup the clipping path, so only texture within the triangle is drawn. var iClipX1 = x1 + iOffsetX; var iClipY1 = y1 + iOffsetY; var iClipX2 = x2 + iOffsetX; var iClipY2 = y2 + iOffsetY; var iClipX3 = x3 + iOffsetX; var iClipY3 = y3 + iOffsetY; // here we try to expand the clip path by 1 pixel to get rid of (some of the) gaps between the triangles // we do this simply by moving the topmost point 1px up, the leftmost point 1px left, and so on. // later, we also render the triangle itself 1 px too big // drawbacks are that the contour of the object will appear a bit "pointy". if (this._bExpandClipPath && false) { if (iClipY1 < iClipY2 && iClipY1 < iClipY3) iClipY1--; else if (iClipY2 < iClipY1 && iClipY2 < iClipY3) iClipY2--; else if (iClipY3 < iClipY1 && iClipY3 < iClipY2) iClipY3--; if (iClipY1 > iClipY2 && iClipY1 > iClipY3) iClipY1++; else if (iClipY2 > iClipY1 && iClipY2 > iClipY3) iClipY2++; else if (iClipY3 > iClipY1 && iClipY3 > iClipY2) iClipY3++; if (iClipX1 < iClipX2 && iClipX1 < iClipX3) iClipX1--; else if (iClipX2 < iClipX1 && iClipX2 < iClipX3) iClipX2--; else if (iClipX3 < iClipX1 && iClipX3 < iClipX2) iClipX3--; if (iClipX1 > iClipX2 && iClipX1 > iClipX3) iClipX1++; else if (iClipX2 > iClipX1 && iClipX2 > iClipX3) iClipX2++; else if (iClipX3 > iClipX1 && iClipX3 > iClipX2) iClipX3++; } oContext.save(); // do the clip path oContext.beginPath(); oContext.moveTo(iClipX1, iClipY1); oContext.lineTo(iClipX2, iClipY2); oContext.lineTo(iClipX3, iClipY3); oContext.closePath(); oContext.clip(); // setup the skew/rotation transformation oContext.translate(x2 + iOffsetX, y2 + iOffsetY); oContext.rotate(fRotation + fTriRotation); oContext.scale(fScaleX, fScaleY); oContext.rotate(-fDegRad45); var fTriScaleX = c / 2; // 100 * 50; var fTriScaleY = b / 2; // 100 * 50; if (oMat.uv) { // we are using UV coordinates for texturing if (this._bCanTextureUV && oMat.facecanvas) { var iTexRes = oMat.facecanvas.resolution; // draw our texture // there will be a small gap between the triangles. Drawing the texture at offset (-1,-1) gets rid of some of it. oContext.drawImage( oMat.facecanvas, iIdx * iTexRes + iIdx*2+1, 1, iTexRes, iTexRes, -1, -1, fTriScaleX + 2, fTriScaleY + 2 ); } } else { // no UV, just draw the same texture for all faces oContext.drawImage( oMatImage.canvas, -1, -1, fTriScaleX + 2, fTriScaleY + 2 ); } oContext.restore(); // if shading is turned on, render a semi-transparent black triangle on top. // that means that a fully lit triangle will just be the raw texture, and the less lit a triangle is, the darker it gets. // we could render semi-transparent white on top to make it brighter, but it doesn't look right, so we settle for that. if (this._bTextureShading && fShade > 0) { oContext.beginPath(); oContext.moveTo(iClipX1, iClipY1); oContext.lineTo(iClipX2, iClipY2); oContext.lineTo(iClipX3, iClipY3); oContext.closePath(); oContext.fillStyle = "rgba(0,0,0," + (fShade*0.5) + ")"; oContext.fill(); } } // draw the mesh on the oContext canvas context, at offset [iOffsetX, iOffsetY] Canvas3D.Mesh.prototype.draw = function(oContext, iOffsetX, iOffsetY) { if (!this._bVisible) return; var oScene = this._oScene; var oCam = oScene.getActiveCamera(); var oAmbient = oScene.getAmbientLight() // if shading is enabled, calculate the direction vectors to all light sources var bLightDirty = false; if (this._bShading && this._bFill) { var aLights = oScene.getLights(); var aLightDirections = []; for (var l=0;l oPoint2.x)){ bDraw = true; } } else { bDraw = true; } if (oCam.clip(aTransVertices[oFace.a]) || oCam.clip(aTransVertices[oFace.b]) || oCam.clip(aTransVertices[oFace.c])) { bDraw = false; } // if triangle is facing camera, draw it if (bDraw) { // get the material for this face var oFaceMat = this._aMaterials[oFace.mat]; if (oFaceMat) { oFaceColor = oFaceMat; } else { oFaceMat = this._oDefaultMaterial; oFaceColor = this._oDefaultColor; } var bFaceTexture = this._bTexture && oFaceMat.t; // save the original color var oFaceOrgColor = {r:oFaceColor.r, g:oFaceColor.g, b:oFaceColor.b}; var fLight = 0; var fShade = 1; if (this._bFill) { // setup ambient face color if (!bFaceTexture) { if (bLightDirty || this._bDirty) { var oFaceColorAmb = { r:(oAmbient.r / 255) * oFaceColor.r, g:(oAmbient.g / 255) * oFaceColor.g, b:(oAmbient.b / 255) * oFaceColor.b }; } } // do lighting if (this._bShading) { if (bLightDirty || this._bDirty) { for (var l=0;l 0) { //fDot = Math.sqrt(fDot); fLight = fDot * aLights[l].getIntensity(); fShade = fShade - fLight; // lighten the face by the light intensity if (!bFaceTexture) { oFaceColorAmb = { r: oFaceColorAmb.r + oFaceColor.r * fLight, g: oFaceColorAmb.g + oFaceColor.g * fLight, b: oFaceColorAmb.b + oFaceColor.b * fLight }; } } } oFaceColorAmb.r = Math.floor(oFaceColorAmb.r); oFaceColorAmb.g = Math.floor(oFaceColorAmb.g); oFaceColorAmb.b = Math.floor(oFaceColorAmb.b); if (oFaceColorAmb.r < 0) oFaceColorAmb.r = 0; if (oFaceColorAmb.g < 0) oFaceColorAmb.g = 0; if (oFaceColorAmb.b < 0) oFaceColorAmb.b = 0; if (oFaceColorAmb.r > 255) oFaceColorAmb.r = 255; if (oFaceColorAmb.g > 255) oFaceColorAmb.g = 255; if (oFaceColorAmb.b > 255) oFaceColorAmb.b = 255; oFace.calccolor = oFaceColorAmb; oFace.shade = fShade; } oFaceColorAmb = oFace.calccolor; fShade = oFace.shade; } if (!bFaceTexture) { oFaceColor = oFaceColorAmb; } } oContext.beginPath(); oContext.moveTo(oPoint1.x + iOffsetX, oPoint1.y + iOffsetY); oContext.lineTo(oPoint2.x + iOffsetX, oPoint2.y + iOffsetY) oContext.lineTo(oPoint3.x + iOffsetX, oPoint3.y + iOffsetY) oContext.closePath(); if (this._bFill) { if (this._bCanTexture && this._bTexture && oFaceMat.image) { this._drawTextureTriangle(oContext, oFaceMat, oPoint1, oPoint2, oPoint3, iOffsetX, iOffsetY, fShade, oNormal, oFace.texidx); } else { oContext.fillStyle = "rgb(" + oFaceColor.r + "," + oFaceColor.g + "," + oFaceColor.b + ")"; oContext.fill(); if (!this._bWire) { oContext.lineWidth = 0.7; oContext.strokeStyle = "rgb(" + oFaceColor.r + "," + oFaceColor.g + "," + oFaceColor.b + ")"; oContext.stroke(); } } } if (this._bWire) { oFaceOrgColor.r = Math.min(Math.max(Math.round(oFaceOrgColor.r),0),255); oFaceOrgColor.g = Math.min(Math.max(Math.round(oFaceOrgColor.g),0),255); oFaceOrgColor.b = Math.min(Math.max(Math.round(oFaceOrgColor.b),0),255); oContext.lineWidth = 1; oContext.strokeStyle = "rgb(" + oFaceOrgColor.r + "," + oFaceOrgColor.g + "," + oFaceOrgColor.b + ")"; oContext.stroke(); } } } while (--f); this._bDirty = false; } Canvas3D.Mesh.prototype.setScene = function(oScene) { if (this._oScene != oScene) { this._oScene = oScene; } } Canvas3D.Mesh.prototype.setLighting = function(bEnable) { this._bShading = bEnable; } Canvas3D.Mesh.prototype.setBackfaceCull = function(bEnable) { this._bBackfaceCull = bEnable; } Canvas3D.Mesh.prototype.setZSort = function(bEnable) { this._bZSort = bEnable; } Canvas3D.Mesh.prototype.setFill = function(bEnable) { this._bFill = bEnable; } Canvas3D.Mesh.prototype.setWire = function(bEnable) { this._bWire = bEnable; } Canvas3D.Mesh.prototype.setTexture = function(bEnable) { this._bTexture = bEnable; } Canvas3D.Mesh.prototype.setTextureShading = function(bEnable) { this._bTextureShading = bEnable; } Canvas3D.Mesh.prototype._updateGlobalVertices = function() { var oRot = this._oRotation; var oPos = this._oPosition; for (var i = 0; i < this._aVertices.length; i++) { var oRotatedVertex = new Canvas3D.Vec3( this._aVertices[i].x, this._aVertices[i].y, this._aVertices[i].z ); if (oRot.x) oRotatedVertex.rotateX(oRot.x); if (oRot.y) oRotatedVertex.rotateY(oRot.y); if (oRot.z) oRotatedVertex.rotateZ(oRot.z); this._aGlobalVertices[i].x = oRotatedVertex.x * this._fScale + oPos.x; this._aGlobalVertices[i].y = oRotatedVertex.y * this._fScale + oPos.y; this._aGlobalVertices[i].z = oRotatedVertex.z * this._fScale + oPos.z; } this._recalcNormals(); } Canvas3D.Mesh.prototype._recalcNormals = function() { for (var f = 0; f < this._aFaces.length; f++) { var oFace = this._aFaces[f]; var oPoint1 = this._aGlobalVertices[oFace.a]; var oPoint2 = this._aGlobalVertices[oFace.b]; var oPoint3 = this._aGlobalVertices[oFace.c]; var oCenter = new Canvas3D.Vec3( (oPoint1.x + oPoint2.x + oPoint3.x) / 3, (oPoint1.y + oPoint2.y + oPoint3.y) / 3, (oPoint1.z + oPoint2.z + oPoint3.z) / 3 ); oFace.center = oCenter; var oNormal = new Canvas3D.Vec3( ((oPoint1.y - oPoint2.y) * (oPoint1.z - oPoint3.z)) - ((oPoint1.z - oPoint2.z) * (oPoint1.y - oPoint3.y)), ((oPoint1.z - oPoint2.z) * (oPoint1.x - oPoint3.x)) - ((oPoint1.x - oPoint2.x) * (oPoint1.z - oPoint3.z)), ((oPoint1.x - oPoint2.x) * (oPoint1.y - oPoint3.y)) - ((oPoint1.y - oPoint2.y) * (oPoint1.x - oPoint3.x)) ).unit(); oFace.normal = oNormal; } } Canvas3D.Mesh.prototype.setPosition = function(oVec) { if (oVec.x != this._oPosition.x || oVec.y != this._oPosition.y || oVec.z != this._oPosition.z) { this._oPosition = oVec; this._updateGlobalVertices(); this._bDirty = true; } } Canvas3D.Mesh.prototype.setRotation = function(oVec) { this._oRotation = oVec; this._updateGlobalVertices(); this._bDirty = true; } Canvas3D.Mesh.prototype.getPosition = function(oVec) { return this._oPosition; } Canvas3D.Mesh.prototype.setForcedZ = function(iZ) { this._iForcedZ = iZ; } Canvas3D.Mesh.prototype.getForcedZ = function() { return this._iForcedZ; } Canvas3D.Mesh.prototype.getHideWhenRotating = function() { return this._bHideWhenRotating; } Canvas3D.Mesh.prototype.setHideWhenRotating = function(bEnable) { this._bHideWhenRotating = bEnable; } Canvas3D.Mesh.prototype.getDirty = function() { return this._bDirty; } Canvas3D.Mesh.prototype.hide = function() { this._bVisible = false; this._bDirty = true; } Canvas3D.Mesh.prototype.show = function() { this._bVisible = true; this._bDirty = true; } Canvas3D.Mesh.prototype.isVisible = function() { return this._bVisible; } Canvas3D.Mesh.prototype.setScale = function(fScale) { this._fScale = fScale; this._bDirty = true; this._updateGlobalVertices(); } Canvas3D.Mesh.prototype.getScale = function() { return this._fScale; }