阴影一直是实时三维渲染的挑战,开发人员必须在合理的情况下找到显示真实阴影的技巧。
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
radius
不会在该类型中生效
// 聚光灯 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()