阴影一直是实时三维渲染的挑战,开发人员必须在合理的情况下找到显示真实阴影的技巧。
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 controlscontrols.update()// Renderrenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)
}tick()

接着再设置阴影跟随球体进行位置变换

  //更新球体阴影贴图位置//阴影贴图跟随球体sphereShadow.position.x = sphere.position.xsphereShadow.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 = 6const 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 = 5scene.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 = trueconst plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(5, 5), material)
plane.rotation.x = -Math.PI * 0.5
plane.position.y = -0.5plane.receiveShadow = truescene.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 sizessizes.width = window.innerWidthsizes.height = window.innerHeight// Update cameracamera.aspect = sizes.width / sizes.heightcamera.updateProjectionMatrix()// Update rendererrenderer.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.5sphere.position.z = Math.cos(elapsedTime) * 1.5//设置触底弹跳效果sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))//更新球低阴影贴图位置//阴影贴图跟随球体sphereShadow.position.x = sphere.position.xsphereShadow.position.z = sphere.position.z//阴影根据球体高度变化,贴图的透明度也有所改变//球体距离平面越高,阴影越透明sphereShadow.material.opacity = (1 - Math.abs(sphere.position.y)) * 0.3// Update controlscontrols.update()// Renderrenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)
}tick()

three.js学习笔记(五)——Shadows阴影相关推荐

  1. Vue.js 学习笔记 五 常用的事件修饰符

    介绍几个常用的事件修饰符 直接上代码 <div id="divApp"><div class="divColor" v-on:click=&q ...

  2. 【Node.js学习笔记五】npm(Node包管理器)命令行选项

    Node包管理器(Node Packaged Manager,NPM) Node封装模块(Node Packaged Module,module)   选项 说明 示例  1  search  在存储 ...

  3. Vue.js 学习笔记 六 v-model 双向绑定数据

    之前说的v-bind指令,可以绑定数据,但是是单向的,从model向view绑定,下面介绍v-model,可以双向绑定数据 <div id="divApp"><p ...

  4. Vue.js 学习笔记 四 用一,二,三的知识做个跑马灯

    做个简单的跑马灯效果 页面定义2个按钮,绑定2个方法. <div id="divApp"><input type="button" value ...

  5. JS学习笔记(五)函数类型、箭头函数、arguments参数、标签函数

    JS学习笔记(五) 本系列更多文章,可以查看专栏 JS学习笔记 文章目录 JS学习笔记(五) 一.函数 1. 函数定义 2. 方法( 对象 + 函数 ) 二.函数参数及返回值 1. 传递原始类型参数 ...

  6. WebGL three.js学习笔记 6种类型的纹理介绍及应用

    WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...

  7. 好程序员教程分析Vue学习笔记五

    好程序员教程分析Vue学习笔记五,上次我们学习了Vue的组件,这次我们来学习一下路由的使用.在Vue中,所谓的路由其实跟其他的框架中的路由的概念差不多,即指跳转的路径. 注意:在Vue中,要使用路由, ...

  8. 【AngularJs学习笔记五】AngularJS从构建项目开始

    为什么80%的码农都做不了架构师?>>>    #0 系列目录# AngularJs学习笔记 [AngularJs学习笔记一]Bower解决js的依赖管理 [AngularJs学习笔 ...

  9. JS学习笔记六:js中的DOM操作

    1. JS学习笔记六:js中的DOM操作 文章目录 1. JS学习笔记六:js中的DOM操作 1.1. 获取Dom节点 1.2. 元素属性的操作方式 1.3. DOM节点的创建.插入和删除 1.4. ...

最新文章

  1. C语言中将0到1000的浮点数用强制指针类型转换的方式生成一幅图像
  2. Paxos、Raft不是一致性算法/协议?
  3. MySQL 使用explain查看执行计划
  4. 15.赋值运算符为什么返回类的引用?不是引用怎么办?
  5. Spring Security OAuth2——自定义OAuth2第三方登录(Gitee)
  6. IntelliJ IDEA 修改单行注释的格式
  7. Programming Ruby学习笔记一
  8. PHP技术亮点,我眼里的THINKPHP5新亮点(1)
  9. c语言中定义宏的利与弊
  10. mac下安装wxPython2.8.12.1方法
  11. 用ISO TOOL破解游戏 图文
  12. Python解析JSON对象
  13. 计算机无法安装网卡驱动,网卡驱动安装不了,详细教您解决网卡驱动安装不了...
  14. 消费金融公司可开展哪些业务类型?
  15. 07 给Form视图添加Chatter(学Odoo,就得Do)
  16. 03虚幻4 场景中的基础光源和视觉效果
  17. Java入土---面向对象(OOP)
  18. 解决警告:Unable to preventDefault inside passive event listener due to target being treated as passive.
  19. Python Web异常处理
  20. SecureCRT标签永久显示IP地址

热门文章

  1. 浅谈移动端Vin码识别技术
  2. 张大哥笔记-如何利用网络赚钱(20种网络赚钱方法推荐)
  3. 看一个师兄的操作系统视频有感
  4. Java创建对象方式初谈
  5. 自动控制原理7.7---离散系统的数字校正
  6. 【历史上的今天】12 月 4 日:JavaScript 问世;开源运动的领导者诞生;人类第一次用计算机点外卖
  7. 计算机硬件基础与实践探知
  8. tar命令打包压缩时排除.svn .git .repo等特定文件/文件夹报错/未生效
  9. 安装好office套件以后,右键新建中没有Word、Excel、PPT等怎么办
  10. 维图PDMS切图软件