ThreeJS学习笔记(三)——三维空间用户交互与动画

拾取器raycaster

ThreeJS提供了一个 raycaster的API用于返回用户光标所在位置的所有3维元素,它的实现原理是在屏幕上某个二维坐标点与相机位置和视角形成的向量方向上投射一条射线,返回与射线相交的所有三维物体的集合,集合的第一个物体为距离相机最近的物体,最后一个则为离相机最远的。
当使用拾取器去获取用户点击的物体时,需要事先将所有可参与用户交互的三维物体放到一个集合里。在创建拾取器后获取两个集合的交集,即当前用户在屏幕点击的位置上所有被设置为可被选择的物体,第一个即可视为用户直接点击的物体。

拾取器示例

以下代码段实现当用户鼠标移动到object1和object2上时鼠标指针形状变为pointer;点击时将相机旋转到物体正面

var _raycaster = new THREE.Raycaster();//拾取器
var raycAsix=new THREE.Vector2();//屏幕点击点二维坐标
var _curObj=null;//当前点击物体
function onDocumentMouseMove( event ) {
    event.preventDefault();
    raycAsix.x = ( (event.pageX-$(container).offset().left) / container.offsetWidth ) * 2 - 1;
    raycAsix.y = - ( (event.pageY-$(container).offset().top) /container.offsetHeight ) * 2 + 1;
    _raycaster.setFromCamera(raycAsix, camera );
    var intersects = _raycaster.intersectObjects( clickObjects );//获取投射线上与用户预设的可被点击物体的集合的交集
    if ( intersects.length > 0 ) {
        document.body.style.cursor = 'pointer';
        console.log(intersects[0].object.name);
    }else{
        document.body.style.cursor = 'default';
    }
}
    
function onDocumentClick( event ) {
    
    event.preventDefault();
    _raycaster.setFromCamera( raycAsix, camera );
    var intersects = _raycaster.intersectObjects( clickObjects );
    if(intersects.length== 0){
        return;
        
    }
    if ( intersects.length > 0 &&intersects[ 0 ].object!=_curObj) {
        if(_userView.curObj ==intersects[ 0 ].object){
            return;
        }
        _curObj =intersects[ 0 ].object;
        rotateTo(intersects[ 0 ]);//点击时旋转到物体的位置
    }
}
        

关于动画

动画一般是在render()函数里处理,实时修改元素的位置大小等。
上面的rotateTo()函数里旋转动画是使用一个 tween.js实现缓动,并在render()中根据缓动计算的数值去修改相机的位置。大部分交互动画需要使用运动曲线的都可以使用此插件完成。

GITHUP地址:https://github.com/tweenjs/tween.js

  • 利用TweenJS的缓动曲线改变相机的theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R
var _userView={};//用于存储相机theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R,使用这三个变量去修改相机的位置
function rotateTo(obj){//
    _isRotateing=true;
    controls.enabled = false;
    var point=obj.point;
    var pointAngle=Math3D.get3DAngle(point.x,point.y,point.z);//点击点的角度和球半径
    var toAngle={//需要旋转到的用户视角的角度和半径
        theta:pointAngle.theta,
        phi:30/180*Math.PI,
        r:1000
    }       
    _userView.cameraPosTo=Math3D.get3DAxis(toAngle.theta,toAngle.phi,toAngle.r);//旋转用户视角停止时摄像机位置
    _userView.dmy={};
    _userView.dmy.theta=Math3D.getAngleByAxis2d({x:camera.position.x,y:camera.position.z});//当前摄像机与Z轴的水平夹角
    _userView.dmy.r=Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z);//当前摄像机离坐标轴原点的水平距离
    _userView.dmy.y=camera.position.y;//当前摄像机的Y点坐标
    
    var dmyStop={};//相机将到移动到的最终位置
    dmyStop.theta=Math3D.getAngleByAxis2d({x:point.x,y:point.z});//旋转到用户点击点所在位置时摄像机与Z轴的水平夹角
    dmyStop.r=1000;//旋转到用户点击点所在位置时摄像机与坐标原点的水平距离
    dmyStop.y=300;//旋转到用户点击点所在位置时摄像机Y点坐标
    var tween = new TWEEN.Tween(_userView.dmy).to(dmyStop, 1000).easing(TWEEN.Easing.Quadratic.InOut)
    .onComplete(function(){
        _isRotateing=false;
        controls.enabled = true;
    })
    .start();//设置缓动动画
}
  • 在render函数里根据缓动计算出来的相机的theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R去计算相机的position并设置。
function render() {

    if(_isRotateing){//用户点击行为执行旋转动画
        var newCameraPos=Math3D.getAxis2dByAngle(_userView.dmy.theta,_userView.dmy.r);
        camera.position.x=newCameraPos.x;
        camera.position.y=_userView.dmy.y;
        camera.position.z=newCameraPos.y;
    }else {//自动旋转
        var newCameraPos=Math3D.getRotateAxis2d({
            x:camera.position.x,
            y:camera.position.z
        },-0.001,0);
        camera.position.x=newCameraPos.x;
        camera.position.z=newCameraPos.y;
    }
    camera.lookAt( scene.position );
    renderer.render( scene, camera );

}

  • 注意要在animate函数中执行TWEEN.update();才会更新_userView变量
function animate() {
        requestAnimationFrame( animate );
        controls.update();
        TWEEN.update();
        render();
}
  • 如果不需要做二次计算也可以直接使用TweenJS去设置动画元素的属性如:
var tween = new TWEEN.Tween(camera.position).to({x:100,y:100,z:100}, 1000).easing(TWEEN.Easing.Quadratic.InOut).start();

本章示例


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <div id="space"></div>  
    <script   src="https://code.jquery.com/jquery-1.12.4.min.js"   integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="   crossorigin="anonymous"></script>
    <script src="../js/lib/threejs/three.js"></script>
    <script src="../js/lib/threejs/MTLLoader.js"></script>
    <script src="../js/lib/threejs/OBJLoader.js"></script>
    <script src="../js/lib/threejs/OrbitControls.js"></script>
    <script src="../js/lib/Tween.js"></script>
    <script>
        var Math3D=function(window,document){
    function _createRandomCoord(maxR,minR){
        var r=Math.round(Math.random()*(maxR-minR))+minR;
        var theta=Math.random()*Math.PI*2;
        //console.log(theta+"="+theta/Math.PI*180);
        var phi=Math.random()*Math.PI*2;
        //console.log(phi+"="+phi/Math.PI*180);
        
        return get3DAxis(theta,phi,r);
    }
    function get3DAxis(theta,phi,r){
        //X=rsinθcosφ y=rsinθsinφ z=rcosθ
        return{
            x:r*Math.sin(theta)*Math.cos(phi),
            y:r*Math.sin(theta)*Math.sin(phi),
            z:r*Math.cos(theta)
        }
    }
    function get3DAngle(x,y,z){
        //r=sqrt(x*2 + y*2 + z*2); θ= arccos(z/r); φ=arctan(y/x);
        var r=Math.sqrt(x*x + y*y + z*z);
        return{
            theta:Math.acos(z/r),
            phi:Math.atan(y/x),
            r:r
        }
    }
    function getAngle(point){
                return Math.atan2(point.y,point.x)//atan2自带坐标系识别, 注意X,Y的顺序
            }
    function Rotate(source,angle,rudius)//Angle为正时逆时针转动, 单位为弧度
    {
        var A,R;
        A = getAngle(source);
        A += angle;//旋转
        R = Math.sqrt(source.x * source.x + source.y * source.y)//半径
        if(rudius){
            R-=rudius
        }
        return {
            x : Math.cos(A) * R,
            y : Math.sin(A) * R
        }
    }
    function getpositionFromAngel(A,R)//Angle为正时逆时针转动, 单位为弧度
    {
        
        return {
            x : Math.cos(A) * R,
            y : Math.sin(A) * R
        }
    }
    
    return{
        createRandomCoord:_createRandomCoord,
        getAngleByAxis2d:getAngle,
        getRotateAxis2d:Rotate,
        getAxis2dByAngle:getpositionFromAngel,
        get3DAxis:get3DAxis,
        get3DAngle:get3DAngle
    }
}(window,document,undefined);


        var container, stats;

        var camera, scene, renderer,controls;

        var mouseX = 0, mouseY = 0;

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

        var clickObjects=[];
        var _raycaster = new THREE.Raycaster();
        var raycAsix=new THREE.Vector2();
        var _curObj=null,_isRotateing=false;
        var _userView={};
        init();
        animate();
        var mesh;

        function init() {

            container = document.getElementById("space")
            camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 8000 );
            camera.position.set(0, 0, 1500);
            
            scene = new THREE.Scene();

            var ambient = new THREE.AmbientLight( 0xffffff );
            scene.add( ambient );
            
            
            var directionalLight = new THREE.DirectionalLight( 0xffffff );
            directionalLight.position.set( -5, 5, 5).normalize();
            scene.add( directionalLight );

            var pointlight = new THREE.PointLight(0x63d5ff, 1, 200); 
            pointlight.position.set(0, 0, 200);
            scene.add( pointlight );                
            var pointlight2 = new THREE.PointLight(0xffffff, 1, 200); 
            pointlight2.position.set(-200, 200, 200);
            scene.add( pointlight2 );
            var pointlight3 = new THREE.PointLight(0xffffff, 1.5, 200); 
            pointlight3.position.set(-200, 200, 0);
            scene.add( pointlight3 );
            scene.add( new THREE.PointLightHelper( pointlight3 ) );
            scene.add( new THREE.PointLightHelper( pointlight2 ) );
            scene.add( new THREE.PointLightHelper( pointlight ) );
        
            
            var path = "../resource/sky";
            var format = '.jpg';
            var urls = [
                    path + 'px' + format, path + 'nx' + format,
                    path + 'py' + format, path + 'ny' + format,
                    path + 'pz' + format, path + 'nz' + format
                ];
            var skyMaterials = []; 
            for (var i = 0; i < urls.length; ++i) {
                var loader = new THREE.TextureLoader();
                loader.setCrossOrigin( this.crossOrigin );
                var texture = loader.load( urls[i], function(){}, undefined, function(){} );
                
                skyMaterials.push(new THREE.MeshBasicMaterial({
                    //map: THREE.ImageUtils.loadTexture(urls[i], {},function() { }), 
                    map: texture, 
                    overdraw: true,
                    side: THREE.BackSide,
                    //transparent: true,
                    //needsUpdate:true,
                    premultipliedAlpha: true
                    //depthWrite:true,
                    
    //              wireframe:false,
                })
                ); 
                
            } 
            
            var cube = new THREE.Mesh(new THREE.CubeGeometry(4000, 4000,4000), new THREE.MeshFaceMaterial(skyMaterials)); 
            cube.name="sky";
            scene.add(cube);
            
            createMtlObj({
                mtlBaseUrl:"../resource/haven",
                mtlPath: "../resource/haven",
                mtlFileName:"threejs.mtl",
                objPath:"../resource/haven",
                objFileName:"threejs.obj",
                completeCallback:function(object){
                    object.traverse(function(child) { 
                        if (child instanceof THREE.Mesh) { 
                            child.material.side = THREE.DoubleSide;
                            child.material.emissive.r=0;
                            child.material.emissive.g=0.01;
                            child.material.emissive.b=0.05;
                            child.material.transparent=true;
                            //child.material.opacity=0;                     
                            //child.material.shading=THREE.SmoothShading;
                            clickObjects.push(child);
                        }
                    });

                    object.emissive=0x00ffff;
                    object.ambient=0x00ffff;
                    //object.rotation.x= 10/180*Math.PI;
                    object.position.y = 0;
                    object.position.z = 0;
                    object.scale.x=1;
                    object.scale.y=1;
                    object.scale.z=1;
                    object.name="haven";
                    object.rotation.y=-Math.PI;
                    scene.add(object);
                },
                progress:function(persent){
                    
                    $("#havenloading .progress").css("width",persent+"%");
                }
            })
            controls = new THREE.OrbitControls(camera,container);
            controls.maxPolarAngle=1.5;
            controls.minPolarAngle=1;
            controls.enableDamping=true;
            controls.enableKeys=false;
            controls.enablePan=false;
            controls.dampingFactor = 0.1;
            controls.rotateSpeed=0.1;
    //      controls.enabled = false;
            controls.minDistance=1000;
            controls.maxDistance=3000;
            
            renderer = new THREE.WebGLRenderer();
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            container.appendChild( renderer.domElement );
            
            window.addEventListener( 'resize', onWindowResize, false );
            window.addEventListener( 'mousemove', onDocumentMouseMove, false ); 
            window.addEventListener( 'click', onDocumentClick, false );     
        }
        
        
        function createMtlObj(options){
        //      options={
        //          mtlBaseUrl:"",
        //          mtlPath:"",
        //          mtlFileName:"",
        //          objPath:"",
        //          objFileName:"",
        //          completeCallback:function(object){  
        //          }
        //          progress:function(persent){
        //              
        //          }
        //      }
                //THREE.Loader.Handlers.add( //.dds$/i, new THREE.DDSLoader() );
            var mtlLoader = new THREE.MTLLoader();
            mtlLoader.setBaseUrl( options.mtlBaseUrl );
            mtlLoader.setPath( options.mtlPath );
            mtlLoader.load( options.mtlFileName, function( materials ) {
                materials.preload();
                var objLoader = new THREE.OBJLoader();
                objLoader.setMaterials( materials );
                objLoader.setPath( options.objPath );
                objLoader.load( options.objFileName, function ( object ) {
                    if(typeof options.completeCallback=="function"){
                        options.completeCallback(object);
                    }
                }, function ( xhr ) {
                    if ( xhr.lengthComputable ) {
                        var percentComplete = xhr.loaded / xhr.total * 100;
                        if(typeof options.progress =="function"){
                            options.progress( Math.round(percentComplete, 2));
                        }
                        //console.log( Math.round(percentComplete, 2) + '% downloaded' );
                    }
                }, function(error){
                    
                } );
        
            });
        }
        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 onDocumentMouseMove( event ) {
            event.preventDefault();
            
            raycAsix.x = ( (event.pageX-$(container).offset().left) / container.offsetWidth ) * 2 - 1;
            raycAsix.y = - ( (event.pageY-$(container).offset().top) /container.offsetHeight ) * 2 + 1;
            _raycaster.setFromCamera(raycAsix, camera );
            var intersects = _raycaster.intersectObjects( clickObjects );
            if ( intersects.length > 0 ) {
                document.body.style.cursor = 'pointer';
                console.log(intersects[0].object.name);
            }else{
                document.body.style.cursor = 'default';
            }
        }
            
        function onDocumentClick( event ) {
            
            event.preventDefault();
            _raycaster.setFromCamera( raycAsix, camera );
            var intersects = _raycaster.intersectObjects( clickObjects );
            if(intersects.length== 0){
                return;
                resetRotate();
            }
            if ( intersects.length > 0 &&intersects[ 0 ].object!=_curObj) {
                if(_userView.curObj ==intersects[ 0 ].object){
                    return;
                }
                _curObj =intersects[ 0 ].object;
                rotateTo(intersects[ 0 ]);
            }
        }
        
        function rotateTo(obj){
            _isRotateing=true;
            controls.enabled = false;
            var point=obj.point;
            var pointAngle=Math3D.get3DAngle(point.x,point.y,point.z);//点击点的角度和球半径
            var toAngle={//需要旋转到的用户视角的角度和半径
                theta:pointAngle.theta,
                phi:30/180*Math.PI,
                r:1000
            }
            
            _userView.cameraPosTo=Math3D.get3DAxis(toAngle.theta,toAngle.phi,toAngle.r);//旋转用户视角停止时摄像机位置
            _userView.dmy={};
            _userView.dmy.theta=Math3D.getAngleByAxis2d({x:camera.position.x,y:camera.position.z});//当前摄像机与Z轴的水平夹角
            _userView.dmy.r=Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z);//当前摄像机离坐标轴原点的水平距离
            _userView.dmy.y=camera.position.y;//当前摄像机的Y点坐标
            
            var dmyStop={};
            dmyStop.theta=Math3D.getAngleByAxis2d({x:point.x,y:point.z});//旋转到用户点击点所在位置时摄像机与Z轴的水平夹角
            dmyStop.r=1000;//用户视角模式时摄像机与坐标原点的水平距离
            dmyStop.y=300;//用户视角模式时摄像机Y点坐标
            var tween = new TWEEN.Tween(_userView.dmy).to(dmyStop, 1000).easing(TWEEN.Easing.Quadratic.InOut)
            .onComplete(function(){
                _isRotateing=false;
                controls.enabled = true;
            })
            .start();//设置缓动动画
        }
        function animate() {
            requestAnimationFrame( animate );
            controls.update();
            TWEEN.update();
            render();
        }
        function render() {
//              camera.position.x += ( mouseX - camera.position.x ) ;
//              camera.position.y += ( mouseY - camera.position.y ) ;
            if(_isRotateing){
                var newCameraPos=Math3D.getAxis2dByAngle(_userView.dmy.theta,_userView.dmy.r);
                camera.position.x=newCameraPos.x;
                camera.position.y=_userView.dmy.y;
                camera.position.z=newCameraPos.y;
            }else {
                var newCameraPos=Math3D.getRotateAxis2d({
                    x:camera.position.x,
                    y:camera.position.z
                },-0.001,0);
                camera.position.x=newCameraPos.x;
                camera.position.z=newCameraPos.y;
        
                
            }
            
            camera.lookAt( scene.position );
            renderer.render( scene, camera );

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



来源:http://feg.netease.com/archives/401.html

WEBGL学习网(WebGLStudy.COM)专注提供WebGL 、ThreeJS、BabylonJS等WEB3D开发案例源码下载。
声明信息:
1. 本站部分资源来源于用户上传和网络,如有侵权请邮件联系站长:1218436398@qq.com!我们将尽快处理。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源打赏售价用于赞助本站提供的服务支出(包括不限服务器、网络带宽等费用支出)!
7.欢迎加QQ群学习交流:549297468 ,或者搜索微信公众号:WebGL学习网
WEBGL学习网 » ThreeJS学习笔记(三)——三维空间用户交互与动画

发表评论

提供优质的WebGL、ThreeJS源码

立即查看 了解详情