Advertisement

Computer Graphics From Scratch - Chapter 4

阅读量:

系列文章目录

简介部分:《从零开始的计算机图形学》简介部分
第1章:《从零开始的计算机图形学》第1章 基础概念介绍
第2章:《从零开始的计算机图形学》第2章 基础光线追踪技术
第3章:《从零开始的计算机图形学》第3章 光照效果与技术


Chapter 4

  • 文章目录

    • 阴影与反射
    • 第1章、阴影 - Shadow Analysis
      • 1.1 解析阴影的本质 - Understanding Shadows in Depth
        • 1.2 辍制与视觉效果提升 - Achieving Enhanced Visual Effects Through Shading Techniques
  • 二、反思(Reflections)- 反思

      • 2.1. 镜子与反思(Mirrors and Reflection)- 镜像与反思
      • 2.2. 使用反思进行渲染(Rendering with Reflection)- 使用反身性进行渲染技术
    • 三、概括


Shadows And Reflections

We are dedicated to rendering scenes with an increasing degree of realism. The previous chapter focused on modeling how light interacts with surfaces. This chapter will delve into two critical aspects of light interaction: shadow projection by objects and reflection dynamics between objects.

我们持续采用越来越逼真的技术来呈现场景细节。 在上一章中, 我们深入探讨了光与表面的相互作用机制。 本章将深入研究光与场景交互的两种基本机制: 产生阴影效应以及实现光的反射传递。


一、Shadows - 阴影

在什么地方存在灯光与物体时,在那个地方就会出现阴影。 我们拥有照明设备以及相关的物體。 那麼我們的陰影出現在哪儿?

1.1 Understanding Shadows - 了解阴影

让我们从最基本的问题入手。 那么问题来了:为什么会有影子? 当光线无法照射到某个物体时就会出现影子 原因在于其他物体阻挡了光线

在上一章所述内容里,在讨论光源与表面相互作用时仅专注于它们之间的局限性关系。而导致遗漏了场景中发生的所有其他事件的影响。为了生成阴影效果,则必须采用一种更为全面的视角,并考察光源、待绘制的表面以及场景中存在的其它物体之间的相互作用。

就概念而言,默认情况下我们的操作相对比较简单。 想要加入一点逻辑的话,请注意以下条件:当光线与物体接触时,则无需考虑来自该光源的光照影响。


我们要区分的两种情况如图 4-1 所示。

在这里插入图片描述

图 4-1:只要光源和该点之间有物体,就会在该点上投射阴影。


事实证明,我们已经拥有了执行此操作所需的所有工具。

让我们从定向光开始。 我们知道点P; 这就是我们感兴趣的点。我们知道向量\vec{L}; 这是光定义的一部分。
知道了P\vec{L},我们可以定义一条射线,即P + t\vec{L},从表面上的点到无限远的光源。
这条射线是否与任何其他物体相交? 如果不是,那么点和光之间就没有任何东西,所以我们像以前一样计算这个光的照明。 如果是这样,则该点处于阴影中,因此我们忽略此光的照明。

我们已有能力计算光线与球体之间的最短距离 :我们采用 TraceRay 函数以追踪来自相机的所有光线。 我们可以通过重用(或共用)这些结果来计算光线与场景其余部分之间的最短距离

但是,此函数的参数略有不同:

  1. 光线并非从视点出发,而是起始于点P。
  2. 方向并非由向量(\overrightarrow{VO})确定,而是由\vec{L}决定。
  3. 为了避免后方物体遮挡其表面并使其产生影子,则必须设定t_{min} = 0
  4. 对于无限远处的定向光源(infinite directional light),即使被照射物体距离视点P极其遥远依然会产生投影,则最大投影深度t_{max}应设为趋近于正无穷大。

图4-2展示了两个关键点P_0P_1的位置关系。当从P_0出发沿着光线方向进行追踪时,计算结果表明没有任何物体与其相交这一现象;这表明光线能够直接到达P_0位置而无需经过任何遮挡物。然而,在分析P_1的情况时我们发现与球体存在两个不同的相交位置其中参数t>0(这表示这些交点位于球体表面与光线之间)。因此在t>0的情况下该区域将被遮挡从而导致该区域将被遮挡进而使得该区域处于阴影状态

在这里插入图片描述

图 4-2:球体在 P_1 上投射阴影,但不在 P_0 上。


我们可以用本质上相同的方式处理点光源系统,然而存在两个例外情况。
一方面,在这种情况下\vec{L}并不是一个常数向量,但根据给定的参数P以及光源的位置我们仍然能够对其进行计算。
另一方面,在远离灯光的物体不会在投影面P上产生阴影这一情况下,则需要设定t_{max}=1以便让光线在灯光位置处自然终止。

图4-3展示了这些情况。当我们将光线从P_0投向L_0方向照射时,我们发现与小球体相交的位置;然而这些交点对应的参数t均大于1,说明这些交点并不位于光线与P_0之间因此我们可以忽略这些情况。由此可见P_0不在阴影区域内。另一方面当我们将光线从P_1投向L_1方向照射时大球体会与之相交且满足条件为0 < t < 1从而导致大球体会在该区域投下阴影

有必要考察所有可能的边界条件。 考察射线方程的形式为 P + t\vec{L} 。 假设我们从参数值 t_{min} = 0 开始搜索交点,则在 t=0 处即可找到该几何体所在位置的一个交点! 已知当 t=0 时有 P + 0 \cdot \vec{L} = P ,也就是说,在其自身位置处该几何体会投下一个自身阴影。

在这里插入图片描述

图 4-3:我们使用交点处的 t 值来确定它们是否在该点上投下阴影。


最简单的解决方案是将t_{min}设定为一个适当的小正值\epsilon而非零值。
几何上而言,则希望光线从包含点P的表面开始延伸而非直接穿过点P。
由此可得范围为:对于定向光束而言为[\epsilon, +\infty);而对于点光源则限定在区间[\epsilon, 1]内。

为了简化光线与 P 所属球体的交点计算问题而采取的方法可能具有吸引力。这种方法仅限于球体这一简单几何形状的应用范围之外。例如,在防护观察者免受阳光直射的情况下, 观察者的某一部分(如手掌)会投射出阴影到其表面上的不同区域——即身体的不同部位。


1.2. Rendering with Shadows - 使用阴影渲染

让我们把上面的讨论变成伪代码。

在早期版本中,默认情况下使用TraceRay进行光线球体交点的确定,并随后针对每个交点进行光照模拟。为了提高效率和复用性,在上一阶段我们提取并保存了最近的一组光线与曲面相交的数据,并将其用于后续阴影渲染。

复制代码
    ClosestIntersection(O, D, t_min, t_max) 
    {
    	closest_t = inf
    	closest_sphere = NULL
    	for sphere in scene.Spheres 
    	{
    		t1, t2 = IntersectRaySphere(O, D, sphere)
    		if t1 in [t_min, t_max] and t1 < closest_t 
    		{
    			closest_t = t1
    			closest_sphere = sphere
    		}
    		if t2 in [t_min, t_max] and t2 < closest_t 
    		{
    			closest_t = t2
    			closest_sphere = sphere
    		}
    	}
    	return closest_sphere, closest_t
    }

示例 4-1:计算最近的交点

我们可以重写 TraceRay 以重用该函数,并且生成的版本要简单得多(示例 4-2)。

复制代码
    TraceRay(O, D, t_min, t_max) 
    {
    	closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)
    	if closest_sphere == NULL 
    	{
    		return BACKGROUND_COLOR
    	}
    	P = O + closest_t * D
    	N = P - closest_sphere.center
    	N = N / length(N)
    	return closest_sphere.color * ComputeLighting(P, N, -D, closest_sphere.specular)
    }

示例4-2: 分解 ClosestIntersection 后的更简单版本的 TraceRay


然后,我们需要将阴影检查❶添加到 ComputeLighting(示例 4-3)

复制代码
    ComputeLighting(P, N, V, s) 
    {
    	i = 0.0
    	for light in scene.Lights 
    	{
    		if light.type == ambient 
    		{
    			i += light.intensity
    		} 
    		else 
    		{
    			if light.type == point 
    			{
    				L = light.position - P
    				t_max = 1
    			} 
    			else 
    			{
    				L = light.direction
    				t_max = inf
    			}
    			
    			// Shadow check
    			❶ shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
    			if shadow_sphere != NULL 
    			{
    				continue
    			}
    			
    			// Diffuse
    			n_dot_l = dot(N, L)
    			if n_dot_l > 0 
    			{
    				i += light.intensity * n_dot_l / (length(N) * length(L))
    			}
    			// Specular
    			if s != -1 
    			{
    				R = 2 * N * dot(N, L) - L
    				r_dot_v = dot(R, V)
    				if r_dot_v > 0 
    				{
    					i += light.intensity * pow(r_dot_v / (length(R) * length(V)), s)
    				}
    			}
    		}
    	}
    	return i
    }

示例 4-3:具有阴影支持的 ComputeLighting


图 4-4 显示了新渲染的场景的样子。

在这里插入图片描述

图 4-4:光线追踪场景,现在有阴影。

在[https://gabriel gambetta.com/cgfs/shadows-demo](https://gabrielgambetta.com/computer- graphics-from-scape/demos/raytracer-04.html)上可以看到该算法的实际运行情况。
在该演示中, 您可以选择从时间参数t=0开始还是t=\epsilon开始跟踪光线, 以便更清晰地观察到这种差异的影响。

当前阶段我们取得了令人瞩目的进展。
场景中的物体之间呈现出更为真实的互动关系,
相互投射阴影
随后我们将深入研究更多物体间的互动机制——这些机制将帮助我们更好地理解整体系统的行为模式。


二、Reflections - 反射

在上一章里探讨了具有镜面效果的部分;然而这些表面虽然光滑如镜面却只能反射出反射光而无法呈现真实影像这种现象确实存在


2.1. Mirrors and Reflection - 镜像和反射

我们来探讨一下镜子的工作原理。 观察镜子时,在镜面上发生的是光的反射现象。 光线遵循对称定律在表面法线方向上进行反射(如图4-5所示)。

在这里插入图片描述

图 4-5:一束光线以与镜子法线对称的方向从镜子反射。


假设我们正在追踪一条光线,并注意到最近的一次交叉是一个镜子表面。那么这条光线呈现出什么颜色呢?由于我们观察到的是反射光而非镜面本身的颜色特性,在这种情况下我们需要确定这条光线来自何处以及它的具体颜色特征。因此我们的任务就是通过计算反射光线的方向并由此确定相应入射方向上的光源颜色

如果我们有一个函数,给定一条射线,返回来自它的方向的光的颜色 . .

等一下! 我们确实有一个,它叫做 TraceRay

在主循环中处理每一个像素时,在图像生成的过程中会生成一条从相机指向场景的射线,并通过调用 TraceRay 函数确定光线与场景交互的颜色。 当遇到镜子时(即返回的结果是一面镜子),程序只需计算反射光线的方向,并进一步确定来自该方向光线的颜色。 这种情况下的处理逻辑相对简单,并且可以通过函数自身即可完成这一操作。

此处我希望你能重新通读一下最后一部分,并且要确保自己彻底理解了内容。假如这是您首次接触递归光线追踪相关内容,请不要感到惊讶——或许会需要花费一些时间去深入理解这些概念。


继续,我会等待! 时刻已经开始减弱,让我们正式化一下。

当我们构建一个递归算法(自我调用的算法)时, 我们必须确保不会导致死循环(亦称"此程序已停止响应. 您要终止它吗?")。
该算法有两种退出途径: 光线在非反射物表面发生反射后离开; 光线未能与任何物体发生碰撞后结束运行.
然而存在一个典型案例会陷入死循环——无限霍尔效应现象. 当你将一面镜子置于另一面镜子前进行观察时会发生这种情况——镜中会出现你的无限副本.

避免无限递归的方式多种多样。为了避免这种情况,在算法中仅引入递归限制即可实现深度控制。
为了限制算法的深度,我们仅在其中引入递归限制。
记作 r:
当 r 等于 0 时,观察到物体但未检测到反射;
而当 r 等于 1 时,则不仅看到物体还能捕获其表面反射(如图 4-6所示)。


在这里插入图片描述

图 4-6:仅在单层递归调用下发生反射(r = 1)。我们观察到球体在其表面发生镜面反射现象,并且被反射的球体本身并不呈现镜面效果。

图 4-6:仅在单层递归调用下发生反射(r = 1)。我们观察到球体在其表面发生镜面反射现象,并且被反射的球体本身并不呈现镜面效果。


当参数 r 设为 2 时(此处可考虑将 "设为" 改为 "取值" 或 "设置为"),我们观察到物体及其反射情况(对于更大的 r 值而言,则会呈现不同的现象)。如图4-7所示(此处可考虑将 "显示了" 改为 "展示了" 或 "呈现了"),当参数 r 增加至3时,则会得到相应的结果。通常情况下(此处可考虑将 "一般来说" 改为 "一般而言" 或 "通常来说"),进一步深入三个层级的效果变化不大。

在这里插入图片描述

图 4-7:反射限制为三个递归调用(r = 3)。 现在我们可以看到球体反射的反射。


在每个表面上标注一个介于 01 之间的数值来表示物体表面的反射等级。随后我们以该数值为权重求取局部照明颜色与反射颜色的加权平均值

最后,递归调用 TraceRay 的参数是什么?

  • 光线始于物体表面的点 P
  • 在程序中我们用 \vec{D} 表示入射向量,则反射向量为 -\\vec{D} 关于法向量 N 的镜像。
  • 类似处理阴影的方法, 我们令t_{min} = ϵ, 以避免自反射现象。
  • 我们愿意接受来自任何距离的反射体, 因此t_{max} = +∞.
  • 为了避免无限递归, 允许的最大递归深度比当前设置少一单位。

现在我们准备把它变成实际的伪代码。


2.2. Rendering with Reflections - 用反射渲染

为光线追踪器增加反射属性。
然后,在修改场景定义时
我们会设置每个表面的镜面效果的程度
0.0 表示其不具有镜面效果
1.0 表示完美镜面效果:

复制代码
    sphere {
    	center = (0, -1, 3)
    	radius = 1
    	color = (255, 0, 0) # Red
    	specular = 500 # Shiny
    	reflective = 0.2 # A bit reflective
    }
    sphere {
    	center = (-2, 1, 3)
    	radius = 1
    	color = (0, 0, 255) # Blue
    	specular = 500 # Shiny
    	reflective = 0.3 # A bit more reflective
    }
    sphere {
    	center = (2, 1, 3)
    	radius = 1
    	color = (0, 255, 0) # Green
    	specular = 10 # Somewhat shiny
    	reflective = 0.4 # Even more reflective
    }
    sphere {
    	color = (255, 255, 0) # Yellow
    	center = (0, -5001, 0)
    	radius = 5000
    	specular = 1000 # Very shiny
    	reflective = 0.5 # Half reflective
    }

我们已在镜面反射期间应用了**'反射光线方程'**这一工具;因此我们可以对其进行分解分析。该方法基于射线\vec{R}和法向量\vec{N}运算,并返回经法向量\vec{N}反转变换后的射线\vec{R}

复制代码
    ReflectRay(R, N) 
    {
    	return 2 * N * dot(N, R) - R;
    }

在修改ComputeLighting时的主要变化是将反射计算公式取代为调用新的ReflectRay函数。

main方法中存在一项微小的改进——该递归界限必须被传递给顶层的TraceRay函数调用

复制代码
    color = TraceRay(O, D, 1, inf, recursion_depth)

如前面所述,我们可以将 recursion_depth 的初始值设置为合理的值 ,例如 3

转变仅在TraceRay结束时发生。我们通过递归的方式处理了光线的反射。您可以在示例4-4中看到这一更改。

复制代码
    TraceRay(O, D, t_min, t_max, recursion_depth) 
    {
    	closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)
    	if closest_sphere == NULL 
    	{
    		return BACKGROUND_COLOR
    	}
    	
    	// Compute local color
    	P = O + closest_t * D
    	N = P - closest_sphere.center
    	N = N / length(N)
    	local_color = closest_sphere.color * ComputeLighting(P, N, -D, closest_sphere.specular)
    	
    	// If we hit the recursion limit or the object is not reflective, we're done
    	❶ r = closest_sphere.reflective
    	if recursion_depth <= 0 or r <= 0 
    	{
    		return local_color
    	}
    	
    	// Compute the reflected color
    	R = ReflectRay(-D, N)
    	❷ reflected_color = TraceRay(P, R, 0.001, inf, recursion_depth - 1)
    	❸ return local_color * (1 - r) + reflected_color * r
    }

示例 4-4:光线追踪器伪代码,现在带有反射。


修改代码相对容易。
第一步我们需要评估是否需要计算反射步骤❶。
如果球体不发生反射现象或系统达到递归限制条件,则算法终止并返回当前球体的颜色值。

这种现象表现为TraceRay函数采用适当设置的自反参数来实现自我调用;重要的是该算法中设置了逐步减少的递归深度计数器;此方法借助相关技术手段来避免无限循环。

当我们获得了球体表面各点处的局部颜色信息以及对应的反射光方向时,在此基础上我们将这些颜色值进行融合处理 使用"该球体表面点处的反射强度"这一参数作为混合权重 以实现对整体着色效果的精确控制

我会让结果自己说话。 查看图 4-8。


在这里插入图片描述

图 4-8:光线追踪场景,现在有反射。

https://gabrielgambetta.com/cgfs/reflections-demo 这个网站链接上,您可以访问到该算法的在线演示版。


三、概括

在前面的章节中,我们构建了一个基础模块,在二维画布中呈现三维场景,并模拟光与物体表面的互动过程。这为我们提供了构建复杂三维场景的基础模型。

在本章里,我们对这一框架进行了扩展,并非仅是为了使其能够与光线交互作用那么简单。事实上,在本章中我们对其进行了深化改进,并使它不仅能够与光线交互作用、还能通过彼此投射阴影以及进行反射互动来实现更为复杂的场景模拟。这样一来,在渲染过程中所呈现的场景更加逼真且具沉浸感。

在下一部分中, 我们将对扩展这项工作的不同方法进行简要探讨, 超出球体范围的对象及其相关因素将是重点关注对象。


全部评论 (0)

还没有任何评论哟~