[Shader] Unity CG/HLSL [2]

56 篇文章 2 订阅

4.0.1. | 着色器和材质之间的类比.

根据其术语,

材质是表面渲染方式的定义,包括对纹理、平铺、偏移、颜色等的引用。 材质的选项取决于所使用的着色器。

我们如何将上述定义转化为实际水平? 让我们将材质视为“着色器的容器”,这意味着我们拥有执行表面计算(着色器)的程序和能够读取这些计算的容器(材质)。

单独的材料无法执行任何操作。 如果它没有着色器,它将不知道应该如何渲染它,同样,如果不通过材质,着色器就无法应用于对象,因此,这个比喻之间材质和着色器是一个"预览的图形数学计算"。

4.0.2. | 我们的第一个 Cg 或 HLSL 着色器.

我们将继续使用我们在本章开头创建的“USB_simple_color”着色器。

我们已经知道,我们的默认着色器有一个名为 _MainTex 的纹理,它是在属性中配置的。 接下来我们要做的是添加颜色来改变纹理的色调。

在开始之前,我们必须记住,我们不能直接将着色器应用于场景中的对象。 为此,我们需要创建一个材质并将着色器分配给它,以便可以以图形方式渲染它。 要创建材质,我们必须转到我们的项目,右键单击我们正在其中工作的文件夹,转到创建并选择材质。 材质的默认配置取决于我们刚刚创建的渲染管道。 一般来说,当我们在内置 RP 中创建材质时,它会附带标准表面着色器类型,使我们能够可视化照明、阴影和与其相关的其他计算的方向。

因此,我们进入层次结构并创建一个 3D 对象。 我们将着色器 USB_simple_color 分配给我们最近创建的材质,然后将该材质应用于场景中的对象。 此时,我们可以为要投影到对象上的材质分配纹理。

返回我们的 USB_simple_color 着色器并创建一个颜色属性,正如我们将在以下示例中看到的:

Shader "USB/USB_simple_color"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Color ("Texture Color", Color) = (1, 1, 1, 1)
	}
	SubShader {}
}

我们刚刚所做的是为我们的着色器声明一个颜色属性。 如果我们保存并返回到 Unity,我们可以看到这个属性出现在材质检查器中,但是,它还没有工作,因为我们还没有在 CGPROGRAM 中声明它。 接下来我们需要做的是添加 _Color 连接变量,以便我们可以在程序中使用它。 为此,我们进入顶点着色器阶段; 其中 _MainTex 已声明为 Sampler2D 并按以下方式创建连接变量:

uniform sampler2D _Maintex;
uniform float4 _MainTex_ST;
uniform float4 _Color; // 连接变量

正如我们所看到的,我们已经在 CGPROGRAM 或 HLSLPROGRAM 中声明了 _Color 的全局变量,但是,我们仍然没有使用它。 要更改纹理的色调,我们将进入片段着色器阶段并使用乘号将 col(纹理颜色)乘以 _Color。 操作应该如下所示:

// CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
	fixed4 col = tex2D(_MainTex, i.uv);
	return col * _Color;
} 
// HLSLPROGRAM
half4 frag (v2f i) : SV_Target
{
	half4 col = tex2D(_MainTex, i.uv);
	return col * _Color;
}

如果我们保存着色器并返回到 Unity,我们现在可以从材质检查器更改色调纹理。

4.0.3. | 在 Cg 或 HLSL 中添加透明度

在本节中,我们将添加混合,以便我们的着色器具有定义的 Alpha 通道。 在之前的 USB_simple_color 配置中,我们添加了颜色来更改纹理的色调。 现在,值得一提的是,颜色属性有四个通道(RGBA),但是,如果我们从 Inspector 中修改颜色的 Alpha 通道,我们会发现这并没有产生任何变化,这是为什么呢? 主要是因为它不具有 Blending 属性。 为了使我们的着色器受到透明度的影响,我们必须:将 RenderType 配置为透明,添加具有透明度的队列并添加我们要使用的混合类型。

Shader "USB/USB_simple_color"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Color ("Texture Color", Color) = (1, 1, 1, 1)
	}
	SubShader
	{
		Tags { "RenderType"="Transparent" "Queue"="Transparent" }
		Blend SrcAlpha OneMinusSrcAlpha
		LOD 100
		Pass {}
	}
}

如果我们保存着色器并返回到 Unity,我们现在可以更改 Alpha 通道颜色,因此,这将影响材质纹理的透明度。

4.0.4 | HLSL 中函数的结构

与 C# 中一样,在 HLSL 中我们可以声明空函数 (void) 或返回值 (return) 的函数。

我们必须使用依赖于函数类型的“声明”; 它们确定一个值是否对应于输入(in)、输出(out)、全局变量(uniform)或常量值(const)。

我们将首先使用以下语法分析一个空函数:

void functionName_precision (declaration type arg)
{
	float value = 0;
	arg = value;
}

要声明空函数,我们从 void 命名法开始,然后是函数名称以及精度和最终参数。 正如我们在前面的示例中看到的,在函数字段内我们编写要执行的算法或操作。 一般来说,在参数中,我们必须定义这些是输入还是输出。

我们如何知道他们是否有声明? 一切都取决于我们想要作为参数传递的函数。 为了理解这个概念,假设我们想要创建一个函数来计算对象中的照明,为此,我们需要的属性之一是法线,通过它我们可以识别光源将从哪个方向照亮我们的物体。 目的。 因此,法线将是我们空函数内的“计算输入”。

void FakeLight_float (in float3 Normal, out float3 Out)
{
	float[n] operation = Normal;
	Out = operation;
}

该函数不实现任何特定功能,但我们将使用它来理解之前提到的概念。

该函数称为“FakeLight”,“_float”对应于其精度,可以是 float 或 half 类型,因为我们知道,这些是 HLSL 兼容格式。

我们必须始终为空函数添加精度,否则,它无法在我们的程序中编译。 然后在参数中,我们可以看到对象的法线(float3 Normal)已通过“in”声明声明为输入,同样,有一个名为“out”的输出,它将成为操作的最终值。

要在另一个函数中使用这种类型的函数,我们必须在函数本身之前在代码中声明输入和输出。

让我们在片段着色器阶段模拟 FakeLight 函数,就像它实际运行一样

// 创建我们的函数
void FakeLight_float (in float3 Normal, out float3 Out)
{
	float[n] operation = Normal;
	Out = operation; 
}
half4 frag (v2f i) : SV_Target
{
	// 声明法线。
	float3 n = i.normal;
	// 声明输出。
	float3 col = 0;
	// 将两个值作为参数传递。
	FakeLight_float (n, col);
	
	return float4(col.rgb, 1);
}

在上面的示例中,发生了几种情况。 首先,“FakeLight”函数在“frag”函数之前声明,因为 GPU 从上到下读取我们的代码。 然后,在片段着色器阶段,我们创建了一个名为“n”的三维向量和另一个名为“col”的三维向量。 在本例中,两者都是三维向量类型,这是因为我们将在 FakeLight_float 函数中使用这两个向量作为参数,该函数需要三维输入和输出向量。 然后,第一个参数对应于对象的正常输入,第二个参数对应于 FakeLight_float 函数内执行的操作的结果。

col 向量从“零”开始,这意味着它的红、绿、蓝 (RGB) 颜色为“0”,默认情况下对应于黑色,但是,由于它已被声明为输出,因此它是 现在发生在 FakeLight_float 函数内部。

最后,我们返回一个四维向量,其中前三个值对应于 RGB 中的 col 向量,“one”对应于 Alpha。

为什么我们要返回一个四维向量? 这是因为函数frag是half4类型,即四维向量。

现在我们将分析返回值的函数的结构。 本质上,它们非常相似,不同之处在于,在这种情况下,不需要添加精度函数。 为了说明这一点,我们将使用相同的 FakeLight 函数,但这次它将返回一个值。

// create our function
half3 FakeLight (float3 Normal)
{
	float[n] operation = Normal;
	return operation;
}
half4 frag (v2f i) : SV_Target
{
	// declare normals
	float3 n = i.normal;
	float3 col = FakeLight_float (n);
	return float4(col.rgb, 1);
}

与空函数不同,我们只添加 Normal 参数,因为它不需要输出,同样,在片段着色器阶段,我们使向量“col”等于该函数,因为它返回与该向量拥有的相同维数。

4.0.5. | 调试着色器

当我们用C#编写脚本时,在Unity中我们可以使用Debug.Log(对象消息)函数来调试我们的程序。 该函数允许我们在控制台上打印代码的操作,但是,该函数在 Cg 或 HLSL 中不可用。

那么我们如何调试着色器呢? 为此,我们必须考虑几个因素,包括:“颜色”。 在着色器中,我们经常看到三种重要的颜色,它们是白色 (1, 1, 1, 1)、黑色 (0, 0, 0, 1) 和洋红色 (1, 0, 1, 1)。 白色代表默认值,黑色代表初始化值。 洋红色代表图形错误,事实上,将洋红色资源导入 Unity 到我们的场景中是很常见的。

在这里插入图片描述
(图4.0.5a)

为了解决这个概念,让我们回顾一下 ShaderLab 中的属性声明,为此,我们将重新创建一个颜色属性。

_Color ("Tint", Color) = (1, 1, 1, 1)

在上面的例子中,属性默认被声明为“白色”,我们可以在四维向量中证实这一点; 操作结束时 (1, 1, 1, 1)。 该属性将在 Unity Inspector 中生成一个“颜色选择器”,其颜色和/或初始化值将为白色。

我们来分析一下另一个属性。

_MainTex ("Texture", 2D) = "white"{}

同样,这个属性将在 Unity Inspector 中生成一个“纹理选择器”,但是如果我们不使用任何纹理,它的默认颜色是什么? 它的颜色为白色,我们可以在该属性的“white”声明中确认这一点。

我们在代码中的内部向量和变量的声明中经常看到黑色,事实上,将向量初始化为“0”然后添加一些操作是很常见的,例如四维向量“float4 col = 0” 已初始化为“0”,这意味着其所有通道(RGBA)的默认值都为零。

注意,白色代表一个像素的最大照度值,黑色代表其最小照度。 记住这一点很重要,因为随着我们的前进,我们将有一些超出最大 (x > 1) 和最小 (x < 0) 值的操作。 在这些情况下,会出现颜色饱和,为此,我们将使用一些功能,例如“clamp”,它允许我们将值限制在两个数字(最小值和最大值)之间。

为什么我们的物体有时会呈现洋红色? 我们已经知道,这种颜色代表“图形错误”。 基本上,当 GPU 无法执行着色器中的操作时,它会返回默认的洋红色。 在Unity中,出现这种颜色的原因主要有两个因素:

  1. 渲染管道尚未配置。
  2. 或者着色器的代码中有错误。

当我们将资产导入软件时,我们首先需要知道的是,我们正在使用哪个渲染管道? 请记住,Unity 中存在三种类型的渲染管道,每种类型都有不同的配置。

假设我们在 Universal RP 中导入一个包含具有标准表面着色器材质的资源,那么我们的资源将以洋红色显示,因为这种类型的着色器仅在Built-in RP 中受支持。 为了解决这个图形错误,我们必须选择材质,进入 inspector 并将着色器类型更改为“Shader Graphs”中找到的类型。

一旦我们确定了正在使用的渲染管道的类型,我们就必须继续进行着色器评估。

由于对象获得洋红色,因此着色器中的错误在图形上很明显。 如果我们注意控制台,我们可以找到它的定义。 为了解决它们,我们必须考虑一些因素:在HLSL和ShaderLab中完成一条指令或函数。 大多数错误是由拼写错误或语法错误产生的。 下面,我们将回顾一个相当常见的:

在这里插入图片描述
我们该如何解读上面的陈述呢? 在这种情况下,“Unlit”菜单中找到的名为“USB_simple_color”的着色器在代码行 60 中出现错误,无法在 Direct3D 11 中编译。

现在,这是着色器问题吗? 为此,我们将分析生成错误的代码行。

58 fixed4 frag (v2f i) : SV_Target
59 {
60 		fixed4 col = tex2D(_MainTex, i.uv);
61 		return col // (;)
62 }

根据控制台,错误是在第 60 行代码中生成的,但正如我们所看到的,那里没有问题。 那么我们必须问自己,到底发生了什么? 错误是因为我们忘记关闭第 61 行代码中的操作。如果我们查看返回结果,我们会发现 col 向量缺少分号 (😉,因此 GPU 认为操作继续,因此无法继续编译它。

以下是我们经常看到的另一个错误:

在这里插入图片描述

half4 frag (v2f i) : SV_Target
{
	half4 col = tex2D(_MainTex, i.uv);
	half2 uv = 0;
	col = uv; // cannot be converted
	return col;
}

在本例中,由于我们试图将四维向量 (col) 转换为二维向量 (uv),因此产生了错误。 col 向量是四维的,具有 RGBA 或 XYZW 通道,而 uv 向量是二维的,仅具有两个通道(RG、RB、GB 等)的组合,因此它不能从二维向量转换为四维向量。

4.0.6. | 添加 URP 兼容性

到目前为止,我们已经实现的许多变量、函数和向量都适用于 Cg 和 HLSL,但是,在某些情况下,我们必须添加 URP 支持,以便我们的着色器可以编译。

对于 Shader Graph,如果我们想使用 Universal RP 通过代码创建着色器,我们必须添加一些依赖项,以便 GPU 可以读取此渲染管道。 这种依赖关系可以在不同的路径中找到,其中我们可以提到:

Packages / Core RP Library / ShaderLibrary
Packages / Universal RP / ShaderLibrary

当我们在项目中安装 Shader Graph 时,会自动包含“Core RP Library”,同样,当我们选择此渲染管道作为渲染引擎时,也会包含“Universal RP”包。

这两个包都有扩展名为“.hlsl”的文件,我们的程序需要这些文件来编译 HLSL 中的着色器。

为了理解这个概念,让我们复制着色器 USB_simple_color 并将其重命名为 USB_simple_color_URP。 值得一提的是,着色器 USB_simple_color (Cg) 作为基本颜色模型,已经在 Universal RP 中具有兼容性,但是,我们将生成其副本并对其进行修改,以详细查看其在 HLSL 中的实现。

我们将首先添加标签“RenderPipeline”并使其等于“UniversalRenderPipeline”,这样GPU就会知道该着色器在此渲染引擎中具有兼容性。

Tags
{
	"RenderType"="Transparent"
	"Queue"="Transparent"
	"RenderPipeline"="UniversalRenderPipeline"
}

正如本书中提到的,Universal RP 使用 HLSL 语言,因此 CGPROGRAM/ENDCG 块必须分别替换为 HLSLPROGRAM 和 ENDHLSL。

Pass
{
	HLSLPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	 …
	ENDHLSL
}

为了让我们的着色器完美编译并使其过程更加高效,我们必须包含依赖项“Core.hlsl”,它包含函数、结构和其他内容,从而允许程序在 Universal RP 中正常运行。 就其本身而言,Core.hlsl 取代了“UnityCg.cginc”,这意味着我们必须消除着色器的这种依赖性。

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma multi_compile_fog
// #include "UnityCg.cginc"
#include “Package/com.unity.render-pipelines.universal/ShaderLibrary/Core.
hlsl”
…
ENDHLSL

与 Unity 安装中包含的 UnityCg.cginc 不同,一旦我们在项目中安装 Universal RP,Core.hlsl 就会作为包添加,因此,我们必须在项目中写入其位置的完整路径。 一旦我们替换了这些依赖项,就会发生两件事。

  1. GPU 将无法编译雾坐标(UNITY_FOG_COORDS、UNITY_TRANSFER_FOR 和 UNITY_APPLY_FOR),因为这些坐标包含在 UnityCg.cginc 中。
  2. 出于同样的原因,您也无法编译 UnityObjectToClipPos 函数。

相反,我们必须使用函数 TransformObjectToHClip(v.vertex),它包含在依赖项“SpaceTransforms.hlsl”中,而“SpaceTransforms.hlsl”又包含在“Core.hlsl”中。

TransformObjectToHClip 函数执行与 UnityObjectToClipPos 相同的操作,即将顶点位置从对象空间变换到剪辑空间,但前者的执行过程更高效。

v2f vert (appdata v)
{
	v2f o;
	// o.vertex = UnityObjectToClipPos(v.vertex);
	o.vertex = TransformObjectToHClip(v.vertex);
	o.uv = TRANSFORM_TEX(v.uv, _MainTex);
	return o;
}

如果此时我们保存着色器并返回Unity,我们可以看到控制台中生成了“无法识别的标识符”类型的错误,这是什么原因? 请记住,默认情况下,片段着色器阶段和 col 向量的类型均为固定4。 所以我们可以在函数中检查它。

fixed4 frag (v2f i) : SV_Target
{
	// sample the texture
	fixed4 col = tex2D(_MainTex, i.uv);
	// apply fog
	// UNITY_APPLY_FOG(i.fogcoord, col);
	return col * _Color;
}

在这里插入图片描述
我们已经知道,Universal RP 无法编译“fixed”类型的变量或向量,而我们必须使用“half or float”。 因此,此时我们可以做两件事。

  1. 手动替换 fixed 类型的变量和向量。
  2. 或者包含“HLSLSupport.cginc”依赖项,为着色器编译添加帮助器宏和跨平台定义。
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma multi_compile_fog
// #include "UnityCg.cginc"
#include "HLSLSupport.cginc"
#include "Package/com.unity.render-pipelines.universal/ShaderLibrary/Core.
hlsl"
…
ENDHLSL

一旦包含这种依赖性,我们就可以使用 fixed 类型的变量和向量,因为现在我们的程序将根据其精度识别这种类型的数据,并自动将它们替换为 half 或 float。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值