【Three.js】第十六章 Shadows 阴影

16. Shadows 阴影

介绍

上节课我们学会了灯光,现在我们需要阴影。物体的背面应该在黑暗中,这就是阴影所谓的核心。我们缺少的是物体对象的投影,也就是根据被投影的对象在其他对象身上创建阴影。
阴影渲染一直是实时 3D 渲染的一大挑战,开发人员必须找到技巧以合理的帧速率显示逼真的阴影。
实现它们的方法有很多种,Three.js 有一个内置的解决方案。请注意,此解决方案很方便,但并非完美。

怎么运行的

我们不会详细说明阴影渲染在底层是如何工作的,但我们可以尝试了解基础知识。
当您进行一次渲染时,Three.js 将首先为每个灯光照射下应该投射阴影的地方进行一次渲染。渲染器将模拟光线下阴影所看到的样子,就好像它是一台相机一样。在这些灯光渲染期间,MeshDepthMaterial将替换所有网格材质。
最后将结果存储为纹理和包含命名过的阴影贴图。
您看不到那些存储好的阴影贴图,它们用于投射到几何体上然后替换其原本的材质。
这是定向光和聚光灯看到的一个很好的例子: https: //threejs.org/examples/webgl_shadowmap_viewer.html

设置

我们的启动器由一个平面上的一个简单球体组成,该球体具有一个定向光和一个环境光。
您可以在 Dat.GUI 中控制这些灯光以及材质的金属度和粗糙度。

如何激活阴影

首先,我们需要激活阴影贴图渲染器renderer

renderer.shadowMap.enabled = true

然后,我们需要遍历场景中的每个对象,并确定该对象是否可以使用该属性castShadow投射阴影,以及该对象是否可以使用该属性receiveShadow接收阴影。
尝试在尽可能少的对象上激活它们:

sphere.castShadow = true

// ...
plane.receiveShadow = true

最后,使用castShadow属性激活光的阴影。
只有以下类型的灯光支持阴影:

再次尝试在尽可能少的灯光上激活阴影:

directionalLight.castShadow = true


你应该在平面上得到球体的阴影。
可悲的是,这个渲染的影子看起来很可怕。让我们尝试改进它。

阴影贴图优化

渲染尺寸

正如我们在课程开始时所说,Three.js 可以为每盏灯进行称为阴影贴图的渲染。您可以使用灯光上的shadow属性访问此阴影贴图(以及许多其他内容):

console.log(directionalLight.shadow)

image.png
至于我们的渲染,我们需要给阴影贴图指定一个大小。默认情况下,出于性能原因阴影贴图大小仅512x512像素。我们可以改进它,但请记住, mipmapping限制,您需要 2 的幂值:

directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024


阴影应该已经看起来比原先的更真实了。

扫描二维码关注公众号,回复: 15802806 查看本文章

近与远

Three.js 使用相机进行阴影贴图渲染。这些相机与我们已经使用的相机具有相同的属性。这意味着我们必须定义 nearfar它不会真正提高阴影的质量,但它可能会修复您看不到阴影或阴影突然被裁剪的错误
为了帮助我们调试相机并预览nearfar,我们可以将CameraHelper与位于directionalLight.shadow.camera属性中的阴影贴图相机一起使用:

const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)


现在您可以直观地看到相机的nearfar尝试找到适合场景的值:

directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6

振幅

通过我们刚刚添加的相机助手,我们可以看到相机的振幅太大了。
因为我们使用的是DirectionalLight,Three.js 使用的是OrthographicCamera。如果您还记得相机课程,我们可以使用toprightbottomleft属性控制相机在每一侧可以看到的距离。让我们减少这些属性:

directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = - 2
directionalLight.shadow.camera.left = - 2


值越小,阴影越精确。但如果它太小,阴影就会被裁剪掉。
完成后,您可以隐藏相机助手:

directionalLightCameraHelper.visible = false

模糊

您可以使用以下属性控制阴影模糊radius

directionalLight.shadow.radius = 10


此技术不使用相机与对象的接近度。这只是一个普通而廉价的模糊。

阴影贴图算法

可以将不同类型的算法应用于阴影贴图:

  • THREE.BasicShadowMap:性能非常好但质量很差
  • THREE.PCFShadowMap:性能较差但边缘更平滑
  • THREE.PCFSoftShadowMap:性能较差但边缘更柔和
  • THREE.VSMShadowMap:性能较差,约束较多,可能会产生意想不到的结果

要更改它,请更新renderer.shadowMap.type属性。默认是THREE.PCFShadowMap,但您可以使用THREE.PCFSoftShadowMap以获得更好的质量。

renderer.shadowMap.type = THREE.PCFSoftShadowMap


请注意,radius 属性不适用于**THREE.PCFSoftShadowMap**. 你必须做出选择。

聚光灯

让我们尝试像在Lights课程中所做的那样添加一个SpotLight,并将castShadow属性添加为true ,不要忘记将target属性添加到scene
我们还将添加一个相机助手:

// Spot light
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)
scene.add(spotLight.target)

const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)

如果场景太亮,您可以降低其他灯光强度:

const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)

// ...

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)


如您所见,阴影不能很好地融合。它们是独立处理的,不幸的是,现在没有什么可做的优化。
但是我们可以使用和定向光源相同技术 :控制尺寸的来提高阴影质量。
改变shadow.mapSize

spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024


因为我们现在使用的是SpotLight点光源,Three.js 在内部使用的是PerspectiveCamera透视相机。这意味着我们必须要更改fov属性,而不是toprightbottomleft属性。我们尝试在不裁剪阴影的情况下找到尽可能小的角度(相机距离越小,阴影质量越高):

spotLight.shadow.camera.fov = 30


更改nearfar值:

spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6


完成后,您可以隐藏相机助手:

spotLightCameraHelper.visible = false

点光源

让我们试试最后一个支持阴影的光源,即PointLight

// Point light
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)

如果场景太亮,您可以降低其他灯光强度:

const ambientLight = new THREE.AmbientLight(0xffffff, 0.3)

// ...

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3)

// ...

const spotLight = new THREE.SpotLight(0xffffff, 0.3, 10, Math.PI * 0.3)


如您所见,相机助手是一个PerspectiveCamera (就像SpotLight一样)但这个相机的面是朝下的。这是根据 Three.js 如何处理PointLight的阴影贴图决定的。
因为点光源照亮各个方向,Three.js 必须渲染 6 个方向创建立方体阴影贴图。您看到的相机助手是相机在这 6 个渲染中的最后一个位置(向下)。
进行所有这些渲染会产生性能问题。尽量避免在启用阴影的情况下使用过多的PointLight
您可以在此处调整的唯一属性是mapSize,nearfar

pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024

pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5


完成后,您可以隐藏相机助手:

pointLightCameraHelper.visible = false

烘焙阴影 Baking

如果场景简单,three.js 阴影可能非常有用,反之阴影可能会变得凌乱。
烘焙阴影是一个很好的选择方案。我们在上节课讲过烘焙灯,其实是一回事。阴影贴图被集成到我们应用于材料的纹理中。
无需注释所有与渲染阴影相关的代码行,我们可以简单地在渲染器和每盏灯上停用它们即可:

directionalLight.castShadow = false
// ...
spotLight.castShadow = false
// ...
pointLight.castShadow = false
// ...
renderer.shadowMap.enabled = false


现在我们可以使用经典的TextureLoader加载位于/static/textures/bakedShadow.jpg其中的阴影纹理。

在创建对象和灯光之前添加以下代码:

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')

最后,我们将使用带有的简单MeshBasicMaterial map:bakedShadow,而不是在平面上使用MeshStandardMaterial

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    new THREE.MeshBasicMaterial({
    
    
        map: bakedShadow
    })
)


您应该会看到一个漂亮的模糊且逼真的假阴影。主要问题是它不是动态的,如果球体或灯光移动,阴影也不会。

烘焙阴影替代品

一个不太现实但更动态的解决方案是在球体下方和平面上方使用更简单的阴影

纹理是一个简单的光晕。白色部分将可见,黑色部分将不可见。
然后,我们用球体移动那个影子。
首先,让我们通过将MeshStandardMaterial放回平面上来移除之前的烘焙阴影:

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)

然后,我们可以加载位于 /static/textures/simpleShadow.jpg 中的基本阴影纹理。

const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')

我们可以使用一个简单的平面来创建阴影,该平面旋转并放置在地板上方一点。材质必须是黑色,但带有阴影纹理作为alphaMap. 不要忘记更改transparenttrue,并将网格添加到scene

const sphereShadow = new THREE.Mesh(
    new THREE.PlaneGeometry(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(sphere, sphereShadow, plane)


一个不那么逼真但非常高效的阴影。
如果要为球体设置动画,只需相应地为阴影设置动画并根据球体的高度更改其不透明度:

const clock = new THREE.Clock()

const tick = () =>
{
    
    
    const elapsedTime = clock.getElapsedTime()

    // Update the sphere
    sphere.position.x = Math.cos(elapsedTime) * 1.5
    sphere.position.z = Math.sin(elapsedTime) * 1.5
    sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))

    // Update the shadow
    sphereShadow.position.x = sphere.position.x
    sphereShadow.position.z = sphere.position.z
    sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3

    // ...
}

tick()

tutieshi_640x299_4s.gif

使用哪种技术

找到处理阴影的正确解决方案取决于您。这取决于项目、表现和您掌握的技术。您也可以将它们组合起来。

猜你喜欢

转载自blog.csdn.net/m0_68324632/article/details/131152031