• 创建动画和移动相机


    1.如何通过鼠标获取网格对象

        首先需要把鼠标的起始位置在左上角的屏幕坐标转换为笛卡尔坐标。然后将坐标转为为以Camera为中心点的三维空间坐标。接下来根据摄像头位置和鼠标位置的法向量创建射线对象。最终根据射线对象的intersectObjects函数确认哪个网格被选中。

        下面是比较经典的使用方法:

    function onDocumentMouseMove(event) {
                if (controls.showRay) {
                    var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
                    vector = vector.unproject(camera);
    
                    var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
                    var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
    
                    if (intersects.length > 0) {
    
                        var points = [];
                        points.push(new THREE.Vector3(-30, 39.8, 30));
                        points.push(intersects[0].point);
    
                        var mat = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.6});
                        var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), 60, 0.001);
    
                        if (tube) scene.remove(tube);
    
                        if (controls.showRay) {
                            tube = new THREE.Mesh(tubeGeometry, mat);
                            scene.add(tube);
                        }
                    }
                }
            }

    2.使用Tween.js动画

        Tween.js是一个小型的Javascript库,可以从http://github.com/sole/tween.js/下载。这个库可以 用来定义某个属性在两个值之间的过度,自动计算出起始值和结束值之间的所有中间值。这个过程叫做tweening(补间)。例如下面的代码:

    var pointCloud = new THREE.Object3D();
            var loadedGeometry;
    
            var posSrc = {pos: 1};
            var tween = new TWEEN.Tween(posSrc).to({pos: 0}, 5000);
            tween.easing(TWEEN.Easing.Sinusoidal.InOut);
    
            var tweenBack = new TWEEN.Tween(posSrc).to({pos: 1}, 5000);
            tween.easing(TWEEN.Easing.Sinusoidal.InOut);
    
            tween.chain(tweenBack);
    
            var onUpdate = function(){
                var count = 0;
                var pos = this.pos;
    
                loadedGeometry.vertices.forEach(function(e){
                   var newY = ((e.y + 3.22544) * pos) - 3.22544;
                    pointCloud.geometry.vertices[count++].set(e.x, newY, e.z);
                });
    
                pointCloud.sortParticles = true;
            }
    
            tween.onUpdate(onUpdate);
            tweenBack.onUpdate(onUpdate);
    
            var loader = new THREE.PLYLoader();
            loader.load("../assets/models/test.ply", function(geometry){
                loadedGeometry = geometry.clone();
                var material = new THREE.PointCloudMaterial({
                    color: 0xffffff,
                    size: 0.4,
                    opacity: 0.6,
                    transparent: true,
                    blending: THREE.AdditiveBlending,
                    map: generateSprite()
                });
    
                pointCloud = new THREE.PointCloud(geometry, material);
                pointCloud.sortParticles = true;
    
                tween.start();
                scene.add(pointCloud);
            });

        代码定义了两个补间对象tween和tweenBack,让pos值从1减到0,再从0增加到1。tween会在中间按照动画效果补充很多中间pos值,调用tween.OnUpdate给补间动画注册一个回调事件,这个回到事件中可获取补间值(this.pos)。我们可通过这个补间值来更新坐标值从而实现动画。另外我们可以调用tween.easing指定补间动画按照那种动画效果产生。

        设置完成后,需要调用tween.start()启动动画。但现在我们还不知道什么时候执行补间更新通知。所以我们可以在渲染函数每次执行时调用。

    function render() {
                stats.update();
                TWEEN.update();
                requestAnimationFrame(render);
                webGLRenderer.render(scene, camera);
            }

    3.相机控件

        Three.js提供了几个相机控件,可以用来控制场景中的相机。这些控件在Three.js发布包中,控件包括:

        控件名称/描述

        FirstPersonControls(第一人称控件)/该控件的行为类似第一人称设计游戏中的相机,用键盘移动,用鼠标转动

        FlyControls(飞行控件)/飞机模拟器控件,用键盘和鼠标来控制相机的移动和转动

        RollControls/该控件时FlyControls的简化版,让你可以绕着z轴旋转

        TrackballControls(轨迹球控件)/最常用的控件,你可以用鼠标(或轨迹球)来轻松移动、平移和缩放场景

        OrbitControls(轨道控件)/用于特定场景,模拟轨道中的卫星,你可以用鼠标和键盘在场景中游走

        PathControls(路径控件)/使用这个控件,相机可以沿着预定义的路径移动。你可以将它跟过山车相比较,在过山车上你可以朝四周看,但不能改变自身位置

    4.轨迹球控件TrackballControls

        使用TrackballConrols之前需要引入TrackballControls.js文件。通过控件可以旋转、缩放、平移网格,并且操作速度可以控制。例下面一段代码实现了轨迹球控制功能。首先创建一个轨迹球控件对象,并设置旋转、缩放、移动速度。代码使用OBJMTLLoader把一个外部模型加载进来,setRandomColors函数用来随机设置外部模型外建筑的材质颜色。通过递归查询类型为THREE.Mesh对象,然后设置其材质的环境色以及透明度等参数。

    var trackballControls = new THREE.TrackballControls(camera);
                trackballControls.rotateSpped = 1.0;
                trackballControls.zoomSpeed = 1.0;
                trackballControls.panSpeed = 1.0;
                trackballControls.staticMoving = true;
    
                var ambientLight = new THREE.AmbientLight(0x383838);
                scene.add(ambientLight);
    
                var spotLight = new THREE.SpotLight(0xffffff);
                spotLight.position.set(300, 300, 300);
                spotLight.intensity = 1;
                scene.add(spotLight);
    
                // add the output of the renderer to the html element
                document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
    
                var step = 0;
    
                var mesh;
    
                var loader = new THREE.OBJMTLLoader();
                var load = function(object){
                    var scale = chroma.scale(["red", "green", "blue"]);
                    setRandomColors(object, scale);
                    mesh = object;
                    scene.add(mesh);
                }
    
                render();
    
                function setRandomColors(object, scale){
                    var children = object.children;
    
                    if(children && children.length > 0){
                        children.forEach(function(e){
                            setRandomColors(e, scale);
                        });
                    }else{
                        if(object instanceof THREE.Mesh){
                            object.material.color = new THREE.Color(scale(Math.random()).hex());
                            if(object.material.name.indexOf("building") === 0){
                                object.material.emissive = new THREE.Color(0x444444);
                                object.material.transparent = true;
                                object.material.opacity = 0.8;
                            }
                        }
                    }
                }
    
                function render(){
                    stats.update();
                    var delta = clock.getDelta();
                    trackballControls.update(delta);
                    requestAnimationFrame(render);
                    webGLRenderer.render(scene, camera);
                }

        这里使用了一个颜色操作的库chroma.js,它用来生成随机颜色。还需要注意的是,我们需要调用TrackballControls的update(delta)函数更新相机的位置。delta是此次调用和上次调用的时间间隔。

        如何求时间间隔?这里我们使用Three.js自带的THREE.Clock对象,我们在初始化时就创建对象,在下次渲染时可调用它的getDelta()函数获取本次和上次的时间间隔。

        使用TrackballControls,可以通过以下操作方式来旋转、缩放、移动网格:

        操作/动作

        按住左键,拖动/在场景中旋转、翻滚相机

        转动鼠标滚轮/放大和缩小

        按住中间,拖动/放大和缩小

        按住右键,拖动/在场景中平移

    5.飞行控件FlyControls

        和TrackballControls功能相似。首先需要引入FlyControls.js文件。我们可以配置控件,并绑定到相机。

    var flyControls = new THREE.FlyControls(camera);
            flyControls.movementSpeed = 25;
            flyControls.domElement = document.querySelector("#WebGL-output");
            flyControls.rollSpeed = Math.PI/24;
            flyControls.dragToLook = false;

        控件必须设置domElement属性,该属性和WebGLRenderer指向同一个Dom元素。movementSpeed设置移动速度,rollSpeed设置滚动速度,dragToLook设置鼠标悬浮时还是鼠标按下时移动摄像头。

        最后也别忘了在render函数中调用flyControls.update(delta)去移动摄像头。控件操控方式如下:

        操控/动作

        按住左键和中间/往前移动

        按住右键/往后移动

        鼠标移动/往四周看

        W/往前移动, S/往后移动,A/左移,D/右移,R/上移,F/下移

        上、下、左、右键/向上、下、左、右看

        Q/向左翻滚

        E/向右翻滚

    6.第一人称控件FirstPersonControls

        第一人称控件对应的js库名称为FirstPersonControls.js,使用前需引入该js文件。下面实例化该对象的代码:

    var camControls = new THREE.FirstPersonControls(camera);
            camControls.lookSpeed = 0.4;
            camControls.movementSpeed = 20;
            camControls.noFly = true;
            camControls.lookVertical = true;
            camControls.constrainVertical = true;
            camControls.verticalMin = 1.0;
            camControls.verticalMax = 2.0;
            camControls.lon = -150;
            camControls.lat = 120;

        使用该控件时只有最后两个属性(lon、lat)需要小心对待。这两个属性定义的是场景初次渲染时相机指向的位置。操控方式如下:

        操控/动作

        移动鼠标/往四周看

        上、下、左、右方向键/前、后、左、右移动

        W/前移,A/左移,S/后移,D/右移,R/上移,F/下移, Q/停止

    7.轨道控件OrbitControl

        OrbitControl控件时在场景中绕某个对象旋转、平移的好方法。OrbitControl是Three.js的扩展库,对应OrbitControls.js文件。实例化代码如下:

    var orbitControls = new THREE.OrbitControls(camera);
            orbitControls.autoRotate = true;

        代码中设置了autoRotate属性,使摄像头绕着lookAt位置旋转。OrbitControl也支持鼠标和键盘操作。操作如下:

        操控/动作

        按住左键,并移动/绕着场景中心旋转相机

        按住滚动或按住中间,并移动/放大缩小

        按住右键,并移动/在场景中移动

        上、下、左、右方向键/在场景中移动

    8.用MorphAnimMesh制作动画

        hree.js提供一种方法使得模型可以从一个位置移到另一个位置,但是这也意味着我们可能不得不手工记录当前所处的位置,以及下一个变形目标的位置。一旦达到目标位置,我们就得重复这个过程已达到下一个位置。幸运的是,Three.js提供了一个特别的网格,MorphAnimMesh(变形动画网格),该网格帮我们处理这些细节。

        下面是使用MorphAnimMesh的一段代码:

    var loader = new THREE.JSONLoader();
    loader.load("../assets/models/horse.js", function(geometry, mat){
        var mat = new THREE.MeshLambertMaterial({
            morphTargets: true,
            vertexColors: THREE.FaceColors
        });
    
        var mat2 = new THREE.MeshLambertMaterial({
            vertexColors: THREE.FaceColors,
            color: 0xffffff
        });
    
        mesh = new THREE.Mesh(geometry, mat);
        mesh.position.x = -100;
        frames.push(mesh);
        currentMesh = mesh;
        morphColorsToFaceColors(geometry);
    
        mesh.geometry.morphTargets.forEach(function(e){
            var geom = new THREE.Geometry();
            geom.vertices = e.vertices;
            geom.faces = geometry.faces;
    
            var morphMesh = new THREE.Mesh(geom, mat2);
            frames.push(morphMesh);
            morphMesh.position.x = -100;
        });
    
        geometry.computeVertexNormals();
        geometry.computeFaceNormals();
        geometry.computeMorphNormals();
    
        meshAnim = new THREE.MorphAnimMesh(geometry, mat);
        meshAnim.duration = 1000;
        meshAnim.position.x = 200;
        meshAnim.position.z = 0;
    
        scene.add(meshAnim);
    
        showFrame(0);
    }, "../assets/models");
    
    function showFrame(e){
        scene.remove(currentMesh);
        scene.add(frames[e]);
        currentMesh = frames[e];
        console.log(currentMesh);
    }
    
    function morphColorsToFaceColors(geom){
        if(geom.morphColors && geom.morphColors.length){
            var colorMap = geom.morphColors[0];
            for(var i = 0; i < colorMap.colors.length; i++){
                geom.faces[i].color = colorMap.colors[i];
                geom.faces[i].color.offsetHSL(0, 0.3, 0);
            }
        }
    }

        代码从外部加载了一个json格式的模型,当加载完成后,创建一个材质设置属性morphTargets为true,这样网格才会动起来。所有动画几何体都存储在mesh.geometry.morphTargets数组中,我们可以遍历该数组直接读取他获取不同位置的几何体。
        导入的几何体我们还需要分别调用几何体的computeVertexNormals()、computeFaceNormals()、computeMorphNormals()函数重新计算顶点、面、变形发向量。最后使用MorphAnimMesh对象创建一个动画网格,并设置duration以及position属性等。和其他动画控件一样,要让网格动起来,每次渲染时还得调用updateAnimation函数,代码如下:

    function render(){
        stats.update();
        var delta = clock.getDelta();
        if(meshAnim){
            meshAnim.updateAnimation(delta * 1000);
            meshAnim.rotation.y += 0.01;
        }
    
        webGLRenderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    9.通过设置morphTargetInfluence属性创建动画

        网格包含morphTargetInflences属性,他对应了geometry的morphTargets数组。如下面的代码,cubeGeometry的morphTargets包含了两个值,对应了两个不同的顶点集合。在controls中的update函数,我们设置了cube的morphTargetInfluences属性。morphTargetInfluences[0]相当于是morphTargets[0]的动画时间戳,值从0到1。当morphTargetInfluences[0]等于0,网格显示的是cube原始的顶点,当morphTargetInfluences[0]等于1时cube的顶点完全过度到morphTargets[0]了。

    var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
    var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000, morphTargets: true});
    
    var cubeTarget1 = new THREE.BoxGeometry(2, 10, 2);
    var cubeTarget2 = new THREE.BoxGeometry(8, 2, 8);
    
    cubeGeometry.morphTargets[0] = {name: "t1", vertices: cubeTarget2.vertices};
    cubeGeometry.morphTargets[1] = {name: "t2", vertices: cubeTarget1.vertices};
    
    var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.position.x = 0;
    cube.position.y = 3;
    cube.position.z = 0;
    scene.add(cube);
    
    
    var controls = new function(){
        this.influence1 = 0.01;
        this.influence2 = 0.02;
    
        this.update = function(){
            cube.morphTargetInfluences[0] = controls.influence1;
            cube.morphTargetInfluences[1] = controls.influence2;
        }
    };

        加入我们在render函数中逐渐提增influences的值,那么我们就可以看到变形动画了。代码如下:

    function render() {
        stats.update();
    
        controls.influence1 += 0.001;
        controls.influence2 += 0.001;
        controls.update();
    
        // render using requestAnimationFrame
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    10.用骨骼和蒙皮制作动画

        骨骼动画比变形动画复杂些。当你用骨骼来做动画时,你移动一下骨骼,而Three.js必须决定如何相应地迁移附着在骨骼上的皮肤。针对此动画Three.js提供了SkinnedMesh网格对象,但我们修改它骨骼属性,该对象自动处理皮肤的位置。下面是的例子加载了一个骨骼手臂,并设置了它的位置属性。

    var loader = new THREE.JSONLoader();
    loader.load('../assets/models/hand-1.js', function (geometry, mat) {
        var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
        mesh = new THREE.SkinnedMesh(geometry, mat);
    
        // rotate the complete hand
        mesh.rotation.x = 0.5 * Math.PI;
        mesh.rotation.z = 0.7 * Math.PI;
    
        // add the mesh
        scene.add(mesh);
    
        // and start the animation
        tween.start();
    
        }, '../assets/models');
    
        var onUpdate = function () {
        var pos = this.pos;
    
        console.log(mesh.skeleton);
    
        // rotate the fingers
        mesh.skeleton.bones[5].rotation.set(0, 0, pos);
        mesh.skeleton.bones[6].rotation.set(0, 0, pos);
        mesh.skeleton.bones[10].rotation.set(0, 0, pos);
        mesh.skeleton.bones[11].rotation.set(0, 0, pos);
        mesh.skeleton.bones[15].rotation.set(0, 0, pos);
        mesh.skeleton.bones[16].rotation.set(0, 0, pos);
        mesh.skeleton.bones[20].rotation.set(0, 0, pos);
        mesh.skeleton.bones[21].rotation.set(0, 0, pos);
    
        // rotate the wrist
        mesh.skeleton.bones[1].rotation.set(pos, 0, 0);
    };
    
    var tween = new TWEEN.Tween({pos: -1})
    .to({pos: 0}, 3000)
    .easing(TWEEN.Easing.Cubic.InOut)
    .yoyo(true)
    .repeat(Infinity)
    .onUpdate(onUpdate);

         代码用了TWEEN动画库,具体的api可以在官网查看。这里主要介绍onUpdate函数,动画在执行时,tween的pos属性值也在变化,逐渐从-1变动0,正好用这个属性来设置骨骼对象的rotation属性。mesh.skeleton.bones包含了很多个骨骼对象,具体要设置哪一个,需要了解清楚模型文件。上面的代码只是实现了动画,要让骨骼动起来,还得在render函数中调用:TWEEN.update()。

    11.用Blender创建骨骼动画

        使用Blender可以创建动画,我们可以使用three.js导出插件导出包含动画的模型。在导出时需要注意一下细节:
        模型中的顶点至少要在一个顶点组中;
        Blender中顶点组的名字必须跟控制这个顶点组的骨头的名字相对应。只有这样,当过被移除时Three.js才能找到需要修改的顶点;
        只有第一个action(动作)可以导出,所以要保证你想要导出的动画时第一个action;
        创建keyframs时,最后选择所有骨头,即便没有改变;
        导出模型时,要保证模型处于静止状态。如果不这样,那么你看到的动画将会非常混乱;
        导出模型之后,使用JSONLoader加载模型。使用THREE.Animation创建动画对象。然后调用animation.play()函数开始播放动画。Three.js提供了一个辅助类SkeletonHelper,它可以通过连线查看我们的动画效果。示例代码如下:

    var loader = new THREE.JSONLoader();
    loader.load('../assets/models/hand-2.js', function (model, mat) {
    
        var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
        mesh = new THREE.SkinnedMesh(model, mat);
    
        var animation = new THREE.Animation(mesh, model.animation);
    
        mesh.rotation.x = 0.5 * Math.PI;
        mesh.rotation.z = 0.7 * Math.PI;
        scene.add(mesh);
    
        helper = new THREE.SkeletonHelper(mesh);
        helper.material.linewidth = 2;
        helper.visible = false;
        scene.add(helper);
    
        // start the animation
        animation.play();
    
    }, '../assets/models');
    和其他模型一样,在render函数中需要调用update函数。代码如下:
    function render() {
        stats.update();
    
    
        var delta = clock.getDelta();
        if (mesh) {
    
            helper.update();
            THREE.AnimationHandler.update(delta);
    
        }
    
    
        // render using requestAnimationFrame
        requestAnimationFrame(render);
        webGLRenderer.render(scene, camera);
    }

    12.加载Collada动画

        加载Collada动画和其他加载方式相似。这里使用的是ColladaLoader加载器,加载完成返回模型是包含了整个场景。根据需要我们只取skins里边的骨骼网格。取出之后根据这个网格创建animation动画,并根据实际显示设置网格的位置和缩放。示例代码如下:

    var loader = new THREE.ColladaLoader();
    loader.load('../assets/models/monster.dae', function (collada) {
        var child = collada.skins[0];
        scene.add(child);
    
        var animation = new THREE.Animation(child, child.geometry.animation);
        animation.play();
    
        // position the mesh
        child.scale.set(0.15, 0.15, 0.15);
        child.rotation.x = -0.5 * Math.PI;
        child.position.x = -100;
        child.position.y = -60;
    });

        当然,在render函数中我们还是的调用THREE.AnimationHandler.update(delta),根据时间戳更新动画。

    13.加载MD2动画

        MD2格式是设计用来构建雷神之锤的角色模型。尽管新一代引擎使用了不同的格式,但是你依然可以找到很多MD2格式的模型。在使用该模型时需要将其转换为Three.js格式的javascript文件。所以我们直接使用JSONLoader加载。动画可以调用mesh.playAnimation(animationName, fps)执行动画,由于模型文件提供了很多动画,所以我们需要传递一个name,让mesh知道执行哪个动画。在执行动画之前,还得重新计算下集合体的法向量。下面是加载并执行md2动画的示例代码:

    var loader = new THREE.JSONLoader();
    loader.load("../assets/models/ogre/ogro.js", function(geometry, mat){
        geometry.computeMorphNormals();
    
        var mat = new THREE.MeshLambertMaterial({
            map: THREE.ImageUtils.loadTexture("../assets/models/ogre/skins/skin.jpg"),
            morphTargets: true,
            morphNormals: true
        });
    
        mesh = new THREE.MorphAnimMesh(geometry, mat);
    
        mesh.rotation.y = 0.7;
        mesh.parseAnimations();
    
        var animLabels = [];
        for(var key in mesh.geometry.animations){
            if(key === "length" || !mesh.geometry.animations.hasOwnProperty(key)) continue;
            animLabels.push(key);
        }
    
        gui.add(controls, "animations", animLabels).onChange(function(e){
            mesh.playAnimation(controls.animations, controls.fps);
        });
    
        gui.add(controls, "fps", 1, 20).onChange(function(e){
            mesh.playAnimation(controls.animations, controls.fps);
        });
    
        mesh.playAnimation("crattack", 10);
    
        scene.add(mesh);
    });

        特别需要注意的是,加载进来的动画列表是空的,我们需要调用mesh.parseAnimations()函数把动画都转换出来。接下来我们可以遍历mesh.geometry.animations获取所有动画名称。想要动画执行起来,还得在render中调用 mesh.updateAnimation(delta * 1000)函数。clock.getDelta()获取的时间戳是单位是秒,所以要乘以1000。

  • 相关阅读:
    libreoffice转PDF文件
    window端设置elasticsearch的堆内存大小
    kubeadm 部署 K8S 1.21.5
    GitLab缓存问题
    【博客园美化】参考资料
    【Qt】QObject::connect: Cannot queue arguments of type 'QVector<int>' Make sure 'QVector<int>' is registered using qRegisterMetaType()解决办法
    【jeecgboot】启动报错
    验证下MarkDown
    nginx代理转发因http_version导致lua读取文件不存在问题
    怎么看mac是arm64还是x64
  • 原文地址:https://www.cnblogs.com/w-wanglei/p/6759972.html
Copyright © 2020-2023  润新知