手把手教你软渲染 #2 - 两大直线算法
前言
众所周知,模型是由很多个小三角面组成的,而三角面又是由三条边组成的,所以要想渲染模型,第一步是画直线。这一节要详细讲三个经典的直线算法。
在上一讲中我们讨论了如何用 xmake 来做跨平台的编译工作,我对 Windows 和 Mac 平台的编译做了区分:在 win 平台链接下载好的 lib,在 mac 平台使用 brew 来引入 SDL2 的依赖。然后 xmake 的作者 ruki 大佬指出可以直接用 xmake 的 libsdl 包,支持跨平台,可以用优美而一致的代码实现两个平台的配置。
所以文件 xmake.lua
可以改为:
add_rules("mode.debug", "mode.release")
set_languages("c++14")-- SDL2
add_requires("libsdl")if is_os("windows") then-- Avoid for error LINK1561add_ldflags("/SUBSYSTEM:CONSOLE")
endtarget("DLSoftRenderer")set_kind("binary")add_includedirs("src/include")add_files("src/*.cpp")add_packages("libsdl")
然后我们就可以安心地把 SDL2 的文件夹从项目中移除了,完全交给 xmake 去处理。
概述
由于显示器精度总是有限的,所以在屏幕上只能用离散的点去逼近直线,如下图所示:
直线算法的目标就是以最少的计算量,绘制出最逼近指定直线路径上的最佳像素点。该篇博客主要讲述两种画直线的算法:DDA 算法和 Bresenham 算法。
DDA 算法
第一个要介绍的算法叫做 DDA (Digital Differential Analyzer,数值微分法),顾名思义,该方法是用微积分的思想来画直线。
直线最简单的表示方法为斜截式:
y=kx+ty = kx + t y=kx+t
给定直线的两个端点 P0(x0,y0)P_0(x_0,y_0)P0(x0,y0) 和 P1(x1,y1)P_1(x_1,y_1)P1(x1,y1),则微分形式可以表示为:
k=ΔxΔy=x1−x0y1−y0k=\frac{\Delta x}{\Delta y}=\frac{x_1-x_0}{y_1-y_0} k=ΔyΔx=y1−y0x1−x0
基本思想是沿着某一个轴递增,计算另一个轴的增量,根据斜率拆解成两种情况:k<1k<1k<1 和 k≥1k\ge1k≥1。
综合考虑两种情况,写成伪代码就是:
DrawLine(x0, y0, x1, y1)dx = x1 - x0dy = y1 - y0step = max(abs(dx), abs(dy))xIncre = dx / stepyIncre = dy / stepfor i from 0 to stepSetPixel(round(x), round(y))x += xIncrey += yIncre
DDA 算法的每一步都是在上一步的值加上一个增量来获得的,故称为增量算法,DDA 尽管实现起来很简单,但是计算过程中有浮点数的运算,其效率因此不高。我们的软渲染器不会采用 DDA 算法。
中点 Bresenham 算法
Bresenham 算法才是我们今天的主角,因为它实现了完全无浮点数绘制直线,是现代应用最广范的直线生成算法。接下来我将带着你一步步推导 Bresenham 算法。
首先,直线可以用隐式方程来表示:
F(x,y)=y−kx−t=0F(x,y)=y-kx-t=0 F(x,y)=y−kx−t=0
如下图所示,Pi(xi,yi)P_i(x_i,y_i)Pi(xi,yi) 是当前点,Pu(xi+1,yi+1)P_u(x_i+1,y_i+1)Pu(xi+1,yi+1) 和 Pd(xi+1,yi)P_d(x_i+1,y_i)Pd(xi+1,yi) 是两个候选点,M(xi+1,yi+0.5)M(x_i+1,y_i+0.5)M(xi+1,yi+0.5) 是候选点的中点。
可以根据判别式决定下一个渲染的点是 PuP_uPu 还是 PdP_dPd:
d=F(xM,yM)d=F(x_M,y_M) d=F(xM,yM)
上述情况下的判别式则表示为:
di=F(xi+1,yi+0.5)=yi+0.5−k(xi+1)−td_i=F(x_i+1,y_i+0.5)=y_i+0.5-k(x_i+1)-t di=F(xi+1,yi+0.5)=yi+0.5−k(xi+1)−t
当 d<0d<0d<0 时,中点 MMM 在直线下方,直线为图中黑色实线位置,取候选点 PuP_uPu;当 d≥0d\ge0d≥0 时,中点 MMM 在直线上方,直线为图中绿色虚线位置,取候选点 PdP_dPd。
然后我们继续看后续的迭代,设当前点为 Pi+1(xi+1,yi+1)P_{i+1}(x_{i+1},y_{i+1})Pi+1(xi+1,yi+1),这时需要分类讨论:
- 若 di<0d_i<0di<0,此时候选点分别为 Pu(xi+2,yi+2)P_u(x_i+2,y_i+2)Pu(xi+2,yi+2) 和 Pd(xi+2,yi+1)P_d(x_i+2,y_i+1)Pd(xi+2,yi+1)。判别式为 di+1=F(xi+2,yi+1.5)=yi+1.5−k(xi+2)−t=di+1−kd_{i+1}=F(x_i+2,y_i+1.5)=y_i+1.5-k(x_i+2)-t=d_i+1-kdi+1=F(xi+2,yi+1.5)=yi+1.5−k(xi+2)−t=di+1−k,即相对于 did_idi 的增量是 1−k1-k1−k。
- 若 d≥0d \ge 0d≥0,此时候选点分别为 Pu(xi+2,yi+1)P_u(x_i+2,y_i+1)Pu(xi+2,yi+1) 和 Pd(xi+2,yi)P_d(x_i+2,y_i)Pd(xi+2,yi)。判别式为 di+1=F(xi+2,yi+0.5)=di−kd_{i+1}=F(x_i+2,y_i+0.5)=d_i-kdi+1=F(xi+2,yi+0.5)=di−k,即相对于 did_idi 的增量是 −k-k−k。
接下来,只要求出 d0d_0d0 就可以得到 ddd 的递推公式了。假设 P0(x0,y0)P_0(x_0,y_0)P0(x0,y0) 是刚好在直线上的点,则有 F(x0,y0)=y0−kx0−t=0F(x_0,y_0)=y_0-kx_0-t=0F(x0,y0)=y0−kx0−t=0,此时两个候选点的中点坐标为 M(x0+1,y0+0.5)M(x_0+1,y_0+0.5)M(x0+1,y0+0.5),有 d0=F(x0+1,y0+0.5)=y0−kx0−t+0.5−k=0.5−kd_0=F(x_0+1,y_0+0.5)=y_0-kx_0-t+0.5-k=0.5-kd0=F(x0+1,y0+0.5)=y0−kx0−t+0.5−k=0.5−k。
整理一下,ddd 的初值为 d0=0.5−kd_0=0.5-kd0=0.5−k,若 di<0d_i<0di<0,则 di+1=di+1−kd_{i+1}=d_i+1-kdi+1=di+1−k;若 di≥0d_i\ge0di≥0,则 di+1=di−kd_{i+1}=d_i-kdi+1=di−k。
经观察发现,在对判定式 ddd 的迭代过程中,作为判定的只有 did_idi 的正负。为了消除浮点数,可以对所有的 ddd 乘上同一个数,这个倍数就是 2Δx2\Delta x2Δx。注意 k=Δy/Δxk=\Delta y / \Delta xk=Δy/Δx 表示斜率。
那么,对 ddd 的迭代过程就修改为:初值为 d0=Δx−2Δyd_0=\Delta x-2\Delta yd0=Δx−2Δy,若 di<0d_i<0di<0,则 di+1=di+2Δx−2Δyd_{i+1}=d_i+2\Delta x-2\Delta ydi+1=di+2Δx−2Δy;若 di≥0d_i\ge0di≥0,则 di+1=di−2Δyd_{i+1}=d_i-2\Delta ydi+1=di−2Δy。
到此,Bresenham 算法已推导完毕,可以写出伪代码了:
DrawLine(x0, y0, x1, y1)dx = x1 - x0dy = y1 - y0y = y0d = dx - 2 * dyfor x from x0 to x1SetPixel(x, y)if d < 0y++d += 2 * (dx - dy)elsed -= 2 * dy
但是,上述代码只考虑了 k∈[0,1]k \in [0, 1]k∈[0,1] 的情况,要扩展到完整体也不难:
DrawLine(x0, y0, x1, y1)dx = x1 - x0dy = y1 - y0d = dx - 2 * dyif dx < 0swap(x0, x1)dx = -dxif dy < 0swap(y0, y1)dy = -dyif dx > dyy = y0for x from x0 to x1SetPixel(x, y)if d < 0y++d += 2 * (dx - dy)elsed -= 2 * dyelsex = x0for y from y0 to y1SetPixel(x, y)if d < 0x++d += 2 * (dy - dx)elsed -= 2 * dx
改进的 Bresenham 算法
接下来是改进的 Bresenham 算法,它是从前面所述的中点 Bresenham 算法优化而来的。
前面介绍的 Bresenham 算法用中点 MMM 与直线的位置关系来引入误差项,其实误差项有更直观的计算方法。如下图所示,我们以起点 P0(x0,y0)P_0(x_0,y_0)P0(x0,y0) 为原点构造网格线,ddd 为误差项。
每次 xxx 增加 111,若 d>0.5d>0.5d>0.5,则 yyy 增加 111;若 d≤0.5d\le0.5d≤0.5,则 yyy 保持不变。
ddd 的迭代过程可描述为:初值为 d0=0d_0=0d0=0,若 di>0.5d_i>0.5di>0.5,则 di+1=di−1,yi+1=yi+1d_{i+1}=d_i-1,\ y_{i+1}=y_i+1di+1=di−1, yi+1=yi+1;若 di≤0.5d_i\le0.5di≤0.5,则 di+1=di,yi+1=yid_{i+1}=d_i,\ y_{i+1}=y_idi+1=di, yi+1=yi。为消除浮点数,令 e=d−0.5e=d-0.5e=d−0.5,然后同样放大 2Δx2 \Delta x2Δx 倍,得到:
{e0=−Δxei+1=ei+2Δyxi+1=xi+1e>0⇒yi+1=yi+1,ei+1=ei+1−2Δxe≤0⇒yi+1=yi\left\{ \begin{aligned} & e_0 = -\Delta x \\ & e_{i+1} = e_i + 2 \Delta y \\ & x_{i+1} = x_i + 1 \\ & e>0 \Rightarrow y_{i+1} = y_i + 1,\ e_{i+1}=e_{i+1}-2 \Delta x \\ & e\le0 \Rightarrow y_{i+1} = y_i \end{aligned} \right. ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧e0=−Δxei+1=ei+2Δyxi+1=xi+1e>0⇒yi+1=yi+1, ei+1=ei+1−2Δxe≤0⇒yi+1=yi
于是可以写出伪代码:
DrawLine(x0, y0, x1, y1)dx = x1 - x0dy = y1 - y0if dx < 0swap(x0, x1)dx = -dxif dy < 0swap(y0, y1)dy = -dyif dx > dyy = y0e = -dxfor x from x0 to x1SetPixel(x, y)e += 2 * dyif e > 0y++e -= 2 * dxelsex = x0e = -dyfor y from y0 to y1SetPixel(x, y)e += 2 * dxif e > 0x++e -= 2 * dy
极简版 Bresenham
最后贴上极简版 Bresenham 算法的实现(请原谅我水平有限,该版本实在是推导不来
手把手教你软渲染 #2 - 两大直线算法相关推荐
- 机器学习笔记——2 简单线性模型及局部加权线性模型的基本原理和python实现(参数估计的两个基本角度:几何直观和概率直观。函数最值问题的两大基本算法:梯度方法与迭代方法)
简单线性模型及局部加权线性模型的基本原理和python实现(参数估计的两个基本角度:几何直观和概率直观.函数最值问题的两大基本算法:梯度方法与迭代方法) 线性模型是什么? 线性模型是监督学习的各类学习 ...
- 人工智能换脸python_AI换脸(手把手教你实现吴彦祖变苏大强)
课程背景 朱茵秒变杨幂,AI换脸有多神奇? 前些时间在网上大火的AI换脸黑科技在网上受到很多人的追捧,通过AI换脸技术,朱茵变杨幂,苏大强变吴彦祖,换脸后毫无违和感,真正做到无缝连接!那么这个换脸黑科 ...
- 数据卡片_手把手教你构建企业实时数据大屏
大数据时代,企业的任何规划和决策都离不开数据分析的支撑.领导开会要看数据,项目拉投资要看数据,活动复盘要看数据-- 而传统的看数据方式要提前知晓领导需要哪些维度的数据,再由数据分析师基于历史数据做好报 ...
- 来手把手教你通过Matlab用两种方法实现图像压缩与解压(附超详细代码),赶紧点赞收藏吧
图像压缩方法 DCT图像压缩 DCT原理介绍 DCT和它解压时的反运算的具体算法 详细实现代码 结果展示 行程编码压缩与解压 读入图像 图像转为矩阵 行程编码压缩 行程编码解压 显示图像 完整代码附录 ...
- 麦语言和python区别_麦语言编程教程之二:解决两大类问题
所有的编程都可以归结为以下三类: 第一,表达式问题.以当前视角,给出变量的计算公式.所谓的当前视角,就是不考虑位置关系,把那个位置当作是当前位置.事实上,前面那张表中任何一个方格的数据,都是把这个方格 ...
- 六大排序算法与常见的两大查找算法汇总(C语言)
一.冒泡排序算法: void Bubblesort(int arry[],int len)//冒泡排序算法 ,len为数组长度 {int i,j;for(i=0;i<len-1;i++){for ...
- 比MySQL快801倍,OLAP两大神器ClickHouse+Doris技术选型攻略分享
数字化时代,数据即价值.商战即信息战,如何从海量数据中提取精准的用户群体信息成为众多企业经营的重中之重,这就对开发工程师在速度和精准度方面的要求越来越高. 海量订单如何精准获取地域.时间.来源.渠道等 ...
- 比MySQL快801倍,OLAP两大神器ClickHouse和Doris到底怎么选?
对于匠人而言,一项手艺,如果学不会,结果只有两种:一是低头求人,二是花钱买成品. 对于程序员而言,一款热门软件,如果玩不转,结果也是两种:一是向高手求教,二是看着别人升职加薪. 2021年4月程序员薪 ...
- 2021年大数据Hive(三):手把手教你如何吃透Hive数据库和表操作(学会秒变数仓大佬)
全网最详细的Hive文章系列,强烈建议收藏加关注! 后面更新文章都会列出历史文章目录,帮助大家回顾知识重点. 目录 系列历史文章 前言 Hive数据库和表操作 一.数据库操作 1.创建数据库 2.创建 ...
最新文章
- DecimalFormat 的使用
- 图片碎片化mask动画
- mongo执行逻辑表达式_MongoDB 高级查询
- 自适应宽_移动端实现自适应缩放界面的方法汇总
- “中国工程设计大师”俞加康:为地铁耕耘“时不我待,只争朝夕”
- 在Python中没有测试[重复]
- 【数据结构和算法笔记】:树的概念和性质总结
- win10+anaconda3+python3.6+opencv3.1.0
- 常用js(javascript)函数
- STP是一个需要众力协作的协议
- YOLOv5、v7改进之三十九:引入改进遮挡检测的Tri-Layer插件 | BMVC 2022
- SugarCRM将告诉你什么是客户关系管理系统中最成功解决方案
- 19-21Consent Page页实现
- java布局怎么让他变色,墙纸这是怎么了,为什么变红了自己有这一块变色了很明显的变色,有啥办法能让他变回去么...
- 学习笔记:匿名通信与暗网研究综述
- ipad照片文件删除了怎么恢复
- LearnOpenGL 入门—摄像机
- [附源码]Java计算机毕业设计SSM大学生运动员健康管理系统
- AES加密解密算法设计(C++)
- 新绝代双骄3终极全攻略4