一篇文章搞懂到底什么是渲染流水线

本文实际上是《Unity Shader入门精要》一书的读书笔记,书中关于渲染流水线的讲解清楚易懂,非常适合作为Shader学习的入门书籍。自知好记性不如烂笔头,遂将相关内容再结合自己的一些理解写作这篇博客记录下来。

我们将图像绘制的流程称为渲染流水线,是由CPU和GPU协作完成的。一般一个渲染流程可以分成3个概念阶段,分别是:应用阶段(Application Stage),几何阶段(Geometry Stage),光栅化阶段(Rasterizer Stage)。

应用阶段

应用阶段是在CPU中进行的,主要任务是准备好场景数据,设置好渲染状态,然后输出渲染图元,即为下一阶段提供所需的几何信息。什么是图元?图元是指渲染的基本图形,通俗来讲图元可以是顶点,线段,三角面等,复杂的图形可以通过渲染多个三角形来实现。
应用阶段可细分为3个子阶段

  1. 把数据加载到显存中。所有渲染所需的数据都需要从硬盘加载到系统内存中(RAM),然后网格和纹理等数据又被加载到显存(VRAM)。这是因为显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。
  2. 设置渲染状态。比如设置使用的着色器,材质,纹理,光源属性等。
  3. 调用Draw Call。Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息,这是因为我们已经在上一个阶段设置过了。当给定了一个Draw Call时,GPU就会根据渲染状态和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。

几何阶段

几何阶段是在GPU上进行的,主要任务是输出屏幕空间的顶点信息。几何阶段用于处理从上一阶段接收到的待绘制物体的几何数据(可以理解为Draw Call指向的图元列表),与每个渲染图元打交道,进行逐顶点,逐多边形的操作。几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅化器进行处理。通过对输入的图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标,每个顶点对应的深度值,着色等相关信息。

光栅化阶段

这一阶段也是在GPU上执行的,将会使用上个阶段传递的数据来产生屏幕上的像素,并输出最终的图像。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标,顶点颜色等)进行插值,然后再进行逐像素处理。可以这样理解,几何阶段只是得到了图元顶点的相关信息,例如对于三角形图元,得到的就是三个顶点的坐标和颜色信息等。而光栅化阶段要做的就是根据这三个顶点,计算出这个三角形覆盖了哪些像素,并为这些像素通过插值计算出它们的颜色。

GPU渲染流水线(几何阶段和光栅化阶段)

绿色表示完全可编程控制,黄色表示可配置,蓝色表示由GPU固定实现,不可修改。实线表示必须由开发者编程实现,虚线表示该Shader是可选的。下面我们将分别介绍上图中的主要子阶段。
(顺便提一下,曲面细分着色器可用于细分图元,例如将三角面细分成更小的三角面来添加几何细节。几何着色器可决定输出的图元类型和个数,当输出的图元减少时,实际上起到了裁剪的作用,当输出的图元增多或类型改变时,起到了产生或改变图元的作用)

顶点着色器

顶点着色器的处理单位是顶点,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点和顶点之间的关系,例如我们无法得知两个顶点是否属于同一个三角网格。但正因为这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点,这意味着这一阶段的处理速度会很快。
顶点着色器完成的工作主要有:坐标变换和逐顶点光照。

顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。例如我们可能需要进行逐顶点的光照。
坐标变换,就是对顶点的坐标进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。无论我们在顶点着色器中怎样改变顶点的位置,一个基本的顶点着色器必须要完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。

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

裁剪

裁剪阶段的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片(面片通常是由一个一个更小的图元来构成的)。
一个图元和摄像机视野的关系有3种:完全在视野内,部分在视野内,完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要被裁剪。例如,一条线段的顶点在视野内,而另一个顶点不在视野内,那么在视野外部的顶点应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。

屏幕映射

这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下,这实际上是一个缩放的过程。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。

屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
屏幕映射不会对输入的z坐标做任何处理。实际上,屏幕坐标系和z坐标一起构成了窗口坐标系。这些值会被一起传递到光栅化阶段。

三角形设置

这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。

三角形遍历

三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。像素和片元是一一对应的,每个像素都会生成一个片元,片元中的状态记录了对应像素的信息,是对三个顶点的信息进行插值得到的。

这一步的输出就是得到一个片元序列。需要注意的是一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了但不限于它的屏幕坐标,深度信息,以及其他从几何阶段输出的顶点信息,例如法线,纹理坐标等。

片元着色器

片元着色器用于实现逐片元的着色操作,输出是一个或者多个颜色值(即计算该片元对应像素的颜色,但不是最终颜色)。这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。

根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色
虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。当然导数信息例外。

逐片元操作

逐片元操作阶段负责执行很多重要的操作,例如修改颜色,深度缓冲,进行混合等。
这一阶段有几个主要任务

  1. 决定每个片元的可见性。这涉及了很多测试工作,例如深度测试,模板测试等。
  2. 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

一个片元,只有通过了所有的测试后,才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区。

模板测试

模板测试,可以作为一种丢弃片元的辅助方法,与之相关的是模板缓冲。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取到(使用读取掩码)的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃。如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。另外模板测试还有一些更高级的用法,如渲染阴影,轮廓渲染等。

深度测试

如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是由开发者设置的。通常如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。因为我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模板测试不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。

混合

为什么需要混合?渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是混合需要解决的问题。
对于不透明物体,开发者可以关闭混合操作。但对于不透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。

使用混合函数来进行混合操作。混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加,相减,相乘等。
需要注意的是,上面给出的测试顺序并不是唯一的,对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试。但是,如果将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突。例如,如果我们在片元着色器进行了透明度测试,而这个片元没有通过透明度测试,我们会通过调用API来手动将其舍弃掉。这就导致GPU无法提前执行各种操作。因此现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是,这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。

原文地址:https://www.cnblogs.com/iwiniwin/p/12515439.html

时间: 2024-08-28 06:37:46

一篇文章搞懂到底什么是渲染流水线的相关文章

一篇文章搞懂python2、3编码

说在前边: 编码问题一直困扰着每一个程序员的编程之路,如果不将它彻底搞清楚,那么你的的这条路一定会走的格外艰辛,尤其是针对使用python的程序员来说,这一问题更加显著, 因为python有两个版本,这两个版本编码格式却完全不同,但我们却经常需要兼顾这两个版本,所以出现各种问题的几率就大了很多. 所以在这里我试图用一篇文章来彻底梳理整个python语言的编码问题,尽量降低以后在这方面举到问题的可能性. ps 此文一定程度上参考和引用了alex的博客:“https://www.cnblogs.co

一篇文章搞懂DataSet、DataFrame、RDD-《每日五分钟搞定大数据》

1. 三者共性: 1.RDD.DataFrame.Dataset全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利 2.三者都有惰性机制,执行trainform操作时不会立即执行,遇到Action才会执行 3.三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出 4.三者都有partition的概念,如 var predata=data.repartition(24).mapPartitions{       PartLine => {     

一篇文章搞懂装饰器所有用法

如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖. 它放在一个函数开始定义的地方,它就像一顶帽子一样戴在这个函数的头上.和这个函数绑定在一起.在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶上这顶帽子,这顶帽子我们称之为装饰函数 或 装饰器. 你要问我装饰器可以实现什么功能?我只能说你的脑洞有多大,装饰器就有多强大. 装饰器的使用方法很固定: 先定义一个装饰函数(帽子)(也可以用类.偏函数实现) 再

一篇文章搞懂DOM

学习JavaScript肯定是会遇到DOM操作,那么什么是DOM?它又是干嘛用的?这篇文章为你揭晓答案. DOM是document object model的缩写,简称文档对象模型. 简单的说DOM是一套对文档的内容进行抽象和概念化的方法.我们可以把HTML文档模型化,当作对象来处理. 基本概念: 文档(document): HTML或XML文件. 节点(node):HTML文档中的所有内容都可以称之为节点,常见的节点有 元素节点 属性节点 文本节点 注释节点. 元素(element): HTM

【朝花夕拾】一篇文章搞懂Android跨进程通信

前言 只要是面试中高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点.Android系统的运行由大量相互独立的进程相互协助来完成的,所以Android进程间通信问题,是做好Android开发高级工程师必须要跨过的一道坎.如果您还对这方面的知识还做不到如数家珍,那就和我一起来攻克它吧! 本文主要包含了如下内容: 其行文脉络大致如下,希望能加深读者对这方面内容的记忆:(1)Android基于Linux系统,所以先说系统进程相关知识和Linux IPC.(2)总结Android的IPC

一篇文章搞懂Android组件化

网上组件化的文章很多,我本人学习组建化的过程也借鉴了网上先辈们的文章.但大多数文章都从底层的细枝末节开始讲述,由下而上给人一种这门技术“博大精深”望而生畏的感觉.而我写这篇文章的初衷就是由上而下,希望别人在阅读的过程中能够觉得“组件化原来也就是这几个东西”的感觉. 首先我们来看一下组件化项目和传统项目的区别 在传统的项目里 我们通常情况下会有一个commonLib的Libary模块和一个app的application模块,业务中的逻辑都写在app中各个功能模块放到不同的包下.这样做有以下几个主要

一篇文章搞懂Nginx是什么,能干什么

Nginx的产生 没有听过Nginx?那么一定听过它的"同行"Apache吧!Nginx同Apache一样都是一种WEB服务器.基于REST架构风格,以统一资源描述符(Uniform Resources Identifier)URI或者统一资源定位符(Uniform Resources Locator)URL作为沟通依据,通过HTTP协议提供各种网络服务. 然而,这些服务器在设计之初受到当时环境的局限,例如当时的用户规模,网络带宽,产品特点等局限并且各自的定位和发展都不尽相同.这也使得

一篇文章搞懂android存储目录结构

前言 前两天因为开发一个app更新的功能,我将从服务器下载的apk文件放在了内部存储目录(测试手机为小米,路径为:data/user/0/packagename/files)下面,然后安装的时候一直安装不了,提示解析包出错.后来查询发现,安装apk是调用了PackageInstaller,没有相关权限,这个无法获取内部路径,所以会安装不了.借机也复习了一遍Android下面存储相关的知识点,特来总结一番. 存储分类 对于Android存储目录,我总结成一张思维导图,如果有需要原图的,请在我的公众

一篇文章搞懂移位运算

前提知识: 1. 计算机中对于有符号数的表示有三种方式,原码,补码,反码. 2. 在Java中,二进制数最高位是符号位,0表示正数,1表示负数: 3. 正数的表示,例如byte/int 数3,  二进制就是 0000 0011,负数的表示稍微麻烦一点(负数在计算机中是以补码的形式存储的) -5 的二进制: 1. -5的绝对值二进制表示  0000 0101 2. 然后求这个数的反码  1111 1010 3. 将反码加1 变成  1111 1011 , 这个就是-5的二进制表示(补码) 移位运算