3.11.3像素着色器阶段处理-简单示例
现在我们继续考虑如何使用像素着色器阶段来实现渲染模型。此阶段的主要职责是生成将融合到管道末端绑定的渲染目标中的输出颜色。因此,开发人员必须做出的最终决定是执行什么类型的计算,以及为该计算输入什么数据。算法的类型可以从渲染的所有像素的单色输出,一直到在执行最终渲染之前需要几个模拟步骤的完整全局照明系统。
为了探索如何实现各种算法,我们将首先检查一个简单的渲染示例,并了解如何使用像素着色器来制定渲染的颜色输出。在此过程中,我们将解释此示例场景如何表示像素着色器操作的基本方法。通过深入了解这些概念,我们将能够实现各种各样的算法,包括一些甚至不用于生成颜色数据的算法,如渲染图。实现的细节不如阶段中如何执行处理的细节重要。
如上所述,像素着色器负责生成最终颜色值,这些颜色值将合并到管道末端的渲染目标中。在这个示例场景中,颜色确定将是对象的材料特性以及材料与之相互作用的一些环境特性的产物,例如光的存在。当在不同的照明条件下查看时,具有相同材质特性的对象可能会显得非常不同,就像具有不同材质特性的模型即使在相同的照明条件中渲染也可能显得不同一样。因此,必须使这两组属性可用于像素着色器程序,以正确确定要生成的颜色。
材料属性
对象的材质特性决定了它在基本级别上的外观。典型的材料特性包括物体的基本颜色、描述表面反射程度的一些系数,以及表示精细表面颜色变化的纹理贴图。我们将假设像素着色器接收顶点法向量和纹理坐标作为插值输入属性。
pixelshader程序将使用清单3.23中所示的函数计算对象材质的颜色。
float4 PSMAIN( in VS_OUTPUT input ) : SV_Target
{
// Determine the color properties of the surface from a texture
float4 SurfaceColor = ColorTexture.Sample(LinearSampler, input.tex );
// Return the surface color
return( SurfaceColor * input.color );
}
在典型的场景中,需要渲染许多不同的对象,并且每个对象都需要与其他对象至少部分不同的属性。对于本例,我们将假设像素着色器在每个渲染的对象中是通用的。考虑到这一点,我们可以逐步完成几个对象渲染序列,并考虑每个序列在像素着色器方面的区别。我们将假设场景中有三个对象,称为对象A、B和C。对于要渲染的每个对象,在使用其中一个绘制调用执行管道之前,必须使用对象属性配置管道。
当管道配置为渲染对象A时,对象的纹理将绑定到具有着色器资源视图的管道,并且通过将常量缓冲区绑定到包含适当颜色的像素着色器阶段来指定其颜色。每个生成的纹理都被传递到像素着色器,在那里对纹理进行采样并乘以常量缓冲区中提供的颜色,如清单。管道执行完成后,应用程序将为渲染对象B配置管道。这将需要绑定不同的纹理和颜色常量缓冲区,然后执行管道。对象C将遵循相同的模式,但在这种情况下,它将使用与对象B相同的纹理,并且只需要一个新的颜色常量缓冲区。
这个简短的序列演示了对象的材质特性通常作为着色器程序本身外部的资源提供。模型的颜色和纹理都由操作像素着色器阶段状态的应用程序控制,而不是通过交换像素着色器程序来控制。此外,通过管道的几何形状对材料特性并不重要。在这三种情况下,输入的几何图形都可以在对象之间交换,因此材质的外观将保持不变。这是因为像素着色器是在管道中的几何数据转换为光栅化形式后进行操作的。它处理光栅化后的数据,因此不受几何图形变化的影响。记住这些属性的同时,我们接下来将考虑如何通过向示例中添加照明来使示例对象与其环境相互作用。
光照属性
通过在场景中提供照明信息,我们可以显著提高其质量。照明是我们看待物理世界的一个非常基本的部分,使用它可以显著改善生成的场景渲染。在现代实时渲染中使用了许多不同类型的光源表示,但就本示例而言,我们将使用一个简单的定向光源模型。光将由方向矢量和颜色来描述,这两者都将在第二恒定缓冲器中提供。到达给定表面的光量通常通过取表面法向量和表示光传播方向的向量的乘积来近似。这会产生一个在[0.0,1.0]范围内的标量值(只要法线向量和光向量都被归一化),并可用于缩放应用于曲面的光量。清单3.24中提供了一个执行此操作的函数。
float4 PSMAIN( in VS_OUTPUT input ) : SV_Target
{
//归一化世界空间法线和光向量
float3 n = normalize( input.normal );
float3 1 = normalize( input.light );
// 计算到达这个碎片的光量
float4 Illumination = max(dot(n,l),0) + 0.2f;
// 确定纹理确定曲面的颜色特性
float4 SurfaceColor = ColorTexture.Sample( LinearSampler, input.tex );
// 返回由照明调制的表面颜色
return( SurfaceColor * input.color * Illumination );
}
现在,我们可以返回到对象A、B和C的样例渲染。渲染顺序将重复,但照明信息必须在第二个常量缓冲区中提供给像素着色器。由于这些对象中的每一个都位于同一场景中,因此它们都使用相同的灯光描述。这意味着所有管道执行都可以重用相同的常量缓冲区。为了实现该示例,以与以前相同的方式为每个对象配置管道,只是还绑定了照明常数缓冲区。当像素着色器执行时,将根据作为输入属性传递到像素着色器的法线向量和光方向向量,以与以前相同的方式找到对象的材质颜色,并计算每个像素位置的可见光量。然后使用照明计算的结果来调制片段的颜色。当渲染每个附加对象时,会对其每个材质特性精确重复该过程,并且生成的渲染现在除了材质颜色外还包含灯光。
在这种情况下,我们可以看到整个场景中的照明信息是相同的,因此以相同的方式应用于我们的三个对象中的每一个。环境数据对于共享同一环境的所有对象都是相同的。我们还看到,尽管三个物体中的每一个都有不同的材料外观,但它们都以相同的方式与光相互作用,使用相同的计算,无论它们是什么颜色。
总结
那么,我们从这个例子中学到了什么?如何利用这些简单实验的结果来理解使用像素着色器实现渲染技术的一般概念?像素着色器程序只是一个函数,它接受一定数量的输入参数并生成颜色。有些输入在每次管道执行时都会更改,例如材质属性,有些输入在每个渲染帧更改一次,例如照明属性。还有一些在应用程序的整个生命周期中根本不会更改,例如模型的顶点法线向量。像素着色器提供的灵活性现在变得非常清晰。计算生成片段颜色的函数是什么并不重要。只要它产生的颜色结果在输入变化时适当变化,渲染模型就可以达到其目的。
开发不同的渲染模型围绕着决定应该使用哪些输入来计算输出颜色,开发执行从输入到输出的映射的函数,然后生成适合渲染模型的几何内容。更复杂的渲染模型可能需要更多的输入,包括使用动态生成的输入的可能性,例如附加渲染过程的结果。但是,像素着色器本身始终解析为将输入数据转换为输出颜色的方法。