要实现的效果

阴影效果

系列教程

  1. 【WebGL】从零开始的Web端渲染器(1)——框架搭建
  2. 本篇
  3. 【WebGL】从零开始的Web端渲染器(3)——法线贴图
  4. 【WebGL】从零开始的Web端渲染器(4)——贴图及阴影抗锯齿(还在写)

思路设计

原理

思路比较简单,就是逐个灯光绘制阴影深度图,然后再逐个灯光渲染的时候对比对应像素世界位置距离光源的距离,和深度图的深度。如果距离更远就表示他与光源之间被其他东西挡住了。

这个shadow map技术,网上一群教程,这里就不在赘述了。(其实最好还要模糊一下,抗锯齿,但是我还不会,有生之年再做)

设计

这里的设计是参考我之间的文章

  1. 每个灯光有自己的Shadow Map,在场景加载过程中初始化。
  2. 在渲染循环中,先让每个灯光绘制自己的shadow map,再正常渲染。
  3. 因为要绘制shadow map,所以材质要加一个shader用于投射阴影。
  4. 在着色器中,获取阴影贴图并且解码获得当前像素是否在阴影中。

实现

灯光Shadow Map

下方是初始化shadow map的代码,由一个负责颜色缓冲区的贴图和负责深度缓冲区的renderbuffer组成。这里在一开始分配给该shadow map了一个Texture Unit的编号,并做了绑定,方便后面使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
initShadowMap(lightIndex, startTexUnit) {
this.lightIndex = lightIndex;
this.shadowMapTexUnit = gl.TEXTURE0 + startTexUnit + lightIndex;
let texture, depthbuffer;

// 错误函数
var error = () => {
if (texture) gl.deleteTexture(texture);
if (depthbuffer) gl.deleteRenderbuffer(depthbuffer);
if (framebuffer) gl.deleteFramebuffer(framebuffer);
console.error('灯光Shadow Map加载失败');
}

// 创建贴图作为rgb通道
texture = gl.createTexture();
if (!texture) error();

gl.activeTexture(this.shadowMapTexUnit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.shadowMapRes, this.shadowMapRes, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

// 创建renderbuffer 作为深度通道
depthbuffer = gl.createRenderbuffer();
if (!depthbuffer) error();

gl.bindRenderbuffer(gl.RENDERBUFFER, depthbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.shadowMapRes, this.shadowMapRes);

// 创建framebuffer
this.shadowMap = gl.createFramebuffer();
if (!this.shadowMap) error();

// 绑定贴图和renderbuffer到framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowMap);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthbuffer);

// 检查framebuffer是否完整
let e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (e !== gl.FRAMEBUFFER_COMPLETE) {
console.log('Framebuffer不完整');
error();
}

gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}

Shadow Caster着色器

这个很简单,就是添加一个着色器,然后加载的时候加载两个就好了。

ShadowCaster代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
attribute vec3 a_Normal;

uniform mat4 u_Matrix_M_I;
uniform mat4 u_Matrix_MVP;
uniform vec4 u_LightPos;
uniform vec4 u_LightColor;

// Main函数在这里
void main()
{
gl_Position = u_Matrix_MVP * a_Position;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#version 100

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#endif

uniform mat4 u_Matrix_M_I;
uniform mat4 u_Matrix_MVP;
uniform vec4 u_LightPos;
uniform vec4 u_LightColor;

// 基本不用改
void main()
{
const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
const vec4 bitMask = vec4(1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0, 0.0);
vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
rgbaDepth -= rgbaDepth.gbaa * bitMask;
gl_FragColor = rgbaDepth;
}

这里为了提高阴影贴图精度,将深度信息以256划分之后存在了rgba四个通道里。

所以使用的时候要再解码。这里举个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float unpackDepth(const in vec4 rgbaDepth) {
const vec4 bitShift = vec4(1.0, 1.0 / 256.0, 1.0 / (256.0 * 256.0), 1.0 / (256.0 * 256.0 * 256.0));
float depth = dot(rgbaDepth, bitShift);
return depth;
}

// 获取阴影函数
float getShadow() {
vec3 shadowCoord = (v_PositionFromLight.xyz / v_PositionFromLight.w) / 2.0 + 0.5;
vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);
float depth = unpackDepth(rgbaDepth);
float shadow = (shadowCoord.z > depth + 0.0001) ? 0.0 : 1.0;
return shadow;
}

直接调用GetShadow函数就可以获得一个非0既1的数了,表示在不在当前光源的阴影里。

绘制shadow map

先逐光源绑定绘制目标为灯光shadow map,绘制shadow map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
render(clearColor)
{
// 绘制灯光ShadowMap
this.lightList.forEach((light, lightIndex, arr) =>
{
gl.bindFramebuffer(gl.FRAMEBUFFER, light.shadowMap);
gl.viewport(0, 0, light.shadowMapRes, light.shadowMapRes);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

this.meshList.forEach((mesh, meshIndex, arr) =>
{
this.drawShadowMap(mesh, light);
})
})

// 绘制到屏幕
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, width, height);
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

this.lightList.forEach((light, lightIndex, arr) =>
{
this.meshList.forEach((mesh, meshIndex, arr) =>
{
this.drawMesh(mesh, light, lightIndex);
})
})
}

在绘制函数中,直接传入灯光的mvp矩阵绘制就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
drawShadowMap(mesh, light)
{
gl.useProgram(mesh.material.getShadowCasterProgram());

if (mesh.material.bDepthTest) gl.enable(gl.DEPTH_TEST);
else gl.disable(gl.DIPTH_TEST);

// 绑定Vertex Buffer
if (mesh.material.shadowCaster.a_Position >= 0) this.bindAttributeToBuffer(mesh.material.shadowCaster.a_Position, mesh.model.vertexBuffer);
if (mesh.material.shadowCaster.a_TexCoord >= 0) this.bindAttributeToBuffer(mesh.material.shadowCaster.a_TexCoord, mesh.model.texCoordBuffer);
if (mesh.material.shadowCaster.a_Normal >= 0) this.bindAttributeToBuffer(mesh.material.shadowCaster.a_Normal, mesh.model.normalBuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.model.indexBuffer);

let mvpMatrix = new Matrix4().set(light.vpMatrix).multiply(mesh.mMatrix);
gl.uniformMatrix4fv(mesh.material.shadowCaster.u_Matrix_MVP, false, mvpMatrix.elements);
gl.uniformMatrix4fv(mesh.material.shadowCaster.u_Matrix_M_I, false, mesh.mIMatrix.elements);

// 绘制
gl.drawElements(gl.TRIANGLES, mesh.model.indexNum, mesh.model.indexBuffer.dataType, 0);
}

使用Shadow Map

把该灯光的mvp矩阵再次传进去,同时传入shadow map的编号,以在片段着色器中使用shadow map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
drawMesh(mesh, light) {
// Base Shader渲染
if (light.lightIndex === 0) {
gl.useProgram(mesh.material.getBaseProgram());
if (mesh.material.bDepthTest) gl.enable(gl.DEPTH_TEST);
else gl.disable(gl.DIPTH_TEST);
// 绑定Vertex Buffer
···
// 传入默认变量
···
// 阴影相关变量
let mvpMatrixLight = new Matrix4().set(light.vpMatrix).multiply(mesh.mMatrix);
if (mesh.material.baseShader.u_Matrix_Light) gl.uniformMatrix4fv(mesh.material.baseShader.u_Matrix_Light, false, mvpMatrixLight.elements);
if (mesh.material.baseShader.u_ShadowMap) gl.uniform1i(mesh.material.baseShader.u_ShadowMap, light.shadowMapTexUnit - gl.TEXTURE0);

// 绘制
gl.drawElements(gl.TRIANGLES, mesh.model.indexNum, mesh.model.indexBuffer.dataType, 0);
}
else { }
}

然后直接调用GetShadow函数就可以获得阴影了。

结束

上一篇教程:【WebGL】从零开始的Web端渲染器(1)——框架搭建

下一个教程:【WebGL】从零开始的Web端渲染器(3)——法线贴图