three.js 入門詳解(二)

9. 動畫

在本章以前,全部畫面都是靜止的,本章將介紹若是使用Three.js進行動態畫面的渲染。此外,將會介紹一個Three.js做者寫的另一個庫,用來觀測每秒幀數(FPS)。javascript

9.1 實現動畫效果

9.1.1 動畫原理

  • 在這裏,咱們將動態畫面簡稱爲動畫(animation)。正如動畫片的原理同樣,動畫的本質是利用了人眼的視覺暫留特性,快速地變換畫面,從而產生物體在運動的假象。而對於Three.js程序而言,動畫的實現也是經過在每秒鐘屢次重繪畫面實現的。
  • 爲了衡量畫面切換速度,引入了每秒幀數FPS(Frames Per Second)的概念,是指每秒畫面重繪的次數。FPS越,則動畫效果越平滑,當FPS小於20時,通常就能明顯感覺到畫面的卡滯現象。
  • 那麼FPS是否是越大越好呢?其實也未必。當FPS足夠大(好比達到60),再增長幀數人眼也不會感覺到明顯的變化,反而相應地就要消耗更多資源(好比電影的膠片就須要更長了,或是電腦刷新畫面須要消耗計算資源等等)。所以,選擇一個適中的FPS便可。
  • NTSC標準的電視FPS是30,PAL標準的電視FPS是25,電影的FPS標準爲24。而對於Three.js動畫而言,通常FPS在3060之間都是可取的。

9.1.2 setInterval方法

若是要設置特定的FPS(雖然嚴格來講,即便使用這種方法,JavaScript也不能保證幀數精確性),可使用JavaScript DOM定義的方法:html

setInterval(fn,mesc)
  • 其中,fn是每過msec毫秒執行的函數,若是將fn定義爲重繪畫面的函數,就能實現動畫效果。setInterval函數返回一個變量timer,若是須要中止重繪,須要使用clearInterval方法,並傳入該變量timer,具體的作法爲:
  • 一、首先,在init函數中定義每20毫秒執行draw函數的setInterval,返回值記錄在全局變量timer中:
timer = setInterval(draw,20);
  • 二、在draw函數中,咱們首先設定在每幀中的變化(畢竟,若是每幀都是相同的,即便重繪再屢次,仍是不會有動畫的效果),這裏咱們讓場景中的長方體繞y軸轉動。而後,執行渲染:
function draw() {
    // 每過20ms 就會執行一次這個函數,rotation.y就會加0.01
    // 轉完360度就會進行取餘,因此就會一直轉下去
    mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
    renderer.render(scene, camera);
}
  • 這樣,每20毫秒就會調用一次draw函數,改變長方體的旋轉值,而後進行重繪。最終獲得的效果就是FPS50的旋轉長方體。
  • 三、咱們在HTML中添加兩個按鈕,一個是按下後中止動畫,另外一個是按下後繼續動畫:
<button id="stopBtn" onclick="stop()">Stop</button> 
<button id="startBtn" onclick="start()">Start</button>
  • 四、對應的stopstart函數爲:
function stop() {
    if (timer !== null) {
        clearInterval(timer);
        timer = null;
    }
}

function start() {
    if (timer == null) {
        clearInterval(timer);
        timer = setInterval(draw, 20);
    }
}
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>動畫效果</title>
    <script type="text/javascript" src="js/three.js"></script>

    <script type="text/javascript">
        var scene = null;
        var camera = null;
        var renderer = null;

        var mesh = null;
        var timer = null;

        function init() {
            renderer = new THREE.WebGLRenderer({
                canvas: document.getElementById('mainCanvas')
            });
            renderer.setClearColor(0x000000);
            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 5, 20);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
                new THREE.MeshLambertMaterial({
                    color: 0xffff00
                }));
            scene.add(mesh);

            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(20, 10, 5);
            scene.add(light);

            timer = setInterval(draw, 20);
        }

        function draw() {
            mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
            renderer.render(scene, camera);
        }

        function stop() {
            if (timer !== null) {
                clearInterval(timer);
                timer = null;
            }
        }

        function start() {
            if (timer == null) {
                clearInterval(timer);
                timer = setInterval(draw, 20);

            }
        }
    </script>
</head>

<body onload="init()">
    <canvas id="mainCanvas" width="800px" height="600px"></canvas>
    <button id="stopBtn" onclick="stop()">Stop</button>
    <button id="startBtn" onclick="start()">Start</button>
</body>

</html>
  • 效果圖:

image

9.1.3 requestAnimationFrame方法

大多數時候,咱們並不在乎多久重繪一次,這時候就適合用requestAnimationFrame方法了。它告訴瀏覽器在合適的時候調用指定函數,一般可能達到60FPSjava

  • requestAnimationFrame一樣有對應的cancelAnimationFrame取消動畫:
function stop() {
    if (timer !== null) {
        cancelAnimationFrame(timer);
        timer = null;
    }
}
  • setInterval不一樣的是,因爲requestAnimationFrame只請求一幀畫面,所以,除了在init函數中須要調用,在被其調用的函數中須要再次調用requestAnimationFrame
function draw() {
    mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
    renderer.render(scene, camera);
    timer = requestAnimationFrame(draw);
}
  • 由於requestAnimationFrame較爲「年輕」,於是一些老的瀏覽器使用的是試驗期的名字:mozRequestAnimationFramewebkitRequestAnimationFramemsRequestAnimationFrame,爲了支持這些瀏覽器,咱們最好在調用以前,先判斷是否認義了requestAnimationFrame以及上述函數:
var requestAnimationFrame = window.requestAnimationFrame 
        || window.mozRequestAnimationFrame
        || window.webkitRequestAnimationFrame
        || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>動畫效果</title>
    <script type="text/javascript" src="js/three.js"></script>

    <script type="text/javascript">
        var requestAnimationFrame = window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
        window.requestAnimationFrame = requestAnimationFrame;

        var scene = null;
        var camera = null;
        var renderer = null;

        var mesh = null;
        var timer = null;

        function init() {
            renderer = new THREE.WebGLRenderer({
                canvas: document.getElementById('mainCanvas')
            });
            renderer.setClearColor(0x000000);
            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 5, 20);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
                new THREE.MeshLambertMaterial({
                    color: 0xffff00
                }));
            scene.add(mesh);

            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(20, 10, 5);
            scene.add(light);

            id = requestAnimationFrame(draw);
        }

        function draw() {
            mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
            renderer.render(scene, camera);
            timer = requestAnimationFrame(draw);
        }

        function stop() {
            if (timer !== null) {
                cancelAnimationFrame(timer);
                timer = null;
            }
        }

        function start() {
            if (timer == null) {
                timer = requestAnimationFrame(draw);
            }
        }
    </script>
</head>

<body onload="init()">
    <canvas id="mainCanvas" width="800px" height="600px"></canvas>
    <button id="stopBtn" onclick="stop()">Stop</button>
    <button id="startBtn" onclick="start()">Start</button>
</body>

</html>

setInterval和requestAnimationFrame的區別:node

  • setInterval方法與requestAnimationFrame方法的區別較爲微妙。一方面,最明顯的差異表如今setInterval能夠手動設定FPS,而requestAnimationFrame則會自動設定FPS;但另外一方面,即便是setInterval也不能保證按照給定的FPS執行,在瀏覽器處理繁忙時,極可能低於設定值。當瀏覽器達不到設定的調用週期時,requestAnimationFrame採用跳過某些幀的方式來表現動畫,雖然會有卡滯的效果可是總體速度不會拖慢,而setInterval會所以使整個程序放慢運行,可是每一幀都會繪製出來;
  • 總而言之,requestAnimationFrame適用於對於時間較爲敏感的環境(可是動畫邏輯更加複雜),而setInterval則可在保證程序的運算不至於致使延遲的狀況下提供更加簡潔的邏輯(無需自行處理時間)。

9.2 使用stat.js記錄FPS

stat.js是Three.js的做者Mr.Doob的另外一個有用的JavaScript庫。不少狀況下,咱們但願知道實時的FPS信息,從而更好地監測動畫效果。這時候,stat.js就能提供一個很好的幫助,它佔據屏幕中的一小塊位置(如左上角),效果爲:image,單擊後顯示每幀渲染時間:imagegit

<script type="text/javascript" src="stat.js"></script>
  • 在頁面初始化的時候,對其初始化並將其添加至屏幕一角。這裏,咱們以左上角爲例:
var stat = null;

function init() {
    stat = new Stats();
    stat.domElement.style.position = 'absolute';
    stat.domElement.style.left = '0px';
    stat.domElement.style.top = '0px';
    document.body.appendChild(stat.domElement);

    // Three.js init ...
}
  • 而後,在上一節介紹的動畫重繪函數draw中調用stat.begin();stat.end();分別表示一幀的開始與結束:
function draw() {
    stat.begin();

    mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
    renderer.render(scene, camera);

    stat.end();
}
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>stats</title>
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript" src="Stats.js"></script>

    <script type="text/javascript">
        var requestAnimationFrame = window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
        window.requestAnimationFrame = requestAnimationFrame;

        var scene = null;
        var camera = null;
        var renderer = null;

        var mesh = null;
        var id = null;

        var stat = null;

        function init() {
            stat = new Stats();
            stat.domElement.style.position = 'absolute';
            stat.domElement.style.left = '0px';
            stat.domElement.style.top = '0px';
            document.body.appendChild(stat.domElement);

            renderer = new THREE.WebGLRenderer({
                canvas: document.getElementById('mainCanvas')
            });
            renderer.setClearColor(0x000000);
            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 5, 20);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
                new THREE.MeshLambertMaterial({
                    color: 0xffff00
                }));
            scene.add(mesh);

            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(20, 10, 5);
            scene.add(light);

            timer = requestAnimationFrame(draw);
        }

        function draw() {
            stat.begin();

            mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
            renderer.render(scene, camera);
            timer = requestAnimationFrame(draw);

            stat.end();
        }

        function stop() {
            if (timer !== null) {
                cancelAnimationFrame(timer);
                timer = null;
            }
        }
    </script>
</head>

<body onload="init()">
    <canvas id="mainCanvas" width="800px" height="600px"></canvas>
    <button id="stopBtn" onclick="stop()">Stop</button>

</body>

</html>

9.3 彈球案例

本節咱們將使用一個彈球的例子來完整地學習使用動畫效果。github

  • 一、首先,咱們把通用的框架部分寫好,按照以前的方法實現動畫重繪函數,並加入stat.js庫:
var requestAnimationFrame = window.requestAnimationFrame 
        || window.mozRequestAnimationFrame
        || window.webkitRequestAnimationFrame
        || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;

var stat;
var renderer;
var scene;
var camera;
var light;

function init() {
    stat = new Stats();
    stat.domElement.style.position = 'absolute';
    stat.domElement.style.left= '0px';
    stat.domElement.style.top = '0px';
    document.body.appendChild(stat.domElement);

    renderer = new THREE.WebGLRenderer({
        canvas: document.getElementById('mainCanvas')
    });
    scene = new THREE.Scene();

    timer = requestAnimationFrame(draw);
}

function draw() {
    stat.begin();

    renderer.render(scene, camera);

    timer = requestAnimationFrame(draw);

    stat.end();
}

function stop() {
    if (timer !== null) {
        cancelAnimationFrame(timer);
        timer = null;
    }
}
  • 二、而後,爲了實現彈球彈動的效果,咱們建立一個球體做爲彈球模型,建立一個平面做爲彈球反彈的平面。爲了在draw函數中改變彈球的位置,咱們能夠聲明一個全局變量ballMesh,以及彈球半徑ballRadius
var ballMesh;
var ballRadius = 0.5;
  • 三、在init函數中添加球體平面,使彈球位於平面上,平面採用棋盤格圖像做材質:
// 加載貼圖
texture = THREE.ImageUtils.loadTexture('images/chess.png', {}, function() {
    renderer.render(scene, camera);
});
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);

// 平面模型
var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8),
    new THREE.MeshLambertMaterial({
        map: texture
    }));
// 沿x軸旋轉-90度
plane.rotation.x = Math.PI / -2;
scene.add(plane);

// 球模型
ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 40, 16),
    new THREE.MeshLambertMaterial({
        color: 0xffff00
    }));
  • 四、爲了記錄彈球的狀態,咱們至少須要位置速度加速度三個矢量,爲了簡單起見,這裏彈球只作豎直方向上的自由落體運動,所以位置、速度、加速度只要各用一個變量表示。其中,位置就是ballMesh.position.y,不須要額外的變量,所以咱們在全局聲明速度v加速度a
var v = 0;
var a = -0.01;
  • 這裏,a = -0.01表明每幀小球向y方向負方向移動0.01個單位。
  • 五、一開始,彈球從高度爲maxHeight(本身定義的一個高度)處自由下落,掉落到平面上時會反彈,而且速度有損耗。當速度很小的時候,彈球會在平面上做振幅微小的抖動,因此,當速度足夠小時,咱們須要讓彈球中止跳動。所以,定義一個全局變量表示是否在運動,初始值爲false
var isMoving = false;
  • 六、在HTML中定義一個按鈕,點擊按鈕時,彈球從最高處下落:
<button id="dropBtn" onclick="drop()">Drop</button>

<script>
    function drop() {
        isMoving = true;
        ballMesh.position.y = maxHeight;
        v = 0;
    }
</script>
  • 七、下面就是最關鍵的函數了,在draw函數中,須要判斷當前的isMoving值,而且更新小球的速度和位置:
function draw() {
    stat.begin();
    if (isMoving) {
        ballMesh.position.y += v;
        // a= -0.01
        v += a;
        // 當小球從定義的高度落到小球停在平面時的高度的時候
        if (ballMesh.position.y <= ballRadius) {
            // 讓小球彈起來
            v = -v * 0.9;
        }
        // 當小球的速度小於設定值的時候
        if (Math.abs(v) < 0.001) {
            // 讓它停下來
            isMoving = false;
            ballMesh.position.y = ballRadius;
        }
    }
    renderer.render(scene, camera);
    requestAnimationFrame(draw);
    stat.end();
}
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>彈彈彈</title>
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript" src="Stats.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        
        body {
            position: fixed;
        }
        
        button {
            position: fixed;
            left: 50%;
            margin-left: -50px;
            bottom: 20%;
            width: 100px;
            height: 30px;
            background-color: #d3d3d3;
            border: none;
            border-radius: 15px;
            outline: none;
            font-size: 18px;
            font-weight: 700;
            color: #333;
            box-shadow: -1px -1px 1px #fff, 1px 1px 1px #000;
        }
    </style>
    <script>
        var stat;
        var renderer;
        var scene;
        var camera;
        var light;
        var texture;
        var ballMesh;
        var ballRadius = 0.5;

        var isMoving = false;
        var maxHeight = 5;
        var v = 0;
        var a = -0.01;

        function init() {
            // 處理requireAnimationFrame兼容性
            var requestAnimationFrame = window.requestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.msRequestAnimationFrame;
            window.requestAnimationFrame = requestAnimationFrame;

            // FPS 插件
            stat = new Stats();
            stat.domElement.style.position = 'absolute';
            stat.domElement.style.left = '0px';
            stat.domElement.style.top = '0px';
            document.body.appendChild(stat.domElement);

            // 渲染器
            renderer = new THREE.WebGLRenderer({
                antialias: true
            });
            width = window.innerWidth;
            height = window.innerHeight;
            renderer.setSize(width, height);
            document.body.appendChild(renderer.domElement);
            renderer.setClearColor(0xd3d3d3);

            // 場景
            scene = new THREE.Scene();

            // 相機
            camera = new THREE.OrthographicCamera(width / -128, width / 128, height / 128, height / -128, 1, 1000);
            camera.position.set(10, 15, 25);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            // 添加光照
            light = new THREE.DirectionalLight(0xffffff);
            light.position.set(-10, 30, 25);
            scene.add(light);

            // 加載貼圖
            texture = THREE.ImageUtils.loadTexture('images/chess.png', {}, function() {
                renderer.render(scene, camera);
            });
            texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
            texture.repeat.set(4, 4);

            // 平面模型
            var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8),
                new THREE.MeshLambertMaterial({
                    map: texture
                }));
            // 沿x軸旋轉-90度
            plane.rotation.x = Math.PI / -2;
            scene.add(plane);

            // 球模型
            ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 40, 16),
                new THREE.MeshLambertMaterial({
                    color: 0xffff00
                }));
            // 設置球的位置
            ballMesh.position.y = ballRadius;
            scene.add(ballMesh);

            // 座標軸
            /* drawAxes(scene);
            function drawAxes(scene) {
                // x-axis
                var xGeo = new THREE.Geometry();
                xGeo.vertices.push(new THREE.Vector3(0, 0, 0));
                xGeo.vertices.push(new THREE.Vector3(7, 0, 0));
                var xMat = new THREE.LineBasicMaterial({
                    color: 0xff0000
                });
                var xAxis = new THREE.Line(xGeo, xMat);
                scene.add(xAxis);

                // y-axis
                var yGeo = new THREE.Geometry();
                yGeo.vertices.push(new THREE.Vector3(0, 0, 0));
                yGeo.vertices.push(new THREE.Vector3(0, 7, 0));
                var yMat = new THREE.LineBasicMaterial({
                    color: 0x00ff00
                });
                var yAxis = new THREE.Line(yGeo, yMat);
                scene.add(yAxis);

                // z-axis
                var zGeo = new THREE.Geometry();
                zGeo.vertices.push(new THREE.Vector3(0, 0, 0));
                zGeo.vertices.push(new THREE.Vector3(0, 0, 7));
                var zMat = new THREE.LineBasicMaterial({
                    color: 0x00ccff
                });
                var zAxis = new THREE.Line(zGeo, zMat);
                scene.add(zAxis);

            } */
            requestAnimationFrame(draw);
        }

        // 計算球運動的速度和位置
        function draw() {
            stat.begin();
            if (isMoving) {
                ballMesh.position.y += v;
                // a= -0.01
                v += a;
                // 當小球從定義的高度落到小球停在平面時的高度的時候
                if (ballMesh.position.y <= ballRadius) {
                    // 讓小球彈起來
                    v = -v * 0.9;
                }
                // 當小球的速度小於設定值的時候
                if (Math.abs(v) < 0.001) {
                    // 讓它停下來
                    isMoving = false;
                    ballMesh.position.y = ballRadius;
                }
            }
            renderer.render(scene, camera);
            requestAnimationFrame(draw);
            stat.end();
        }
        // 觸發函數
        function drop() {
            isMoving = true;
            // 小球起落位置
            ballMesh.position.y = maxHeight;
            // 加速度爲0
            v = 0;
        }
    </script>
</head>

<body onload="init();">
    <button id="dropBtn" onclick="drop();">Drop</button>
</body>

</html>
  • 效果圖:

image

10. 外部模型

前面咱們瞭解到,使用Three.js建立常見幾何體是十分方便的,可是對於人或者動物這樣很是複雜的模型使用幾何體組合就很是麻煩了。所以,Three.js容許用戶導入由3ds Max等工具製做的三維模型,並添加到場景中。web

  • 本章以3ds Max爲例,介紹如何導入外部模型。

10.1 支持格式

Three.js有一系列導入外部文件的輔助函數,是在three.js以外的,使用前須要額外下載,在https://github.com/mrdoob/three.js/tree/master/examples/js/loaders能夠找到,選擇對應的模型加載器,系在下來。npm

  • *.obj是最經常使用的模型格式,導入*.obj文件須要OBJLoader.js;導入帶*.mtl材質的*.obj文件須要MTLLoader.js以及OBJMTLLoader.js。另有PLYLoader.jsSTLLoader.js等分別對應不一樣格式的加載器,能夠根據模型格式自行選擇。
  • 目前,支持的模型格式有:canvas

    • *.obj
    • *.obj, *.mtl
    • *.dae
    • *.ctm
    • *.ply
    • *.stl
    • *.wrl
    • *.vtk

10.2 無材質的模型

本節中,咱們將將導出的沒有材質的模型使用Three.js導入場景中。瀏覽器

  • 首先,下載OBJLoader.js並在HTML的<head>中使用:
<script type="text/javascript" src="OBJLoader.js"></script>
  • 而後,咱們須要準備一個*.obj模型,在init函數中,建立loader變量,用於導入模型:
var loader = new THREE.OBJLoader();
  • loader導入模型的時候,接受兩個參數,第一個表示模型路徑,第二個表示完成導入後的回調函數,通常咱們須要在這個回調函數中將導入的模型添加到場景中。
loader.load('../lib/port.obj', function(obj) {
    //儲存到全局變量中
    mesh = obj; 
    scene.add(obj);
});
  • 能夠看到一個沒有材質的茶壺

image

  • 咱們在重繪函數中讓茶壺旋轉:
function draw() {
    renderer.render(scene, camera);

    mesh.rotation.y += 0.01;
    if (mesh.rotation.y > Math.PI * 2) {
        mesh.rotation.y -= Math.PI * 2;
    }
}
  • 能夠看到在某些角度時,好像有些面片沒有被繪製出來,於是後方的茶嘴彷佛穿越到前方了:

image

  • 這是因爲默認的狀況下,只有正面的面片被繪製,而若是須要雙面繪製,須要這樣設置:
var loader = new THREE.OBJLoader();
loader.load('port.obj', function(obj) {
    obj.traverse(function(child) {
        if (child instanceof THREE.Mesh) {
            child.material.side = THREE.DoubleSide;
        }
    });

    mesh = obj;
    scene.add(obj);
});
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript" src="OBJLoader.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        
        body {
            position: fixed;
        }
    </style>
    <script type="text/javascript">
        var scene = null;
        var camera = null;
        var renderer = null;

        var mesh = null;
        var id = null;

        function init() {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(800, 600);
            document.body.appendChild(renderer.domElement);
            renderer.setClearColor(0x000000);
            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-8, 8, 6, -6, 0.1, 100);
            camera.position.set(15, 25, 25);
            camera.lookAt(new THREE.Vector3(0, 2, 0));
            scene.add(camera);

            var loader = new THREE.OBJLoader();
            loader.load('port.obj', function(obj) {
                obj.traverse(function(child) {
                    if (child instanceof THREE.Mesh) {
                        child.material.side = THREE.DoubleSide;
                    }
                });

                mesh = obj;
                scene.add(obj);
            });

            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(20, 10, 5);
            scene.add(light);

            id = setInterval(draw, 20);
        }

        function draw() {
            renderer.render(scene, camera);

            mesh.rotation.y += 0.01;
            if (mesh.rotation.y > Math.PI * 2) {
                mesh.rotation.y -= Math.PI * 2;
            }
        }
    </script>
</head>

<body onload="init()">

</body>

</html>
  • 效果圖:

image

10.3 有材質的模型

模型的材質能夠有兩種定義方式,一種是在代碼中導入模型後設置材質,另外一種是在建模軟件中導出材質信息。下面,咱們將分別介紹這兩種方法。

10.3.1 代碼中設置材質

這種方法與上一節相似,不一樣之處在於回調函數中設置模型的材質:

var loader = new THREE.OBJLoader();
loader.load('port.obj', function(obj) {
    obj.traverse(function(child) {
        if (child instanceof THREE.Mesh) {
            /* 修改這裏如下的代碼 */
            child.material = new THREE.MeshLambertMaterial({
                color: 0xffff00,
                side: THREE.DoubleSide
            });
            /* 修改這裏以上的代碼 */
        }
    });

    mesh = obj;
    scene.add(obj);
});
  • 效果圖:

image

10.3.2 建模軟件中設置材質

導出3D模型的時候,選擇導出port.obj模型文件以及port.mtl材質文件。

  • 如今,咱們再也不使用OBJLoader.js,而是使用MTLLoader.jsOBJMTLLoader.js,而且要按該順序引用:
<script type="text/javascript" src="MTLLoader.js"></script>
<script type="text/javascript" src="OBJMTLLoader.js"></script>
  • 調用的方法也略有不一樣:
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath('');
mtlLoader.load('port.mtl', function(materials) {
    materials.preload();
    // model loader
    var objLoader = new THREE.OBJLoader();
    objLoader.setMaterials(materials);
    objLoader.setPath('');
    objLoader.load('port.obj', function(object) {
        object.position.y = -95;
        // if has object, add to scene
        if (object.children.length > 0) {
            scene.add(object.children[0]);
        }
    });
});
  • 完整代碼:
<html>

<head>
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript" src="MTLLoader.js"></script>
    <script type="text/javascript" src="OBJLoader.js"></script>

    <script type="text/javascript">
        var scene = null;
        var camera = null;
        var renderer = null;

        var mesh = null;
        var id = null;

        function init() {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(800, 600);
            document.body.appendChild(renderer.domElement);
            renderer.setClearColor(0x000000);
            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-8, 8, 6, -6, 0.1, 100);
            camera.position.set(15, 25, 25);
            camera.lookAt(new THREE.Vector3(0, 2, 0));
            scene.add(camera);

            // material loader
            var mtlLoader = new THREE.MTLLoader();
            mtlLoader.setPath('');
            mtlLoader.load('port.mtl', function(materials) {
                materials.preload();
                // model loader
                var objLoader = new THREE.OBJLoader();
                objLoader.setMaterials(materials);
                objLoader.setPath('');
                objLoader.load('port.obj', function(object) {
                    object.position.y = -95;
                    // if has object, add to scene
                    if (object.children.length > 0) {
                        scene.add(object.children[0]);
                    }

                });
                mesh = materials;
                console.log(mesh);
            });
            var light = new THREE.DirectionalLight(0xffffff);
            light.position.set(20, 10, 5);
            scene.add(light);

            id = setInterval(draw, 20);
        }

        function draw() {
            renderer.render(scene, camera);
        }
    </script>
</head>

<body onload="init()">
</body>

</html>
  • 導出時自帶的效果圖:

image

11. 光與影

圖像渲染的豐富效果很大程度上也要歸功於光與影的利用。真實世界中的光影效果很是複雜,可是其本質—光的傳播原理卻又是很是單一的,這即是天然界繁簡相成的又一例證。爲了使計算機模擬豐富的光照效果,人們提出了幾種不一樣的光源模型(環境光平行光點光源聚光燈等),在不一樣場合下組合利用,將能達到很好的光照效果。

  • 在Three.js中,光源與陰影的建立和使用是十分方便的。在學會了如何控制光影的基本方法以後,若是能將其靈活應用,將能使場景的渲染效果更加豐富逼真。在本章中,咱們將探討四種經常使用的光源(環境光、點光源、平行光、聚光燈)和陰影帶來的效果,以及如何去建立使用光影。

11.1 環境光(AmbientLight)

環境光是指場景總體的光照效果,是因爲場景內若干光源的屢次反射造成的亮度一致的效果,一般用來爲整個場景指定一個基礎亮度。所以,環境光沒有明確的光源位置,在各處造成的亮度也是一致的。

  • 在設置環境光時,只需指定光的顏色:
var light = new THREE.AmbientLight(hex);
scene.add(light);
  • 其中hex是十六進制的RGB顏色信息,如紅色表示爲0xff0000
  • 可是,若是此時場景中沒有物體,只添加了這個環境光,那麼渲染的結果仍然是一片黑。因此,咱們添加兩個長方體看下效果:
// 建立一個綠色的正方體
var greenCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2),
        new THREE.MeshLambertMaterial({color: 0x00ff00}));
greenCube.position.x = 3;
scene.add(greenCube);

// 建立一個白色的正方體
var whiteCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2),
        new THREE.MeshLambertMaterial({color: 0xffffff}));
whiteCube.position.x = -3;
scene.add(whiteCube);
  • 效果如圖:

image

  • 若是想讓環境光暗些,能夠將其設置爲new THREE.AmbientLight(0xcccccc)等,效果爲:

image

11.2 點光源(PointLight)

點光源是不計光源大小,能夠看做一個點發出的光源。點光源照到不一樣物體表面的亮度是線性遞減的,所以,離點光源距離越的物體會顯得越

  • 點光源的構造函數是:
THREE.PointLight(hex, intensity, distance);
  • 其中,hex是光源十六進制的顏色值;intensity是亮度,缺省值爲1,表示100%亮度;distance是光源最遠照射到的距離,缺省值爲0
  • 建立點光源並將其添加到場景中的完整作法是:
var light = new THREE.PointLight(0xffffff, 2, 100);
light.position.set(0, 1.5, 2);
scene.add(light);
  • 效果圖:

image

  • 注意,這裏光在每一個面上的亮度是不一樣的,對於每一個三角面片,將根據三個頂點的亮度進行插值。

11.3 平行光(DirectionalLight)

咱們都知道,太陽光經常被看做平行光,這是由於相對地球上物體的尺度而言,太陽離咱們的距離足夠遠。對於任意平行的平面,平行光照射的亮度都是相同的,而與平面所在位置無關。

  • 平行光的構造函數是:
THREE.DirectionalLight(hex, intensity)
  • 其中,hex是光源十六進制的顏色值;intensity是亮度,缺省值爲1,表示100%亮度。
  • 此外,對於平行光而言,設置光源位置尤其重要。
var light = new THREE.DirectionalLight();
light.position.set(2, 5, 3);
scene.add(light);
  • 注意,這裏設置光源位置並不意味着全部光從(2, 5, 3)點射出(若是是的話,就成了點光源),而是意味着,平行光將以矢量(-2, -5, -3)的方向照射到全部平面。所以,平面亮度與平面的位置無關,而只與平面的法向量相關。只要平面是平行的,那麼獲得的光照也必定是相同的。
  • 示例代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="js/three.js"></script>
    <script>
        var stat;
        var renderer;
        var scene;
        var camera;

        function init() {

            // 渲染器
            renderer = new THREE.WebGLRenderer({
                antialias: true
            });
            renderer.setSize(800, 600);
            document.body.appendChild(renderer.domElement);
            renderer.setClearColor(0x000000);

            // 場景
            scene = new THREE.Scene();

            // 相機
            var camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 15, 25);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            // 平行光
            var light = new THREE.DirectionalLight();
            light.position.set(2, 5, 3);
            scene.add(light);
            
            // 右側正方體
            var rightCube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
                new THREE.MeshLambertMaterial({
                    color: 0x00ff00
                }));
            rightCube.position.x = 1;
            rightCube.position.y = -1;
            scene.add(rightCube);
            
            // 左側正方體
            var leftCube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
                new THREE.MeshLambertMaterial({
                    color: 0x00ff00
                }));
            leftCube.position.x = -1;
            scene.add(leftCube);

            // 渲染
            renderer.render(scene, camera);
        }
    </script>
</head>

<body onload="init();">

</body>

</html>
  • 效果圖:

image

11.4 聚光燈(SpotLight)

能夠看出,聚光燈是一種特殊的點光源,它可以朝着一個方向投射光線。聚光燈投射出的是相似圓錐形的光線,這與咱們現實中看到的聚光燈是一致的。

  • 其構造函數爲:
THREE.SpotLight(hex, intensity, distance, angle, exponent)
  • 相比點光源,多了angleexponent兩個參數。angle是聚光燈的張角,缺省值是Math.PI / 3,最大值是Math.PI / 2exponent是光強在偏離target的衰減指數(target須要在以後定義,缺省值爲(0, 0, 0)),缺省值是10
  • 在調用構造函數以後,除了設置光源自己的位置,通常還須要設置target
light.position.set(x1, y1, z1);
light.target.position.set(x2, y2, z2);
  • 除了設置light.target.position的方法外,若是想讓聚光燈跟着某一物體移動(就像真的聚光燈!),能夠target指定爲該物體
var cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
                    new THREE.MeshLambertMaterial({color: 0x00ff00}));

var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
light.target = cube;
  • 示例代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript">
        var scene = null;
        var camera = null;
        var renderer = null;

        var cube = null;
        var alpha = 0;

        function init() {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(800, 600);

            document.body.appendChild(renderer.domElement);

            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 15, 25);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            // 平面
            var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8, 16, 16),
                new THREE.MeshLambertMaterial({
                    color: 0xcccccc
                }));
            plane.rotation.x = -Math.PI / 2;
            plane.position.y = -1;
            plane.receiveShadow = true;
            scene.add(plane);

            // 立方體
            cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
                new THREE.MeshLambertMaterial({
                    color: 0x00ff00,
                }));
            cube.position.x = 2;
            scene.add(cube);

            // 聚光燈
            var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
            light.position.set(2, 5, 3);
            light.target = cube;
            scene.add(light);

            // 環境光
            var ambient = new THREE.AmbientLight(0x666666);
            scene.add(ambient);

            requestAnimationFrame(draw);
        }

        function draw() {
            alpha += 0.01;
            if (alpha > Math.PI * 2) {
                alpha -= Math.PI * 2;
            }

            cube.position.set(2 * Math.cos(alpha), 0, 2 * Math.sin(alpha));

            renderer.render(scene, camera);

            requestAnimationFrame(draw);
        }
    </script>
</head>

<body onload="init();">

</body>

</html>
  • 效果圖:

image

11.5 陰影

明暗是相對的,陰影的造成也就是由於比周圍得到的光照更少。所以,要造成陰影,光源必不可少。

  • 在Three.js中,能造成陰影的光源只有THREE.DirectionalLightTHREE.SpotLight;而相對地,能表現陰影效果的材質只有THREE.LambertMaterialTHREE.PhongMaterial。於是在設置光源和材質的時候,必定要注意這一點。
  • 下面,咱們以聚光燈爲例,在以前的基礎上增長陰影效果。
  • 首先,咱們須要在初始化時,告訴渲染器渲染陰影:
renderer.shadowMapEnabled = true;
  • 而後,對於光源以及全部要產生陰影的物體調用:
// 上面的案例,產生陰影的物體是正方體
cube.castShadow = true;
  • 對於接收陰影的物體調用:
// 接收陰影的物體是平面
plan.receiveShadow = true;
  • 好比場景中一個平面上有一個正方體,想要讓聚光燈照射在正方體上,產生的陰影投射在平面上,那麼就須要對聚光燈和正方體調用castShadow = true,對於平面調用receiveShadow = true
  • 以上就是產生陰影效果的必要步驟了,不過一般還須要設置光源的陰影相關屬性,才能正確顯示出陰影效果。
  • 對於聚光燈,須要設置shadowCameraNearshadowCameraFarshadowCameraFov三個值,類比咱們在第二章學到的透視投影照相機,只有介於shadowCameraNearshadowCameraFar之間的物體將產生陰影,shadowCameraFov表示張角。
  • 對於平行光,須要設置shadowCameraNearshadowCameraFarshadowCameraLeftshadowCameraRightshadowCameraTop以及shadowCameraBottom六個值,至關於正交投影照相機的六個面。一樣,只有在這六個面圍成的長方體內的物體纔會產生陰影效果。
  • 爲了看到陰影照相機的位置,一般能夠在調試時開啓light.shadowCameraVisible = true
  • 若是想要修改陰影的深淺,能夠經過設置shadowDarkness,該值的範圍是01,越小越淺。
  • 另外,這裏實現陰影效果的方法是Shadow Mapping,即陰影是做爲渲染前計算好的貼圖貼上去的,於是會受到貼圖像素大小的限制。因此能夠經過設置shadowMapWidthshadowMapHeight值控制貼圖的大小,來改變陰影的精確度。
  • 而若是想實現軟陰影的效果,能夠經過renderer.shadowMapSoft = true;方便地實現。
  • 完整代碼:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="js/three.js"></script>
    <script type="text/javascript">
        var scene = null;
        var camera = null;
        var renderer = null;

        var cube = null;
        var alpha = 0;

        function init() {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(800, 600);

            document.body.appendChild(renderer.domElement);
            renderer.shadowMapEnabled = true;


            scene = new THREE.Scene();

            camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
            camera.position.set(5, 15, 25);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8, 16, 16),
                new THREE.MeshLambertMaterial({
                    color: 0xcccccc
                }));
            plane.rotation.x = -Math.PI / 2;
            plane.position.y = -1;
            plane.receiveShadow = true;
            scene.add(plane);

            cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
                new THREE.MeshLambertMaterial({
                    color: 0x00ff00
                }));
            cube.position.x = 2;
            cube.castShadow = true;
            scene.add(cube);

            var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
            light.position.set(2, 5, 3);
            light.target = cube;
            light.castShadow = true;

            light.shadowCameraNear = 2;
            light.shadowCameraFar = 10;
            light.shadowCameraFov = 30;

            light.shadowMapWidth = 1024;
            light.shadowMapHeight = 1024;
            light.shadowDarkness = 0.3;

            scene.add(light);

            // ambient light
            var ambient = new THREE.AmbientLight(0x666666);
            scene.add(ambient);

            requestAnimationFrame(draw);
        }

        function draw() {
            alpha += 0.01;
            if (alpha > Math.PI * 2) {
                alpha -= Math.PI * 2;
            }

            cube.position.set(2 * Math.cos(alpha), 0, 2 * Math.sin(alpha));

            renderer.render(scene, camera);

            requestAnimationFrame(draw);
        }
    </script>
</head>

<body onload="init();">

</body>

</html>
  • 效果圖:

image

補充問題

<span id = "jump"></span>

本地服務器

  • 一、下載安裝node.js,由於node.js自帶npm
  • 二、打開電腦命令行工具,輸入npm install -g live-server 全局安裝
  • 三、在須要運行文件的文件夾下,按住shift鍵,點擊鼠標右鍵在此處打開命令窗口
  • 四、輸入live-server回車

nmp官方說明

相關文章
相關標籤/搜索