简而言之,步骤如下:
1.从灯光位置视点(阴影相机)创建深度图。
2.从相机的位置角度进行屏幕渲染,在每个像素点,比较由
阴影相机的MVP矩阵
计算的深度值和深度图的值的大小,如果深度图值小的话,则表示该像素点有阴影,就在此处渲染阴影状态。
演示效果
1.创建深度图
基本上,从头开始做阴影贴图时,只需要准备三样东西:灯光位置、阴影相机和深度图。为了更方便理解,本文用ShadowMapViewer
来将深度图进行可视化。
方向光(Directional Light)
首先创造一个光源。
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set(-30, 40, 10);
scene.add(light);
DirectionalLight
有一个shadow
参数,因此附加一个从灯光位置观察的阴影相机和一个从阴影相机角度写入深度值的fbo(frame buffer object,帧缓存对象)。
阴影相机(Shadow Camera)
由于光线是定向的,因此使用OrthographicCamera(正视相机)
作为阴影相机来创建平行投影的深度图。
最重要的是必须设置好相机范围(视锥体),如果阴影相机范围太宽,深度图会不准确,因此最好将其设置在尽可能渲染阴影的最小范围,不要太宽或太窄。
const frustumSize = 80;
light.shadow.camera = new THREE.OrthographicCamera(
-frustumSize / 2,
frustumSize / 2,
frustumSize / 2,
-frustumSize / 2,
1,
80
);
// 和灯光位置保持一致
light.shadow.camera.position.copy(light.position);
light.shadow.camera.lookAt(scene.position);
scene.add(light.shadow.camera);
深度图(Depth Map)
接下来,为阴影相机视点准备深度图。
深度图如果分辨率设置太低图像会很粗糙,所以这次我们将准备一个 2048 x 2048 的fbo。
一般为了尽可能以高精度写入深度值,通常使用16位或32位纹理,但由于WebGL尚不兼容尚不支持浮动纹理的设备,所以我们用8位纹理的所有四个通道来存储单个32位值(在本例中为深度值),我们将使用three.js的ShaderChunk,方便转换。
light.shadow.mapSize.x = 2048;
light.shadow.mapSize.y = 2048;
const pars = {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat
};
light.shadow.map = new THREE.WebGLRenderTarget( light.shadow.mapSize.x, this.light.shadow.mapSize.y, pars );
用于渲染深度图的材质
const shadowMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: shadowFragmentShader
});
顶点shader基本是一样的。
void main(){
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
因为我们要写入的深度图是8bit纹理,但是要输入的数据是32bit。在这里可以用three.js的shaderChunk中使用packDepthToRGBA
来存储使用rgba通道的深度值。
// https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl.js#L18
#include <packing>
void main(){
// gl_FragCoord.z contains depth values from 0 to 1 in the viewing frustum range of the shadow camera.
// 0 for near clip, 1 for far clip
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
}
写入深度值
将shadowMaterial
赋到mesh上,并渲染为深度图。
由于需要为阴影相机视点创建深度图,因此将深度图指定为“renderTarget”,将shadowCamera指定为“camera”。
// 更新每一帧
mesh.material = shadowMaterial;
renderer.setRenderTarget(light.shadow.map);
renderer.render(scene, light.shadow.camera);
这样的话我们就渲染了深度图,然后用我刚才说的ShadowMapViewer
来查看深度图的调试效果。
// https://threejs.org/examples/?q=shadow#webgl_shadowmap_viewer
const depthViewer = new ShadowMapViewer(light);
depthViewer.size.set( 300, 300 );
...
// render to canvas
renderer.setRenderTarget(null);
depthViewer.render( renderer );
越靠近阴影相机的地方,深度值越小(因为ShadowMapViewer
对结果取反了,所以越是白的地方,深度值越小)。
2.比较深度并创建阴影
屏幕渲染材质
将光照位置和深度图放入uniform变量中,阴影相机投影矩阵和视图矩阵也放入uniform变量中,因为在阴影相机的 MVP矩阵中计算的深度也必须在这个着色器中计算并与深度图进行比较。
const uniforms = {
uColor: {
value: new THREE.Color(color)
},
uLightPos: {
value: light.position
},
uDepthMap: {
value: light.shadow.map.texture
},
uShadowCameraP: {
value: light.shadow.camera.projectionMatrix
},
uShadowCameraV: {
value: light.shadow.camera.matrixWorldInverse
},
}
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
});
在顶点着色器中添加一些代码。
uniform mat4 uShadowCameraP;
uniform mat4 uShadowCameraV;
varying vec4 vShadowCoord;
varying vec3 vNormal;
void main(){
vNormal = normal;
vec3 pos = position;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);
// 阴影相机视点的坐标传递给片段着色器并与深度图进行比较
vShadowCoord = uShadowCameraP * uShadowCameraV * modelMatrix * vec4(pos, 1.0);
}
vShadowCoord
的结果是裁剪空间中的坐标,因此vShadowCoord.xyz / vShadowCoord.w
的范围从 (-1, -1, -1)到(1,1,1)。
vShadowCoord.z / vShadowCoord.w
是深度值,所以让它在0和1之间转换并与深度图进行比较。并让vShadowCoord.xy / vShadowCoord.w
在(0,0)和(1,1)之间转换为uv来参考深度图。之所以使用MVP矩阵计算得到的结果作为uv,是因为我们可以参考与生成深度图的像素相同点的深度值。
由于深度图值是较早通过在rgba中分配32位数据输入的,因此在引用时需要将其恢复为原始值。
此解码使用来自three.js中相同ShaderChunk的unpackRGBAToDepth
。
uniform vec3 uColor;
uniform sampler2D uDepthMap;
uniform vec3 uLightPos;
varying vec3 vNormal;
varying vec4 vShadowCoord;
// https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl.js#L24
#include <packing>
void main(){
vec3 shadowCoord = vShadowCoord.xyz / vShadowCoord.w * 0.5 + 0.5;
float depth_shadowCoord = shadowCoord.z;
vec2 depthMapUv = shadowCoord.xy;
float depth_depthMap = unpackRGBAToDepth(texture2D(uDepthMap, depthMapUv));
// Compare and if the depth value is smaller than the value in the depth map, then there is an occluder and the shadow is drawn.
float shadowFactor = step(depth_shadowCoord, depth_depthMap);
// check the result of the shadow factor.
gl_fragColor = vec4(vec3(shadowFactor), 1.0);
}
在循环函数中,将屏幕渲染过程放在深度图渲染之后。
// 在循环函数中写入深度图
mesh.material = shaderMaterial;
renderer.setRenderTarget(light.shadow.map);
renderer.render(scene, light.shadow.camera);
// 放置一个用于屏幕渲染的材质并将其渲染到画布上。
mesh.material = material;
renderer.setRenderTarget(null);
renderer.render(scene, camera);
调整深度值比较
当显示shadowFactor(比较深度值的结果)
时,会生成阴影,但会显示出一些奇怪类似摩尔纹的图案,这种现象被称为shadow acne
,必须通过减去这一点的bias
来比较深度值。
void main(){
...
float cosTheta = dot(normalize(uLightPos), vNormal);
float bias = 0.005 * tan(acos(cosTheta)); // cosTheta is dot( n,l ), clamped between 0 and 1
bias = clamp(bias, 0.0, 0.01);
float shadowFactor = step(depth_shadowCoord - bias, depth_depthMap);
gl_fragColor = vec4(vec3(shadowFactor), 1.0);
}
乘以定向光,然后完成着色。