WebGL编程指南学习(10)
This is just the end of the beginning. —— Winston Curchill
实现阴影有若干种不同的方法,书里的例子是阴影贴图(Shadow map)或称深度贴图(Depth map)
// 第一组着色器:生成阴影贴图
// 顶点缓冲区
var SHADOW_VSHADER_SOURCE =
//...
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
'}\n';
// z值缓冲区
var SHADOW_FSHADER_SOURCE =
//...
'void main() {\n}' +
' gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);\n' +
'}\n';
// 第二组着色器:正常绘制
var VSHADER_SOURCE =
//...
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' v_PositionFromLight = u_MvpMatrixFromLight * a_Position;\n' +
//...
var FSHADER_SOURCE =
//...
'uniform sampler2D u_ShadowMap;\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.r;\n' +
' float visibility = (shadowCoord.z > depth + 0.005) ? 0.7 : 1.0;\n' +
' gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n' +
'}\n';
u_MvpMatrix
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);
u_MvpMatrix
是视点在远处的MVP矩阵,而视点位于光源处的MVP矩阵是u_MvpMatrixFromLight
gl_Position.z/gl_Position.w/2.0+0.5;
这个计算方法就是默认的gl_FragCoord.z。只不过这里gl_FragCoord不是要求的,所以得显示写出这个计算过程
这里恰好,xy坐标的归一化方式和纹理坐标归一化一致,所以一行代码算出的就是纹理上的坐标
var OFFSCREEN_WIDTH = 1024, OFFSCREEN_HEIGHT = 1024; // 渲染阴影贴图的纹理大小
var LIGHT_X = 0, LIGHT_Y = 7, LIGHT_Z = 2; // 光源位置
function main() {
//...
// 第一组着色器
var shadowProgram = createProgram(gl, SHADOW_VSHADER_SOURCE, SHADOW_FSHADER_SOURCE);
// 第二组着色器
var normalProgram = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE);
//...
// 设置顶点信息
var triangle = initVertexBuffersForTriangle(gl);
var plane = initVertexBufferForPlane(gl);
//...
// 初始化帧缓冲区(FBO)
var fbo = initFrameBufferObject(gl);
//...
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.texture) // 把纹理对象挂到FBO的texture上
//...
// 计算视点到光源的MVP,为渲染阴影贴图准备
var viewProjMatrixFromLight = new Matrix4();
viewProjMatrixFromLight.setPerspective(70.0, OFF_SCREEN_WIDTH/OFF_SCREEN_HEIGHT, 1.0, 100.0);
viewProjMatrixFromLight.lookAt(LIGHT_X, LIGHT_Y, LIGHT_Z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// 设置正常视点的MVP,为正常渲染准备
var viewProjMatrix = new Matrix4();
viewProjMatrix.setPerspective(45, canvas.width/canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(0.0, 7.0, 9.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// Bonus: 额外任务,让三角形旋转起来!
var currentAngle = 0.0; // 当前旋转角度
var mvpMatrixFromLight_t = new Matrix4(); // 三角形的MVP
var mvpMatrixFromLight_p = new Matirx4(); // 平面的MVP
var tick = function(){
currentAngle = animate(currentAngle);
// 将渲染对象切换为FBO
gl.bindFrameBuffer(gl.FRAMEBUFFER, fbo);
//...
gl.useProgram(shadowProgram);
// 渲染以生成纹理贴图
drawTriangle(gl, shadowProgram, triangle, currentAngle, viewProjMatrixFromLight);
mvpMatrixFromLight_t.set(g_mvpMatrix);
drawPlane(gl, shadowProgram, plane, viewProjMatrixFromLight);
mvpMatrixFromLight_p.set(g_mvpMatrix);
// 正常渲染
gl.bindFrameBuffer(gl.FRAMEBUFFER, null);
gl.useProgram(normalProgram);
gl.uniform1i(normalProgram.u_ShadowMap, 0); // 传递gl.TEXTURE0
gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatirxFromLighjt_t.elements);
drawTriangle(gl, normalProgram, triangle, currentAngle, viewProjMatrix);
gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatirxFromLighjt_p.elements);
drawTriangle(gl, normalProgram, plane, currentAngle, viewProjMatrix);
window.requestAnimationFrame(tick, canvas);
};
tick();
}
如果光源与照射物体变远,gl_FragCoord.z的值也会增大。当光源足够远时,gl_FragCoord.z大到无法存储在只有8位的R分量中了。
咋办呢?
使用阴影贴图的RGBA4个分量,共32位来存储z值。
把gl_FragCoord.z拆为4个字节RGBA,每个字节的精度是1/256,所以把大于1/256的部分存在R中,1/256到1/(256*256)的部分存在G中,以此类推。使用内置函数fract()计算上述分量的值。fract()的作用是舍弃整数部分,返回小数部分.
const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
因为rgbaDepth是vec4类型的精度高于8位,还需要将多余的部分砍掉
const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
rgbaDepth -= rgbaDepth.gbaa * bitMask;
例如:若要保存的深度值为:0.111,经过运算以后是(0.111, 0.416, 0.496, 0.976)
bitMask计算结果是(0.001625, 0.0019375, 0.0038125, 0)
再经过修改变成(0.109375, 0.4140625, 0.4921875, 0.976)
unpack的结果是0.111
由此可见,这里深度范围不能超过1。
d e p t h = r g b a D e p t h . r ∗ 1.0 + r g b a D e p t h . g 256.0 + r g b a D e p t h . b 256.0 ∗ 256.0 + r g b a D e p t h . a 256.0 ∗ 256.0 ∗ 256.0 depth = rgbaDepth.r * 1.0 + \frac{rgbaDepth.g}{256.0} + \frac{rgbaDepth.b}{256.0*256.0}+\frac{rgbaDepth.a}{256.0*256.0*256.0} depth=rgbaDepth.r∗1.0+256.0rgbaDepth.g+256.0∗256.0rgbaDepth.b+256.0∗256.0∗256.0rgbaDepth.a
这个可以通过内置函数dot()实现
const vec4 bitShit = vec4(1.0, 1.0/256.0, 1.0/256.0/256.0, 1.0/256.0/256.0/256.0);
depth = dot(rgbaDepth, bitShift);
读取三维模型文件,只需要搞清楚文件格式即可。
# Blender v2.60 (sub 0) OBJ File: "
# www.blender.org
mtllib cube.mtl
o Cube
v 1.000000 -1.000000 -1.000000
...
usemtl Material
f 1 2 3 4
...
usemtl Material.001
f 1 5 6 2
# Blender MTL File:"
# Material Count: 2
newmtl Material
Ka 0.000000 0.000000 0.000000
Kd 1.000000 0.000000 0.000000
Ks 0.000000 0.000000 0.000000
Ns 96.078431
Ni 1.000000
d 1.000000
illum 0
newmtl Material.001
...
在理解模型文件的基础上,就可以使用JavaScript文件进行读取和解析了。
**算法步骤 **
function main() {
...
// 为顶点坐标、颜色和法线准备空缓冲区对象
var model = initVertexBuffers(gl, program);
...
// 读取OBJ文件
readOBJFile('../resources/cube.obj', gl, model, 60, true);
...
}
// 创建缓冲区对象并进行初始化
function initVertexBuffers(gl, program) {
var o = new Object();
o.vertexBuffer = createEmptyArrayBuffer(gl, program.a_Position, 3, gl.FLOAT);
o.normalBuffer = createEmptyArrayBuffer(gl, program.a_Normal, 3, gl.FLOAT);
o.colorBuffer = createEmptyArrayBuffer(gl, program.a_Color, 4, gl.FLOAT);
o.indexBuffer = gl.createBuffer();
...
return o;
}
// 创建缓冲区对象,并将其分配给相应的attribute变量,并开启之
function createEmptyArrayBuffer(gl, a_attribute, num, type)
{
var buffer = gl.createBuffer();
...
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute); // 开启attribute变量
return buffer;
}
// 读取OBJ文件
function readOBJFile(fileName, gl, model, scale, reverse)
{
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status !== 404) {
onReadOBJFile(request.responseText, fileName, gl, model, scale, reverse);
}
}
request.open('GET', fileName, true); // 创建请求
request.send(); // 发起请求
}
事件响应函数
模型读取函数
// 读取
function onReadOBJFile(fileString, fileName, gl, o, scale ,reverse)
{
var objDoc = new OBJDoc(fileName); // 创建OBJDoc对象
var result = objDoc.parse(fileString, scale, reverse); // 调用解析方法
...
g_objDoc = objDoc;
}
// 定义OBJDoc类
// 构造函数
var OBJDoc = function(fileName){
this.fileName = fileName;
this.mtls = new Array(0); // 材质MTL列表
this.objects = new Array(0); // 对象object列表
this.vertices = new Array(0); // 顶点Vertex列表
this.normals = new Array(0); // 法线Normal列表
}
// OBJDoc类的parse方法,用来解析obj文件中的字符串文本
OBJDoc.prototype.parse = function(fileString, scale, reverseNormal)
{
var lines = fileString.split('\n'); // 首先把字符串拆成一行一行
lines.push(null); // 添加行末标识
var index = 0; // 这是行索引
var currentObject = null;
var currentMaterialName = "";
// 逐行解析
var line; // 接收当前行文本
var sp = new StringParser(); // 这是StringParser,自定义字符串解析类的一个实例
while ((line = lines[index++]) != null){
sp.init(line); // 初始化sp
var command = sp.getWord(); // 获取指令名,每一行的第一个单词
if (command == null) continue;
switch (command){
case '#':
continue; // 这是注释行,直接跳过
case 'mtllib': // 这行是要读取某个材质文件,文件名是下一个单词
var path = this.parseMtllib(sp, this.fileName);
var mtl = new MTLDoc(); // 创建新的MTL类实例
this.mtls.push(mtl); // 把这个mtl实例推入mtl数组
// 类似obj文件的方式,使用XMLHttprequest读取mtl文件
//...
continue;
case 'o':
case 'g': // 读取对象名称(模型叫啥)
var object = this.parseObjectName(sp);
this.objects.push(object); // 把这个模型送入数组
currentObject = object; // 当前处理的是currentObject模型的数据
continue;
case 'v':
var vertex = this.parseVertex(sp, scale); // 解析顶点,这里scale是缩放因子
this.vertices.push(vertex); // 顶点推入顶点数组里
continue;
case 'vn':
var normal = this.parseNormal(sp); // 解析法向
this.normals.push(normal);
continue;
case 'usemtl': // 这行要求读取某材质,后面跟的是材质名
currentMaterialName = this.parseUsemtl(sp);
continue;
case 'f': // 读取表面
var face = this.parseFace(sp, currentMaterialName, this.vertices, reverse);
currentObject.addFace(face); // 给当前处理的object添加face数据
continue;
}
}
return true;
}
自定义的StringParse类支持的方法
方法 | 描述 |
---|---|
StringParser.init(str) | 初始化StringParser对象 |
StringParser.getWord() | 获取一个单词 |
StringParser.skipToNextWord() | 跳至下一个单词 |
StringParser.getInt() | 获取单词并将其转化为整型 |
StringParser.getFloat() | 获取单词并将其转化为浮点数 |
不同类型数据的解析方法——以顶点为例
OBJDoc.prototype.parseVertex = function(sp, scale){
var x = sp.getFloat() * scale;
...
return (new Vertex(x,y,z));
}
把存储在数组的值转成绘制用的数组(Float32Array等格式)
for (var i = 0; i < this.objects.length; i++){
var object = this.objects[i];
for (var j = 0; j < object.faces.length; j++){
var face = obejct.face[j];
var color = this.findColor(face.materialName);
var faceNormal = face.normal;
for (var k = 0; k < face.vIndices.length; k++){
// 设置索引
indices[index_indices] = index_indices;
// 复制顶点
var vIdx = face.vIndices[k];
var vertex = this.vertices[vIdx]; // 从总的那个vertex数组中检索
vertices[index_indices * 3 + 0] = vertex.x;
vertices[index_indices * 3 + 1] = vertex.y;
vertices[index_indices * 3 + 2] = vertex.z;
// 复制颜色
colors[index_indices * 4 + 0] = color.r;
colors[index_indices * 4 + 1] = color.g;
colors[index_indices * 4 + 2] = color.b;
colors[index_indices * 4 + 3] = color.a;
// 复制法向
var nIdx = face.nIndices[k];
if (nIdx >= 0){
// 说明原始文件中有法向信息
var normal = this.normals[nIdx];
normals[index_indices * 3 + 0] = normal.x;
normals[index_indices * 3 + 1] = normal.y;
normals[index_indices * 3 + 2] = normal.z;
}
else{
// 直接复制表面的法向
normals[index_indices * 3 + 0] = faceNormal.x;
normals[index_indices * 3 + 0] = faceNormal.x;
normals[index_indices * 3 + 0] = faceNormal.x;
}
index_indices ++;
}
}
}
计算机从休眠中唤醒,或者后台切换,有可能会导致WebGL程序停止,这种现象就是上下文丢失。
webglcontextlost // 上下文丢失事件
webglcontextresotred // 上下文恢复事件
WebGL提供了两个事件,上下文丢失事件和上下文恢复事件。
给canvas注册上下文丢失和上下文恢复事件的响应函数
canvas.addEventListener(type, handler, useCapture)
// 把handler作为type事件的响应函数注册到canvas元素上去
// 参数:
// type:监听事件的名称、字符串
// handler: 响应函数
// useCapture: 事件触发后是否捕获。true的话就捕获事件,canvas的父元素就不会触发该事件;false的话,事件触发后还要向上层继续传递
function main(){
var canvas = document.getElementById('webgl');
// 注册事件响应函数
canvas.addEventListener('webglcontextlost', contextLost, false);
canvas.addEventListener('webglcontextrestored', function(ev){start(canvas);}, false);
start(canvas);
}
调用start方法渲染上下文
function start(canvas)
{
var gl = getWebGLContext(canvas);
...
}
通过把原本main里的绝大多数操作转移到start函数里,当上下文丢失又恢复的时候,再次调用该函数就可以了。
需要注意的是:
上下文丢失响应函数
function contextLost(ev){
cancelAnimationFrame(g_requestID); // 停止动画
ev.preventDefault(); // 阻止默认行为
}
上下文丢失响应函数就保证在上下文恢复前,不再尝试重绘、阻止浏览器对该事件的默认处理行为。浏览器对上下文丢失事件的默认处理行为是,不再触发上下文恢复事件。而我们这里要触发上下文恢复事件,所以要组织浏览器的默认行为。
上下文恢复响应函数
调用start即可重置WebGL系统。这里用匿名函数。