你好呀!今天继续讨论 。这篇文章将比上一篇文章大两倍。抓紧! Unity中的渲染 什么是着色器? 根据上一篇文章中的描述,着色器是一个小程序,可用于在我们的项目中创建有趣的效果。它包含数学计算和指令(命令)列表。它们使我们能够处理计算机屏幕上覆盖对象的区域中每个像素的颜色,或者进行对象变换(例如,创建动态草或水)。 该程序允许我们根据多边形对象的属性绘制元素(使用坐标系)。着色器在 上执行,因为它具有并行架构,由数千个小型高效核心组成,旨在同时处理任务。顺便说一句,CPU 是为顺序串行处理而设计的。 GPU 请注意,Unity 中存在三种类型的着色器相关文件。 首先,我们有带有“.shader”扩展名的程序,能够编译不同类型的渲染管道。 其次,我们的程序具有“.shadergraph”扩展名,只能编译为 URP 或 HDRP。此外,我们还有扩展名为“.hlsl”的文件,允许我们创建自定义函数。这些通常用在称为“自定义函数”的节点类型中,该节点类型可在 Shader Graph 中找到。 还有另一种带有“.cginc”扩展名的着色器类型 - 计算着色器。它与“.shader”CGPROGRAM 关联,而“.hlsl”与“.shadergraph”HLSLPROGRAM 关联。 在Unity中,至少定义了四种类型的结构用于着色器生成。其中,我们可以找到顶点和片段着色器的组合、用于自动光照计算的表面着色器以及用于更高级概念的计算着色器。 着色器语言的小游览 在我们开始编写着色器之前,我们应该考虑到 Unity 中有三种着色器编程语言: HLSL(高级着色器语言 - Microsoft) Cg(C 代表图形 - NVIDIA)- 一种过时的格式 ShaderLab - 一种声明性语言 - Unity 我们将快速浏览一下 Cg、 ,并稍微介绍一下 HLSL。 ShaderLab Cg 是一种高级编程语言,旨在在大多数 GPU 上进行编译。 NVIDIA 与 Microsoft 合作开发了它,并使用了与 HLSL 非常相似的语法。着色器使用 Cg 语言的原因是它们可以使用 HLSL 和 GLSL(OpenGL 着色语言)进行编译,从而加快和优化视频游戏材质的创建过程。 Unity 中的所有着色器(Shader Graph 和 Compute 除外)都是用名为 ShaderLab 的声明性语言编写的。这种语言的语法允许我们在 Unity 检查器中显示着色器的属性。这非常有趣,因为我们可以实时操纵变量和向量的值,自定义着色器以获得所需的结果。 在ShaderLab中,我们可以手动定义几个属性和命令,其中包括Fallback。它与 Unity 中存在的不同类型的渲染管道兼容。 Fallback 是多平台游戏中的基本代码块。它允许我们编译另一个着色器,而不是生成错误的着色器。如果某个着色器在编译期间中断,则 Fallback 返回另一个着色器,并且图形硬件可以继续其工作。这是必要的,这样我们就不必为 Xbox 和 PlayStation 编写不同的着色器,而是使用统一的着色器。 Unity 中的基本着色器类型 Unity 中的基本着色器类型允许我们创建用于不同目的的子例程。 让我们讨论一下每种类型负责什么: 这种类型的着色器的特点是对与基础光照模型交互的代码编写进行了优化,并且仅适用于内置 RP。 标准表面着色器。 未照亮的着色器。它指的是原色模型,并且将是我们通常用来创建效果的基本结构。 从结构上讲,它与 Unlit 着色器非常相似。这些着色器主要用于内置 RP 后处理效果,需要“OnRenderImage()”函数 (C#)。 图像效果着色器。 这种类型的特点是它在视频卡上执行,并且在结构上与前面提到的着色器有很大不同。 计算着色器。 一种实验类型的着色器,允许实时收集和处理光线追踪。它仅适用于 HDRP 和 DXR。 光线追踪着色器。 一个基于图形的空着色器,您无需了解使用节点的着色器语言即可使用它。 空白着色器图。 它是一个子着色器,可以在其他 Shader Graph 着色器中使用。 子图。 Unity 中的渲染是一个困难的话题,因此请仔细阅读本指南。 着色器结构 要分析Shader的结构,我们需要做的就是基于Unlit创建一个简单的Shader并对其进行分析。 当我们第一次创建着色器时,Unity 会添加默认代码以简化编译过程。在着色器中,我们可以找到 ,以便 GPU 可以解释它们。 结构化的代码块 如果我们打开着色器,它的结构看起来很相似: Shader "Unlit/OurSampleShaderUnlit" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags {"RenderType"="Opaque"} LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler 2D _MainTex; float4 _MainTex; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o, o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } } 通过当前的示例及其基本结构,它变得更加清晰一些。着色器以 Unity 编辑器检查器中的路径 (InspectorPath) 和名称 (shaderName) 开始,然后是属性(例如纹理、矢量、颜色等),最后是 SubShader。最后,可选的 Fallback 参数支持不同的变体。 使用 ShaderLab 大多数着色器首先在 Unity 检查器中声明着色器及其路径及其名称。这两个属性(例如 SubShader 和 Fallback)均以 ShaderLab 声明性语言编写在“Shader”字段内。 Shader "OurPath/shaderName" { // The shader code will be here } 路径和着色器名称都可以根据项目中的需要进行更改。 着色器属性对应于可以在 Unity 检查器中操作的参数列表。从价值和实用性来看,有八种不同的属性。我们动态地或在运行时使用与要创建或修改的着色器相关的这些属性。声明属性的语法如下: PropertyName ("display name", type) = defaultValue “PropertyName”代表属性的名称(例如,_MainTex),“显示名称”指定Unity检查器中属性的名称(例如,Texture),“type”表示其类型(例如,Color、Vector、2D等)。最后,“defaultValue”是分配给属性的默认值(例如,如果属性是“Color”,我们可以将其设置为白色(1, 1, 1, 1, 1, 1)。 着色器的第二个组件是 SubShader。每个着色器至少由一个子着色器组成,以实现完美加载。当有多个 SubShader 时,Unity 会处理每个 SubShader,并根据硬件规格选择最合适的一个,从列表中的第一个开始,到最后一个结束(例如,分离 iOS 和 Android 的着色器)。当不支持SubShader时,Unity将尝试使用与标准着色器对应的Fallback组件,以便硬件可以继续其任务而不会出现图形错误。 Shader "OurPack/OurShader" { Properties { ... } SubShader { // Here will be the shader configuration } } 您可以 和 阅读有关参数和子着色器的更多信息。 在此处 此处 混合 我们需要混合来将两个像素合并为一个像素。内置和 SRP 均支持混合。 混合发生在将像素的最终颜色与其深度相结合的步骤中。当执行模板缓冲区、z 缓冲区和颜色混合时,此阶段发生在片段着色器阶段之后渲染管道的末尾。 默认情况下,此属性不会写入着色器中,因为它是一个可选功能,主要在处理透明对象时使用。例如,当我们需要在另一个像素前面绘制一个具有低不透明度像素的像素时,就会使用它(这在 UI 中经常使用)。 我们可以在这里启用混合: Blend [SourceFactor] [DestinationFactor] 您可以 阅读有关混合的更多信息。 在此处 Z 缓冲区(深度缓冲区) 要理解这两个概念,我们必须首先了解 Z 缓冲区(也称为深度缓冲区)和深度测试的工作原理。 在开始之前,我们必须考虑像素具有深度值。这些值存储在深度缓冲区中,它确定一个对象是位于屏幕上另一个对象的前面还是后面。 另一方面,深度测试是确定深度缓冲区中的像素是否更新的条件。 我们已经知道,像素有一个分配的值,该值以 RGB 颜色测量并存储在颜色缓冲区中。 Z 缓冲区添加了一个附加值,用于根据距相机的距离来测量像素的深度,但仅限于正面区域内的那些表面。这允许 2 个像素颜色相同但深度不同。 物体距离相机越近,Z 缓冲区值越小,缓冲区值较小的像素会覆盖值较大的像素。 为了理解这个概念,假设我们的场景中有一个相机和一些图元,它们都位于“Z”空间轴上。 “缓冲区”一词指的是临时存储数据的“内存空间”,因此 Z 缓冲区指的是分配给每个像素的场景中的对象和相机之间的深度值。 借助 Unity 中的 ZTest 参数,我们可以控制深度测试。 剔除 此属性与内置 RP 和 URP/HDRP 兼容,控制在处理像素深度时将删除多边形的哪些面。 这是什么意思?回想一下,多边形对象具有内边缘和外边缘。默认情况下,外边缘是可见的 (CullBack)。但是,我们可以激活内部边缘: 对象的两个边缘均被渲染 剔除。 默认情况下,显示对象的后边缘 剔回。 渲染对象的前边缘。 剔除前线。 该命令有三个值,即 Back、Front 和 Off。默认情况下,“后退”命令处于活动状态;但是,通常,出于优化目的,与剔除相关的代码行在着色器中不可见。如果我们想更改参数,我们必须添加“Cull”一词,后跟我们要使用的模式。 Shader "Culling/OurShader" { Properties { [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 0 } SubShader { // Cull Front // Cull Off Cull [_Cull] } } 我们还可以通过“UnityEngine.Rendering.CullMode”依赖项 。它是枚举并作为参数传递给函数。 在Unity检查器中动态配置剔除参数 使用 Cg/HLSL 在我们的着色器中,我们可以找到至少三种默认指令的变体。这些是 Cg 或 HLSL 中包含的处理器指令。它们的功能是帮助我们的着色器识别和编译某些无法识别的函数: 它允许顶点着色器阶段作为顶点着色器编译到 GPU 中。 #pragma 顶点 vert。 该指令执行与 pragma vertex 相同的功能,不同之处在于它允许将名为“frag”的片段着色器阶段编译为代码中的片段着色器 #pragma 片段 frag。 与之前的指令不同,它具有双重功能。首先,multi_compile 指的是变体着色器,它允许我们在着色器中生成具有不同功能的变体。其次,“_fog”一词包含 Unity 中照明窗口的雾功能。如果我们进入环境/其他设置,我们可以激活或停用着色器的雾选项。 #pragma multi_compile_fog。 我们还可以将 Cg/HLSL 文件插入到我们的着色器中。通常我们在插入 UnityCG.cginc 时执行此操作。它包括雾坐标、剪辑的对象位置、纹理变换、雾携带等等,包括 UNITY_PI 常量。 我们可以用Cg/HLSL做的最重要的事情是为顶点和片段着色器编写直接处理函数,使用这些语言的变量和各种坐标,如纹理坐标(TEXCOORD0)。 #pragma vertex vert #pragma fragment frag v2f vert (appdata v) { // Ability to work with the vertex shader } fixed4 frag (v2f i) : SV_Target { // Ability to work with fragment shader } 您可以 阅读有关 Cg/HLSL 的更多信息。 在此处 着色器图 Shader Graph 是 Unity 的一个新解决方案,允许您在不了解着色器语言的情况下创建解决方案。视觉节点用于处理它(但没有人禁止将它们与着色器语言结合起来)。 Shader Graph 仅适用于 HDRP 和 URP。 您必须记住,在使用 Shader Graph 时,为 Unity 2018 开发的版本是 BETA 版本,并且不受支持。为 Unity 2019.1+ 开发的版本积极兼容并获得支持。 另一个问题是,使用此接口创建的着色器很可能无法在不同版本中正确编译。这是因为每次更新都会添加新功能。 那么,Shader Graph 是着色器开发的好工具吗?当然如此。它不仅可以由图形程序员处理,也可以由技术设计师或艺术家处理。 要创建图表,我们需要做的就是在 Unity 编辑器中选择我们想要的类型。 在开始之前,我们先简单介绍一下Shader Graph级别的顶点/片段着色器。 正如我们所看到的,顶点着色器阶段定义了三个入口点,即:Position(3)、Normal(3) 和 Tangent(3),就像在 Cg 或 HLSL 着色器中一样。与常规着色器相比,这意味着 Position(3) = POSITION[n]、Normal(3) = NORMAL[n] 且 Tangent(3) = TANGENT[n]。 为什么 Shader Graph 有 3 个维度,而 Cg 或 HLSL 有 4 个维度? 回想一下,向量的四个维度对应于其分量 W,在大多数情况下为“一或零”。当W = 1时,向量对应于空间或点位置。而当 W = 0 时,矢量对应于空间中的方向。 因此,为了设置着色器,我们首先进入编辑器并创建两个参数:颜色 - _Color 和Texture2D - _MainTex。 要在 ShaderLab 属性和我们的程序之间创建链接,我们必须在 CGPROGRAM 字段中创建变量。然而,这个过程在Shader Graph中是不同的。我们必须将要使用的属性拖放到节点工作区中。 为了使Texture2D与SampleTexture2D节点结合使用,我们需要做的就是将_MainTex属性的输出连接到输入Texture(T2)。 要相乘两个节点(颜色和纹理),我们只需调用 Multiply 节点并将两个值作为输入点传递。最后,我们需要在片段着色器阶段发送基色中的颜色输出。现在保存着色器,我们就完成了。我们的第一个着色器已准备就绪。 我们还可以转向一般图形设置,分为节点和图形部分。它们具有可定制的属性,使我们能够改变颜色再现。我们可以找到混合、alpha 剪切等选项。此外,我们可以在 Shader Graph 配置中自定义节点的属性。 节点本身提供了我们在 ShaderLab 中编写的某些函数的类似物。以函数 Clamp 的代码为例: void Unity_Clamp_float4(float4 In, float4 Min, float4 Max, out float4 Out) { Out = clamp(In, Min, Max); } 通过这种方式,我们可以简化我们的生活并减少编写着色器的时间,但以牺牲可视化图形为代价。 结论 我可以长时间谈论着色器,也可以谈论渲染过程本身。我已经在本 所有基础知识。这里我没有讨论光线追踪着色器和计算着色。我粗略地介绍了着色器语言,并且只描述了冰山一角的过程。 指南中讨论了有关 Unity 渲染的