2016年9月10日 星期六

使用raycaster實現Object的選取 - three.js

three.js可以在Scene放置許多的Object3D,但如果我們想要讓User跟那些Obejct做互動,那要如何做呢,例如User在Camera的畫面中看到許多的物件,想用滑鼠去點擊那些物件去做互動。

在three.js中,有一個Raycaster類可以做到上述需求,它的原裡是,想像有一條光束(ray)從Camera畫面中滑鼠指的位置,以垂直於Camera的X-Y平面的方式向Camera的負Z方向射出,並且計算在Scene的children中有哪些物件被這條線射中(ray可以穿過所有Object3D)。 

在這裡,我要來示範一個使用three.js的Raycaster的範例,需求是這樣的:

需求:
  1. 畫面上有許多的Object3D物件,有各自的顏色。
  2. 當滑鼠指到(hover)某一個Object時,將指到的第一個Object變成紅色(即它後面的Object不會變色)。
  3. 滑鼠移開後,剛剛指到但現在沒指的Object變回原來的顏色。
接下來一步步的講解每一個步驟:

  1. 首先我們先設計主要的範例架構
  2. 設計html頁
    <!DOCTYPE html>
    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            
            <script type="text/javascript" src="js/jquery-3.0.0.min.js"></script>
     <script type="text/javascript" src="js/three.js"></script>
     <script type="text/javascript" src="js/threeJsControls/OrbitControls.js"></script>
            <script type="text/javascript" src="js/main.js"></script>
        </head>
        <body>
            <div id="container" style='width:50%; height:500px; float: left;'></div>
            <div id="container2" style='width:50%; height:500px; float: left;'></div>
        </body>
    </html>
    

    在這邊為了了解raycaster的運作方式,擺了兩個Camera,
    其中<div id="container">是用來展示成果的地方。
    而<div id="container2">是另一個Camera在另一個角度看到的樣子,我在滑鼠移動時不斷的在Scene中畫了一條由滑鼠射出去的ray的綠線,以便觀看raycaster的運作原理。
  3. 再來是Javascript的實作程式碼,相關的解釋都寫在註解中

    $(document).ready(function () {
        var scene, camera, renderer, container, controls;
        var camera2, renderer2, container2, controls2;
    
        var onMouseMove = (function () {
            var selectedObjects = new Map();  //在object被變顏色之前,會先把object和原來的顏色存到這個Map,
                                              //Key是object、Value是原來的顏色
            var rayLine;  //用來顯示raycaster運作原裡的線,為THREE.Line
            return function (event) {
                //如果Scene上有rayLine,先把它除Scene中移除
                if (rayLine) {
                    scene.remove(rayLine);
                }
                var raycaster = new THREE.Raycaster();
                var mouse = new THREE.Vector2();
                // 將滑鼠的x、y位置換算成-1~1的值,從renderer的畫面左下角(-1,-1),到右上解(1,1)
                mouse.x = ((event.clientX - container.offsetLeft + document.body.scrollLeft) / container.offsetWidth) * 2 - 1;
                mouse.y = -((event.clientY - container.offsetTop + document.body.scrollTop) / container.offsetHeight) * 2 + 1;
                //設置Raycaster,setFromCamera(對renderer來說的x,y位置:-1~1, -1~1,Camera物件)
                raycaster.setFromCamera(mouse.clone(), camera);
                //將Scene的children(即各物件)和raycaster一起計算,得到滑鼠選中的Object(ray會貫穿,不只一個)
                var intersects = raycaster.intersectObjects(scene.children);
                var isFoundInSelectedObjects  = false; //選到的第一個Object有無存在selectedObjects中
                //遍歷selectedObjects中的Object
                selectedObjects.forEach(function (value, key) {
                    if (!!intersects[0] && key === intersects[0].object) {
                        //如果selectedObjects中的Object是選到的第一個Object
                        //直接改變Object的顏色
                        intersects[0].object.material.color.setHex(0xff0000);
                        isFoundInSelectedObjects = true;
                    } else {
                        //如果selectedObjects中的Object不是選到的第一個Object
                        //把Object的顏色(原來的顏色存在以Object為key的value中)改回來
                        //將Object的資料從selectedObjects中刪掉
                        key.material.color.setHex(value);
                        selectedObjects.delete(key);
                    }
                });
                if (!!intersects[0] && !isFoundInSelectedObjects) {
                    //如果選到的第一個Object沒有存紀錄在selectedObjects中
                    //將Object原來的顏色存進selectedObjects中,以Object為key
                    selectedObjects.set(intersects[0].object, intersects[0].object.material.color.getHex());
                    //改變Object的顏色
                    intersects[0].object.material.color.setHex(0xff0000);
                }
                ////////
                //製造要標示raycaster運作原裡的線,THREE.Line
                var rayLineGeometry = new THREE.Geometry();
                //設置兩點來畫線
                rayLineGeometry.vertices.push(raycaster.ray.origin.clone());
                rayLineGeometry.vertices.push(raycaster.ray.origin.clone().add(raycaster.ray.direction.clone().setLength(10000)));
                var rayLineMaterial = new THREE.LineBasicMaterial({
                    color: 0x00ff00
                });
                var line = new THREE.Line(rayLineGeometry, rayLineMaterial);
                rayLine = line;
                scene.add(line); //加到Scene中
                //////////
            };
        })();
        ////////////////////////////////////////////
        function init() {
            //得到container的DOM
            container = document.getElementById("container");
            container2 = document.getElementById("container2");
    
            //設置camera和camera2
            camera = new THREE.OrthographicCamera(container.offsetWidth / -1, container.offsetWidth / 1, container.offsetHeight / 1, container.offsetHeight / -1, 1, 10000);
            camera.position.set(2000, 2000, 2000);
            camera.zoom = 1;
            camera.updateProjectionMatrix();
    
            camera2 = new THREE.OrthographicCamera(container.offsetWidth / -1, container.offsetWidth / 1, container.offsetHeight / 1, container.offsetHeight / -1, 1, 10000);
            camera2.position.set(3000, 2000, 1000);
            camera2.lookAt(new THREE.Vector3(0, 0, 0));
            camera2.zoom = 1;
            camera2.updateProjectionMatrix();
    
            // 設置scene
            scene = new THREE.Scene();
            //在scene中產生各種Object
            generateObjects(scene);
    
            //設置renderer和renderer2
            renderer = new THREE.WebGLRenderer();
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(container.offsetWidth, container.offsetHeight);
    
            renderer2 = new THREE.WebGLRenderer();
            renderer2.setPixelRatio(window.devicePixelRatio);
            renderer2.setSize(container.offsetWidth, container.offsetHeight);
    
            //在container DOM下放置renderer的domElement以顯示畫面
            container.appendChild(renderer.domElement);
            container2.appendChild(renderer2.domElement);
            
            //設置提供旋轉、平移、縮放Camera的controls
            controls = new THREE.OrbitControls(camera, container);
            controls2 = new THREE.OrbitControls(camera2, container2);
            
            //監聽滑鼠移動事件
            window.addEventListener('mousemove', onMouseMove, false);
        }
        
        // Renders the scene and updates the render as needed.
        function animate() {
            requestAnimationFrame(render);
        }
        
        function render() {
            renderer.render(scene, camera);
            renderer2.render(scene, camera2);
            requestAnimationFrame(render);
        }
    
        function generateObjects(scene) {
            //產生27個不同顏色的方塊物件,並放到Scene中
            for (var i = 0; i < 3; i++) {
                for (var j = 0; j < 3; j++) {
                    for (var k = 0; k < 3; k++) {
                        var geometry = new THREE.BoxGeometry(100, 100, 100);
                        var material = new THREE.MeshLambertMaterial({
                            color: 0x2194ce + (i + j + k) * 0xaaaaaa
                        });
                        var mesh = new THREE.Mesh(geometry, material);
                        mesh.position.set(-150 * 3 + 150 * i, -150 * 3 + 150 * j, -150 * 3 + 150 * k);
                        scene.add(mesh);
                    }
                }
            }
            //設置光線,light
            var spotLight = new THREE.SpotLight(0xffffff);
            spotLight.position.set(100, 1000, 100);
    
            spotLight.castShadow = true;
    
            spotLight.shadow.mapSize.width = 1024;
            spotLight.shadow.mapSize.height = 1024;
    
            spotLight.shadow.camera.near = 500;
            spotLight.shadow.camera.far = 4000;
            spotLight.shadow.camera.fov = 30;
    
            scene.add(spotLight);
            scene.add(new THREE.AmbientLight(0xdddddd));
        }
    
        init();
        animate();
    });
最後,這個影片顯示了完成的成果:
 
原始碼下載:
threeJs_raycaster_test.7z

沒有留言 :

張貼留言