关于渲染流水线与Shader的理解

关于渲染流水线与Shader的理解

《Unity Shader入门精要》二周目系列之一

​ 一年前开始学Shader方面的知识,靠着冯乐乐著的Shader书入门,也是我这方面知识的启蒙书,上个星期工作时在研究SSS,遇到了一个基础的法线纹理的知识没理解好,掏出了这本书再看一遍,发现对法线纹理没理解好的细节都在书中有所提及,只是第一遍学习没注意到。想来当初刚开始学,书中应该还有很多细节没意识到重要性,由此萌发二刷的想法,二刷这本书,从一个初学者再到有一定学习基础的角度转变,看到的应该跟一年前有所不同,开启本系列记录一周目时被忽略掉或没理解好又特别有用的书中细节。

第1章

程序员的三大浪漫,编译原理、操作系统和图形学。 我们是程序员中的“外貌协会”,期待着用代码编写出一个绚丽多姿的世界。这就是我们的浪漫。

当初就是因为这样一句话入坑的…

第2章

顶点着色器

顶点着色器本身不可以创建或者摧毁任何顶点,而且无法得到顶点与顶点之间的关系。

所以处理顶点创建和摧毁或者顶点与顶点间联系的操作都需要放在CPU进行。
几何着色器是否可以?待验证

把顶点左边转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化设备坐标(NDC)

Vertex Shader

上图的坐标范围为OpenGL同时也是Unity使用的NDC,z分量范围在[-1,1]之间。

DirectX中,NDC的z分量范围是[0,1]。

这在使用的z坐标时需要注意,特别是有时用于深度比较,比如计算水面深度,不仅要注意z分量范围,也要注意转化为线性值去比较。

现代的Shader Model中,顶点着色器可以把数据发送给曲面细分着色器或几何着色器

这点我倒是很好奇,国外的大佬keijiro对这块研究挺深的,列入学习队列hhh。

裁剪

GPU流水线

  • 牢记这个过程,裁剪是在顶点着色器之后的,裁剪前的三个阶段都有可能更改顶点的位置,导致相机能看到的顶点发生变化。
  • 裁剪前,已经将顶点转换到NDC坐标了

屏幕映射

ScreenMapping

  • 屏幕映射将x,y坐标从(-1,1)范围转换到屏幕坐标系中,是一个缩放的过程
  • 屏幕映射对z坐标不会做任何处理
  • 屏幕映射跟在顶点着色器中获取屏幕空间坐标两回事,有时候一些效果需要用到屏幕空间坐标,比如采样深度图需要屏幕空间坐标,可通过o.screen = ComputeScreenPos(o.pos); 此时的o.screen.xy范围为(0,1),
  • 关于 ComputeScreenPos(o.pos)的xyz的值,首先z值无处理,若想要让z值有意义,比如存储该顶点的线性深度,以便与深度纹理的深度进行比较,可在顶点着色器使用宏定义 COMPUTE_EYEDEPTH(o.screen.z); 而xy值的范围并非(0,1),而是 (0,w),所以在采样深度纹理时,需要采用tex2Dproj或者是i.screen.xy/i.screen.w

screenPos

  • 注意OpenGL和DirectX之间的差异,OpenGl从左下开始算0,而DirectX从左上开始计0,同样的,这在采样深度纹理时需要额外注意
    ![Screen Mapping_OpenGL_DirectX](Screen Mapping_OpenGL_DirectX.png)

  • 可以用 #if UNITY_UV_STARTS_AT_TOP 可以用来判断我们是否是在 Direct3D 平台下。关于UNITY_UV_STARTS_AT_TOP 的扩展

三角形设置

  • 这一步开始进入光栅化阶段,光栅化阶段两个重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色
  • 上一步仅得到三角网格的顶点,而三角形设置可以理解为将顶点与顶点连接起来
  • 计算三角网格表示数据的过程就叫做三角形设置,它的输出是为了下一个阶段做准备的

三角形遍历

  • 检查像素是否被三角网格覆盖,如果是则生成一个片元,并使用三角网格3个顶点的信息对整个覆盖区域的像素进行插值
  • 一个片元并不是真正意义上的像素,而是包含了很多状态的合集,这些状态用于计算每个像素最终的颜色。这些状态包括但不限于屏幕坐标、深度信息、顶点信息(法线、纹理坐标)等

片元着色器

  • 在DirectX中,片元(Fragment)着色器被称为像素(Pixel)着色器,但片元着色器是一个更加合适的名字,一个片元并不是真正意义上的像素

  • 片元着色器的局限,仅可以影响单个片元,也就是在执行片元着色器时,不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息(gradient,或者说是derivative)

    • 在纹理采样时会遇到这样的情况,即不同纹理将过渡混合的情况,最常见的Mipmap,以及地形纹理拼接。用tex2D采样时,是有用到偏导信息来进行混合的。参考下面链接:

逐片元操作

  • 渲染流水线最后一步。逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,被成为输出合并阶段(Output-Merger)

  • 这一阶段的主要任务

    • 决定每个片元的可见性
    • 决定颜色
  • 逐片元操作是高度可配置性的,可以配置模板测试,深度测试,混合

    ![Per-fragment Operations](Per-fragment Operations.png)

  • 测试的过程在不同的图形接口(例如OpenGL和DirectX)的实现细节也不尽相同

    ![Stencil Test_Depth Test](Stencil Test_Depth Test.png)

Blending

  • 想要充分提高GPU性能,则尽早知道哪些片元会被舍弃,就不需要再使用片元着色器计算颜色,所以在流水线中能看到深度测试在片元着色器之前。但如果在片元着色器中进行透明度测试,则必须禁用深度测试,这也导致有更多片元需要处理,所以这就是透明度测试会使性能下降的原因。

  • GPU会采用双重缓冲(Double Buffering)的策略,避免我们看到正在进行光栅化的图元

一些容易困惑的地方

  • CPU、OpenGL/DirectX、显卡驱动和GPU之间的关系

OpenGL和DirectX

  • 一个常见的误区:Draw Call 中造成性能问题的元凶是GPU,认为GPU的状态切换是耗时的,其实不是,正在“拖后腿”的其实是CPU

CPU和GPU是如何实现并行工作的?

  • 要让CPU和GPU并行工作,解决方案是使用一个命令缓冲区(Command Buffer)

  • 命令缓冲区包含一个命令队列,CPU加命令,GPU读命令,两者互相独立。 CPU需要渲染时,添加命令,GPU在完成上一次渲染任务后,再从命令队列中取一个命令执行

  • 命令有很多种类,Draw Call是一种,其他命令还有改变渲染状态(Set Pass,这也是性能消耗的大头,考虑合批来减少)等

    CommandBuffer

为什么Draw Call多了会影响帧率?

  • 每次再调用Draw Call前,CPU要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作,例如检查渲染状态等。而GPU渲染能力是很强的,渲染200个或2000个都没什么区别,渲染速度快于CPU提交命令的速度。如果Draw Call太多,CPU会把大量的时间花费在提交Draw Call上,造成CPU的过载

    SmallCommand

如何减少Draw Call?

  • 合批(Batching)

    • 静态合批
    • 动态合批
    • GPU Instancing
  • 为了减少Draw Call开销,有两点需要注意:

    • 避免使用大量很小的网格。(但目前大量相同的网格可以使用GPU Instancing技术进行合批,但有一定限制条件)
    • 避免使用过多的材质。可以在不同网格之间共用一个材质(减少Set Pass)

总结:什么是Shader?

  • Shader是渲染流水线中的一些阶段,是渲染流水线的一部分
  • 具体来说,Shader是:
    • GPU流水线上一些可高度编程的阶段,由着色器编译出来的最终代码是会在GPU上运行的
    • 有一些特定类型的着色器,如顶点着色器、片元着色器等
    • 依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染
    • 可在Shader中设置适当的渲染状态,使用合适的渲染函数,开启还是关闭深度测试/深度写入等

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×