unity shader详解

2015年01月28日 13:40 0 点赞 0 评论 更新于 2025-11-21 15:37

在日常开发中,我们可能会熟练地使用 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"
}

代码解释

  1. 第一行:定义 Shader 的名字为 Custom/Test0。如果 Shader 成功编译,就可以在 Unity 的 Shader 中找到它。
  2. 2 - 4 行:定义属性。这里定义了一个 2D 的纹理 _MainTex。属性定义的语法为:_Name("Display Name", type) = defaultValue[{options}]
  3. 第 6 行:标签。"RenderType" = "Opaque" 告诉系统应该在渲染非透明物体时调用该 Shader。与之相对的是 "RenderType" = "Transparent",表示在渲染含有透明效果的物体时调用。
  4. 第 7 行:LOD(Level of Detail)。这里指定为 200,这是 Unity 的内建 Diffuse 着色器的设定值。这个数值决定了我们能用什么样的 Shader。在 Unity 的 Quality Settings 中可以设定允许的最大 LOD,当设定的 LOD 小于 SubShader 所指定的 LOD 时,这个 SubShader 将不可用。我们在实现自己的 Shader 时,可以参考 Unity 内建 Shader 定义的一组 LOD 数值来设定自己的 LOD,以便在调整画质时进行精确控制。
  5. 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 结构,第二个参数是 inoutSurfaceOutput 结构。这里使用了 tex2d 函数在贴图上进行采样,将采样结果的 RGB 值赋予输出的像素颜色,将 A 值赋予透明度。
  6. 第 25 行:在所有 Subshaders 之后定义了一个 FallBack。如果没有一个 Subshaders 可以在当前硬件上运行,那么就尝试使用 Fallback 后面的 Shader。

步骤 4:应用材质

将创建的材质 Test0 拖到模型的 Skinned Mesh Renderer 的 Material 上。由于初始时没有贴图,模型会显示为灰色。将贴图拖到 Shader 上,此时 Mesh 上最终的颜色是 Lambert 光照模型计算出来的颜色。

通过以上步骤,我们完成了一个简单的 Shader 入门示例。希望本文能帮助你更好地理解 Unity Shader 的基本原理和使用方法。

作者信息

feifeila

feifeila

共发布了 3994 篇文章