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

1,138 阅读5分钟

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

目标

承接上篇,这次主要以三角面为基本的图元组织顶点,并对其进行光栅化处理,然后对三角面的每个片元进行着色。 image.png

实践部分

图元装配

图元装配在我的理解里就是如何去组织数据里的这些顶点,比如我以两个顶点连成的线为基本图元,或者以三个顶点构成的三角面为基本图元等等。

以点精灵为基本图元进行图元装配 image.png 以线为基本图元进行图元装配

image.png 以三角面为基本图元进行图元装配

image.png

光栅化

我们知道屏幕是由一个个像素组成的,所以要在屏幕上显示三维模型,就要用这一个个像素去采样摄像机视角下的模型画面。在上面我们将顶点数据进行图元装配,这其实把用像素采样模型画面的问题分解成了用像素采样每个基本的图元这样的问题。一般常用三角面为基本图元进行图元装配,因为三角面是构成一个面的最小单元,而且有很多可以利用的几何性质,便于算法计算。关于三角形光栅化的教学资源我推荐这两个,比较简单明了Lesson 2: Triangle rasterization and back face culling演示光栅化算法的动画

我用的算法是先找出三角面的最小包围矩形,然后判断该矩形内的每个像素是否在三角面中来进行采样。 image.png

判断像素点是否在三角面中的方法

判断的方法有很多,我推荐这一篇文章[判断点是否在三角形内],我用的是重心法来进行判断,因为后面会利用重心法计算出的权值进行UV坐标、深度值z和片元法线的插值。

深度测试

深度测试的目的就是模拟一个画面的遮挡效果,比如如果一个立方体前有一个不透明的物体正好遮住了立方体,那么我们应该是只能看到前面这个不透明的物体,而后面的立方体应该是不会出现在画面中的。还记得我们在进行视图变换的过程中,对于顶点的Z值只是缩放了大小,并没有改变其方向即如下图所示:

image.png

也就是Z值越大,越靠近你,那么我们只要设置个保存Z值的 widthheightwidth*height 大小的缓冲区,然后采样三角面的过程中,保留同一像素所对应Z值最大的片元就行了。

片元着色

我在这里实现的软光栅渲染效果,只是给模型映射上纹理以及简单的用插值出来的片元法线和光线计算来光照强度,因为想在后续的学习过程中把相关的光照模型整理一下,再做其他系列的笔记。之前学习的过程中,一直被纹理贴图这一个概念迷惑,就很抽象,还有什么法线贴图、凹凸贴图啊等等,后来我就不纠结这些名词了,因为从量的角度讲,管你这些贴图用来干什么,实际就是一个二维矩阵或者二维数组,你可以以图片的方式观察它,就像下面这样:

african_head_diffuse.jpg 但我觉得最好从二维矩阵的角度来理解它,纹理映射我推荐这一篇《纹理映射》。在光栅的过程中,我们会得到三角面的所有片元,每个片元需要的值都要依赖顶点直接或者间件的插值获得,比如用于纹理映射的UV坐标,需要依据每个顶点对应的UV坐标,通过权值插值得到片元的UV坐标,再去获取相应纹理贴图对应坐标下的值。这个插值的过程也有坑就是你插值的权值的问题,我用的是利用重心法求出的权值来进行插值,但这是一种没有进行透视校正的插值方法,贴图会出现透视不正确的现象,关于透视校正插值我推荐这一篇文章《图形学 - 关于透视矫正插值那些事》。我这里也没有进行透视校正,主要更多的是体会思想,渲染管线的很多流程步骤都可以展开成很深的领域,学海无涯啊。获取纹理贴图对应坐标下的值,是一个采样的问题,这里面也是一个深坑,涉及到数字信号处理的一些理论。

这一部分的代码

function barycentric(A, B, C, P) {
  let s = []
  for(let i = 0; i < 2; i++) {
    let tmp = glMatrix.vec3.create()
    tmp[0] = C[i]-A[i]
    tmp[1] = B[i]-A[i]
    tmp[2] = A[i]-P[i]
    s.push(tmp)
  }

  let u = glMatrix.vec3.create()
  glMatrix.vec3.cross(u, s[0], s[1])

  if ( Math.floor(Math.abs(u[2])) != 0 ) {
    let result = glMatrix.vec3.fromValues(1-(u[0]+u[1])/u[2], u[1]/u[2], u[0]/u[2])
    return result
  }

  return glMatrix.vec3.fromValues(-1, -1, -1)

}

function triangle_raster(pts, uvs, normals, light_dir, zBuffer, width, getTexture, fragementShader, drawPixel) {
  let bboxmin = [Infinity, Infinity]
  let bboxmax = [-Infinity, -Infinity]
  for(let i = 0; i < 3; i++) {               //找到三角形面的边框
    for(let j = 0; j < 2; j++) {
      bboxmin[j] = Math.min(bboxmin[j], pts[i][j])
      bboxmax[j] = Math.max(bboxmax[j], pts[i][j])
    }
  }
  
  let p = glMatrix.vec2.create()


  for (p[0] = Math.floor(bboxmin[0]); p[0] <= bboxmax[0]; p[0]++) {
    for(p[1] = Math.floor(bboxmin[1]); p[1] <= bboxmax[1]; p[1]++) {
      let c = barycentric(pts[0], pts[1], pts[2], p) //由重心坐标得到的权值


      let z = pts[0][2]*c[0] + pts[1][2]*c[1] + pts[2][2]*c[2]  //插值出P的深度值

      if (c[0] < 0 || c[1] < 0 || c[2] < 0 || zBuffer[Math.floor(p[0]) + Math.floor(p[1])*width] > z) 
        continue;

      zBuffer[Math.floor(p[0]) + Math.floor(p[1])* width] = z;

      let uv = glMatrix.vec3.create()           //插值出P的uv坐标
      glMatrix.vec3.transformMat3(uv, c, uvs)

      let normal = glMatrix.vec3.create()       //插值出P的法线向量
      glMatrix.vec3.normalize(normal, normal)
      glMatrix.vec3.transformMat3(normal, c, normals)

      glMatrix.vec3.normalize(light_dir, light_dir)
      drawPixel(p[0], p[1], fragementShader(uv, getTexture, normal, light_dir))
    }
  }
}

function fragementShader(uv, texture, normal, light_dir){
    let color = texture(uv)
    let intensity = glMatrix.vec3.dot(normal, light_dir)
    glMatrix.vec3.scale(color, color, intensity)
    return color
}

结果展示

image.png

2021-08-30-17-19-05.gif

实现一个简单的软光栅渲染效果的源码和数据

github.com/HappyFronte…

参考资料

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

Fundamentals of Computer Graphics, Fourth Edition

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

资源推荐

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