three.js 渲染调优,如何提升3d场景更逼真的渲染效果

作者: TAIS3 分类: JavaScript,threeJS 发布时间: 2022-08-29 11:16

three.js就不介绍了,本章内容主要讲解怎么渲染出更逼真的3d场景效果、渲染出更真实的图片。一般用了three.js的人都想把渲染效果做的更好, 最终效果受很多情况影响,比如材质、灯光、环境、模型质量,还需要结合实际情况调节。从各个地方收集的信息写成笔记。

1、渲染参数调优

// ================================================================================
// 平行光参数优化(模拟太阳)
// --------------------------------------------------------------------------------

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.castShadow = true

directionalLight.shadow.mapSize.height = 512 * 2
directionalLight.shadow.mapSize.width = 512 * 2

// 解决暗影
// 0.00  0.05 最好的区间
directionalLight.shadow.bias = 0.05 // 平面
directionalLight.shadow.normalBias = 0.05 // 圆形表面,缩小受影响的网格,使其不会在自身上投射阴影

// ================================================================================


// ================================================================================
// Encoding
// --------------------------------------------------------------------------------

// 环境贴图
const cubeTextureLoader = new THREE.CubeTextureLoader()
const environmentMapTexture = cubeTextureLoader.load([
  "/textures/environmentMaps/px.jpg",
  "/textures/environmentMaps/nx.jpg",
  "/textures/environmentMaps/py.jpg",
  "/textures/environmentMaps/ny.jpg",
  "/textures/environmentMaps/pz.jpg",
  "/textures/environmentMaps/nz.jpg",
])

environmentMapTexture.encoding = THREE.sRGBEncoding

// gltf,模型启用阴影和环境贴图
const gltfLoader = new GLTFLoader()
gltfLoader.load("/model.glb", (gltf) => {
  const model = gltf.scene

  model.traverse((child) => {
    if (
      child instanceof THREE.Mesh &&
      child.material instanceof THREE.MeshStandardMaterial
    ) {
      child.castShadow = true
      child.receiveShadow = true

      child.material.envMap = environmentMapTexture
      child.material.envMapIntensity = 3

      // child.material.needsUpdate = true 
    }
  })
})

// renderer
renderer.outputEncoding = THREE.sRGBEncoding

// ================================================================================


// ================================================================================
// Tonemapping
// --------------------------------------------------------------------------------

// 色调映射参数
// THREE.NoToneMapping
// THREE.LinearToneMapping
// THREE.ReinhardToneMapping
// THREE.CineonToneMapping
// THREE.ACESFilmicToneMapping

// 使用算法将HDR值转换为LDR值,使其介于0到1之间, 0  1
renderer.toneMapping = THREE.ACESFilmicToneMapping
// 渲染器将允许多少光线进入
renderer.toneMappingExposure = 3

// ================================================================================


// ================================================================================
// Rendering
// --------------------------------------------------------------------------------

renderer.physicallyCorrectLights = true // synchronise light values between 3D software and three.js

// 启用阴影,调整阴影类型
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap

// ================================================================================

2、材质调优

// ================================================================================
// 纹理特征
// --------------------------------------------------------------------------------

//  不透明度仅在透明打开时有效
material.transparent = true
material.opacity = 0.5

// 材质面,双面、前面、背面
material.side = THREE.DoubleSide || THREE.FrontSide || THREE.BackSide

//  改变材质的平面着色需要重新编译材质
material.flatShading = true || false
material.needsUpdate = true

// ================================================================================

// ================================================================================
//  加载纹理
// --------------------------------------------------------------------------------

// 使用全局 LoadingManager 来相互化/合并所有纹理加载器
const loadingManager = new THREE.LoadingManager()
loadManager.onStart = () => {
  console.log("loading started")
}
loadManager.onProgress = () => {
  console.log("loading")
}
loadManager.onLoad = () => {
  console.log("loading completed")
}
loadManager.onError = () => {
  console.log("loading failed")
}

const cubeTextureLoader = new THREE.CubeTextureLoader(loadManager)
// 立方体贴图必须有 6 个面
const environmentMapTexture = cubeTextureLoader.load([
  "/environmentMaps/px.png", // positive x
  "/environmentMaps/nx.png", // negative x
  "/environmentMaps/py.png",
  "/environmentMaps/ny.png",
  "/environmentMaps/pz.png",
  "/environmentMaps/nz.png",
])

const material = new THREE.MeshStandardMaterial({
  envMap: environmentMapTexture,
  metalness: 0.7,
  roughness: 0.2,
})
// or
scene.background = environmentMapTexture

// ================================================================================

// ================================================================================
// 使用纹理
// --------------------------------------------------------------------------------

const textureLoader = new THREE.TextureLoader(loadingManager)

//  颜色(反照率)纹理 Color (Albedo) texture
const colorTexture = textureLoader.load("texture.jpg")
const material = new THREE.MeshStandardMaterial({
  map: colorTexture,
})

// 透明贴图纹理 Alpha texture
const alphaTexture = textureLoader.load("alpha.jpg")
const material = new THREE.MeshStandardMaterial({
  transparent: true,
  alphaMap: alphaTexture,
})

// 位移(高度)纹理 Displacement (height) texture
// 需要几何体中有很多顶点才能准确置换材质的高度
const displacementTexture = textureLoader.load("displacement.jpg")
const material = new THREE.MeshStandardMaterial({
  displacementMap: displacementTexture,
  displacementScale: 0.35,
})

// 普通纹理 Normal texture
// 建议使用 PNG 将每个顶点的精确位置与纹理细节匹配
const normalTexture = textureLoader.load("normal.png")
const material = new THREE.MeshStandardMaterial({
  normalMap: normalTexture,
})
material.normalScale.x = 0.5 // 0  1
material.normalScale.y = 0.5 // 0  1

//  环境遮挡纹理
const ambientOcclusionTexture = textureLoader.load("ambientOcclusion.jpg")
const cube = new THREE.BoxBufferGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
  aoMap: ambientOcclusionTexture,
})
// AO 贴图需要将现有的 uv 坐标复制到 uv2
cube.geometry.setAttribute(
  "uv2",
  new THREE.BufferAttribute(cube.geometry.attributes.uv.array, 2)
)

// 金属度和粗糙度纹理 Metalness & Roughness texture
const metalnessTexture = textureLoader.load("metalness.jpg")
const roughnessTexture = textureLoader.load("roughness.jpg")
const material = new THREE.MeshStandardMaterial({
  metalnessMap: metalnessTexture,
  roughnessMap: roughnessTexture,
  //  使用金属度和粗糙度贴图时,不应显式声明金属度和粗糙度值
})

//  MatCap(材质捕获)纹理 MatCap (material capture) texture
const matcapTexture = textureLoader.load("matcap.jpg")
const material = new THREE.MeshMatcapMaterial({
  matcap: matcapTexture,
})

// 渐变纹理
// 渐变纹理可以是一个非常小的正方形图像,从黑色到白色的阴影
const gradientTexture = textureLoader.load("gradient.jpg")
const material = new THREE.MeshToonMaterial({
  gradientMap: gradientTexture,
})
// 卡通外观
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false

// ================================================================================

3、阴影调优

import * as THREE from "three"

// ================================================================================
// 渲染阴影
// --------------------------------------------------------------------------------
// 启用渲染阴影
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap // THREE.PCFShadowMap (default)

// 阴影贴图大小在增加之前应该仔细考虑,因为它可能导致非常大的阴影贴图文件
// 尝试先收紧阴影的相机,直接观察物体
// 由于 mipmapping,必须是 2 的幂(缩小到 1x1)
lightThatCastShadow.shadow.mapSize.height = 512 * 2 // 1024 x 1024
lightThatCastShadow.shadow.mapSize.width = 512 * 2

lightThatCastShadow.shadow.radius = 10 // 阴影半径不适用于 THREE.PCFSoftShadowMap

// 不在一行中渲染阴影相机助手
lightThatCastShadow.visible = false

// DirectionalLight 阴影
// --------------------------------------------------------------------------------
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.castShadow = true

directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.bottom = -2
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.right = 2

directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6

// DirectionalLightCameraHelper是正交相机
const directionalLightCameraHelper = new THREE.CameraHelper(
  directionalLight.shadow.camera
)
scene.add(directionalLightCameraHelper)

// spotLight 聚光灯阴影
// --------------------------------------------------------------------------------
const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3)
spotLight.castShadow = true

spotLight.shadow.camera.fov = 30
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6

// SpotLightCameraHelper是一个透视相机
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)

// PointLight 点光源阴影
// --------------------------------------------------------------------------------
const pointLight = new THREE.PointLight(0xffffff, 0.3)
pointLight.castShadow = true

pointLight.shadow.mapSize.height = 512 * 2
pointLight.shadow.mapSize.width = 512 * 2

pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5

// PointLight 阴影的 fov 不应改变,因为它被设置为捕捉场景各个方向的阴影

// PointLightCameraHelper 是许多透视相机的集合,但它只显示最后一个,它在y轴上指向下
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
scene.add(pointLightCameraHelper)

// ================================================================================


// ================================================================================
// Baked 烘焙阴影
// --------------------------------------------------------------------------------
const textureLoader = new THREE.TextureLoader()

// 静态烘焙阴影,用于保持静止的对象
const staticBakedShadow = textureLoader.load("/textures/bakedShadow.jpg")
new THREE.MeshBasicMaterial({ map: staticBakedShadow })

// 动态动画烘焙阴影,用于在场景中移动的对象。
// 这需要阴影跟踪对象的位置,以便真实地为自己设置动画,例如它的不透明度和比例
const dynamicBakedShadow = textureLoader.load(
  "/textures/dynamicBakedShadow.jpg"
)
// 动态烘焙阴影将跟踪的对象
const sphere = new THREE.Mesh(
  new THREE.SphereBufferGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial()
)
// 烘焙阴影
const sphereShadow = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(1.5, 1.5),
  new THREE.MeshBasicMaterial({
    color: 0x000000, // black shadow
    alphaMap: dynamicBakedShadow,
    transparent: true,
  })
)
// 在每一帧中,烘焙阴影跟随球体的位置,并且其不透明度动态跟踪到球体的 y 位置
requestAnimationFrame(() => {
  sphereShadow.position.x = sphere.position.x
  sphereShadow.position.z = sphere.position.z
  sphereShadow.material.opacity = 1 - sphere.position.y + 0.2
})

// ================================================================================

4、灯光调优

环境光	均匀照亮整个场景
半球光	天空颜色和地板颜色之间的渐变光,照亮整个场景
定向光	相互平行的太阳光线,无论位置如何,都具有无限范围
点光源	无限小灯笼,从其位置向各个方向照明
聚光灯	手电筒,锥形从亮到暗
矩形区域光	摄影棚灯
## 性能成本
AmbientLight  HemisphereLight  DirectionalLight  PointLight  SpotLight  RectAreaLight

// ================================================================================
// Lights
// --------------------------------------------------------------------------------
// 如果灯光的位置或旋转发生变化,所有灯光助手都需要在 requestAnimationFrame 中更新,以准确反映灯光的位置

// AmbientLight 环境光
// --------------------------------------------------------------------------------
// AmbientLight 没有灯光助手
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)

// HemisphereLight 半球光
// --------------------------------------------------------------------------------
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x00ff00) // (sky color, floor color)
const hemisphereLightHelper = new THREE.HemisphereLightHelper(
  hemisphereLight,
  0.5
)
scene.add(hemisphereLight)
scene.add(hemisphereLightHelper)

// DirectionalLight 平行光、定向光
// --------------------------------------------------------------------------------
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
const directionalLightHelper = new THREE.DirectionalLightHelper(
  directionalLight,
  0.5
)
scene.add(directionalLight)
scene.add(directionalLightHelper)

// PointLight 点光源(蜡烛)
// --------------------------------------------------------------------------------
const pointLight = new THREE.PointLight(0xffffff, 0.5, 10, 2)
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.5)
scene.add(pointLight)
scene.add(pointLightHelper)

// SpotLight 聚光灯(舞台聚光灯)
// --------------------------------------------------------------------------------
const spotlight = new THREE.SpotLight()
spotlight.color = 0xffffff
spotlight.intensity = 0.5
spotlight.distance = 10 // 光线从亮到暗的范围
spotlight.angle = Math.PI / 4 // 光锥末端的宽度,以弧度为单位
spotlight.penumbra = 0.25 // 光锥形状边缘的锐度,1 最锐利
spotlight.decay = 1 // 光线在远处变暗的速度,通常设置为 1 让距离决定光线衰减

// 改变聚光灯方向
scene.add(spotlight.target)
spotlight.target.position.set(0, 1, 0)
scene.add(spotlight)

// spotlight helper  聚光灯助手
const spotLightHelper = new THREE.SpotLightHelper(spotlight) // 无助手大小
scene.add(spotLightHelper)
requestAnimationFrame(() => {
  spotLightHelper.update()
})

// RectAreaLight 矩形区域光
// --------------------------------------------------------------------------------
const rectAreaLight = new THREE.RectAreaLight(0xffffff, 1, 3, 1)
//  RectAreaLight 默认不看(0,0,0),需要手动设置
rectAreaLight.lookAt(new THREE.Vector3()) // empty vector 3 is (0,0,0)
scene.add(rectAreaLight)

// RectAreaLightHelper 需要从 three.js 库中自定义导入
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper"
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)
requestAnimationFrame(() => {
  rectAreaLightHelper.position.copy(rectAreaLight.position) // helper 复制自己的光源的位置
  rectAreaLightHelper.quaternion.copy(rectAreaLight.quaternion) // helper 复制自身光照的旋转
  rectAreaLightHelper.update()
})

// ================================================================================

5、光线投射(对象拾取)

const raycaster = new THREE.Raycaster()

// 默认情况下,raycaster 从 0 投射到 Infinity (near = 0, far = Infinity)

// ================================================================================
// 自定义光线投射
// --------------------------------------------------------------------------------

const rayOrigin = new THREE.Vector3(-5, 0, 0)
const rayDirection = new THREE.Vector3(10, 0, 0)

// 在 0  1 之间变换光线方向,以确保光线方向的矢量仍然是 1 个单位长
rayDirection.normalize()

raycaster.set(rayOrigin, rayDirection)

// ================================================================================

// ================================================================================
// Raycaster 交点
// --------------------------------------------------------------------------------
// 铸造一个对象
raycaster.intersectObject(object)

// 转换多个对象(对象必须在数组中)
const objects = [object1, object2, object3]
raycaster.intersectObjects(objects)

// 相交对象
// distance - 光线投射器原点(通常是相机)和对象面部之间的距离长度
// face - 包含 Face3(a, b, c) 和人脸的法线 (x, y, z)
// 对象 - 相交的对象
// point - 3D世界空间中的交点坐标(Vector3)(通常基于原点(0,0,0))
// uv - 交点的 uv

// Raycaster 鼠标事件
const mouse = new THREE.Vector2() // stores two values, (x, y)
let currentIntersect = null // objects currently intersected

// 鼠标进入鼠标离开
window.addEventListener("mousemove", (e) => {
  const { clientX, clientY } = e

  // 根据three.js (x, y) 轴在 -1 和 1 之间归一化鼠标
  mouse.x = (clientX / sizes.width) * 2 - 1
  mouse.y = -(clientY / sizes.height) * 2 + 1

  // 使用基于场景相机的鼠标坐标
  raycaster.setFromCamera(mouse, camera)

  const intersects = raycaster.intersectObjects(objects)

  // 鼠标与对象相交
  const isIntersecting = intersects.length > 0

  if (isIntersecting) {
    // 鼠标进入
    if (!currentIntersect) {
      currentIntersect = intersects[0].object
      // 鼠标进入时改变对象颜色为绿色
      intersects[0].object.material.color.set("green")
    }
  } else {
    // 鼠标离开
    if (currentIntersect) {
      currentIntersect = null
      // 在鼠标离开时将所有对象的颜色更改为蓝色(它们的默认状态)
      objects.forEach((obj) => obj.material.color.set("blue"))
    }
  }
})

// 鼠标点击
window.addEventListener("click", () => {
  if (currentIntersect) {
    // 将点击的对象变为橙色
    switch (currentIntersect) {
      case object1:
        object1.material.color.set("orange")
        break
      case object2:
        object2.material.color.set("orange")
        break
      case object3:
        object3.material.color.set("orange")
        break
    }
  }
})

// ================================================================================

6、其他笔记

//加载管理器
const loadingManager = new THREE.LoadingManager()
 loadingManager.onStart = () =>
 {
     console.log('loading started')
 }
 loadingManager.onLoad = () =>
 {
     console.log('loading finished')
 }
 loadingManager.onProgress = () =>
 {
     console.log('loading progressing')
 }
 loadingManager.onError = () =>
 {
     console.log('loading error')
 }
 const textureLoader = new THREE.TextureLoader(loadingManager)
 const colorTexture = textureLoader.load('/textures/minecraft.png')
 //  colorTexture.repeat.x = 2
//  colorTexture.repeat.y = 3

//  uv重复方式、防止uv拉伸
//  colorTexture.wrapS = THREE.RepeatWrapping
//  colorTexture.wrapT = THREE.RepeatWrapping

//  colorTexture.offset.x = 0.5
//  colorTexture.offset.y = 0.5
 
//  colorTexture.rotation = Math.PI * 0.25
//  colorTexture.center.x = 0.5
//  colorTexture.center.y = 0.5

colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter

//window resize窗口监听需要做的
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}
window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

7、色调映射 Tone mapping

色调映射Tone mapping旨在将超高的动态范围HDR转换到我们日常显示的屏幕上的低动态范围LDR的过程。
说明一下HDR和LDR(摘自知乎LDR和HDR):

  • 因为不同的厂家生产的屏幕亮度(物理)实际上是不统一的,那么我们在说LDR时,它是一个0到1范围的值,对应到不同的屏幕上就是匹配当前屏幕的最低亮度(0)和最高亮度(1)
  • 自然界中的亮度差异是非常大的。例如,蜡烛的光强度大约为15,而太阳光的强度大约为10w。这中间的差异是非常大的,有着超级高的动态范围。
  • 我们日常使用的屏幕,其最高亮度是经过一系列经验积累的,所以使用、用起来不会对眼睛有伤害;但自然界中的,比如我们直视太阳时,实际上是会对眼睛产生伤害的。

总结

上面是记录一些调渲染需要用到的参数笔记,那么怎么才能调节最好的效果,最重要的设置如下:

我们渲染采用最为专业的ACES色调映射(也是UE里面默认的色调映射),也只有ACES支持虚幻功能,虚幻绽放亮度。再然后我们需要把色彩空间编码改成THREE.sRGBEncoding即可。

在着色器中色值的提取与色彩的计算操作一般都是在线性空间。在webgl中,贴图或者颜色以srgb传入时,必须转换为线性空间。计算完输出后再将线性空间转为srgb空间。

  1. linear颜色空间:物理上的线性颜色空间,当计算机需要对sRGB像素运行图像处理算法时,一般会采用线性颜色空间计算。
  2. sRGB颜色空间: sRGB是当今一般电子设备及互联网图像上的标准颜色空间。较适应人眼的感光。sRGB的gamma与2.2的标准gamma非常相似,所以在从linear转换为sRGB时可通过转换为gamma2.2替代。
  3. gamma转换:线性与非线性颜色空间的转换可通过gamma空间进行转换。
最重要的几项设置!!!
renderer.physicallyCorrectLights = true 	//正确的物理灯光照射
renderer.outputEncoding = THREE.sRGBEncoding	//采用sRGBEncoding
renderer.toneMapping = THREE.ACESFilmicToneMapping  //aces标准
renderer.toneMappingExposure = 1.25		//色调映射曝光度
renderer.shadowMap.enabled = true	//阴影就不用说了
renderer.shadowMap.type = THREE.PCFSoftShadowMap //阴影类型(处理运用Shadow Map产生的阴影锯齿)

//此时模型效果很好了,但是如果使用了天空盒子,天空盒子贴图为rgb色彩空间,
//此时天空会出现灰蒙蒙,失去了对比度一样,那是因为贴图色彩空间默认不是
//THREE.sRGBEncoding,所以得在场景里面所有的图都使用THREE.sRGBEncoding,具体操作如下

environmentMap.encoding = THREE.sRGBEncoding
scene.background = environmentMap
scene.environmentMap = environmentMap

//之后就是材质贴图,three.js默认认将贴图编码格式定义为Three.LinearEncoding
const textureLoader = new THREE.TextureLoader();
        textureLoader.load( "./assets/texture/tv-processed0.png", function(texture){
            texture.encoding = THREE.sRGBEncoding;
        });
//这样也行        
basic.map.encoding = THREE.sRGBEncoding;

//最后想要环境反射,就把环境贴图贴上去
const updateAllMaterials = () => {
        scene.traverse(child => {
            if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
                child.material.envMap = environmentMap
                child.material.envMapIntensity = debugObject.envMapIntensity
                child.material.needsUpdate = true
                child.castShadow = true
                child.receiveShadow = true
            }
        })
    }

//顺便说一句,阴影看到奇怪的条纹,表示阴影失真
//在计算曲面是否处于阴影中时,由于精度原因,阴影失真可能会发生在平滑和平坦表面上,因此我们必须调整灯光阴影shadow的“偏移bias”和“法线偏移normalBias”属性来修复此阴影失真,
//bias通常用于平面
//normalBias通常用于圆形表面
directionalLight.shadow.normalBias = 0.05


注意:并不是所有的贴图都需要采用THREE.sRGBEncoding,其实规则很直接,所有我们能够直接看到的纹理贴图,比如map,就应该使用THREE.sRGBEncoding作为编码;而其他的纹理贴图比如法向纹理贴图normalMap就该使用THREE.LinearEncoding。
你可能会问那模型上的各种纹理贴图都要一个个亲自去设置吗?大可不必,因为GLTFLoader会将加载的所有纹理自动进行正确的编码

当然也有人认为GammaEncoding优于sRGBEncoding,原本还可以通过gamma矫正,但是从three.js r136 版本之后已经移除了gamma矫正,不建议使用了,全部使用THREE.sRGBEncoding替代,目前three.js输出渲染编码默认为THREE.LinearEncoding,并且从three.js官方讨论得知,有人建议设置THREE.sRGBEncoding为默认值,色调映射也可能会移除其他类型,只留下ACES映射、关闭色调映射。

后续再更新

文章来源于互联网:three.js 渲染调优,如何提升3d场景更逼真的渲染效果

发表回复