[CG] TinyRenderer 学习记录(3):Z-Buffer 与 Shader

TL;DR

  • 移除了 fRange 函数。
  • 实现了 Z-Buffer 算法——可以正确处理遮挡关系了!
  • 引入了可编程 Shader(目前仅支持顶点着色器和片段着色器)。
    • 引入了透视除法、NDC 和视口变换 (这些都是顶点着色器连带的东西,所以顺便实现了)
  • 把 Model 封装成类,将模型的加载和渲染解耦。

代码就不放在本文里面了,可以参考我的 pyTinyRenderer 。虽然我当初的计划是把这个学习记录做成类似教程的东西,但又想到网上类似的文章很多,而且写得很好,遂作罢。

还有一件事。如果你看过原作者 ssloy 的 tinyrenderer 教程,可能会注意到我的学习记录和他的教程不是完全对应的。这是因为他的教程面向的对象是零基础,在初期为了实现某些本来只有后期才能实现的效果时,做了一些妥协,导致代码看上去不是很清晰,有点 “堆积” 的感觉,然后等学到后面之后又要把前面这些多余的代码删掉或是封装起来。我因为之前学过,所以就不拐弯抹角了,直接按照 pipeline 写,遇到不太清楚的地方再参考教程。

接下来主要讲讲实现过程中遇到的两个问题。

线性变换与非线性变换

本节含有误导性内容,已废弃。原因见文末。

首先是线性变换和非线性变换的顺序问题。结论是应当先做线性变换,再做非线性变换,这个没有异议,关键是原因。我记得去年暑假初学图形学的时候还推导过,但现在已经忘得一干二净了……

这个问题源于视口变换。视口变换将 NDC 空间的坐标变换到屏幕空间,也就是:

[1,1]×[1,1][0,w]×[0,h]

为此需要计算视口变换矩阵 M

习惯性地,我写出:

M=[w200w20h20h200100001]

如果按照 “先做线性变换,再做非线性变换” 的规则得到的就是上述矩阵,写法也很简单,左上角的 3x3 矩阵是线性变换矩阵,最右边的一列是平移向量,两者互不干扰。

但我又想,如果把顺序反过来,矩阵会是什么样子的呢?于是我在纸上写出如下内容:

M=[w20000h20000100001][1001010100100001]=[w200w20h20h200100001]

结果居然没有变化!我开始怀疑自己的记忆是否出了问题……

画了图之后才发现是特殊情况。

一般来说,调换顺序前后得到的两个矩阵是不相等的,比如先做平移(非线性变换)将物体移动至 (1,2) 再做 2 倍的缩放(线性变换)最终会导致物体被移动至 (2,4) 且尺寸变为原来的两倍,也就是平移的效果也被缩放了。这种情况下你只能先做线性变换再做非线性变换,因为反过来写矩阵非常不好写,需要考虑到平移和缩放之间的互相作用。

但是,本次变换的目标十分特殊——[0,w]×[0,h],无论顺序如何我们都能很快地写出变换矩阵,这是因为目标的左端点位于 0 处,此时的缩放是等比例的。

我画了一个示意图:
image
原始物体为 1,变换后物体为 2,执行的都是 2 倍的缩放变换,但是下面的物体在变换后发生了移动,原因前面已经解释过了。

保留小数是画蛇添足

然后就是小数的问题。顶点数据一般都是带有小数部分的,但实际的屏幕 / 图像由一个个像素组成,使用整数来索引。本着尽可能不损失信息的原则,我将取整操作放到 “写像素” 这一步进行。

举个例子,在利用重心坐标对三角形进行光栅化的函数中,第一步是计算包围盒,第二步是遍历包围盒判断点是否在三角形内,如果是则绘制。这两步涉及的坐标,我用的都是浮点数,直到最后的 setPixel 时才取整。本以为这样会更加精确,但结果反而如下图所示
image

出现了很多细细的白线,专业一点叫 artifact。

如果在第一步计算包围盒时取整,结果就是正确的:
image

原因我暂时不太清楚,但可以确定的是,保留小数是画蛇添足的行为,所以我把相关的代码全都改过了,并且删掉了 fRange 函数。

最终结果

写完上面这些内容后,打了两个小时的鬼泣 5,见到了名场面)
睡前打开工程,例行回顾了一下今天写的代码,发现 Z-Buffer 的插值写错了……
将错误更正后得到的结果如下:
image

有点恐怖谷效应——不过作者的教程里面有更瘆人的,感兴趣的话可以看看

废弃 “线性变换与非线性变换” 一节的原因

今天(24 年 3 月 21 日)推导投影矩阵的时候发现一味遵从 “先线性变换再非线性变换” 的规则反而会搞复杂……可能是因为太久没看图形学,臆造出来了这样的规则,看来复习一下还是很有必要的。

问题是这样的:因为透视投影矩阵有一部分来自于正射投影矩阵,所以我从正射投影矩阵开始推导。目标很简单,就是把一个由 left, right, top, bottom, near, far 六个变量定义的长方体盒变换为 [1,1]×[1,1]×[1,1] 的立方体盒。由于变换前的长方体盒的中心不一定(多数情况下)不在坐标原点,因此先做线性变换(缩放)就会导致它距离原点的这一段位移也被缩放,后续计算容易出错,而先平移再缩放就没有这么多麻烦,可以很轻松地写出两个矩阵,乘一下就好了。

嗯,我想之所以会有 “先线性变换再非线性变换” 的规则,主要是因为这样的变换矩阵可以直接写出来,不需要额外的矩阵乘法,但这一规则如前面所说容易出错。

总之,具体问题具体分析。

posted @   ZXPrism  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示