阴影一直是实时三维渲染的挑战,开发人员必须在合理的情况下找到显示真实阴影的技巧。
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使用灯光摄像机进行阴影贴图渲染。这些相机具有相同的属性,像nearfar
为了方便调试,我们可以往场景中添加摄像机辅助对象(摄像机助手),要做的就是把平行光用于渲染阴影的灯光摄像机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

虽然阴影没有什么变化,但至少减少了些性能消耗
在这里插入图片描述

振幅amplitude

通过观察上图,使用相机助手后我们可以发现灯光相机所看到的区域还是太大了,溢出了不少。因为我们正在使用的是平行光,它使用的是正交相机OrthographicCamera
所以我们可以通过正交相机的toprightbottomleft四个属性来控制摄像机视锥体的哪一边可以看多远距离。

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柔软阴影贴图

// 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()

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐