[图形学笔记系列] 实现一个简单的软光栅渲染效果-02

810 阅读4分钟

上一篇:[图形学笔记系列] 实现一个简单的软光栅渲染效果-01

目标

承接上篇,本章将完成从模型数据的原始坐标至屏幕坐标的转换。

image.png

实践部分

裁剪坐标系

模型的原始数据经过模型变换、视图变换以及投影将转换为裁剪坐标系下的坐标,这里之所以叫裁剪坐标系是因为下面要进行齐次裁剪这个操作,就是把摄像机观察范围以外的部分删除掉,不要多想,在裁剪坐标系内的点坐标,此时还是齐次坐标系即(x,y,z,w)(x,y,z,w)

关于坐标变换的相关内容我推荐这一篇文章: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的时候加了视口变换):

image.png

齐次裁剪

齐次裁剪我推荐看这一篇文章《一篇文章彻底弄懂齐次裁剪》,讲的挺好的,但其实我实现的时候就用的简单的齐次裁剪方法,也就是要满足wx,y,zw-w \leq x,y,z \leq wnearwfarnear \leq w \leq far这两个条件。其实很好理解,在透视投影矩阵推导的过程中,我们是将视锥体内的点压缩到一个[1,1][1,1][1,1][-1, 1]*[-1, 1]*[-1,1]这样的标准化立方体里,也就是说由齐次坐标转化为笛卡尔坐标系后的点坐标[x/w,y/w,z/w,1][x/w, y/w, z/w, 1]要满足该约束,也就可以推导出第一个条件。第二个条件是这样的推导的。还记得透视投影矩阵推导过程中ww的值是什么?是-Z吧,z|z|这表示的几何意思是该点在未进行投影前到yOxyOx面的距离,那么你要保证该点在视锥体内,也就是要在近平面和远平面之间,也就推导出了第二个条件。

//简单齐次裁剪函数, 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标准化设备坐标系

标准化设备坐标系就是点坐标都为(xw,yw,zw,1)(\frac{x}{w},\frac{y}{w},\frac{z}{w},1)在一个2222*2*2立方体中,且都满足1x,y,z1-1 \leq x,y,z \leq 1条件的坐标系,经过齐次裁剪后裁剪坐标系内的点进行归一化处理使得ww值为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;
    }

添加个旋转动画,看一下动起来的效果

2021-08-28-17-52-15.gif

未完待续

下一篇:[图形学笔记系列] 实现一个简单的软光栅渲染效果-03

参考资料

GAMES101-现代计算机图形学入门-闫令琪

Fundamentals of Computer Graphics, Fourth Edition

Tiny renderer or how OpenGL works: software rendering in 500 lines of code

资源推荐

零基础如何学习计算机图形学