案例查看地址:点击这里
实现阴影的基本思想是:太阳看不见阴影。如果在光源处放置以为观察者,其视线方向与光线一致,那么观察者也看不到阴影。他看到的每一处都在光的照射下,而那些背后的,他没有看到的物体则处在阴影中。这里,我们需要用到光源与物体之间的距离(实际上也就是物体在光源坐标系下的深度z值)来决定物体是否可见。如图所示,同一条光线上有两个点P1和P2,由于P2的z值大于P1,所以P2在阴影中。
我们需要使用两对着色器以实现阴影:[1]一对着色器用来计算光源到物体的距离,[2]另一对着色器根据[1]中计算出的距离绘制场景。使用一张纹理图像把[1]的结果传入[2]中,这张纹理图像就被称为阴影贴图(shadow map),而通过阴影贴图实现阴影的方法就被称为阴影映射(shadow mapping)。阴影映射的过程包括以下两步:
(1)将视点移动到光源的位置处,并运行[1]中的着色器。这时,那些“将要被绘出”的片元都是被光照射到的,即落在这个像素上的各个片元中最前面的。我们并不实际地绘制出片元的颜色,而是将片元的z值写入到阴影贴图中。
(2)将视点移回原来的位置,运行[2]中的着色器绘制场景。此时,我们计算出每个片元在光源坐标系(即[1]中的视点坐标系)下的坐标,并与阴影贴图中记录的z值比较,如果前者大于后者,就说明当前片元处在阴影之中,用较深暗的颜色绘制。
案例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Title</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
margin: 0;
display: block;
}
</style>
</head>
<body οnlοad="main()">
<canvas id="canvas" height="800" width="800"></canvas>
</body>
<script src="lib/webgl-utils.js"></script>
<script src="lib/webgl-debug.js"></script>
<script src="lib/cuon-utils.js"></script>
<script src="lib/cuon-matrix.js"></script>
<script>
//设置WebGL全屏显示
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//设置阴影贴图顶点着色器
var shadowVertexShaderSource = "" +
"attribute vec4 a_Position;\n" +
"uniform mat4 u_MvpMatrix;\n" +
"void main(){\n" +
" gl_Position = u_MvpMatrix * a_Position;\n" + //计算出在灯源视点下各个坐标的位置
"}\n";
//设置阴影贴图的片元着色器
var shadowFragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"void main(){\n" +
" gl_FragColor = vec4( 0.0, 0.0, 0.0,gl_FragCoord.z);\n" + //将灯源视点下的每个顶点的深度值存入绘制的颜色内
"}\n";
//正常绘制的顶点着色器
var vertexShaderSource = "" +
"attribute vec4 a_Position;\n" +
"attribute vec4 a_Color;\n" +
"uniform mat4 u_MvpMatrix;\n" + //顶点的模型投影矩阵
"uniform mat4 u_MvpMatrixFromLight;\n" + //顶点基于光源的模型投影矩阵
"varying vec4 v_PositionFromLight;\n" + //将基于光源的顶点位置传递给片元着色器
"varying vec4 v_Color;\n" + //将颜色传递给片元着色器
"void main(){\n" +
" gl_Position = u_MvpMatrix * a_Position;\n" + //计算并设置顶点的位置
" v_PositionFromLight = u_MvpMatrixFromLight * a_Position;\n" + //计算基于光源的顶点位置
" v_Color = a_Color;\n" +
"}\n";
//正常绘制的片元着色器
var fragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_ShadowMap;\n" + //纹理的存储变量
"varying vec4 v_PositionFromLight;\n" + //从顶点着色器传过来的基于光源的顶点坐标
"varying vec4 v_Color;\n" + //顶点的颜色
"void main(){\n" +
" vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" +
" vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" +
" float depth = rgbaDepth.a;\n" +
" float visibility = (shadowCoord.z > depth + 0.005) ? 0.5 : 1.0;\n" +
" gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" +
"}\n";
//生成的纹理的分辨率,纹理必须是标准的尺寸 256*256 1024*1024 2048*2048
var resolution = 256;
var offset_width = resolution;
var offset_height = resolution;
//灯光的位置
var light_x = 0.0;
var light_y = 7.0;
var light_z = 2.0;
function main() {
var canvas = document.getElementById("canvas");
var gl = getWebGLContext(canvas);
if(!gl){
console.log("无法获取WebGL的上下文");
return;
}
//初始化阴影着色器,并获得阴影程序对象,相关变量的存储位置
var shadowProgram = createProgram(gl, shadowVertexShaderSource, shadowFragmentShaderSource);
shadowProgram.a_Position = gl.getAttribLocation(shadowProgram, "a_Position");
shadowProgram.u_MvpMatrix = gl.getUniformLocation(shadowProgram, "u_MvpMatrix");
if(shadowProgram.a_Position < 0 || !shadowProgram.u_MvpMatrix ){
console.log("无法获取到阴影着色器的相关变量");
return;
}
//初始化正常绘制着色器,获取到程序对象并获取相关变量的存储位置
var normalProgram = createProgram(gl, vertexShaderSource, fragmentShaderSource);
normalProgram.a_Position = gl.getAttribLocation(normalProgram, "a_Position");
normalProgram.a_Color = gl.getAttribLocation(normalProgram, "a_Color");
normalProgram.u_MvpMatrix = gl.getUniformLocation(normalProgram, "u_MvpMatrix");
normalProgram.u_MvpMatrixFromLight = gl.getUniformLocation(normalProgram, "u_MvpMatrixFromLight");
normalProgram.u_ShadowMap = gl.getUniformLocation(normalProgram, "u_ShadowMap");
if(normalProgram.a_Position < 0 || normalProgram.a_Color < 0 || !normalProgram.u_MvpMatrix || !normalProgram.u_MvpMatrixFromLight || !normalProgram.u_ShadowMap){
console.log("无法获取到正常绘制着色器的相关变量");
return;
}
//设置相关数据,并存入缓冲区内
var triangle = initVertexBuffersForTriangle(gl);
var plane = initVertexBuffersForPlane(gl);
if(!triangle || !plane){
console.log("无法设置相关顶点的信息");
return;
}
//设置帧缓冲区对象
var fbo = initFramebufferObject(gl);
if(!fbo){
console.log("无法设置帧缓冲区对象");
return;
}
//开启0号纹理缓冲区并绑定帧缓冲区对象的纹理
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.texture);
//设置背景设并开启隐藏面消除功能
gl.clearColor(0.0,0.0,0.0,1.0);
gl.enable(gl.DEPTH_TEST);
//声明一个光源的变换矩阵
var viewProjectMatrixFromLight = new Matrix4();
viewProjectMatrixFromLight.setPerspective(70.0, offset_width/offset_height, 1.0, 100.0);
viewProjectMatrixFromLight.lookAt(light_x, light_y, light_z,0.0,0.0,0.0,0.0,1.0,0.0);
//为常规绘图准备视图投影矩阵
var viewProjectMatrix = new Matrix4();
viewProjectMatrix.setPerspective(45.0, canvas.width/canvas.height, 1.0, 100.0);
viewProjectMatrix.lookAt(0.0,7.0,9.0,0.0,0.0,0.0,0.0,1.0,0.0);
var currentAngle = 0.0; //声明当前旋转角度的变量
var mvpMatrixFromLight_t = new Matrix4(); //光源(三角形)的模型投影矩阵
var mvpMatrixFromLight_p = new Matrix4(); //光源(平面)的模型投影矩阵
(function tick() {
currentAngle = animate(currentAngle);
//切换绘制场景为帧缓冲区
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0.0,0.0,offset_height,offset_height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(shadowProgram); //使用阴影程序对象绘制阴影纹理
//绘制三角形和平面(用于生成阴影贴图)
drawTriangle(gl, shadowProgram, triangle, currentAngle, viewProjectMatrixFromLight);
mvpMatrixFromLight_t.set(g_mvpMatrix); //稍后使用
drawPlane(gl, shadowProgram, plane, viewProjectMatrixFromLight);
mvpMatrixFromLight_p.set(g_mvpMatrix); //稍后使用
//解除帧缓冲区的绑定,绘制正常颜色缓冲区
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0.0, 0.0, canvas.width, canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
//切换为正常的程序对象并绘制
gl.useProgram(normalProgram);
gl.uniform1i(normalProgram.u_ShadowMap, 0.0);
//绘制三角形和平面(正常绘制的图形)
gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatrixFromLight_t.elements);
drawTriangle(gl, normalProgram, triangle, currentAngle, viewProjectMatrix);
gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatrixFromLight_p.elements);
drawPlane(gl, normalProgram, plane, viewProjectMatrix);
requestAnimationFrame(tick);
})();
}
//声明坐标转换矩阵
var g_modelMatrix = new Matrix4();
var g_mvpMatrix = new Matrix4();
function drawTriangle(gl,program,triangle,angle,viewProjectMatrix) {
//设置三角形图形的旋转角度,并绘制图形
g_modelMatrix.setRotate(angle, 0.0, 1.0, 0.0);
draw(gl, program, triangle, viewProjectMatrix);
}
function drawPlane(gl, program, plane, viewProjectMatrix) {
//设置平面图形的旋转角度并绘制
g_modelMatrix.setRotate(-45.0, 0.0, 1.0, 1.0);
draw(gl, program, plane, viewProjectMatrix);
}
function draw(gl, program, obj, viewProjectMatrix) {
initAttributeVariable(gl, program.a_Position, obj.vertexBuffer);
//判断程序对象上面是否设置了a_Color值,如果有,就设置颜色缓冲区
if(program.a_Color != undefined){
initAttributeVariable(gl, program.a_Color, obj.colorBuffer);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexBuffer);
//设置模板视图投影矩阵,并赋值给u_MvpMatrix
g_mvpMatrix.set(viewProjectMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
gl.drawElements(gl.TRIANGLES, obj.numIndices, gl.UNSIGNED_BYTE, 0);
}
function initAttributeVariable(gl, a_attribute, buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
var angle_step = 30;
var last = +new Date();
function animate(angle) {
var now = +new Date();
var elapsed = now - last;
last = now;
var newAngle = angle + (angle_step*elapsed)/1000.0;
return newAngle%360;
}
function initFramebufferObject(gl) {
var framebuffer, texture, depthBuffer;
//定义错误函数
function error() {
if(framebuffer) gl.deleteFramebuffer(framebuffer);
if(texture) gl.deleteFramebuffer(texture);
if(depthBuffer) gl.deleteFramebuffer(depthBuffer);
return null;
}
//创建帧缓冲区对象
framebuffer = gl.createFramebuffer();
if(!framebuffer){
console.log("无法创建帧缓冲区对象");
return error();
}
//创建纹理对象并设置其尺寸和参数
texture = gl.createTexture();
if(!texture){
console.log("无法创建纹理对象");
return error();
}
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, offset_width, offset_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
framebuffer.texture = texture;//将纹理对象存入framebuffer
//创建渲染缓冲区对象并设置其尺寸和参数
depthBuffer = gl.createRenderbuffer();
if(!depthBuffer){
console.log("无法创建渲染缓冲区对象");
return error();
}
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, offset_width, offset_height);
//将纹理和渲染缓冲区对象关联到帧缓冲区对象上
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER,depthBuffer);
//检查帧缓冲区对象是否被正确设置
var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if(gl.FRAMEBUFFER_COMPLETE !== e){
console.log("渲染缓冲区设置错误"+e.toString());
return error();
}
//取消当前的focus对象
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return framebuffer;
}
function initVertexBuffersForPlane(gl) {
// 创建一个面
// v1------v0
// | |
// | |
// | |
// v2------v3
// 顶点的坐标
var vertices = new Float32Array([
3.0, -1.7, 2.5, -3.0, -1.7, 2.5, -3.0, -1.7, -2.5, 3.0, -1.7, -2.5 // v0-v1-v2-v3
]);
// 颜色的坐标
var colors = new Float32Array([
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
]);
// 顶点的索引
var indices = new Uint8Array([0, 1, 2, 0, 2, 3]);
//将顶点的信息写入缓冲区对象
var obj = {};
obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
if(!obj.vertexBuffer || !obj.colorBuffer || !obj.indexBuffer) return null;
obj.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return obj;
}
function initVertexBuffersForTriangle(gl) {
// Create a triangle
// v2
// / |
// / |
// / |
// v0----v1
// 顶点的坐标
var vertices = new Float32Array([-0.8, 3.5, 0.0, 0.8, 3.5, 0.0, 0.0, 3.5, 1.8]);
// 颜色的坐标
var colors = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0, 1.0, 0.0, 0.0]);
// 顶点的索引
var indices = new Uint8Array([0, 1, 2]);
//创建一个对象保存数据
var obj = {};
//将顶点信息写入缓冲区对象
obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
if(!obj.vertexBuffer || !obj.colorBuffer || !obj.indexBuffer) return null;
obj.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return obj;
}
function initArrayBufferForLaterUse(gl, data, num, type) {
var buffer = gl.createBuffer();
if(!buffer){
console.log("无法创建缓冲区对象");
return null;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) {
var buffer = gl.createBuffer();
if(!buffer){
console.log("无法创建着色器");
return null;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
</script>
</html>
前两个带shadow前缀的着色器负责生产阴影贴图。我们需要将绘制目标切换到帧缓冲区对象,把视点在光源处的模型视图投影矩阵传给u_MvpMatrix变量,并运行着色器。着色器会将每个片元的z值写入帧缓冲区关联的阴影贴图中。顶点着色器的任务很简单,将顶点坐标乘以模型视图投影矩阵,而片元着色器略复杂一些,它将片元的z值写入了纹理贴图中。为此,我们使用了片元着色器的内置变量gl_FragCoord。
gl_FragCoord的内置变量是vec4类型的,用来表示片元的坐标。gl_FragCoord.x和gl_FragCoord.y是片元在屏幕上的坐标,而gl_FragCoord.z是深度值。它们是通过(gl_Position.xyz/gl_Position.w)/2.0+0.5 计算出来的,都被归一化到[0.0,1.0]区间。如果gl_FragCoord.z是0.0,则表示该片元在近裁剪面上,如果是1.0, 则表示片元在远才见面上。我们将该值写入到阴影贴图的R分量重。当然,你也可以使用其他分量。
" gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);\n" +
这样,着色器就将视点位于光源时每个片元的z值存储在阴影贴图中。阴影贴图将被作为纹理对象传给另一对着色器中的u_shadowMap变量。
正常绘制的顶点着色器和片元着色器实现了第2步。将绘制目标切换回颜色缓冲区,把视点移回原位,开始真正地绘制场景。此时,我们需要比较片元在光源坐标系下的z值和阴影贴图中对应的值来决定当前片元是否处在阴影之中。u_MvpMatrix变量是视点在原处的模型视图投影矩阵,而u_MvpMatrixFromLight变量是第1步中视点位于光源处时的模型视图投影矩阵。顶点着色器计算每个顶点在光源坐标系(即第1步中的视图坐标系)中的坐标v_PositionFromLight(等价于第1不重的gl_Position),并传入片元着色器。
片元着色器的任务是根据片元在光源坐标系中的坐标v_PositionFromLight 计算出可以与阴影贴图相比较的z值。前面说过,阴影贴图中的z值是通过(gl_Position.z/gl_Position.w)/2.0+0.5 计算出来的,为使这里的结果能够与之比较,我们也需要通过(v_PositionFromLight.z / v_PositionFromLight.w) / 2.0 + 0.5 来进行归一化。然后,为了将z值与阴影贴图中的相应纹素值比较,需要通过 v_PositionFromLight 的 x 和 y 坐标从阴影贴图中获取纹素。但我们知道,WebGL中的x和y坐标都是在[-1.0, 1.0]区间中的, 而纹理坐标s和t是在[0.0, 1.0]的区间中的。所以我们还需要将x和y坐标转化为s和t坐标:
s = (v_PositionFromLight.x / v_PositionFromLight.w) / 2.0 + 0.5
t = (v_PositionFromLight.y / v_PositionFromLight.w) / 2.0 + 0.5
其归一化的方式与z值的归一化的方式一致。所以我们在一行代码中完成xyz的归一化(74行),计算出shadowCoord变量,其x和y分量为当前片元在阴影贴图中对应的纹素的纹理坐标,而z分量表示当前片元在光源坐标系中的归一化z值,可与阴影贴图中的纹素值比较。
"void main(){\n" +
" vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" +
" vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" +
" float depth = rgbaDepth.r;\n" +
" float visibility = (shadowCoord.z > depth + 0.005) ? 0.5 : 1.0;\n" +
" gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" +
"}\n";
然后,我们通过shadowCoord.xy从阴影贴图中抽取出纹素(75,76行),你应该还记得,这并非单纯的抽取纹理的纹素,而涉及内插过程。由于之前z值被写在了R分量中(48行),所以这里也只需提取R分量,并保存在depth变量中。接着,我们通过比较shadowCoord.z和depth来决定片元是否是在阴影中,如果前者比较大,说明当前片元在阴影中,就为visibility变量赋值为0.7,否则就赋为1.0.改变量参与了计算片元的最终颜色的过程,如果其为0.7,那么片元就会深暗一些,以表示在阴影中。
个人心得:
上面是原搬的书中的解释,但是对实现的原理的解释过于原理,而且里面掺杂着相关变量,变得更加难懂。博主本来上一节的在帧缓冲区绘制纹理就看得不怎么懂,没想到在这一节还竟然用的这种方法,所以,个人感觉在这里解释一波比较好。
(1)其实WebGL绘制模型就和作画一样,在2D界面上展现3D的效果。与其说是3D模型,不如说有3D效果的2D图。动画,顾名思义,就是动起来的画,切换的速度快了,就感觉不出来是2D平面的效果。
(2)如果能理解(1)所说的东西了,下一步就好理解了。所以,这一节的两个着色器绘制了两张图片而已,fbo是啥,就是能够不直接在画布上绘制,而是直接凭空绘制完成,还放到纹理缓存区。它绘制出来了啥,就是绘制出来了所有能够被光照射到的地方。
(3)绘制第一对着色器保存了绘制的每个点与光源的距离,这个是干嘛用的,如果再绘制别的地方的时候,绘制的点比这个距离大的话,就肯定处于阴影当中了。(教程中是把这个z值赋值给了RGBA中的R变量,但是如果纹理设置的分辨率太低的话,图形的边缘也会出现显示出是贴图的效果。这是因为片元着色器获取纹理时的一个内插的效果,所以我改成了A变量,效果比之前好了很多。)
(4)第二对着色器绘制时干了啥,首先就是计算出了绘制的当前点在第一对着色器中时这个点的xyz的值,就是计算出来这个点距离光源的距离z,然后和放到纹理中的这个z值进行对比。如果距离比纹理中的能证明处于亮面的距离远的话,就能判断出来是处在阴影当中,就让RGBA乘以0.7,比不处于亮面的颜色显得黑,就做出来了阴影。
马赫带:
在77行中,我们对比的时候给depth添加了0.005的偏移量。如果将这个偏移量删除掉的话,再运行程序,就会发现会出现和上图一样的马赫带(Mach band)。
偏移量0.005的作用是消除马赫带。出现马赫带的原因虽然有点复杂,但三维图形学中经常会出现类似的问题,这个问题值得弄明白。我们知道,纹理图像RGBA分量中,每个分量都是8位,那么存储在阴影贴图中的z值精度也只有8位,而与阴影贴图进行比较的值shadowCoord.z是float类型的,有16位。比如说z值是0.1234567,8位的浮点数的精度是1/256,也就是0.00390625。根据:0.1234567/(1/256) = 31.6049152 在8位精度下,0.1234567实际上是31个1/256,即0.12109375 。同理,在16位精度下,0.1234567实际上是8090个1/65536,即0.12344360 。前者比后者小。这意味着,即使是完全相同的坐标,在阴影贴图中的z值可能会比shadowCoord.z中的值小,这就造成了矩形平面的某些区域被误认为是阴影了。我们再进行比较时,为阴影贴图添加了一个偏移量0.005,就可以避免产生马赫带。注意,偏移量应当略大于精度,比如这里的0.005就略大于1/256 。
提高精度:
虽然我们已经成功地实现了场景中的阴影效果,但这仅仅使用与光源距离物体很近的情况。如果我们将光源拿远一些,比如将其y坐标改为40:
86行
//灯光的位置
var light_x = 0.0;
var light_y = 40.0;
var light_z = 2.0;
再次运行,就会发现阴影就会消失掉。
阴影消失的原因是,随着光源与照射物体间的距离变远,gl_FragCoord的值也会增大,当光源足够远时,gl_FragCoord.z就大到无法存储在只有8位的R分量中了。简单的解决方法是,使用阴影贴图中的R、G、B、A这四个分量,用4个字节共32位来存储z值。实际上,已经有列行的方法来完成这项任务了,让我们来看看示例程序修改:
将设置阴影贴图的片元着色器修改为:
var shadowFragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"void main(){\n" +
" const vec4 bitShift = vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0);\n" +
" const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);\n" +
" vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);\n" +
" rgbaDepth -= rgbaDepth.gbaa * bitMask;\n" +
" gl_FragColor = rgbaDepth;\n" + //将灯源视点下的每个顶点的深度值存入绘制的颜色内
"}\n";
var fragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_ShadowMap;\n" + //纹理的存储变量
"varying vec4 v_PositionFromLight;\n" + //从顶点着色器传过来的基于光源的顶点坐标
"varying vec4 v_Color;\n" + //顶点的颜色
//从rgba这4个分量中重新计算出z值的函数
"float unpackDepth(const in vec4 rgbaDepth){\n" +
" const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));\n" +
" float depth = dot(rgbaDepth, bitShift);\n" +
" return depth;\n" +
"}\n" +
"void main(){\n" +
" vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" +
" vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" +
" float depth = unpackDepth(rgbaDepth);\n" + //重新计算出z值
" float visibility = (shadowCoord.z > depth + 0.0015) ? 0.5 : 1.0;\n" +
" gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" +
"}\n";
片元着色器shadowFragmentShaderSource将gl_FragCoord.z拆为了4个字节R、G、B、A 。因为1个字节的精度是1/256,所以我们将大于1/256的部分存储在R分量中,将1/256到1/(256*256)的部分存储在G分量中,将1/(256*256)到1/(256*256*256)存储在B分量中,并将小于1/(256*256*256)的部分存储在A分量中。我们使用内置函数fract()来计算上述分量的值,改函数舍弃参数的整数部分,返回小数部分。此外,由于rgbaDepth是vec4类型的,精度高于8位,还需要将多余的部分砍掉。最后,将rbgaDepth赋值给gl_FragColor,这样就将z值保存在阴影贴图的4个分量中,获得了更高的精度。
片元着色器fragmentShaderSource调用unpackDepth()函数获取z值。该函数是自定义函数,该函数根据如下公式从RGBA分量中还原出高精度的原始z值。此外,由于该公式与点积公式的形式一样,所以我们结果了dot()函数完成了计算。
这样,我们还原了原始的z值,并将它与shadowCoord.z相比较。我们仍然添加了一个偏移量0.0015来消除马赫带,因为此时z值得精度已经提高到了float,在medium精度下,精度为2-10=0.000976563,这样就又能够正确的绘制出阴影了