「C3dl」修訂間的差異
出自 MozTW Wiki
(→Scene) |
(→觀景窗 Scene) |
||
行 179: | 行 179: | ||
== 觀景窗 Scene == | == 觀景窗 Scene == | ||
+ | <pre> | ||
+ | /* | ||
+ | Copyright (c) 2008 Seneca College | ||
+ | Licenced under the MIT License (http://www.c3dl.org/index.php/mit-license/) | ||
+ | */ | ||
+ | |||
+ | /** | ||
+ | @class A Scene should be thought of as a scene on a movie set. A scene | ||
+ | would typically contain objects which are moving and a current camera, | ||
+ | lights, etc. | ||
+ | */ | ||
+ | function Scene() | ||
+ | { | ||
+ | // Engine Variables | ||
+ | var glCanvas3D = null; // OpenGL Context (Canvas) | ||
+ | var renderer = null; // GLES 1.1 or 2.0 | ||
+ | var camera = null; // Reference to a Camera type | ||
+ | |||
+ | // A reference to a model which will actually act as a | ||
+ | // SkyBox, except any Model can be used, not just a box. | ||
+ | var skyModel = null; | ||
+ | |||
+ | var objList = new Array(); // An array of Objects to draw | ||
+ | var exitFlag = false; // Exits the mail loop | ||
+ | var cvs = null; // Canvas Tag | ||
+ | var canvasHeight = 0; | ||
+ | var canvasWidth = 0; | ||
+ | var canvas2Dlist = new Array(); | ||
+ | |||
+ | // Input Handler Variables | ||
+ | var kybdHandler = null; | ||
+ | var mouseHandler = null; | ||
+ | var updateHandler = null; | ||
+ | |||
+ | // Performance Variables | ||
+ | var lastTimeTaken = Date.now(); | ||
+ | var timerID = 0; | ||
+ | var lastTimePrinted = Date.now(); // performance logging | ||
+ | var frameCount = 0; | ||
+ | var drewOnce = false; | ||
+ | var totalFrameCount = 0; | ||
+ | var backgroundColor = new Array(0.4, 0.4, 0.6, 1.0); | ||
+ | var ambientLight = new Array(1,1,1,1); | ||
+ | |||
+ | // Shaders | ||
+ | this.sp = null; | ||
+ | var textureManager = null; | ||
+ | |||
+ | var thisScn = null; | ||
+ | |||
+ | |||
+ | // ------------------------------------------------------- | ||
+ | |||
+ | // Getters | ||
+ | |||
+ | /** | ||
+ | Get the camera of the scene. | ||
+ | |||
+ | @returns {Camera} The camera of the scene. | ||
+ | */ | ||
+ | this.getCamera = function() | ||
+ | { | ||
+ | return camera; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the number of objects in the scene. | ||
+ | |||
+ | @returns {int} The number of objects in the Object list. | ||
+ | */ | ||
+ | this.getObjListSize = function(){ | ||
+ | return objList.length; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the context. | ||
+ | |||
+ | @returns {Context} | ||
+ | */ | ||
+ | this.getGL = function(){ | ||
+ | return glCanvas3D; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | */ | ||
+ | this.getTotalFrameCount = function(){ | ||
+ | return totalFrameCount; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the scene's Renderer | ||
+ | |||
+ | |||
+ | */ | ||
+ | this.getRenderer = function(){ | ||
+ | return renderer; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the TextureManager of the scene. One of the primary | ||
+ | responsibilities of a TextureManager is to ensure textures | ||
+ | are only loaded once. | ||
+ | |||
+ | @returns {TextureManager} The TextureManager of the scene. | ||
+ | */ | ||
+ | this.getTextureManager = function(){ | ||
+ | return textureManager; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the Scene. | ||
+ | |||
+ | @returns {Scene} | ||
+ | */ | ||
+ | this.getScene = function(){ | ||
+ | return thisScn; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the SkyModel. | ||
+ | |||
+ | @returns {Model} The Scene's SkyModel. | ||
+ | */ | ||
+ | this.getSkyModel = function() | ||
+ | { | ||
+ | return skyModel; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get the ambient light of the scene. | ||
+ | |||
+ | @returns {Array} An Array of four values. | ||
+ | */ | ||
+ | this.getAmbientLight = function() | ||
+ | { | ||
+ | return new Array(ambientLight[0]/5,ambientLight[1]/5,ambientLight[2]/5,ambientLight[3]); | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Get a reference of a particular object in the scene. | ||
+ | |||
+ | @param indxNum The index number of the object. | ||
+ | |||
+ | @return the reference to the object at index number indxNum or null | ||
+ | if indxNum was out of bounds. | ||
+ | */ | ||
+ | this.getObj = function(indxNum) | ||
+ | { | ||
+ | if (isNaN(indxNum)) | ||
+ | { | ||
+ | logWarning('Scene::getObj() called with a parameter that\'s not a number'); | ||
+ | return null; | ||
+ | } | ||
+ | // Check if the index that was asked for is inside the bounds of our array | ||
+ | if (indxNum < 0 || indxNum >= objList.length) | ||
+ | { | ||
+ | logWarning('Scene::getObj() called with ' + indxNum +', which is not betwen 0 and ' + objList.length); | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | // We do this because we dont want outsiders modifying the object list, | ||
+ | // just the object themselves (ie. changing position, orientation, etc) | ||
+ | return objList[indxNum]; | ||
+ | } | ||
+ | |||
+ | // ------------------------------------------------------- | ||
+ | |||
+ | // Setters | ||
+ | /** | ||
+ | Set the functions to call when a key is pressed or released. | ||
+ | |||
+ | @param {function} keyUpCB The callback function for the up key. | ||
+ | @param {function} keyDownCD The callback function for the down key. | ||
+ | */ | ||
+ | this.setKeyboardCallback = function(keyUpCB, keyDownCB) | ||
+ | { | ||
+ | if (cvs) | ||
+ | { | ||
+ | // Register True keyboard listeners | ||
+ | if (keyUpCB != null) document.addEventListener("keyup", keyUpCB, false); | ||
+ | if (keyDownCB != null) document.addEventListener("keydown", keyDownCB, false); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Pass in the functions to call when mouse event occur such as when | ||
+ | a button is pressed, released or the mousewheel is scrolled. The | ||
+ | scene will call these functions when the event occur. | ||
+ | |||
+ | @param {function} mouseUpCB | ||
+ | @param {function} mouseDownCB | ||
+ | @param {function} mouseMoveCB | ||
+ | @param {function} mouseScrollCB | ||
+ | */ | ||
+ | this.setMouseCallback = function(mouseUpCB, mouseDownCB, mouseMoveCB, mouseScrollCB) | ||
+ | { | ||
+ | if (cvs) | ||
+ | { | ||
+ | // Register all Mouse listeners | ||
+ | if (mouseMoveCB != null) cvs.addEventListener("mousemove", mouseMoveCB, false); | ||
+ | if (mouseUpCB != null) cvs.addEventListener("mouseup", mouseUpCB, false); | ||
+ | if (mouseDownCB != null) cvs.addEventListener("mousedown", mouseDownCB, false); | ||
+ | if (mouseScrollCB != null) cvs.addEventListener("DOMMouseScroll", mouseScrollCB, false); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | |||
+ | /** | ||
+ | Set the SkyModel. A SkyModel acts like a skybox, when the camera | ||
+ | moves in the scene the SkyModel maintains the same distance from | ||
+ | the camera. This creates the illusion that there are parts to | ||
+ | the scene that are very far away which cannot be reached. | ||
+ | Applications of this would include creating clouds, or mountain | ||
+ | ranges, starfields, etc. Any Model can be passed in and is not | ||
+ | restricted to a Cube. Whatever model that is appropirate should | ||
+ | be used. | ||
+ | |||
+ | @param {Model} sky A Model which will maintain the same distance | ||
+ | from the Scene's camera. | ||
+ | */ | ||
+ | this.setSkyModel = function(sky) | ||
+ | { | ||
+ | if( sky instanceof Model) | ||
+ | { | ||
+ | skyModel = sky; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Set the function to call everytime the scene is updated. | ||
+ | |||
+ | @param {function} updateCB The function to call everytime the | ||
+ | scene is updated. | ||
+ | */ | ||
+ | this.setUpdateCallback = function(updateCB) | ||
+ | { | ||
+ | if (cvs) | ||
+ | { | ||
+ | if (updateCB != null) updateHandler = updateCB; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Set the renderer. Currently only 2 renderers are supported: | ||
+ | OpemGLES11 and OpenGLES20. | ||
+ | |||
+ | @param {OpenGLES11|OpenGLES20} | ||
+ | */ | ||
+ | this.setRenderer = function(renderType) | ||
+ | { | ||
+ | // Set the type of renderer to use | ||
+ | if (renderType instanceof OpenGLES11 || | ||
+ | renderType instanceof OpenGLES20) | ||
+ | { | ||
+ | renderer = renderType; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | @param canvasTag The name of the canvas, that is the value which is | ||
+ | assigned to the id property of the canvas tag in the html file. | ||
+ | */ | ||
+ | this.setCanvasTag = function(canvasTag) | ||
+ | { | ||
+ | // Get the Canvas tag | ||
+ | cvs = document.getElementById(canvasTag); | ||
+ | |||
+ | if (cvs == null) | ||
+ | { | ||
+ | logWarning('Scene::createScene() No canvas tag with name ' + canvasTag + ' was found.'); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Set the Scene's camera. | ||
+ | |||
+ | @param {Camera} cam The camera. | ||
+ | */ | ||
+ | this.setCamera = function(cam) | ||
+ | { | ||
+ | // Check to see if we were passed a correct Camera class | ||
+ | if (cam instanceof ChaseCamera || | ||
+ | cam instanceof FreeCamera || | ||
+ | cam instanceof FixedCamera || | ||
+ | cam instanceof PanCamera) | ||
+ | { | ||
+ | camera = cam; | ||
+ | return true; | ||
+ | } | ||
+ | |||
+ | logWarning('Scene::setCamera() invalid type of camera.'); | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | This one just calls addTextToModel() | ||
+ | //!! need dest as a parameter, probably in pixels, where to put the text | ||
+ | */ | ||
+ | this.addFloatingText = function(text, fontStyle, fontColour, backgroundColour) | ||
+ | { | ||
+ | var box = this.addTextToModel(null, text, fontStyle, fontColour, backgroundColour); | ||
+ | box.stayInFrontOfCamera = true; | ||
+ | this.addObjectToScene(box); | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Create a 2D canvas, render the text into it, and use that as a texture for model. | ||
+ | If model is null, create a rectangle and stick the text onto it. | ||
+ | */ | ||
+ | this.addTextToModel = function(model, text, fontStyle, fontColour, backgroundColour) | ||
+ | { | ||
+ | // Create a SPAN element with the string and style matching what the user asked | ||
+ | // for the floating text. | ||
+ | var tempSpan = document.createElement('span'); | ||
+ | var tempSpanStyle = document.createElement('style'); | ||
+ | var tempSpanStyleContent = document.createTextNode('span{' + | ||
+ | 'font: ' + fontStyle + ';' + | ||
+ | 'color: ' + fontColour + '; ' + | ||
+ | 'background: ' + backgroundColour + | ||
+ | ';}'); | ||
+ | var tempText = document.createTextNode(text); | ||
+ | tempSpanStyle.appendChild(tempSpanStyleContent); | ||
+ | tempSpan.appendChild(tempSpanStyle); | ||
+ | tempSpan.appendChild(tempText); | ||
+ | |||
+ | // Append it to the body so it's momentarily displayed. I couldn't find a way to measure | ||
+ | // the text box's size without displaying it. | ||
+ | document.body.appendChild(tempSpan); | ||
+ | |||
+ | var actualStringWidth = tempSpan.offsetWidth; | ||
+ | var actualStringHeight = tempSpan.offsetHeight; | ||
+ | var stringWidth = roundUpToNextPowerOfTwo(tempSpan.offsetWidth); | ||
+ | var stringHeight = roundUpToNextPowerOfTwo(tempSpan.offsetHeight); | ||
+ | |||
+ | // Now get rid of that element, we only needed it to measure it | ||
+ | tempSpan.removeChild(tempSpanStyle); | ||
+ | document.body.removeChild(tempSpan); | ||
+ | |||
+ | var box; | ||
+ | if (model == null) | ||
+ | { | ||
+ | var whRatio = stringWidth / stringHeight; | ||
+ | |||
+ | // Model for the plane with the text, size based on whRatio | ||
+ | var smallCanvasVertices = | ||
+ | [ | ||
+ | [-1.0 * (whRatio / 2), -1.0, 0.0], // 0 - bottom left | ||
+ | [-1.0 * (whRatio / 2), 1.0, 0.0], // 1 - top left | ||
+ | [ 1.0 * (whRatio / 2), 1.0, 0.0], // 2 - top right | ||
+ | [ 1.0 * (whRatio / 2), -1.0, 0.0], // 3 - bottom right | ||
+ | ]; | ||
+ | var smallCanvasNormals = | ||
+ | [ | ||
+ | [0,0,-1] | ||
+ | ]; | ||
+ | var smallCanvasUVs = | ||
+ | [ | ||
+ | [0.0,1.0], // 0 - bottom left | ||
+ | [0.0,0.0], // 1 - top left | ||
+ | [1.0,0.0], // 2 - top right | ||
+ | [1.0,1.0] // 3 - bottom right | ||
+ | ]; | ||
+ | var smallCanvasFaces = | ||
+ | [ | ||
+ | [0,0,0], [3,3,0], [2,2,0], | ||
+ | [0,0,0], [2,2,0], [1,1,0] | ||
+ | ]; | ||
+ | |||
+ | box = new Model(); | ||
+ | box.init(smallCanvasVertices, smallCanvasNormals, smallCanvasUVs, smallCanvasFaces); | ||
+ | //box.setAngularVel(new Array(0.003, 0.000, 0.000)); | ||
+ | //box.pitch(-0.4); | ||
+ | |||
+ | //!! need something user-specified | ||
+ | box.setPosition(new Array(5, 0, 5)); | ||
+ | } | ||
+ | else | ||
+ | box = model; | ||
+ | |||
+ | // Draw the text into the 2D canvas and use it for the above model's texture | ||
+ | var textureCanvas = this.create2Dcanvas(stringWidth, stringHeight); | ||
+ | if (textureCanvas.getContext) | ||
+ | { | ||
+ | var ctx = textureCanvas.getContext('2d'); | ||
+ | |||
+ | if (fontStyle) | ||
+ | ctx.mozTextStyle = fontStyle; | ||
+ | |||
+ | // Fill everything with backgroundColour if it's specified | ||
+ | if (backgroundColour) | ||
+ | { | ||
+ | ctx.fillStyle = backgroundColour; | ||
+ | ctx.fillRect(0, 0, stringWidth, stringHeight); | ||
+ | } | ||
+ | |||
+ | // Center the text in the 2D canvas | ||
+ | ctx.translate((stringWidth - actualStringWidth) / 2, | ||
+ | stringHeight - (stringHeight - actualStringHeight)); | ||
+ | |||
+ | if (fontColour) | ||
+ | ctx.fillStyle = fontColour; | ||
+ | else | ||
+ | ctx.fillStyle = 'black'; | ||
+ | |||
+ | ctx.mozDrawText(text); | ||
+ | |||
+ | box.setTextureFromCanvas2D(textureCanvas.id); | ||
+ | textureManager.addTextureFromCanvas2D(textureCanvas.id); | ||
+ | } | ||
+ | else | ||
+ | logWarning("addFloatingText(): call to create2Dcanvas() failed"); | ||
+ | |||
+ | return box; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Create a 2D canvas for drawing text and other stuff. Keep a | ||
+ | reference to it. | ||
+ | |||
+ | @return {CanvasTag} | ||
+ | */ | ||
+ | this.create2Dcanvas = function(width, height) | ||
+ | { | ||
+ | var newCanvas = document.createElement('canvas'); | ||
+ | newCanvas.id = 'changemetorandomstring'; | ||
+ | newCanvas.width = width; | ||
+ | newCanvas.height = height; | ||
+ | cvs.appendChild(newCanvas); | ||
+ | |||
+ | canvas2Dlist.push(newCanvas); | ||
+ | |||
+ | return newCanvas; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Set the color of the background. Values are clamped to the | ||
+ | range [0,1]. | ||
+ | |||
+ | @param {Array} bgColor Four values in the order: red, green, | ||
+ | blue, alpha/intensity. | ||
+ | */ | ||
+ | this.setBackgroundColor = function(bgColor) | ||
+ | { | ||
+ | if( bgColor.length == 4 ) | ||
+ | { | ||
+ | for(var i=0; i < 4; i++) | ||
+ | { | ||
+ | backgroundColor[i] = bgColor[i]; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Set the ambient light of the scene. | ||
+ | |||
+ | @param {Array} light An array of 4 floating point values | ||
+ | ranging from 0 to 1. | ||
+ | */ | ||
+ | this.setAmbientLight = function(light) | ||
+ | { | ||
+ | if( light.length == 4 ) | ||
+ | { | ||
+ | // convert the values passed in which range from 0 to 1 to a range of | ||
+ | // 0 to 5, which OpenGL wants. | ||
+ | ambientLight = new Array(light[0]*5, light[1]*5, light[2]* 5, 1); | ||
+ | // set the ambient light for the scene | ||
+ | // note the values passed to lightModel are not clamped | ||
+ | // also only available in 1.1 | ||
+ | if( renderer instanceof OpenGLES11 && glCanvas3D != null) | ||
+ | { | ||
+ | glCanvas3D.lightModel(glCanvas3D.LIGHT_MODEL_AMBIENT, ambientLight); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | |||
+ | /** | ||
+ | Acquire the OpenGL Context | ||
+ | |||
+ | @param {string} name | ||
+ | |||
+ | @returns {boolean} | ||
+ | */ | ||
+ | this.init = function(name) | ||
+ | { | ||
+ | if (renderer != null && cvs != null) | ||
+ | { | ||
+ | // Initialize the renderer | ||
+ | if (!renderer.createRenderer(cvs)) | ||
+ | { | ||
+ | logWarning('Scene::createScene() Renderer failed to initialize.'); | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | // Get the Canvas | ||
+ | glCanvas3D = renderer.getGLContext(); | ||
+ | |||
+ | // Set our global (fake static variable) to be used in rendering | ||
+ | thisScn = this; | ||
+ | |||
+ | textureManager = new TextureManager(glCanvas3D); | ||
+ | |||
+ | // Get the size of the Canvas Space for Aspect Ratio calculation | ||
+ | canvasWidth = cvs.width; | ||
+ | canvasHeight = cvs.height; | ||
+ | |||
+ | // Initialize the renderer | ||
+ | return renderer.init(canvasWidth, canvasHeight); | ||
+ | } | ||
+ | |||
+ | logWarning('Scene::createScene() No renderer was specified.'); | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | |||
+ | /** | ||
+ | Add the object 'obj' to the scene. Currently only objects which are | ||
+ | Models, Primitives or Cubes can be added to the scene. | ||
+ | |||
+ | @param {Model|Primitive|Cube} obj A reference to an object, such as a Model or a Cube. | ||
+ | |||
+ | @return {boolean} True if the object was added to the scene, false otherwise. | ||
+ | */ | ||
+ | this.addObjectToScene = function(obj) | ||
+ | { | ||
+ | // Check to see if we were passed a correct Camera class | ||
+ | if (obj instanceof Model || | ||
+ | obj instanceof Primitive || | ||
+ | obj instanceof Cube) | ||
+ | { | ||
+ | objList.push(obj); | ||
+ | |||
+ | return true; | ||
+ | } | ||
+ | |||
+ | logWarning('Scene::addObjectToScene() called with a parameter that\'s not a Model, Primitive, or Cube'); | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Remove an object from the scene. This is an O(n) operation. | ||
+ | |||
+ | @param {Model|Primitive|Cube} obj The object to remove from the scene. | ||
+ | |||
+ | @return {boolean} True if the object was found and removed from the scene and | ||
+ | false if the obj argument was not a type of Model, Primitive or Cube | ||
+ | or could not be found. | ||
+ | */ | ||
+ | this.removeObjectFromScene = function(obj) | ||
+ | { | ||
+ | // Check to see if we were passed a correct Camera class | ||
+ | if (obj instanceof Model || | ||
+ | obj instanceof Primitive || | ||
+ | obj instanceof Cube ) | ||
+ | { | ||
+ | // Check against each item in the list | ||
+ | for (var i = 0; i < objList.length; i++) | ||
+ | { | ||
+ | if (objList[i] == obj) | ||
+ | { | ||
+ | // Remove the item | ||
+ | objList.splice(i, 1); | ||
+ | |||
+ | return true; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | logWarning('Scene::removeObjectFromScene() called with a parameter that\'s not a Model or Primitive'); | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Start scene sets a default ambient light to white with full | ||
+ | intensity. If this ambient lighting is not desired, call | ||
+ | setAmbientLight(..) after this method, which will undo the | ||
+ | default ambient light values. | ||
+ | */ | ||
+ | this.startScene = function() | ||
+ | { | ||
+ | var timeDate = new Date(); | ||
+ | var lastTimeTaken = timeDate.getMilliseconds(); | ||
+ | totalFrameCount = 0; | ||
+ | |||
+ | // Safety Checks | ||
+ | if (glCanvas3D == null) return false; | ||
+ | if (renderer == null) return false; | ||
+ | if (camera == null) return false; | ||
+ | |||
+ | // Start the timer | ||
+ | lastTimeTaken = Date.now(); | ||
+ | this.render(); | ||
+ | |||
+ | // Benchmark hook: | ||
+ | if (typeof(benchmarkSetupDone) == "function") benchmarkSetupDone(); | ||
+ | |||
+ | // Create a timer for this object | ||
+ | timerID = setInterval(this.render, 25); | ||
+ | |||
+ | this.setAmbientLight(new Array(ambientLight[0], ambientLight[1],ambientLight[2],ambientLight[3])); | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Render Loop | ||
+ | */ | ||
+ | this.render = function() | ||
+ | { | ||
+ | // If a user wants to stop rendering, this is where it happens | ||
+ | if (exitFlag) | ||
+ | { | ||
+ | timerID = clearInterval(timerID); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Performance logging | ||
+ | if (0) | ||
+ | { | ||
+ | var currentTime = new Date(); | ||
+ | |||
+ | if (currentTime - lastTimePrinted >= 1000) | ||
+ | { | ||
+ | logInfo(frameCount + ' FPS'); | ||
+ | lastTimePrinted = currentTime; | ||
+ | |||
+ | frameCount = 0; | ||
+ | } | ||
+ | |||
+ | frameCount++; | ||
+ | } | ||
+ | |||
+ | var cameraUpdated; | ||
+ | var objectsUpdated; | ||
+ | |||
+ | // Update the Camera (If there is animation) | ||
+ | cameraUpdated = camera.update(Date.now() - lastTimeTaken); | ||
+ | |||
+ | // Update the objects | ||
+ | objectsUpdated = thisScn.updateObjects(Date.now() - lastTimeTaken, camera); | ||
+ | lastTimeTaken = Date.now(); | ||
+ | |||
+ | if (cameraUpdated || objectsUpdated || !drewOnce) | ||
+ | { | ||
+ | // Set the clearColor which is the color clear() uses when its called | ||
+ | glCanvas3D.clearColor(backgroundColor[0],backgroundColor[1],backgroundColor[2],backgroundColor[3]); | ||
+ | |||
+ | // clear the depth buffer, needs to be done for every | ||
+ | // frame since objects may be moved every frame | ||
+ | glCanvas3D.clear(glCanvas3D.COLOR_BUFFER_BIT | glCanvas3D.DEPTH_BUFFER_BIT); | ||
+ | |||
+ | // Set the camera in world space | ||
+ | camera.applyToWorld(glCanvas3D, thisScn); | ||
+ | |||
+ | // Do Rendering | ||
+ | thisScn.renderObjects(glCanvas3D); | ||
+ | |||
+ | // Swap buffers to render | ||
+ | glCanvas3D.swapBuffers(); | ||
+ | |||
+ | // textures need time to load and if the scene is static, | ||
+ | // then the scene is likely to render then load the textures | ||
+ | // leaving the objects without their textures. For now, draw | ||
+ | // the scene regardless if objects or the camera moves or not. | ||
+ | //drewOnce = true; | ||
+ | } | ||
+ | totalFrameCount++; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Updates all objects based on time. | ||
+ | |||
+ | @param {float} timeElapsed | ||
+ | |||
+ | @returns {boolean} True if something updated. | ||
+ | */ | ||
+ | this.updateObjects = function(timeElapsed, camera) | ||
+ | { | ||
+ | var updatedSomething = false; | ||
+ | |||
+ | // Call the User's update callback | ||
+ | if (updateHandler != null) | ||
+ | { | ||
+ | updateHandler(timeElapsed); | ||
+ | } | ||
+ | |||
+ | // Update the rest of the objects individually | ||
+ | for (var i = 0; i < objList.length; i++) | ||
+ | { | ||
+ | if (objList[i].update(timeElapsed, camera)) | ||
+ | updatedSomething = true; | ||
+ | } | ||
+ | |||
+ | // update the SkyModel | ||
+ | if(skyModel) | ||
+ | { | ||
+ | // scale it, rotate it, translate it, whatever. | ||
+ | if(skyModel.update(timeElapsed)) | ||
+ | { | ||
+ | updatedSomething = true; | ||
+ | } | ||
+ | |||
+ | // but in the end, move it so the camera is at its center. | ||
+ | // Let the user scale it and rotate it if they wish. | ||
+ | skyModel.setPosition(camera.getPosition()); | ||
+ | } | ||
+ | |||
+ | return updatedSomething; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Renders all objects to the screen. | ||
+ | */ | ||
+ | this.renderObjects = function() | ||
+ | { | ||
+ | // draw the skyModel if there is one. | ||
+ | if(skyModel) | ||
+ | { | ||
+ | // We need to be able to draw the SkyModel, but without occluding any | ||
+ | // objects in the scene. If the SkyModel is too small, it will occlude | ||
+ | // other objects. To prevent this, we turn off the depth buffer, that | ||
+ | // way ANY object draw will just be drawn ontop of the SkyModel. | ||
+ | glCanvas3D.disable(glCanvas3D.DEPTH_TEST); | ||
+ | |||
+ | // now turn off the lights since the 'skybox' is prelit | ||
+ | // and lights illuminating it would really make no sense. | ||
+ | // But we first have to save the light states so we turn | ||
+ | // on the right ones after the 'skybox' is rendered. | ||
+ | // to do.... | ||
+ | |||
+ | // Since we are drawing the 'inside' of a Model in this case, we have | ||
+ | // to cull faces pointing away from the camera. | ||
+ | glCanvas3D.enable(glCanvas3D.CULL_FACE); | ||
+ | glCanvas3D.frontFace(glCanvas3D.CW); | ||
+ | |||
+ | skyModel.render(glCanvas3D, this); | ||
+ | |||
+ | // turn the depth buffer back on. | ||
+ | glCanvas3D.enable(glCanvas3D.DEPTH_TEST); | ||
+ | |||
+ | // turn the lights that were on, back on. | ||
+ | // to do.... | ||
+ | } | ||
+ | |||
+ | // don't draw the insides of objects. | ||
+ | glCanvas3D.enable(glCanvas3D.CULL_FACE); | ||
+ | glCanvas3D.frontFace(glCanvas3D.CCW); | ||
+ | |||
+ | // Render each object separately | ||
+ | for (var i = 0; i < objList.length; i++) | ||
+ | { | ||
+ | objList[i].render(glCanvas3D, this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Flags the main loop for exit. | ||
+ | */ | ||
+ | this.stopScene = function() | ||
+ | { | ||
+ | // This flags the main loop to exit gracefully | ||
+ | exitFlag = true; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | Loads images before they are actually used. If a Model is created | ||
+ | later in the life of the script with a texture which has not yet | ||
+ | been loaded, the Model will be drawn without a texture until the | ||
+ | texture is loaded. This function prevents this from happening. | ||
+ | Alternatively, the textureManager can be acquired from the Scene | ||
+ | and multiple calls to addTexture() can be called. This method simply | ||
+ | serves as a convenience. | ||
+ | |||
+ | <p><b>This must be called after Scene's init()</b></p> | ||
+ | |||
+ | @param {string[]} imagePaths An array of paths of the images | ||
+ | relative to the html file which holds the main script. | ||
+ | */ | ||
+ | this.preloadImages = function(imagePaths) | ||
+ | { | ||
+ | if(textureManager) | ||
+ | { | ||
+ | for(var i=0; i < imagePaths.length; i++) | ||
+ | { | ||
+ | textureManager.addTexture(imagePaths[i]); | ||
+ | } | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | logError("preloadImage() must be called after Scene's init()"); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
== 虛擬世界的物件 == | == 虛擬世界的物件 == |
於 2008年10月9日 (四) 13:23 的修訂
簡介
Canvas 3D JS Library (C3DL) 是 javascript 函式庫,也就是裡頭提供的全部是 Javascript, 必須安裝 Canvas3D extension of firefox, c3dl 程式碼在此。主要目的是讓你在 Firefox/Mozilla 平台用 Canvas/OpenGL 的方式撰寫 3D 的網路應用。
C3DL 提供一系列的數學、景觀、及3D物件類別,讓你在用 Canvas 會更有彈性,當然主要就是要縮短開發時間。
本專案開發人員
- Catherine Leung
- Mark Paruzel (CodeBot)
- Andrew Smith
- Chris Bishop (Javascript)
- Andor Salga
有用的連結
- 我們的網站
- 我們的 blog
- Our SVN Repo: svn://cdot.senecac.on.ca/canvas3d
- 在 Vlad's hg repo 中的 canvas3d 源碼
- OpenGL 文件
- Nehe 的 OpenGL 教學
- .off (object file format) format info
類別繼承圖
數學運算
我的感覺是這份 c3dl 並不是非常有效率,可以稍微修正寫法,譬如參考 paperVision3D
Vector 向量類別
一個向量基本上就是在 3D 世界的 X, Y, Z 三個軸的座標系統描述一個「具備大小的方向」。3D 數學存在各種不同的座標系統,離開向量類別的封裝則不復存在所謂的 3D。向量類別具有下列的成員;
- isValidVector(vecArr) - 判斷參數是否為一有效的向量
- copyVector(srcVec) - 從 srcVec 複製並傳回
- copyVectorContents(srcVec, destVec) - 效果相當於(不等於,因為有多作判斷是否為有效向量) destVec = copyVector(srcVec);
- makeVector(newX, newY, newZ) - copyVector() 就是用這個函數實作向量複製
- normalizeVector(vec) - 計算與向量相同方向但長度為一的「單位向量」並傳回
- vectorDotProduct(vecOne, vecTwo) - 傳回兩向量的內積(點積),其值為純量
- vectorCrossProduct(vecOne, vecTwo, dest) - 將 vecOne 與 vecTwo 兩個向量做外積(叉積)後指定給 dest)
- vectorLength(vec) - 計算並傳回向量的長度(相當於與自己的內積開根號)
- vectorLengthSq(vec) - 計算向量長度的平方,相比於直接利用長度的運算上,少了一個根號後再平方的無用計算
- addVectors(vecOne, vecTwo, dest) - 相當於 dest = vecOne + vecTwo
- subtractVectors(vecOne, vecTwo, dest) - 相當於 dest = vecOne - vecTwo
- multiplyVector(vec, scalar, dest) - 相當於 dest = vec * scalar; 效果相當於將向量放大(scalar 小於1的正數則為縮小,負數則為反向)
- divideVector(vec, scalar, dest) - 相當於 dest = vec / scalar; 效果相當於將向量縮小(scalar 小於1的正數則為放大,負數則為反向)
- multiplyVectorByVector(vecOne, vecTwo, dest) - 既非內積也非外積,而是相當於 dest = [X1*X2, Y1*Y2, Z1*Z2]; 的乘法
- isVectorEqual(vecOne, vecTwo) - 判斷兩向量是否相等
- isVectorZero(vec) - 判斷向量長度是否為 0,正確的說法: 判斷是否極接近 0,每個軸向誤差在 0.00001 以內
- getAngleBetweenVectors(vecOne, vecTwo) - 計算兩向量間的夾角
Matrix 矩陣類別
c3dl 的矩陣在數學上是一個 4x4 的二維矩陣,但是在 Javascript 實作上是用一維陣列來表達,而不是二維陣列,其索引值如下:
+- -+ | 0, 4, 8, 12 | | 1, 5, 9, 13 | | 2, 6, 10, 14 | | 3, 7, 11, 15 | +- -+
- isValidMatrix(mat) - 判斷是否為有效的矩陣
- makeIdentityMatrix() - 產生單位矩陣,也就是斜對角都為 1 其餘為 0 的矩陣
- makeZeroMatrix() - 產生全部是0 的矩陣
- makeMatrix(e00, e01, e02, e03, e10, e11, e12, e13, e20, e21, e22, e23, e30, e31, e32, e33) - 利用參數產生矩陣,其索引值順序如上述,或由此處的宣告亦可得知
- matricesEqual(matrix1, matrix2) - 判斷兩矩陣是否相等
- makePoseMatrix(vecLeft, vecUp, vecFrwd, vecPos) - 位置矩陣,通常用來處理「手勢」,效果如下:
+- -+ | Left.x, Up.x, Fwd.x, Pos.x | | Left.y, Up.y, Fwd.y, Pos.y | | Left.z, Up.z, Fwd.z, Pos.z | | 0.0, 0.0, 0.0, 1.0 | +- -+
- transposeMatrix(mat) - 傳回轉置矩陣: 型如
+- -+ | A, B, C, D | | E, F, G, H | | I, J, K, L | | M, N, O, P | +- -+
轉置結果為:
+- -+ | A, E, I, M | | B, F, J, N | | C, G, K, O | | D, H, L, P | +- -+
- inverseMatrix(mat) - 計算並傳回 mat 的反矩陣,使得 res * mat = I, 其中 I 是單位矩陣,參考 反矩陣
- matrixDeterminant(mat) - 計算並傳回 mat 的行列式,通常用來算面積或體積,其值為純量,參考 行列式
- matrixAdjoint(mat) - 不知道怎麼翻,伴隨矩陣?共軛矩陣?參考 adjoint matrix
- multiplyMatrixByScalar(mat, scalar) - 每個元素都乘以 scalar
- divideMatrixByScalar(mat, scalar) - 每個元素都除以 scalar
- multiplyMatrixByMatrix(matOne, matTwo) - 矩陣乘法,結果亦為矩陣
- multiplyMatrixByVector(mat, vec) - 將矩陣乘以向量,通常用來作向量的旋轉之類用途,結果亦為向量(其實是 4x4 矩陣與 4x1 矩陣相乘)
- addMatrices(matOne, matTwo) - 兩矩陣相對應元素相加
- subtractMatrices(matOne, matTwo) - 兩矩陣相對應元素相減
Quaternion 四元數
四元數顧名思義就是四個元素的數,請參考 四元數
- isValidQuat(quat) - 傳回是否為有效的四元數
- makeQuat(newW, newX, newY, newZ) - 傳回四元數 Quat = W + X * i + Y * j + Z * k, 其中 i, j, k 是虛部
- quatToMatrix(quat) - 將四元數以矩陣來表示,其轉換如下:
+ ------------ ---------- ---------- --- + 1 - 2(YY+ZZ) 2(XY+WZ) 2(XZ-WY) 0 2(XY-WZ) 1-2(XX+ZZ) 2(YZ+WX) 0 2(XZ+WY) 2(YZ-WX) 1-2(XX+YY) 0 0 0 0 1 + ------------ ---------- ---------- --- +
- quatToAxisAngle(axisVec, angleScalar) - 用來將四元素的旋轉變成軸角的旋轉,不過似乎有 bug
- axisAngleToQuat(axisVec, angleScalar) - 轉換軸角的旋轉為四元數的旋轉
- matrixToQuat(newMat) - 將矩陣轉為四元數
- quatLengthSq(quat) - 四元數的長度平方,等於 XX+YY+ZZ
- quatLength(quat) - 四元數的長度 sqrt(XX+YY+ZZ)
- addQuats(quatOne, quatTwo) - 兩個四元數相加
- subtractQuats(quatOne, quatTwo) - 兩個四元數相減
- multiplyQuatByScalar(quatOne, scalar) - 兩個四元數相減
- getQuatConjugate(quat) - 四元數的共軛四元數(虛部分別為其負值)
- quatDotProduct(quatOne, quatTwo) - 四元數的內積,其值為純量,但是並不等於 3D 意義中的長度
- normalizeQuat(quat) - 四元數的正規化
- inverseQuat(quat) - 反四元數
Camera 相機
Camera 有分 ChaseCamera, FixedCamera, FreeCamera, PanCamera,就讓我們一個一個來看吧
ChaseCamera
尚未實作,空的
FixedCamera
嗚嗚,似乎也是未實作,看 code 似乎也沒人用到
- 屬性
- globalPos - 相機位置,向量
- globalOri - 相機方向,矩陣
- nearClipping - 預設值 1.0
- farClipping - 預設值 500
- fieldView - 預設值 60
- aspectRatio - 預設值 0
- 方法
- setGlobalPos(posVec) - 未實作
- setGlobalOri(oriMat) - 未實作
PanCamera
尚未實作,空的
FreeCamera
- 方法: 讀值
- getPosition() - 傳回位置向量
- getUp() - 傳回相機的 up vector(老實說我不知道這是啥或是要幹嘛)
- getDir() - 傳回相機瞧的方向 vector
- getLeft() - 傳回相機的 left vector(老實說我不知道這是啥或是要幹嘛)
- getLinearVel() - 註解說是 Animation of positions,其值是向量
- getAngularVel() - Animations of rotation around (side Vector, up Vector, dir Vector)
- 方法: 設定
- setPosition(newVec) - 設定相機位置
- setLookAtPoint(newVec) - 設定相機看的點,必須移(應該是沒有轉)動相機(其實是轉動所有物件)
- setUpVector(newVec) - 設定 up vector
- setLinearVel(newVec) - 設定相機的旋轉速度?,其值是向量
- setAngularVel(newVec) - 設定相機轉動軸?
- 其他功能
- rotateOnAxis(axis, angle) - 讓相機沿著某個向量軸 axis 繞行 angle 角度
- yaw(angle) - 讓相機沿著其 Up vector 轉動 angle 角度
- roll(angle) - 讓相機沿著其方向向量轉動 angle 角度
- pitch(angle) - 讓相機沿著其 left vector 轉動 angle 角度
- update(timeElapsed) - 這個函數跟設定旋轉速度有關,我猜是供內部呼叫
- applyToWorld(glCanvas3D, scene) - 應該是「畫」出來的意思
觀景窗 Scene
/* Copyright (c) 2008 Seneca College Licenced under the MIT License (http://www.c3dl.org/index.php/mit-license/) */ /** @class A Scene should be thought of as a scene on a movie set. A scene would typically contain objects which are moving and a current camera, lights, etc. */ function Scene() { // Engine Variables var glCanvas3D = null; // OpenGL Context (Canvas) var renderer = null; // GLES 1.1 or 2.0 var camera = null; // Reference to a Camera type // A reference to a model which will actually act as a // SkyBox, except any Model can be used, not just a box. var skyModel = null; var objList = new Array(); // An array of Objects to draw var exitFlag = false; // Exits the mail loop var cvs = null; // Canvas Tag var canvasHeight = 0; var canvasWidth = 0; var canvas2Dlist = new Array(); // Input Handler Variables var kybdHandler = null; var mouseHandler = null; var updateHandler = null; // Performance Variables var lastTimeTaken = Date.now(); var timerID = 0; var lastTimePrinted = Date.now(); // performance logging var frameCount = 0; var drewOnce = false; var totalFrameCount = 0; var backgroundColor = new Array(0.4, 0.4, 0.6, 1.0); var ambientLight = new Array(1,1,1,1); // Shaders this.sp = null; var textureManager = null; var thisScn = null; // ------------------------------------------------------- // Getters /** Get the camera of the scene. @returns {Camera} The camera of the scene. */ this.getCamera = function() { return camera; } /** Get the number of objects in the scene. @returns {int} The number of objects in the Object list. */ this.getObjListSize = function(){ return objList.length; } /** Get the context. @returns {Context} */ this.getGL = function(){ return glCanvas3D; } /** */ this.getTotalFrameCount = function(){ return totalFrameCount; } /** Get the scene's Renderer */ this.getRenderer = function(){ return renderer; } /** Get the TextureManager of the scene. One of the primary responsibilities of a TextureManager is to ensure textures are only loaded once. @returns {TextureManager} The TextureManager of the scene. */ this.getTextureManager = function(){ return textureManager; } /** Get the Scene. @returns {Scene} */ this.getScene = function(){ return thisScn; } /** Get the SkyModel. @returns {Model} The Scene's SkyModel. */ this.getSkyModel = function() { return skyModel; } /** Get the ambient light of the scene. @returns {Array} An Array of four values. */ this.getAmbientLight = function() { return new Array(ambientLight[0]/5,ambientLight[1]/5,ambientLight[2]/5,ambientLight[3]); } /** Get a reference of a particular object in the scene. @param indxNum The index number of the object. @return the reference to the object at index number indxNum or null if indxNum was out of bounds. */ this.getObj = function(indxNum) { if (isNaN(indxNum)) { logWarning('Scene::getObj() called with a parameter that\'s not a number'); return null; } // Check if the index that was asked for is inside the bounds of our array if (indxNum < 0 || indxNum >= objList.length) { logWarning('Scene::getObj() called with ' + indxNum +', which is not betwen 0 and ' + objList.length); return null; } // We do this because we dont want outsiders modifying the object list, // just the object themselves (ie. changing position, orientation, etc) return objList[indxNum]; } // ------------------------------------------------------- // Setters /** Set the functions to call when a key is pressed or released. @param {function} keyUpCB The callback function for the up key. @param {function} keyDownCD The callback function for the down key. */ this.setKeyboardCallback = function(keyUpCB, keyDownCB) { if (cvs) { // Register True keyboard listeners if (keyUpCB != null) document.addEventListener("keyup", keyUpCB, false); if (keyDownCB != null) document.addEventListener("keydown", keyDownCB, false); } } /** Pass in the functions to call when mouse event occur such as when a button is pressed, released or the mousewheel is scrolled. The scene will call these functions when the event occur. @param {function} mouseUpCB @param {function} mouseDownCB @param {function} mouseMoveCB @param {function} mouseScrollCB */ this.setMouseCallback = function(mouseUpCB, mouseDownCB, mouseMoveCB, mouseScrollCB) { if (cvs) { // Register all Mouse listeners if (mouseMoveCB != null) cvs.addEventListener("mousemove", mouseMoveCB, false); if (mouseUpCB != null) cvs.addEventListener("mouseup", mouseUpCB, false); if (mouseDownCB != null) cvs.addEventListener("mousedown", mouseDownCB, false); if (mouseScrollCB != null) cvs.addEventListener("DOMMouseScroll", mouseScrollCB, false); } } /** Set the SkyModel. A SkyModel acts like a skybox, when the camera moves in the scene the SkyModel maintains the same distance from the camera. This creates the illusion that there are parts to the scene that are very far away which cannot be reached. Applications of this would include creating clouds, or mountain ranges, starfields, etc. Any Model can be passed in and is not restricted to a Cube. Whatever model that is appropirate should be used. @param {Model} sky A Model which will maintain the same distance from the Scene's camera. */ this.setSkyModel = function(sky) { if( sky instanceof Model) { skyModel = sky; } } /** Set the function to call everytime the scene is updated. @param {function} updateCB The function to call everytime the scene is updated. */ this.setUpdateCallback = function(updateCB) { if (cvs) { if (updateCB != null) updateHandler = updateCB; } } /** Set the renderer. Currently only 2 renderers are supported: OpemGLES11 and OpenGLES20. @param {OpenGLES11|OpenGLES20} */ this.setRenderer = function(renderType) { // Set the type of renderer to use if (renderType instanceof OpenGLES11 || renderType instanceof OpenGLES20) { renderer = renderType; } } /** @param canvasTag The name of the canvas, that is the value which is assigned to the id property of the canvas tag in the html file. */ this.setCanvasTag = function(canvasTag) { // Get the Canvas tag cvs = document.getElementById(canvasTag); if (cvs == null) { logWarning('Scene::createScene() No canvas tag with name ' + canvasTag + ' was found.'); } } /** Set the Scene's camera. @param {Camera} cam The camera. */ this.setCamera = function(cam) { // Check to see if we were passed a correct Camera class if (cam instanceof ChaseCamera || cam instanceof FreeCamera || cam instanceof FixedCamera || cam instanceof PanCamera) { camera = cam; return true; } logWarning('Scene::setCamera() invalid type of camera.'); return false; } /** This one just calls addTextToModel() //!! need dest as a parameter, probably in pixels, where to put the text */ this.addFloatingText = function(text, fontStyle, fontColour, backgroundColour) { var box = this.addTextToModel(null, text, fontStyle, fontColour, backgroundColour); box.stayInFrontOfCamera = true; this.addObjectToScene(box); } /** Create a 2D canvas, render the text into it, and use that as a texture for model. If model is null, create a rectangle and stick the text onto it. */ this.addTextToModel = function(model, text, fontStyle, fontColour, backgroundColour) { // Create a SPAN element with the string and style matching what the user asked // for the floating text. var tempSpan = document.createElement('span'); var tempSpanStyle = document.createElement('style'); var tempSpanStyleContent = document.createTextNode('span{' + 'font: ' + fontStyle + ';' + 'color: ' + fontColour + '; ' + 'background: ' + backgroundColour + ';}'); var tempText = document.createTextNode(text); tempSpanStyle.appendChild(tempSpanStyleContent); tempSpan.appendChild(tempSpanStyle); tempSpan.appendChild(tempText); // Append it to the body so it's momentarily displayed. I couldn't find a way to measure // the text box's size without displaying it. document.body.appendChild(tempSpan); var actualStringWidth = tempSpan.offsetWidth; var actualStringHeight = tempSpan.offsetHeight; var stringWidth = roundUpToNextPowerOfTwo(tempSpan.offsetWidth); var stringHeight = roundUpToNextPowerOfTwo(tempSpan.offsetHeight); // Now get rid of that element, we only needed it to measure it tempSpan.removeChild(tempSpanStyle); document.body.removeChild(tempSpan); var box; if (model == null) { var whRatio = stringWidth / stringHeight; // Model for the plane with the text, size based on whRatio var smallCanvasVertices = [ [-1.0 * (whRatio / 2), -1.0, 0.0], // 0 - bottom left [-1.0 * (whRatio / 2), 1.0, 0.0], // 1 - top left [ 1.0 * (whRatio / 2), 1.0, 0.0], // 2 - top right [ 1.0 * (whRatio / 2), -1.0, 0.0], // 3 - bottom right ]; var smallCanvasNormals = [ [0,0,-1] ]; var smallCanvasUVs = [ [0.0,1.0], // 0 - bottom left [0.0,0.0], // 1 - top left [1.0,0.0], // 2 - top right [1.0,1.0] // 3 - bottom right ]; var smallCanvasFaces = [ [0,0,0], [3,3,0], [2,2,0], [0,0,0], [2,2,0], [1,1,0] ]; box = new Model(); box.init(smallCanvasVertices, smallCanvasNormals, smallCanvasUVs, smallCanvasFaces); //box.setAngularVel(new Array(0.003, 0.000, 0.000)); //box.pitch(-0.4); //!! need something user-specified box.setPosition(new Array(5, 0, 5)); } else box = model; // Draw the text into the 2D canvas and use it for the above model's texture var textureCanvas = this.create2Dcanvas(stringWidth, stringHeight); if (textureCanvas.getContext) { var ctx = textureCanvas.getContext('2d'); if (fontStyle) ctx.mozTextStyle = fontStyle; // Fill everything with backgroundColour if it's specified if (backgroundColour) { ctx.fillStyle = backgroundColour; ctx.fillRect(0, 0, stringWidth, stringHeight); } // Center the text in the 2D canvas ctx.translate((stringWidth - actualStringWidth) / 2, stringHeight - (stringHeight - actualStringHeight)); if (fontColour) ctx.fillStyle = fontColour; else ctx.fillStyle = 'black'; ctx.mozDrawText(text); box.setTextureFromCanvas2D(textureCanvas.id); textureManager.addTextureFromCanvas2D(textureCanvas.id); } else logWarning("addFloatingText(): call to create2Dcanvas() failed"); return box; } /** Create a 2D canvas for drawing text and other stuff. Keep a reference to it. @return {CanvasTag} */ this.create2Dcanvas = function(width, height) { var newCanvas = document.createElement('canvas'); newCanvas.id = 'changemetorandomstring'; newCanvas.width = width; newCanvas.height = height; cvs.appendChild(newCanvas); canvas2Dlist.push(newCanvas); return newCanvas; } /** Set the color of the background. Values are clamped to the range [0,1]. @param {Array} bgColor Four values in the order: red, green, blue, alpha/intensity. */ this.setBackgroundColor = function(bgColor) { if( bgColor.length == 4 ) { for(var i=0; i < 4; i++) { backgroundColor[i] = bgColor[i]; } } } /** Set the ambient light of the scene. @param {Array} light An array of 4 floating point values ranging from 0 to 1. */ this.setAmbientLight = function(light) { if( light.length == 4 ) { // convert the values passed in which range from 0 to 1 to a range of // 0 to 5, which OpenGL wants. ambientLight = new Array(light[0]*5, light[1]*5, light[2]* 5, 1); // set the ambient light for the scene // note the values passed to lightModel are not clamped // also only available in 1.1 if( renderer instanceof OpenGLES11 && glCanvas3D != null) { glCanvas3D.lightModel(glCanvas3D.LIGHT_MODEL_AMBIENT, ambientLight); } } } /** Acquire the OpenGL Context @param {string} name @returns {boolean} */ this.init = function(name) { if (renderer != null && cvs != null) { // Initialize the renderer if (!renderer.createRenderer(cvs)) { logWarning('Scene::createScene() Renderer failed to initialize.'); return false; } // Get the Canvas glCanvas3D = renderer.getGLContext(); // Set our global (fake static variable) to be used in rendering thisScn = this; textureManager = new TextureManager(glCanvas3D); // Get the size of the Canvas Space for Aspect Ratio calculation canvasWidth = cvs.width; canvasHeight = cvs.height; // Initialize the renderer return renderer.init(canvasWidth, canvasHeight); } logWarning('Scene::createScene() No renderer was specified.'); return false; } /** Add the object 'obj' to the scene. Currently only objects which are Models, Primitives or Cubes can be added to the scene. @param {Model|Primitive|Cube} obj A reference to an object, such as a Model or a Cube. @return {boolean} True if the object was added to the scene, false otherwise. */ this.addObjectToScene = function(obj) { // Check to see if we were passed a correct Camera class if (obj instanceof Model || obj instanceof Primitive || obj instanceof Cube) { objList.push(obj); return true; } logWarning('Scene::addObjectToScene() called with a parameter that\'s not a Model, Primitive, or Cube'); return false; } /** Remove an object from the scene. This is an O(n) operation. @param {Model|Primitive|Cube} obj The object to remove from the scene. @return {boolean} True if the object was found and removed from the scene and false if the obj argument was not a type of Model, Primitive or Cube or could not be found. */ this.removeObjectFromScene = function(obj) { // Check to see if we were passed a correct Camera class if (obj instanceof Model || obj instanceof Primitive || obj instanceof Cube ) { // Check against each item in the list for (var i = 0; i < objList.length; i++) { if (objList[i] == obj) { // Remove the item objList.splice(i, 1); return true; } } } logWarning('Scene::removeObjectFromScene() called with a parameter that\'s not a Model or Primitive'); return false; } /** Start scene sets a default ambient light to white with full intensity. If this ambient lighting is not desired, call setAmbientLight(..) after this method, which will undo the default ambient light values. */ this.startScene = function() { var timeDate = new Date(); var lastTimeTaken = timeDate.getMilliseconds(); totalFrameCount = 0; // Safety Checks if (glCanvas3D == null) return false; if (renderer == null) return false; if (camera == null) return false; // Start the timer lastTimeTaken = Date.now(); this.render(); // Benchmark hook: if (typeof(benchmarkSetupDone) == "function") benchmarkSetupDone(); // Create a timer for this object timerID = setInterval(this.render, 25); this.setAmbientLight(new Array(ambientLight[0], ambientLight[1],ambientLight[2],ambientLight[3])); } /** Render Loop */ this.render = function() { // If a user wants to stop rendering, this is where it happens if (exitFlag) { timerID = clearInterval(timerID); return; } // Performance logging if (0) { var currentTime = new Date(); if (currentTime - lastTimePrinted >= 1000) { logInfo(frameCount + ' FPS'); lastTimePrinted = currentTime; frameCount = 0; } frameCount++; } var cameraUpdated; var objectsUpdated; // Update the Camera (If there is animation) cameraUpdated = camera.update(Date.now() - lastTimeTaken); // Update the objects objectsUpdated = thisScn.updateObjects(Date.now() - lastTimeTaken, camera); lastTimeTaken = Date.now(); if (cameraUpdated || objectsUpdated || !drewOnce) { // Set the clearColor which is the color clear() uses when its called glCanvas3D.clearColor(backgroundColor[0],backgroundColor[1],backgroundColor[2],backgroundColor[3]); // clear the depth buffer, needs to be done for every // frame since objects may be moved every frame glCanvas3D.clear(glCanvas3D.COLOR_BUFFER_BIT | glCanvas3D.DEPTH_BUFFER_BIT); // Set the camera in world space camera.applyToWorld(glCanvas3D, thisScn); // Do Rendering thisScn.renderObjects(glCanvas3D); // Swap buffers to render glCanvas3D.swapBuffers(); // textures need time to load and if the scene is static, // then the scene is likely to render then load the textures // leaving the objects without their textures. For now, draw // the scene regardless if objects or the camera moves or not. //drewOnce = true; } totalFrameCount++; } /** Updates all objects based on time. @param {float} timeElapsed @returns {boolean} True if something updated. */ this.updateObjects = function(timeElapsed, camera) { var updatedSomething = false; // Call the User's update callback if (updateHandler != null) { updateHandler(timeElapsed); } // Update the rest of the objects individually for (var i = 0; i < objList.length; i++) { if (objList[i].update(timeElapsed, camera)) updatedSomething = true; } // update the SkyModel if(skyModel) { // scale it, rotate it, translate it, whatever. if(skyModel.update(timeElapsed)) { updatedSomething = true; } // but in the end, move it so the camera is at its center. // Let the user scale it and rotate it if they wish. skyModel.setPosition(camera.getPosition()); } return updatedSomething; } /** Renders all objects to the screen. */ this.renderObjects = function() { // draw the skyModel if there is one. if(skyModel) { // We need to be able to draw the SkyModel, but without occluding any // objects in the scene. If the SkyModel is too small, it will occlude // other objects. To prevent this, we turn off the depth buffer, that // way ANY object draw will just be drawn ontop of the SkyModel. glCanvas3D.disable(glCanvas3D.DEPTH_TEST); // now turn off the lights since the 'skybox' is prelit // and lights illuminating it would really make no sense. // But we first have to save the light states so we turn // on the right ones after the 'skybox' is rendered. // to do.... // Since we are drawing the 'inside' of a Model in this case, we have // to cull faces pointing away from the camera. glCanvas3D.enable(glCanvas3D.CULL_FACE); glCanvas3D.frontFace(glCanvas3D.CW); skyModel.render(glCanvas3D, this); // turn the depth buffer back on. glCanvas3D.enable(glCanvas3D.DEPTH_TEST); // turn the lights that were on, back on. // to do.... } // don't draw the insides of objects. glCanvas3D.enable(glCanvas3D.CULL_FACE); glCanvas3D.frontFace(glCanvas3D.CCW); // Render each object separately for (var i = 0; i < objList.length; i++) { objList[i].render(glCanvas3D, this); } } /** Flags the main loop for exit. */ this.stopScene = function() { // This flags the main loop to exit gracefully exitFlag = true; } /** Loads images before they are actually used. If a Model is created later in the life of the script with a texture which has not yet been loaded, the Model will be drawn without a texture until the texture is loaded. This function prevents this from happening. Alternatively, the textureManager can be acquired from the Scene and multiple calls to addTexture() can be called. This method simply serves as a convenience. <p><b>This must be called after Scene's init()</b></p> @param {string[]} imagePaths An array of paths of the images relative to the html file which holds the main script. */ this.preloadImages = function(imagePaths) { if(textureManager) { for(var i=0; i < imagePaths.length; i++) { textureManager.addTexture(imagePaths[i]); } } else { logError("preloadImage() must be called after Scene's init()"); } } }
虛擬世界的物件
物體
基本物件 Primitive
- 基本屬性
- visible: true - 物件是否可見
- textureName - 紋理,譬如木頭?鐵?
- name - 每個物件都有自己的名字
- 原始位置,所有物件含相機都有 left, up, dir, pos 等資訊
- left: (1, 0, 0) - left vector
- up: (0, 1, 0) - up vector
- dir: (0, 0, 1) - forward vector
- pos: (0, 0, 0) - 位置
- scaleVec: (1, 1, 1) - 放大率,預設就是 1
- 移動資訊
- linVel: (0, 0, 0) - 移動"速度"為 0, 預設是靜止
- angVel: (0, 0, 0) - 轉動方向為 0, 預設是不轉動
- 取值
- getPosition() - 傳回 pos 值
- getUp() - 傳回 up vector
- getDirection() - 傳回 dir vector
- getLeft() - 傳回 left vector
- getLinearVel() - 傳回速度 linVel 向量值
- getAngularVel() - 傳迴旋轉向量 angVel
- isVisible() - 傳回是否可見
- getScale() - 傳回放大率
- getName() - 傳回物件名稱
- getTextureName() - 傳迴紋理名稱
- 設定
- setTexture(imageFilename) - 設定紋理檔案名稱
- setTextureFromCanvas2D(sourceCanvas) - 從 Canvas2D 設定紋理
- unsetTexture() - 重設紋理為空的
- setName(name) - 設定物件名稱
- setVisible(show) - 設定物件是否可見
- scale(scaleVec) - 設定物件放大比例
- setPosition(vecPos) - 設定物件位置 pos 值
- translate(translation) - 將物件放到新位置(相當於 pos+translation)
- setForward(newVec) - 將物件往 newVec 方向移動,這會影響到 up, left 等向量
- setUpVector(newVec) - 設定 up vector 成 newVec 值
- setLinearVel(newVec) - 設定速度值為 newVec 值
- setAngularVel(newVec) - 設定旋轉方向為 newVec 值
- rotateOnAxis(axisVec, angle) - 以四元數及矩陣來計算物件的旋轉, 先移動 dir, 再計算相對應的 left, up 值
- yaw(angle) - 效果是 rotateOnAxis(up, angle)
- roll(angle) - 效果是 rotateOnAxis(dir, angle)
- pitch(angle) - 效果是 rotateOnAxis(left, angle)
- update(timeStep) - 依照速度向量 linVel 移動物體的 pos 位置向量,並調整 up, dir, left 等向量值
- render(glCanvas3D) - 透過 glCanvas3D 這個 plugin 把物件畫出來,這邊會有顏色,只是怪怪的
Model (繼承自 Primitive)
- 屬性
- loadedTexture: false - 是否已載入紋理
- expandedVertices - 似乎要用來放大縮小用的
- expandedNormals
- expandedUVs
- buffers
- firstTimeRender: true - 第一次畫出來
- stayInFrontOfCamera: false - 是否在相機前,應該是與是否可見有關
- 方法
- init(vertices, normals, texcoords, faces) - 必要參數是 vertices, faces。normal是單位大小,或許與燈光有關,texcoords 是紋理座標(不懂),而 faces 則是引索陣列,引用 texcoords 來當其表面紋理
- initDAE(path) - dae是Collada的檔案副檔名,是用xml去儲存3d的模型
- createBuffers(glCanvas3D) - 應該是在根據 expandedVertices 準備畫布,若有設定 expandedUVs, expandedNormals, 也會準備相對應的 Buffer
- update(timeStep, camera) - 會先根據相機移動相對位置後,再根據自己的速度與旋轉來移動自己
- render(glCanvas3D, scene) - 要把物體畫出來,相對上麻煩的多
立方體 Cube
繼承自基本物件 Primitive
轉換矩陣
為了效率考量,定義了幾個轉換用的矩陣:
- cube_transition_Vertices =
[ [-1, -1, 1], // 0 - front, bottom, left [-1, 1, 1], // 1 - front, top, left [ 1, 1, 1], // 2 - front, top, right [ 1, -1, 1], // 3 - front, bottom, right [-1, -1, -1], // 4 - back, bottom, left [-1, 1, -1], // 5 - back, top, left [ 1, 1, -1], // 6 - back, top, right [ 1, -1, -1] // 7 - back, bottom, right ];
- cube_transition_Normals =
[ [-0.57735,-0.57735, 0.57735], // front, bottom, left [-0.57735, 0.57735, 0.57735], // front, top, left [ 0.57735, 0.57735, 0.57735], // front, top, right [ 0.57735,-0.57735, 0.57735], // front, bottom, right [-0.57735,-0.57735, -0.57735], // back, bottom, left [-0.57735, 0.57735, -0.57735], // back, top, left [ 0.57735, 0.57735, -0.57735], // back, top, right [ 0.57735,-0.57735, -0.57735] // back, bottom, right ];
- cube_transition_UVs =
[ [0.0,1.0], // 0 - bottom left [0.0,0.0], // 1 - top left [1.0,0.0], // 2 - top right [1.0,1.0] // 3 - bottom right ];
- cube_transition_Faces =
[ [0,0,0], [3,3,3], [2,2,2], // front [0,0,0], [2,2,2], [1,1,1], [5,2,5], [6,1,6], [7,0,7], // back [5,2,5], [7,0,7], [4,3,4], [4,0,4], [7,3,7], [3,2,3], // bottom [4,0,4], [3,2,3], [0,1,0], [1,0,1], [2,3,2], [6,2,6], // top [1,0,1], [6,2,6], [5,1,5], [4,0,4], [0,3,0], [1,2,1], // left side [4,0,4], [1,2,1], [5,1,5], [3,0,3], [7,3,7], [6,2,6], // right side [3,0,3], [6,2,6], [2,1,2] ];
屬性與方法
- 屬性
- m - 就是 Model 物件
- 其餘均繼承自 Model
- 方法 - 除 scale 外,全部繼承自 Model
- init(), isVisible()
- getPosition(), getUp(), getDirection(), getLeft(), getLinearVel(), getAngularVel(), getScale(), getTextureName(),
- setTexture(imageFilename), setTextureFromCanvas2D(sourceCanvas), unsetTexture(), setVisible(show)
- setPosition(vecPos), setForward(newVec), setUpVector(newVec),
- translate(translation), setLinearVel(newVec), setAngularVel(newVec), rotateOnAxis(axisVec, angle)
- yaw(angle), roll(angle), pitch(angle), update(timeStep), render(glCanvas3D, scene)
- scale(scaleVec, scaleY, scaleZ) - 多了 Y, Z 二個軸向的放大縮小?忽略的話就跟 Model 一樣