Displaying a 3D scene is all good and well. We can even interact with the scene already by zooming and rotating the scene. However, we still feel pretty much like an observer, since up to now we have not been able to manipulate objects within the scene, which is what I will cover in this tutorial.
The new principle in this tutorial is the concept of picking. O3D has a class that, given the coordinates of the mouse, and the details on the world view, will determine a world ray. To imagine a world ray, think of an invisible ray shooting out of your eye into the screen. This is the world ray, and using this, we are able to determine if any objects intersect with it, and then do whichever action we would like to the object.
The first new code is the line to include the picking class, which we will use to find the world ray.
o3djs.require('o3djs.picking');
And a few new global variables are next
var g_mouseX = 0; var g_mouseY = 0; var g_spinningObject = false; var g_pickedInfo; var g_treeInfo;
Next I will look at the updateInfo() function, which updates the g_treeInfo variable, containing the tree of the transform info for the scene.
function updateInfo() { if (!g_treeInfo) { g_treeInfo = o3djs.picking.createTransformInfo(g_3dRoot, null); } g_treeInfo.update(); }
I have updated the mouse listeners in the initialisation function to call mouseDown, mouseUp and mouseMove instead of startDrag, dragging and stopDrag, since we now want the mouse events to do more than just dragging.
The mouseMove function now has been expanded to include the check for a selected object. The currently pressed mouse button is used to differentiate the two actions. Left-clicking (a value of 0 for e.button) will run the code to spin the object we are clicking on, while clicking any other button will rotate the scene as before.
The first thing we do is create a world ray, using the current mouse coordinates. We then update the tree transform info in case any objects have moved since the last update, and then using the pick function using the world ray as a parameter, the tree transform object will return a picked info object, which we assign to a global variable. After this we set the spinning flag to true, so that the object will start rotating. This happens in the render callback function.
function mouseDown(e) { if (e.button == 0) { var worldRay = o3djs.picking.clientPositionToWorldRay( e.x, e.y, g_viewInfo.drawContext, g_client.width, g_client.height); g_treeInfo.update(); g_pickedInfo = g_treeInfo.pick(worldRay); if (g_pickedInfo) { g_spinningObject = true; } } else { g_lastRot = g_thisRot; g_aball.click([e.x, e.y]); g_dragging = true; } }
The render function has grown a little. I have firstly replaced the “Hello World” text with the mouse coordinates, which are more useful to us now.
More importantly though, if the spinning flag is set, it means we need to spin the object clicked on. The g_pickedObject.shapeInfo object contains the information for the actual shape we have clicked on, and so we need to change the transform for this object. We set a variable with a reference to the transform, and then rotate it about the X and Y axis. Lastly we call the function to update the tree transform object
function renderCallback(renderEvent) { g_clock += renderEvent.elapsedTime * g_timeMult; drawText("(" + g_mouseX + "," + g_mouseY + ")"); if (g_spinningObject) { var pickTrans = g_pickedInfo.shapeInfo.parent.transform; pickTrans.rotateX(0.05); pickTrans.rotateY(0.05); } updateInfo(); }
Now, to stop the spinning after we let go the mouse buttong, we need to set the flag to false in the mouseUp event handler.
function mouseUp(e) { g_dragging = false; g_spinningObject = false; }
Now, when you run the application, the object will spin when you left click, and the scene will rotate if you click any other mouse button and move the mouse.
Here is the full listing
o3djs.require('o3djs.util'); o3djs.require('o3djs.math'); o3djs.require('o3djs.rendergraph'); o3djs.require('o3djs.canvas'); o3djs.require('o3djs.quaternions'); o3djs.require('o3djs.event'); o3djs.require('o3djs.arcball'); o3djs.require('o3djs.primitives'); o3djs.require('o3djs.picking'); // Events // Run the init() function once the page has finished loading. // Run the uninit() function when the page has is unloaded. window.onload = init; window.onunload = uninit; // global variables var g_o3dElement; var g_o3d; var g_math; var g_client; var g_pack; var g_clock = 0; var g_timeMult = 1; var g_cubeTransform; var g_textCanvas; var g_paint; var g_canvasLib; var g_3dRoot; var g_hudRoot; var g_viewInfo; var g_hudViewInfo; var g_keyPressDelta = 0.05; var g_quaternions; var g_aball; var g_thisRot; var g_lastRot; var g_dragging = false; var g_lightPosition = [5, 5, 7]; var g_camera = { eye: [0, 0, 10], target: [0, 0, 0] }; var g_mouseX = 0; var g_mouseY = 0; var g_spinningObject = false; var g_pickedInfo; var g_treeInfo; //Event handler for the mousedown event function mouseDown(e) { if (e.button == 0) { var worldRay = o3djs.picking.clientPositionToWorldRay( e.x, e.y, g_viewInfo.drawContext, g_client.width, g_client.height); g_treeInfo.update(); g_pickedInfo = g_treeInfo.pick(worldRay); if (g_pickedInfo) { g_spinningObject = true; } } else { g_lastRot = g_thisRot; g_aball.click([e.x, e.y]); g_dragging = true; } } //Event handler for the mousemove event function mouseMove(e) { g_mouseX = e.x; g_mouseY = e.y; if (g_dragging) { var rotationQuat = g_aball.drag([e.x, e.y]); var rot_mat = g_quaternions.quaternionToRotation(rotationQuat); g_thisRot = g_math.matrix4.mul(g_lastRot, rot_mat); var m = g_3dRoot.localMatrix; g_math.matrix4.setUpper3x3(m, g_thisRot); g_3dRoot.localMatrix = m; } } //Event handler for the mouseup event function mouseUp(e) { g_dragging = false; g_spinningObject = false; } //Even handler for the scroll button function scrollMe(e) { if (e.deltaY) { g_camera.eye = g_math.mulScalarVector((e.deltaY < 0 ? 11 : 13) / 12, g_camera.eye); g_viewInfo.drawContext.view = g_math.matrix4.lookAt(g_camera.eye, g_camera.target, [0, 1, 0]); } } // Updates the transform info for our scene function updateInfo() { if (!g_treeInfo) { g_treeInfo = o3djs.picking.createTransformInfo(g_3dRoot, null); } g_treeInfo.update(); } function drawText(str) { // Clear to completely transparent. g_textCanvas.canvas.clear([0.5, 0.5, 0.5, 0.5]); // Reuse the global paint object var paint = g_paint; paint.color = [1, 1, 1, 1]; paint.textSize = 12; paint.textTypeface = 'Comic Sans MS'; paint.textAlign = g_o3d.CanvasPaint.LEFT; paint.shader = null; g_textCanvas.canvas.drawText(str, 10, 30, paint); g_textCanvas.updateTexture(); } /** * This method gets called every time O3D renders a frame. Here's * where we update the cube's transform to make it spin. * @param {o3d.RenderEvent} renderEvent The render event object that * gives us the elapsed time since the last time a frame was rendered. */ function renderCallback(renderEvent) { g_clock += renderEvent.elapsedTime * g_timeMult; drawText("(" + g_mouseX + "," + g_mouseY + ")"); if (g_spinningObject) { var pickTrans = g_pickedInfo.shapeInfo.parent.transform; pickTrans.rotateX(0.05); pickTrans.rotateY(0.05); } updateInfo(); } /** * Function performing the rotate action in response to a key-press. * Rotates the scene based on key pressed. (w ,s, a, d). Note that the * x,y-axis referenced here are relative to the current view of scene. * @param {keyPressed} The letter pressed, in lower case. * @param {delta} The angle by which the scene should be rotated. * @return true if an action was taken. */ function keyPressedAction(keyPressed, delta) { var actionTaken = false; switch(keyPressed) { case 'a': g_3dRoot.localMatrix = g_math.matrix4.mul(g_3dRoot.localMatrix, g_math.matrix4.rotationY(-delta)); actionTaken = true; break; case 'd': g_3dRoot.localMatrix = g_math.matrix4.mul(g_3dRoot.localMatrix, g_math.matrix4.rotationY(delta)); actionTaken = true; break; case 'w': g_3dRoot.localMatrix = g_math.matrix4.mul(g_3dRoot.localMatrix, g_math.matrix4.rotationX(-delta)); actionTaken = true; break; case 's': g_3dRoot.localMatrix = g_math.matrix4.mul(g_3dRoot.localMatrix, g_math.matrix4.rotationX(delta)); actionTaken = true; break; } return actionTaken; } /** * Callback for the keypress event. * Invokes the action to be performed for the key pressed. * @param {event} keyPress event passed to us by javascript. */ function keyPressedCallback(event) { event = event || window.event; // Ignore accelerator key messages. if (event.metaKey) return; var keyChar =String.fromCharCode(o3djs.event.getEventKeyChar(event)); // Just in case they have capslock on. keyChar = keyChar.toLowerCase(); if (keyPressedAction(keyChar, g_keyPressDelta)) { o3djs.event.cancel(event); } } /** * Creates a phong material based on the given single color. * @param {Array} baseColor An array with 4 entries, the R,G,B, and A components * of a color. * @return {Material} A phong material whose overall pigment is baseColor. */ function createPhongMaterial(baseColor) { // Create a new, empty Material object. var material = g_pack.createObject('Material'); o3djs.effect.attachStandardShader( g_pack, material, g_lightPosition, 'phong'); material.drawList = g_viewInfo.performanceDrawList; // Assign parameters to the phong material. material.getParam('emissive').value = [0, 0, 0, 1]; material.getParam('ambient').value = g_math.mulScalarVector(0.1, baseColor); material.getParam('diffuse').value = g_math.mulScalarVector(0.9, baseColor); material.getParam('specular').value = [.2, .2, .2, 1]; material.getParam('shininess').value = 20; return material; } function createShapes() { var cube = o3djs.primitives.createCube( g_pack, createPhongMaterial([1, 0, 0, 1]), Math.sqrt(2)); // The length of each side of the cube. var sphere = o3djs.primitives.createSphere( g_pack, createPhongMaterial([1, 1, 0, 1]), 1.0, // Radius of the sphere. 30, // Number of meridians. 20); // Number of parallels. var cylinder = o3djs.primitives.createCylinder( g_pack, createPhongMaterial([0, 1, 0, 1]), 0.5, // Radius. 1.5, // Depth. 20, // Number of radial subdivisions. 20); // Number of vertical subdivisions. var plane = o3djs.primitives.createPlane( g_pack, createPhongMaterial([0, 0, 1, 1]), 1, // Width. 1.618, // Depth. 3, // Horizontal subdivisions. 3); // Vertical subdivisions. // Make a polygon to extrude for the prism. var polygon = []; var n = 10; for (var i = 0; i < n; ++i) { var theta = 2.0 * i * Math.PI / n; var radius = (i % 2) ? 1 : 0.382; polygon.push([radius * Math.cos(theta), radius * Math.sin(theta)]); } var prism = o3djs.primitives.createPrism( g_pack, createPhongMaterial([0, 1, 1, 1]), polygon, // The profile polygon to be extruded. 1); // The depth of the extrusion. var disc = o3djs.primitives.createDisc( g_pack, createPhongMaterial([1, 0, 1, 1]), 1, // Radius. 7, // Divisions. 2, // Stacks (optional). 0, // Start Stack (optional). 2); // Stack Power (optional). // Add the shapes to the transforms. var transformTable = [ {shape: cube, translation: [-2, 1, 0]}, {shape: sphere, translation: [0, 1, 0]}, {shape: cylinder, translation: [2, 1, 0]}, {shape: plane, translation: [-2, -1, 0]}, {shape: prism, translation: [0, -1, 0]}, {shape: disc, translation: [2, -1, 0]} ]; for (var i = 0; i < transformTable.length; i++) { var transform = g_pack.createObject('Transform'); transform.addShape(transformTable[i].shape); transform.translate(transformTable[i].translation); transform.parent = g_3dRoot; } } /** * Creates the client area. */ function init() { o3djs.util.makeClients(initStep2); } /** * Initializes O3D. * @param {Array} clientElements Array of o3d object elements. */ function initStep2(clientElements) { // Initializes global variables and libraries. g_o3dElement = clientElements[0]; g_client = g_o3dElement.client; g_o3d = g_o3dElement.o3d; g_math = o3djs.math; g_quaternions = o3djs.quaternions; // Initialize O3D sample libraries. o3djs.base.init(g_o3dElement); // Create a pack to manage the objects created. g_pack = g_client.createPack(); //Create the arcball which is used for the rotation g_aball = o3djs.arcball.create(300, 300); //Initialise rotation matrixes g_lastRot = g_math.matrix4.identity(); g_thisRot = g_math.matrix4.identity(); // Create 2 root transforms, one for the 3d parts and 2d parts. // This is not strictly neccassary but it is helpful. g_3dRoot = g_pack.createObject('Transform'); g_hudRoot = g_pack.createObject('Transform'); // Create the render graph for a view. g_viewInfo = o3djs.rendergraph.createBasicView( g_pack, g_3dRoot, g_client.renderGraphRoot); // Set the background color to black. g_viewInfo.clearBuffer.clearColor = [0, 0, 0, 1]; // Create a second view for the hud. g_hudViewInfo = o3djs.rendergraph.createBasicView( g_pack, g_hudRoot, g_client.renderGraphRoot); // Make sure the hud gets drawn after the 3d stuff g_hudViewInfo.root.priority = g_viewInfo.root.priority + 1; // Turn off clearing the color for the hud since that would erase the // 3d parts but leave clearing the depth and stencil so the HUD is // unaffected by anything done by the 3d parts. g_hudViewInfo.clearBuffer.clearColorFlag = false; // Set up a perspective view g_viewInfo.drawContext.projection = g_math.matrix4.perspective( g_math.degToRad(30), // 30 degree fov. g_client.width / g_client.height, 1, // Near plane. 5000); // Far plane. // Set up our view transformation to look towards the world origin // where the cube is located. g_viewInfo.drawContext.view = g_math.matrix4.lookAt(g_camera.eye, //eye g_camera.target, // target [0, 1, 0]); // up //Set up the 2d orthographic view g_hudViewInfo.drawContext.projection = g_math.matrix4.orthographic( 0 + 0.5, g_client.width + 0.5, g_client.height + 0.5, 0 + 0.5, 0.001, 1000); g_hudViewInfo.drawContext.view = g_math.matrix4.lookAt( [0, 0, 1], // eye [0, 0, 0], // target [0, 1, 0]); // up createShapes(); // Create the global paint object that's used by draw operations. g_paint = g_pack.createObject('CanvasPaint'); // Creates an instance of the canvas utilities library. g_canvasLib = o3djs.canvas.create(g_pack, g_hudRoot, g_hudViewInfo); // Create a canvas that will be used to display the text. g_textCanvas = g_canvasLib.createXYQuad(0, 0, 0, 100, 50, true); // Set our render callback for animation. // This sets a function to be executed every time frame is rendered. g_client.setRenderCallback(renderCallback); //Set up a callback to interpret keypresses window.document.onkeypress = keyPressedCallback; //Set up mouse events o3djs.event.addEventListener(g_o3dElement, 'mousedown', mouseDown); o3djs.event.addEventListener(g_o3dElement, 'mousemove', mouseMove); o3djs.event.addEventListener(g_o3dElement, 'mouseup', mouseUp); o3djs.event.addEventListener(g_o3dElement, 'wheel', scrollMe); } /** * Removes callbacks so they aren't called after the page has unloaded. */ function uninit() { if (g_client) { g_client.cleanup(); } }
Comments