使用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智能仓库——第五章: 点我跳转.

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