unity shader详解
在日常开发中,我们可能会熟练地使用 Shader,但对其原理和细节却缺乏深入了解。本文将详细介绍 Unity Shader 的相关知识,包括基本概念、光照模型、内置 Shader 以及实战示例。
Shading 概念
在素描中,Shading 指的是给物体绘制明暗调子。在图形学里,Shading 则是为 Mesh 上色的过程。Mesh 是由一堆三角面片组成的,包含顶点坐标、法线坐标、UV 坐标等信息。根据维基百科的定义,Shading 是根据物体相对于光线的角度及其与光源的距离,改变物体颜色以生成逼真效果的过程。我们编写的处理 Shading 的程序被称为 Shader,中文叫做着色器。该程序的输入包括颜色、纹理、坐标等,输出则是 Mesh 网格的最终颜色。
光照模型
光照模型主要包含两个部分:光源的定义和 Mesh 表面的光照计算。
光源类型
根据现实中的光源,可将其抽象为点光源、方向光源和聚光灯。
Mesh 表面光照计算
主要计算三个部分:环境光(Ambient light)、漫反射光(Diffuse light)和全反射光(Specular light)。下面详细介绍 Lambert 光照模型。
Lambert 光照模型
Lambert 光照模型属于经验模型,主要用于简单模拟粗糙物体表面的光照现象。该模型假设物体表面为理想漫反射体(即只产生漫反射现象,也称为 Lambert 反射体),并且场景中存在两种光:环境光和方向光。我们分别计算这两种光照射到粗糙物体表面所产生的光照现象,最后将两个结果相加,得出反射后的光强值。
环境光计算
环境光的计算公式为: [I_{ambdiff} = K_d \times I_a] 其中,(K_d) 为粗糙物体表面材质对光的反射系数,由程序编写者在宿主程序中给出;(I_a) 为环境光的光强,通常是一个 float3 型的变量,表示环境光的颜色数值。
方向光计算
方向光的计算公式为: [I_{ldiff} = K_d \times Il \times \cos\alpha] 根据向量的点积公式 (\cos\alpha = \mathbf{N} \cdot \mathbf{L}),方向光的计算公式可变为: [I{ldiff} = K_d \times I_l \times (\mathbf{N} \cdot \mathbf{L})] 其中,(\mathbf{N}) 是物体表面的法线向量,(\mathbf{L}) 是光线的方向向量。
漫反射光强
综上,漫反射后的光强为: [I_{diff} = K_d \times I_a + K_d \times I_l \times (\mathbf{N} \cdot \mathbf{L})]
Build-in Shader
Build-in Shader 是 Unity3D 自带的 Shader。
MeshFilter、MeshRenderer 和 Skinned Mesh Renderer
当将一个静态的 Mesh 网格添加到场景中时,Unity 会自动为该 GameObject 添加 MeshFilter 和 MeshRenderer 组件。
- MeshFilter:用于保存网格过滤器,从资源中获取网格信息(Mesh),并将其传递给 MeshRenderer。
- MeshRenderer:从 MeshFilter 获得几何形状,并根据物体的 Transform 组件的定义位置进行渲染。
- Skinned Mesh Renderer:当导入有蒙皮的网格时,该组件会自动添加到导入的网格。
材质和 Shader 的关系
在 3ds Max 中,给模型添加贴图需要新建一个材质球。Shader 实际上是一小段程序,它负责将输入的 Mesh 以指定的方式与输入的贴图或颜色等组合作用,然后输出结果。绘图单元依据这个输出来将图像绘制到屏幕上。输入的贴图或颜色等,加上对应的 Shader,以及对 Shader 的特定参数设置,将这些内容(Shader 及输入参数)打包存储在一起,就得到了一个 Material(材质)。之后,我们可以将材质赋予合适的 Renderer(渲染器)来进行渲染。
实战示例
步骤 1:准备工作
打开 Unity3D(U3D),将准备好的模型拖到场景中。拖进来的模型会自带一个 Skinned Mesh Renderer,用于显示带动画的模型,并且 Unity 会自动创建一个材质 material_0,使用自带的 Diffuse Shader 进行渲染,并将其赋给该 Renderer。
步骤 2:创建 Shader 和 Material
点击 Project 面板的 Create,创建一个 Shader 和 Material,名字都叫 Test0。
步骤 3:分析 Test0.shader
以下是 Test0.shader 的代码:
Shader "Custom/Test0" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
代码解释
- 第一行:定义 Shader 的名字为 Custom/Test0。如果 Shader 成功编译,就可以在 Unity 的 Shader 中找到它。
- 2 - 4 行:定义属性。这里定义了一个 2D 的纹理 _MainTex。属性定义的语法为:
_Name("Display Name", type) = defaultValue[{options}]。 - 第 6 行:标签。
"RenderType" = "Opaque"告诉系统应该在渲染非透明物体时调用该 Shader。与之相对的是"RenderType" = "Transparent",表示在渲染含有透明效果的物体时调用。 - 第 7 行:LOD(Level of Detail)。这里指定为 200,这是 Unity 的内建 Diffuse 着色器的设定值。这个数值决定了我们能用什么样的 Shader。在 Unity 的 Quality Settings 中可以设定允许的最大 LOD,当设定的 LOD 小于 SubShader 所指定的 LOD 时,这个 SubShader 将不可用。我们在实现自己的 Shader 时,可以参考 Unity 内建 Shader 定义的一组 LOD 数值来设定自己的 LOD,以便在调整画质时进行精确控制。
- 9 - 23 行:一段 CG 的代码。
- 第 10 行:声明要写一个表面 Shader,并指定了光照模型。其写法为
#pragma surface surfaceFunction lightModel [optionalparams]。在我们的代码中,声明了一个表面着色器,实际的代码在surf函数中,使用 Lambert(普通的 Diffuse)作为光照模型。 - 第 12 行:声明 2D 纹理 _MainTex。在 CG 程序中,要访问在 Properties 中定义的变量,必须使用相同的名字进行声明。
sampler2D _MainTex;再次声明并链接了 _MainTex,使得后续的 CG 程序能够使用这个变量。 - 14 - 16 行:定义一个结构体
Input,可以将参与计算的数据放入该结构体中,传入surf函数使用。这里只放了 UV 坐标。在 CG 程序中,在贴图变量前加上uv两个字母,表示提取它的 UV 值。 - 18 - 22 行:表面着色器的着色函数
surf。CG 规定了表面着色器方法的参数类型和名字,第一个参数是Input结构,第二个参数是inout的SurfaceOutput结构。这里使用了tex2d函数在贴图上进行采样,将采样结果的 RGB 值赋予输出的像素颜色,将 A 值赋予透明度。
- 第 10 行:声明要写一个表面 Shader,并指定了光照模型。其写法为
- 第 25 行:在所有 Subshaders 之后定义了一个 FallBack。如果没有一个 Subshaders 可以在当前硬件上运行,那么就尝试使用 Fallback 后面的 Shader。
步骤 4:应用材质
将创建的材质 Test0 拖到模型的 Skinned Mesh Renderer 的 Material 上。由于初始时没有贴图,模型会显示为灰色。将贴图拖到 Shader 上,此时 Mesh 上最终的颜色是 Lambert 光照模型计算出来的颜色。
通过以上步骤,我们完成了一个简单的 Shader 入门示例。希望本文能帮助你更好地理解 Unity Shader 的基本原理和使用方法。