HPP_Graphics_3.3 曲面细分和几何着色器

一、两者的应用列举

1-1.曲面着色器的应用

①海浪、雪地等

    • img
    • 如右图一样,将一条直线进行细分,向一条曲线慢慢逼近

②著名的应用:和置换贴图(DIsplacement mapping,也叫位移贴图)结合使用

    • img
  • 使用普通法线的模型,在边缘部分的凹凸感会不理想

  • 如果使用置换贴图,因为它是真正改变物体的形状,所以边缘部分的凹凸感就会很真实
  • 注意:使用置换贴图,对模型的面数有要求。

  • 正是这个原因,让它和曲面细分着色器有着很好的契合度。

③雪地里出现的脚印

  • 可以用曲面细分着色器进行优化

1-2.为什么不用复杂的模型,而要用曲面细分着色器?

  • 曲面细分着色器可以根据距离/一些规则,动态的调整模型的复杂度,带来更好的性能。

2.几何着色器的应用

①几何动画

  • imgimg
  • 简单的几何动画、甚至可以做一些破碎的效果

②草地等效果(与曲面细分结合)

  • img
    • 自定义草的画法,再和曲面细分着色器结合,就可以得到一个可以动态调整草密度的一个草地效果。

二、从管线顺序来看

  • img

  • 整体顺序:顶点 → 曲面细分 → 几何 → 片元

    • 曲面细分又分为:Hull shader 、Tessellation Primitive Generator 、 Domain shader

      • Hull shader主要作用:定义一些细分的参数(如:每条边上如何细分,内部三角形如何细分)

      • Tessellation Primitive Generator,不可编程的

      • Domain shader:经过曲面细分着色器细分后的点是位于重心空间的,这部分的作用就是把它转化到我们要用的空间。

  • 在D3D11 和 OpenGL中,名字/叫法有差异,问题不大

三、曲面细分着色器-Tessellation shader(TESS)

1.TESS的输入和输出

输入

  • 称为Patch,可以看成是多个顶点的集合,包含每个顶点的属性。(属性是所有顶点共享的,不是每个顶点有独自的属性)

功能

  • 将图元进行细分。

    • 图元可以是三角形、矩形等
  • 不同的图元,输入参数也不一样。

输出

  • 细分后的顶点

2.TESS的流程

Hull shader → Tessellation Primitive Generator → Domain shader

Hull shader

  • 定义细分的参数

    • 设定Tessellation factor以及Inside Tessellation factor
  • (如果需要的话)可以对输入的Patch参数进行改变

Tessellation Primitive Generator

  • 这部分是不可编程、无法控制的
  • 进行细分操作

Domain shader

  • 对细分后的点进行处理,从重心空间(Barycentric coordinate system)转换到屏幕空间

3.Hull shader参数详解

①Tessellation Factor

  • 定义把一条边分为几个部分
  • 切分的方法有三种

    • equal_Spacing
      • 把一条边等分(二、三分等等..)
      • img
  • fractional_even_spacing

    • 向上取最近的偶数
    • 最小值是2
    • 会把周长分为n-2的等长部分、以及两端不等长的部分(两端部分和小数有关,具体看gif)
    • img
  • fractional_odd_spacing

    • 向上取最近的奇数
    • 最小值是1
    • 会把周长分为n-2的等长部分、以及两端不等长的部分
    • img
  • 目的:让细分更加平滑

②Inner Tessellation Factor

  • 定义内部的三角形/矩形是怎么画出来的
  • 三角形情况

  • img

  • 例如上图三等分的情况:

    • 将三条边三等分,然后从一个端点开始,取邻近的两个切分点做延长线,两者的交点就是新三角形的一个端点。以此类推就是左图的效果。
  • 上图四等分、甚至更多点的情况:

    • 上述三等分步骤之后,内部三角形的每个边的等分点做延长线,交点就是
  • 矩形情况

    • imgimg
    • 同样的,做延长线,交点,直到没有交点或者交于重心一个点

4.曲面细分Demo部分

//注:代码直接复制的评论区里同学码出来的,懒得再打一遍了233

Demo1:曲面细分算法展示

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
//曲面细分Demo1
Shader "Unlit/TessShader"
{
Properties
{
_TessellationUniform("TessellationUniform",Range(1,64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
//定义2个函数 hull domain
#pragma hull hullProgram
#pragma domain ds

#pragma vertex tessvert
#pragma fragment frag

#include "UnityCG.cginc"
//引入曲面细分的头文件
#include "Tessellation.cginc"

#pragma target 5.0

struct VertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

VertexOutput vert (VertexInput v)
//这个函数应用在domain函数中,用来空间转换的函数
{
VertexOutput o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.tangent = v.tangent;
o.normal = v.normal;
return o;
}

//有些硬件不支持曲面细分着色器,定义了该宏就能够在不支持的硬件上不会变粉,也不会报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
//顶点着色器结构的定义
struct TessVertex{
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};

struct OutputPatchConstant {
//不同的图元,该结构会有所不同
//该部分用于Hull Shader里面
//定义了patch的属性
//Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};

TessVertex tessvert (VertexInput v){
//顶点着色器函数
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}

float _TessellationUniform;
OutputPatchConstant hsconst (InputPatch<TessVertex,3> patch){
//定义曲面细分的参数
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}

[UNITY_domain("tri")]//确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")]//拆分edge的规则,equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")]//一个patch一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] //不同的图元会对应不同的控制点

TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID){
//定义hullshaderV函数
return patch[id];
}

[UNITY_domain("tri")]//同样需要定义图元
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
//bary:重心坐标
{
VertexInput v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;

VertexOutput o = vert (v);
return o;
}
#endif

float4 frag (VertexOutput i) : SV_Target
{

return float4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

Demo2:和置换贴图结合

  • 基本原理

    • 通过置换贴图的深度,来把顶点沿着它的法线方向进行移动,以此来对mash进行形变。
  • 代码部分和上个Demo的区别也就是在顶点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
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
176
177
//曲面细分Demo2:与置换贴图结合使用
Shader "Unlit/Tess_Diss_Shader"
{
Properties
{
_MainTex("MainTex",2D) = "white"{}
_DisplacementMap("_DisplacementMap",2D)="gray"{}
_DisplacementStrength("DisplacementStrength",Range(0,1)) = 0
_Smoothness("Smoothness",Range(0,5))=0.5
_TessellationUniform("TessellationUniform",Range(1,64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque"
"LightMode"="ForwardBase"}
LOD 100
Pass
{
CGPROGRAM
//定义2个函数 hull domain
#pragma hull hullProgram
#pragma domain ds

#pragma vertex tessvert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"
//引入曲面细分的头文件
#include "Tessellation.cginc"

#pragma target 5.0
float _TessellationUniform;
sampler2D _MainTex;
float4 _MainTex_ST;

sampler2D _DisplacementMap;
float4 _DisplacementMap_ST;
float _DisplacementStrength;
float _Smoothness;

struct VertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 worldPos:TEXCOORD1;
half3 tspace0 :TEXCOORD2;
half3 tspace1 :TEXCOORD3;
half3 tspace2 :TEXCOORD4;
};

VertexOutput vert (VertexInput v)
//这个函数应用在domain函数中,用来空间转换的函数
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
//Displacement
//由于并不是在Fragnent shader中读取图片,GPU无法获取mipmap信息,因此需要使用tex2Dlod来读取图片,使用第四坐标作为mipmap的level,这里取了0
float Displacement = tex2Dlod(_DisplacementMap,float4(o.uv.xy,0.0,0.0)).g;
Displacement = (Displacement-0.5)*_DisplacementStrength;
v.normal = normalize(v.normal);
v.vertex.xyz += v.normal * Displacement;

o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);

//计算切线空间转换矩阵
half3 vNormal = UnityObjectToWorldNormal(v.normal);
half3 vTangent = UnityObjectToWorldDir(v.tangent.xyz);
//compute bitangent from cross product of normal and tangent
half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
half3 vBitangent = cross(vNormal,vTangent)*tangentSign;
//output the tangent space matrix
o.tspace0 = half3(vTangent.x,vBitangent.x,vNormal.x);
o.tspace1 = half3(vTangent.y,vBitangent.y,vNormal.y);
o.tspace2 = half3(vTangent.z,vBitangent.z,vNormal.z);
return o;
}

//有些硬件不支持曲面细分着色器,定义了该宏就能够在不支持的硬件上不会变粉,也不会报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
//顶点着色器结构的定义
struct TessVertex{
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};

struct OutputPatchConstant {
//不同的图元,该结构会有所不同
//该部分用于Hull Shader里面
//定义了patch的属性
//Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};

TessVertex tessvert (VertexInput v){
//顶点着色器函数
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}

//float _TessellationUniform;
OutputPatchConstant hsconst (InputPatch<TessVertex,3> patch){
//定义曲面细分的参数
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}

[UNITY_domain("tri")]//确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")]//拆分edge的规则,equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")]//一个patch一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] //不同的图元会对应不同的控制点

TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID){
//定义hullshaderV函数
return patch[id];
}

[UNITY_domain("tri")]//同样需要定义图元
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
//bary:重心坐标
{
VertexInput v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;

VertexOutput o = vert (v);
return o;
}
#endif

float4 frag (VertexOutput i) : SV_Target
{
float3 lightDir =_WorldSpaceLightPos0.xyz;
float3 tnormal = UnpackNormal (tex2D (_DisplacementMap, i.uv));
half3 worldNormal;
worldNormal.x=dot(i.tspace0,tnormal);
worldNormal.y= dot (i.tspace1, tnormal);
worldNormal.z=dot (i.tspace2, tnormal);
float3 albedo=tex2D (_MainTex, i.uv). rgb;
float3 lightColor = _LightColor0.rgb;
float3 diffuse = albedo * lightColor * DotClamped(lightDir,worldNormal);
float3 viewDir = normalize (_WorldSpaceCameraPos. xyz-i. worldPos. xyz);
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = albedo * pow (DotClamped (halfVector, worldNormal), _Smoothness * 100);
float3 result = specular + diffuse;
return float4(result, 1.0);

return float4(result,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

四、几何着色器-Geometry shader (GS)

1.GS的输入和输出

输入

  • 输入为单个图元(三角形、矩形、线等等)
  • 根据不同的图元,shader中会出现不同的顶点数量

输出

  • 输出也为图元(一个或者多个)
  • 同时还要定义输出的最大顶点数
  • 输出的图元需要自己一个点一个点的自己去构建,顺序很重要(这个着色器最主要的功能:自己构建图元)

2.流程

  • 输入输出结构
  • 定义最大输出定点数
  • 几何着色器

五、其他资料


Homework

Homework1