概述
什么是管线(Pipeline)?
管线我们可以理解为流水线,流水线大家都懂,就是工厂里面生产东西的一整套流程。例如饮料厂要生产饮料,需要洗瓶子,倒饮料,加盖,包装等等,这一整套过程就是流水线。那么我们要把一个三维场景渲染成一幅二维的图像,同样需要一个先干嘛再干嘛然后干嘛最后干嘛的过程,这个过程或流水线,我们就称之为管线。
图形/渲染管线(Graphics/ Rendering Pipeline)
通过前面几篇的学习,我们知道了要把一个三维场景渲染成一幅二维的图像大致分为以下几个步骤:
- MVP变换和视口变换,把三维场景变的屏幕空间大小一样
- 光栅化,把物体离散成一个个的像素
- 深度缓存,根据每个像素做深度缓存
- 着色,根据着色频率的不同,不同的着色会在不同的时间点进行。
对于这一套流程,我们就可以称之为实时渲染管线,大致整理一下可以得到如下的流程图:
我们的MVP变换,视口变换实则是对物体表面也就是Mesh中的每个三角形的顶点进行变换,这步操作我们可以称之为 Vertex Processing,操作的内容都是顶点,即Vertex Stream。同时我们之前所说的高洛德着色就是在这里进行操作的,因为高洛德着色处理的就是顶点嘛。
顶点变换好后,我们自然可以通过这些顶点来获得我们想要的三角形,指定三个顶点即可连成一个三角形,即可得到所有的三角形面,即Triangle Stream,这步操作称之为 Triangle Processing。
然后我们就要把这些三角形进行光栅化,离散成一个个像素。在OpenGL中引入了 Fragment 的概念,我们可以理解为一个采样点所覆盖的区域即为一个Fragment,若我们对一个像素进行一次采样,那么Fragment就是一个像素,但是如果我们做MSAA操作,例如在一个像素选取四个点进行采样,那么这个像素就有四个Fragment。每个Fragment都会记录颜色,深度,透明度等信息,本文我们就简单的把一个Fragment当作是一个像素。因此在光栅化操作(Rasterization)中,我们得到的即是Fragment的集合(Fragment Stream)。
然后我们的深度缓存,以及冯氏着色都是针对每个Fragment进行处理的,这些相关操作我们称之为 Fragment Processing。在这里通过重心坐标我们就可以知道每个Fragment对应的颜色,从而得到每个像素对应的颜色。
这样,我们的一个管线就走完了,即可得到我们最终的结果,也就是一幅二维的图像。
当然了,上面的介绍仅仅只是一个大致的说明,实际上的渲染管线还要复杂的多,并且在不同的图形API里面,整个过程也是有所不同的,甚至相同的图形API里,随着版本的迭代,渲染管线也不断的更新。
Shader
上面所说的管线,都是在我们GPU里制定好的,但是在现代的GPU里,允许Vertex Processing和Fragment Processing这两部分是可编程的。也就是说我们可以通过自己写代码来控制它们是怎么着色的,而这部分代码就是Shader。
Shader本身是一个能在GPU上执行的模块,作用在Vertex Processing的我们称之为Vertex Shader,作用在Fragment Processing的我们称之为Fragment Shader。
从基本意义上来说,Shader只是一种把输入转化为输出的程序,也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。Vertex Shader的输入即是顶点的属性,而它的输出往往会作为Fragment Shader的输入,Fragment Shader输出则是一个颜色值,代表每个像素的最终颜色。
我们可以使用我们常说的那些图形API(例如OpenGL和DirectX)来编写Shader,目前主要有3种语言:
- 基于OpenGL的OpenGL Shading Language,简称GLSL。
- 基于DirectX的High Level Shading Language,简称HLSL。
- NVIDIA公司的C for Graphic,简称Cg语言。
OpenGL和DirectX属于敌对的关系,我们可以把GLSL转换为HLSL。而Cg语言(C for Graphic)是为GPU编程设计的高级着色语言,可以被OpenGL和Direct3D广泛支持的图形处理器编程语言。因此Cg语言和GLSL、HLSL并不是同一层次的语言,而是它们的上层,即Cg程序是运行在OpenGL和DirectX标准顶点和像素着色的基础上的。Cg由NVIDIA公司和微软公司相互协作在标准硬件光照语言的语法和语义上达成了一致开发,所以HLSL和Cg其实是同一种语言。虽然它目前还在被使用,但是已停止了更新(https://developer.nvidia.com/cg-toolkit)。
接下来我们来简单的介绍下OpenGL和DirectX它们的渲染管线以及shader。
OpenGL
OpenGL(Open Graphics Library)是一个定义了跨编程语言、跨平台的编程接口规格的专业图形程序接口。它用于二维/三维图像,是一个功能强大,调用方便的底层图形库,是行业领域中最为广泛接纳的2D/3D图形API。OpenGL是一个与硬件无关的软件接口,可以在不同的平台如Windows、Linux、MacOS之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。
官方文档:https://www.khronos.org/opengl/wiki/Main_Page
中文教程:https://learnopengl-cn.github.io/
OpenGL的渲染管线如下,来自:https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview:
有关使用GLSL写Shader的学习,在官方文档里也有介绍,同时也可参考中文教程里的着色器部分。
我们来看一个GLSL写的Fragment Shader的例子:
uniform sampler2D myTexture;
uniform vec3 lightDir;
in vec2 uv;
in vec3 norm;
out vec4 FragColor;
void diffuseShader()
{
vec3 kd;
kd = texture2d(myTexture, uv);
kd *= clamp(dot(-lightDir, norm), 0.0, 1.0);
FragColor= vec4(kd, 1.0);
}
首先是关键字uniform,uniform定义的是全局的变量,其值是外部application程序传递过来的。在例子中,我们用它定义了一个纹理和一个光照方向。
然后是in,out,每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。在例子中,我们用 in 定义了顶点对应的uv和法线,这些值在vertex shader里会 out 出来。然后用 out 定义了该片段最终的输出颜色。
注:在OpenGL 3+,varying 和 gl_FragColor 被弃用(deprecated)
接下来就是函数体了,我们用该函数实现了一个简单的漫反射。函数里我们用texture2d函数根据uv坐标对纹理进行采样,得到的颜色存在kd变量里,这kd即我们漫反射公式里的系数。至于texture2d内部怎么实现的,这个我们不用管,OpenGL为我们做好了。但是通过纹理映射的学习,我们能够懂这块的原理就好了。然后我们把光的方向和法线方向点乘,即Lambert's 余弦定率,得到的值和采样得到的颜色相乘,得到的结果就是我们光照影响后的漫反射结果,将其输出。(注:这里省略了光能量的传播,即少了 I/(r*r) 这一项)
在我们的Shader中,我们并没有用循环来遍历所有的像素来一一着色,这是因为我们Shader里的操作是针对每个顶点和每个像素的,也就是说每个顶点都会执行一遍Vertex Shader,每个像素都会执行一遍Fragment Shader,我们只需要为它们写一个统一的逻辑即可,这样可以最大限度的发挥我们GPU的性能,即可以并行操作多个顶点或像素。
可以发现在这个Fragment Shader里,涉及到的纹理映射,uv,漫反射,还有例如一些上面没提到的转换矩阵,都是图形学中所学习的,因此只要我们学好图形学,再来理解这些图形API,是很容易的。
在OpenGL里,我们不仅可以自定义Vertex Shader和Fragment Shader,我们还可以自定义Tessellation Shader和Geometry Shader。
Tessellation Shader为曲面细分,发生在顶点处理阶段,我们可以用它生成新的顶点和面,即把一个三角形拆分为多个三角形。
Geometry Shader为几何着色器,它的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。也就是说我们可以在这里改变几何的形状。
Direct3D
DirectX(Direct eXtension)是由微软公司创建的多媒体编程接口。被广泛适用于Microsoft Windows、Microsoft XBOX电子游戏开发,并且只能支持这些平台。最新版本为DirextX 12,创建在最新的Windows 10。DirectX是这样一组技术:它们旨在使基于Windows的计算机成为运行和显示具有丰富多媒体元素(例如全色图形、视频、3D动画和丰富音频)的应用程序的理想平台。DirectX包括安全和性能更新程序,以及许多涵盖所有技术的新功能,应用程序可以通过使用DirectX API来访问这些新功能。
可以看出DirectX相比OpenGL不仅支持图形的处理,同时还支持音频视频等的处理。
官方文档:https://docs.microsoft.com/en-us/windows/win32/direct3d
Direct12的渲染管线示意图如下,来自https://docs.microsoft.com/en-us/windows/win32/direct3d12/pipelines-and-shaders-with-directx-12:
用DirectX的GLSL语言同样也可以编写Shader,官方文档:https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl
Unity与Render pipelines
官方文档:https://docs.unity3d.com/Manual/render-pipelines.html
Unity的Render pipelines从大模块来分,有剔除(Culling),渲染(Rendering)和后期处理(Post-processing)三块内容。不同的Render pipelines有不同的能力和表现效果,适用于不同的项目。
Unity目前提供了如下三种Render pipelines:
Built-in Render Pipeline:内置渲染管道,Unity默认的Render pipelines,比较古老,我们很难进行定制,不可编程。
Universal Render Pipeline (URP):通用渲染管道,是一个可编程的Render pipelines,可以自己进行一些定制,让我们在各种平台上快速轻松地创建优化后的图形,适用于做手机游戏。
High Definition Render Pipeline (HDRP) :高清渲染管道,也是一个可编程的Render pipelines,可以在高端平台上创建尖端、高保真的图形,适用于做PC游戏。
除此之外我们也可用Unity的Scriptable Render Pipeline API创建自己的Render pipelines。
并且这些Render pipelines选定一个后很难移植到另一个上去,因为它们可能使用的不同的Shader输出,且可能没有相同的特征。
在每个Render pipelines里面我们可以选择不同的Rendering path,一个Rendering path指的是一系列有关光照和阴影的操作,因此不同的Rendering path也会有不同的表现效果。选择哪个Rendering path主要看我们的项目需求以及硬件设备,常见的Rendering path有如下两种:
Forward Rendering:正向渲染,它是我们渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。它非常容易理解,也很容易实现,但是同时它对程序性能的影响也很大,因为对于每一个需要渲染的物体,程序都要对每一个光源每一个需要渲染的片段进行迭代,这是非常多的!因为大部分片段着色器的输出都会被之后的输出覆盖,正向渲染还会在场景中因为高深的复杂度(多个物体重合在一个像素上)浪费大量的片段着色器运行时间。
Deferred Shading:延迟渲染,为了解决Forward Rendering的问题而诞生了,它大幅度地改变了我们渲染物体的方式。这给我们优化拥有大量光源的场景提供了很多的选择,因为它能够在渲染上百甚至上千光源的同时还能够保持能让人接受的帧率。
参考:https://learnopengl-cn.github.io/05%20Advanced%20Lighting/08%20Deferred%20Shading/
Unity与Shader
官方文档:https://docs.unity3d.com/Manual/ShadersOverview.html
Unity里的Shader曾经使用的是Cg,所以我们能看见很多CGPROGRAM这样的关键字,以及.cginc的文件。但是现在Unity使用HLSL来写Shader,例如最新的URP里面的Shader,都是由HLSL所写的。
与我们之前OpenGL里看见的Shader代码不一样,Unity里面Shader代码都写在一个称为ShaderLab的格式里,如下:
Shader "MyShader" {
Properties {
_MyTexture ("My Texture", 2D) = "white" { }
// Place other properties like colors or vectors here as well
}
SubShader {
// here goes your
// - Surface Shader or
// - Vertex and Fragment Shader or
// - Fixed Function Shader
}
SubShader {
// Place a simpler "fallback" version of the SubShader above
// that can run on older graphics cards here
}
}
在Unity中常说到的SurfaceShader实际上属于对Vertex Shader和Fragment Shader的一种封装,为我们事先实现了光照和阴影的代码。至于Shader里的代码,了解了管线具体流程的话,理解起来其实也会非常的简单。
Compute Rendering / Compute Shader
如今还有更牛逼的Compute Shader,可用于高频的大量计算用于辅助渲染,例如一帧内要做成千上万次矩阵乘法。在它的帮助下,可直接将GPU作为并行处理器加以利用,GPU将不仅具有3D渲染能力,也具有其他的运算能力。
在之前不管是OpenGL还是DirectX的渲染管线图里面,我们并看不见Compute Shader的身影,这是因为Compute Shader是独立于渲染管线之外。也就是说,Compute Shader是个完全独立的东西,你任何时候,不做渲染的时候,你都可以通过它来实现大量的计算,它有自己的管线,即Compute Rendering:
Vulkan
Vulkan属于是OpenGL的下一代版本,它的Render pipelines如下,参考自:
https://geek-docs.com/vulkan/vulkan-tutorial/vulkan-graphic-pipeline.html
https://vulkan.lunarg.com/doc/view/1.2.135.0/windows/tutorial/html/14-init_pipeline.html
Vulkan的Shader与之前的提到的都不同,Vulkan中的Shader代码必须以二进制字节码的格式使用,而不是像GLSL和HLSL这样具有比较好的可读性的语法。此字节格式成为SPIR-V。这样可使得GPU厂商编写将Shader转换为本地代码的编译器复杂度减少了很多。当然了,我们不需要手写这样的二进制文件,我们可以把GLSL文件编译成SPIR-V格式的。
参考:https://geek-docs.com/vulkan/vulkan-tutorial/vulkan-shader-modules.html
最后
以上就是跳跃航空为你收集整理的图形学基础知识:着色(三)Rendering Pipeline与Shader什么是管线(Pipeline)?图形/渲染管线(Graphics/ Rendering Pipeline)ShaderOpenGLDirect3DUnity与Render pipelinesUnity与ShaderCompute Rendering / Compute ShaderVulkan的全部内容,希望文章能够帮你解决图形学基础知识:着色(三)Rendering Pipeline与Shader什么是管线(Pipeline)?图形/渲染管线(Graphics/ Rendering Pipeline)ShaderOpenGLDirect3DUnity与Render pipelinesUnity与ShaderCompute Rendering / Compute ShaderVulkan所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复