阴影一直是实时三维渲染的挑战,开发人员必须在合理的情况下找到显示真实阴影的技巧。
Three.js 有一个内置的解决方案,虽然其并不完美,但用起来很方便。
当你进行一次渲染时,Three.js将对每个支持阴影的光线进行渲染,那些渲染会像摄像机那样模拟光线所看到的内容,而在这些灯光渲染下,网格材质将被深度网格材质MeshDepthMaterial所替代。
灯光渲染将像纹理一样被存储起来,称为阴影贴图,之后它们会被用于每个支持接收阴影的材质并投射到几何体上。
1.想要激活并使用阴影,就得先在渲染器renderer
的.shadowMap.enabled
属性中设置开启,允许在场景中使用阴影贴图
renderer.shadowMap.enabled = true
2.检查每个对象,确定它是否可以使用castshadow
投射阴影,以及是否可以使用receiveshadow
接收阴影。
现在我们的场景里有一个球体和一块平面,光源有环境光和平行光。
设置球体可以投射阴影,平面可以接收阴影
sphere.castShadow = true
plane.receiveShadow = true
然后使用castShadow
激活灯光上的阴影
directionalLight.castShadow = true
我们可以在每个灯光的阴影属性中访问阴影贴图
console.log(directionalLight.shadow);
可以看到,默认的贴图尺寸是512x512,我们可以设置其为2的n次幂,因为这涉及到mip映射。之后会发现当数值越高,阴影拥有越清晰的细节,数值越低,阴影越模糊
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024
上面说到Three.js使用灯光摄像机进行阴影贴图渲染。这些相机具有相同的属性,像near
和far
为了方便调试,我们可以往场景中添加摄像机辅助对象(摄像机助手),要做的就是把平行光用于渲染阴影的灯光摄像机directionalLight.shadow.camera
给添加到摄像机助手中
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)
可以看到红线交叉处是我们的平行光光源,正方形矩形部分是near
值,而far
处于非常远的地方,这就是性能需要优化的地方
接下来改变平行光渲染阴影的灯光摄像机可视范围的远近值
directionalLight.shadow.camera.near = 2
directionalLight.shadow.camera.far = 6
通过观察上图,使用相机助手后我们可以发现灯光相机所看到的区域还是太大了,溢出了不少。因为我们正在使用的是平行光,它使用的是正交相机OrthographicCamera。
所以我们可以通过正交相机的top
,right
,bottom
,left
四个属性来控制摄像机视锥体的哪一边可以看多远距离。
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.bottom = -2
可以观察到现在的阴影跟前面没有调整相机前的阴影相比较起来,细节程度有所提高
灯光相机的可视范围越小,阴影越精确,当然如果设置得实在太小,阴影将会被裁剪掉
// 当相机far值过小,阴影被裁剪掉
directionalLight.shadow.camera.far = 3.8
我们可以通过radius
属性控制阴影模糊程度,它不会改变灯光相机与物体的距离。
directionalLight.shadow.radius = 10
有不同类型的算法可以应用于阴影贴图
THREE.BasicShadowMap-性能非常好但是质量很差
THREE.PCFShadowMap-性能较差但边缘更平滑(默认)
THREE.PCFSoftShadowMap-性能较差但边缘更柔和
THREE.VSMShadowMap-性能差,约束多,但能够产生意想不到的效果。
// PCF柔软阴影贴图
renderer.shadowMap.type = THREE.PCFSoftShadowMap
// 聚光灯
const spotLight = new THREE.SpotLight(0xffffff,0.4,10,Math.PI*0.3)
spotLight.castShadow = true
spotLight.position.set(0,2,2)
scene.add(spotLight)
// 如果要使聚光灯看向某处记得把target添加场景中
scene.add(spotLight.target)
添加相机助手
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)
和前面优化平行光阴影贴图一样。
spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6
但因为它是聚光灯,使用的是透视相机PerspectiveCamera,所以可以通过fov
属性改变摄像机视锥体垂直视野角度
spotLight.shadow.camera.fov = 30
//点光源
const pointLight = new THREE.PointLight(0xffffff,0.3)
pointLight.castShadow = true
pointLight.position.set(-1,1,0)
scene.add(pointLight)
添加相机助手
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
scene.add(pointLightCameraHelper)
pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024
pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5
点光源摄像机使用的也是透视相机,但最好不要去改变它的视锥体垂直视野角度fov
属性
烘培阴影是Three.js阴影的一个很好的替代品。我们可以将阴影集成到纹理中,并将其应用到材质上。
先关闭渲染器的阴影贴图渲染,之后就看不到场景中的阴影了
renderer.shadowMap.enabled = false
// Textures
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
平面使用基础网格材质(MeshBasicMaterial
)并应用烘培阴影纹理贴图
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(5, 5),
new THREE.MeshBasicMaterial({map:bakedShadow})
)
这种方案适用于静态物体,因为当物体位置有所变化后,阴影并不会跟着移动。
我们还可以使用更简单的烘焙阴影贴图并移动它,使其一直保持在球体下方。
// 加载简单阴影
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
我们要创建一个略高于地板的平面,把它的材质的alphaMap
属性设置为简单阴影纹理贴图,
const sphereShadow = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1.5,1.5),
new THREE.MeshBasicMaterial({
color:0x000000,
transparent:true,
alphaMap:simpleShadow
})
)
sphereShadow.rotation.x = - Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphereShadow)
接着我们为球体添加动画,使其绕着地板平面做圆周运动,并且有在地板触底弹跳的效果
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
//update sphere animate
// 圆周运动
sphere.position.x = Math.sin(elapsedTime)
sphere.position.z = Math.cos(elapsedTime)
// 触底弹跳
sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
接着再设置阴影跟随球体进行位置变换
//更新球体阴影贴图位置
//阴影贴图跟随球体
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
//阴影根据球体高度变化,贴图的透明度也有所改变
//球体距离平面越高,阴影越透明
sphereShadow.material.opacity = (1 - Math.abs(sphere.position.y)) * 0.3
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'dat.gui'
/**
* Base
*/
// Debug
const gui = new dat.GUI()
// Canvas
const canvas = document.querySelector('canvas.webgl')
// Scene
const scene = new THREE.Scene()
// Textures
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
/**
* Lights
*/
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3)
gui
.add(ambientLight, 'intensity')
.min(0)
.max(1)
.step(0.001)
scene.add(ambientLight)
// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3)
directionalLight.position.set(2, 2, -1)
gui
.add(directionalLight, 'intensity')
.min(0)
.max(1)
.step(0.001)
gui
.add(directionalLight.position, 'x')
.min(-5)
.max(5)
.step(0.001)
gui
.add(directionalLight.position, 'y')
.min(-5)
.max(5)
.step(0.001)
gui
.add(directionalLight.position, 'z')
.min(-5)
.max(5)
.step(0.001)
scene.add(directionalLight)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024
directionalLight.shadow.camera.near = 2
directionalLight.shadow.camera.far = 6
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.bottom = -2
directionalLight.shadow.radius = 10
// console.log(directionalLight.shadow);
const directionalLightCameraHelper = new THREE.CameraHelper(
directionalLight.shadow.camera
)
directionalLightCameraHelper.visible = false
scene.add(directionalLightCameraHelper)
// 聚光灯
const spotLight = new THREE.SpotLight(0xffffff, 0.3, 10, Math.PI * 0.3)
spotLight.castShadow = true
spotLight.position.set(0, 2, 2)
scene.add(spotLight)
scene.add(spotLight.target)
spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024
spotLight.shadow.camera.fov = 30
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
spotLightCameraHelper.visible = false
scene.add(spotLightCameraHelper)
//点光源
const pointLight = new THREE.PointLight(0xffffff, 0.3)
pointLight.castShadow = true
pointLight.position.set(-1, 1, 0)
pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024
pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5
scene.add(pointLight)
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
pointLightCameraHelper.visible = false
scene.add(pointLightCameraHelper)
/**
* Materials
*/
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.7
gui
.add(material, 'metalness')
.min(0)
.max(1)
.step(0.001)
gui
.add(material, 'roughness')
.min(0)
.max(1)
.step(0.001)
/**
* Objects
*/
const sphere = new THREE.Mesh(
new THREE.SphereBufferGeometry(0.5, 32, 32),
material
)
sphere.castShadow = true
const plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(5, 5), material)
plane.rotation.x = -Math.PI * 0.5
plane.position.y = -0.5
plane.receiveShadow = true
scene.add(sphere, plane)
const sphereShadow = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
alphaMap: simpleShadow,
})
)
sphereShadow.rotation.x = -Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphereShadow)
/**
* Sizes
*/
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))
})
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
)
camera.position.x = 1
camera.position.y = 1
camera.position.z = 2
scene.add(camera)
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.shadowMap.enabled = false
renderer.shadowMap.type = THREE.PCFSoftShadowMap
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
//更新球体位置
//设置圆周运动轨迹
sphere.position.x = Math.sin(elapsedTime) * 1.5
sphere.position.z = Math.cos(elapsedTime) * 1.5
//设置触底弹跳效果
sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))
//更新球低阴影贴图位置
//阴影贴图跟随球体
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
//阴影根据球体高度变化,贴图的透明度也有所改变
//球体距离平面越高,阴影越透明
sphereShadow.material.opacity = (1 - Math.abs(sphere.position.y)) * 0.3
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()