最新進展
最近這兩天因爲項目上比較空閒了,所以就想着怎麼給我這個粗劣的小玩意兒加點高大上的東西,經過身邊同事的提醒,我發現自己做的這個倉庫只有一個房間,但是一般來講廠房內可能會有多個倉庫,或者說同一個倉庫也有可能會有好幾層。
所以開發一個場景切換的功能至關重要。經過一天的探索與發現,我終於順利解決了這個問題,歸功於網絡上眼花繚亂的開源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智能倉庫——第五章: 點我跳轉.