3. 基于物理渲染PBR

150 阅读8分钟

1. 基于物理的渲染和照明

PBRPhysically 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)

image.png

纹理和几何模型都已内置好了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即可

固定帧和动态帧

固定帧动画速率设备帧率变大而加快,例如60FPS240FPS,若定义物体每一帧移动1m,那么两者在1s内移动的距离分别是60m240m(应该避免这种情况)

动态帧动画速率不随设备帧率改变而改变(帧率越高动画越平滑),实现原理:根据当前帧率动态调整物体每帧移动的距离,并采用累加物体在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

属于直接光,模拟太阳光,拓展使用targetshadowcopy(),意味着可指定照射目标、投影、复制坐标

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
  }
}