最终实现效果
从最简单的开始,构建地面
无论怎样的高楼大厦都是从一砖一瓦一步步搭建的,我们的3D仓库也一样,虽然这是最简单的功能(如下图),但却包含了最基本的要素:场景,相机,光照,物体,渲染器,控制器。接下来我将一一为大家介绍。
准备——浏览器跨域设置
我们在本地开发的时候经常遇到跨域的问题,即在html中无法读取到本地存放的图片等等,下图展示了跨域问题出现的时候
可见门,窗户,地板等有贴图的地方全部变成了空白,F12调试模式下可以看到跨域错误的具体报错信息,解决方法很简单,就是在谷歌浏览器快捷方式下右键属性,在快捷方式的目标下,加入 --allow-file-access-from-files,注意最前面有一个空格,如下图所示
完成后重新打开浏览器,就可以看到跨域问题已经解决啦,门窗地板全部显示出来啦!
; 初始化场景、相机、灯光、渲染器
这部分代码比较简单,而且不同的项目基本都类似,所以我就直接放出代码了,不再做过多介绍。
完成之后能够显示背景为蓝色的一个场景,当然里面什么都没有,因为我们还没有添加物体。
var stats = initStats();
var scene, camera, renderer, light;
function initScene() {
scene = new THREE.Scene();
}
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 );
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);
});
}
这里使用了本地的图片作为地板的贴图素材,大家可以自定义自己需要的图片。完成之后大概是这个样子
光秃秃的背景上出现了一个光秃秃的地面。(我变秃了也变强了)
创建控制器及其他
完成上面的步骤,我们发现场景还无法旋转、平移、放大缩小等。这些就要依赖我们的控制器了,ThreeJs官网上有很多类型的控制器,我们这里选用比较常用的轨道球控制器,代码很简单。
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;
}
这里设置了最大的旋转角度,防止某些绅士看到人家下面呢!
完整的代码
<!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;
}
</style>
<script src="./ThreeJs/three.js"></script>
<script src="./ThreeJs/stats.min.js"></script>
<script src="./ThreeJs/OBJLoader.js"></script>
<script src="./ThreeJs/OrbitControls.js"></script>
</head>
<body>
<div id="container"></div>
<script>
var stats = initStats();
var scene, camera, renderer, controls, light;
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 );
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 initContent() {
createFloor();
}
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>
完成之后就可以看到一个可以随意转动移动缩放的地面啦!
如何添加墙壁、窗户、门
这部分内容整体来说并不难,无论是墙壁,还是门窗户,其实质都是一个长方体,我们使用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)这个方法创建出挖去门窗的墙面,参数很简单,第一个是被挖去的墙,第二个是要挖去的物体的数组。该方法代码如下:
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 );
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;
}
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}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0x9cb2d1}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 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>
https://github.com/xiao149/ThreeJsDemo
选中物体的原理
鉴于我也是从各种百度到的地方了解到这些内容的,只能说是知其然而不知其所以然,再次我就仅仅描述我所知道的东西,若有所纰漏还请原谅~~
选中一个物体我们需要依靠Raycaster这个工具,其实质是在你鼠标所指的地方(如下图)发射一条射线,对我们可以想象一条直线,垂直你的电脑屏幕,在你鼠标的位置,从屏幕外直直指向屏幕内,系统将会获取到该射线依次经过的物体。
举个栗子,我们可以在浏览器中按下F12打开调试模式,我这时点击了一个窗户,进入断点在我红色箭头所指地方的intersects便保存了射线依次经过的物体,分别是窗户、地面、地面。
换一个比较刁钻的角度,这次我点击了最左侧的门,由下图可见射线返回了两个物体,分别是左门1和左边的那个墙面:
; 选中物体的实现
经过上面的描述,我想大家对如何点击选中一个物体有了初步的了解,现在我们可以来创建ThreeJs_Composer这个自定义的JS了:
THREE.ThreeJs_Composer = function ( _renderer, _scene, _camera) {
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var selectedObjects = [];
window.addEventListener( 'click', onMouseClick);
function onMouseClick( event ) {
var x, y;
if ( event.changedTouches ) {
x = event.changedTouches[ 0 ].pageX;
y = event.changedTouches[ 0 ].pageY;
} else {
x = event.clientX;
y = event.clientY;
}
mouse.x = ( x / window.innerWidth ) * 2 - 1;
mouse.y = - ( y / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, _camera );
var intersects = raycaster.intersectObjects( [ _scene ], true );
if(intersects.length == 0){
$("#label").attr("style","display:none;");
return;
}
if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
$("#label").attr("style","display:none;");
selectedObjects.pop();
}else{
$("#label").attr("style","display:block;");
$("#label").css({left: x, top: y-40});
$("#label").text(intersects[0].object.name);
selectedObjects.pop();
selectedObjects.push( intersects[0].object );
}
}
}
我来简单介绍下,首先需要传入的三个参数_renderer, _scene, _camera分别是HTML中创建好的渲染器,场景和相机,我们需要在test.html中导入这个JS就像这样
<script src="./ThreeJs/ThreeJs_Composer.js"></script>
然后在init()方法中加入(这里的test.html依旧是第二章所讲的那个,忘记的同学可以回到第二章回顾下)
new THREE.ThreeJs_Composer(renderer, scene, camera);
回到我们创建的JS中,下面这个变量已经保存了我们鼠标点击处发出射线所依次经过的物体:
var intersects = raycaster.intersectObjects( [ _scene ], true );
接下来我们会有三种情况:
1.鼠标点击的地方啥都没有,就直接隐藏我们的说明性标签(就是显示在鼠标上面的那个框),直接return。
if(intersects.length == 0){
$("#label").attr("style","display:none;");
return;
}
2.点击到了地面或者墙面,这种情况下我们一般不显示说明,并且把selectedObjects这个数组清空,这个数组将会存放我们选中的物体,方便之后添加发光特效。
if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
$("#label").attr("style","display:none;");
selectedObjects.pop();
}
3.点击到了门窗或者其他能够选中的物体,这种情况下显示说明(物体的名字),并且把selectedObjects这个数组清空后赋值。
else{
$("#label").attr("style","display:block;");
$("#label").css({left: x, top: y-40});
$("#label").text(intersects[0].object.name);
selectedObjects.pop();
selectedObjects.push( intersects[0].object );
}
完成后效果图如下(鼠标点击了左门1):
添加选中后的发光特效
这个需要我们用到ThreeJs的后期处理THREE.EffectComposer,以及一系列"通道",THREE.RenderPass,THREE.OutlinePass,THREE.ShaderPass
我们在之前的自定义JS中修改如下:
THREE.ThreeJs_Composer = function ( _renderer, _scene, _camera) {
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var composer = new THREE.EffectComposer( _renderer );
var renderPass = new THREE.RenderPass( _scene, _camera );
var selectedObjects = [];
composer.addPass( renderPass );
var outlinePass = new THREE.OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), _scene, _camera );
outlinePass.edgeStrength = 5;
outlinePass.edgeGlow = 0.5;
outlinePass.edgeThickness = 2;
outlinePass.pulsePeriod = 2;
outlinePass.visibleEdgeColor.set( '#ffffff' );
outlinePass.hiddenEdgeColor.set( '#190a05' );
composer.addPass( outlinePass );
var effectFXAA = new THREE.ShaderPass( THREE.FXAAShader );
effectFXAA.uniforms[ 'resolution' ].value.set( 1 / window.innerWidth, 1 / window.innerHeight );
effectFXAA.renderToScreen = true;
composer.addPass( effectFXAA );
window.addEventListener( 'click', onMouseClick);
function onMouseClick( event ) {
var x, y;
if ( event.changedTouches ) {
x = event.changedTouches[ 0 ].pageX;
y = event.changedTouches[ 0 ].pageY;
} else {
x = event.clientX;
y = event.clientY;
}
mouse.x = ( x / window.innerWidth ) * 2 - 1;
mouse.y = - ( y / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, _camera );
var intersects = raycaster.intersectObjects( [ _scene ], true );
if(intersects.length == 0){
$("#label").attr("style","display:none;");
return;
}
if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
$("#label").attr("style","display:none;");
selectedObjects.pop();
}else{
$("#label").attr("style","display:block;");
$("#label").css({left: x, top: y-40});
$("#label").text(intersects[0].object.name);
selectedObjects.pop();
selectedObjects.push( intersects[0].object );
outlinePass.selectedObjects = selectedObjects;
}
}
return composer;
}
关于后期通道这方面我了解的很少,有兴趣的同学可以自行百度,官网上也有很多栗子,比如雪花特效啊,变暗变亮的特效啊等等。这里修改后的JS和之前章节创建的差不多,白色光圈的特效主要依靠OutlinePass通道,我们将射线获取到的selectedObjects赋给outlinePass就可以实现文章开头演示的效果啦!其中下面的这些参数大家可以依据自己的喜好适度修改。
outlinePass.edgeStrength = 5;
outlinePass.edgeGlow = 0.5;
outlinePass.edgeThickness = 2;
outlinePass.pulsePeriod = 2;
outlinePass.visibleEdgeColor.set( '#ffffff' );
outlinePass.hiddenEdgeColor.set( '#190a05' );
HTML更新如下
<!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/js/three.js"></script>
<script src="./ThreeJs/js/stats.min.js"></script>
<script src="./ThreeJs/js/DragControls.js"></script>
<script src="./ThreeJs/js/OrbitControls.js"></script>
<script src="./ThreeJs/js/dat.gui.min.js"></script>
<script src="./ThreeJs/js/EffectComposer.js"></script>
<script src="./ThreeJs/js/RenderPass.js"></script>
<script src="./ThreeJs/js/OutlinePass.js"></script>
<script src="./ThreeJs/js/FXAAShader.js"></script>
<script src="./ThreeJs/js/CopyShader.js"></script>
<script src="./ThreeJs/js/ShaderPass.js"></script>
<script src="./ThreeJs/js/OBJLoader.js"></script>
<script src="./ThreeJs/js/MTLLoader.js"></script>
<script src="./ThreeJs/js/ThreeBSP.js"></script>
<script src="./ThreeJs/ThreeJs_Composer.js"></script>
<script src="./ThreeJs/js/jquery-1.11.0.min.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);
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;
}
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}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0x9cb2d1}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));
matArrayB.push(new THREE.MeshPhongMaterial({color: 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();
composer = new THREE.ThreeJs_Composer(renderer, scene, camera);
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);
composer.render();
update();
}
init();
animate();
</script>
</body>
</html>