使用ThreeJs從零開始構建3D智能倉庫——第二章(創建地面與門窗)

最新進展

最近這兩天因爲項目上比較空閒了,所以就想着怎麼給我這個粗劣的小玩意兒加點高大上的東西,經過身邊同事的提醒,我發現自己做的這個倉庫只有一個房間,但是一般來講廠房內可能會有多個倉庫,或者說同一個倉庫也有可能會有好幾層。
所以開發一個場景切換的功能至關重要。經過一天的探索與發現,我終於順利解決了這個問題,歸功於網絡上眼花繚亂的開源Js和插件,現將效果展示在下面:
2019.11.26 更新:我最近建立了個人網站,大家可以訪問下面的鏈接查看演示
3D倉庫演示
2019.11.28 更新:代碼和圖片資源等已上傳至GitHub
https://github.com/xiao149/ThreeJsDemo
在這裏插入圖片描述
可見右側新添了一個選擇的控件,總共有兩個場景(第二個場景暫時還沒做,先用一張圖片代替),通過點擊不同的按鈕來實現不同場景的切換,切換過程中有類似翻轉的特效。這部分的內容如果大家有興趣的話我會放到以後講解,今天還是繼續第一章的內容,來看看如何添加牆壁、窗戶、門和一個很關鍵的重點:如何選中一個物體並添加選中特效。如何選中一個物體並添加選中特效。如何選中一個物體並添加選中特效。(重要的話說三遍,選中是之後很多功能的前提)

如何添加牆壁、窗戶、門

這部分內容整體來說並不難,無論是牆壁,還是門窗戶,其實質都是一個長方體,我們使用THREE.BoxGeometry這個幾何體來構建這一切,完成後的效果如下:
在這裏插入圖片描述

添加三面實心的牆壁

實心的牆壁是很簡單的,這裏直接給出代碼

//創建牆
function createCubeWall(width, height, depth, angle, material, x, y, z, name){
    var cubeGeometry = new THREE.BoxGeometry(width, height, depth );
    var cube = new THREE.Mesh( cubeGeometry, material );
    cube.position.x = x;
    cube.position.y = y;
    cube.position.z = z;
    cube.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
    cube.name = name;
    scene.add(cube);
}

稍微解釋一下,width, height, depth這三個參數定義了長方體的長寬高,angle定義了長方體旋轉的角度,material定義了物體的材質,x, y, z定義了該物體放在場景中的具體位置,name定義了該物體的名字。然後再初始化的函數中加入

//創建牆
createCubeWall(10, 200, 1400, 0, new THREE.MeshPhongMaterial({color: 0xafc0ca}), -1295, 100, 0, "牆面");

然後就可以看到這樣的效果,地面上出現了一堵左側的牆
在這裏插入圖片描述
依葫蘆畫瓢可以很簡單地創建出三面實心的牆

//創建牆
createCubeWall(10, 200, 1400, 0, new THREE.MeshPhongMaterial({color: 0xafc0ca}), -1295, 100, 0, "牆面");
createCubeWall(10, 200, 1400, 1, new THREE.MeshPhongMaterial({color: 0xafc0ca}), 1295, 100, 0, "牆面");
createCubeWall(10, 200, 2600, 1.5, new THREE.MeshPhongMaterial({color: 0xafc0ca}), 0, 100, -700, "牆面");

在這裏插入圖片描述

創建挖去門窗的牆

首先我們來分析下,要創造一個挖去某些部分的牆其實也很簡單,其實質就是先創建一個實心的牆面,然後再創建出實心的門窗,最後用某個工具像做減法一樣:實心的牆面減去實心的門窗。就可以得到挖去了門窗的牆面。很幸運的是,ThreeJs給我們提供了這樣的方法,使用ThreeBSP這個庫就可以實現差集(相減)、並集(組合、相加)、交集(兩幾何體重合的部分)等一系列功能
我們先來創建需要的一面實心牆,兩扇門,四扇窗戶,代碼如下:

//返回牆對象
function returnWallObject(width, height, depth, angle, material, x, y, z, name){
    var cubeGeometry = new THREE.BoxGeometry(width, height, depth);
    var cube = new THREE.Mesh( cubeGeometry, material );
    cube.position.x = x;
    cube.position.y = y;
    cube.position.z = z;
    cube.rotation.y += angle*Math.PI;
    cube.name = name;
    return cube;
}
//創建挖了門的牆
var wall = returnWallObject(2600, 200, 10, 0, matArrayB, 0, 100, 700, "牆面");
var door_cube1 = returnWallObject(200, 180, 10, 0, matArrayB, -600, 90, 700, "前門1");
var door_cube2 = returnWallObject(200, 180, 10, 0, matArrayB, 600, 90, 700, "前門2");
var window_cube1 = returnWallObject(100, 100, 10, 0, matArrayB, -900, 90, 700, "窗戶1");
var window_cube2 = returnWallObject(100, 100, 10, 0, matArrayB, 900, 90, 700, "窗戶2");
var window_cube3 = returnWallObject(100, 100, 10, 0, matArrayB, -200, 90, 700, "窗戶3");
var window_cube4 = returnWallObject(100, 100, 10, 0, matArrayB, 200, 90, 700, "窗戶4");
var objects_cube = [];
objects_cube.push(door_cube1);
objects_cube.push(door_cube2);
objects_cube.push(window_cube1);
objects_cube.push(window_cube2);
objects_cube.push(window_cube3);
objects_cube.push(window_cube4);
createResultBsp(wall, objects_cube);

這裏我們創建了一個數組objects_cube來存放要挖去的內容,最後使用createResultBsp(wall, objects_cube)這個方法創建出挖去門窗的牆面,參數很簡單,第一個是被挖去的牆,第二個是要挖去的物體的數組。該方法代碼如下:

//牆上挖門窗,通過兩個幾何體生成BSP對象
function createResultBsp(bsp,objects_cube){
    var material = new THREE.MeshPhongMaterial({color:0x9cb2d1,specular:0x9cb2d1,shininess:30,transparent:true,opacity:1});
    var BSP = new ThreeBSP(bsp);
    for(var i = 0; i < objects_cube.length; i++){
        var less_bsp = new ThreeBSP(objects_cube[i]);
        BSP = BSP.subtract(less_bsp);
    }
    var result = BSP.toMesh(material);
    result.material.flatshading = THREE.FlatShading;
    result.geometry.computeFaceNormals();  //重新計算幾何體側面法向量
    result.geometry.computeVertexNormals();
    result.material.needsUpdate = true;  //更新紋理
    result.geometry.buffersNeedUpdate = true;
    result.geometry.uvsNeedUpdate = true;
    scene.add(result);
}

如此這般,我們就能看到如下的效果(是不是還挺簡單的呢):
在這裏插入圖片描述

安裝門及窗戶

接下來我們要在上面完成的挖去了門窗的牆面上安裝門和窗戶,我們使用三個方法來實現這個功能,因爲我這裏的門分爲左門和右門,如果不需要的話只留下一種門就可以啦,代碼如下,這三個方法類似,我就只介紹一個,方法參數很簡單,width, height, depth定義了門窗的長寬高,angle定義了門窗的旋轉角度,x, y, z定義了門窗的空間位置,name定義了門窗的名字。
這裏都使用了THREE.TextureLoader來加載本地的圖片作爲門窗的貼圖,門窗的實體設置爲全透明,也就是opacity = 1.0和transparent = true,其他不做過多闡述。

//創建門_左側
function createDoor_left(width, height, depth, angle, x, y, z, name){
   var loader = new THREE.TextureLoader();
   loader.load("./ThreeJs/images/door_left.png",function(texture){
       var doorgeometry = new THREE.BoxGeometry(width, height, depth);
       doorgeometry.translate(50, 0, 0);
       var doormaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
       doormaterial.opacity = 1.0;
       doormaterial.transparent = true;
       var door = new THREE.Mesh( doorgeometry,doormaterial);
       door.position.set(x, y, z);
       door.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
       door.name = name;
       scene.add(door);
   });
}

//創建門_右側
function createDoor_right(width, height, depth, angle, x, y, z, name){
   var loader = new THREE.TextureLoader();
   loader.load("./ThreeJs/images/door_right.png",function(texture){
       var doorgeometry = new THREE.BoxGeometry(width, height, depth);
       doorgeometry.translate(-50, 0, 0);
       var doormaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
       doormaterial.opacity = 1.0;
       doormaterial.transparent = true;
       var door = new THREE.Mesh( doorgeometry,doormaterial);
       door.position.set(x, y, z);
       door.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
       door.name = name;
       scene.add(door);
   });
}

//創建窗戶
function createWindow(width, height, depth, angle, x, y, z, name){
   var loader = new THREE.TextureLoader();
   loader.load("./ThreeJs/images/window.png",function(texture){
       var windowgeometry = new THREE.BoxGeometry(width, height, depth);
       var windowmaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
       windowmaterial.opacity = 1.0;
       windowmaterial.transparent = true;
       var window = new THREE.Mesh( windowgeometry,windowmaterial);
       window.position.set(x, y, z);
       window.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
       window.name = name;
       scene.add(window);
   });
}

最後在初始化函數里加上如下的代碼,就可以看到門窗都已經順利安裝成功了!

//爲牆面安裝門
createDoor_left(100, 180, 2, 0, -700, 90, 700, "左門1");
createDoor_right(100, 180, 2, 0, -500, 90, 700, "右門1");
createDoor_left(100, 180, 2, 0, 500, 90, 700, "左門2");
createDoor_right(100, 180, 2, 0, 700, 90, 700, "右門2");
//爲牆面安裝窗戶
createWindow(100, 100, 2, 0, -900, 90, 700, "窗戶");
createWindow(100, 100, 2, 0, 900, 90, 700, "窗戶");
createWindow(100, 100, 2, 0, -200, 90, 700, "窗戶");
createWindow(100, 100, 2, 0, 200, 90, 700, "窗戶");

在這裏插入圖片描述

完整的代碼

想了想還是把選中的功能放到下一章來講吧,這部分比較複雜,寫在這一章的話實在太長了,還請同學們多多期待吧,最後照例給出該章全部的代碼。

<!DOCTYPE html>
<html>
<head includeDefault="true">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <title>3D庫圖顯示</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
        #label {
            position: absolute;
            padding: 10px;
            background: rgba(255, 255, 255, 0.6);
            line-height: 1;
            border-radius: 5px;
        }
    </style>
    <script src="./ThreeJs/three.js"></script>
    <script src="./ThreeJs/stats.min.js"></script>
    <script src="./ThreeJs/OrbitControls.js"></script>
    <script src="./ThreeJs/OBJLoader.js"></script>
    <script src="./ThreeJs/MTLLoader.js"></script>
    <script src="./ThreeJs/ThreeBSP.js"></script>
</head>
<body>
    <div id="label"></div>
    <div id="container"></div>
    <script>

        var stats = initStats();
        var scene, camera, renderer, controls, light, composer;
        var matArrayA=[];//內牆
        var matArrayB = [];//外牆
        var group = new THREE.Group();

        // 初始化場景
        function initScene() {
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog( scene.background, 3000, 5000 );
        }

        // 初始化相機
        function initCamera() {
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
            camera.position.set(0, 800, 1500);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
        }

        // 初始化燈光
        function initLight() {
            var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.3 );//模擬遠處類似太陽的光源
            directionalLight.color.setHSL( 0.1, 1, 0.95 );
            directionalLight.position.set( 0, 200, 0).normalize();
            scene.add( directionalLight );

            var ambient = new THREE.AmbientLight( 0xffffff, 1 ); //AmbientLight,影響整個場景的光源
            ambient.position.set(0,0,0);
            scene.add( ambient );
        }

        // 初始化性能插件
        function initStats() {
            var stats = new Stats();

            stats.domElement.style.position = 'absolute';
            stats.domElement.style.left = '0px';
            stats.domElement.style.top = '0px';

            document.body.appendChild(stats.domElement);
            return stats;
        }

        // 初始化渲染器
        function initRenderer() {
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setClearColor(0x4682B4,1.0);
            document.body.appendChild(renderer.domElement);
        }

        //創建地板
        function createFloor(){
            var loader = new THREE.TextureLoader();
            loader.load("./ThreeJs/images/floor.jpg",function(texture){
                texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
                texture.repeat.set( 10, 10 );
                var floorGeometry = new THREE.BoxGeometry(2600, 1400, 1);
                var floorMaterial = new THREE.MeshBasicMaterial( { map: texture, side: THREE.DoubleSide } );
                var floor = new THREE.Mesh(floorGeometry, floorMaterial);
                floor.position.y = -0.5;
                floor.rotation.x = Math.PI / 2;
                floor.name = "地面";
                scene.add(floor);
            });
        }

        //創建牆
        function createCubeWall(width, height, depth, angle, material, x, y, z, name){
            var cubeGeometry = new THREE.BoxGeometry(width, height, depth );
            var cube = new THREE.Mesh( cubeGeometry, material );
            cube.position.x = x;
            cube.position.y = y;
            cube.position.z = z;
            cube.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
            cube.name = name;
            scene.add(cube);
        }

        //創建門_左側
        function createDoor_left(width, height, depth, angle, x, y, z, name){
            var loader = new THREE.TextureLoader();
            loader.load("./ThreeJs/images/door_left.png",function(texture){
                var doorgeometry = new THREE.BoxGeometry(width, height, depth);
                doorgeometry.translate(50, 0, 0);
                var doormaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
                doormaterial.opacity = 1.0;
                doormaterial.transparent = true;
                var door = new THREE.Mesh( doorgeometry,doormaterial);
                door.position.set(x, y, z);
                door.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
                door.name = name;
                scene.add(door);
            });
        }

        //創建門_右側
        function createDoor_right(width, height, depth, angle, x, y, z, name){
            var loader = new THREE.TextureLoader();
            loader.load("./ThreeJs/images/door_right.png",function(texture){
                var doorgeometry = new THREE.BoxGeometry(width, height, depth);
                doorgeometry.translate(-50, 0, 0);
                var doormaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
                doormaterial.opacity = 1.0;
                doormaterial.transparent = true;
                var door = new THREE.Mesh( doorgeometry,doormaterial);
                door.position.set(x, y, z);
                door.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
                door.name = name;
                scene.add(door);
            });
        }

        //創建窗戶
        function createWindow(width, height, depth, angle, x, y, z, name){
            var loader = new THREE.TextureLoader();
            loader.load("./ThreeJs/images/window.png",function(texture){
                var windowgeometry = new THREE.BoxGeometry(width, height, depth);
                var windowmaterial = new THREE.MeshBasicMaterial({map:texture,color:0xffffff});
                windowmaterial.opacity = 1.0;
                windowmaterial.transparent = true;
                var window = new THREE.Mesh( windowgeometry,windowmaterial);
                window.position.set(x, y, z);
                window.rotation.y += angle*Math.PI;  //-逆時針旋轉,+順時針
                window.name = name;
                scene.add(window);
            });
        }

        //返回牆對象
        function returnWallObject(width, height, depth, angle, material, x, y, z, name){
            var cubeGeometry = new THREE.BoxGeometry(width, height, depth);
            var cube = new THREE.Mesh( cubeGeometry, material );
            cube.position.x = x;
            cube.position.y = y;
            cube.position.z = z;
            cube.rotation.y += angle*Math.PI;
            cube.name = name;
            return cube;
        }

        //牆上挖門,通過兩個幾何體生成BSP對象
        function createResultBsp(bsp,objects_cube){
            var material = new THREE.MeshPhongMaterial({color:0x9cb2d1,specular:0x9cb2d1,shininess:30,transparent:true,opacity:1});
            var BSP = new ThreeBSP(bsp);
            for(var i = 0; i < objects_cube.length; i++){
                var less_bsp = new ThreeBSP(objects_cube[i]);
                BSP = BSP.subtract(less_bsp);
            }
            var result = BSP.toMesh(material);
            result.material.flatshading = THREE.FlatShading;
            result.geometry.computeFaceNormals();  //重新計算幾何體側面法向量
            result.geometry.computeVertexNormals();
            result.material.needsUpdate = true;  //更新紋理
            result.geometry.buffersNeedUpdate = true;
            result.geometry.uvsNeedUpdate = true;
            scene.add(result);
        }

        //創建牆紋理
        function createWallMaterail(){
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //前  0xafc0ca :灰色
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //後
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //上  0xd6e4ec: 偏白色
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //下
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //左    0xafc0ca :灰色
            matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //右

            matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //前  0xafc0ca :灰色
            matArrayB.push(new THREE.MeshPhongMaterial({color: 0x9cb2d1}));  //後  0x9cb2d1:淡紫
            matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //上  0xd6e4ec: 偏白色
            matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //下
            matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //左   0xafc0ca :灰色
            matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //右

        }

        // 初始化模型
        function initContent() {
            createFloor();
            createWallMaterail();
            createCubeWall(10, 200, 1400, 0, matArrayB, -1295, 100, 0, "牆面");
            createCubeWall(10, 200, 1400, 1, matArrayB, 1295, 100, 0, "牆面");
            createCubeWall(10, 200, 2600, 1.5, matArrayB, 0, 100, -700, "牆面");
            //創建挖了門的牆
            var wall = returnWallObject(2600, 200, 10, 0, matArrayB, 0, 100, 700, "牆面");
            var door_cube1 = returnWallObject(200, 180, 10, 0, matArrayB, -600, 90, 700, "前門1");
            var door_cube2 = returnWallObject(200, 180, 10, 0, matArrayB, 600, 90, 700, "前門2");
            var window_cube1 = returnWallObject(100, 100, 10, 0, matArrayB, -900, 90, 700, "窗戶1");
            var window_cube2 = returnWallObject(100, 100, 10, 0, matArrayB, 900, 90, 700, "窗戶2");
            var window_cube3 = returnWallObject(100, 100, 10, 0, matArrayB, -200, 90, 700, "窗戶3");
            var window_cube4 = returnWallObject(100, 100, 10, 0, matArrayB, 200, 90, 700, "窗戶4");
            var objects_cube = [];
            objects_cube.push(door_cube1);
            objects_cube.push(door_cube2);
            objects_cube.push(window_cube1);
            objects_cube.push(window_cube2);
            objects_cube.push(window_cube3);
            objects_cube.push(window_cube4);
            createResultBsp(wall, objects_cube);
            //爲牆面安裝門
            createDoor_left(100, 180, 2, 0, -700, 90, 700, "左門1");
            createDoor_right(100, 180, 2, 0, -500, 90, 700, "右門1");
            createDoor_left(100, 180, 2, 0, 500, 90, 700, "左門2");
            createDoor_right(100, 180, 2, 0, 700, 90, 700, "右門2");
            //爲牆面安裝窗戶
            createWindow(100, 100, 2, 0, -900, 90, 700, "窗戶");
            createWindow(100, 100, 2, 0, 900, 90, 700, "窗戶");
            createWindow(100, 100, 2, 0, -200, 90, 700, "窗戶");
            createWindow(100, 100, 2, 0, 200, 90, 700, "窗戶");
        }

        // 初始化軌跡球控件
        function initControls() {
            controls = new THREE.OrbitControls( camera, renderer.domElement );
            controls.enableDamping = true;
            controls.dampingFactor = 0.5;
            // 視角最小距離
            controls.minDistance = 100;
            // 視角最遠距離
            controls.maxDistance = 5000;
            // 最大角度
            controls.maxPolarAngle = Math.PI/2.2;
        }

        // 更新控件
        function update() {
            stats.update();
            controls.update();
        }

        // 初始化
        function init() {
            initScene();
            initCamera();
            initRenderer();
            initContent();
            initLight();
            initControls();
            document.addEventListener('resize', onWindowResize, false);
        }

        // 窗口變動觸發的方法
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
            update();
        }

        init();
        animate();

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

結束語

回顧下第一章我們涉及了基礎場景的創建,包括場景、相機、光源、控制器等等。第二章我們講解了如何創造實心的牆面和帶有門窗的牆面。下一章我們將會推出如何選中一個物體並給其加上選中後的特效。
我跟廣大學習ThreeJs的初學者一樣,仍帶着懵懂的心去探索這片新大陸,CSDN上的許多前輩都給了我很多關鍵的靈感和技術方法,如果大家有興趣,也可以互相交流成長,歡迎大家指導諮詢。PS:大家有興趣可以點進去我的頭像,陸陸續續也寫了十來篇了。
鏈接:使用ThreeJs從零開始構建3D智能倉庫——第一章: 點我跳轉.
鏈接:使用ThreeJs從零開始構建3D智能倉庫——第二章: 點我跳轉.
鏈接:使用ThreeJs從零開始構建3D智能倉庫——第三章: 點我跳轉.
鏈接:使用ThreeJs從零開始構建3D智能倉庫——第四章: 點我跳轉.
鏈接:使用ThreeJs從零開始構建3D智能倉庫——第五章: 點我跳轉.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章