1. 基于物理的渲染和照明
PBR:Physically Based Rendering,即物理渲染技术,使用真实世界的物理学来计算表面对光的反应方式。
物理上正确的光照强度计算
物理正确照明意味着使用真实世界的物理方程计算 光如何随着与光源的距离(衰减)而衰减
而在three.js中,渲染器设置正确照明,将physicallyCorrectLights属性设置为true即可
renderer.physicallyCorrectLights = true
基于物理的渲染涉及以物理上正确的方式 计算光与表面的反应,幸运的是,我们不必了解它们即可使用它们!
2. 光照
three.js 中所有的大小单位是米
2.1 直接光照
直接来自灯泡并撞击物体的光线,有四种可选类型
DirectionalLight:阳光
PointLight:灯泡
SpotLight:聚光灯
RectAreaLight:条形照明或明亮的窗户
2.2 环境光
光线在击中物体之前已经从墙壁和房间内的其他物体反弹,每次反弹都会改变颜色并失去强度,
Renderer
class Renderer {
constructor(opts) {
opts = {
antialias: false, // 抗锯齿AA
physicallyCorrectLights: false, // 物理校正灯光
domElement: document.createElement('canvas'), // 基于canvas绘制
// ...
...opts
}
for(let key in opts) this[key] = opts[key]
},
render(scene, camera) { ... } // 基于webGL引擎绘制canvas
setSize(width, height) { ... } // 初始化渲染宽高比
setPixelRatio(pr) { ... } // 初始化像素比pr
setAnimationLoop(cb) { cb ? cb() : null } // 根据设备帧率执行动画循环,底层基于requestAnimationFrame
}
Material
class Metarial {
constructor(opts) {
opts = {
color: 'white',
opacity: 1, // 透明度
transparent: false, // 是否透明,为true时动态更新opacity为0
visible: true, // 是否可见
shadowSide: null, // 定义投射阴影面,可选值:FrontSide,BackSide,DoubleSide
map: undefined, // textures UV映射
envMaps: undefined, // enviroment IBL反射映射,即Image-Based Lighting
// ...
...opts
}
}
clone() { ... } // 返回具有相同参数的新材质
copy(metarial) { ... } // 将传入材料的参数复制到本身
toJSON(meta) { ... } // 返回three.js JSON对象
}
纹理
几何建模比材质更耗性能,对于一个复杂模型:大尺寸使用几何建模级别,小尺寸使用材质级别
纹理映射意味着拿着图像并将其拉伸到3D对象的表面上
可以使用textures纹理来表示颜色、粗糙度和不透明度等材料属性
1. 投影映射
如今已经开发了许多纹理映射技术,其中最简单的是 投影映射,它将纹理投影到一个对象(或场景)上,就好像通过电影放映机照射一样——想象一下,将您的手放在电影放映机前,并看到投影到您皮肤上的图像
2. UV映射
将纹理映射到不规则几何体上不适合使用投影映射,而是采用UV映射技术,将二维纹理映射到三维几何体即(u,v)⟶(x,y,z)
纹理和几何模型都已内置好了UV映射,因此我们无须手动映射
当使用纹理来表示颜色时,我们会说我们正在将纹理分配给材质上的颜色贴图槽
构成屏幕像素的点为像素*pixels*,构成纹理的点称为纹素 *texels*
使用TextureLoader可以将PNG、JPG、GIF、BMP等格式的2D图片文件load处理为textures,随后设置为material.map即可
let textureLoader = new TextureLoader() // 纹理Loader
let texture = textureLoader.load('@/assets/textures/floor.png') // 纹理
let material = new MeshStandardMaterial({ // 纹理材料
map: texture
})
let geometry = new BoxBufferGeometry(2, 2, 2) // 立方体
let cube = new Mesh(geometry, material) // 纹理材质的立方体
3. IBL映射
全称为Image-Based Lighting,预先计算照明信息,存储在纹理中,然后作对物体作环境映射(也称反射映射),设置为material.envMap即可
固定帧和动态帧
固定帧:动画速率随设备帧率变大而加快,例如60FPS和240FPS,若定义物体每一帧移动1m,那么两者在1s内移动的距离分别是60m和240m(应该避免这种情况)
动态帧:动画速率不随设备帧率改变而改变(帧率越高动画越平滑),实现原理:根据当前帧率动态调整物体每帧移动的距离,并采用累加物体在1s内移动的距离即可
可采用Clock类实现动态帧
import { Clock } from 'three'
let clock = new Clock()
renderer.setAnimationLoop(() => {
let delta = clock.getDelta() // 返回与上次调用的间隔时长,在60fps在1/60附近,在240fps在1/240附近(帧率会改变)
o.position.x += 1 * delta // 动态调整每帧移动距离
renderer.render()
// 验证在1s内,60fps和240fps帧率下物体移动距离相同
// 60fps:调用约60次,累加移动距离x = 1 * (1/60) * 60 = 1m
// 240fps:调用约240次,累加移动距离x = 1 * (1/240) * 240 = 1m
})
Vector3
class Vector3 {
constructor(x = 0, y = 0, z = 0) {
Vector3.prototype.isVector3 = true
this.x = x
this.y = y
this.z = z
}
set(x, y, z) {
this.x = x
this.y = y
this.z = z
return this
}
setX(x) {
this.x = x
return this
}
copy(v) { // 复制向量
this.setXYZ(v, (key, val) => this[key] = val)
return this
}
clone() { // 返回自身克隆
let { x, y, z } = this
return new this.constructor(x, y, z)
}
toArray(arr = [], offset = 0) {
let { x, y, z } = this
arr[offset++] = x
arr[offset++] = y
arr[offset++] = z
return arr
}
// 向量计算
add(v) {
let { x, y, z } = v
this.x += x
this.y += y
this.z += z
return this
}
addScalar(s) { // scalar:标量
this.x += s
this.y += s
this.z += s
return this
}
addVectors(a, b) {
let { x: x1, y: y1, z: z1 } = a
let { x: x2, y: y2, z: z2 } = b
this.x = x1 + x2
this.y = y1 + y2
this.z = z1 + z2
return this
}
mutiply(v) {
let { x, y, z } = v
this.x *= x
this.y *= y
this.z *= z
return this
}
mutiplyScalar(s) {
this.x *= s
this.y *= s
this.z *= s
return this
}
mutiplyVectors(a, b) {
let { x: x1, y: y1, z: z1 } = a
let { x: x2, y: y2, z: z2 } = b
this.x = x1 * x2
this.y = y1 * y2
this.z = z1 * z2
return this
}
sub(v) { ... }
subScalar(s) { ... }
subVectors(a, b) { ... }
}
Object3D
let _object3DId = 0
class Object3D {
constructor() {
this.isObject3D = true
const position = new Vector3()
const scale = new Vector3(1, 1, 1)
const target = new Vector3() // 观察目标
const rotation = new Euler()
this.matrix = new Matrix4()
this.matrixWorld = new Matrix4()
this.name = ''
this.children = [] // 子集
this.parent = null // 老爹
this.type = 'Object3D'
this.castShadow = false // 投射阴影
this.receiveShadow = false // 接收阴影
Object.defineProperties(this, {
'position': {
configurable: true,
enumerable: true,
value: position
},
'scale': {
configurable: true,
enumerable: true,
value: scale
},
'rotation': {
configurable: true,
enumerable: true,
value: rotation
},
'target': {
configurable: true,
enumerable: true,
value: target
},
'id': {
value: _object3DId++
}
})
}
lookAt(x, y, z) {
x.isVector3 ? this.target.copy(x) : this.target.set(x, y, z)
// ...
}
add(o) {
if (!o || !o.isObject3D || o === this) {
console.error('err')
return this
}
if (arguments.length > 1) {
for (let i = 0; i < arguments.length; i++) this.add(arguments[i])
return this
}
if (o && o.isObject3D) {
o.parent && o.parent.remove(o)
o.parent = this
this.children.push(o)
return this
}
}
remove(o) {
if (arguments.length > 1) {
for (let i = 0; i < arguments.length; i++) this.remove(arguments[i])
return this
}
let idx = this.children.indexOf(o)
if (idx != -1) {
o.parent = null
this.children.splice(idx, 1)
}
return this
}
clear() { ... } // 移除所有子元素
getObjectByName(name) {
return this.getObjectByProperty('name', name)
}
getObjectById(id) {
return this.getObjectByProperty('id', id)
}
getObjectByProperty(key, val) {
if (this[key] == val) return this
for (let child of this.children) {
let obj = child.getObjectByProperty(key, val)
if(obj) return obj
}
return undefined
}
getObjectsByProperty(key, val) {
let ret = []
if (this[ key ] == value) ret.push(this)
for (let child of this.children) {
let childRet = child.getObjectsByProperty(key, val)
ret = [ ret, ...childRet ]
}
return ret
}
}
OrbitControls
class OrbitControls extens Object3D {
constructor(camera, canvas) {
this.camera = camera
this.canvas = canvas
this.enabled = true // 开启轨道控制器
this.enablePan = true // 开启拖动
this.enableRotate = true // 开启旋转
this.enableZoom = true // 开启缩放
this.autoRotate = false // 关闭围绕target旋转
this.enableDamping = false // 关闭阻尼(启用阻尼可以模拟现实世界中的惯性,结合update方法更丝滑)
this.minDistance = 0 // 最小缩放级别
this.maxDistance = Infinite // 最大缩放级别
this.minAzimuthAngle = -Infinity // 最小水平旋转角 (方位角AzimuthAngle) 绕Z轴
this.maxAzimuthAngle = Infinity // 最大水平旋转角
this.minPolarAngle = 0 // 最小垂直旋转角 (极角PolarAngle) 绕X轴
this.maxPolarAngle = Math.PI // 最大垂直旋转角
this.dampingFactor = 0.05 // 阻尼速度,范围最好限定在(0,1)范围,值越大对运动“阻力”越大
this.autoRotateSpeed = 2 // 围绕target旋转速度
}
update() { ... } // 在动画循环每帧中调用此函数来更新轨道控制器时更丝滑
listenToKeyEvents(window) { ... } // 监听按键并使用箭头键平移相机
}
Light
0. Light基类
// Light继承Object3D,但rotate和scale对其无效
class Light extends Object3D {
constructor(color = 0xffffff, intensity = 1) {
super()
this.isLight = true
this.type = 'Light'
this.color = new color(color)
this.intensity = intensity // 光强
}
copy(source, recursive) {
let { color, intensity } = source
super(source, recursive)
this.color.copy(color)
this.intensity = intensity
return this
}
toJSON(data){ ... }
}
1. DirectionalLight
属于直接光,模拟太阳光,拓展使用target、shadow、copy(),意味着可指定照射目标、投影、复制坐标
class DirectionalLight extends Light {
constructor(color, intensity) {
super(color, intensity)
this.isDirectionalLight = true
this.type = 'DirectionalLight'
this.target = new Object3D()
this.shadow = new DirectionalLightShadow()
}
copy(source) {
super.copy(source)
this.target = source.target.clone()
this.shadow = source.shadow.clone()
return this
}w
}
2. AmbientLight
属于间接光,模拟环境光,纯继承,意味着无投影、无需指定照射目标和自身坐标
// 对于AmbientLight,position、scale、rotate都无效
class AmbientLight extends Light {
constructor(color, intensity) {
super(color, intensity)
this.isAmbientLight = true
this.type = 'AmbientLight'
}
}
通常会将间接光和直接光结合使用,且间接光强通常设置比直接光强小,这样就能模拟更真实的现实光照
3. HemisphereLight
属于间接光,模拟天空、地面光渐变
class HemisphereLight extends Light {
constructor(skyColor, groundColor, intensity) {
super(skyColor, intensity)
this.isHemisphereLight = true
this.type = 'HemisphereLight'
this.groundColor = new Color(groundColor)
}
copy(source, recursive) {
super.copy(source, recursive)
this.groundColor.copy(source.groundColor)
return this
}
}
使用HemisphereLight模拟天空、地面光渐变可创建更真实的户外和室内光照场景
在户外场景中,物体从上方被太阳和天空照亮,然后从地面反射的阳光中接收二次光
在室内环境中,最亮的灯通常位于天花板上,这些灯会反射到地板上以形成昏暗的二次照明
4. PointLight
属于直接光,模拟灯泡点光源,
class PointLight extends Light {
constructor(color, intensity, distance = 0, decay = 2) {
super(color, intensity)
this.isPointLight = true
this.type = 'PointLight'
this.distance = distance // 光源到光强为0的距离
this.decay = decay // 光强随距离衰退量
this.shadow = new PointLightShadow()
}
get power() { // 光功率,与光强相关联,intensity以1为单位,power以4π为单位
return this.intensity * 4 * Math.PI
}
set power(power) {
this.intensity = power / (4 * Math.PI)
}
copy(source, recursive) {
super.copy(source, recursive)
let { distance, decay, shadow } = source
this.distance = distance
this.decay = decay
this.shadow = shadow.clone()
return this
}
}