three.js 渲染调优,如何提升3d场景更逼真的渲染效果
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空间。
- linear颜色空间:物理上的线性颜色空间,当计算机需要对sRGB像素运行图像处理算法时,一般会采用线性颜色空间计算。
- sRGB颜色空间: sRGB是当今一般电子设备及互联网图像上的标准颜色空间。较适应人眼的感光。sRGB的gamma与2.2的标准gamma非常相似,所以在从linear转换为sRGB时可通过转换为gamma2.2替代。
- 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场景更逼真的渲染效果