Shader 笔记一 表面着色器 Surface Shader

参考

猫都能学会的Unity3D Shader入门指南(一)

猫都能学会的Unity3D Shader入门指南(二)

https://docs.unity3d.com/Manual/SL-SurfaceShaders.html

概念

Shader和Material:

Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质)。之后,我们便可以将材质赋予合适的renderer(渲染器)来进行渲染(输出)了。

所以说Shader只是一段规定好输入(颜色,贴图等)和输出(渲染器能够读懂的点和颜色的对应关系)的程序。开发者只需根据输入,进行计算变换,产生输出而已。

shader的结构:

属性定义:用来指定这段代码将有哪些输入

子着色器:代码的主体,可以有多个,每一个子着色器中包含一个或者多个的Pass。在计算着色时,平台先选择最优先可以使用的着色器,然后依次运行其中的Pass,然后得到输出的结果。

回滚:用来处理所有Subshader都不能运行的情况(比如目标设备实在太老,所有Subshader中都有其不支持的特性)

需要提前说明的是,在实际进行表面着色器的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass。

Shader大体上可以分为如下三类:

固定功能管线着色器(Fixed Function Shaders)

固定功能管线着色器的关键代码一般都在Pass的材质设置Material{}和纹理设置SetTexture{}部分。

表面着色器(Surface Shader)

在Unity中,表面着色器的关键代码用Cg/HLSL语言编写,然后嵌在ShaderLab的结构代码中使用。使用表面着色器,用户仅需要编写最关键的表面函数,其余周边代码将由Unity自动生成,包括适配各种光源类型、渲染实时阴影以及集成到前向/延迟渲染管线中等。

顶点片段着色器(Vertex And Fragment Shader)

顶点片段着色器运行于具有可编程渲染管线的硬件上,它包括顶点程序Vertex Programs和片段程序Fragment Programs。当在使用顶点程序或片段程序进行渲染的时候,图形硬件的固定功能管线会关闭,具体来说就是编写的顶点程序会替换掉固定管线中标准的3D变换,光照,纹理坐标生成等功能,而片段程序会替换掉SetTexture命令中的纹理混合模式。因此编写顶点片段着色器需要对3D变化,光照计算等有非常透彻的了解,需要写代码来替代D3D或者OpenGL原先在固定功能管线中要做的工作。

 

Surface Shader

本文先主要讲讲简单的表面着色器,我们先来自己试试手,在Project面板中,右击Create->Shader->Standard Surface Shader,命名为SurfaceShaderDemo.shader,如下:

//shader 名称
Shader "Custom/SurfaceShaderDemo" {
	//属性定义
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	//子着色器
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Standard fullforwardshadows
		#pragma target 3.0

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		UNITY_INSTANCING_CBUFFER_START(Props)
		UNITY_INSTANCING_CBUFFER_END

		void surf (Input IN, inout SurfaceOutputStandard o) {
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	//回滚
	FallBack "Diffuse"
}

然后我们新建一个Material,右击Create->Material,命名为MaterialDemo。在Inspector面板中,即可选择上面我们新建的Shader(Custom->SurfaceShaderDemo,对应shader的名称)

下面红框的部分就是对应Shader中的属性定义模块。接下里我们讲讲属性定义中的一些常用字段的含义

Properties

在Properties{}中定义着色器属性,在这里定义的属性将被作为输入提供给所有的子着色器。每一条属性的定义的语法是这样的:

_Name("Display Name", type) = defaultValue[{options}]

_Name - 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容
Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容
type - 这个属性的类型,可能的type所表示的内容有以下几种:

Color 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义
2D 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来
Rect 一个非2阶数大小的贴图
Range(min, max) 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等)
Cube 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样
Float 任意一个浮点数
Vector 一个四维数

defaultValue - 定义了这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值

Color 以0~1定义的rgba颜色,比如(1,1,1,1)
2D/Rect/Cube 对于贴图来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者”white”,”black”,”gray”,”bump”中的一个
Float,Range 某个指定的浮点数
Vector 一个4维数,写为 (x,y,z,w)

{option} - 它只对2D,Rect或者Cube贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一个,这些都是OpenGL中TexGen的模式。

Subshader

关于Tag和LOD等属性,由于知识点也不少,所以单独写在了一起,详情请见https://blog.csdn.net/wangjiangrong/article/details/89335208

Shader本体

CGPROGRAM,这是一个开始标记,表明从这里开始是一段CG程序(我们在写Unity的Shader时用的是Cg/HLSL语言)。最后一行的ENDCG与它是对应的,表明CG程序到此结束。

#pragma 编译指令:

// #pargma 关键词 函数名 光照模型 [其它选项]
#pragma surface surfaceFunction lightModel [optionalparams]

例如上面代码中的:

//表示当前是一个 surface 着色器, 函数名是 surf(在下面能找到该函数), 使用 Standard 基于物理系统光照模式, 有一个完整的向前的阴影 (Standard 必须是Unity 5.x后才有)
#pragma surface surf Standard fullforwardshadows

或者

//表示当前是一个 surfac 着色器, 函数名是 surf, 使用 Lambert 兰伯特光照模型, addshadow 表示给物体添加一个阴影
#pragma surface surf Lambert addshadow

内置的光照模型:

1. Standard 光照模型使用 SurfaceOutputStandard 作为输出结构并且匹配了Unity3D内置的Standard Shader(金属流)。 

2. StandardSpecular 光照模型使用 SurfaceOutputStandardSpecular 作为输出结构并且匹配了Unity3D内置的Standard Shader(镜面反射)。

3. LambertBlinnPhong 光照模型则不以物理为基础,使用 SurfaceOutput 作为输出结构,使用它们时可以在低端设备上运行得更快。

可选参数(部分):

详见:《Unity3D高级编程之进阶主程》第七章,Shader(八) - Surface

alpha

透明( Alpha)混合模式。使用它可以写出半透明的着色器(重要!!!

alpha:blend,开启alpha混合。

alpha:fade,开启传统的渐进透明函数。

alpha:premul,开启左自乘alpha透明度。

alphatest:VariableName 透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量
vertex:VertexFunction 自定义的顶点函数(vertex function)
nolightmap   在这个着色器上禁用光照贴图(lightmap) (适合写一些小着色器)
fullforwardshadows  正向(forward)渲染路径中支持所有阴影类型
dualforward  正向(forward)渲染路径中使用 双重光照贴图(dual lightmaps)
addshadow  添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上
finalcolor:ColorFunction  自定义的最终颜色函数(final color function)。 请参考范例:表面着色器例子(Surface Shader Examples

#pragma target 3.0  使用model 3.0 可以得到一个更好的光照效果, 默认是2.0

变量:

在Properties中我们定义了不少的着色器属性,但是要在CG代码块中访问那些属性必须使用和之前变量相同的名字进行声明。如果在Properties使用2D,CG里要用sampler2D,代表使用的是2维纹理(相应的,还有sampler1D,sampler3D,samplerCube等等格式),如果在Properties使用color, CG里要用fixed4,如果在Properties使用Range, CG里要用half,实际上描述的是一个float。

例如:

//Properties
_MainTex ("Albedo (RGB)", 2D) = "white" {}

//SubShader
sampler2D _MainTex;

Input,SurfaceOutputStandard:

前面提到着色器就是给定了输入,然后给出输出进行着色的代码。CG规定了声明为表面着色器的方法(就是后面的surf方法)的参数类型和名字,因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutputStandard结构。

其中Input其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input结构中,传入surf函数使用:

// 输入结构体用于模述UV的坐标。必须命名为Input
struct Input {
    // 变量必须是uv_开头,_号后面的_MainTex自动对应Properties中的_MainTex和sampler2D
    // _MainTex 也是不能变的。
    float2 uv_MainTex;
};

这个结构体中定义了一个float2的变量,通过访问uv_MainTex即可取得这张贴图当前需要计算的点的坐标值了。

除此以外我们还可以添加如下数据:

float3 viewDir 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值
float4 COLOR 每个顶点(per-vertex)颜色的插值
float4 screenPos  屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息
float3 worldPos 世界空间中的位置
float3 worldRefl 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数
float3 worldNormal 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数
INTERNAL_DATA 当输入结构包含worldRefl或worldNormal且表面函数会写入输出结构的Normal字段时需包含此声明

知识点1:float2,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。例如float4 num,表示变量num中包含四个float的数据。可以使用.xyzw,.rgba或它们的部分比如.x等,来获得某个值。num.xyzw,num.x。

知识点2:UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。

SurfaceOutputStandard是已经定义好了里面类型的输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。

//SurfaceOutput原型如下: 
struct SurfaceOutput{
    fixed3 Albedo;     // 漫反射颜色
    fixed3 Normal;     // 切线空间法线
    fixed3 Emission;   // 自发光
    half Specular;     // 镜面 in 0..1 range
    fixed Gloss;       // 光泽度
    fixed Alpha;       // 透明度
};

//SurfaceOutputStandard原型如下:    
struct SurfaceOutputStandard {  
    fixed3 Albedo;
    fixed3 Normal;
    half3 Emission;
    half Metallic;     // 金属度;取0为非金属, 取1为金属  
    half Smoothness;   // 光泽度;取0为非常粗糙, 取1为非常光滑  
    half Occlusion;    // 遮挡(默认值为1)  
    fixed Alpha;
};

函数:

最后就是我们在#pragma中声明的函数surf了。例子中,我们根据输入的值,对金属度和光泽度进行了新的赋值,并且对_MainTex输入点的颜色乘以了Properties中输入的颜色。并将rgb赋值给了输出的像素颜色,将a值赋予透明度。

知识点:tex2d函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4。

最后我们在场景中创建一个Cube,然后拖上自己新建的material,选择刚刚创建的shader后,通过设置相关属性,即可看见效果了。

发布了71 篇原创文章 · 获赞 160 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/89207014