概述
本文将会涵盖以下内容
- Three.js场景中使用的组件
- THREE.Scene对象的作用
- 几何网格Geometry 和网格 Mesh 使如何联系的
创建场景
场景的意义
THREE.Scene可以说使场景的代码化, 它保存了所有图形场景的必要信息, 比如图形对象, 光源, 和渲染所需要的其他对象. 每个添加到场景的对象, 甚至场景本身都是继承自 THREE.Object3D对象.
创建一个简单场景
// 导出给controller使用
export const scene = new THREE.Scene()
export const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100)
export const renderer = new THREE.WebGLRenderer()
export const planeGeometry = new THREE.PlaneGeometry(60, 40, 1, 1)
window.scene = scene
export const init = function() {
const axes = new THREE.AxesHelper(20)
scene.add(axes)
scene.add(camera)
renderer.setClearColor(new THREE.Color(0x000000))
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
const planeMaterial = new THREE.MeshLambertMaterial({
color: 'red',
})
const plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.receiveShadow = true
plane.rotation.x = -0.5 * Math.PI
plane.position.x = 0
plane.position.y = 0
plane.position.z = 0
plane.name = 'plane'
scene.add(plane)
camera.position.x = -30
camera.position.y = 40
camera.position.z = 30
camera.lookAt(plane.position)
// 使材质对光产生反应
const ambientLight = new THREE.AmbientLight(0x3c3c3c)
scene.add(ambientLight)
// 创建一个点光源
const spotLight = new THREE.SpotLight(0xffffff, 1.2, 150, 120)
spotLight.position.set(-40, 60, -10)
spotLight.castShadow = true
scene.add(spotLight)
document.getElementById('webgl-output').appendChild(renderer.domElement)
renderer.render(scene, camera)
}
创建一个辅助控制器, 可以动态往场景里添加cube并查看有多少个cube
const gui = new dat.GUI()
const addBlock = function() {
this.numberOfCubes += 1
const cubeL = Math.random() * 5
const cubeGeometry = new THREE.BoxGeometry(cubeL, cubeL, cubeL)
const cubeMaterial = new THREE.MeshLambertMaterial({
color: Math.random() * 0xffffff,
})
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
cube.castShadow = true
cube.position.x = camera.position.x + Math.round(Math.random() * planeGeometry.parameters.width)
cube.position.y = Math.round(Math.random() * 5)
cube.position.z = -20 + Math.round(Math.random() * planeGeometry.parameters.height)
cube.name='cube-'+this.numberOfCubes
scene.add(cube)
this.numberOfObjects = scene.children.length,
renderer.render(scene, camera)
}
const removeBlock = function() {
if (this.numberOfCubes > 0) {
const lastOne = scene.getObjectByName(`cube-${this.numberOfCubes}`)
scene.remove(lastOne)
this.numberOfCubes -= 1
renderer.render(scene, camera)
this.numberOfObjects = scene.children.length
}
}
export const controller = {
addBlock: () => addBlock.call(controller),
numberOfCubes: 0,
numberOfObjects: scene.children.length,
removeBlock: () => removeBlock.call(controller),
}
// 选择哪些参数需要添加到控制GUI中
gui.add(controller, 'addBlock')
gui.add(controller, 'numberOfCubes').listen()
gui.add(controller, 'numberOfObjects').listen()
gui.add(controller, 'removeBlock')
运行截图
以上代码分别使用了scene的以下方法:
- scene.add
- scene.remove
- scene.children
- scene.getObjectByName 这个方法的设计跟DOM的getElementByClass类似.
下面添加一个动画, 让添加的cube自动旋转起来
// controller.js
gui.add(controller, 'rotationSpeed', 0, 0.5)
export const controller = {
// ...
rotationSpeed: 0,
}
// animation.js
export const animationRender = function render() {
scene.traverse((obj) => {
if (obj.name.includes('cube')) {
obj.rotation.x += controller.rotationSpeed
obj.rotation.y += controller.rotationSpeed
obj.rotation.Z += controller.rotationSpeed
}
})
requestAnimationFrame(animationRender)
renderer.render(scene, camera)
}
这里又使用了一个新的scene的方法, traverse, 遍历所有的子对象树, 不过与scene.children.forEach的差别在于: 如果子对象本身还有子对象, 最会在该子对象上执行traverse方法. 使用requestAnimationFrame使动画连贯. 递归执行
为场景添加雾化效果
fog表示场景的雾化属性, 雾化的效果使: 场景中的物体离得越远越模糊, 跟拍照一样.
添加的方法使:
// 添加白色雾化效果, near和far值
scene.fog = new THREE.Fog(0xffffff, 0.015, 100)
// 指数增长的雾化浓度
scene.fog = new THREE.FogExp(0xffffff, 0.01)
线性渐变截图
指数渐变截图
使用overrideMaterial属性
可以使用这一属性批量修改在场景中的所有物体的材质, 方便进行统一的控制, 即使是物体本身设置了属性.
scene.overrideMaterial = new THREE.MeshLambertMaterial({
color: 0xfffffff
})
覆盖截图:
上述内容即是关于场景的所有常用属性的描述
几何体和网格
几何体和网格的意义
用于填充场景, 表现具体的物体内容.
几何体的属性和方法
大多数几何体均是三维空间的点集(vertex, 顶点)和将这些点连接起来的面.以立方体为例
- 立方体有8个角
- 立方体有6个面, 但在THREE里面3个点构成一个面,那么就是12个面
// customeCube.js
const vertices = [
new THREE.Vector3(1, 3, 1),
new THREE.Vector3(1, 3, -1),
new THREE.Vector3(1, -1, 1),
new THREE.Vector3(1, -1, -1),
new THREE.Vector3(-1, 3, -1),
new THREE.Vector3(-1, 3, 1),
new THREE.Vector3(-1, -1, -1),
new THREE.Vector3(-1, -1, 1),
]
const faces = [
new THREE.Face3(0, 2, 1),
new THREE.Face3(2, 3, 1),
new THREE.Face3(4, 6, 5),
new THREE.Face3(6, 7, 5),
new THREE.Face3(4, 5, 1),
new THREE.Face3(5, 0, 1),
new THREE.Face3(7, 6, 2),
new THREE.Face3(6, 3, 2),
new THREE.Face3(5, 7, 0),
new THREE.Face3(7, 2, 0),
new THREE.Face3(1, 3, 4),
new THREE.Face3(3, 6, 4),
]
const gem = new THREE.Geometry()
gem.vertices = vertices
gem.faces = faces
gem.computeFaceNormals()
const mat = [
new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true}),
new THREE.MeshLambertMaterial({opacity: 0.6, color: 0x44ff44, transparent: true}),
]
const group = new THREE.Group()
for ( let i = 0, l = mat.length; i < l; i ++ ) {
group.add( new THREE.Mesh( gem, mat[i] ) )
}
// 遍历所有mesh
group.children.forEach((mesh) => mesh.castShadow = true)
group.name = 'customCube'
export default group
最终展示的效果:
注意在创建faces的时候, 如果需要让这个面, 面向摄像机, 那么3个点需要按照顺时针方向添加,反之为逆时针
最后一个clone方法
const clone = function() {
const geo = scene.getObjectByName('customCube').children[0].geometry
const mats = [
new THREE.MeshLambertMaterial({opacity: 0.6, color: 0xff44ff, transparent: true}),
new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true}),
]
const group = new THREE.Group()
for (let i = 0, l=mats.length; i < l; i ++) {
group.add(new THREE.Mesh(geo, mats[i]))
}
group.translateX(10 * (Math.random() + 1))
group.translateY(3)
group.name = 'clone'
scene.remove(scene.getObjectByName('clone'))
scene.add(group)
}
clone可以克隆整个物体, 也可以只复制geometry, 再重新给material.
网格对象的属性和方法
网格的创建需要一个几何体以及一个或多个材质, 创建完成之后, 添加到场景中进行渲染. 以下属性用于改变网格在场景中的位置和显示的效果
- position: 相对于父对象的位置
- rotation: 设置绕每个轴的旋转弧度, 还可以单独设置, 比如rotateX/Y/Z, 注意amount, 要使用弧度, 换算方式为
degree * (Math.PI/180)- rotation.x = amount
- rotation.set(x,y,z)
- rotation = new THREE.Vector3(x,y,z)
- scale: 沿X,Y,Z放大缩小
- translateX/Y/Z(amount): 水平移动amount的距离, 不改变绝对位置(可参考css的transform属性)
- visible: 如果为false, 不会渲染到场景中
需要注意的是dat.GUI的用法, 以下是设置controller
const controls = new function () {
this.scaleX = 1;
this.scaleY = 1;
this.scaleZ = 1;
this.positionX = 0;
this.positionY = 4;
this.positionZ = 0;
this.rotationX = 0;
this.rotationY = 0;
this.rotationZ = 0;
this.scale = 1;
this.translateX = 0;
this.translateY = 0;
this.translateZ = 0;
this.visible = true;
// 点击时调用函数
this.translate = function () {
cube.translateX(controls.translateX);
cube.translateY(controls.translateY);
cube.translateZ(controls.translateZ);
controls.positionX = cube.position.x;
controls.positionY = cube.position.y;
controls.positionZ = cube.position.z;
}
};
按类别添加到dat.GUI中, 并监听变化进行响应
const gui = new dat.GUI();
// addFolder, 添加一个文件夹的层级
guiScale = gui.addFolder('scale');
guiScale.add(controls, 'scaleX', 0, 5);
guiScale.add(controls, 'scaleY', 0, 5);
guiScale.add(controls, 'scaleZ', 0, 5);
guiPosition = gui.addFolder('position');
const contX = guiPosition.add(controls, 'positionX', -10, 10);
const contY = guiPosition.add(controls, 'positionY', -4, 20);
const contZ = guiPosition.add(controls, 'positionZ', -10, 10);
// 监听器
contX.listen();
// 响应, 每次更改时触发
contX.onChange(function (value) {
cube.position.x = controls.positionX;
});
contY.listen();
contY.onChange(function (value) {
cube.position.y = controls.positionY;
});
contZ.listen();
contZ.onChange(function (value) {
cube.position.z = controls.positionZ;
});
摄像机
一个场景的渲染如果没有摄像机就无法被展示出来, 一共有两种摄像机
- 正交投影: 提供伪3D效果
- 透视投影: 提供近大远小的真实效果
创建透视投影的参数有:
- fov: 视场
- near: 进场
- far: 远场
- aspect: 长宽比
- zoom: 缩放比
创建正交投影的参数为, 很像一个长方体:
- left: 左边界
- right: 右边界
- top: 上边界
- bottom: 下边界
- near: 近场
- fat: 远场
- zoom: 缩放比
摄像机都有一个方法: lookAt(new THREE.Vector3(x,y,z)) 表示摄像机应该指向的地方, 可以指向一个点, 也可以指向一个具体的物体, 比如plane, 下一部分会详细说明摄像机的具体用法, 此处先带过
总结
上述部分讲述了THREE.Scene的所有属性和方法, THREE.Geometry对象或使用内置几何体创建几何体, 最后是两种摄像机的简介.相信你们可以制作一些简单的场景和几何体了.