文章目錄
  1. 1. Rendering
  2. 2. Unity Shader
    1. 2.1. Surface Shaders
    2. 2.2. Vertex and Fragment Shaders
  3. 3. Unity Shader入门精要
    1. 3.1. 渲染管线
    2. 3.2. Unity Shader基础
      1. 3.2.1. 材质和Unity Shader
      2. 3.2.2. ShaderLab
      3. 3.2.3. Unity Shader结构
    3. 3.3. 数学相关知识
    4. 3.4. Unity Shader学习之旅
      1. 3.4.1. 一个最简单的顶点/片元着色器
      2. 3.4.2. Unity内置文件和变量
      3. 3.4.3. CG/HLSL语义
      4. 3.4.4. Shader Debugger
        1. 3.4.4.1. Color Info
        2. 3.4.4.2. Unity Frame Debugger
        3. 3.4.4.3. VS Graphics Debugger
      5. 3.4.5. NVIDIA NSight
    5. 3.5. Unity中的基础光照
      1. 3.5.1. 光照知识回顾
      2. 3.5.2. 标准光照模型
        1. 3.5.2.1. 漫反射光照模型
          1. 3.5.2.1.1. 兰伯特光照模型
          2. 3.5.2.1.2. 半兰伯特(Half Lambert)光照模型
        2. 3.5.2.2. 高光反射光照模型
      3. 3.5.3. Lighting in Unity
        1. 3.5.3.1. Realtime Lighting
        2. 3.5.3.2. Precomputed Lighting
          1. 3.5.3.2.1. Bake Realtime Lighting实战
        3. 3.5.3.3. Render Path
        4. 3.5.3.4. Color Space
      4. 3.5.4. High Dynamic Range
    6. 3.6. 基础纹理
      1. 3.6.1. 纹理类型分类
        1. 3.6.1.1. Delfaut(Advanced)
        2. 3.6.1.2. Normal Map
        3. 3.6.1.3. Sprite
        4. 3.6.1.4. Lightmap
      2. 3.6.2. 纹理大小
      3. 3.6.3. 图片格式
      4. 3.6.4. 纹理压缩方式
      5. 3.6.5. Unity纹理使用
        1. 3.6.5.1. 纹理的基础使用
        2. 3.6.5.2. Mipmap
        3. 3.6.5.3. 凹凸映射
          1. 3.6.5.3.1. Height Map
          2. 3.6.5.3.2. Normal Mapping
          3. 3.6.5.3.3. Ramp Texture(渐变纹理)
          4. 3.6.5.3.4. Mask Texture(遮罩纹理)
    7. 3.7. 透明效果
  4. 4. Reference Website

参考书籍《Unity Shader 入门精要》 — 冯乐乐(一位在图形渲染方面很厉害的女生)

使用的Unity版本:
Unity 5.4.0f3

Rendering

关于图形渲染相关知识以及OpenGL相关学习参见:
Computer_Graphic_Study
OpenGL_Study

Unity Shader

首先让我们通过官网的一张图来看看Unity Shader里面占据重要地位的3D Model,Material,Shader之间的关系:
UnityShaderFundamention
可以看出Material会决定使用哪些Texture和Shader去渲染Model。
Shader是针对Texture和Model去做运算的地方。

Unity Shader最主要的有两种(还有Image Effect Shader,Computer Shader等后续会学习):

  1. Surface shader
    Time to use — “Whenever the material you want to simulate needs to be affected by lights in a realistic way, chances are you’ll need a surface shader. Surface shaders hide the calculations of how light is reflected and allows to specify “intuitive” properties such as the albedo, the normals, the reflectivity and so on in a function called surf.”
    可以看出Unity的Surface Shader帮我们把光照对物体的影响的计算抽象了出来。我们只需设定一些控制系数,然后通过Surface Shader的surf方法就能触发光照作用对表面的运算。
    下面简单看下官网给出的Surface Shader Code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    CGPROGRAM
    // Uses the Lambertian lighting model
    #pragma surface surf Lambert

    sampler2D _MainTex; // The input texture

    struct Input {
    float2 uv_MainTex;
    };

    void surf (Input IN, inout SurfaceOutput o) {
    o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    }
    ENDCG

    Note:
    必须使用#pragma surface去表示是surface shader
    Surface Shader的Code是包含在SubShader里的并且必须写在CGPROGRAM和ENDCG标识之间。

  2. Fragment and Vertex Shaders
    Fragment and Vertex Shaders就跟OpenGL里差不多,model的vertex会经历完整的Shader Pipeline,最终通过计算得出最终的颜色。
    让我们来看看官网给出的Fragment and Vertex Shaders Code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Pass {
    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag

    struct vertInput {
    float4 pos : POSITION;
    };

    struct vertOutput {
    float4 pos : SV_POSITION;
    };

    vertOutput vert(vertInput input) {
    vertOutput o;
    o.pos = mul(UNITY_MATRIX_MVP, input.pos);
    return o;
    }

    half4 frag(vertOutput output) : COLOR {
    return half4(1.0, 0.0, 0.0, 1.0);
    }
    ENDCG
    }

可以看出vert方法好比OpenGL里vertex shader的main函数入口。
frag方法好比OpenGL里fragment shader的main函数入口。
这里frag方法return的half4相当于OpenGL FS里的FragColor。
可以看出我们还是需要在VS里先将vertex变换到透视投影坐标系,然后最后传递给FS,FS计算出最终的颜色。
Note:
这里需要注意的一点是,在VS和FS Shader里,code是包含在Pass {…..}里的。

让我们来看看官网给出的Shader的大体结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Shader "MyShader"
{
Properties
{
// The properties of your shaders
// - textures
// - colours
// - parameters
// ...
}

SubShader
{
// The code of your shaders
// - surface shader
// OR
// - vertex and fragment shader
// OR
// - fixed function shader
}
}

Properties:
“The properties of your shader are somehow equivalent to the public fields in a C# script; they’ll appear in the inspector of your material, giving you the chance to tweak them.”
从这里看出Properties在Unity里相当于OPENGL Shader里定义的Uniform,Sampler,全局变量等,用于作为可控的输入。
让我们来看看官网给出的例子

1
2
3
4
5
6
7
8
9
10
11
12
Properties
{
_MyTexture ("My texture", 2D) = "white" {}
_MyNormalMap ("My normal map", 2D) = "bump" {} // Grey

_MyInt ("My integer", Int) = 2
_MyFloat ("My float", Float) = 1.5
_MyRange ("My range", Range(0.0, 1.0)) = 0.5

_MyColor ("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A)
_MyVector ("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)
}

让我们先看看对应显示在Unity面板是什么模样:
UnityShaderProperty
2D代表_MyTexture,_MyNormalMap是texture类型.Color代表颜色。Range代表范围值……
可以看到Properties里定义的变量都显示在了Unity面板里用于控制。但是真正通过Shader去访问,我们还需要在SubShader里重新定义对应的变量。
SubShader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SubShader
{
// Code of the shader
// ...
sampler2D _MyTexture;
sampler2D _MyNormalMap;

int _MyInt;
float _MyFloat;
float _MyRange;
half4 _MyColor;
float4 _MyVector;

// Code of the shader
// ...
}

可以看出Texture在Unity Shader里面的定义和在OpenGL Shader里差不多,也是通过sampler2D,只是不再需要加uniform修饰了。
这里值得注意的是Color对应的不是Vector4而是half4。
还有就是在Properties里定义的变量名在SubShader要一一对应。

接下来让我们看看官网给出的SubShader里Shader具体是怎样的格式:

1
2
3
4
5
6
7
8
9
10
11
12
SubShader
{
Tags
{
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
CGPROGRAM
// Cg / HLSL code of the shader
// ...
ENDCG
}

真正的Shader Code是在CGPROGRAM和ENGCG里。
这里需要先提一下Tags的作用,Tags是为了告诉Unity我们Shader的特定属性(比如RenderType-渲染类型,Queue—渲染顺序)。

Surface Shaders

让我们看看Surface Shader的大体流程:
下图来源
SurfaceShaderFlowChart
从上图可以看出,Surace Shader通过Surface function生成最终参与光照计算所需的数据(rendering properties — e.g. texture……)。

让我们来看看Surface Shader里的关键组成部分:

  1. Surface function
    Surface function把model data作为输入,把rendering properties作为输出,这些rendering properties最后会参与光照的计算得到最终颜色。
  2. Input
    Input structure一般包含了texture相关的信息(比如 UV坐标信息),也可以包含一些额外信息(参见)
    Note:
    定义texture coordinate的时候必须以uv或者uv2开头加texture名字。
  3. SurfaceOutput
    SurfaceOutput描述了surface的属性,这些属性最终会参与光照计算。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct SurfaceOutput
    {
    fixed3 Albedo; // diffuse color
    fixed3 Normal; // tangent space normal, if written
    fixed3 Emission;
    half Specular; // specular power in 0..1 range
    fixed Gloss; // specular intensity
    fixed Alpha; // alpha for transparencies
    };

    我们可以通过Shader去计算上面参与光照计算的属性去影响Surface的最终效果,从而达到Surface Shader的真正目的。

  4. Light Model
    Light Model是真正通过SurfaceOutput属性去计算光照的地方,通过使用或编写不同的Light Model我们可以实现不同的光照影响效果。
    那么如何定义光照运算函数了?
    1. half4 Lighting (SurfaceOutput s, half3 lightDir, half atten);
      因为没有viewDir所以这里是用于不需要知道观察点的Ambient光照计算
    2. half4 Lighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
      因为有了viewDir,知道了观察点信息,我们可以计算出需要知道观察点的Diffuse和Specular光照。
    3. half4 Lighting_PrePass (SurfaceOutput s, half4 light);
      这个用于Deferer Shading(但这里我比较好奇的是如果要计算Diffuse和Specular光照还是需要知道lightDir和viewDir等信息才对。)
      Note:
      自定义光照模型的时候,我们不需要定义所有的光照函数,只需定义我们需要的即可。
  5. Vertex Function
    Provide the ability to change vertices before sending them to surf.
    定义方式如下:

    1
    void vert(inout appdata_full v){......}

    appdata_full包含了所有vertex相关的信息

  6. Finalcolor Function
    用于对经过Surface Shader处理后的pixel进行最后的处理。
    格式如下:
    1
    2
    3
    4
    5
    #pragma surface surf Lambert finalcolor:mycolor
    void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)
    {

    color *= _ColorTint;
    }

接下来让我们通过创建自己的Surface Shader来感受下Surface Shader实际运用效果(这里我拿了SurviveShooter里的模型做输入):
以下学习参考至Surface shaders in Unity3D
我们删掉默认创建的Shader Code,只简单输出Albedo(Diffuse Color)看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Shader "Custom/TonyShader" {
SubShader {
// 定义渲染顺序 - RenderType
Tags { "RenderType"="Opaque" }

CGPROGRAM
// #pragma surface指明是surface shader
// surf Lambert指明用的光照模式是Lambert(会决定光照是如何计算的)
#pragma surface surf Lambert

struct Input {
float2 color : COLOR;
};

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

SurfaceShaderWithAlbedoSetting
接下来让我们给Hellephant加上纹理图案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Shader "Custom/TonyShader" {
Properties{
// 为了在编辑器里提供控制纹理输入,Properties必须和struct Input里的uv_MainTex对应
_MainTex("Texture", 2D) = "white" {}
}

SubShader{
Tags{ "RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert
// 提供纹理输入
struct Input{
float2 uv_MainTex
};
sampler2D _MainTex;

void surf (Input IN, inout SurfaceOutputStandard o) {
o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback "Diffuse"
}

SurfaceShaderWithTexture
让我们看看现在对应的Unity Material Editor:
SurfaceShaderMaterialEditorWithTexture
图像看起来还不真实,没有深度感,接下来我们添加Normal Mapping 参见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Shader "Custom/TonyShader" {
Properties{
.....
_BumpMap("Bumpmap", 2D) = "bump" {}
}

SubShader{
......

struct Input{
......
float2 uv_BumpMap;
};
......
sampler2D _BumpMap;

void surf (Input IN, inout SurfaceOutputStandard o) {
......
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_Bumpmap));
}
ENDCG
}
Fallback "Diffuse"
}

UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3)
比较有无Normal map的效果,见如下:
SurfaceShaderWithNormalMap
SurfaceShaderWithTexture
接下来让我们通过计算Normal和Viedir的情况去高亮物体:
这里需要用到build-in variable viewDir(代表观察者看顶点的方向)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Shader "Custom/TonyShader" {
Properties{
......
_RimColor("Rim Color", Color) = (1.0, 0.0, 0.0, 0.0)
_RimPower("Rim Power", Range(0.5, 8.0)) = 3.0
}
SubShader {
......

struct Input {
.....
float3 viewDir;
};
......
float4 _RimColor;
float _RimPower;

void surf (Input IN, inout SurfaceOutput o) {
......
half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
o.Emission = _RimColor.rgb * pow(rim, _RimPower);
}
ENDCG
}
FallBack "Diffuse"
}

saturate的作用是把观察者方向和顶点法线夹角Cos值限定在[0,1],再用1减去这个值,表明观察者方向和顶点法线夹角越小rim值越小,当夹角大于等于90度的时候达到rim最大值。从而实现高亮边缘的效果(周边和观察者角度往往比较大)。
SurfaceShaderWithEmssion
接下来我们看看通过vertex function去动态计算新vertex值的效果(这里我们根据顶点法线方向去收缩顶点postion):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Shader "Custom/TonyShader" {
Properties{
......
_Amount("Extrusion Amount", Range(-0.5,0.5)) = 0.0
}
SubShader {
......

CGPROGRAM
// vertex:vert定义了vert方法名
#pragma surface surf Lambert vertex:vert

float _Amount;
// vert方法定义,根据顶点法线去计算新的顶点位置
void vert(inout appdata_full v){
v.vertex.xyz += v.normal * _Amount;
}

void surf (Input IN, inout SurfaceOutput o) {
......
}
ENDCG
}
FallBack "Diffuse"
}

SurfaceShaderWithNegativeExtrusion
SurfaceShaderWithPositiveExtrusion
在vert function里我们也可以自定义一些成员在surface shader里传递,但这样的话vert必须有两个参数,一个inout appdata_full v,一个out Input o,这里的out Input o回座位surf function的Input IN参数作为输入。Custom Input Member详细用法参见
接下来让我们看看Light Model对于最终颜色计算的影响:
待学习了解
最后我们来看看Final Color Function是如何影响最终颜色计算的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Shader "Custom/TonyShader" {
Properties{
......
_ColorTint("Tint", Color) = (0.0, 1.0, 1.0, 1.0)
}
SubShader {
Tags { "RenderType"="Opaque" }

CGPROGRAM
// finalcolor:mycolor定义final function name
#pragma surface surf Lambert vertex:vert finalcolor:mycolor

......

fixed4 _ColorTint;
// final color function格式, color会影响所有lightmaps,light probes等的颜色信息
void mycolor(Input IN, SurfaceOutput o, inout fixed4 color)
{

color *= _ColorTint;
}

void surf (Input IN, inout SurfaceOutput o) {
......
}
ENDCG
}
FallBack "Diffuse"
}

SurfaceShaderWithFinalColorFunction

Vertex and Fragment Shaders

见后续Shader学习

Unity Shader入门精要

渲染管线

参考以前的学习:
Computer_Graphic_Study
OpenGL_Study

Unity Shader基础

材质和Unity Shader

结合前面的学习,我们知道了在Unity里Material会决定使用那些Texture和Shader去渲染Model。

这里再简单梳理下他们之间的关系:

  1. Model(模型) — 最终我们需要通过渲染去实现特定效果的模型。(提供模型数据 e.g. 顶点,法线等)
  2. Texture(贴图数据) — 这里没有直接说成纹理是因为Texture不仅能提供纹理数据,还能提供作为其他计算的数据(e.g. 光照方面的数据等)
  3. Material(材质) — 会决定使用哪些Texture作为数据输入(e.g. 纹理数据,光照计算相关数据等)。通过指定Shader去处理Model和Texture等传入的数据进行处理。而材质面板是与Shader沟通的桥梁,允许我们通过面板暴露Shader暴露出的参数进行渲染控制。
  4. Shader — 处理前面传入的Model,Texture等数据加以处理计算实现各式各样的渲染效果。

Unity Shader分类:

  1. Standard Surface Shader — 前面我们提到的提供了一个包含标准光照模型计算的表面着色器。(最终还是会被编译成对应图形API接口的Vertex and Fragment Shader)
  2. Unlit Shader — 不包含光照的Vertex and Fragment 。Shader,需要自己编写完整的渲染管线流程代码。
  3. Image Effect Shader — 主要用于实现各种屏幕后处理效果。
  4. Compute Shader — 利用GPU的并行性进行一些与常规渲染流水线无关的计算。

这里我们主要学习Unlit Shader。

Shader对于Unity而言也是一种资源文件,所以依然有导入设置:
UnityShaderSettingPanel
Note:
Unity Shader最终还是会被编译成对应图形编程接口(e.g. DX,OpenGL……)的Shader代码实现真正的渲染。

ShaderLab

“ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。ShaderLab类似于CgFX和DX Effects语言。
“(引用自《Unity Shader入门精要》)

ShaderLab帮助我们快速定义Shader编写过程中需要资源和设置等。Unity会最终把ShaderLab编写的Shader编译成对应平台的真正代码和Shader文件,实现跨平台。

Unity Shader结构

这里我们创建一个基本的UnlitShader,结合里面的代码来学习理解(详情参考代码里的注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 材质索引Shader时的路径
Shader "Custom/UnlitShader"
{
// Shader暴露给材质面板的属性用于动态修改控制
// Properties在Unity里相当于OPENGL Shader里定义的Uniform,Sampler,全局变量等,用于作为可控的输入

// 2D代表_MyTexture,_MyNormalMap是texture类型.Color代表颜色。Range代表范围值,更多内容查官网
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

// 每一个Unity Shader至少要包含一个SubShader(Unity会使用第一个可以在目标平台运行的SubShader用于渲染,如果都不支持则会使用Fallback语义指定的Unity Shader)
// SubShader定义了真正的Shader相关代码(渲染流程,渲染状态设置等)
SubShader
{
// Tags是为了告诉Unity我们Shader的特定属性(比如RenderType - 渲染类型,Queue—渲染顺序)。
// Note:
// Tag也可以在Pass里声明,但SubShader里的Tags相当于针对所有Pass的Tag设置,优先于Tag本身的Tag设置
Tags { "RenderType"="Opaque" }
LOD 100

// Pass代表一次完成的渲染流程
// 这个好比OpenGL里实现Shadow Map效果时需要先通过First Pass生成光源点角度的Depth Texture,
// 然后Second Pass通过比较摄像机角度的Depth Texture得出哪些点是Shadow的结论里的Pass一样
Pass
{
// Pass支持定义名字
// Name "PassName"
// 我们可以通过这个名字去指定使用特定Pass
// UsePass "ShaderName/PassName"
// GrabPass负责抓取屏幕并存储在一张纹理中用于后续Pass处理

// Unity Shader采用的是类似CG的Shader语言编写
// Shader代码被包含在CGPROGRAM和ENDCG之间
// Note:
// 如果想要使用GLSL来编写需要包含在GLSLPROGRAM和ENDGLSL之间
CGPROGRAM
// 真正的Shader代码从这里开始
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};

// 映射Properties里暴露给Material材质面板的对象
sampler2D _MainTex;
float4 _MainTex_ST;

// Vertex Shader入口
v2f vert (appdata v)
{

v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}

// Fragment Shader入口
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;
}
ENDCG
}
}

// 在所有Pass都不被支持时,我们可以指定最终使用的Pass
// Fallback "PassName"
}

数学相关知识

参考书籍:
《3D数学基础:图形与游戏开发》
《Fundamentals of Computer Graphics (3rd Edition)》 — Peter Shirley, Steve Marschnner
《Real-Time Rendering, Third Edition》 — Tomas Akenine-Moller, Eric Haines, Naty Hoffman

这里只说几点DX和OpenGL还有Unity之间需要注意的特殊点:

  1. 坐标系
    DX和Unity使用的是左手坐标系。
    OpenGL使用的是右手坐标系。
  2. 行列向量
    DX使用的是行向量(矩阵乘法是右乘)
    OpenGL和Unity使用的是列向量(矩阵乘法是左乘)
  3. 正交矩阵
    因为正交矩阵M(T)=M(-1)
    在提前得知是正交矩阵的前提下可以直接使用M(T)(转置矩阵)快速求的M(-1)(逆矩阵)(e.g. 旋转是正交的)
    逆矩阵可以用于快速回复之前的矩阵变化
  4. 44矩阵
    | M(3
    3) t(31)|
    | 0(1
    3) 1 |
    M(33)代表线性变换(e.g. 旋转,缩放,错切,径向,正交投影…..)
    t(3
    1)代表平移变换
    0(1*3)代表零矩阵
    1代表标量1
  5. 纹理坐标
    OpenGL中(0,0)是左下角
    DX中(0,0)是左上角

关于渲染流程里的坐标系转换相关知识参考之前的学习:
Coordinate Transformations & Perspective Projection

Unity Shader的内置变量参考:
UnityShaderVariables.cginc

Unity Shader学习之旅

本人使用的Unity版本为5.4.0f3 Personal

一个最简单的顶点/片元着色器

依然采用代码实战,跟之前Unity Shader结构讲的类似,但更细节一些,相关解释都写在注释里了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Shader "Custom/SimpleShader"
{
// 定义Material材质与Shader的交流面板
Properties{
// 声明一个Color类型的属性
_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}

SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 定义一个与Material面板交流控制的对应属性
fixed4 _Color;

// 自定义结构体定义vertex shader输入
struct a2v {
// POSITION告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
// NORMAL告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
// TEXTURE0告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};

// 自定义结构体用于vertex shader和fragment shader之间数据传入
struct v2f {
// SV_POSITION告诉Unity,pos是裁剪空间中的顶点坐标位置信息
float4 pos : SV_POSITION;
// color存储颜色信息
fixed3 color : COLOR0;
};

v2f vert (a2v v)
{

// 声明输出结构体实例
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 将v.normal的顶点法线信息从[-1.0,1.0]到[0.0, 1.0]并存储在颜色信息中
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}

// SV_Target告诉渲染器把用户的输出颜色存储到一个渲染目标中,这里默认是帧缓存中
// 因为vertex shader是针对顶点运算,fragment shader是针对fragment
// 所以fragment的输入是顶点数据进行插值后得到的
// 顶点数据只有三角形的三个顶点,中间的fragment信息通过插值得到
// i是vertex shader输出后通过插值传递过来的
fixed4 frag (v2f i) : SV_Target
{

// v.normal映射后的颜色信息
fixed3 c = i.color;
// 使用Material面板控制的_Color参与最终的fragment颜色运算
c *= _Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
}
}
}

Unity内置文件和变量

Unity Shader内置文件官网下载地址

内置文件包含文件:

1
#include "**.cginc"

内置Shader目录结构:
UnityBuildInShaderFolderStructure

  1. CGIncludes — 包含所有的内置包含文件
  2. DefaultResources — 包含了一些内置组件或者功能需要的Unity Shader
  3. DefaultResourcesExtra — 包含了所有的Unity中内置的Unity Shader
  4. Editor — 只有一个脚本,定义了Unity 5引入的Standard Shader所有的材质面板

常用包含文件介绍:

  1. UnityCG.cginc — 包含了常用的帮助函数,宏和结构体等
  2. UnityShaderVariables.cginc — 包含了许多内置的全局变量,e.g. UNITY_MATRIX_MVP
  3. Lighting.cginc — 包含了各种内置的光照模型
  4. HLSLSupport.cginc — 声明了很多用于跨平台编译的宏和定义

Note:
Unity Shader内置文件如果安装时下载安装了,也可以在Editor\Data\CGIncludes目录下找到所有内置文件

CG/HLSL语义

“语义实际上就是让Shader知道从哪里读取数据,并把数据输出到哪里。”

“SV代表的含义是系统数据(system-value)。DX10以后引入的。大部分时候SV_POSITION和POSITION是等价的。但为了更好的跨平台最好尽量采用SV开头的语义来修饰。”

Shader semantics官网查询

Shader Debugger

欲善其事,必先利其器。
在学习Unity Shader之前我们需要知道如何去调试Unity Shader。

Color Info

在渲染的时候把数据信息以颜色的方式渲染到物体上,用于了解特定数据信息。

Unity Frame Debugger

Unity提供的的帧调试器,是基于Unity API层面。(Window -> Frame Debug)

VS Graphics Debugger

VS提供的基于渲染API层面的调试器。
Unity Shader里要写#pragma enable_d3d11_debug_symbols开启DX11渲染

具体调试方法参考:
Implementing Fixed Function TexGen in Shaders Debugging DirectX 11 shaders with Visual Studio

Note:
Frames can only be captured if Unity is running under DirectX 11(只能基于DX11)

NVIDIA NSight

NVIDIA公司提供的基于渲染API层面的调试器。
之前了解过,好像只支持N卡。具体没有使用过。

Unity中的基础光照

光照知识回顾

关于光照的基本学习参考之前OpenGL中的学习:
Light and Shadow

从上面可以看出Light影响着每一个物体最终颜色的成像计算。

  1. 环境光(Ambient) — 环境光与光照方向无关,只需考虑方向光的颜色和方向光所占比重,决定着物体的基本颜色。
  2. 漫反射(Diffuse) — 与光照的方向和顶点normal有关,决定着不同角度(顶点法线)的顶点反射能力。
  3. 镜面反射(Specular) — 与光照的方向和eye观察还有顶点normal有关,决定了不同观察位置的反射高光系数。

标准光照模型

“光照模型 — 根据材质属性(如漫反射属性等),光源信息(如光源方向,辐照度),使用一个等式去计算沿某个方向的出射度的过程。”

BRDF(Bidirectional Reflectance Distribution Function)光照模型
“当给定射入光线的方向和辐射度后,BRDF可以给出在某个出射方向上的光照能量分布。”

那么什么是标准光照模型了?
“标准光照模型只关心直接光照,也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。”

标准光照模型有四部分组成:

  1. 自发光(emissive)
  2. 高光反射(specular)
  3. 漫反射(diffuse)
  4. 环境光(ambient)

从这里可以看出之前学习OpenGL的光照计算可以算是基于标准光照模型来计算的,只是没有考虑自发光的光照运算。不难看出所谓的光照模型其实就是不同的光照计算公式。

在之前OpenGL的学习里,都是通过顶点法线插值后在Fragment Shader里计算Specular,Diffuse,Ambient来进行光照运算的,这种技术被称为Phong着色(Phong Shading)。(针对每一个fragment进行光照运算)

我们也可以针对每一个顶点进行光照运算,e.g. 高洛德着色(Gouraud shading)。针对每个顶点计算光照后,通过线性插值后最终输出显示。(因此不适合非线性的光照计算显示)

但上述标准光照模型(这里称为Blinn-Phong模型)无法模拟真实的物理现象的效果。
“Blinn-Phong模型是各项同性(isotropic)的,也就是说,当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。各向异性(anisotropic)反射性质的效果需要机遇物理的光照模型来运算。”

漫反射光照模型

兰伯特光照模型

c(light) — 光源颜色信息
m(diffuse) — 漫反射系数
n — 顶点法线
l — 光源方向

兰伯特光照模型:
c(diffuse) = (c(light) m(diffuse)) max(0, dot(n,l))

基于顶点的兰伯特光照模型Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// 逐顶点的漫反射光照计算Shader
// 逐顶点是在vertex shader里针对每一个vert计算Diffuse光照然后通过顶点颜色插值显示出来
Shader "Custom/DiffuseVertexLevelShader"
{
// Diffuse Light Model
// c(light) -- 光源颜色信息
// m(diffuse) -- 漫反射系数
// n -- 顶点法线
// l -- 光源方向_WorldToObject
// c(diffuse) = (c(light) * m(diffuse)) * max(0, dot(n, l))

Properties
{
// 给Material面板暴露漫反射系数c(diffuse)用于动态控制漫反射系数
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags { "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

// 因为是计算基于Vertex Level的漫反射光照模型计算
// 所以在vertex shader里已经完成了对光照color的运算
// 这里只需将结果通过插值传递给fragment shader即可
struct v2f
{
float4 pos : SV_POSITION;
float3 color : COLOR;
};

// 对应材质面板的漫反射系数
fixed4 _Diffuse;

v2f vert (a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算漫反射光
// 为了确保法线从物体坐标系转换到世界坐标系正确,这里需要乘以object2world矩阵的逆矩阵的转置矩阵
// _World2Object代表object2world的逆矩阵
// 因为unity使用的是列向量,所以通过mul()_World2Object作为第二个参数相当于左乘_World2Object的转置矩阵
// 之所以只乘以3x3的矩阵是因为平移变换不会影响法线方向
fixed3 worldnormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 假设只有一个全局direction light
fixed3 worldlightdir = normalize(_WorldSpaceLightPos0.xyz);
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldnormal, worldlightdir));
o.color = ambient + diffuse;
return o;
}

fixed4 frag (v2f i) : SV_Target
{

// 将vertex shader计算出的环境光加漫反射颜色显示出来
return fixed4(i.color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

基于片元计算的兰伯特光照模型Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// 逐片元的漫反射光照计算Shader
// 逐片元是在fragment shader里针对每一个fragment计算Diffuse光照
Shader "Custom/DiffuseFragLevelShader"
{
// Diffuse Light Model
// c(light) -- 光源颜色信息
// m(diffuse) -- 漫反射系数
// n -- 顶点法线
// l -- 光源方向
// c(diffuse) = (c(light) * m(diffuse)) * max(0, dot(n, l))

Properties
{
// 给Material面板暴露漫反射系数c(diffuse)用于动态控制漫反射系数
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

// 因为是计算基于Fragment Level的漫反射光照模型计算
// 所以在vertex shader需要把world space的normal传给fragment shader进行diffuse光照运算
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : NORMAL;
};

// 对应材质面板的漫反射系数
fixed4 _Diffuse;

v2f vert(a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 计算漫反射光
// 为了确保法线从物体坐标系转换到世界坐标系正确,这里需要乘以object2world矩阵的逆矩阵的转置矩阵
// _World2Object代表object2world的逆矩阵
// 因为unity使用的是列向量,所以通过mul()_World2Object作为第二个参数相当于左乘_World2Object的转置矩阵
// 之所以只乘以3x3的矩阵是因为平移变换不会影响法线方向
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 假设只有一个全局direction light
fixed3 worldlightdir = normalize(_WorldSpaceLightPos0.xyz);
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldlightdir));
fixed3 color = ambient + diffuse;
// 将计算出的环境光加漫反射颜色显示出来
return fixed4(color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

问题:
背光区域没有明暗变化看起来像一个平面。

半兰伯特(Half Lambert)光照模型

c(light) — 光源颜色信息
m(diffuse) — 漫反射系数
n — 顶点法线
l — 光源方向
a — a倍漫反射辐射度
b — 漫反射辐射度偏移量

半兰伯特光照模型:
c(diffuse) = c(light) m(diffuse) (a * dot(n,l) + b)

基于片元的半拉伯特特光照模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// 逐片元的半兰伯特光照模型没有对漫反射辐射度负值做max处理,
// 而是通过a倍缩放加上b偏移量得出最终的漫反射辐射度,
// 这样一来背面漫反射辐射度也会被映射到不同的值而非0,从而拥有不同的漫反射效果。
Shader "Custom/HalfLambertShader"
{
// Half Labert Model
// c(light) -- 光源颜色信息
// m(diffuse) -- 漫反射系数
// n -- 顶点法线
// l -- 光源方向
// a -- a倍漫反射辐射度
// b -- 漫反射辐射度偏移量
// 半兰伯特光照模型 :
// c(diffuse) = c(light) * m(diffuse) * (a * dot(n, l) + b)

Properties
{
// 给Material面板暴露漫反射系数c(diffuse)用于动态控制漫反射系数
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

// 因为是计算基于Fragment Level的漫反射光照模型计算
// 所以在vertex shader需要把world space的normal传给fragment shader进行diffuse光照运算
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : NORMAL;
};

// 对应材质面板的漫反射系数
fixed4 _Diffuse;

v2f vert(a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 计算漫反射光
// 为了确保法线从物体坐标系转换到世界坐标系正确,这里需要乘以object2world矩阵的逆矩阵的转置矩阵
// _World2Object代表object2world的逆矩阵
// 因为unity使用的是列向量,所以通过mul()_World2Object作为第二个参数相当于左乘_World2Object的转置矩阵
// 之所以只乘以3x3的矩阵是因为平移变换不会影响法线方向
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 假设只有一个全局direction light
fixed3 worldlightdir = normalize(_WorldSpaceLightPos0.xyz);
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// (dot(i.worldNormal, worldlight) * 0.5 + 0.5)将漫反射辐射度映射到[0,1],背面不再全是0
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (dot(i.worldNormal, worldlightdir) * 0.5 + 0.5);
fixed3 color = ambient + diffuse;
// 将计算出的环境光加漫反射颜色显示出来
return fixed4(color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

Note:
半兰伯特光照模型没有对漫反射辐射度负值做max处理,而是通过a倍缩放加上b偏移量得出最终的漫反射辐射度,这样一来背面漫反射辐射度也会被映射到不同的值,从而拥有不同的漫反射效果。

高光反射光照模型

c(light) — 光源颜色信息
m(specular) — 材质高光反射系数
v — 视角方向
r — 反射方向
n — 顶点法线
l — 光源方向
m(gloss) — 发光系数

r可以通过n和l计算出来:
r = 2 dot(-n,l) n + l
跟OpenGL一样,Unity提供了直接计算反射光线方向的API:reflect(i,n)

c(specular) = (c(light) m(specular)) pow(max(0,dot(v,r)), m(gloss))
上面这个公式和之前在OpenGL之前学习的高光反射并没有太多出入,详情参考

这里主要要区分Phong光照模型和Blinn-Phong光照模型:

  1. Phong光照模型
    c(specular) = (c(light) m(specular)) pow(max(0,dot(v,r)), m(gloss))
    dot(v,r) — 是指观察者所在位置和完美反射光线之间夹角
  2. Blinn-Phong光照模型
    h = (v + l) / |v + l|
    c(specular) = (c(light) m(specular)) pow(max(0,dot(n,h)), m(gloss))
    dot(n,h) — 是指将v和l向量归一化后的向量与n之间的夹角

可以看出来两者在计算高光反射的发光基准系数时采用了不同的运算方式。虽然两者都是经验模型,但后者是对于前者的一种改进方式。

跟漫反射一样,高光反射的计算依然有基于顶点和基于片元之分。
基于片元的Phhong高光反射光照模型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// 逐顶点的环境光+漫反射+Phong高光反射光照计算Shader
Shader "Custom/LightModel/PhongSpecularFragLevelShader"
{
// Specular Light Model
// c(light) -- 光源颜色信息
// m(specular) -- 材质高光反射系数
// v -- 视角方向
// r -- 光线反射方向
// n -- 顶点法线
// l -- 光源方向
// m(gloss) -- 发光系数
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(v,r)), m(gloss))

Properties
{
// 给Material面板暴露漫反射系数c(diffuse)用于动态控制漫反射系数
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系的顶点位置
float3 worldNormal : NORMAL; // 世界坐标系的法线
float3 worldPos : TEXCOORD0; // 世界坐标系的顶点位置
};

// 对应材质面板的漫反射系数
fixed4 _Diffuse;
// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;

v2f vert(a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 计算漫反射光
// 为了确保法线从物体坐标系转换到世界坐标系正确,这里需要乘以object2world矩阵的逆矩阵的转置矩阵
// _World2Object代表object2world的逆矩阵
// 因为unity使用的是列向量,所以通过mul()_World2Object作为第二个参数相当于左乘_World2Object的转置矩阵
// 之所以只乘以3x3的矩阵是因为平移变换不会影响法线方向
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 假设只有一个全局direction light
fixed3 worldlightdir = normalize(_WorldSpaceLightPos0.xyz);
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldlightdir));

// 光线反射方向
fixed3 reflectdir = normalize(reflect(-worldlightdir, i.worldNormal));
// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 计算高光反射的颜色信息
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(v,r)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectdir, viewdir)), _Gloss);
// 将高光反射的颜色信息输出到最终的顶点颜色
fixed3 color = ambient + diffuse + specular;

// 将计算出的环境光+漫反射+Phong高光反射颜色显示出来
return fixed4(color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

基于片元的Blinn-Phhong高光反射光照模型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// 逐顶点的环境光+漫反射+BlinnPhong高光反射光照计算Shader
Shader "Custom/LightModel/BlinnPhongSpecularFragLevelShader"
{
// Specular Light Model
// c(light) -- 光源颜色信息
// m(specular) -- 材质高光反射系数
// v -- 视角方向
// r -- 光线反射方向
// n -- 顶点法线
// l -- 光源方向
// m(gloss) -- 发光系数
// h = (v + l) / |v + l|
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))

Properties
{
// 给Material面板暴露漫反射系数c(diffuse)用于动态控制漫反射系数
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系的顶点位置
float3 worldNormal : NORMAL; // 世界坐标系的法线
float3 worldPos : TEXCOORD0; // 世界坐标系的顶点位置
};

// 对应材质面板的漫反射系数
fixed4 _Diffuse;
// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;

v2f vert(a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 计算漫反射光
// 为了确保法线从物体坐标系转换到世界坐标系正确,这里需要乘以object2world矩阵的逆矩阵的转置矩阵
// _World2Object代表object2world的逆矩阵
// 因为unity使用的是列向量,所以通过mul()_World2Object作为第二个参数相当于左乘_World2Object的转置矩阵
// 之所以只乘以3x3的矩阵是因为平移变换不会影响法线方向
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 假设只有一个全局direction light
fixed3 worldlightdir = normalize(_WorldSpaceLightPos0.xyz);
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldlightdir));

// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// h = (v + l) / |v + l| v和l向量归一化后的向量
fixed3 halfdir = normalize(worldlightdir + viewdir);
// 计算高光反射的颜色信息
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfdir)), _Gloss);
// 将高光反射的颜色信息输出到最终的顶点颜色
fixed3 color = ambient + diffuse + specular;

// 将计算出的环境光+漫反射+BlinnPhong高光反射颜色显示出来
return fixed4(color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

效果图:
LightModelShader

Note:
“如果没有全局光照技术,这些自发光的表面并不会真的着凉周围的物体,而是它本身看起来更亮了而已。”
计算机图形学的第一定律:如果它看起来是对的,那么它就是对的。

上面的运算都是我们通过基础的向量,矩阵运算来实现。在Unity中我们可以使用Unity内置的函数来帮助我们快速的计算得到指定结果。详情查看UnityCG.cginc。

Note:
“如果没有全局光照技术,这些自发光的表面并不会真的着凉周围的物体,而是它本身看起来更亮了而已。”
计算机图形学的第一定律:如果它看起来是对的,那么它就是对的。

Lighting in Unity

Global illumination, or ‘GI’, is a term used to describe a range of techniques and mathematical models which attempt to simulate the complex behaviour of light as it bounces and interacts with the world.(GI代表通过技术或者数学模型模拟光照运算交互。e.g. 半兰伯特(Half Lambert)光照模型

Light在Unity主要分为Realtime(相当于Forward Rendering)和Precomputed(预先生成相关光照贴图数据参与光照计算)

Realtime Lighting

directional, spot and point, are realtime. This means that they contribute direct light to the scene and update every frame.(实时的顾名思义实时计算光照)

有了实时光照计算为什么还需要Precomputed了?
Unfortunately, the light rays from Unity’s realtime lights do not bounce when they are used by themselves. In order to create more realistic scenes using techniques such as global illumination we need to enable Unity’s precomputed lighting solutions.(从官方原文来看,实时光照并没有计算光照被其他物体反弹之后的运算量,要想更加真实的光照所以需要Precomputed Lighting)

Precomputed Lighting

Static Lightmap(Baked GI Lighting):
Lightmap — When ‘baking’ a ‘lightmap’, the effects of light on static objects in the scene are calculated and the results are written to textures which are overlaid on top of scene geometry to create the effect of lighting.(Bake GI Light会生成一张LightMap,这张Lightmap记录了场景里所有静态物体的光照运算结果。)

Note:
静态Lightmap一旦生成就不能再被运行时修改,实现不了场景里光照动态变化的效果

Realtime Lightmap(Baked Realtime GI Lighting):
Realtime GI Lightmap弥补了Static Lightmap的不足,可以实现预先计算所有实时光照(这里的实时光照是指全局光照(含间接光照的影响计算)))需要的数据去参与运行时全局光照计算。

To speed up the precompute further Unity doesn’t directly work on lightmaps texels, but instead creates a low resolution approximation of the static geometry in the world, called ‘clusters’.(Unity为了加快Precompute速度,Lightmap的运算并不是texels级而是clusters级,可以理解成相对粗糙一点的精确度)

具体使用哪种光照策略取决于用户实际情况(显存,内存,GPU,CPU等)。

Baked Light Rules:

  1. Only Static GameObject才会参与Baked Lighting。
  2. Light Bake Mode取决于光照的Bake设置。
  3. Mixed的Bake Mode即会参与Bake也会参与Realtime。
  4. 具体Bake设置见Window -> Lighting(设置Bake Static Lightmap还是Realtime)

Note:
Precomputed Lighting是个典型空间换时间的例子。通过预算生成大量的光照信息Lightmap来避免大量的实时光照运算。

Using both Baked GI and Precomputed Realtime GI together in your scene can be detrimental to performance. A good practise is to ensure that only one system is used at a time, by disabling the other globally. (官方建议,Baked GI和Precomputed Realtime GI两者选其一不建议同时使用,避免性能问题)

Bake Realtime Lighting实战
  1. Realtime Resolution
    Realtime Resolution is the number of realtime lightmap texels (texture pixels) used per world unit.(一个world unit使用多少个纹理贴图的像素)

这个参数会影响最终Bake出来的光照采样计算结果,同时也会影响Precompute的时间消耗。合适的值取决于场景对于光照采样精度的需求与Bake时间之间的平衡。如果场景小(比如室内)且光源很多那么精度高一点Bake时间长一点是有意义的。如果场景大(比如室外)光源变化不大,那么精度高对最终的采样光照计算结果并没有太大影响只会增加Bake时间,那这样是不值得的。

  1. Chart(光照图)
    a Chart is an area of a lightmap texture to which we map the lightmap UVs of a given Scene object. We can think of this as a small tile containing an image of the lighting affecting that object.(Chart表示一个光照贴图,用于映射特定Object的Lightmap UV的信息)

A Chart is made up of two parts: irradiance (lighting) and directionality (encoding the dominant light direction).(Chart由两部分组成:辐射度和光照方向)

LightmapChart

Note:
Unity Unit设置的不同对于Realtime Resolution的选择也会有影响。

Baked GI是生成一张Lightmap Texture用于运行时参与光照计算。Baked Realtime Lighting是将信息存到Lighting Data Asset(包含了运行时生成低分辨率lightmap的相关信息)里用于运行时生成低解析度的光照图。

Chart is a minimum of 4x4 texels(Chart最小是4*4像素)。

Charts are created to encompass the UV lightmap coordinates of a Static Mesh Renderer. The number of Charts that an object requires is therefore largely determined by the number of UV shells (pieces) needed to unwrap the object in question.(光照图(Charts)的目的主要是用来包住静态网格着色器(Static Mesh Renderer)的UV贴图坐标。一个物件所需要的光照图数量主要是看物体有多少片UI Shell需要拆解。)

降低Chart的数量是减少Precompute Time的关键因素之一。
除了选择合适的Realtime Resolution,还有什么方式可以帮助我们降低Chart数量了?

  1. 降低Static物体的UV Shells(相当于减少特定物体所需的Lightmap Chart)
  2. 减少Static物体(用Lighting Probe实现)
  3. 减少Clusters数量(PRGI基础运算单元)

在减少UV Shells数量之前,先了解下背后的原理,以及相关帮助和查看窗口:
官方的UV Shells拆分示例:
MultipleUVShells

SingleUIShell

查看LightmapChart:
Scene -> Draw Mode -> GI -> UV Chart
UVChart

查看特定物件的UV Chart:

  1. 选中要查看的物体
  2. Window -> Lighting -> Obejct
  3. 左上角选择Chart模式

SpecificObjectUVChart
Charting模式的预览视窗会用不同的颜色的格子表示光照图,并用浅蓝色的线表示UV贴图。

更多关于UV Chart生成相关的因素,参考:
Unwrapping and Chart reduction

这里简单提一下:
Mesh Renderer上面的:

  1. Auto UV Max Distance(自動最大UV距離)
  2. Auto UV Max Angle(自動最大UV角度)
  3. Preserve UVs(保留UV)
  4. Ignore Normals(忽略法線)

开启Auto Lighting,然后调节相关数值通过UV Chart Preview帮助我们快速查看UV Chart实时效果。

如何通过UV Chats判定失真程度?
UVChartDistortionAnalysi
当启用UI Charts绘制模式时,失真的程度能从场景里的棋盘圆拉扯状况来评估。

如何正确使用Probe Lighting减少Static物体参与PRGI?
Probe lighting is a fast technique for approximating lighting in realtime rendering applications such as games.(光照探测技术是一个能在游戏里让即时光照更逼真的快速演算法)

原理:
Probe lighting works by sampling the incoming lighting at a specific point in 3D space and encoding this information across a sphere using mathematical functions known as spherical harmonics. These coefficients have low storage cost and can be then be quickly ‘unpacked’ at gameplay and used by shaders within the Scene to approximate surface lighting.(光照探测的原理是透过放在3D空间里的探针来接受照明信息,然后用像球鞋函数(spherical harmonics)的数学算法将结果编码在一个球体上,这些信息暂用空间很小,但在游戏运行时解包很快,并参与表面光照计算)

Note:
我們的目的是取样场景里的间接或反射光照。为了让每个光照探头效能花在刀口上, 尽量确保他们对一些变化明显的地方进行采样

如何减少Cluster数量?
让我们先通过查看贴图了解下Cluster真实情况:
ClusterPicture

Clusters sample the albedo of the Static geometry to which they are mapped. Then, during the Light Transport stage of the precompute, the relationship between these Clusters is calculated so that light can be propagated throughout the Cluster network.
(Cluster会对静态物体表面的反射率(Albedo)进行采样,在光照传递计算阶段计算Cluster之间的关系好让光在整个Cluster网之间传递)

一旦与计算完成后,你就可以修改Skybox或光线的位置,强度和颜色,不需要重新与计算,光线就会透过这些Cluster网信息,联通场景材质和发光材质一并考虑计算光照反射。

Clustering或Lit Clustering Scene Mode模式来观察Cluster分布

了解了Cluster相关知识概念后,那么如何真正降低Cluster数量了?
Lightmap Parameters控制,用于公用。
Asset > Create > Lightmap Parameters
通过GameObejct -> Mesh Renderer -> Lightmao Parameters指定
LightMapParameter

具体参数详细学习参考:
Unity預計算即時GI - 8.微調光照參數

PRGI实战:

  1. 设置需要Bake的物体为Static(规划好层级可快速批量设置Static)
  2. Window -> Lighting -> Build(勾选Auto,自动触发Precompute)
  3. 减少Lightmap Chart数量(部分物体(比如凸起的小物件)用Lighting Probe技术而非Static)
  4. 管理Lighting Probe(创建GameObject > Light > Light Probe Group)
  5. 放置Lighting Probe(GameObject > Light > Light Probe Group -> Edit Light Probes)

Baked Lighting AssetBundle实战:
目标:

  1. Baked Lighting,通过打包AB的形式加载还原Baked Lighting
  2. 支持Reflection Probe的AB加载还原(这个只是单纯加载了依赖的Cubemap没有做任何的还原操作,暂时表现是对的,具体待学习了解)

实战:

  1. 新建一个场景,添加必要的物件与光照
  2. 设置需要Bake的物体为Static
  3. 减少Lightmap Chart数量(部分物体(比如凸起的小物件)用Lighting Probe技术而非Static)
  4. 设置所有需要参与Bake的光照为Baked模式(只用于Baked Lighting)
  5. Window -> Lighting -> Build(勾选Auto,自动触发Precompute Lightmap)
  6. 删除所有完成静态烘焙的Light
  7. 记录场景里所使用的光照信息(LightmapSettings.lightmaps)以及所有参与了静态光照的静态物体所使用的光照图信息(Renderer.lightmapIndex和Renderer.lightmapScaleOffset)
  8. 运行时加载所有依赖的光照贴图信息,并还原所有静态物体使用的光照贴图信息

烘焙之后的场景光照使用信息:
LightmapBake

烘焙之后记录下场景使用的光照信息运行时还原:

打包场景AB以及光照信息记录代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var sceneasset = AssetDatabase.LoadAssetAtPath<Object>(abfullpath);
AssetImporter assetimporter = AssetImporter.GetAtPath(abfullpath);
var abname = ABPackageHelper.Singleton.getABTypeCorrespondingName(EABType.E_AB_SCENE, sceneasset.name);
assetimporter.assetBundleName = abname;

// 记录Lightmap相关信息,加载场景时用于还原Lightmap
var scenename = Path.GetFileNameWithoutExtension(abfullpath);

//打开需要打包的场景
var scene = EditorSceneManager.OpenScene(abfullpath);

var scenego = GameObject.Find(scenename);
var lmr = scenego.GetComponent<LightMapRestore>();
if (lmr != null)
{
GameObject.DestroyImmediate(lmr);
}
lmr = scenego.AddComponent<LightMapRestore>();

// 记录Lightmap使用信息
Debug.Log(string.Format("LightmapSettings.lightmaps.Length = {0}", LightmapSettings.lightmaps.Length));

// 测试目的,暂时只默认只有一个Lightmap
lmr.LightMapColor = LightmapSettings.lightmaps[0].lightmapColor == null ? string.Empty : LightmapSettings.lightmaps[0].lightmapColor.name;
lmr.LightMapDir = LightmapSettings.lightmaps[0].lightmapDir == null ? string.Empty : LightmapSettings.lightmaps[0].lightmapDir.name;
lmr.LightMapDataList = new List<LightMapData>();

// 打印所有烘焙对象的Lightmap相关信息(测试查看)
var allrootgos = scene.GetRootGameObjects();
for (int m = 0; m < allrootgos.Length; m++)
{
var allmeshcom = allrootgos[m].transform.GetComponentsInChildren<Renderer>();
for (int n = 0; n < allmeshcom.Length; n++)
{
LightMapData lmd = new LightMapData();
lmd.RendererObject = allmeshcom[n];
lmd.LightMapIndex = allmeshcom[n].lightmapIndex;
lmd.LightMapScaleOffset = allmeshcom[n].lightmapScaleOffset;
lmr.LightMapDataList.Add(lmd);
}
}

// 保存场景
EditorSceneManager.SaveScene(scene);

AssetDatabase.Refresh();

还原代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class LightMapData
{
public Renderer RendererObject;

public int LightMapIndex;

public Vector4 LightMapScaleOffset;
}

/// <summary>
/// 静态光照烘焙还原
/// </summary>
public class LightMapRestore : MonoBehaviour {

public string LightMapColor;

public string LightMapDir;

public List<LightMapData> LightMapDataList;

void Start()
{

// 还原各静态烘焙物体的lightmap信息
foreach(var lmd in LightMapDataList)
{
Debug.Log(string.Format("还原{0}Lightmap信息:LightMapIndex:{1} LightMapScaleOffset:{2}", lmd.RendererObject.name, lmd.LightMapIndex, lmd.LightMapScaleOffset));
lmd.RendererObject.lightmapIndex = lmd.LightMapIndex;
lmd.RendererObject.lightmapScaleOffset = lmd.LightMapScaleOffset;
}
}
}

光照信息记录:
LightMapData

加载还原后的效果:
LightMapRestore

那如何Bake多份Lightmap进行动态切换了?
Unity 5.X如果想Bake多份Lightmap,需要复制保留之前Bake出的Texture,然后通过修改LightmapSetting的值即可(因为是同一个场景,静态物体的lightmapIndex和lightmapScaleOffset值都没变,所以直接切换LightmapSetting里的lightmaps即可)。

1
2
3
4
var lightmaptex = TextureResourceLoader.getInstance().loadTexture(newlightmapname);
var newlightmapdata = new LightmapData();
newlightmapdata.lightmapColor = lightmaptex;
LightmapSettings.lightmaps = new LightmapData[] { newlightmapdata };

参考文章:
Introduction to Precomputed Realtime GI
Unity預計算即時GI

Render Path

Forward Rendering
实时光照运算,计算量跟灯光数量成正比

Deffered Rendering
实时光照运算,计算量跟Pixels数量成正比而非光照

Edit -> Project Setting -> Player -> Rendering -> Rendering Path

Color Space

Linear Color Space
A significant advantage of using Linear space is that the colors supplied to shaders within your scene will brighten linearly as light intensities increase.

Gamma Color Space
Gamma’ Color Space, brightness will quickly begin to turn to white as values go up, which is detrimental to image quality.

Edit -> Project Setting -> Player -> Rendering -> Color Space

High Dynamic Range

HDR(High Dynamic Range) defines how extremely bright or dark colors are captured by scene cameras.(HDR决定光照颜色信息精度)

Note:
HDR is unsupported by some mobile hardware. It is also not supported in Forward Rendering when using techniques such as multi-sample anti-aliasing (MSAA).(Forward Rendering使用MSAA时不支持HDR)

待续

基础纹理

纹理类型分类

一下以Unity支持的导入设置纹理格式来分(一下只重点关注几个重要的):
Note:
随着Unity版本的变化,以下格式可能有细微变化(比如5.4里面的Adavanced已经没有了,取而代之的是细分到其他几个类型里)

Delfaut(Advanced)

这个是Unity在高版本默认的纹理格式,也是平时我们最常用的纹理格式之一。

如果要想将2D纹理图片作为纹理渲染到3D物体上,我们需要选择Default进行纹理格式的基本导入设置。

这里只提几个比较重要的设置,后续会讲到相关的概念。

  1. Non Power of 2(用于将不是2的倍数的纹理图片进行优化处理成2的倍数的宽高)
  2. Generate Mip Maps(预生成多个不同大小的纹理图片,用于不同距离是显示,解决很大的纹理在显示很小的时候失真和浪费的问题)
  3. Wrap Mode(用于设置处理在tiled纹理图片时的方式)

Normal Map

在之前的OpenGL学习中已经接触过Normal Map了,这里只是简单说一下什么是和为什么需要Normal Map。

什么是Normal Map?
Normal Mapping也叫做Dot3 Bump Mapping,它也是Bump Mapping的一种,区别在于Normal Mapping技术直接把Normal存到一张NormalMap里面,从NormalMap里面采回来的值就是Normal,不需要像HeightMap那样再经过额外的计算。
值得注意的是,NormalMap存的Normal是基于切线空间的,因此要进行光照计算时,需要把Normal,Light Direction,View direction统一到同一坐标空间中。

至于什么是Bump Mapping,为什么normal map的normal值是存在tangent space,什么是tangent space,详情参考:
Normal Map

为什么需要Normal Map?
如果要在几何体表面表现出凹凸不平的细节,那么在建模的时候就会需要很多的三角面,如果用这样的模型去实时渲染,出来的效果是非常好,只是性能上很有可能无法忍受。Bump Mapping不需要增加额外的几何信息,就可以达到增强被渲染物体的表面细节的效果,可以大大地提高渲染速度,因此得到了广泛的应用。

Sprite

这个是我们制作UI和2D游戏最常用的格式。

这里也将几个Sprite比较重要的设置:

  1. Packing Tag(Unity自带的打包图集工具所使用用于决定哪些图片时打在同一个图集里的标识。采用TexturePacker单独处理图片格式以及图集见后面)
  2. Pixels Per Unit(用于决定图片在屏幕上的映射显示大小,这个以及Camera Orthographic Size会影响我们制作Sprite时的大小标准以及Pixel Perfect显示。详情参考之前的学习Unity Unit & Pixel Per UnitNGUI 2.7屏幕自适应)
  3. Generate Mip Maps(这个概念和前面Default提到的一样,但大部分时候UI和2D游戏都不需要设置这个,因为UI一般不会有太大的大小变化,2D游戏里的图片同理)

Lightmap

待学习

更多的纹理格式还有Editor GUI and Legacy, Cursor, Cookier, Single Channel这里就不一一详解了,详情参见TextureTypes]

纹理大小

Ideally, Texture dimension sizes should be powers of two on each side (that is, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 pixels (px), and so on). The Textures do not have to be square; that is the width can be different from height.

上面是Unity官方给出的建议,Texture纹理大小宽高最好是2的N次方倍数。

那么为什么会有Power Of 2这个原则了?

  1. 硬件GPU(以前的GPU还不支持Non Power Of 2大小的Texture加载。现在虽然支持了但Power Of 2的加载速度始终要比Non Power Of 2快)
  2. 内存使用(None Power Of 2的Texture在分配内存时始终会分配最近的Power Of 2大小,如果不采用Power Of 2的Texture会浪费部分内存分配)
  3. 纹理压缩格式支持(有些纹理压缩格式只支持特定宽高的纹理图片,比如DXT1只支持mutilple of 4的Texture Size压缩)

Texture的最大Size受限于渲染API(相当于硬件所支持的API)等级有关:
TextureMaxSize

Note:
如果我们想在Unity里使用Non Power Of 2的Texture,我们需要在导入Texture时手动设置成NPO2,不然Unity会自动帮我们转换成Power Of 2的Texture。

参考文章:
OpenGL* Performance Tips: Power of Two Textures Have Better Performance

图片格式

图片是游戏里使用最基础也是最频繁的资源,图片的格式是决定内存占用多少的关键。

所以这里需要了解为什么图片格式会影响最终的内存占用以及如何选择正确的图片格式达到最低的内存占用实现我们所需的效果。以及相关工具帮助我们优化图片内存占用的问题。

图片加载内存计算方式?
图片宽度 图片高度 每个像素点的位数 = 内存大小

图片格式决定了每个像素点的位数的多少,比如RGBA8888(32-bit图)代表每个像素点存储了8 bit的R,G,B,A(分别代表红绿蓝透明通道),总共32 bit即4 byte大小。

所以1024 1024大小的RGBA8888的图片所需内存占用计算如下:
1024
1024 * 4byte = 4M

图片格式有哪些?
以下引用至图片格式及内存占用:
(1)RGBA8888(32-bit)
RGBA 4个通道各占8位,如果想获得最好的图片质量,使用这种格式最靠谱,但他占用的内存会比16位的纹理多一倍,加载速度相对较慢。
(2)RGBA4444(16-bit)
RGBA 4个通道各占4位,他对每个通道都有不错的支持,还能保证相对良好的图片质量,以及加载速度和内存占用。
(3)RGB5A1 (16-bit)
RGB 3个通道各占5位,A通道仅占一位,RGB通道表现良好,A通道只有透明和不透明两种,加载速度和内存占用表现良好。
(4)RGB565 (16-bit)
RGB 3个通道分别占5、6、5位,没有A通道,在16为纹理中,图片质量最好。

如何选择正确的图片格式?
了解各个图片格式的详细信息后,图片格式的选取根据具体需求而定。取舍主要在于内存占用和显示质量。待深入度研究。

参考文章:
图片格式及内存占用

纹理压缩方式

While Unity supports many common image formats as source files for importing your Textures (such as JPG, PNG, PSD and TGA), these formats are not used during realtime rendering by 3D graphics hardware such as a graphics card or mobile device. 3D graphics hardware requires Textures to be compressed in specialized formats which are optimised for fast Texture sampling. The various different platforms and devices available each have their own different proprietary formats.

从上述官网描述可以看出,我们游戏最终使用的纹理格式并非最初我们导入的JPG,PNG,PSD or TGA,而是经过特定压缩方式(ETC,PVRTC,ATC,DXT……)压缩后的指定纹理格式的Texture,目的是为了运行使用Texture时更高效,内存占用和显示质量上可以做取舍。

各平台主流通用的纹理压缩方式:
PC — DXT
Android — ETC
IOS — PVRTC

Unity纹理使用

下面是个人总结的部分知识点:
Unity最终使用的不是我们最初导入到项目工程里的JPG,PNG等,而是通过根据指定压缩方式(e.g. ETC)得到的特定纹理格式(e.g. RGBA8888)的纹理图片,目的是为了减少内存开销以及加快GPU的纹理采样速度。

如何选择正确的纹理格式?
了解各个纹理格式的详细信息后,纹理格式的选取根据具体需求而定。取舍主要在于内存占用和显示质量之间。

那么有什么相关工具可以帮助我们优化图片使用过程中的内存占用问题吗?
答案是:SpritePacker(Unity官方自带工具,最新的Unity 2017貌似推出了Sprite Atlas,打包图集更加灵活) 和 TexturePacker(第三方工具,打包图集(优化空白处减少内存开销,指定图集导出格式减少内存占用,减少drawcall等好处))

那么使用哪一个(Sprite Packer or Texture Packer)用于Unity图集打包工具更好了?
Unity通过SpritePacker模糊了图集的概念,通过Sprite Packer设置Tag打包的图集只有在最后打包的时候才会被打进包里。Sprite Packer虽然与Unity开发结合的比较紧密,但灵活度上有所欠缺,Unity 2017推出的Sprite Atlas在灵活度上加强了不少。
TexturePacker作为第三方工具,结合Unity使用灵活度更高,可以自定义图集打包的各项参数等,最终放到Unity里的图集是已经打包好的图集。
就当前5.4版本的Unity而言,在没有Sprite Altas的前提下,个人觉得采用Texture Packer会更灵活一些,通过Texture Packer我们可以预先打出图集,然后直接将图集打包成ab。

纹理的基础使用

纹理最基础的应用是用于模型外观。

我们直接来看看BlinnPhong光照模型结合纹理贴图的使用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

// Blinn-Phong光照模型结合单独的纹理使用Shader
Shader "Custom/Texture/SingleTextureShader"
{
Properties
{
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
// 给Material面板暴露纹理图片
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION; // 物体坐标系顶点位置
float3 normal : NORMAL; // 物体坐标系法线
float4 texcoord : TEXCOORD0; // 纹理
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系顶点位置
float3 worldnormal : TEXCOORD0; // 世界坐标系法线
float3 worldpos : TEXCOORD1; // 世界坐标系顶点位置
float2 uv : TEXCOORD2; // 纹理映射信息
};

// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;
// 对应材质面板的纹理贴图
sampler2D _MainTex;
// 纹理材质的纹理属性(e.g. 纹理缩放值,偏移值)
float4 _MainTex_ST;

v2f vert (a2v v)
{

v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldnormal = normalize(UnityObjectToWorldNormal(v.normal));
o.worldpos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 通过纹理基础信息和纹理属性得出纹理映射UV坐标
// TRANSFORM_TEX() == (tex.xy * name##_ST.xy + name##_ST.zw)
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{

// 使用纹理贴图的颜色信息作为漫反射系数
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

//漫反射
// 顶点光线入射向量
fixed3 worldlightdir = normalize(UnityWorldSpaceLightDir(i.worldpos.xyz));
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(i.worldnormal, worldlightdir));

// 高光反射
// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(UnityWorldSpaceViewDir(i.worldpos.xyz));
// h = (v + l) / |v + l| v和l向量归一化后的向量
fixed3 halfdir = normalize(worldlightdir + viewdir);
// 计算高光反射的颜色信息
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldnormal, halfdir)), _Gloss);
// 将高光反射的颜色信息输出到最终的顶点颜色
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

BlinnPhong光照模型的纹理使用

Mipmap

说到纹理的使用,这里不得不提的就是Mipmap的应用:
mipmap主要是由于物体在场景中因为距离的缘故会在屏幕显示的大小有变化,如果我们在物体很远,只需要显示很小一块的时候还依然采用很大的纹理贴图,最终显示在屏幕上的纹理会很不清晰(失真)。为了解决这个问题,mipmap应运而生,通过事先生成或指定多个级别的同一纹理贴图,然后在程序运作的过程中通过计算算出应该使用哪一个等级的纹理贴图来避免大纹理小色块失真的问题。

在Unity为Texture开启Mipmap,我们需要把纹理类型设置成Advanced,然后勾选Generate Mip Map即可:
TextureMipmapImportSetting

需要注意的是,Unity开启Mipmap后会导致资源纹理内存占用增加。关于Advanced纹理的各个详细参数信息参考Texture Import Setting

Note:
注意纹理在DX和OpenGL里的起点不一样,DX是左上角(0,0),OpenGL和Unity是左下角(0,0)。

凹凸映射

这里的凹凸映射指的就是之前OpenGL学习的Normal Mapping和Bump Mapping,这里直接使用之前的总结:高模normal map用于低模模型上,即不增加渲染负担又能增加渲染细节。

还有一个凹凸映射叫做高度纹理(Height Map),一般用于存储地形高度信息。

Height Map

用于存储高度信息,通常用于地形的Height Map。

Normal Mapping

关于Normal Mapping要注意的地方主要是两点:

  1. 存储的法线信息是基于切线空间的,需要转换计算位于世界坐标系的法线信息。
  2. 纹理贴图的信息范围是[0,1],而法线信息时[-1,1]需要通过映射转换。

Normal Mapping详细信息参考

这里直接给出使用Unity编写Normal Mapping的使用代码和效果(以下代码采用的是将切线空间的法线转换到世界坐标系参与光照计算的方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/Texture/NormalMappingShader"
{
Properties
{
// 给Material面板暴露主纹理
_MainTex("Texture", 2D) = "white" {}
// 给Material面板暴露法线贴图
// bump是Unity自带的法线纹理
_NormalMap("Normal Map", 2D) = "bump" {}
// 法线贴图的有效程度,用于控制凹凸程度
_BumpScale("Bump Scale", Float) = 1.0
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION; // 物体坐标系顶点位置
float3 normal : NORMAL; // 物体坐标系法线
float4 tangent : TANGENT; // 切线空间的顶点切线(用于计算切线空间的坐标系)
float4 texcoord : TEXCOORD0; // 纹理位置信息(用于通过采样获取纹理的UV信息)
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系顶点位置
float4 uv : TEXCOORD0; // 主纹理和法线纹理的UV坐标信息(xy为主纹理的UV,zw为法线纹理的UV,用于后续的纹理采样)
float4 ttow0 : TEXCOORD1; // 切线空间y轴转换到世界坐标系后的y轴
float4 ttow1 : TEXCOORD2; // 切线空间x轴转换到世界坐标系后的x轴
float4 ttow2 : TEXCOORD3; // 切线空间z轴转换到到世界坐标系后的z轴
};

// 对应材质面板的纹理贴图
sampler2D _MainTex;
// 纹理材质的纹理属性(e.g. 纹理缩放值,偏移值)
float4 _MainTex_ST;
// 对应材质面板法线贴图
sampler2D _NormalMap;
// 法线贴图材质的纹理属性(e.g. 纹理缩放值,偏移值)
float4 _NormalMap_ST;
// 对应材质面板法线贴图有效程度参数
float _BumpScale;
// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;

v2f vert(a2v v)
{

// 通过tangent和normal值计算出将tangent转换到世界坐标系的矩阵(世界坐标系下的XYZ轴)
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

// **_ST
// x contains X tiling value
// y contains Y tiling value
// z contains X offset value
// w contains Y offset value
// 主纹理的UV信息
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// 法线纹理的UV信息
o.uv.zw = v.texcoord.xy * _NormalMap_ST.xy + _NormalMap_ST.zw;

float3 worldpos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 世界坐标系下的normal(相当于y轴)
fixed3 worldnormal = UnityObjectToWorldNormal(v.normal);
// 世界坐标系下的tangent(相当于x轴)
fixed3 worldtangent = UnityObjectToWorldDir(v.tangent.xyz);
// 世界坐标系下的normal和tangent计算出binormal
// 世界坐标系下的binormal(相当于z轴)
fixed3 worldbinormal = cross(worldnormal, worldtangent) * v.tangent.w;

// 填充从tangent space转换到world space的矩阵
// 注意因为是列向量,所以注意填充方式
// worldpos填充在w后续使用
o.ttow0 = float4(worldtangent.x, worldbinormal.x, worldnormal.x, worldpos.x);
o.ttow1 = float4(worldtangent.y, worldbinormal.y, worldnormal.y, worldpos.y);
o.ttow2 = float4(worldtangent.z, worldbinormal.z, worldnormal.z, worldpos.z);

return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 世界坐标系顶点位置
fixed3 worldpos = float3(i.ttow0.w,i.ttow1.w,i.ttow2.w);

// 计算世界坐标系的法线信息
// bump为切线空间的法线信息
fixed3 bump = UnpackNormal(tex2D(_NormalMap, i.uv.zw));

bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

// 将切线空间空间的法线信息转换到世界坐标系下
bump = normalize(half3(dot(i.ttow0.xyz, bump), dot(i.ttow1.xyz, bump), dot(i.ttow2.xyz, bump)));

// 利用转换到世界坐标系的法线信息,正式开始光照计算
// 使用纹理贴图的颜色信息作为漫反射系数
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

// 漫反射
// 顶点光线入射向量
fixed3 worldlightdir = normalize(UnityWorldSpaceLightDir(worldpos));
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump, worldlightdir));

// 高光反射
// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(UnityWorldSpaceViewDir(worldpos));
// h = (v + l) / |v + l| v和l向量归一化后的向量
fixed3 halfdir = normalize(worldlightdir + viewdir);
// 计算高光反射的颜色信息
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump, halfdir)), _Gloss);
// 将高光反射的颜色信息输出到最终的顶点颜色
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

BlinnPhong光照模型的Normal Mapping纹理使用

Ramp Texture(渐变纹理)

比较普遍的用法是使用渐变纹理来控制漫反射光照运算。
让我们直接看代码和效果图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
Shader "Custom/Texture/RampTextureShader"
{
Properties
{
// 给Material面板暴露渐变纹理贴图
_RampTex("Ramp Texture", 2D) = "white" {}
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION; // 物体坐标系顶点位置
float3 normal : NORMAL; // 物体坐标系法线
float4 texcoord : TEXCOORD0; // 纹理位置信息(用于通过采样获取纹理的UV信息)
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系顶点位置
float3 worldnormal : TEXCOORD0; // 世界坐标系下的法线
float3 worldpos : TEXCOORD1; // 世界坐标系下的顶点位置
float2 uv : TEXCOORD2; // 渐变纹理的UV坐标信息
};

// 对应材质面板渐变纹理贴图
sampler2D _RampTex;
// 渐变纹理贴图的纹理属性(e.g. 纹理缩放值,偏移值)
float4 _RampTex_ST;
// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;

v2f vert(a2v v)
{

// 通过tangent和normal值计算出将tangent转换到世界坐标系的矩阵(世界坐标系下的XYZ轴)
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldnormal = UnityObjectToWorldNormal(v.normal);
o.worldpos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{

fixed3 worldnormal = normalize(i.worldnormal);
fixed3 worldlightdir = normalize(UnityWorldSpaceLightDir(i.worldpos));

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 半兰伯特反漫射光照模型
fixed halflambert = dot(i.worldnormal, worldlightdir) * 0.5 + 0.5;
// 利用半兰伯特得到的[0,1]范围值渐变纹理采样
fixed3 diffusecolor = tex2D(_RampTex, fixed2(halflambert, halflambert)).rgb;

// _LightColor0代表全局唯一的direction light光照的颜色信息
fixed3 diffuse = _LightColor0 * diffusecolor;

// 高光反射
// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(UnityWorldSpaceViewDir(i.worldpos));
// h = (v + l) / |v + l| v和l向量归一化后的向量
fixed3 halfdir = normalize(worldlightdir + viewdir);
// 计算高光反射的颜色信息
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldnormal, halfdir)), _Gloss);

// 将高光反射的颜色信息输出到最终的顶点颜色
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

渐变纹理
可以看出利用半兰伯特把法线映射到[0,1]后,作为渐变纹理采样的uv,这样一来面向光照方向的顶点会更靠纹理采样的1,反之完全与光照相反方向的顶点会更靠近纹理采样的0的位置,然后把渐变纹理的采样信息作为漫反射的基础颜色信息参与光照运算。

Mask Texture(遮罩纹理)

“遮罩允许我们可以保护某些区域,使它们免于修改。遮罩纹理已经不止限于保护区域避免修改,而是存储我们希望逐像素控制的表面属性。”
让我们直接看代码和效果图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/Texture/MaskTextureShader"
{
Properties
{
// 给Material面板暴露主纹理
_MainTex("Texture", 2D) = "white" {}
// 给Material面板暴露法线贴图
// bump是Unity自带的法线纹理
_NormalMap("Normal Map", 2D) = "bump" {}
// 法线贴图的有效程度,用于控制凹凸程度
_BumpScale("Bump Scale", Float) = 1.0
// 给Material面板暴露高光反射遮罩纹理
_SpecularMask("Specular Mask", 2D) = "white" {}
// 给Material面板暴露高光反射系数
_Specular("Specular", Color) = (1, 1, 1, 1)
// 给Material面板暴露高光范围
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 指定Pass用于Forward Rendering中的Light计算
Tags{ "LightMode" = "ForwardBase" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct a2v
{
float4 vertex : POSITION; // 物体坐标系顶点位置
float3 normal : NORMAL; // 物体坐标系法线
float4 tangent : TANGENT; // 切线空间的顶点切线(用于计算切线空间的坐标系)
float4 texcoord : TEXCOORD0; // 纹理位置信息(用于通过采样获取纹理的UV信息)
};

struct v2f
{
float4 pos : SV_POSITION; // 投影坐标系顶点位置
float2 uv : TEXCOORD0; // 主纹理和法线纹理和遮罩纹理公用一个UV坐标信息
float4 ttow0 : TEXCOORD1; // 切线空间y轴转换到世界坐标系后的y轴
float4 ttow1 : TEXCOORD2; // 切线空间x轴转换到世界坐标系后的x轴
float4 ttow2 : TEXCOORD3; // 切线空间z轴转换到到世界坐标系后的z轴
};

// 对应材质面板的纹理贴图
sampler2D _MainTex;
// 纹理材质的纹理属性(e.g. 纹理缩放值,偏移值)
float4 _MainTex_ST;
// 对应材质面板法线贴图
sampler2D _NormalMap;
// 对应材质面板法线贴图有效程度参数
float _BumpScale;
// 对应材质面板的高光反射遮罩纹理
sampler2D _SpecularMask;
// 对应材质面板的高光反射系数
fixed4 _Specular;
// 对应材质面板的高光范围
float _Gloss;

v2f vert(a2v v)
{

// 通过tangent和normal值计算出将tangent转换到世界坐标系的矩阵(世界坐标系下的XYZ轴)
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

// 公用的纹理UV信息
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

float3 worldpos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 世界坐标系下的normal(相当于y轴)
fixed3 worldnormal = UnityObjectToWorldNormal(v.normal);
// 世界坐标系下的tangent(相当于x轴)
fixed3 worldtangent = UnityObjectToWorldDir(v.tangent.xyz);
// 世界坐标系下的normal和tangent计算出binormal
// 世界坐标系下的binormal(相当于z轴)
fixed3 worldbinormal = cross(worldnormal, worldtangent) * v.tangent.w;

// 填充从tangent space转换到world space的矩阵
// 注意因为是列向量,所以注意填充方式
// worldpos填充在w后续使用
o.ttow0 = float4(worldtangent.x, worldbinormal.x, worldnormal.x, worldpos.x);
o.ttow1 = float4(worldtangent.y, worldbinormal.y, worldnormal.y, worldpos.y);
o.ttow2 = float4(worldtangent.z, worldbinormal.z, worldnormal.z, worldpos.z);

return o;
}

fixed4 frag(v2f i) : SV_Target
{

// 世界坐标系顶点位置
fixed3 worldpos = float3(i.ttow0.w,i.ttow1.w,i.ttow2.w);

// 计算世界坐标系的法线信息
// bump为切线空间的法线信息
fixed3 bump = UnpackNormal(tex2D(_NormalMap, i.uv));

bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

// 将切线空间空间的法线信息转换到世界坐标系下
bump = normalize(half3(dot(i.ttow0.xyz, bump), dot(i.ttow1.xyz, bump), dot(i.ttow2.xyz, bump)));

// 利用转换到世界坐标系的法线信息,正式开始光照计算
// 使用纹理贴图的颜色信息作为漫反射系数
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;

// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

// 漫反射
// 顶点光线入射向量
fixed3 worldlightdir = normalize(UnityWorldSpaceLightDir(worldpos));
// 得到世界坐标系的顶点法线和direction light方向后可以开始计算漫反射
// _LightColor0代表全局唯一的direction light光照的颜色信息
// 点乘世界坐标系的法线和光源方向得到漫反射辐射度
// saturate防止负值
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump, worldlightdir));

// 高光反射
// 世界坐标系下的从顶点到观察点的观察方向v
fixed3 viewdir = normalize(UnityWorldSpaceViewDir(worldpos));
// h = (v + l) / |v + l| v和l向量归一化后的向量
fixed3 halfdir = normalize(worldlightdir + viewdir);
// 高光遮罩纹理采样信息(这里只是用了r红色通道信息作为mask)
fixed specularmask = tex2D(_SpecularMask, i.uv).r;
// 计算高光反射的颜色信息(高光遮罩纹理参与过滤)
// c(specular) = (c(light) * m(specular)) * pow(max(0, dot(n,h)), m(gloss))
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump, halfdir)), _Gloss) * specularmask;
// 将高光反射的颜色信息输出到最终的顶点颜色
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

高光遮罩纹理
可以看到我们利用遮罩纹理的R颜色信息作为高光的过滤依据,也就是说遮罩纹理里R信息为0的顶点将不会计算高光反射。

透明效果

Reference Website

Surface shaders in Unity3D
Unity Shader内置文件官网下载地址
Shader semantics官网查询
Unity Shader入门精要勘误
Unity Shader入门级你要源代码
Gouraud Shading

文章目錄
  1. 1. Rendering
  2. 2. Unity Shader
    1. 2.1. Surface Shaders
    2. 2.2. Vertex and Fragment Shaders
  3. 3. Unity Shader入门精要
    1. 3.1. 渲染管线
    2. 3.2. Unity Shader基础
      1. 3.2.1. 材质和Unity Shader
      2. 3.2.2. ShaderLab
      3. 3.2.3. Unity Shader结构
    3. 3.3. 数学相关知识
    4. 3.4. Unity Shader学习之旅
      1. 3.4.1. 一个最简单的顶点/片元着色器
      2. 3.4.2. Unity内置文件和变量
      3. 3.4.3. CG/HLSL语义
      4. 3.4.4. Shader Debugger
        1. 3.4.4.1. Color Info
        2. 3.4.4.2. Unity Frame Debugger
        3. 3.4.4.3. VS Graphics Debugger
      5. 3.4.5. NVIDIA NSight
    5. 3.5. Unity中的基础光照
      1. 3.5.1. 光照知识回顾
      2. 3.5.2. 标准光照模型
        1. 3.5.2.1. 漫反射光照模型
          1. 3.5.2.1.1. 兰伯特光照模型
          2. 3.5.2.1.2. 半兰伯特(Half Lambert)光照模型
        2. 3.5.2.2. 高光反射光照模型
      3. 3.5.3. Lighting in Unity
        1. 3.5.3.1. Realtime Lighting
        2. 3.5.3.2. Precomputed Lighting
          1. 3.5.3.2.1. Bake Realtime Lighting实战
        3. 3.5.3.3. Render Path
        4. 3.5.3.4. Color Space
      4. 3.5.4. High Dynamic Range
    6. 3.6. 基础纹理
      1. 3.6.1. 纹理类型分类
        1. 3.6.1.1. Delfaut(Advanced)
        2. 3.6.1.2. Normal Map
        3. 3.6.1.3. Sprite
        4. 3.6.1.4. Lightmap
      2. 3.6.2. 纹理大小
      3. 3.6.3. 图片格式
      4. 3.6.4. 纹理压缩方式
      5. 3.6.5. Unity纹理使用
        1. 3.6.5.1. 纹理的基础使用
        2. 3.6.5.2. Mipmap
        3. 3.6.5.3. 凹凸映射
          1. 3.6.5.3.1. Height Map
          2. 3.6.5.3.2. Normal Mapping
          3. 3.6.5.3.3. Ramp Texture(渐变纹理)
          4. 3.6.5.3.4. Mask Texture(遮罩纹理)
    7. 3.7. 透明效果
  4. 4. Reference Website