系列文章目录

简介:Computer Graphics From Scratch-《从零开始的计算机图形学》简介
第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念
第二章:Computer Graphics From Scratch - Chapter 2 基本光线追踪
第三章:Computer Graphics From Scratch - Chapter 3 光照
第四章:Computer Graphics From Scratch - Chapter 4 阴影和反射
第五章:Computer Graphics From Scratch - Chapter 5 扩展光线追踪


Chapter 6

  • 系列文章目录
  • Lines - 线条
  • 一、Describing Lines - 描述线
  • 二、Drawing Lines - 画线
  • 三、Drawing Lines with Any Slope - 绘制任意斜率的线
  • 四、The Linear Interpolation Function - 线性内插函数
  • 五、Summary - 概括

Lines - 线条

In Part I of this book, we studied raytracing extensively and developed a raytracer that could render our test scene with accurate lighting, material properties, shadows, and reflection using relatively simple algorithms and math.
This simplicity comes at a cost: performance. While non-real-time performance is fine for certain applications, such as architectural visualization or visual effects for movies, it’s not enough for other applications, such as video games.

在本书的第一部分中,我们广泛研究了光线追踪,并开发了一种光线追踪器,它可以使用相对简单的算法和数学来渲染我们的测试场景,包括准确的光照材质属性阴影和反射。 这种简单性是有代价的:性能。 虽然非实时性能对于某些应用程序来说很好,例如建筑可视化或电影的视觉效果,但对于其他应用程序(例如视频游戏)来说还不够。


在本书的这一部分,我们将探索一组完全不同的算法,这些算法有利于性能而不是数学纯度。

我们的光线追踪器从相机开始,通过视口探索场景。 对于画布的每个像素,我们都会回答“场景的哪个对象在这里可见?”这个问题。 现在我们将采用一种在某种意义上相反的方法:对于场景中的每个对象,我们将尝试回答“该对象在画布的哪些部分可见?”这个问题。

事实证明,只要我们愿意在精度上做出一些权衡,我们就可以开发出比光线追踪更快地回答这个新问题的算法。 稍后,我们将探索如何使用这些快速算法来获得与光线追踪器相当的质量的结果。

我们将再次从头开始:我们有一个尺寸为 C w C_w Cw​ 和 C h C_h Ch​ 的画布,我们可以使用 PutPixel() 设置单个像素的颜色,但仅此而已。 让我们探索如何在画布上绘制最简单的元素:两点之间的线


一、Describing Lines - 描述线

假设我们有两个画布点 P 0 P_0 P0​ 和 P 1 P_1 P1​,坐标分别为 ( x 0 , y 0 ) (x_0,y_0) (x0​,y0​) 和 ( x 1 , y 1 ) (x_1,y_1) (x1​,y1​)。 我们如何绘制 P 0 P_0 P0​ 和 P 1 P_1 P1​ 之间的直线段?

让我们从用参数坐标表示一条线开始,就像我们之前对光线所做的那样(实际上,您可以将“光线”视为 3D 中的线)。 从 P 0 P_0 P0​ 开始,沿从 P 0 P_0 P0​ 到 P 1 P_1 P1​ 的方向移动一段距离,可以得到线上的任意点 P P P:

P = P 0 + t ( P 1 − P 0 ) P = P_0 + t (P_1 - P_0) P=P0​+t(P1​−P0​)

我们可以将这个方程分解为两个,每个坐标一个:

x = x 0 + t ⋅ ( x 1 − x 0 ) − − − − − ① x = x_0 + t \cdot (x_1 - x_0)----- ① x=x0​+t⋅(x1​−x0​)−−−−−①

y = y 0 + t ⋅ ( y 1 − y 0 ) − − − − − ② y = y_0 + t \cdot (y_1 - y_0)----- ② y=y0​+t⋅(y1​−y0​)−−−−−②

让我们取第一个方程并求解 t t t:

x − x 0 = t ⋅ ( x 1 − x 0 ) x - x_0 = t \cdot (x_1 - x_0) x−x0​=t⋅(x1​−x0​)

x − x 0 x 1 − x 0 = t \dfrac{x - x_0}{x_1 - x_0} = t x1​−x0​x−x0​​=t

我们现在可以将 t t t 的这个表达式代入第二个方程:

y = y 0 + x − x 0 x 1 − x 0 ⋅ ( y 1 − y 0 ) y = y_0 + \dfrac{x - x_0}{x_1 - x_0} \cdot (y_1 - y_0) y=y0​+x1​−x0​x−x0​​⋅(y1​−y0​)

稍微重新整理一下:

y = y 0 + ( x − x 0 ) ⋅ y 1 − y 0 x 1 − x 0 y = y_0 + (x - x_0) \cdot \dfrac{y_1 - y_0}{x_1 - x_0} y=y0​+(x−x0​)⋅x1​−x0​y1​−y0​​

请注意 y 1 − y 0 x 1 − x 0 \dfrac{y_1 - y_0}{x_1 - x_0} x1​−x0​y1​−y0​​是一个仅取决于端点的常数部分; 让我们称之为 a a a[也就是我们好久之前学的斜率]。 所以我们可以将上面的等式改写为:

y = y 0 + a ⋅ ( x − x 0 ) y = y_0 + a \cdot (x - x_0) y=y0​+a⋅(x−x0​)


什么是 a a a? 根据我们定义的方式,它测量每单位 x 坐标变化的 y 坐标变化; 换句话说,它是线斜率的度量。

让我们回到等式。 乘法分配律:

y = y 0 + a ⋅ x − a ⋅ x 0 y = y_0 + a \cdot x - a \cdot x_0 y=y0​+a⋅x−a⋅x0​

对常量进行分组:
y = a ⋅ x + ( y 0 − a ⋅ x 0 ) y = a \cdot x + (y_0 - a \cdot x_0 ) y=a⋅x+(y0​−a⋅x0​)

同样, ( y 0 – a x 0 ) (y_0 – ax_0) (y0​–ax0​) 仅取决于段的端点; 让我们称之为b。 最后我们得到:
y = a x + b y = ax+b y=ax+b


这是线性函数的标准公式,几乎可以用来表示任何直线。 当我们求解 t t t 时,我们直接除以 x 1 – x 0 x_1 – x_0 x1​–x0​ ,而不考虑如果 x 1 = x 0 x1 = x0 x1=x0 会发生什么。 我们不能除以零,这意味着这个公式不能表示 x 1 = x 0 x_1 = x_0 x1​=x0​ 的线——即一条垂直的线

为了解决这个问题,我们暂时忽略垂直线,稍后再弄清楚如何处理它们。


二、Drawing Lines - 画线

我们现在有一种方法可以获取我们感兴趣的每个 x x x 值的 y y y 值。这给了我们一个满足直线方程的 ( x , y ) (x,y) (x,y) 对。

我们现在可以编写一个从 P 0 P_0 P0​ 到 P 1 P_1 P1​ 绘制线段的函数的第一个近似值。
设 x 0 x_0 x0​ 和 y 0 y_0 y0​ 分别是 P 0 P_0 P0​ 的 坐标, x 1 x_1 x1​ 和 y 1 y_1 y1​ 是 P 1 P_1 P1​ 的坐标。即: P 0 ( x 0 , y 0 ) , P 1 ( x 1 , y 1 ) P_0(x_0,y_0),P_1(x_1, y_1) P0​(x0​,y0​),P1​(x1​,y1​)
假设 x 0 < x 1 x_0 < x_1 x0​<x1​,我们可以从 x 0 x_0 x0​ 到 x 1 x_1 x1​,计算之间每个 x x x 值以及其对应的 y y y 值,并在这些坐标处绘制一个像素。


DrawLine(P0, P1, color)
{a = (y1 - y0) / (x1 - x0)b = y0 - a * x0for x = x0 to x1 {y = a * x + bcanvas.PutPixel(x, y, color)}
}

请注意,除法运算符 / / /被期望执行实数除法,而不是整数除法。尽管在这种情况下 x x x和 y y y是整数,因为它们代表画布上的像素坐标


另外 注意,我们认为for循环包括范围内的最后一个值。
在C、C++、Java和JavaScript等语言中,这将被写成 f o r ( x = x 0 ; x < = x 1 ; + + x ) for(x = x_0; x <= x1; ++x) for(x=x0​;x<=x1;++x)。
我们将在本书中使用这一约定。

这个函数是上述等式的直接、简单的实现。 它有效,但我们可以让它更快吗?

我们不会为任意 x x x 计算 y y y 的值。 相反,我们仅以 x x x 的整数增量计算它们,并且我们是按顺序进行的。 在计算 y ( x ) y(x) y(x) 之后,我们立即计算 y ( x + 1 ) y(x + 1) y(x+1):

y ( x ) = a x + b y(x) = ax + b y(x)=ax+b
y ( x + 1 ) = a ⋅ ( x + 1 ) + b y(x+1) = a \cdot (x+1) + b y(x+1)=a⋅(x+1)+b

我们可以稍微操作一下第二个表达式:

y ( x + 1 ) = a x + a + b y(x+1) = ax + a + b y(x+1)=ax+a+b
y ( x + 1 ) = ( a x + b ) + a y(x+1) = (ax + b ) + a y(x+1)=(ax+b)+a
y ( x + 1 ) = y ( x ) + a y(x+1) = y(x) + a y(x+1)=y(x)+a

这不足为奇。 毕竟,斜率 a a a 是当 x x x 增加 1 1 1 时 y y y 变化的量度,这正是我们在这里所做的。

这意味着我们可以通过取 y y y 的前一个值并加上斜率来计算 y y y 的下一个值; 不需要逐像素乘法,这使函数更快。 一开始没有“ y y y 的先前值”,所以我们从 ( x 0 , y 0 ) (x_0,y_0) (x0​,y0​) 开始。 然后我们继续将 1 1 1 加到 x x x 和 a a a 到 y y y 直到我们得到 x 1 x_1 x1​。

再次假设 x 0 < x 1 x_0 < x_1 x0​<x1​,我们可以将函数重写如下:

DrawLine(P0, P1, color)
{a = (y1 - y0) / (x1 - x0)y = y0for x = x0 to x1 {canvas.PutPixel(x, y, color)y = y + a}
}

到目前为止,我们一直假设 x 0 < x 1 x_0 < x_1 x0​<x1​。 有一个简单的解决方法来支持不成立的线:因为我们绘制像素的顺序无关紧要,如果我们得到一条从右到左的线,我们可以交换 P 0 P_0 P0​ 和 P 1 P_1 P1​ 将其转换为 同一行的从左到右的版本,并像以前一样绘制它:

DrawLine(P0, P1, color)
{// Make sure x0 < x1if x0 > x1 {swap(P0, P1)}a = (y1 - y0) / (x1 - x0)y = y0for x = x0 to x1 {canvas.PutPixel(x, y, color)y = y + a}
}

让我们用我们的函数画几条线。 图 6-1 显示了线段 ( – 200 , – 100 ) – ( 240 , 120 ) (–200, –100) – (240, 120) (–200,–100)–(240,120),图 6-2 显示了线段的特写.

图 6-1:一条直线

图 6-2:放大直线


线条看起来是锯齿状的,因为我们只能在整数坐标上绘制像素,而数学线条实际上的宽度为零; 我们绘制的是从 ( – 200 , – 100 ) – ( 240 , 120 ) (–200, –100) – (240, 120) (–200,–100)–(240,120) 的理想线量化近似值

有一些方法可以绘制更漂亮的线条近似值(您可能想研究 MSAAFXAASSAATAA 作为一组有趣的兔子洞的可能入口点)。 我们不会去那里有两个原因:(1)它更慢,(2)我们的目标不是画漂亮的线条,而是开发一些基本的算法来渲染 3 D 3D 3D 场景。


让我们试试另一行, ( – 50 , – 200 ) – ( 60 , 240 ) (–50, –200) – (60, 240) (–50,–200)–(60,240)。 图 6-3 显示了结果,图 6-4 显示了相应的特写。

图 6-3:另一条斜率更高的直线

图 6-4:放大第二条直线


该算法完全按照我们告诉它的去做; 它从左到右,为每个 x x x 值计算一个 y y y 值,并绘制相应的像素。 问题是它为每个 x x x 值计算了一个 y y y 值,而在这种情况下,我们实际上需要为某些 x x x 值计算几个 y y y 值。

发生这种情况是因为我们选择了 y = f ( x ) ; y = f(x); y=f(x); 的公式。 事实上,这也是我们不能画垂直线的原因—— y y y 的所有值都对应 x x x 的相同值的极端情况。


三、Drawing Lines with Any Slope - 绘制任意斜率的线

选择 y = f ( x ) y = f(x) y=f(x) 是任意选择; 我们同样可以选择将这条线表示为 x = f ( y ) x = f(y) x=f(y)。 通过交换 x x x 和 y y y 重新构造所有方程,我们得到以下算法:

DrawLine(P0, P1, color)
{// Make sure y0 < y1if y0 > y1 {swap(P0, P1)}a = (x1 - x0)/(y1 - y0)x = x0for y = y0 to y1 {canvas.PutPixel(x, y, color)x = x + a}
}

这与之前的 DrawLine 相同,只是交换了 x x x 和 y y y 计算。 这个可以处理垂直线,并且可以正确绘制 ( 0 , 0 ) – ( 50 , 100 ) (0, 0) – (50, 100) (0,0)–(50,100); 但当然,它根本无法处理水平线,或者正确绘制 ( 0 , 0 ) − ( 100 , 50 ) (0, 0) - (100, 50) (0,0)−(100,50)! 该怎么办?

我们可以保留函数的两个版本,并根据我们尝试绘制的线选择使用哪一个。 标准很简单; 这条线的 x x x 值是否比 y y y 的不同值更多? 如果 x x x 的值多于 y y y,我们使用第一个版本; 否则,我们使用第二个。

示例6-1显示了一个处理所有情况的DrawLine的版本。

DrawLine(P0, P1, color)
{dx = x1 - x0dy = y1 - y0if abs(dx) > abs(dy) {// Line is horizontal-ish// Make sure x0 < x1if x0 > x1 {swap(P0, P1)}a = dy/dxy = y0for x = x0 to x1 {canvas.PutPixel(x, y, color)y = y + a}} else {// Line is vertical-ish// Make sure y0 < y1if y0 > y1 {swap(P0, P1)}a = dx/dyx = x0for y = y0 to y1 {canvas.PutPixel(x, y, color)x = x + a}}
}

示例6-1:一个处理所有情况的DrawLine的版本

这当然有效,但它并不漂亮。 有很多代码重复,选择使用哪个函数的逻辑,计算函数值的逻辑,以及像素绘制本身都是交织在一起的。 我们当然可以做得更好!


四、The Linear Interpolation Function - 线性内插函数

我们有两个线性函数 y = f ( x ) y = f(x) y=f(x) 和 x = f ( y ) x = f(y) x=f(y)。 为了抽象出我们正在处理像素这一事实,让我们以更通用的方式将其写为 d = f ( i ) d = f(i) d=f(i),其中 i i i 是自变量,我们为其选择值, d d d 是因变量 ,其值取决于另一个并且我们想要计算的那个。 在水平方向的情况下, x x x 是自变量, y y y 是因变量; 在垂直的情况下,情况正好相反。

当然,任何函数都可以写成 d = f ( i ) d = f(i) d=f(i)。 我们知道另外两件事完全定义了我们的函数:它是线性的,以及它的两个值——即 d 0 = f ( i 0 ) d_0 = f(i_0) d0​=f(i0​) 和 d 1 = f ( i 1 ) d_1 = f(i_1) d1​=f(i1​)。 我们可以编写一个简单的函数来获取这些值并返回 d d d 的所有中间值的列表,假设和之前一样 i 0 < i 1 i_0 < i_1 i0​<i1​:

Interpolate (i0, d0, i1, d1)
{values = []a = (d1 - d0) / (i1 - i0)d = d0for i = i0 to i1 {values.append(d)d = d + a}return values
}

该函数与前两个版本的 DrawLine 具有相同的“形状”,但变量被称为 i i i 和 d d d 而不是 x x x 和 y y y,并且这个函数不是绘制像素,而是将值存储在一个列表中。

注意 i 0 i_0 i0​对应的 d d d的值在 v a l u e s [ 0 ] values[0] values[0]中返回, i 0 + 1 i_0+1 i0​+1 的值在 v a l u e s [ 1 ] values[1] values[1]中返回,以此类推; 通常, i n i_n in​ 的值在 v a l u e s [ i n − i 0 ] values[i_n - i_0] values[in​−i0​] 中返回,假设 i n i_n in​ 在 [ i 0 , i 1 ] [i0, i1] [i0,i1] 范围内。

我们需要考虑一个极端情况:我们可能想要为 i i i 的单个值计算 d = f ( i ) d = f(i) d=f(i),即当 i 0 = i 1 i_0 = i_1 i0​=i1​ 时。 在这种情况下,我们甚至无法计算 a a a,因此我们将其视为一种特殊情况:

Interpolate (i0, d0, i1, d1) {if i0 == i1 {return [ d0 ]}values = []a = (d1 - d0) / (i1 - i0)d = d0for i = i0 to i1 {values.append(d)d = d + a}return values
}

作为实现细节,对于本书的其余部分,自变量 i i i 的值总是整数,因为它们代表像素,而因变量 d d d 的值总是浮点值,因为它们代表泛型的值 线性函数

现在我们可以使用 Interpolate 编写 DrawLine(示例 6-2)。

DrawLine(P0, P1, color)
{if abs(x1 - x0) > abs(y1 - y0) {// Line is horizontal-ish// Make sure x0 < x1if x0 > x1 {swap(P0, P1)}ys = Interpolate(x0, y0, x1, y1)for x = x0 to x1 {canvas.PutPixel(x, ys[x - x0], color)}} else {// Line is vertical-ish// Make sure y0 < y1if y0 > y1 {swap(P0, P1)}xs = Interpolate(y0, x0, y1, x1)for y = y0 to y1 {canvas.PutPixel(xs[y - y0], y, color)}}
}

示例 6-2:使用 InterpolateDrawLine 版本


这个 DrawLine 可以正确处理所有情况(图 6-5)。

图 6-5:重构算法正确处理所有情况


您可以在以下位置查看此重构算法的现场演示https://gabrielgambetta.com/computer-graphics-from-scratch/demos/raster-02.html


虽然这个版本并不比前一个版本短很多,但它清晰地将 y y y 和 x x x 的中间值的计算与决定哪个是自变量以及像素绘图代码本身分开。

令人惊讶的是,这种线算法并不是最好的或最快的。 这种区别可能属于 Bresenham 算法。 提出这个算法的原因是双重的。 首先,它更容易理解,这是本书的首要原则。 其次,它为我们提供了 Interpolate 函数,我们将在本书的其余部分广泛使用它。


五、Summary - 概括

在本章中,我们已经迈出了构建光栅化器的第一步。 使用我们唯一的工具 PutPixel,我们开发了一种可以在画布上绘制直线段的算法。

我们还开发了插值辅助方法,这是一种有效计算线性函数值的方法。 在继续之前确保你理解它,因为我们会经常使用它。

在下一章中,我们将使用 Interpolate 在画布上绘制更复杂和有趣的形状:三角形。


Computer Graphics From Scratch - Chapter 6相关推荐

  1. Computer Graphics From Scratch - Chapter 3

    系列文章目录 简介: Computer Graphics From Scratch-<从零开始的计算机图形学>简介 第一章: Computer Graphics From Scratch ...

  2. Computer Graphics From Scratch - Chapter 5

    系列文章目录 简介:Computer Graphics From Scratch-<从零开始的计算机图形学>简介 第一章: Computer Graphics From Scratch - ...

  3. Computer Graphics From Scratch - Introduction

    <Computer Graphics From Scratch><从零开始的计算机图形学> Introduction 简介 一.这本书是给谁看的? 二.本书涵盖的内容 三.为什 ...

  4. HDU4716 A Computer Graphics Problem

    问题链接:HDU4716 A Computer Graphics Problem.入门练习题,用C语言编写. 题意简述:根据输入的数(代表剩余电量,值范围是0到100),打印一个反映剩余电量的字符图案 ...

  5. Mathematics for Computer Graphics

    Mathematics for Computer Graphics 最近严重感觉到数学知识的不足! http://bbs.gameres.com/showthread.asp?threadid=105 ...

  6. Vector Math for 3D Computer Graphics

    2019独角兽企业重金招聘Python工程师标准>>> http://programmedlessons.org/VectorLessons/vectorIndex.html#09 ...

  7. 计算机图形(Computer Graphics)经典书籍推荐(1)

    这些书都是非常非常经典!!!!! 1- An Introduction to Ray Tracing. 1989 2- Physically Based Rendering_From Theory T ...

  8. 计算机图形学多边形填充代码_计算机图形学 Computer Graphics (第一周笔记及课件翻译)...

    本文使用 Zhihu On VSCode 创作并发布 注:本文部分内容源自于UDE课程 Computer Graphics(Prof. Dr. Jens Krüger),仅供本人自己学习与作为课程笔记 ...

  9. GAMES101笔记_Lec01_计算机图形学概述 Overview of Computer Graphics

    作为一名想要了解图形学的学生,已经在无数地方看到有人推荐闫令琪老师的GAMES101课程,但由于自己是美术专业,在笼统看过这门课程之后认为这门课有一定学习难度,所以为了打下比较扎实的基础和方便自己日后 ...

最新文章

  1. Contos7 克隆实例 以及 配置网络-服务-等相关信息
  2. modelsim的destbench模型1
  3. keras 与tensorflow绑定在一起用的,何以见得
  4. Android Studio 快捷键使用说明
  5. 高级C语言教程-C语言函数setjmp()函数
  6. 网易云信12月大事记
  7. GDI+ 设置不同的分辨率来显示不同大小的图片
  8. linux安装Git依赖的包出错,技术|Linux有问必答:如何在Linux上安装Git
  9. Linux文件属性及如何修改文件属性
  10. [转载] Python快速编程入门课后程序题答案
  11. 【学堂在线数据挖掘:理论方法笔记】第八天(4.2)
  12. 关于vim复制剪贴粘贴命令的总结-转
  13. 软件测试岗位职责和划分
  14. 教孩子编程python 语言 nostarch 下载_教孩子学编程 Python语言版
  15. 阿铭Linux_传统IDC 部署网站学习笔记20190125
  16. 【Oracle 管理员账号密码忘记的快速解决方法!十分细节!强烈建议收藏!!!】
  17. 在OSM上下载历史数据
  18. 香港十大炒黄金交易公司排名2020版一览
  19. 在网页上打印时用javascript设置打印区域和不打印区域,分页等
  20. JAVA基础----终弄清java核心技术卷1中的int fourthBitFromRight = (n 0b1000 ) / 0b1000;是怎么回事了。。。

热门文章

  1. 瓦努阿图旅游注意事项你了解吗
  2. html 数字自动换行,css怎么让连续数字字母强制换行?
  3. iOS之iPhone手机通讯录和短信搜索界面的实现以及UISearchController和UISearchDisplayController的浅析
  4. linux执行lsof命令_linux lsof命令详解
  5. android 拉取traces.txt分析ANR
  6. 前端 表单校验的使用和实现
  7. 【BZOJ2393】Cirno的完美算数教室
  8. 二度云自助建站-一个不需要写代码,不需要懂设计的建站系统,轻松就能搭建出你想要的网站
  9. Project Anarchy 概要
  10. 零基础入门UI设计,如何提升审美能力呢?