设计模式:桥接模式(Bridge Pattern)

将类的功能层次结构和实现层次结构相分离,使二者能够独立地变化,并在两者之间搭建桥梁,实现桥接。它是一种对象结构型模式,又称为柄体(Handle and Body)模式接口(Interfce)模式

意图:在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化。

  • 将类的功能层次分离开,父类拥有子类所共有的功能,子类里实现新的功能。
  • 将类的实现层次分离开,父类声明抽象方法,子类来实现。

桥接模式主要包含以下几个角色

  • Abstraction:抽象类,抽象了功能的实现。
  • RefinedAbstraction:扩充抽象类实现了具体的新的功能,构成功能层次结构。
  • Implementor:实现类接口,提供了用于抽象类的接口。
  • ConcreteImplementor:具体实现类,构成实现层次结构。

现要画一个不同颜色不同形状组合的圆,把抽象化和实现分离开来使其能独立地变化

抽象类:

1
2
3
4
5
6
7
8
9
10
11
public abstract class Shape
{
public Color color;

public void SetColor(Color color)
{
this.color = color;
}

public abstract void Draw();
}

扩充抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Circle : Shape
{
public override void Draw()
{
color.BePaint("圆");
}
}


public class Square : Shape
{
public override void Draw()
{
color.BePaint("正方形");
}
}

实现类的接口:

1
2
3
4
public interface Color
{
void BePaint(string shape);
}

具体实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Red : Color
{
public void BePaint(string shape)
{
Console.WriteLine("红色的" + shape);
}
}

public class Black : Color
{
public void BePaint(string shape)
{
Console.WriteLine("黑色的" + shape);
}
}

画出不同颜色不同形状的圆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Draw
{
public void DoDraw()
{
Color red = new Red();
Color black = new Black();
Shape circle = new Circle();

//画红色的圆
circle.SetColor(red);
circle.Draw();

//画黑色的圆
circle.SetColor(black);
circle.Draw();

}
}

设计模式:享元模式

一、什么是享元模式?

享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。UML结构图如下:
享元模式UML图.png

Flyweight是抽象享元角色。它是产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现;ConcreteFlyweight是具体享元角色,是具体的产品类,实现抽象角色定义的业务;
UnsharedConcreteFlyweight是不可共享的享元角色,一般不会出现在享元工厂中;
FlyweightFactory是享元工厂,它用于构造一个池容器,同时提供从池中获得对象的方法。

二、内部状态与外部状态的区分

享元享元,共享细粒度的单元。那么什么是细粒度的单元呢?如果用乐高积木作比喻,那么一个积木人可以称为一个完整的对象。如果我们把积木人拆开,可以进一步得到头、身躯,腿三个部分。而这些部分,相比于完整的积木人而言,它们三个就是细粒度的单元。

那么为什么要把一个完整的对象区分内外部呢?这岂不是增加了代码的复杂度?好,我们暂时搁置,接下来思考这样一个问题,如果我们要创造 10 个积木人,用程序怎么表示呢?

我们先创建一个积木人的类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LegoMan
{
public string Head;
public string Torso;
public string Leg;

public LegoMan(string head, string torso, string leg)
{
this.Head = head;
this.Torso = torso;
this.Leg = leg;
}
}

接着我们开始创造积木人,先采用直接 new 的方式:

1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args)
{
LegoMan man1 = new LegoMan("head1", "torso1", "leg1");
LegoMan man2 = new LegoMan("head2", "torso2", "leg2");
...
LegoMan man10 = new LegoMan("head10", "torso10", "leg10");
}
}

现在我们得到了 10 个积木人,接下来我们对积木人作出一些限制,我们现在需要 10 个士兵积木人,由于士兵的制服统一,那么这一百个积木人的下半身是完全一样的,也就是说除了 Head,积木人的 Torso 和 Leg 都是一样的。现在我们继续创建十个士兵积木人。好吧,和上面的例子一样,只是传入的后两个参数均一致。

1
LegoMan manN = new LegoMan("headN", "torsoStandard", "legStandard")

接下来,我们思考这样一个问题,如果需要 1000 个士兵积木人呢?如果采用一般的方式,需要创建 1000 个实例对象,但是这 1000 个对象都有着共同的部分,就是它们的 Torso 和 Leg。那我们可不可以把共同的部分抽取出来呢?当然可以,现在我们把 Torso 和 Leg 整合为一个 Body 类,如下:

1
2
3
4
5
6
7
8
9
10
11
public class Body
{
public string Torso;
public string Leg;

public Body(string torso, string leg)
{
this.Torso = torso;
this.Leg = leg;
}
}

既然 Body 被提取出来了,那么 LegoMan 这个类也要被重写了,如下:

1
2
3
4
5
6
7
8
9
10
11
public class LegoMan
{
public string Head;
public Body BodyIntrinsic;

public LegoMan(string head, Body body)
{
this.Head = head;
this.BodyIntrinsic = body;
}
}

现在我们再来创建 1000 个积木人士兵的话,应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
public class Example {
public static void main(String[] args)
{
//先创建一个通用的 Body
Body bodyStandard = new Body("torsoStandard", "legStandard");

LegoMan man1 = new LegoMan("head1", bodyStandard);
LegoMan man2 = new LegoMan("head2", bodyStandard);
...
LegoMan man1000 = new LegoMan("head1000", bodyStandard);
}
}

发现了吗?现在虽然也是1000个 LegoMan 的实例,但是却只有一个 Body,也就是说,1000个积木人士兵 的 Head,共享了一个 Body。听起来很疯狂,九头蛇也才九头,一千个头的怪物得多可怕!?哈哈,虽然积木人的玩具不可能这么拼,但是程序里,这种共享机制是可行的。Body 就是享元模式中的内部状态,一个重复度很高的细粒度单元。而 Head 则对应外部状态,会随着需求发生变化。这么分离的好处也很明显,就是大大减少了总数据量。如果不分离内外部,创建 1000 个积木人士兵的成本就是1000个 Head 和1000个 Body,而采用分离策略后,就只需要1000个 Head 外加1个 Body了。当随着创建对象数量级的增大,这种策略带来的好处会越来越明显。

三、完整的享元模式

理解了分离内外部的原因后,下面简单实现一下享元模式

1.Flyweight抽象类

通过最上面的 UML 图可以看出,Flyweight 被分为两部分,ConcreteFlyweight(共享的内部)和UnsharedConcreteFlyweight(不可共享的外部)。所以 Flyweight 最好被做成接口,或者抽象类,这里用抽象类实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Flyweight 
{
//内部状态
public String intrinsic;
//外部状态
public String extrinsic;

//要求享元角色必须接受外部状态
public Flyweight(String extrinsic)
{
this.extrinsic = extrinsic;
}

//定义业务操作
public abstract void Operate(int extrinsic);

}

2. ConcreteFlyweight类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteFlyweight : Flyweight 
{
//接受外部状态
public ConcreteFlyweight(String extrinsic):base(extrinsic)
{
Debug.Log("共享的 " + extrinsic);
}

//根据外部状态进行逻辑处理
public void Operate(string extrinsic)
{
Debug.Log("处理共享数据 " + extrinsic);
}
}

3. UnsharedConcreteFlyweight类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UnsharedConcreteFlyweight : Flyweight
{
public UnsharedConcreteFlyweight(String extrinsic):base(extrinsic)
{
Debug.Log("非共享的 " + extrinsic);
}

public void Operate(int extrinsic)
{
Debug.Log("处理非共享数据 " + extrinsic);
}

}

4. FlyweightFactory类

既然是处理大量数据,那免不了用一个对象池来进行管理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FlyweightFactory 
{
//定义一个池容器
private static List<Flyweight> pool = new List<Flyweight>();

//享元工厂
public static Flyweight GetFlyweight(String extrinsic)
{
var flyweight = pool.Find(obj => obj.extrinsic == extrinsic);

if (flyweight == null)
{
flyweight = new ConcreteFlyweight(extrinsic);
pool.Add(flyweight);
Debug.Log("新创建 " + extrinsic);
}
else
{
Debug.Log("从池中取出 " + extrinsic);
}

return flyweight;
}
}

5.客户端的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {

public static void main(String[] args)
{
Flyweight flyweight1 = FlyweightFactory.GetFlyweight("one");
flyweight1.Operate("one");

Flyweight flyweight2 = FlyweightFactory.GetFlyweight("two");
flyweight2.Operate("two");

Flyweight flyweight3 = FlyweightFactory.getFlyweight("one");
flyweight3.Operate("one");

Flyweight unsharedFlyweight = new UnsharedConcreteFlyweight("one");
unsharedFlyweight.operate("one");
}
}

打印结果如下:
新创建 one
处理共享数据 one
新创建 two
处理共享数据 two
从池中取出 one
处理共享数据 one
非共享的 one
处理非共享数据 one

参考博客:
https://www.cnblogs.com/adamjwh/p/9070107.html
https://blog.csdn.net/justloveyou_/article/details/55045638

设计模式:命令模式

命令模式在GoF中的定义是:

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式是一种回调的面向对象实现。

游戏设计模式里把它精简为:

命令是具现化的方法调用。

两种术语都意味着将概念变成数据一个对象可以存储在变量中,传给函数。
所以称命令模式为“具现化方法调用”,意思是方法调用被存储在对象中。
类似C#里的回调
把一个对象传递到方法中,让方法内部解析。

下面是一个C#版本的角色控制,传入一个角色,就能调用对应的各种行动。

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
using UnityEngine;

/// <summary>
/// 命令基类
/// </summary>
public abstract class Command
{
public abstract void Execute(BaseCharacter character);
}

/// <summary>
/// 跳的命令
/// </summary>
public class JumpCommand : Command
{
public override void Execute(BaseCharacter character)
{
character.Jump();
}
}

/// <summary>
/// 射击的命令
/// </summary>
public class FireCommand : Command
{
public override void Execute(BaseCharacter character)
{
character.Fire();
}
}

/// <summary>
/// 移动的命令
/// </summary>
public class MoveCommand : Command
{
public override void Execute(BaseCharacter character)
{

}
}

/// <summary>
/// 对输入的解析
/// </summary>
public class InputHandler
{

private JumpCommand buttonA;
private FireCommand buttonD;
private MoveCommand buttonW;

public Command HandleInputAction()
{
if (Input.GetKeyDown(KeyCode.A)) { return buttonA; }
if (Input.GetKeyDown(KeyCode.D)) { return buttonD; }
if (Input.GetKeyDown(KeyCode.W)) { return buttonW; }

return null;
}



private Player mPlayer;
private Enemy mEnemy;

public void Command()
{
Command command = HandleInputAction();

if (command != null)
{
command.Execute(mPlayer);
command.Execute(mEnemy);
}
}
}

public class Player : BaseCharacter
{
public override void Fire()
{
base.Fire();
}
public override void Jump()
{
base.Jump();
}
}

public class Enemy : BaseCharacter
{
public override void Fire()
{
base.Fire();
}
}
/// <summary>
/// 角色的基类
/// </summary>
public abstract class BaseCharacter
{
public virtual void Jump() { }
public virtual void Fire() { }
}

设计模式:状态模式(有限、分层和下推状态机)

实现了最简单的有限状态机

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
public class FSM : MonoBehaviour
{
public MonoStateMachine monoStateMachine = new MonoStateMachine();

public RunState RunState;
public IdleState IdleState;

private void Start()
{
monoStateMachine.StartState(RunState);


}

private void Update()
{
monoStateMachine.Update();
}
}

public class RunState : MonoState
{
public static RunState instance;

public static RunState Instance()
{
if (instance == null)
{
instance = new RunState();
}
return instance;
}

public override void Enter()
{
Debug.Log("进入跑步");
}

public override void Execute()
{
Debug.Log("开始跑步");
}

public override void Exit()
{
Debug.Log("退出跑步");
}


}

public class IdleState : MonoState
{

public static IdleState instance;

public static IdleState Instance()
{
if (instance == null)
{
instance = new IdleState();
}
return instance;
}

public override void Enter()
{

}

public override void Execute()
{

}

public override void Exit()
{

}

}

public class MonoState
{
public virtual void Enter()
{

}
public virtual void Execute()
{

}

public virtual void Exit()
{

}

}

public class MonoStateMachine
{
// private MonoState mOwner;

private MonoState mCurrentState;
private MonoState mPreviousState;
private MonoState mGlobalState;


public MonoStateMachine()
{
mCurrentState = null;
mPreviousState = null;
mGlobalState = null;
}

/// <summary>
/// 设置初始状态
/// </summary>
/// <param name="state"></param>
public void StartState(MonoState state)
{
mCurrentState = state as MonoState;
mCurrentState.Enter();

}

public void ChangeState(MonoState state)
{
if (state == null)
{
Debug.Log("无法找到此状态");
}

mPreviousState = mCurrentState;
mCurrentState.Exit();
//转换后
mCurrentState = state as MonoState;
mCurrentState.Enter();

}
/// <summary>
/// 还原之前的状态
/// </summary>
public void RevertToPreviouState()
{
ChangeState(mPreviousState);
}

/// <summary>
/// 得到当前状态
/// </summary>
/// <returns></returns>
public MonoState GetCurrentState()
{
return mCurrentState;
}

/// <summary>
/// 得到之前的状态
/// </summary>
/// <returns></returns>
public MonoState GetPreviousState()
{
return mPreviousState;
}

public void Update()
{
if (mCurrentState!=null)
{
mCurrentState.Execute();
}
}
}

单例

  • 单例为最常见的一种设计模式,目前在用的主要有两种方式,一种是基于Unity的,一种是基于C#的
  • 一般来说,unity里的单例分为两种,一种是继承于Monobehaviour的,一种是不继承于它的;
  • 这里给出了两种实现方法

使用Unity里的方法

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
public abstract class ScriptSingleton<T>  : MonoBehaviour where T : ScriptSingleton<T>
{
protected static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
//从场景中找T脚本的对象
_instance = FindObjectOfType<T>();
if (FindObjectsOfType<T>().Length > 1)
{
Debug.LogError("场景中的单例脚本数量 > 1:" + _instance.GetType().ToString());
return _instance;
}
//场景中找不到的情况
if (_instance == null)
{
string instanceName = typeof(T).Name;
GameObject instanceGO = GameObject.Find(instanceName);
if (instanceGO == null)
{
instanceGO = new GameObject(instanceName);
DontDestroyOnLoad(instanceGO);
_instance = instanceGO.AddComponent<T>();
DontDestroyOnLoad(_instance);
}
else
{
//场景中已存在同名游戏物体时就打印提示
Debug.LogError("场景中已存在单例:" + instanceGO.name);
}
}
}
return _instance;
}
}

void OnDestroy()
{
_instance = null;
}
}

通过反射实现的单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class Singleton<T> where T : Singleton<T> 
{
protected static T mInstance = null;
protected Singleton() { }
public static T Instance
{
get
{
if (mInstance == null)
{
var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);
if (ctor == null)
{
throw new Exception("Non-public ctor() not found!");
}
mInstance = ctor.Invoke(null) as T; }

return mInstance;
}
}
}
}

模板测试(Stencil Test)相关

介绍

模板测试一般发生在深度测试前,在片段着色器处理完一个片段后执行。和深度测试一样也会丢弃片元。模板测试是根据一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer)

一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。

使用方法

模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以读取这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥,但大体的步骤如下:

  • 启用模板缓冲的写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲的写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

所以,通过使用模板缓冲,我们可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。

具体案例

一般来说,stencil完整的语法如下

1
2
3
4
5
6
7
8
9
10
stencil
{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation
}

我们可以用模板缓冲来实现一个物体轮廓,步骤如下:

  1. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
  2. 渲染物体。
  3. 禁用模板写入以及深度测试。
  4. 将每个物体缩放一点点。
  5. 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
  6. 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
  7. 再次启用模板写入和深度测试。

这个过程将每个物体的片段的模板缓冲设置为1,当我们想要绘制边框的时候,我们主要绘制放大版本的物体中模板测试通过的部分,也就是物体的边框的位置。我们主要使用模板缓冲丢弃了放大版本中属于原物体片段的部分。

面剔除(Culling)

​ 一个3DCube我们最多只能看到3个面,那多余的几个面就能丢弃它,能节省片段着色器的执行数,这就是面剔除(Culling),我们可以分析顶点数据的环绕顺序(Winding Order)来区分正面和背面,而反面在绝大多数情况下是不用渲染的。

在Unity中Cull有三种用于控制几何体的哪一面会被剔除

Cull Back || Front || Off

  • Cull Back——不渲染多边形的背面(默认)。
  • Cull Front——不渲染多边形的正面。这将看到原来被正面遮挡的背面内容。
  • Cull Off——禁用剔除,两个面都会被绘制。同于一些特殊的效果。(一般用于绘制比较薄的对象,如:纸)
默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形。

观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形。如下图:

​ 在顶点数据中,我们将两个三角形都以逆时针顺序定义(正面的三角形是1、2、3,背面的三角形也是1、2、3(如果我们从正面看这个三角形的话))。然而,如果从观察者当前视角使用1、2、3的顺序来绘制的话,从观察者的方向来看,背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的,它现在是以顺时针顺序渲染的了。这正是我们想要剔除(Cull,丢弃)的不可见面了

混合(Blending)

混合(Blending)通常是实现物体透明度(Transparency)的一种技术。

在Unity中提供了渲染队列来实现透明效果,使用SubShader的Queue标签决定渲染队列,索引越小越早渲染。

Background 索引 效果
Background 1000 最先绘制,通常绘制背景
Geometry 2000 默认
AlphaTest 2450 需要透明度测试使用此队列
Transparent 3000 从后往前渲染,透明度混合使用此队列
Overlay 4000 实现叠加效果

Unity中实现物体透明分全透明和半透明两种:

  1. 透明度测试(全透明),给定一个值,不满足条件的都将被舍弃

    函数: void clip (float x)

    例如:

    1
    2
    3
    4
    clip(float x);
    if(x<0.5f){ //舍弃小于0.5的片元
    discard;
    }
  2. 透明度混合(半透明),Blend是Unity提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在的颜色缓冲中的颜色值进行混合。

    一般的混合都是通过以下方程来实现:

    • C source:源颜色向量。这是源自纹理的颜色向量。
    • C destination:目标颜色向量。这是当前储存在颜色缓冲中的颜色向量。
    • F source:源因子值。指定了alpha值对源颜色的影响。
    • F destination:目标因子值。指定了alpha值对目标颜色的影响。

    例如:要实现红和绿两种颜色的混合

    结果就是重叠方形的片段包含了一个60%绿色,40%红色的一种脏兮兮的颜色:

​ 要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:

  1. 先绘制所有不透明的物体。
  2. 对所有透明的物体排序。
  3. 按顺序绘制所有透明的物体。

在Unity中为了得到透明物体的排序我们需要开启深度写入,但这会使透明无法进行,所以需要两个Pass来渲染,第一个开启深度写入,但不输出颜色,第二个Pass进行正常的透明度混合。

1
2
3
4
5
6
7
Pass{
ZWrite On
ColorMask 0
}
Pass{
//混合颜色
}

  • 相同的材质,属性不同,避免创建新的材质

    MaterialPropertyBlock

1
2
3
4
5
6
//会创建一个新的材质
meshRenderer.material.color = clolr;
//不会创建一个新的材质
var propertyBlock = new MaterialPropertyBlock();
propertyBlock.SetColor("_Color", color);
meshRenderer.SetPropertyBlock(propertyBlock);

深度测试相关(ZTest)

在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。而有了深度缓冲以后,绘制物体的顺序就不那么重要了,都能按照远近(Z值)正常显示,这很关键。

深度

深度为该像素点离摄像机的距离(z值),存在深度缓冲中,通常会以16、24、32位float来存储,精度越高越精确。通常为24位

深度测试

将该像素点的z值对比G-Buffer的值,通过则更新为新的深度值,测试失败则丢弃该片段

深度缓冲

深度缓冲中存着深度数据,由于深度缓冲是在片段着色器运行后在屏幕空间进行,现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。

深度值精度

深度缓冲包含一个介于0.0和1.0之前的值,里面存着深度值为距离摄像机的距离,但几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。这样才能更大的利用深度值的精度。

由于非线性方程与 1/z 成正比,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,这就是一个float提供给我们的一半精度了,这在z值很小的情况下提供了非常大的精度。在50.0和100.0之间的z值将会只占2%的float精度,这正是我们所需要的。这样的一个考虑了远近距离的方程是这样的:

变成图大概是这样

深度冲突

一个很常见的错误是俩个平面紧密贴在一起时,深度缓冲没有足够的精度来决定谁在前面,就会出现交替闪烁的现象,叫深度冲突(Z-fighting)

抗深度冲突技术中,最简单的就是使用更高的精度深度缓冲,会牺牲掉一些性能,却能获得更好的效果。