【UnityShader入门精要学习笔记】第三章(2)Unity Shader的形式,章节答疑

2024-01-09 10:03:44

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

总之适用于同样开始学习Shader的同学们进行有取舍的参考。



(该系列笔记中大多数都会复习前文的知识,特别是前文知识非常重要的时候,这是为了巩固记忆,诸位可以直接通过目录跳转)


Unity Shader的形式

尽管Unity Shader可以做的事情非常多(例如设置渲染状态等),但最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语句块中(表面着色器),也可以写在Pass语句块中(顶点/片元着色器和固定函数着色器)

表面着色器

表面着色器(Surface Shader) 是Unity自己创作的一种着色器代码类型,它的本质和顶点/片元着色器(VF) 是一样的。我们定义了一个表面着色器,Unity做的工作仍然是将其转换为顶点/片元着色器再处理。因此表面着色器可以看作Unity对顶点/片元着色器的更高一层抽象。使用表面着色器,Unity帮我们处理了很多光照细节,我们就不必要去操心哪些烦人的事情。

一个简单的表面着色器示例如下:

Shader "Custom/Simple Surface Shader"{
	SubShader{
		Tags{ "RenderType" = "Opaque"}
		// 以CGPROGRAM开头代表下列代码使用了CG编写
		CGPROGRAM
		// #pragma为预处理指令,声明surface表面着色器将使用函数surf作为表面函数,Lambetr作为光照函数(其中surf是自定义的,Lambert是Unity自带的光照函数)
		#pragma surface surf Lambetr
		// 定义结构体Input
		struct Input{
			float4 color: COLOR;
		};
		//定义Input类型的形参IN,以及SurfaceOutput类型的形参o,
		// 其中inout是一个关键字
		void surf (Input IN, inout SurfaceOutput o){
			o.Albedo = 1;
		}
		// 结束CG
		ENDCG
	}
	Fallback "Diffuse"
}

上述表面着色器定义在了SubShader语句块 (而非Pass语句块中)中的CGPROGRAM和ENDCG之间。当我们使用表面着色器,就不需要关心使用多少个Pass,这些Unity会帮我们处理好,我们要做的只是定义好使用的表面函数和光照函数即可。

CGPROGRAM和ENDCG中的代码是使用CG/HLSL编写的。语法上和CG/HLSL的语法基本一致。

拓展,CG语法中的in,out,inout关键字

既然上述代码中使用到了inout,我们可以简单讲一下这个关键字。其实类比C#中的in,out关键字以及C++的指针就方便理解了。

void surf (Input IN, inout SurfaceOutput o){
	o.Albedo = 1;
}

我们定义了一个函数,这个函数有两个形参,分别是IN和o。其中IN我们没有指定关键字(限定符),而o指定了inout关键字。在CG/HLSL/GLSL中,函数形参可以通过关键字来定义:

限定符说明
in如果不使用限定符时,默认形参的限定符就是in ,传入函数的形参实际上是实参的复制值。in代表了对实参可读不可写(形参由实参的复制值得到,且修改形参不影响实参)
outout代表了对实参可写不可读(相当于指针,out修饰的形参指向这个实参本身,修改时会影响实参的值)
inout对实参可读且可写,传入形参的是实参的复制值,但是函数结束后会根据这个值对应地修改实参

顶点/片元着色器

在Unity中我们可以使用CG/HLSL来编写顶点/片元着色器(Vertex/Fragment Shader,简称VF) 。它们更加复杂,但灵活度也更高。

下面是一个顶点/片元着色器的示例代码:

Shader "Custom/Simple VertexFragment Shader"{
	SubShader{
		Pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
		
			float4 vert(float4 v : POSITION) : SV_POSITION {
				return mul (UNITY_MATRIX_MVP, v);
			}

			fixed4 frag() : SV_Target {
				return fixed4(1.0 , 0.0, 0.0 , 1.0);
			}
			ENDCG
		}
	}
}

与表面着色器相似,顶点/片元着色器代码也是定义在CGPROGRAM和ENDCG区块之间的。原因是,我们需要自己定义每个Pass需要使用的Shader代码,虽然我们可能需要编写更多的代码,但带来的好处是灵活性更高,更重要的是,我们可以控制渲染的实现细节。


固定函数着色器

表面着色器和顶点/片元着色器都使用了可编程的渲染管线。对于那些不支持可编程渲染管线的老式设备,我们只能使用固定函数着色器(Fixed Function Shader) 来完成渲染了,这些着色器只能完成一些简单的效果。

Shader "Tutorial/Basic" {
	Properties {
		_Color ("Main Color",Color) = (1, 0.5, 0.5, 1)
	}
	SubShader {
		Pass {
			Material {
				Diffuse {_Color}
			}
			Lighting On
		}
	}
}

固定函数着色器是完全使用ShaderLab语法的。我们只能在Pass语句块中定义,这些代码相当于Pass中的一些渲染设置。(实际上这些固定函数着色器也会被Unity编译成对应的顶点/片元着色器)


对Unity Shader选择的建议

那么我们究竟应该选择哪一种着色器进行Unity Shader的编写?

  • 如果有很明确的需求,必须在旧时设备上运行,选择固定函数着色器。否则请选择可编程的着色器,即表面着色器和顶点/片元着色器。
  • 如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但是小心在移动平台上的性能表现。
  • 如果需要使用的光照数量少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
  • 更重要的是,如果你有很多自定义的渲染效果,请使用顶点/片元着色器。

最重要的一点,不要单纯地去记忆,怎么使用,在何时使用,去理解,去掌握,知其然然后知其所以然。


(依然是作者答疑)

章节答疑

Unity Shader != Shader

在第二章学习渲染流水线的时候,我们学习到了着色器Shader。我们之前也提到了,Unity Shader和渲染流水线的Shader不是同一个东西,只是由于我们在Unity开发中常常把Unity Shader简称为Shader,因此不少人会将二者搞混。

需要区分这两个概念,传统意义上的Shader,指的是渲染流水线的着色器代码文件。而Unity Shader指的是ShaderLab编写的文件,其文件名后缀为.shader。它们的区别如下:

  • 传统Shader中,我们仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等(也就是说一个Shader只能对应一种着色器)。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器(一个Unity Shader可以编写多个着色器)
  • 传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些需要开发者在其他代码中设置。而Unity Shader中,我们通过一行特定的指令就可以完成这些设置。(Unity Shader可以顺带进行渲染设置)
  • 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以通过Material来方便地修改这些属性。并且对于模型自带的数据(如顶点位置、纹理坐标、法线等),Unity Shader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。(简化使用,对数据的访问更加灵活)

虽然Unity Shader相对于传统Shader使用上更方便了,但由于高度的抽象和封装,导致我们可以编写的Shader类型和语法都被限制了。对于一些类型的Shader,例如曲面细分着色器,几何着色器等等,Unity的支持就相对差一些。除此之外,一些高级的Shader语法Unity Shader也不支持。

Untiy Shader提供了一种让开发者同时控制渲染流水线中多个阶段的一种方式,而不仅仅是Shader代码。作为开发者而言,Unity Shader提供的抽象层使我们只需要关心Unity Shader的实现代码,而不需要关心底层渲染引擎的实现细节。

Unity Shader和CG/HLSL的关系

Unity Shader是使用ShaderLab语言编写的。但是对于表面着色器和顶点/片元着色器,我们可以在ShaderLab内部使用CG/HLSL来编写。这些CG/HLSL代码是嵌套在CGPROGRAM ENDCG标签内部的。在Unity里CG和HLSL几乎是等价的,所以想要编写好Unity Shader,不仅需要掌握ShaderLab,还需要学习CG/HLSL语言的实现。

通常来说,CG代码位于Pass块内部:

Pass {
	// Pass的标签和状态设置
	CGPROGRAM
	// 编译指令,例如:
	#pragma vertex vert
	#pragma fragment frag
	// CG代码
	ENDCG
	// 其他一些设置
}

但是之前在编写表面着色器的示例代码中,我们发现CGPROGRAM定义在了SubShader块中,这是由于表面着色器实质上还是Unity对顶点/片元着色器的一层封装,Unity在背后会将其转化为一个包含多Pass的顶点/片元着色器,所以还是符合我们上述CG代码位于Pass块内部的定义的。

虽然我们上文讲到Unity提供了三种着色器:表面着色器顶点/片元着色器固定函数着色器。但实质上,表面着色器和固定函数着色器最后都会被转化为顶点/片元着色器。所以本质上讲Unity中只存在顶点/片元着色器。

Unity Shader的编译

Unity编辑器会把CG片段翻译成低级语言编译到不同渲染平台上。这些编译过程比较复杂,Unity会使用不同的编译器来把CG转化成对应平台的代码。
在这里插入图片描述
通过面板上的编译和生成代码, 来查看不同平台生成的Shader代码。以进行Debug和优化。

当游戏发布后,游戏数据文件会移除目标平台不需要的代码,保留平台需要的编译代码。

可以使用GLSL吗?

可以,使用GLSLPROGRAM ENDGLSL标签。如果选择GLSL意味着不支持PC、XBOX这样的仅支持DirectX的平台。

如何在Unity中使用GLSL
Unity shader-writing文档

还有需要纠正一下,NVIDIA已经放弃CG了,所以还是学习GLSL和HLSL吧。

文章来源:https://blog.csdn.net/milu_ELK/article/details/135414513
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。