three.js学习笔记(五)——Shadows阴影
阴影一直是实时三维渲染的挑战,开发人员必须在合理的情况下找到显示真实阴影的技巧。
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
虽然阴影没有什么变化,但至少减少了些性能消耗
振幅amplitude
通过观察上图,使用相机助手后我们可以发现灯光相机所看到的区域还是太大了,溢出了不少。因为我们正在使用的是平行光,它使用的是正交相机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柔软阴影贴图
// 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阴影相关推荐
- Vue.js 学习笔记 五 常用的事件修饰符
介绍几个常用的事件修饰符 直接上代码 <div id="divApp"><div class="divColor" v-on:click=&q ...
- 【Node.js学习笔记五】npm(Node包管理器)命令行选项
Node包管理器(Node Packaged Manager,NPM) Node封装模块(Node Packaged Module,module) 选项 说明 示例 1 search 在存储 ...
- Vue.js 学习笔记 六 v-model 双向绑定数据
之前说的v-bind指令,可以绑定数据,但是是单向的,从model向view绑定,下面介绍v-model,可以双向绑定数据 <div id="divApp"><p ...
- Vue.js 学习笔记 四 用一,二,三的知识做个跑马灯
做个简单的跑马灯效果 页面定义2个按钮,绑定2个方法. <div id="divApp"><input type="button" value ...
- JS学习笔记(五)函数类型、箭头函数、arguments参数、标签函数
JS学习笔记(五) 本系列更多文章,可以查看专栏 JS学习笔记 文章目录 JS学习笔记(五) 一.函数 1. 函数定义 2. 方法( 对象 + 函数 ) 二.函数参数及返回值 1. 传递原始类型参数 ...
- WebGL three.js学习笔记 6种类型的纹理介绍及应用
WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...
- 好程序员教程分析Vue学习笔记五
好程序员教程分析Vue学习笔记五,上次我们学习了Vue的组件,这次我们来学习一下路由的使用.在Vue中,所谓的路由其实跟其他的框架中的路由的概念差不多,即指跳转的路径. 注意:在Vue中,要使用路由, ...
- 【AngularJs学习笔记五】AngularJS从构建项目开始
为什么80%的码农都做不了架构师?>>> #0 系列目录# AngularJs学习笔记 [AngularJs学习笔记一]Bower解决js的依赖管理 [AngularJs学习笔 ...
- JS学习笔记六:js中的DOM操作
1. JS学习笔记六:js中的DOM操作 文章目录 1. JS学习笔记六:js中的DOM操作 1.1. 获取Dom节点 1.2. 元素属性的操作方式 1.3. DOM节点的创建.插入和删除 1.4. ...
最新文章
- C语言中将0到1000的浮点数用强制指针类型转换的方式生成一幅图像
- Paxos、Raft不是一致性算法/协议?
- MySQL 使用explain查看执行计划
- 15.赋值运算符为什么返回类的引用?不是引用怎么办?
- Spring Security OAuth2——自定义OAuth2第三方登录(Gitee)
- IntelliJ IDEA 修改单行注释的格式
- Programming Ruby学习笔记一
- PHP技术亮点,我眼里的THINKPHP5新亮点(1)
- c语言中定义宏的利与弊
- mac下安装wxPython2.8.12.1方法
- 用ISO TOOL破解游戏 图文
- Python解析JSON对象
- 计算机无法安装网卡驱动,网卡驱动安装不了,详细教您解决网卡驱动安装不了...
- 消费金融公司可开展哪些业务类型?
- 07 给Form视图添加Chatter(学Odoo,就得Do)
- 03虚幻4 场景中的基础光源和视觉效果
- Java入土---面向对象(OOP)
- 解决警告:Unable to preventDefault inside passive event listener due to target being treated as passive.
- Python Web异常处理
- SecureCRT标签永久显示IP地址