/** Company: Shout3D LLC Project: Shout3D 2.0 Sample Code Class: ExaminePanel Date: September 15, 1999 Description: Class for ExaminePanel (C) Copyright Shout3D LLC. - 1997-2001 - All rights reserved */ package applets; import shout3d.core.*; import shout3d.math.*; import shout3d.*; /** * Shout3D ExaminePanel. This class provides the user with the ability to * examine an item. It is similar * to but not exactly like, VRML "EXAMINE" mode. * * Basic Controls: * -- rot = drag * -- pan = +drag * -- zoom = +drag * * Drag Styles * Mouse motion is interpreted via one of two dragStyles. * The dragStyle may be changed with an applet parameter. * -- "by speed" changes values over time by a velocity. * The velocity is proportional to how far the mouse has been moved * since mouse down. * -- "by offset" changes the value once per mouse-movement. * The value is saved upon mouse-down, and with each move, the new * value is set to be the initial value plus an offset that is * proportional to how far the mouse has been moved since mouse down. * * Rotation Center. * -- by default, the camera rotates about the center of the scene's bounding box. * -- the rotationCenter applet parameter lets you change this center. * * Framing the Scene: * -- The starting distance from the camera to the rotation center is calculated * to include the entire scene within frame limits, within an offset of backupSlack * to allow adjustment. See the backupSlack applet parameter below. * * Cameras & Scenes: * -- Works fine if you bind a new camera or change scenes. * -- resetCamera() method returns to the original setting of the camera. * * Applet Parameters: * -- dragStyle (default = "by speed") * Values are "by speed" or "by offset", corresponding to the drag styles described above * -- rotationCenter (default = undefined, meaning use scene bbox center) * If present, the parameter should be 3 floats, for example: "0 0 0" * If present, rotation is about this worldspace point instead of the scene's bbox center * -- backupSlack (default = 0.1) * For framing the scene in the panel. * If 0, then the scene will fit the frame as exactly as possible. * If > 0, then the camera will be backed up further to show more. The value is multiplied * by the original framing distance to get a new one. For example, if a distance of 10 frames * perfectly, then a backupSlack of 0.1 means that the new distance will be 11. * -- bySpeedZoomDragFactor (default = .05) * -- bySpeedPanDragFactor (default = .025) * -- bySpeedRotateDragFactor (default = .033) * The above three parameters only apply when dragStyle is "by speed." They specify the * amount of zoom, pan, or rotate velocity per pixel moved. For example, dragging 100 pixels * in zoom mode moves the camera at 5 units/second. * -- byOffsetZoomDragFactor (default = .01) * -- byOffsetPanDragFactor (default = .005) * -- byOffsetRotateDragFactor (default = .016) * The above three parameters only apply when dragStyle is "by offset." They specify the * total change in zoom, pan, or rotation per pixel moved. For example, dragging 100 pixels * in zoom mode moves the camera by 5 units. * * @author Jim Stewartson * @author Paul Isaacs * @author Dave Westwood * @author Rory Lane Lutter */ public class ExaminePanel extends Shout3DPanel implements DeviceObserver { ///////////////////////////////////////////////////////////// // Variables set via applet parameters // // See comments at top of file for more info. ///////////////////////////////////////////////////////////// // Selection of mapping between mouse motion and the camera. // See comments at top of file. static public final int BY_SPEED = 0; static public final int BY_OFFSET = 1; int currentDragStyle = BY_SPEED; // Center of rotation. If null, bbox center will be used. float[] rotationCenter = null; // For framing the scene. float backupSlack = 0.1f; // Speeds for "by speed" dragStyle float bySpeedZoomDragFactor = .05f; float bySpeedPanDragFactor = .025f; float bySpeedRotateDragFactor = .033f; // Speeds for byOffset dragStyle float byOffsetZoomDragFactor = .01f; float byOffsetPanDragFactor = .005f; float byOffsetRotateDragFactor = .016f; /////////////////////////////////////////////////////////////////// // Public methods to call to control the behavior /////////////////////////////////////////////////////////////////// /** * call this whenever you want to go reset the camera. * Result is that camera goes back to its initial orientation, * * Position is chosen to look at the current center of rotation. In the * case where objects are moving, this may be different than it was * when the scene was first loaded. */ public void resetCamera() { wantToReset = true; } /////////////////////////////////////////////////////////////////// // Protected member variables used in doing calculations /////////////////////////////////////////////////////////////////// // The three modes of manipulation for the camera: static final int PAN = 0; static final int ROTATE = 1; static final int ZOOM = 2; // The mode of manipulation currently in use. int currentMode; // cached reference to the initial viewpoint Viewpoint camera; Node[] pathToCameraParent = null; // cached reference to the scene root Transform root; // These store the initial cursor location of a click-drag-release sequence. float startX = 0; float startY = 0; // This holds the value of the camera's position relative to the // center of rotation, prior to rotation about that center. // The x,y values hold any x or y panning that's been done. // The z value holds the distance back from the bbox center, modified by // any zooming that's been done. float[] unrotatedCameraOffset = new float[3]; // This is what the unrotatedCameraOffset turns into after being rotated // by the camera's orientation. Add this to the center of rotation to determine // where the camera should be placed. float[] rotatedCameraOffset = new float[3]; // These hold values of the camera when the mouse was first pressed // during a click-drag-release sequence float[] startUnrotatedCameraOffset = new float[3]; float[] startHeadingPitchRoll = new float[3]; // Used for rotational calculations Quaternion tempQuat = new Quaternion(); // speed values for manipulating camera float cameraXPanSpeed = 0; float cameraYPanSpeed = 0; float cameraHeadingSpeed = 0; float cameraPitchSpeed = 0; float cameraZoomSpeed = 0; float cameraHeading = 0; float cameraPitch = -0.2f; float cameraRoll = 0; float cameraDist = 0; float[] bboxMin; float[] bboxMax; float[] bboxCenter; // For resetting the camera when resetCamera() is called. boolean wantToReset = false; boolean gotInitCameraOrientation = false; float[] initCameraOrientation = { 0, 0, 0, 0 }; /** * Constructs an ExaminePanel */ public ExaminePanel(Shout3DApplet applet) { super(applet); } /** * Constructs an ExaminePanel * * @param applet the Shout3DApplet in which this panel is to be drawn * @param width the width of the panel in pixels * @param height the height of the panel in pixels */ public ExaminePanel(Shout3DApplet applet, int width, int height) { super(applet,width,height); } /** * Constructs an ExaminePanel * * @param applet the Shout3DApplet in which this panel is to be drawn * @param x the x position of the panel in pixels * @param y the x position of the panel in pixels * @param width the width of the panel in pixels * @param height the height of the panel in pixels */ public ExaminePanel(Shout3DApplet applet, int x, int y, int width, int height){ super(applet,x,y,width,height); } /** * Remove observers when done with the panel */ public void finalize()throws Throwable { applet.getDeviceListener().removeDeviceObserver(this, "DeviceInput"); applet.getRenderer().removeRenderObserver(this); // Call the parent class. // Technically not required since parent class' method does nothing. // But good practice because in classes derived from this one, // it will be necessary to call the superclass to get the // functionality herein. // Hence keeping the call here is good practice, so that if this // method is copied/pasted in a subclass, all will be well. super.finalize(); } // a float field being used only to convert the javascript string //into a float[] in the init rotationCenter param FloatArrayField rotationCenterField = new FloatArrayField(getScene(), "rotationCenterField", 0, (new float[]{0.0f, 0.0f, 0.0f})); /** * Overrides Shout3DPanel.customInitialize() */ public void customInitialize() { camera = (Viewpoint)(applet.getCurrentBindableNode("Viewpoint")); root = (Transform)getScene(); // Get applet parameters String valueString; valueString = applet.getParameter("dragStyle"); if (valueString != null && (valueString.equals("by offset") || valueString.equals("by_offset") || valueString.equals("BY OFFSET") || valueString.equals("BY_OFFSET"))) { currentDragStyle = BY_OFFSET; } valueString = applet.getParameter("rotationCenter"); if (valueString != null) { rotationCenterField.setValueByString(valueString); rotationCenter = rotationCenterField.getValue(); } valueString = applet.getParameter("backupSlack"); if (valueString != null) { backupSlack = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("bySpeedZoomDragFactor"); if (valueString != null) { bySpeedZoomDragFactor = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("bySpeedPanDragFactor"); if (valueString != null) { bySpeedPanDragFactor = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("bySpeedRotateDragFactor"); if (valueString != null) { bySpeedRotateDragFactor = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("byOffsetZoomDragFactor"); if (valueString != null) { byOffsetZoomDragFactor = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("byOffsetPanDragFactor"); if (valueString != null) { byOffsetPanDragFactor = Float.valueOf(valueString).floatValue(); } valueString = applet.getParameter("byOffsetRotateDragFactor"); if (valueString != null) { byOffsetRotateDragFactor = Float.valueOf(valueString).floatValue(); } // register for device input applet.getDeviceListener().addDeviceObserver(this, "DeviceInput", null); // register for rendering notification applet.getRenderer().addRenderObserver(this, null); } /** * DeviceObserver method */ public boolean onDeviceInput(DeviceInput di, Object userData) { if (di instanceof MouseInput) { MouseInput mi = (MouseInput)di; switch (mi.which) { case MouseInput.DOWN: onMouseDown(mi); break; case MouseInput.DRAG: onMouseDrag(mi); break; case MouseInput.UP: onMouseUp(mi); break; } } // return false, let other entities handle the event too. return false; } /** * Called when the mouse is pressed */ void onMouseDown(MouseInput mi) { // Save the initial X and Y positions of the cursor. startX = mi.x; startY = mi.y; // Based on the modifiers, determine the mode of motion: currentMode = getManipulationMode(mi); if (currentDragStyle == BY_OFFSET) { // For "by offset" dragging, we need to remember the // starting unrotatedCameraOffset and the starting euler parameters. for (int i = 0; i< 3; i++) { startUnrotatedCameraOffset[i] = unrotatedCameraOffset[i]; } // convert the camera's orientation at the beginning into euler parameters. tempQuat.setAxisAngle(camera.orientation.getValue()); tempQuat.getEulers(startHeadingPitchRoll); } } /** * Called when the mouse is dragged */ void onMouseDrag(MouseInput mi) { // If the user has changed modes by pressing or releasing a modifier key, // we need to simulate the ending of the old drag and the beginning of a // new one in order to get a non-jumpy transition. // This is easily achieved by calling onMouseUp, then onMouseDown, // then continuing on as usual. if (currentMode != getManipulationMode(mi)) { onMouseUp(mi); onMouseDown(mi); } if (currentDragStyle == BY_SPEED) { dragMouseBySpeedStyle(mi); } else if (currentDragStyle == BY_OFFSET) { dragMouseByOffsetStyle(mi); } } /** * In this dragStyle, velocities are set for each of the types of motion. * Then, in onPreRender, the velocities are multiplied by time to * produce a resulting change from frame to frame. */ void dragMouseBySpeedStyle(MouseInput mi) { // Start by setting all speeds to 0. cameraXPanSpeed = 0; cameraYPanSpeed = 0; cameraHeadingSpeed = 0; cameraPitchSpeed = 0; cameraZoomSpeed = 0; if (currentMode == ZOOM) { // zoom is based on vertical mouse motion cameraZoomSpeed = (float)(mi.y - startY)*bySpeedZoomDragFactor; } else if (currentMode == PAN) { // pan is based on motion in both directions cameraXPanSpeed = -(float)(mi.x - startX)*bySpeedPanDragFactor; cameraYPanSpeed = (float)(mi.y - startY)*bySpeedPanDragFactor; } else { // current mode is ROTATE. // rotate is based on both directions. cameraHeadingSpeed = -(float)(mi.x - startX)*bySpeedRotateDragFactor; cameraPitchSpeed = -(float)(mi.y - startY)*bySpeedRotateDragFactor; } } /** * In this dragStyle, values for the camera parameters are saved when the * mouse first is pressed. Then, a change relative to that value is * created based on how far the mouse has moved during the drag. */ void dragMouseByOffsetStyle(MouseInput mi) { if (currentMode == ZOOM) { // zoom is based on vertical mouse motion unrotatedCameraOffset[2] = startUnrotatedCameraOffset[2] +( (float)(mi.y - startY) )*byOffsetZoomDragFactor; } else if (currentMode == PAN) { // pan is based on motion in both directions unrotatedCameraOffset[0] = startUnrotatedCameraOffset[0]-( (float)(mi.x - startX) )*byOffsetPanDragFactor; unrotatedCameraOffset[1] = startUnrotatedCameraOffset[1]+( (float)(mi.y - startY) )*byOffsetPanDragFactor; } else { // current mode is ROTATE. // rotate is based on both directions. cameraPitch = startHeadingPitchRoll[1] -( (float)(mi.y - startY)*byOffsetRotateDragFactor ); cameraHeading = startHeadingPitchRoll[0] -( (float)(mi.x - startX)*byOffsetRotateDragFactor ); } } /** * Called when the mouse is released */ void onMouseUp(MouseInput mi) { // stop rotating the camera cameraXPanSpeed = 0; cameraYPanSpeed = 0; cameraHeadingSpeed = 0; cameraPitchSpeed = 0; cameraZoomSpeed = 0; } /** * Gets the manipulation mode based on the modifier keys in the * MouseInput. */ int getManipulationMode(MouseInput mi) { if ((mi.modifiers & DeviceInput.CTRL_MASK) != 0) { // Zoom when control key is down. return ZOOM; } else if ((mi.modifiers & DeviceInput.SHIFT_MASK) != 0) { // Pan when the shift key is down. return PAN; } else { // Rotate when neither the shift or control key is down. return ROTATE; } } /** * Utility function for getting the distance between two points */ float getDistance(float[] from, float[] to) { float x = from[0] - to[0]; float y = from[1] - to[1]; float z = from[2] - to[2]; float dist = (float)Math.sqrt(x*x + y*y + z*z); return dist; } /** * RenderObserver function. * Does the camera movement calculations after each render. * * Note that no onPreRender() method is implemented in this class, * even though it is required for all classes implementing RenderObserver. * This is because the super class implements RenderObserver fully * and onPreRender is inherited. * Hence this class needs only to override the onPostRender() method, * making sure to call super.onPostRender(r,userData) within the body * of the method. */ public void onPostRender(Renderer r, Object userData) { // Call the parent class. // Technically not required since parent class' method does nothing. // But good practice because in classes derived from this one, // it will be necessary to call the superclass to get the // functionality herein. // Hence keeping the call here is good practice, so that if this // method is copied/pasted in a subclass, all will be well. super.onPostRender(r,userData); // If the camera or scene has changed, do necessary initialization. checkCameraChange(); // Only need to do this in relative mode (i.e., when currentDragStyle == BY_SPEED). // In "by offset" mode, these five values have been set exactly based on // the location of the mouse. if (currentDragStyle == BY_SPEED) { // In relative mode, the location of the mouse gave only a velocity which // must now be multiplied by elapsed time to give an amount of change that // is added to the current amount. // getFramesPerSecond() returns a rate, frames per second. // Flip this (take 1 divided by this number) to get the number of // seconds per frame. The number of seconds per frame is the elapsed // time since we last rendered. Hence... float timeSinceLastRender = 1/getFramesPerSecond(); // To get how much each of the values should change since the last time // we rendered a frame, increment each of the 5 parameters // by its velocity times the timeSinceLastRender. cameraHeading += cameraHeadingSpeed*timeSinceLastRender; cameraPitch += cameraPitchSpeed*timeSinceLastRender; unrotatedCameraOffset[0] += cameraXPanSpeed*timeSinceLastRender; unrotatedCameraOffset[1] += cameraYPanSpeed*timeSinceLastRender; unrotatedCameraOffset[2] += cameraZoomSpeed*timeSinceLastRender; } // Set the camera orientation based on the current heading, pitch, and roll. // First, get axis/angle equivalent of the heading/pitch/roll. Axis/angle // is required to set a rotation field value. tempQuat.setEulers(cameraHeading, cameraPitch, cameraRoll); float[] camRot = new float[4]; tempQuat.getAxisAngle(camRot); // Next, if the result is different from what's there, set the field value. if (camRot[0] != camera.orientation.getValue()[0] || camRot[1] != camera.orientation.getValue()[1] || camRot[2] != camera.orientation.getValue()[2] || camRot[3] != camera.orientation.getValue()[3]) { camera.orientation.setValue(camRot); } // rotate the camera offset by the camera's rotation, still stored // in tempQuat. rotatedCameraOffset[0] = unrotatedCameraOffset[0]; rotatedCameraOffset[1] = unrotatedCameraOffset[1]; rotatedCameraOffset[2] = unrotatedCameraOffset[2]; tempQuat.xform(rotatedCameraOffset); // Add the rotated distance vector to the center of rotation to place the camera. float[] camPos = new float[3]; camPos[0] = bboxCenter[0]+rotatedCameraOffset[0]; camPos[1] = bboxCenter[1]+rotatedCameraOffset[1]; camPos[2] = bboxCenter[2]+rotatedCameraOffset[2]; if (camPos[0] != camera.position.getValue()[0] || camPos[1] != camera.position.getValue()[1] || camPos[2] != camera.position.getValue()[2]) { camera.position.setValue(camPos); } } public void checkCameraChange() { // If scene or camera has changed, re-initialize. Viewpoint curCam = (Viewpoint)(applet.getCurrentBindableNode("Viewpoint")); Transform curRoot = (Transform)getScene(); if (curCam != camera || curRoot != root) { camera = curCam; pathToCameraParent = null; root = curRoot; gotInitCameraOrientation = false; wantToReset = true; } // Save the current camera orientation if needed if (gotInitCameraOrientation == false && camera != null) { System.arraycopy(camera.orientation.getValue(), 0, initCameraOrientation, 0, 4); gotInitCameraOrientation = true; } // Do we want to reset the camera position/orientation? if (wantToReset == true) { cameraRoll = 0; // put the camera orientation back. // Do not pass the reference to initCameraOrientation, or future edits to the // camera will change our reference as well. Instead, copy the value and call setValue() to // insure notification. System.arraycopy(initCameraOrientation, 0, camera.orientation.getValue(), 0, 4); camera.orientation.setValue( camera.orientation.getValue()); // Setting this to null insures that bbox and camera position will be // re-initialized below. bboxMin = null; // avoid doing this again wantToReset = false; } if (bboxMin == null) { // This is the first render. Establish camera position based on // starting orientation and bbox size. // Get the center of the current scene's bbox, in world space Searcher s = getNewSearcher(); s.setNode(root); Node[] pathToRoot = s.searchFirst(root); s.setNode(camera); Node[] pathToCamera = s.searchFirst(root); bboxMin = root.getWorldBBoxMin(pathToRoot); bboxMax = root.getWorldBBoxMax(pathToRoot); if (bboxMin[0] == 0 && bboxMin[1] == 0 && bboxMin[2] == 0 && bboxMax[0] == 0 && bboxMax[1] == 0 && bboxMax[2] == 0) { // Scene has not yet calculated a bbox, return and wait until next time. bboxMin = null; return; } bboxCenter = new float[3]; // Get the path to the camera's parent, if path has length > 1 if (pathToCamera != null && pathToCamera.length > 1) { pathToCameraParent = new Node[pathToCamera.length - 1]; System.arraycopy(pathToCamera, 0, pathToCameraParent, 0, pathToCameraParent.length); } // Set the bbox center. // If a rotationCenter is specified, set the bboxCenter to be the rotationCenter // and expand the bbox to be centered about the new rotationCenter. for (int i = 0; i < 3; i++) { if (rotationCenter != null) { // rotation center has been specified. use it as the bounding box // center and stretch the bounding box to fit. if (Math.abs(bboxMax[i] - rotationCenter[i])>Math.abs(bboxMin[i] - rotationCenter[i])) { bboxMin[i] = rotationCenter[i]-Math.abs(bboxMax[i] - rotationCenter[i]); } else { bboxMax[i] = rotationCenter[i]+Math.abs(bboxMin[i] - rotationCenter[i]); } bboxCenter[i] = rotationCenter[i]; } else { // otherwise use the current bounding box to find the center. bboxCenter[i] = ((bboxMax[i] - bboxMin[i])/2f)+bboxMin[i]; } } performInitialCameraPlacement(); // Reset the unrotatedCameraOffset based on the new starting position of the // camera. unrotatedCameraOffset[0] = 0f; unrotatedCameraOffset[1] = 0f; unrotatedCameraOffset[2] = getDistance(camera.position.getValue(), bboxCenter); } } public void performInitialCameraPlacement() { // Perform initial camera placement. // Start at the bbox center: float[] cameraPos = { bboxCenter[0], bboxCenter[1], bboxCenter[2]}; // Move back far enough that the largest of width/height/depth of the bbox can be fully seen within the field of view // These are given by the equations: tan(fieldOfView/2) = (width/2)/xbasedZdist; // tan(fieldOfView/2) = (height/2)/ybasedZdist; // tan(fieldOfView/2) = (depth/2)/zbasedZdist; float xbasedZdist = (float)((bboxMax[0] - bboxMin[0])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f))); float ybasedZdist = (float)((bboxMax[1] - bboxMin[1])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f))); float zbasedZdist = (float)((bboxMax[2] - bboxMin[2])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f))); // In addition, need to move back half of the box depth, to situate this viewable // region at the front side of the bbox. This makes the equations float halfBoxDepth = (bboxMax[2] - bboxMin[2])/2f; xbasedZdist += halfBoxDepth; ybasedZdist += halfBoxDepth; zbasedZdist += halfBoxDepth; // Use the bigger of the two distances, with some slack for good measure: if (xbasedZdist > ybasedZdist && xbasedZdist > zbasedZdist) cameraPos[2] += (1f + backupSlack) * xbasedZdist; else if ( ybasedZdist > zbasedZdist) cameraPos[2] += (1f + backupSlack) * ybasedZdist; else cameraPos[2] += (1f + backupSlack) * zbasedZdist; // If needed, transform position into camera parent space: if (pathToCameraParent != null) { // Second argument means get matrix going from top to bottom of path. float[] xfMat = MatUtil.getMatrixAlongPath(pathToCameraParent, false); MatUtil.multVecMatrix(xfMat, cameraPos); } // Now set it the position in the camera camera.position.setValue(cameraPos); // Set the initial cameraHeading and cameraPitch from the current values in the camera. Quaternion initCamQuat = new Quaternion(); initCamQuat.setAxisAngle(camera.orientation.getValue()); float[] headingPitchRoll = new float[3]; initCamQuat.getEulers(headingPitchRoll); cameraHeading = headingPitchRoll[0]; cameraPitch = headingPitchRoll[1]; } }