admin管理员组

文章数量:1390685

I am in the process of creating a game using Three.JS and I have modeled and successfully imported a city created in Sketchup. I now need to dynamically add some "follow me" arrows (as per the yellow arrows in the mockup below). I believe I might need to use Three.CurvePath to achieve this but am not sure if this is the best approach - do I need to manually model the path and calculate the tangent for each of the arrow objects so they point naturally around corners (as per left turn in the mockup)?

Hope this makes sense!

I am in the process of creating a game using Three.JS and I have modeled and successfully imported a city created in Sketchup. I now need to dynamically add some "follow me" arrows (as per the yellow arrows in the mockup below). I believe I might need to use Three.CurvePath to achieve this but am not sure if this is the best approach - do I need to manually model the path and calculate the tangent for each of the arrow objects so they point naturally around corners (as per left turn in the mockup)?

Hope this makes sense!

Share edited Apr 6, 2013 at 10:04 Sidebp asked Apr 2, 2013 at 21:31 SidebpSidebp 7802 gold badges10 silver badges27 bronze badges 1
  • Related: stackoverflow./questions/11179327/… – WestLangley Commented Apr 7, 2013 at 16:42
Add a ment  | 

1 Answer 1

Reset to default 9 +100

I might have a solution. Haven't used three.js in a while so not sure if this is the most elegant solution. I started with the Shapes Example since it shows:

  1. How to build a path procedurally (using mands that also handle curves)
  2. How to extract points from such a path

So I've split the problem into:

  1. Generating the path
  2. Traversing the path and interpolation (position and rotation)

Generating the path I've reused the rounded rectangle definition which looks similar to a part of your screenshot.

var roundedRectShape = new THREE.Shape();

                ( function roundedRect( ctx, x, y, width, height, radius ){

                    ctx.moveTo( x, y + radius );
                    ctx.lineTo( x, y + height - radius );
                    ctx.quadraticCurveTo( x, y + height, x + radius, y + height );
                    ctx.lineTo( x + width - radius, y + height) ;
                    ctx.quadraticCurveTo( x + width, y + height, x + width, y + height - radius );
                    ctx.lineTo( x + width, y + radius );
                    ctx.quadraticCurveTo( x + width, y, x + width - radius, y );
                    ctx.lineTo( x + radius, y );
                    ctx.quadraticCurveTo( x, y, x, y + radius );

                } )( roundedRectShape, 0, 0, 200, 200, 20 );

Your path might not be a rounded rectangle, but the available types of curve functions(quadraticCurveTo,bezierCurveTo,splineThru) are really useful.

Another idea that es to mind is using a Ruby script to export path coordinates from Sketchup to three.js. Either you write that from scratch or use existing scripts. Here's one easily found on google.

Traversing the path

Luckily three.js already implements this through Path's getPoint(t) where t is a number from 0.0 to 1.0 representing the traversal on the path. So getting the position is trivial as getting the next interpolated position on the path. Then it's just a matter of using Math.atan2() to get the rotation:

t = (t + s)%1.0;//increment t while maintaining it between 0.0 and 1.0
                var p = path.getPoint(t);//point at t
                var pn = path.getPoint((t+s)%1.0);//point at next t iteration

                if(p != null && pn != null){
                    //move to current position
                    arrow.position.x = p.x;
                    arrow.position.y = p.y;
                    //get orientation based on next position
                    arrow.rotation.z = Math.atan2(pn.y-p.y,pn.x-p.x);

                }

In conclusion bellow is a basic example (using a cube instead of an arrow shape) to illustrate generating and traversing a path in three.js based on the shapes example:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>path interpolation</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
        <style>
            body {
                font-family: Monospace;
                background-color: #f0f0f0;
                margin: 0px;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <canvas id="debug" style="position:absolute; left:100px"></canvas>

        <script src="../build/three.min.js"></script>

        <script src="js/libs/stats.min.js"></script>


        <script>

            var container, stats;

            var camera, scene, renderer;

            var text, plane;

            var targetRotation = 0;
            var targetRotationOnMouseDown = 0;

            var mouseX = 0;
            var mouseXOnMouseDown = 0;

            var windowHalfX = window.innerWidth / 2;
            var windowHalfY = window.innerHeight / 2;

            init();
            animate();

            var t = 0.0;//traversal on path
            var s = 0.001;//speed of traversal
            var arrow;//mesh to move/rotate on path
            var path;//Path object to traverse

            function init() {

                container = document.createElement( 'div' );
                document.body.appendChild( container );

                camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1000 );
                camera.position.set( 0, 150, 500 );

                scene = new THREE.Scene();

                parent = new THREE.Object3D();
                parent.position.y = 50;
                scene.add( parent );

                arrow = new THREE.Mesh( new THREE.CubeGeometry(20,10,10),new THREE.MeshBasicMaterial({color: 0x009900}));
                parent.add(arrow);
                //this is helpful as a visual aid but not crucial
                function addShape( shape, extrudeSettings, color, x, y, z, rx, ry, rz, s ) {

                    var points = shape.createPointsGeometry();
                    var spacedPoints = shape.createSpacedPointsGeometry( 50 );

                    // transparent line from equidistance sampled points

                    var line = new THREE.Line( spacedPoints, new THREE.LineBasicMaterial( { color: color, opacity: 0.2 } ) );
                    line.rotation.set( rx, ry, rz );
                    parent.add( line );

                    // equidistance sampled points

                    var pgeo = spacedPoints.clone();
                    var particles2 = new THREE.ParticleSystem( pgeo, new THREE.ParticleBasicMaterial( { color: color, size: 2, opacity: 0.5 } ) );
                    particles2.rotation.set( rx, ry, rz );
                    parent.add( particles2 );

                }


                // Rounded rectangle
                //generating the path and populating it is crucial tough
                var roundedRectShape = new THREE.Shape();

                ( function roundedRect( ctx, x, y, width, height, radius ){

                    ctx.moveTo( x, y + radius );
                    ctx.lineTo( x, y + height - radius );
                    ctx.quadraticCurveTo( x, y + height, x + radius, y + height );
                    ctx.lineTo( x + width - radius, y + height) ;
                    ctx.quadraticCurveTo( x + width, y + height, x + width, y + height - radius );
                    ctx.lineTo( x + width, y + radius );
                    ctx.quadraticCurveTo( x + width, y, x + width - radius, y );
                    ctx.lineTo( x + radius, y );
                    ctx.quadraticCurveTo( x, y, x, y + radius );

                } )( roundedRectShape, 0, 0, 200, 200, 20 );
                path = roundedRectShape;

                var extrudeSettings = { amount: 20 }; // bevelSegments: 2, steps: 2 , bevelSegments: 5, bevelSize: 8, bevelThickness:5
                extrudeSettings.bevelEnabled = true;
                extrudeSettings.bevelSegments = 2;
                extrudeSettings.steps = 2;

                addShape( roundedRectShape, extrudeSettings, 0x000000, -150, 150, 0, 0, 0, 0, 1 );

                renderer = new THREE.WebGLRenderer( { antialias: true } );
                renderer.setSize( window.innerWidth, window.innerHeight );

                container.appendChild( renderer.domElement );

                stats = new Stats();
                stats.domElement.style.position = 'absolute';
                stats.domElement.style.top = '0px';
                container.appendChild( stats.domElement );

                document.addEventListener( 'mousedown', onDocumentMouseDown, false );
                document.addEventListener( 'touchstart', onDocumentTouchStart, false );
                document.addEventListener( 'touchmove', onDocumentTouchMove, false );

                //

                window.addEventListener( 'resize', onWindowResize, false );

            }

            function onWindowResize() {

                windowHalfX = window.innerWidth / 2;
                windowHalfY = window.innerHeight / 2;

                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();

                renderer.setSize( window.innerWidth, window.innerHeight );

            }

            //

            function onDocumentMouseDown( event ) {

                event.preventDefault();

                document.addEventListener( 'mousemove', onDocumentMouseMove, false );
                document.addEventListener( 'mouseup', onDocumentMouseUp, false );
                document.addEventListener( 'mouseout', onDocumentMouseOut, false );

                mouseXOnMouseDown = event.clientX - windowHalfX;
                targetRotationOnMouseDown = targetRotation;

            }

            function onDocumentMouseMove( event ) {

                mouseX = event.clientX - windowHalfX;

                targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.02;

            }

            function onDocumentMouseUp( event ) {

                document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
                document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
                document.removeEventListener( 'mouseout', onDocumentMouseOut, false );

            }

            function onDocumentMouseOut( event ) {

                document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
                document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
                document.removeEventListener( 'mouseout', onDocumentMouseOut, false );

            }

            function onDocumentTouchStart( event ) {

                if ( event.touches.length == 1 ) {

                    event.preventDefault();

                    mouseXOnMouseDown = event.touches[ 0 ].pageX - windowHalfX;
                    targetRotationOnMouseDown = targetRotation;

                }

            }

            function onDocumentTouchMove( event ) {

                if ( event.touches.length == 1 ) {

                    event.preventDefault();

                    mouseX = event.touches[ 0 ].pageX - windowHalfX;
                    targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.05;

                }

            }

            //

            function animate() {

                requestAnimationFrame( animate );

                render();
                stats.update();

            }

            function render() {
                t = (t + s)%1.0;//increment t while maintaining it between 0.0 and 1.0
                var p = path.getPoint(t);//point at t
                var pn = path.getPoint((t+s)%1.0);//point at next t iteration

                if(p != null && pn != null){
                    //move to current position
                    arrow.position.x = p.x;
                    arrow.position.y = p.y;
                    //get orientation based on next position
                    arrow.rotation.z = Math.atan2(pn.y-p.y,pn.x-p.x);

                }

                parent.rotation.y += ( targetRotation - parent.rotation.y ) * 0.05;
                renderer.render( scene, camera );

            }

        </script>

    </body>
</html>

Thought I'd add a runnable snippet straight on this page:

            var container;

            var camera, scene, renderer;

            var text, plane;

            var targetRotation = 0;
            var targetRotationOnMouseDown = 0;

            var mouseX = 0;
            var mouseXOnMouseDown = 0;

            var windowHalfX = window.innerWidth / 2;
            var windowHalfY = window.innerHeight / 2;

            init();
            animate();

            var t = 0.0;//traversal on path
            var s = 0.001;//speed of traversal
            var arrows;//mesh to move/rotate on path
            var path;//Path object to traverse

            function init() {

                container = document.createElement( 'div' );
                document.body.appendChild( container );

                camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1000 );
                camera.position.set( 0, 150, 500 );

                scene = new THREE.Scene();

                parent = new THREE.Object3D();
                parent.position.y = 50;
                scene.add( parent );
                
                arrows = [];
                for(var i = 0 ; i < 50; i++){
                arrows[i] = new THREE.Mesh( new THREE.CubeGeometry(10,5,5),new THREE.MeshBasicMaterial({color: 0x009900}));
                parent.add(arrows[i]);
                }
                //this is helpful as a visual aid but not crucial
                function addShape( shape, extrudeSettings, color, x, y, z, rx, ry, rz, s ) {

                    var points = shape.createPointsGeometry();
                    var spacedPoints = shape.createSpacedPointsGeometry( 50 );

                    // transparent line from equidistance sampled points

                    var line = new THREE.Line( spacedPoints, new THREE.LineBasicMaterial( { color: color, opacity: 0.2 } ) );
                    line.rotation.set( rx, ry, rz );
                    parent.add( line );

                    // equidistance sampled points

                    var pgeo = spacedPoints.clone();
                    var particles2 = new THREE.ParticleSystem( pgeo, new THREE.ParticleBasicMaterial( { color: color, size: 2, opacity: 0.5 } ) );
                    particles2.rotation.set( rx, ry, rz );
                    parent.add( particles2 );

                }


                // Rounded rectangle
                //generating the path and populating it is crucial tough
                var roundedRectShape = new THREE.Shape();

                ( function roundedRect( ctx, x, y, width, height, radius ){

                    ctx.moveTo( x, y + radius );
                    ctx.lineTo( x, y + height - radius );
                    ctx.quadraticCurveTo( x, y + height, x + radius, y + height );
                    ctx.lineTo( x + width - radius, y + height) ;
                    ctx.quadraticCurveTo( x + width, y + height, x + width, y + height - radius );
                    ctx.lineTo( x + width, y + radius );
                    ctx.quadraticCurveTo( x + width, y, x + width - radius, y );
                    ctx.lineTo( x + radius, y );
                    ctx.quadraticCurveTo( x, y, x, y + radius );

                } )( roundedRectShape, 0, 0, 200, 200, 20 );
                path = roundedRectShape;

                var extrudeSettings = { amount: 20 }; // bevelSegments: 2, steps: 2 , bevelSegments: 5, bevelSize: 8, bevelThickness:5
                extrudeSettings.bevelEnabled = true;
                extrudeSettings.bevelSegments = 2;
                extrudeSettings.steps = 2;

                addShape( roundedRectShape, extrudeSettings, 0x000000, -150, 150, 0, 0, 0, 0, 1 );

                renderer = new THREE.WebGLRenderer( { antialias: true } );
                renderer.setSize( window.innerWidth, window.innerHeight );

                container.appendChild( renderer.domElement );

               

                document.addEventListener( 'mousedown', onDocumentMouseDown, false );
                document.addEventListener( 'touchstart', onDocumentTouchStart, false );
                document.addEventListener( 'touchmove', onDocumentTouchMove, false );

                //

                window.addEventListener( 'resize', onWindowResize, false );

            }

            function onWindowResize() {

                windowHalfX = window.innerWidth / 2;
                windowHalfY = window.innerHeight / 2;

                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();

                renderer.setSize( window.innerWidth, window.innerHeight );

            }

            //

            function onDocumentMouseDown( event ) {

                event.preventDefault();

                document.addEventListener( 'mousemove', onDocumentMouseMove, false );
                document.addEventListener( 'mouseup', onDocumentMouseUp, false );
                document.addEventListener( 'mouseout', onDocumentMouseOut, false );

                mouseXOnMouseDown = event.clientX - windowHalfX;
                targetRotationOnMouseDown = targetRotation;

            }

            function onDocumentMouseMove( event ) {

                mouseX = event.clientX - windowHalfX;

                targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.02;

            }

            function onDocumentMouseUp( event ) {

                document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
                document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
                document.removeEventListener( 'mouseout', onDocumentMouseOut, false );

            }

            function onDocumentMouseOut( event ) {

                document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
                document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
                document.removeEventListener( 'mouseout', onDocumentMouseOut, false );

            }

            function onDocumentTouchStart( event ) {

                if ( event.touches.length == 1 ) {

                    event.preventDefault();

                    mouseXOnMouseDown = event.touches[ 0 ].pageX - windowHalfX;
                    targetRotationOnMouseDown = targetRotation;

                }

            }

            function onDocumentTouchMove( event ) {

                if ( event.touches.length == 1 ) {

                    event.preventDefault();

                    mouseX = event.touches[ 0 ].pageX - windowHalfX;
                    targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.05;

                }

            }

            //

            function animate() {

                requestAnimationFrame( animate );

                render();
 
            }

            function render() {
                t = (t + s)%1.0;//increment t while maintaining it between 0.0 and 1.0 - could map mouse x position/window width for fun :)
				for(var i = 0 ; i < 50; i++){//for each box
					var ti = ((i/50.0)+t)%1.0;//pute the traversval including each box's own offset on the path
					
					var p = path.getPoint(ti);//point at t
					var pn = path.getPoint((ti+s)%1.0);//point at next t iteration
				
					if(p != null && pn != null){
						//move to current position
						arrows[i].position.x = p.x;
						arrows[i].position.y = p.y;
						//get orientation based on next position
						arrows[i].rotation.z = Math.atan2(pn.y-p.y,pn.x-p.x);
					
					}
				}
                parent.rotation.y += ( targetRotation - parent.rotation.y ) * 0.05;
                renderer.render( scene, camera );

            }
            body {
                font-family: Monospace;
                background-color: #f0f0f0;
                margin: 0px;
                overflow: hidden;
            }
<script src="https://cdnjs.cloudflare./ajax/libs/three.js/r71/three.min.js"></script>

本文标签: javascriptThreeCurvePath and custom markersStack Overflow