Point Drag Controls

Update December 2017

PointDragControls.js has now been updated to correct pointer drift in translation mode. The original controller was rotation only, and the translation controller was tacked on for completeness. Later some mouse pointer drift in the translation mode was since identified, which has now been fixed, and the controller has been improved generally. If you are already using PointDragControls.js, make sure you update to the latest version.

PointDragControls - three.js [Downloads: 4112]

What's This

Point Drag Contols is a controller for Three.js which enables click and drag to translate or rotate individual objects. It was created for a specific application, but is now open source (under the MIT license).

Basically we couldn't find a drag-to-rotate controller which did what we wanted under three.js, so we wrote one. DragControls.js is a nice controller, but it does not currently support rotation. Most snippets of code that can be found dealing with rotation only allow rotation about the objects' origin, and without much control over the axis of rotation.

PointDragControls.js is mainly aimed at solving the rotation problem (but the simpler to implement translation controller is also included.) The goal is create something which (a) is intuitive to use (b) is coordinate system independent (ie works the same way whatever direction the camera points, and where-ever the objects are in the scene) (c) fully supports touch and (d) remains compatible with at least one of the camera controllers (Trackball.js, OrbitControls.js).

Presented here is a first version: it is felt that requirements (a), (b) and (c) have been met with a good level success. (d) has only been looked at in the case of OrbitControls.js - and it is clear some modification of OrbitControls.js is necessary to allow the 2 controllers to sit happily side by side. Basically, at a minimum OrbitControls.js needs to be told to ignore clicks when they are over an object, to avoid dragging both the camera and the object at the same time. The second demo (below) includes a first attempt at a modified OrbitControls.js: while this does work, please be advised it is in an early stage of development. Keep reading for more specifics.

More Specifically

The main advantage of this controller is that the initial click or touch event defines the origin about which to rotate the object. Click and drag from the top of the object and the object will rotate about…  you guessed it…  it's top!

Rotations in 3D can be tricky, and difficult to visualise. In order to be as intuitive as possible it was decided that the directions of the rotation axes should be locked. (That is, locked by default - of course free rotation can also be specified, but is presumed to be not as desirable.) Furthermore, the direction of the rotation axes should be defined in a "polar" sense with respect to the position of the camera. Please refer to the accompanying diagram. The desirable rotation axes form an orthogonal set: the vector k lies directly along the line of sight from the camera to the object, j is the vector perpendicular to k parallel to "ground" (a plane extending horizontally from the camera) and i is also perpendicular to k but lies in the camera's "vertical" plane.

Or looking at it another way, if a rope were to connect the origin to the object, k would lie along the rope, i and j would be the directions the object would begin to move if it were swung vertically and horizontally (respectfully).

It is interesting that this coordinate scheme does not seem to be the most intuitive for translations. Thus our controller reverts to a coordinate system with the horizontal, vertical and "look at" directions of the camera as the axes when the mode is switched to translate.

Isotropic Behaviour

To meet the requirement that the rotation axis set consistently appears in the aforementioned desirable orientation, irrespective of the angle of the camera, the position of the object etc. relative to "world" coordinates, a transformation matrix is computed by multiplying individual transformations for each of the necessary corrections. An incredible amount of joy and fun was derived from trying to figure all this out, juggling different coordinate systems, and then trying to determine which minus sign was missing from where so that 2 of the axes came out flip-flopped at the end. Or was there a mistake in the order of the matrix multiplications (since rotation transformations are in general non-commutative )?

In fact this is so much fun, and not at all confusing, that we are sure you won't want to download our solution, and instead will prefer to sit down with a hot cup of coffee and a piece of paper (several may be needed) and thrash it out from scratch. No, but really.

Alternatively, feel free to get the source .

Usage Instructions

You need script tags to load three.js and PointDragControls.js:

<script src="js/three.js"></script>
<script src="js/PointDragControls.js"></script>

At minimum you need a camera, a scene and a renderer:

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500);
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
scene.add(someObjects);

Then pass the camera, scene and renderer to init():

var controls = new THREE.PointDragControls();
controls.init( scene,camera,renderer );

A more complete example is given below:

<!DOCTYPE html>
<html>
	<head>
		<meta charset=utf-8>
		<title>PointDragControls Example</title>
		<style>
		 body { margin: 0; }
         canvas { width: 100%; height: 100% }
		</style>
	</head>
	<body>

		<script src="js/three.js"></script>
        <script src="js/Detector.js"></script>
        <script src="js/PointDragControls.js"></script>
		<script>
         if (Detector.webgl) { // make sure we can use webgl

             // create the renderer
             var renderer = new THREE.WebGLRenderer();
             renderer.setSize( window.innerWidth, window.innerHeight );
             document.body.appendChild( renderer.domElement );

             // and the camera
             var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500);
             camera.position.set(0, 4, 0);
             camera.lookAt(new THREE.Vector3(0, 0, 0));


             // and something to look at
             var geometry = new THREE.BoxGeometry( 1, 1, 1 );
             var material = new THREE.MeshFaceMaterial([
                 new THREE.MeshBasicMaterial({color: 0x00ff00}),
                 new THREE.MeshBasicMaterial({color: 0xff0000}),
                 new THREE.MeshBasicMaterial({color: 0x0000ff}),
                 new THREE.MeshBasicMaterial({color: 0xffff00}),
                 new THREE.MeshBasicMaterial({color: 0x00ffff}),
                 new THREE.MeshBasicMaterial({color: 0xff00ff})
             ]);
             var cube = new THREE.Mesh( geometry, material );

             // create the scene, add the cube, and add the scene to the renderer
             var scene = new THREE.Scene();
             scene.add(cube);
             renderer.render(scene, camera);

             // initialise controls
             var controls = new THREE.PointDragControls();
             controls.init( scene,camera,renderer, {
                 auto_render: true
             });

         } else { // no webgl - issue warning

             var warning = Detector.getWebGLErrorMessage();
             document.getElementById('container').appendChild(warning);

         }
        </script>
    </body>
</html>

In the example you may notice the absence of an animate() function. In fact PointDragControls can be used with or without animate(). If your scene is generally static apart from when you are dragging objects around, then why animate? In this case it is simpler to ask the controller to render the scene only when an object is moved/rotated. To do this, set the auto_render option to true. Alternatively, if you are calling animate(), then it's probably better to omit this option (the default is false).

Options

You can pass additional options to the controller on initialisation:

controls.init( scene,camera,renderer, {
    // options go here
});

Available options with default values:

objects:            scene.children,             // array of objects to apply controls to

turning_circle:     90,                         // controls mouse rotation sensitivity during object rotation.
                                                // Number of pixels to move the mouse through for a full rotation
                                                // default = 1 pixel per 4 degrees

near:               new THREE.Raycaster.near    // nearest point to apply controls to

far:                new THREE.Raycaster.far     // furthest point to apply controls to

snap_distance:      4,                          // only has effect when pointer axes are locked
                                                // this is the minimum cumulative difference between screen x and y
                                                // values (in pixels) before the active axis is chosen

z_shift_distance:   10,                         // controls the sensitivity by which objects move towards/away from
                                                // the camera when performing translations parallel to the camera
                                                // normal vector. Bigger = less sensitive

z_control_axis:     'y',                        // 'x' or 'y'. Whether horizontal or vertical movement of the
                                                // mouse pointer (or touch) controls forward/back movement of the
                                                // object relative to the camera (when right mouse button pressed or
                                                // in the case of touch, when one addtional finger is held on the empty
                                                // canvas)

mode_auto:          true,                       // true = toggle mode by double-clicking (double-tapping) on empty
                                                // canvas area. false = don't auto change mode at all. Let the app
                                                // handle all mode changes via .toggle_mode() and set_mode(). Note
                                                // .toggle_mode() and .set_mode() still work if mode_auto = true

init_mode:          'translate',                // the mode to initialise with

lock_translation_axes:  false,                  // if true, decide which pointer axis (ie x or y) is preferred early
                                                // in the pointer movement, then lock object translation to this axis

lock_rotation_axes:     true,                   // same as lock_translation_axis but for rotation. This is locked by
                                                // default under the assumption that  rotations are less intuitive
                                                // than translations

auto_render:            false                   // render whenever an object is transformed. False indicates this
                                                // controller will not do any rendering (implies app is using an
                                                // animate() function)

Runtime Functions

Some functions are provided to enable changing of the controller's behaviour after initialisation. These are listed below:

controls.include(extra_objects);        // add extra_objects to the list of objects
                                        // which we are controlling (including an
                                        // object already under control has no effect)
                                        // extra_objects should be an array of three.js
                                        // objects

controls.exclude(objects_to_remove);    // stop controlling objects_to_remove

controls.toggle_mode()                  // toggle current mode between rotate and
                                        // translate

controls.set_mode(new_mode)             // set the current mode to new_mode
                                        // either 'rotate' or 'translate'

Access to Globals

It is intended that the state of the controller be changed during runtime via one of the previously mentioned accessor functions. However, we recognise the code is at an early stage of development, and that you may want to do things which we didn't think of. If this is the case, please do get in touch and tell us your frustrations. Meanwhile, you can access globals via controls.globals. Be careful. Many variables keep track of internal state, and may do odd things if you change them. Here is the list:

raycaster:              new THREE.Raycaster(),

pointer:                new THREE.Vector2(),    // either the mouse position or the touch location

last_pointer_posn:      undefined,              // For working out the size of the change in mousemove &
                                                // touchmove events

intersect: {                                    // world coord points where the mouse click intercepts
    forward:            undefined,              // (forward) the front of the object
    reverse:            undefined               // (reverse) the back of the object
},

active_axes: {
    r:                  undefined,              // rotation axis label (world coords) x, y or z
    t:                  undefined,              // translation axis (world coords) x, y or z
},

origin_touch_id:        undefined,              // for remembering the id of the touch event occurring
                                                // on the object to be rotated (in the 2 touch event
                                                // situation, which is basically confined to z rotations
                                                // on touch devices)

init_dt:                { x: 0, y: 0 },         // for remembering total mouse movement before we decide
                                                // which is the active axis

dt:                     { x: 0, y: 0 },         // size of mouse (or finger) movement during current cycle

mode:                   undefined,              // modes are 'rotate' or 'translate'. Initial mode is
                                                // defined in defaults

click_timer:            undefined,              // for detecting double-click or double-tap

double_click_timeout:   500,                     // max time diff between clicks (taps) to qualify as a
                                                // double click (in ms)

object_id_index:        []                      // track object ids for fast lookups

rev_intercept_from:     99999999,               // represents infinity in the calculation of reverse
                                                // raycast intercept

Use with OrbitControls

Near the beginning of this page it was mentioned that it would be desirable to have this controller work alongside existing camera position controllers (OrbitControls.js and TrackballControls.js, basically). So far we've tried this with OrbitControls.js, and the solution is presented below.

Be advised that at this stage the code changes to OrbitControls.js feel like a bit of a hack, because they may override or fail to take into account the broader flexibility which the original code was written for. They also haven't had much in the way of testing.

We attempted to make the camera and object functionality similar - the camera now shares the ‘rotate' and ‘translate' mode scheme. However with the two sets of controls combined, there ends up being a substantial number of independent ways of interacting with the scene, which is potentially quite confusing. In fact, there may well be too many controls now for it to be intuitive. But see what you think.

To use both controllers, load both scripts as well as three.js. Make sure you use our version of OrbitControls.js.

<script src="js/three.js"></script>
<script src="js/PointDragControls.js"></script>
<script src="js/OrbitControls.js"></script>

Don't forget to give the controllers different names!

var drag_controls = new THREE.PointDragControls();
drag_controls.init(scene,camera, renderer);
orbit_controls = new THREE.OrbitControls( camera, document, renderer.domElement );

Finally, you should pass the array of objects you are using PointDragControls with to a newly introduced property in OrbitControls ignoreObjects:

orbit_controls.ignoreObjects = scene.children; // or the objects you want drag control over

This tells OrbitControls not to move the camera when you are dragging an object.

Demo with Orbit Controls

Encouragingly, this demo does illustrate the coordinate system independence of PointDragControls. Try rotating objects with the camera in different positions - you should find the rotation axes are always aligned with the camera, according to the "polar" system outlined earlier.

An updated version of the control scheme is given below:
There are 2 modes, translate and rotate. Double click on empty canvas to change mode.

In translate mode:

  • Left click and drag, or touch and drag, on an object to move it in the plane of the camera. (from PointDragControls.js)
  • Left click and drag, or touch and drag, on empty canvas to pan the camera (from OrbitControl s.js)
  • Right click on an object and drag mouse pointer in vertical direction to move it in direction camera is pointing (ie closer/further away) (from PointDragControls.js)
  • Right click on empty canvas and drag mouse pointer in vertial direction to move whole scene closer/further away (from OrbitControls.js)

In rotate mode:

  • Left click and drag, or touch and drag, on an object in a vertical direction to rotate it about a horizontal axis. (from PointDragControls.js)
  • Left click and drag, or touch and drag, on an object in a horizontal direction to rotate it about a vertical axis. (from PointDragControls.js)
  • Left click and drag, or touch and drag, on empty canvas rotate the camera (from OrbitControl s.js)
  • Right click on an object and drag mouse pointer in vertical direction to move it in direction camera is pointing (ie closer/further away) (from PointDragControls.js)
  • Right click on empty canvas and drag mouse pointer in vertial direction to move whole scene closer/further away (from OrbitControl s.js)

In rotate mode:

  • "dolly" (move the camera forward/backward) using the mouse wheel

Feedback

If you found this controller useful or interesting, we'd like to hear from you. If you thought it wasn't useful at all, we'd still like to hear from you. And if you have unresolved questions...  guess what? Get in touch and let us know your thoughts!

We wish you many happy rotations!