5.2 Threejs阴影系统
了解光影系统
threejs是实时光影
实时光影是指:光影会随着灯光和物体的改变而改变,且变化速率非常高,参与渲染过程,
web端目前没有优质的实时光影
实时光影一般在B端不会有太好的效果,只有在C端环境下,且需要更强力的硬件支持,才能达到更好的实时光影渲染效果,像实时光追系统,就是C端专属的功能,且对显卡消耗也是巨大,也许未来某一天,随着WebGPU发展的更多,对显卡的使用更加完美后,实时光影或许也会登陆B端,这里我们可以暂且期待一下
实时光影会大幅增加渲染压力
物体越多,光影渲染增加的渲染压力就越高,对大多数情况下,渲染压力会增加一倍,如果你的设备,本身能承担的面数,大概是1000万,那么你用了实时光影后,大概这个极限会缩减到500万甚至更低,所以谨慎使用实时光影
元素越多,点线面越多,都会对最终实时光影的渲染计算量有影响
没有独显的电脑不建议添加实时光影
没有独显的电脑,本身渲染压力已经非常大了,再添加光影,cpu只会压力更大
笔记本一定要看清楚,你的浏览器是否使用独显渲染,别让CPU干渲染的活
阴影配置
什么样的灯光可以产生阴影
在目前threejs的系统下,一共有三个灯光可以产生阴影,分别是
PointLight 点光源
DirectionalLight 平行光
SpotLight 聚光灯
什么样的物体可以产生阴影和接受阴影
不是所有的物体都可以产生阴影和接受阴影
灯光只能产生阴影,不能接收阴影
可以产生并接受阴影的物体有:Mesh,Line等
不能直接产生阴影且不能接收阴影的有:Object3D,Group,
完全不产生阴影且不能接收阴影的有:Sprite(精灵),Points(粒子系统)
注意开启阴影渲染
阴影渲染是有开关的,在renderer下
当允许渲染阴影的时候,Threejs才会开启阴影渲染系统,否则你怎么让物体产生接收阴影,你都看不到任何的光影效果
//开启阴影渲染
renderer.shadowMap.enabled = true;
阴影可以做一些设置,比如说,允许自动更新光影,以及定义阴影类型,一般我们要使得实时光影效果最好,建议使用
THREE.PCFSoftShadowMap,如果你觉得性能太差,使用PCFShadowMap或者更低的BasicShadowMap
关于VSM阴影,这里挖个坑,后续有机会详细讲解,如果你在用PCFSoft的时候,调整不出来正确的光影效果,可以尝试使用VSM阴影来解决
灵活运用阴影
上面提到了,阴影系统可以设置产生阴影和接收阴影,所以你也可以指定某个物体仅接收阴影,但是不产生阴影,也可以设置某个物体只产生阴影不接收阴影,来实现某些效果
也可以通过设置渲染器是否自动更新阴影,来让阴影的渲染变成单帧渲染
平行光阴影
这里官方描述的并不是很清楚,我们写一段代码来研究这个阴影
首先,我们要先让阴影系统生效
import * as THREE from "../three/build/three.module.js";
import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";
window.addEventListener('load',e=>{
init();
addLight();//添加灯光
addLand();//添加地面
addMesh();//添加物体
render();
})
let scene,renderer,camera;
let orbit;
let mesh;
function init(){
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({
alpha:true,
antialias:true
});
renderer.setSize(window.innerWidth,window.innerHeight);
//渲染器开启阴影,并使用PCF算法过滤阴影,且使用软阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
camera.position.set(10,10,10);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
}
function addLight() {
//添加平行光
let directionalLight = new THREE.DirectionalLight(0xffffff,1.0);
//设置平行光产生阴影
directionalLight.castShadow = true;
//设置平行光位置
directionalLight.position.set(0,20,20);
//平行光辅助线
let directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight,1,0xff0000);
//平行光光影辅助线
let cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
//将物体添加到场景中
scene.add(directionalLight);
scene.add(directionalLightHelper);
scene.add(cameraHelper);
}
function addMesh() {
let geometry = new THREE.TorusKnotGeometry(5,1,64,8);
let material = new THREE.MeshBasicMaterial({color:0xffffff * Math.random()});
mesh = new THREE.Mesh(geometry,material);
//给物体添加生成阴影和接收阴影的设置
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.y = 3;
scene.add(mesh);
}
function addLand() {
//添加一个地面
let geometry = new THREE.PlaneGeometry(100,100).rotateX(-Math.PI/2);
let material = new THREE.MeshStandardMaterial({
color:0xffffff
});
let mesh = new THREE.Mesh(geometry,material);
//设定地面仅接收阴影 因为我们使用的是个地面,所以没必要再让它生成阴影浪费算力,除非你的地下有东西
mesh.receiveShadow = true;
scene.add(mesh);
}
function render() {
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
}
从效果中,我们可以清晰的看到,阴影是有范围的,这个范围就是cameraHelper的范围
这是因为,阴影的计算,是借助了相机的算法来计算的,平行光内部使用了正交相机OrthographicCamera来计算阴影
我们可以打印平行光,来看到阴影的基本数据,也可以通过添加CameraHelper来查看实际计算的阴影范围
现在我们上面的效果,遇到了明显的问题,就是阴影不完整,这个时候,我们可以通过调整相机的实际范围来增加阴影的生成区域
控制正交相机的6个要素,分别为 left,right,top,bottom,near, far
正交相机在之前的相机篇已经做了介绍,不懂的可以回顾一下【ThreeJS基础教程-初识Threejs】1.5 选择合适的相机与相机切换
首先我们得知道,默认的值是多少
从侧面拉远了之后,我们看到,物体并没有超出near和far的范畴,所以near和far此时不需要更改
从正面看,明显比物体小一圈,所以我们此时把left,right,top,bottom都翻一倍即可
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
这时,我们的阴影就正常了
点光源阴影
我们将上述代码,替换 addLight() 的部分
function addLight() {
let pointLight = new THREE.PointLight(0xffffff,1.0,40,0.1);
pointLight.position.set(0,20,20);
pointLight.castShadow = true;
console.log(pointLight);
let pointLightHelper = new THREE.PointLightHelper(pointLight,1,0xff0000);
let cameraHelper = new THREE.CameraHelper(pointLight.shadow.camera);
scene.add(pointLight);
scene.add(pointLightHelper);
scene.add(cameraHelper);
}
官方对于点光源阴影的介绍实在太过简单。。。
点光源实际使用透视相机来计算阴影,但是我们实际上通过透视相机辅助线并不能看到透视相机的变化,这个原因就由你们自行研究了
可以说的是,点光源的阴影也是有范围的,且范围跟随distance属性的变化而变化,我们在代码中,把distance设置到了40,并没有完全覆盖物体后面的光影区域,所以不仅照不亮那一块,连阴影也不会产生
我们修改了distance后,达到了比较好的效果
可以看得出,点光源的阴影,是会随着距离点光源的距离,而越来越大,与现实中的灯泡是完全一致的
聚光灯阴影
//和上面一样,我们依然修改addLight()即可
function addLight() {
//聚光灯的属性这里不再赘述
let spotLight = new THREE.SpotLight(0xffffff,10,50,0.5,0.2,0.2);
spotLight.castShadow = true;
spotLight.position.set(0,20,20);
let spotLightHelper = new THREE.SpotLightHelper(spotLight,0xff0000);
scene.add(spotLight);
scene.add(spotLightHelper);
}
聚光灯内部也是使用透视相机进行阴影计算的,和点光源的越远阴影越大一样
如果想对聚光灯调节照射范围,只需要修改distance,angle,等属性即可,无需像上面平行光一样需要手动修改相机
优化阴影
以下方案对上述所有光源均有效
阴影范围外一片漆黑,可以优化一下吗
我们以刚写完的聚光灯案例入手,其实我们只需要加一个亮度不高的环境光,效果就会好很多
这样,即使物体不处在聚光灯光源之下,也能看清楚物体
scene.add(new THREE.AmbientLight(0xffffff,0.2));
一般情况下,场景中是需要一个全局光照的,这个全局光照,可以是一个纯环境光,也可以是一个半球光,根据自己的需求来设置即可,这样开启阴影后,就不会产生某个地方特别漆黑的情况,也能有比较贴近现实的感觉,环境光亮度根据自己的实际需求来调节即可
增加阴影的精度
阴影的本质,其实就是计算一块阴影,然后覆盖到物体的原有的贴图上
所以阴影也是有纹理的性质的,比如说分辨率
如果你的阴影效果比较差,可以使用下面的方式来提高阴影精度
//建议两个参数值相等,且数值为2的幂次倍
// 如: 256 * 256,512 * 512,1024 * 1024,2048 * 2048
light.shadow.mapSize.set(512,512);
由于在demo中,修改此值没有太大区别,所以这里不做演示了
注意:提高了一倍的阴影精度,计算量大约会增加4倍,最高仅建议到4096
消除伪影
摩尔纹效果来源百度,如有侵权请联系笔者
有时候阴影会出现类似上图的摩尔纹效果,可以用调整bias来解决
笔者刚才在尝试的过程中,没有一次能复现摩尔纹效果,所以这里仅找一张百度的图片来代替演示效果,实际上
light.shadow.bias -= 0.0001;
静态阴影渲染 / 渐进式阴影渲染
Threejs官方案例shadowmap_progressive
这里的光影,我们可以从效果中看到,阴影不是第一时间渲染完成的,而是在物体或灯光移动后的几秒钟后完成的,这种阴影渲染叫:静态光影渲染,或渐进式阴影渲染
这种渲染的好处是,我们不需要实时的去更新阴影,只需要在改变物体的一瞬间重新渲染光影即可
这种阴影渲染可以用在不经常移动的物体上
具体的实现方式,请自行查看threejs官方案例的源代码
烘培阴影
烘培阴影已经在上一篇做了简单介绍,这里就不再赘述了
【Threejs基础教程-光影篇】5.1 常用的灯光
使用阴影时需要注意的点
- 阴影计算非常消耗性能,要根据实际需求去决定如何使用阴影渲染
- 你的模型已经很大的情况下,不推荐使用实时阴影
- 减少产生阴影的灯光,产生阴影的光源越多,也会呈指数级的额外消耗性能