three.js学习笔记(十三)——真实渲染
初始场景
只有一个测试白球和gui面板
设置
把球体材质改为标准网格材质MeshStandardMaterial,再添加平行光
const directionalLight = new THREE.DirectionalLight('#ffffff',1)
directionalLight.position.set(0.25,3,-2.25)
scene.add(directionalLight)
gui.add(directionalLight, 'intensity').min(0).max(10).step(0.001).name('光照强度')
gui.add(directionalLight.position, 'x').min(- 5).max(5).step(0.001).name('光X')
gui.add(directionalLight.position, 'y').min(- 5).max(5).step(0.001).name('光Y')
gui.add(directionalLight.position, 'z').min(- 5).max(5).step(0.001).name('光Z')
默认情况下,three.js的光强数值不真实。为了使得光强更趋于真实值,应该把渲染器的physicallyCorrectLights属性设为true
renderer.physicallyCorrectLights = true
可以看到光源暗下来了,回到平行光设置将其光强提升到3
模型
下面把测试球体移除然后加载模型。
- 导入并实例化GLTFLoader
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
......
/**
1. Loader
*/
const gltfLoader = new GLTFLoader()
- 加载飞行员头盔模型
/**
* Model
*/
gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf',(gltf)=>{
scene.add(gltf.scene)
})
- 缩放大小并微调位置
gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf', gltf => {
gltf.scene.scale.set(10, 10, 10)
gltf.scene.position.set(0, -4, 0)
gltf.scene.rotation.y = Math.PI * 0.5
scene.add(gltf.scene)
gui.add(gltf.scene.rotation, 'y').min(- Math.PI).max(Math.PI).step(0.001).name('头盔旋转')
})
环境贴图
关于环境贴图可以看另一篇笔记three.js学习笔记(三)——material材质。我们将使用环境贴图作为背景,并照亮整个模型。
const cubeTextureLoader = new THREE.CubeTextureLoader()
- 加载环境贴图
/**
1. 环境贴图
*/
const environmentMap = cubeTextureLoader.load([
'/textures/environmentMaps/0/px.jpg',
'/textures/environmentMaps/0/nx.jpg',
'/textures/environmentMaps/0/py.jpg',
'/textures/environmentMaps/0/ny.jpg',
'/textures/environmentMaps/0/pz.jpg',
'/textures/environmentMaps/0/nz.jpg'
])
- 把贴图应用到场景的
background
属性
scene.background = environmentMap
将环境贴图应用到模型上
真实渲染的一个基本特征在于使用环境贴图来照亮模型。
前面已经教过如何使用envMap
属性将环境贴图应用到标准网格材质MeshStandardMaterial中,现在的问题在于我们的模型是由许多个网格Mesh组成的,因此我们要使用 traverse(…)方法来遍历场景中的所有三维物体Object3D类对象,包括继承自Object3D的Group和Mesh。
- 在创建环境贴图的前面创建updateAllMaterials函数
/**
1. 更新所有材质
*/
const updateAllMaterials = ()=>{
scene.traverse((child)=>{
console.log(child);
})
}
- 当模型加载完成并添加到场景后调用函数
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) =>
{
// ...
updateAllMaterials()
}
)
- 把envMap应用到材质为标准网格材质的Mesh上
/**
1. 更新所有材质
*/
const updateAllMaterials = ()=>{
scene.traverse((child)=>{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial){
child.material.envMap = environmentMap
}
})
}
- 上图看不出有什么变化,因此可以往GUI面板中添加envMapIntensity来调整环境贴图强度。
// 创建空调试对象
const debugObject = {}
...
debugObject.envMapIntensity = 5
// 当调整面板数值时调用更新材质函数
gui.add(debugObject,'envMapIntensity').min(0).max(10).step(0.01).onChange(updateAllMaterials)
注意在更新材质函数中应用envMapIntensity
const updateAllMaterials = ()=>{
scene.traverse((child)=>{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial){
child.material.envMap = environmentMap
child.material.envMapIntensity = debugObject.envMapIntensity
}
})
}
- 有一种更简单的方法将环境贴图应用到所有对象上,我们可以像更改场景的
background
属性一样去更改场景的environment
属性。这样做的话就不必再在updateAllMaterials函数中去设置环境贴图了。
scene.environment = environmentMap
但是我们仍然无法直接从场景里更改每个材质的环境贴图强度,因此还是需要updateAllMaterials函数。
渲染器
尽管目前看起来效果还行,但在颜色方面还是有点欠缺需要下点工夫。这是因为WebGLRenderer 属性的问题。
Output encoding
outputEncoding
属性控制输出渲染编码。默认情况下,outputEncoding的值为THREE.LinearEncoding
,看起来还行但是不真实,建议将值改为THREE.sRGBEncoding
renderer.outputEncoding = THREE.sRGBEncoding
这下我们可以看到更亮的材质,同时这也影响到环境贴图。
除此之外还有另一个属性值为THREE.GammaEncoding
,这种编码的优点在于它允许我们使用一种表现像亮度brightness
的叫gammaFactor
的值。GammaEncoding是一种存储颜色的方法,根据人眼的敏感度优化明暗值的存储方式。当使用sRGBEncoding时,其实就像使用默认gammaFactor值为2.2的GammaEncoding。
下面链接可以提供更多信息关于GammaEncoding和sRGBEncoding
- https://www.donmccurdy.com/2020/06/17/color-management-in-threejs/
- https://medium.com/game-dev-daily/the-srgb-learning-curve-773b7f68cf7a
尽管这样就可能会有人认为GammaEncoding优于sRGBEncoding,因为我们可以在更暗或更亮的场景里控制gammaFactor,但是实际上这样做在物理层面上并不正确,下面会讲到如何更好管理亮度brightness
Textures encoding
我们可以发现设置完渲染器的输出编码outputEncoding
为THREE.sRGBEncoding
后,我们的环境贴图颜色也改变了,虽然看起来效果不错,但我们还是要选择保留其原先正确的颜色。问题就在于我们设置完渲染器的输出编码之后,环境贴图的纹理还是默认的THREE.LinearEncoding
。
其实规则很直接,所有我们能够直接看到的纹理贴图,比如map
,就应该使用THREE.sRGBEncoding
作为编码;而其他的纹理贴图比如法向纹理贴图normalMap
就该使用THREE.LinearEncoding
。
我们可以直接看到环境贴图,所以应该将其编码设为THREE.sRGBEncoding
environmentMap.encoding = THREE.sRGBEncoding
你可能会问那模型上的各种纹理贴图都要一个个亲自去设置吗?大可不必,因为GLTFLoader会将加载的所有纹理自动进行正确的编码。
Tone mapping
色调映射Tone mapping
旨在将超高的动态范围HDR转换到我们日常显示的屏幕上的低动态范围LDR的过程。
说明一下HDR和LDR(摘自知乎LDR和HDR):
- 因为不同的厂家生产的屏幕亮度(物理)实际上是不统一的,那么我们在说LDR时,它是一个0到1范围的值,对应到不同的屏幕上就是匹配当前屏幕的最低亮度(0)和最高亮度(1)
- 自然界中的亮度差异是非常大的。例如,蜡烛的光强度大约为15,而太阳光的强度大约为10w。这中间的差异是非常大的,有着超级高的动态范围。
- 我们日常使用的屏幕,其最高亮度是经过一系列经验积累的,所以使用、用起来不会对眼睛有伤害;但自然界中的,比如我们直视太阳时,实际上是会对眼睛产生伤害的。
那为了改变色调映射tone mapping
,则要更新WebGLRenderer
上的toneMapping
属性,有以下这些值
- THREE.NoToneMapping (默认)
- THREE.LinearToneMapping
- THREE.ReinhardToneMapping
- THREE.CineonToneMapping
- THREE.ACESFilmicToneMapping
尽管我们的贴图不是HDR,但使用tone mapping
可以塑造更真实的效果。
renderer.toneMapping = THREE.ACESFilmicToneMapping
为了更直观看到不同色调映射的区别,可以将其添加到GUI面板中。切换完选项后还需要更新材质,在updateAllMaterials
函数中添加child.material.needsUpdate = true
然后在gui面板中切换选项后调用函数。
gui
.add(renderer, 'toneMapping', {
No: THREE.NoToneMapping,
Linear: THREE.LinearToneMapping,
Reinhard: THREE.ReinhardToneMapping,
Cineon: THREE.CineonToneMapping,
ACESFilmic: THREE.ACESFilmicToneMapping,
})
.onFinishChange(() => {
// 如果我们在外面打印值比如console.log(THREE.ReinhardToneMapping)你会发现输出结果是数字类型的3
// 然而当我们把对象放到dat.GUI面板里面后其会将值给转化为字符串,导致控制台报警告
// 因此我们需要在这里将其重新转化为数字类型
renderer.toneMapping = Number(renderer.toneMapping)
updateAllMaterials()
})
我们还可以更改色调映射曝光度toneMappingExposure
renderer.toneMappingExposure = 3
// 添加到gui面板
gui
.add(renderer, 'toneMappingExposure')
.min(0)
.max(10)
.step(0.001)
下面使用的是色调映射是THREE.ReinhardToneMapping
抗锯齿
在一些像素比为1
的显示屏上看模型时会发现明显的锯齿。
为此我们只需要在实例化渲染器renderer
时开启抗锯齿antialias
属性即可(开启抗锯齿只能在实例化的时候开启)
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
})
使用抗锯齿会损耗性能这点是众所周知的,其实像素比大于1
的屏幕实际上不怎么需要抗锯齿。一个正确的方法是只在像素比低于2
的屏幕上激活它。
阴影
设置渲染器开启阴影贴图,并将类型设为THREE.PCFSoftShadowMap
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
设置平行光投射阴影
directionalLight.castShadow = true
优化阴影贴图,可以参考另一篇笔记three.js学习笔记(五)——Shadows阴影
directionalLight.shadow.camera.far = 15
directionalLight.shadow.mapSize.set(1024, 1024)
然后添加相机助手查看,可以的话就移除掉助手。
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)
然后,激活模型中所有网格的阴影
const updateAllMaterials = () =>
{
scene.traverse((child) =>
{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
{
// ...
child.castShadow = true
child.receiveShadow = true
}
})
}
最后你只需要将场景中的光源调整到接近环境贴图中光源的位置就行。
汉堡包
我们可以导入上次自己做的汉堡包模型。因为它经过Draco压缩,所以需要导入DRACOLoader。具体步骤参照three.js学习笔记(十一)——导入模型
gltfLoader.load('/models/hamburger.glb', gltf => {
gltf.scene.scale.set(0.3, 0.3, 0.3)
gltf.scene.position.set(0, -1, 0)
gltf.scene.rotation.y = Math.PI * 0.5
scene.add(gltf.scene)
updateAllMaterials()
})
可以看到汉堡包表面有些奇怪的条纹,这种情况被称为“阴影失真shadow acne
”
在计算曲面是否处于阴影中时,由于精度原因,阴影失真可能会发生在平滑和平坦表面上。
而现在在汉堡包上发生的是汉堡包在它自己的表面上投射了阴影。因此我们必须调整灯光阴影shadow
的“偏移bias
”和“法线偏移normalBias
”属性来修复此阴影失真。
bias
通常用于平面,因此不适用于我们的汉堡包。但如果你有在一块平坦的表面上出现阴影失真,可以试着增加偏差直到失真消失。normalBias
通常用于圆形表面,因此我们增加法向偏差直到阴影失真消失。
directionalLight.shadow.normalBias = 0.05
更多推荐
所有评论(0)