/*********************************************************************** * mt4j Copyright (c) 2008 - 2009 Christopher Ruff, Fraunhofer-Gesellschaft All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * ***********************************************************************/ package org.mt4j.input.inputProcessors.componentProcessors.arcballProcessor; import java.util.ArrayList; import java.util.List; import javax.media.opengl.GL; import org.mt4j.components.MTComponent; import org.mt4j.components.bounds.BoundingSphere; import org.mt4j.components.visibleComponents.shapes.AbstractShape; import org.mt4j.input.inputData.InputCursor; import org.mt4j.input.inputData.MTFingerInputEvt; import org.mt4j.input.inputProcessors.IInputProcessor; import org.mt4j.input.inputProcessors.componentProcessors.AbstractComponentProcessor; import org.mt4j.input.inputProcessors.componentProcessors.AbstractCursorProcessor; import org.mt4j.util.math.ToolsMath; import org.mt4j.util.math.Matrix; import org.mt4j.util.math.Quaternion; import org.mt4j.util.math.Ray; import org.mt4j.util.math.Tools3D; import org.mt4j.util.math.Vector3D; import processing.core.PApplet; import processing.opengl.PGraphicsOpenGL; /** * The Class ArcballProcessor. Fires ArcBallGestureEvent events. * * @author Christopher Ruff */ public class ArcballProcessor extends AbstractCursorProcessor { private PApplet applet; private Matrix identityDummy; private BoundingSphere bSphere; // private AbstractShape shape; private MTComponent shape; private float sizeScaled = 1; private IArcball ac; /** The un used cursors. */ private List<InputCursor> unUsedCursors; /** The locked cursors. */ private List<InputCursor> lockedCursors; /** * Instantiates a new arcball processor. * * @param applet the applet * @param shape the shape */ public ArcballProcessor(PApplet applet, AbstractShape shape){ /* super(); this.applet = applet; this.shape = shape; this.unUsedCursors = new ArrayList<InputCursor>(); this.lockedCursors = new ArrayList<InputCursor>(); if (identityDummy == null) identityDummy = new Matrix(); this.bSphere = new BoundingSphere(shape); this.bSphere.setRadius(bSphere.getRadius() * sizeScaled); // ((BoundingSphere)shape.getBoundingShape()).setRadius(((BoundingSphere)shape.getBoundingShape()).getRadius()*2); this.ac = null; this.setLockPriority(1); */ this(applet, shape, new BoundingSphere(shape)); } public ArcballProcessor(PApplet applet, MTComponent component, BoundingSphere bSphere){ this.applet = applet; this.unUsedCursors = new ArrayList<InputCursor>(); this.lockedCursors = new ArrayList<InputCursor>(); if (identityDummy == null) identityDummy = new Matrix(); this.bSphere = bSphere; this.bSphere.setRadius(bSphere.getRadius() * sizeScaled); this.shape = component; // ((BoundingSphere)shape.getBoundingShape()).setRadius(((BoundingSphere)shape.getBoundingShape()).getRadius()*2); this.ac = null; this.setLockPriority(1); logger.debug("Bounding sphere center: " + bSphere.getCenter() + " Radius: " + bSphere.getRadius()); } @Override public void cursorStarted(InputCursor m, MTFingerInputEvt positionEvent) { if (lockedCursors.size() >= 1){ //We assume that drag is already in progress and add this new cursor to the unUsedList unUsedCursors.add(m); }else{ if (unUsedCursors.size() == 0){ //Only start drag if no other finger on the component yet if (this.canLock(m)){//See if we can obtain a lock on this cursor (depends on the priority) this.getLock(m); lockedCursors.add(m); ac = new MyArcBall(m); logger.debug(this.getName() + " successfully locked cursor (id:" + m.getId() + ")"); this.fireGestureEvent(new ArcBallGestureEvent(this, ArcBallGestureEvent.GESTURE_DETECTED, positionEvent.getTargetComponent(), identityDummy)); }else{ unUsedCursors.add(m); } }else{ unUsedCursors.add(m); } } } @Override public void cursorUpdated(InputCursor m, MTFingerInputEvt positionEvent) { if (lockedCursors.contains(m)){ Matrix mat = ac.getNewRotation(m); this.fireGestureEvent(new ArcBallGestureEvent(this, ArcBallGestureEvent.GESTURE_UPDATED, positionEvent.getTargetComponent(), mat)); } } @Override public void cursorEnded(InputCursor m, MTFingerInputEvt positionEvent) { logger.debug(this.getName() + " INPUT_ENDED RECIEVED - MOTION: " + m.getId()); if (lockedCursors.contains(m)){ //cursor was a actual drag cursor lockedCursors.remove(m); if (unUsedCursors.size() > 0){ //check if there are other cursors on the component, we could use for drag InputCursor otherCursor = unUsedCursors.get(0); //TODO cycle through all available unUsedCursors and try to claim one, maybe the first one is claimed but another isnt! if (this.canLock(otherCursor)){ //Check if we have the priority to use this cursor this.getLock(otherCursor); unUsedCursors.remove(otherCursor); lockedCursors.add(otherCursor); ac = new MyArcBall(otherCursor); }else{ this.fireGestureEvent(new ArcBallGestureEvent(this, ArcBallGestureEvent.GESTURE_ENDED, positionEvent.getTargetComponent(), identityDummy)); } }else{ this.fireGestureEvent(new ArcBallGestureEvent(this, ArcBallGestureEvent.GESTURE_ENDED, positionEvent.getTargetComponent(), identityDummy)); } this.unLock(m); }else{ //cursor was not used for dragging if (unUsedCursors.contains(m)){ unUsedCursors.remove(m); } } } /* (non-Javadoc) * @see org.mt4j.input.inputAnalyzers.IInputAnalyzer#cursorLocked(org.mt4j.input.inputData.InputCursor, org.mt4j.input.inputAnalyzers.IInputAnalyzer) */ @Override public void cursorLocked(InputCursor m, IInputProcessor lockingAnalyzer) { if (lockingAnalyzer instanceof AbstractComponentProcessor){ logger.debug(this.getName() + " Recieved MOTION LOCKED by (" + ((AbstractComponentProcessor)lockingAnalyzer).getName() + ") - cursor ID: " + m.getId()); }else{ logger.debug(this.getName() + " Recieved MOTION LOCKED by higher priority signal - cursor ID: " + m.getId()); } if (lockedCursors.contains(m)){ //cursor was a actual gesture cursor lockedCursors.remove(m); //TODO fire ended evt? unUsedCursors.add(m); logger.debug(this.getName() + " cursor:" + m.getId() + " MOTION LOCKED. Was an active cursor in this gesture!"); }else{ //TODO remove else, it is pretty useless if (unUsedCursors.contains(m)){ logger.debug(this.getName() + " MOTION LOCKED. But it was NOT an active cursor in this gesture!"); } } } /* (non-Javadoc) * @see org.mt4j.input.inputAnalyzers.IInputAnalyzer#cursorUnlocked(org.mt4j.input.inputData.InputCursor) */ @Override public void cursorUnlocked(InputCursor m) { logger.debug(this.getName() + " Recieved UNLOCKED signal for cursor ID: " + m.getId()); if (lockedCursors.size() >= 1){ //we dont need the unlocked cursor, gesture still in progress return; } if (unUsedCursors.contains(m)){ if (this.canLock(m)){ this.getLock(m); unUsedCursors.remove(m); lockedCursors.add(m); ac = new MyArcBall(m); //TODO fire started? maybe not.. do we have to? logger.debug(this.getName() + " can resume its gesture with cursor: " + m.getId()); }else{ logger.debug(this.getName() + " still in progress - we dont need the unlocked cursor" ); } } } /** * * @author Chris * */ public interface IArcball{ public Matrix getNewRotation(InputCursor m); } /** * * @author Chris * */ private class MyArcBall implements IArcball{ private Vector3D lastPoint; private Quaternion q; private Matrix returnMatrix; private InputCursor m; private boolean doInWorldCoords = true; private boolean camInSphere = false; private float camDistToInterSection; public MyArcBall(InputCursor m){ this.m = m; lastPoint = getSphereIntersectionObjSpace(); if (lastPoint == null){ lastPoint = new Vector3D(); //TODO hack: we should abort the gesture } // TEST if (doInWorldCoords) lastPoint.transform(shape.getGlobalMatrix()); q = new Quaternion(); returnMatrix = new Matrix(); camDistToInterSection = 1; } public Matrix getNewRotation(InputCursor m){ returnMatrix.loadIdentity(); Vector3D newInterSection = getSphereIntersectionObjSpace(); if (newInterSection != null){ if (doInWorldCoords) newInterSection.transform(shape.getGlobalMatrix()); logger.debug("Sphere hit, hitpoint: " + newInterSection); Vector3D center = bSphere.getCenterPointLocal(); if (doInWorldCoords) //TODO center world cachen? center.transform(shape.getGlobalMatrix()); Vector3D a = lastPoint.getSubtracted(center); Vector3D b = newInterSection.getSubtracted(center); //float dot = a.dot(b); // float angle = Vector3D.angleBetween(a, b); float angle = (float)myAngleBetween(a, b); // angle *= sizeScaled * 1.5f; Vector3D rotationAxis = a.crossLocal(b); //Inverse the angle if we are inside the boundingsphere and //hit the inner side if (camInSphere){ //we hit the backside of the boundingsphere, have to invert direction // angle *= -1; // rotationAxis.rotateZ(PApplet.radians(180)); //better than angle*-1 rotationAxis.rotateZ(ToolsMath.PI); //better than angle*-1 // rotationAxis.rotateX(PApplet.radians(180)); //better than angle*-1 // rotationAxis.scaleLocal(-1); //like angle*-1 // rotationAxis.rotateAroundAxisLocal(rotationAxis, PApplet.radians(90)); } rotationAxis.normalizeLocal(); //TODO map points that didnt intersect to sphere //TODO measure distance from cam to sphere intersection point and multiply angle //so that if distance big -> less angle, if distance small -> more angle // System.out.println("Distance Camera to Sphere Intersection: " + camDistToInterSection); // angle *= 1+ 1/camDistToInterSection; float dist = Vector3D.distance(shape.getViewingCamera().getPosition() , newInterSection); // System.out.println("Dist: " + dist + " Angle: " + angle); //Hack to make rotation faster if near sphere float angleScaleFactor = 500f / dist; if (angleScaleFactor < 1.5f) angleScaleFactor = 1.5f; if (angleScaleFactor > 80f) angleScaleFactor = 80f; // System.out.println("Angle Scale factor: " + angleScaleFactor); angle *= angleScaleFactor; // System.out.println(); // logger.debug("New hitpoint" + NewPt); // logger.debug("Axis: " + cross); // logger.debug("Angle: " +angle + " \n"); // q.fromAngleNormalAxis(angle, cross); // q.toRotationMatrix(returnMatrix); returnMatrix.fromAngleNormalAxis(angle, rotationAxis); //logger.debug(returnMatrix); this.lastPoint.setValues(newInterSection); //TODO why often invalid matrix? because we didnt norm the sphere to -1..1? if (!returnMatrix.isValid()) { // logger.debug("NaN"); returnMatrix.loadIdentity(); return returnMatrix; } // returnMatrix = Matrix.getZRotationMatrix(new Vector3D(), 1); // Matrix.toRotationAboutPointMatrixAndInverse(returnMatrix, null, new Vector3D()); //to rotate relative to world transform rotation point if (doInWorldCoords) center = MTComponent.getGlobalVecToParentRelativeSpace(shape, center); //To rotate about the center of the object Matrix.toRotationAboutPoint(returnMatrix, center); }else{ logger.debug("Sphere wasnt hit!"); } return returnMatrix; } private double myAngleBetween(Vector3D a, Vector3D b) { float dot = a.dot(b); double theta = Math.acos(dot / (length(a) * (length(b)) )); return theta; } /** * Calculate the magnitude (length) of the vector. * * @return the magnitude of the vector */ public double length(Vector3D v) { return Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); } private Vector3D getSphereIntersectionObjSpace(){ // Icamera cam = shape.getAncestor().getGlobalCam(); // // Vector3D rayStartPoint = cam.getPosition(); // Vector3D pointInRayDir = Tools3D.unprojectScreenCoords(applet, m.getLastEvent().getPositionX(), m.getLastEvent().getPositionY()); // // Ray orgRay = new Ray(rayStartPoint, pointInRayDir); Ray realRayForThisObj = Tools3D.getCameraPickRay(applet, shape, m.getCurrentEvent().getPosX(), m.getCurrentEvent().getPosY()); // Ray realRayForThisObj = Tools3D.toComponentCameraPickRay(applet, shape, orgRay); //TRIAL Ray invertedRay = Ray.getTransformedRay(realRayForThisObj, shape.getGlobalInverseMatrix()); Vector3D is = bSphere.getIntersectionLocal(invertedRay); //Test to detect whether were inside the sphere // Vector3D camPos = cam.getPosition(); Vector3D camPos = shape.getViewingCamera().getPosition(); camPos.transform(shape.getGlobalInverseMatrix()); if (is != null){ camDistToInterSection = Vector3D.distance(camPos, is); //bSphere.distanceToEdge(camPos); } if (bSphere.containsPointLocal(camPos)){ //logger.debug("inside the sphere"); camInSphere = true; }else{ camInSphere = false; } return is; } } private class ArcBallContext implements IArcball{ private static final float Epsilon = 1.0e-5f; Quaternion q; Vector3D StVec; //Saved click vector Vector3D EnVec; //Saved drag vector float adjustWidth; //Mouse bounds width float adjustHeight; //Mouse bounds height public ArcBallContext(float NewWidth, float NewHeight) { StVec = new Vector3D(); EnVec = new Vector3D(); setBounds(NewWidth, NewHeight); q = new Quaternion(); } public void mapToSphere(Vector3D point, Vector3D outVector) { //Copy paramter into temp point Vector3D tempPoint = new Vector3D(point.x, point.y, point.z); //Adjust point coords and scale down to range of [-1 ... 1] tempPoint.x = (tempPoint.x * this.adjustWidth) - 1.0f; tempPoint.y = 1.0f - (tempPoint.y * this.adjustHeight); //Compute the square of the length of the vector to the point from the center float length = (tempPoint.x * tempPoint.x) + (tempPoint.y * tempPoint.y); //If the point is mapped outside of the sphere... (length > radius squared) if (length > 1.0f) { //Compute a normalizing factor (radius / sqrt(length)) float norm = (float) (1.0 / Math.sqrt(length)); //Return the "normalized" vector, a point on the sphere outVector.x = tempPoint.x * norm; outVector.y = tempPoint.y * norm; outVector.z = 0.0f; } else { //Else it's on the inside //Return a vector to a point mapped inside the sphere sqrt(radius squared - length) outVector.x = tempPoint.x; outVector.y = tempPoint.y; outVector.z = (float) Math.sqrt(1.0f - length); } } public void setBounds(float NewWidth, float NewHeight) { assert((NewWidth > 1.0f) && (NewHeight > 1.0f));//TODO REMOVE //Set adjustment factor for width/height adjustWidth = 1.0f / ((NewWidth - 1.0f) * 0.5f); adjustHeight = 1.0f / ((NewHeight - 1.0f) * 0.5f); } //Mouse down public void click(Vector3D NewPt) { mapToSphere(NewPt, this.StVec); } //Mouse down public void click(InputCursor m){ // Icamera cam = shape.getAncestor().getGlobalCam(); // Vector3D rayStartPoint = cam.getPosition(); // Vector3D pointInRayDir = Tools3D.unprojectScreenCoords(applet, m.getLastEvent().getPositionX(), m.getLastEvent().getPositionY()); // Ray orgRay = new Ray(rayStartPoint, pointInRayDir); // Ray realRayForThisObj = Tools3D.toComponentCameraPickRay(applet, shape, orgRay); Ray realRayForThisObj = Tools3D.getCameraPickRay(applet, shape, m.getCurrentEvent().getPosX(), m.getCurrentEvent().getPosY()); //TRIAL Ray invertedRay = Ray.getTransformedRay(realRayForThisObj, shape.getGlobalInverseMatrix()); Vector3D NewPt = bSphere.getIntersectionLocal(invertedRay); if (NewPt != null){ PGraphicsOpenGL pgl = ((PGraphicsOpenGL)applet.g); GL gl = pgl.beginGL(); gl.glPushMatrix(); gl.glMultMatrixf(shape.getGlobalMatrix().toFloatBuffer()); NewPt = Tools3D.projectGL(gl, pgl.glu, NewPt, NewPt); gl.glPopMatrix(); pgl.endGL(); this.mapToSphere(NewPt, this.StVec); }else{ logger.error(getName() + " Didnt hit sphere!"); } } public Matrix getNewRotation(InputCursor m){ // Icamera cam = shape.getAncestor().getGlobalCam(); // Vector3D rayStartPoint = cam.getPosition(); // Vector3D pointInRayDir = Tools3D.unprojectScreenCoords(applet, m.getLastEvent().getPositionX(), m.getLastEvent().getPositionY()); // Ray orgRay = new Ray(rayStartPoint, pointInRayDir); // Ray realRayForThisObj = Tools3D.toComponentCameraPickRay(applet, shape, orgRay); Ray realRayForThisObj = Tools3D.getCameraPickRay(applet, shape, m.getCurrentEvent().getPosX(), m.getCurrentEvent().getPosY()); //TRIAL Ray invertedRay = Ray.getTransformedRay(realRayForThisObj, shape.getGlobalInverseMatrix()); Vector3D NewPt = bSphere.getIntersectionLocal(invertedRay); if (NewPt != null){ PGraphicsOpenGL pgl = ((PGraphicsOpenGL)applet.g); GL gl = pgl.beginGL(); gl.glPushMatrix(); gl.glMultMatrixf(shape.getGlobalMatrix().toFloatBuffer()); NewPt = Tools3D.projectGL(gl, pgl.glu, NewPt, NewPt); gl.glPopMatrix(); pgl.endGL(); logger.debug(NewPt); this.drag(NewPt, q); }else{ return Matrix.get4x4Identity(); } return q.toRotationMatrix(); } //Mouse drag, calculate rotation public void drag(Vector3D NewPt, Quaternion NewRot) { // this.EnVec.setValues(NewPt); //Map the point to the sphere this.mapToSphere(NewPt, EnVec); //Return the quaternion equivalent to the ration if (NewRot != null) { // Vector3D Perp = new Vector3D(); //Compute the vector perpendicular to the begin and end vectors // Vector3D.cross(Perp, StVec, EnVec); Vector3D Perp = StVec.getCross(EnVec); //Compute the length of the perpendicular vector if (Perp.length() > Epsilon){ //if its non-zero //We're ok, so return the perpendicular vector as the transform after all NewRot.x = Perp.x; NewRot.y = Perp.y; NewRot.z = Perp.z; //In the quaternion values, w is cosine (theta / 2), where theta is rotation angle // NewRot.w = Vector3D.dot(StVec, EnVec); NewRot.w = StVec.dot(EnVec); } else { //if its zero //The begin and end vectors coincide, so return an identity transform NewRot.x = NewRot.y = NewRot.z = NewRot.w = 0.0f; } } } } // private class ArcBall { // // PApplet parent; // // float center_x, center_y, center_z, radius; // Vector3D v_down, v_drag; // Quaternion q_now, q_down, q_drag; // Vector3D[] axisSet; // int axis; // // /** defaults to radius of min(width/2,height/2) and center_z of -radius */ // public ArcBall(PApplet parent) { // this(parent.g.width/2.0f,parent.g.height/2.0f,-PApplet.min(parent.g.width/2.0f,parent.g.height/2.0f),PApplet.min(parent.g.width/2.0f,parent.g.height/2.0f), parent); // } // // public ArcBall(float center_x, float center_y, float center_z, float radius, PApplet parent) { // // this.parent = parent; // // parent.registerMouseEvent(this); // parent.registerPre(this); // // this.center_x = center_x; // this.center_y = center_y; // this.center_z = center_z; // this.radius = radius; // // v_down = new Vector3D(); // v_drag = new Vector3D(); // // q_now = new Quaternion(); // q_down = new Quaternion(); // q_drag = new Quaternion(); // // axisSet = new Vector3D[] { // new Vector3D(1.0f, 0.0f, 0.0f), new Vector3D(0.0f, 1.0f, 0.0f), new Vector3D(0.0f, 0.0f, 1.0f) }; // axis = -1; // no constraints... // } // // public void mouseEvent(MouseEvent event) { // int id = event.getID(); // if (id == MouseEvent.MOUSE_DRAGGED) { // mouseDragged(); // } // else if (id == MouseEvent.MOUSE_PRESSED) { // mousePressed(); // } // } // // public Matrix drag(InputCursor m){ // //TODO // Icamera cam = shape.getAncestor().getGlobalCam(); // // Vector3D rayStartPoint = cam.getPosition(); // Vector3D pointInRayDir = Tools3D.unprojectScreenCoords(applet, m.getLastEvent().getPositionX(), m.getLastEvent().getPositionY()); // // Ray orgRay = new Ray(rayStartPoint, pointInRayDir); // // Ray realRayForThisObj = Tools3D.getRealPickRay(shape, orgRay); // // //TRIAL // Ray invertedRay = Ray.getTransformedRay(realRayForThisObj, shape.getAbsoluteWorldToLocalMatrix()); // // Vector3D NewPt = bSphere.getIntersectionPoint(invertedRay); // // if (NewPt != null){ // logger.debug(NewPt); // this.mouseDragged(); // //// this.drag(NewPt, q); // }else{ // return Matrix.get4x4Identity(); // } // } // // public void mousePressed() { // v_down = mouse_to_sphere(parent.mouseX, parent.mouseY); // q_down.set(q_now); //// q_drag.reset(); // q_drag.loadIdentity(); // } // // // public void mouseDragged(float x, float y) { // v_drag = mouse_to_sphere(y, y); //// q_drag.set(Vector3D.dot(v_down, v_drag), Vector3D.cross(v_down, v_drag)); // q_drag.set(v_down.dot(v_drag), v_down.getCross(v_drag)); // } // //// public void mouseDragged() { //// v_drag = mouse_to_sphere(parent.mouseX, parent.mouseY); ////// q_drag.set(Vector3D.dot(v_down, v_drag), Vector3D.cross(v_down, v_drag)); //// q_drag.set(v_down.dot(v_drag), v_down.getCross(v_drag)); //// } // // public void pre() { // parent.translate(center_x, center_y, center_z); //// q_now = Quaternion.mul(q_drag, q_down); // q_now = q_drag.mult(q_down); // // applyQuaternion2Matrix(q_now); // parent.translate(-center_x, -center_y, -center_z); // } // // Vector3D mouse_to_sphere(float x, float y) { // Vector3D v = new Vector3D(); // v.x = (x - center_x) / radius; // v.y = (y - center_y) / radius; // // float mag = v.x * v.x + v.y * v.y; // if (mag > 1.0f) { //// v.normalize(); // v.normalizeLocal(); // } // else { // v.z = PApplet.sqrt(1.0f - mag); // } // // return (axis == -1) ? v : constrain_vector(v, axisSet[axis]); // } // // Vector3D constrain_vector(Vector3D vector, Vector3D axis) { // Vector3D res = new Vector3D(); //// res.sub(vector, Vector3D.mul(axis, Vector3D.dot(axis, vector))); // res.subtractLocal(axis.getScaled(axis.dot(vector))); // //// res.normalize(); // res.normalizeLocal(); // return res; // } // // void applyQuaternion2Matrix(Quaternion q) { // // instead of transforming q into a matrix and applying it... // // float[] aa = q.getValue(); // parent.rotate(aa[0], aa[1], aa[2], aa[3]); // } // // } @Override public String getName() { return "Arcball Processor"; } }