目标
承接上篇,本章将完成从模型数据的原始坐标至屏幕坐标的转换。
实践部分
裁剪坐标系
模型的原始数据经过模型变换、视图变换以及投影将转换为裁剪坐标系下的坐标,这里之所以叫裁剪坐标系是因为下面要进行齐次裁剪这个操作,就是把摄像机观察范围以外的部分删除掉,不要多想,在裁剪坐标系内的点坐标,此时还是齐次坐标系即。
关于坐标变换的相关内容我推荐这一篇文章:WebGL简易教程(五):图形变换(模型、视图、投影变换)讲解的很清除,基本上跟着推导一遍就明白了。
当然也可以用glMatrix
库中mat4
的方法便捷的生成模型变换矩阵、视图变换矩阵以及投影变换矩阵。
lookAt(out, eye, center, up)
方法生成视图变换矩阵,需要4个参数out
为输出矩阵,eye
为摄影机所在的世界坐标,center
为摄影机所观察的位置,up
为摄像机局部坐标系朝上的方向向量。perspective(out, fovy, aspect, near, far)
方法生成透视投影变换矩阵,需要4个参数fovy
垂直视角的弧度大小,aspect
为近平面的宽高比,near
为近平面距离摄影机坐标系原点的距离,far
为远平面距离摄影机坐标系原点的距离,我这里用Π/4, widht.height, 0.1, 100
看起来比较舒服,你可以改变一些参数值看看变成啥样。
let m = glMatrix.mat4.create() //模型变换矩阵,这里默认单位矩阵好了
glMatrix.mat4.fromYRotation(m, Math.PI/4) //这里我要求点坐标围绕Y轴逆时针转45°
let v = glMatrix.mat4.create() //创建视图矩阵
let eye = glMatrix.vec3.fromValues(1, 1, 4)
let center = glMatrix.vec3.fromValues(0, 0, 0)
let up = glMatrix.vec3.fromValues(0, 1, 0)
glMatrix.mat4.lookAt(v, eye, center, up)
let p = glMatrix.mat4.create() //透视投影矩阵
let fovy = Math.PI / 4
let aspect = width / height
let near = 0.1
let far = 100
glMatrix.mat4.perspective(p, fovy, aspect, near, far)
有了m,v,p
变换矩阵就可以构造我们的顶点Shader了,因为在可编程渲染管线里顶点shader常用来做顶点位置的变换
function verticeShader(pt, m, v, p) { //pt为一个顶点的xyz坐标
pt = glMatrix.vec4.fromValues(pt[0], pt[1], pt[2], 1); //转为齐次坐标
let mv = glMatrix.mat4.create(); //世界坐标和摄影机坐标转换
glMatrix.mat4.mul(mv, v, m);
let mvp = glMatrix.mat4.create(); //再加上投影变换
glMatrix.mat4.mul(mvp, p, mv);
let clip_position = glMatrix.vec4.create();
glMatrix.vec4.transformMat4(clip_position, pt, mvp);
return clip_position;
}
把顶点渲染出来,我们就可以得到如下图所示的结果(drawPixel
的时候加了视口变换):
齐次裁剪
齐次裁剪我推荐看这一篇文章《一篇文章彻底弄懂齐次裁剪》,讲的挺好的,但其实我实现的时候就用的简单的齐次裁剪方法,也就是要满足和这两个条件。其实很好理解,在透视投影矩阵推导的过程中,我们是将视锥体内的点压缩到一个这样的标准化立方体里,也就是说由齐次坐标转化为笛卡尔坐标系后的点坐标要满足该约束,也就可以推导出第一个条件。第二个条件是这样的推导的。还记得透视投影矩阵推导过程中的值是什么?是-Z
吧,这表示的几何意思是该点在未进行投影前到的距离,那么你要保证该点在视锥体内,也就是要在近平面和远平面之间,也就推导出了第二个条件。
//简单齐次裁剪函数, false为需要裁剪
function clipSpaceCull(pt, near, far) {
let [x, y, z, w] = pt;
if (w >= near && w <= far && x >= -w && x <= w && y >= -w && y <= w &&
z >= -w && z <= w)
return true;
return false;
}
NDC标准化设备坐标系
标准化设备坐标系就是点坐标都为在一个立方体中,且都满足条件的坐标系,经过齐次裁剪后裁剪坐标系内的点进行归一化处理使得值为1的过程称作透视除法,转换后的坐标也就成了标准化设备坐标系下的点坐标。
//齐次裁剪
if (!clipSpaceCull(clip_position, near, far)) break
let ndc_position = glMatrix.vec4.create() //透视除法,转化为标准坐标
glMatrix.vec4.scale(ndc_position, clip_position, 1 / clip_position[3]) //将w转换为1
背面剔除
关于面剔除我推荐这一篇文章从零开始的软渲染器(2)- 旋转木箱 最后一部分有讲背面剔除,背面剔除其实是一个性能优化的过程,你可做可不做,如果要做呢,首先要明确你的正面是什么,在opengl接口这也都是可调的,自己写就测试一下就知道了,因为我自己测试的时候虽然你可以依照上述文章中的算法去做,但如果你改动一下数据存储的顺序结果就会受影响,然后明确你的观察方向就行了,我是这么做的。
function faceCulling(pts) { //这里pts为三角形面的三个顶点坐标
//背面剔除, 返回fasle为需要被剔除
let p0 = glMatrix.vec3.create();
glMatrix.vec3.negate(p0, pts[0]);
let p0p1 = glMatrix.vec3.create();
glMatrix.vec3.add(p0p1, pts[1], p0);
let p0p2 = glMatrix.vec3.create();
glMatrix.vec3.add(p0p2, pts[2], p0);
let n = glMatrix.vec3.create();
glMatrix.vec3.cross(n, p0p1, p0p2);
glMatrix.vec3.normalize(n, n);
return glMatrix.vec3.dot(n, glMatrix.vec3.fromValues(0, 0, -1)) > 0 ? true : false;
}
视口变换
最后一个流程视口变换,就是把标准化设备坐标系下的坐标,转换成你最终渲染到视图上的坐标。关于视口变换我推荐这一篇文章《DirectX视口变换矩阵详解》
//视口变换,这里
function viewPort(pts, w, h) { //这里有个坑嗷,glMatrix是左乘,文章里是右乘,注意,还有glMatrix是列优先。
let viewTrans = glMatrix.mat4.fromValues(
w / 2, 0, 0, 0, 0, -h / 2, 0, 0, 0, 0, 1, 0, w / 2, h / 2, 0, 1);
let viewport_position = [];
pts.forEach((el) => {
let pt = glMatrix.vec4.fromValues(el[0], el[1], el[2], 1);
let v_pt = glMatrix.vec4.create();
glMatrix.vec4.transformMat4(v_pt, pt, viewTrans);
v_pt = glMatrix.vec3.fromValues(v_pt[0], v_pt[1], v_pt[2]);
viewport_position.push(v_pt);
});
return viewport_position;
}
添加个旋转动画,看一下动起来的效果
未完待续
参考资料
Fundamentals of Computer Graphics, Fourth Edition
Tiny renderer or how OpenGL works: software rendering in 500 lines of code