1. 程式人生 > >Three.JS學習 9:WEBVR 入門demo

Three.JS學習 9:WEBVR 入門demo

本文內容是介紹基於Three.js建立一個可以使用谷歌眼鏡演示的WEB虛擬現實網頁。

準備工作

使用自己熟悉的開發環境建立一個web專案,把上面下載專案裡的/js 、 /textures放到專案裡,新建一個index.html檔案。
這裡寫圖片描述

下載的專案裡有完整的原始碼,本文基本是對其程式說明做簡單翻譯。

引用的檔案說明

  • three.min.js : Threejs庫
  • StereoEffect.js:允許我們把普通的Three.js分成兩個顯示的場景合併到一起顯示,這是VR體驗的基本需求
  • DeviceOrientationControls.js:告訴Three.js裝置的朝向、向哪移動。
  • OrbitControls.js:允許我們通過拖動、點選事件來控制場景(在DeviceOrientation事件無效的時候,比如電腦模擬時適用)
  • helvetiker_regular.typeface.js:將在Three.js顯示文字用到的字型

下面是init()函式,其中有一些內容在前幾章已有提及,這裡不會完全詳述。

        function init() {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.001
, 700); camera.position.set(0, 15, 0); scene.add(camera); /* 我們需要一個元素來畫場景,這裡定義一個renderer,並且給HTML元素webglviewer宣告一個變數 */ renderer = new THREE.WebGLRenderer(); element = renderer.domElement; container = document.getElementById('webglviewer'
); container.appendChild(element); //為了有VR雙屏的檢視,需要StereoEffect effect = new THREE.StereoEffect(renderer); //控制攝像機 controls = new THREE.OrbitControls(camera, element); controls.target.set( camera.position.x + 0.15, camera.position.y, camera.position.z ); controls.noPan = true; controls.noZoom = true; //加入裝置事件,該事件返回的結果有三個屬性值 window.addEventListener('deviceorientation', setOrientationControls, true); //如果沒有裝置支援DeviceOrientation特性,還要給controls變數加上OrbitControls物件,並 //且使用我們自己的DeviceOrientationControls物件替換它 //接下來執行connect和update函式 controls = new THREE.DeviceOrientationControls(camera, true); controls.connect(); controls.update(); //滑鼠點選、全屏,這樣在google cardboard裡看起來效果更好 element.addEventListener('click', fullscreen, false); //刪除deviceorientation事件,因為已經定義了我們自己的DeviceOrientationControls物件 window.removeEventListener('deviceorientation', setOrientationControls, true); } function setOrientationControls(e) { //通過alpha屬性來確保監測的是我們需要的事件 if (!e.alpha) { return; } }

建立燈光

var light = new THREE.PointLight(0x999999, 2, 100);
light.position.set(50, 50, 50);
scene.add(light);

var lightScene = new THREE.PointLight(0x999999, 2, 100);
lightScene.position.set(0, 5, 0);
scene.add(lightScene);

建立地板(載入材質)

var floorTexture = THREE.ImageUtils.loadTexture('textures/wood.jpg');
floorTexture.wrapS = THREE.RepeatWrapping;
floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat = new THREE.Vector2(50, 50);

floorTexture.anisotropy = renderer.getMaxAnisotropy();
//我們的地板需要texture和material,其中material控制我們的地板如何跟隨燈光變化
//我們使用了MeshPhoneMaterial可以讓物件跟隨燈光效果看起來更舒適
var floorMaterial = new THREE.MeshPhongMaterial({
  color: 0xffffff,
  specular: 0xffffff,
  shininess: 20,
  shading: THREE.FlatShading,
  map: floorTexture
});

定義幾何體

var geometry = new THREE.PlaneBufferGeometry(1000, 1000);

在場景里加入地板

var floor = new THREE.Mesh(geometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);

把粒子放到一起

先定義一些粒子相關的公用變數,並且建立了一個粒子物件,用來儲存浮動的粒子,後面會詳細講解這些變數。

particles = new THREE.Object3D(),
totalParticles = 200,
maxParticleSize = 200,
particleRotationSpeed = 0,
particleRotationDeg = 0,
lastColorRange = [0, 0.3],
currentColorRange = [0, 0.3],

現在在一個比較高的水平上來整體看一下程式碼。我們把一個透明的png圖”textures/particle.png”初始化為texture。上面定義了總粒子數量為totalParticles,如果想增加場景裡的粒子數量,可以把這個值增大。

下面遍歷粒子並把它們加入到了粒子物件裡,我們需要把粒子物件升高以讓它旋浮起來。

var particleTexture = THREE.ImageUtils.loadTexture('textures/particle.png'),
    spriteMaterial = new THREE.SpriteMaterial({
    map: particleTexture,
    color: 0xffffff
  });

for (var i = 0; i < totalParticles; i++) {
  // Code setting up all our particles!
}

particles.position.y = 70;
scene.add(particles);

接下來建立一個Three.js Sprite物件,並把spriteMaterial賦給它,然後把它縮放到64×64(與texture一樣大)。我們希望粒子是圍繞我們出現在隨機的位置,所以把它設定有x和y值介於-0.5到0.5之間,z值在-0.75到0.25之間。關於為什麼選取這些值,在一些實踐之後,這些應該是最佳的實踐值。

for (var i = 0; i < totalParticles; i++) {
  var sprite = new THREE.Sprite(spriteMaterial);

  sprite.scale.set(64, 64, 1.0);
  sprite.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.75);

把每個粒子的尺寸都限制0到maxParticleSize之間

sprite.position.setLength(maxParticleSize * Math.random());

讓粒子看起來平滑的一個關鍵點是THREE.AdditiveBlending ,它是Three.js裡的彎曲風格。這個會給texture賦給它後面一種texture的顏色,以讓整個粒子系統看起來更平滑。

sprite.material.blending = THREE.AdditiveBlending;

  particles.add(sprite);
}

天氣API

目前已經有了一個擁有地板、燈光的靜態場景。現在新增一個OpenWeatherMap API獲取各城市的天氣以讓demo顯得更有趣。

OpenWeatherMap使用一個HTTP請求來獲取多個城市天氣。下面定義了cityIDs變數儲存需要的各個城市,從網址:
http://78.46.48.103/sample/city.list.json.gz.
可以獲取到城市列表。

function adjustToWeatherConditions() {
  var cityIDs = '';
  for (var i = 0; i < cities.length; i++) {
    cityIDs += cities[i][1];
    if (i != cities.length - 1) cityIDs += ',';
  }

我們的城市陣列包括了名稱和IDs,這樣可以顯示天氣資料的時候也顯示城市的名字。
為了呼叫API,還需要一個API key,可以到網站http://openweathermap.org建立一個賬號
使用getURL()函式可以獲取XMLHttpRequest請求。如果收到一個關於”crossorigin”錯誤,那需要改用JSONP。
這是呼叫示例:

getURL('http://api.openweathermap.org/data/2.5/group?id=' + cityIDs + '&APPID=kj34723jkh23kj89dfkh2b28ey982hwm223iuyhe2c', function(info) {
  cityWeather = info.list;

當然天氣服務並非本文重點,下面跳過一部分內容。

儲存時間

clock = new THREE.Clock();

動起來

在init()函式裡已經呼叫了animate。

我們還需要決定粒子要轉動的方向,如果風力小於或等於180,那就順時針轉,否則就逆時針轉。

function animate() {
  var elapsedSeconds = clock.getElapsedTime(),
      particleRotationDirection = particleRotationDeg <= 180 ? -1 : 1;

為了在Three.js動畫的每一幀真實的旋轉它們,我們需要計算動畫已經運行了多少秒,乘上速度,這樣計算出粒子y值。

particles.rotation.y = elapsedSeconds * particleRotationSpeed * particleRotationDirection;

同樣我們還需要跟蹤當前的和上次的顏色資訊,這樣我們知道在哪些幀裡改變它們。這裡新的光線值介於0.2到0.7之間。

if (lastColorRange[0] != currentColorRange[0] && lastColorRange[1] != currentColorRange[1]) {
  for (var i = 0; i < totalParticles; i++) {
    particles.children[i].material.color.setHSL(currentColorRange[0], currentColorRange[1], (Math.random() * (0.7 - 0.2) + 0.2));
  }

  lastColorRange = currentColorRange;
}

接下來迴圈動畫:

requestAnimationFrame(animate);

最後讓一切平滑連運動起來:

update(clock.getDelta())
render(clock.getDelta()) 
effect.render(scene, camera);

原始碼

其中去掉了天氣部分

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Connecting up Google Cardboard to web APIs</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <style>
      body {
        margin: 0px;
        overflow: hidden;
      }
      #webglviewer {
        bottom: 0;
        left: 0;
        position: absolute;
        right: 0;
        top: 0;
      }
    </style>
  </head>
  <body>
    <div id="webglviewer"></div>

    <script src="./js/three.min.js"></script>
    <script src="./js/StereoEffect.js"></script>
    <script src="./js/DeviceOrientationControls.js"></script>
    <script src="./js/OrbitControls.js"></script>
    <script src="./js/helvetiker_regular.typeface.js"></script>

    <script>
        var scene,
            camera,
            renderer,
            element,
            container,
            effect,
            controls,
            clock,

            // Particles
            particles = new THREE.Object3D(),
            totalParticles = 200,
            maxParticleSize = 200,
            particleRotationSpeed = 0,
            particleRotationDeg = 0,
            lastColorRange = [0, 0.3],
            currentColorRange = [0, 0.3],

            // City and weather API set up
            cities = [['Sydney', '2147714'], ['New York', '5128638'], ['Tokyo', '1850147'], ['London', '2643743'], ['Mexico City', '3530597'], ['Miami', '4164138'], ['San Francisco', '5391959'], ['Rome', '3169070']],
            cityWeather = {},
            cityTimes = [],
            currentCity = 0,
            currentCityText = new THREE.TextGeometry(),
            currentCityTextMesh = new THREE.Mesh();

        init();

        function init() {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.001, 700);
            camera.position.set(0, 15, 0);
            scene.add(camera);

            renderer = new THREE.WebGLRenderer();
            element = renderer.domElement;
            container = document.getElementById('webglviewer');
            container.appendChild(element);

            effect = new THREE.StereoEffect(renderer);

            // Our initial control fallback with mouse/touch events in case DeviceOrientation is not enabled
            controls = new THREE.OrbitControls(camera, element);
            controls.target.set(
              camera.position.x + 0.15,
              camera.position.y,
              camera.position.z
            );
            controls.noPan = true;
            controls.noZoom = true;

            // Our preferred controls via DeviceOrientation
            function setOrientationControls(e) {
                if (!e.alpha) {
                    return;
                }

                controls = new THREE.DeviceOrientationControls(camera, true);
                controls.connect();
                controls.update();

                element.addEventListener('click', fullscreen, false);

                window.removeEventListener('deviceorientation', setOrientationControls, true);
            }
            window.addEventListener('deviceorientation', setOrientationControls, true);

            // Lighting
            var light = new THREE.PointLight(0x999999, 2, 100);
            light.position.set(50, 50, 50);
            scene.add(light);

            var lightScene = new THREE.PointLight(0x999999, 2, 100);
            lightScene.position.set(0, 5, 0);
            scene.add(lightScene);

            var floorTexture = THREE.ImageUtils.loadTexture('textures/wood.jpg');
            floorTexture.wrapS = THREE.RepeatWrapping;
            floorTexture.wrapT = THREE.RepeatWrapping;
            floorTexture.repeat = new THREE.Vector2(50, 50);
            floorTexture.anisotropy = renderer.getMaxAnisotropy();

            var floorMaterial = new THREE.MeshPhongMaterial({
                color: 0xffffff,
                specular: 0xffffff,
                shininess: 20,
                shading: THREE.FlatShading,
                map: floorTexture
            });

            var geometry = new THREE.PlaneBufferGeometry(1000, 1000);

            var floor = new THREE.Mesh(geometry, floorMaterial);
            floor.rotation.x = -Math.PI / 2;
            scene.add(floor);

            var particleTexture = THREE.ImageUtils.loadTexture('textures/particle.png'),
                spriteMaterial = new THREE.SpriteMaterial({
                    map: particleTexture,
                    color: 0xffffff
                });

            for (var i = 0; i < totalParticles; i++) {
                var sprite = new THREE.Sprite(spriteMaterial);

                sprite.scale.set(64, 64, 1.0);
                sprite.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.75);
                sprite.position.setLength(maxParticleSize * Math.random());

                sprite.material.blending = THREE.AdditiveBlending;

                particles.add(sprite);
            }
            particles.position.y = 70;
            scene.add(particles);


            clock = new THREE.Clock();

            animate();
        }


        function animate() {
            var elapsedSeconds = clock.getElapsedTime(),
                particleRotationDirection = particleRotationDeg <= 180 ? -1 : 1;

            particles.rotation.y = elapsedSeconds * particleRotationSpeed * particleRotationDirection;

            // We check if the color range has changed, if so, we'll change the colours
            if (lastColorRange[0] != currentColorRange[0] && lastColorRange[1] != currentColorRange[1]) {

                for (var i = 0; i < totalParticles; i++) {
                    particles.children[i].material.color.setHSL(currentColorRange[0], currentColorRange[1], (Math.random() * (0.7 - 0.2) + 0.2));
                }

                lastColorRange = currentColorRange;
            }

            requestAnimationFrame(animate);

            update(clock.getDelta());
            render(clock.getDelta());
        }

        function resize() {
            var width = container.offsetWidth;
            var height = container.offsetHeight;

            camera.aspect = width / height;
            camera.updateProjectionMatrix();

            renderer.setSize(width, height);
            effect.setSize(width, height);
        }

        function update(dt) {
            resize();

            camera.updateProjectionMatrix();

            controls.update(dt);
        }

        function render(dt) {
            effect.render(scene, camera);
        }

        function fullscreen() {
            if (container.requestFullscreen) {
                container.requestFullscreen();
            } else if (container.msRequestFullscreen) {
                container.msRequestFullscreen();
            } else if (container.mozRequestFullScreen) {
                container.mozRequestFullScreen();
            } else if (container.webkitRequestFullscreen) {
                container.webkitRequestFullscreen();
            }
        }

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

效果:
這裡寫圖片描述

另一個網友的作品

效果:
這裡寫圖片描述

原始碼:

<!DOCTYPE html>
<html>
<head>
  <title>WebVR Demo</title>
  <style>
  body {
    width: 100%;
    height: 100%;
    background-color: #000;
  }
  </style>
</head>
<body>
  <script src="./js/three.min.js"></script>
  <script src="./js/StereoEffect.js"></script>
  <script src="./js/OrbitControls.js"></script>
  <script src="./js/DeviceOrientationControls.js"></script>
  <script src="./js/helvetiker_regular.typeface.js"></script>

  <script>
  var scene, camera, renderer, effect, element, controls, word = "HELLO VR World", cube;
  init();

  function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.001, 700);
    camera.position.set(0, 15, 0);
    scene.add(camera);

    renderer = new THREE.WebGLRenderer();
    element = renderer.domElement;
    document.body.appendChild(renderer.domElement);

    effect = new THREE.StereoEffect(renderer);

    //Handle mouse control
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.target.set(
      camera.position.x + 0.01,
      camera.position.y,
      camera.position.z
    );
    window.addEventListener('deviceorientation', setDeviceOrientationControls, true);

    //Create light
    var light = new THREE.PointLight( 0xffffff, 1.2, 0 );
    light.position.set(0, 50, 0);
    scene.add(light);

    // Create floor
    var floorTexture = THREE.ImageUtils.loadTexture('img/grass.jpg');
    floorTexture.wrapS = THREE.RepeatWrapping;
    floorTexture.wrapT = THREE.RepeatWrapping;
    floorTexture.repeat = new THREE.Vector2(50, 50);
    var floorMaterial = new THREE.MeshPhongMaterial({
      map: floorTexture
    });
    var floorGeometry = new THREE.PlaneBufferGeometry(1000, 1000);
    var floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    scene.add(floor);

    // Create box
    var geometry = new THREE.BoxGeometry(6, 6, 6);
    var material = new THREE.MeshNormalMaterial();
    cube = new THREE.Mesh(geometry, material);
    cube.position.set(-15, 30, 10);
    scene.add(cube);

    //Create text
    var textGeometry = new THREE.TextGeometry(word, {
      size: 5,
      height: 1
    });
    var text = new THREE.Mesh(textGeometry, new THREE.MeshBasicMaterial({
      color: 0xffffff
    }));
    text.position.set(15, 15, -25);
    text.rotation.set(0, 30, 0);
    scene.add(text);

    animate();
  }

  // Our preferred controls via DeviceOrientation
  function setDeviceOrientationControls(e) {
    controls = new THREE.DeviceOrientationControls(camera, true);
    controls.connect();
    controls.update();
    window.removeEventListener('deviceorientation', setDeviceOrientationControls, true);
  }

  function animate() {
    requestAnimationFrame(animate);

    var width = window.innerWidth;
    var height = window.innerHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    effect.setSize(width, height);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    controls.update();
    effect.render(scene, camera);
  }
  </script>
</body>
</html>