1. 应用
1.1. 曲面细分着色器Tessellation Shader (TESS) 的应用
将立方体逐渐细分为球体;将一条直线进行细分,向一条曲线慢慢逼近。
- 使用普通法线的模型,在边缘部分的凹凸感会不理想
- 如果使用置换贴图,因为它是真正改变物体的形状,所以边缘部分的凹凸感就会很真实
- 注意:使用置换贴图,对模型的面数有要求。
-
- 正是这个原因,让它和曲面细分着色器有着很好的契合度。
可以制作海浪、雪地里的脚印等等。
1.2. 几何着色器Geometry Shader(GS)的应用
①几何动画:简单的几何动画、甚至可以做一些破碎的效果
②草地等效果(与曲面细分结合):可以自定义草的画法,再和曲面细分着色器结合,就可以得到一个可以动态调整草密度的一个草地效果。
2. 曲面细分着色器TESS
2.1. 原理
2.1.1. TESS Shader的执行顺序
整体顺序:顶点 → 曲面细分 → 几何 → 片元
- 曲面细分
Tessellation Shader
又分为:Hull shader 、Tessellation Primitive Generator 、 Domain shader:
-
Hull shader
主要作用:定义一些细分的参数(如:每条边上如何细分,内部三角形如何细分)Tessellation Primitive Generator
,不可编程的Domain shader
:经过曲面细分着色器细分后的点是位于重心空间的,这部分的作用就是把它转化到我们要用的空间。
在D3D11 和 OpenGL中,名字/叫法有差异。
2.1.2. TESS的输入和输出
输入
- 称为Patch,可以看成是多个顶点的集合,包含每个顶点的属性。
- 属性是所有顶点共享的,不是每个顶点有独自的属性
struct OutputPatchConstant
{
//不同的图元,该结构会有所不同
//该部分用于Hull Shader里面,定义了patch的属性
//Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;//3就是三角形,4就是矩形等等
float inside : SV_INSIDETESSFACTOR;
};
[UNITY_patchconstantfunc("hsconst")]//一个patch一共有三个点,但是这三个点都共用这个函数
功能
- 将图元进行细分。图元可以是三角形、矩形等
- 不同的图元,输入参数也不一样。
输出
- 细分后的顶点
2.1.3. TESS的流程
- Hull shader
- 定义细分的参数,设定Tessellation factor以及Inside Tessellation factor
- 如果需要的话,可以对输入的Patch参数进行改变
TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID)
{
//定义hullshaderV函数
return patch[id];
}
- Tessellation Primitive Generator
- 这部分是不可编程、无法控制的
- 进行细分操作
- Domain shader
- 对细分后的点进行空间的转换,从重心空间(Barycentric coordinate system)转换到屏幕空间
[UNITY_domain("tri")]//同样需要定义图元
//bary:重心坐标
v2f ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
{
a2v 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;
v2f o = vert (v);
return o;
}
2.1.4. Hull shader的参数
2.1.4.1. Tessellation Factor
定义把一条边分为几个部分,目的是让细分更加平滑。切分的方法有三种:
- equal_Spacing
-
- 把一条边等分(二、三分等等..)
- fractional_even_spacing
-
- 向上取最近的偶数2,4,6....
- 最小值是2
- 会把周长分为n-2的等长部分、以及两端不等长的部分(两端部分和小数有关,具体看gif)
- fractional_odd_spacing
-
- 向上取最近的奇数1,3,5......
- 最小值是1
- 会把周长分为n-2的等长部分、以及两端不等长的部分
[UNITY_partitioning("equal_Spacing")]
[UNITY_partitioning("fractional_even")]
[UNITY_partitioning("fractional_odd")]
2.1.4.2. Inner Tessellation Factor
定义内部的三角形/矩形是怎么画出来的,
例如下图三角形等分的情况:
- 将三条边三等分,然后从一个端点开始,在邻近的两个切分点处做垂线,两条垂线的交点就是新三角形的一个端点。以此类推就是左图的效果。
- 上图四等分、甚至更多点的情况:上述三等分步骤之后,内部三角形的每个边的等分点做延长线,交点就是新三角形的端点或者等分点
矩形等分也是同理,做延长线,交点,直到没有交点或者交于重心一个点:
2.2. 案例
2.2.1. 将一个Quad细分
体现具体的使用方法,通过一个quad可以看出不同数值对细分的影响(之后可以使用在草地中),基于shader再回顾一遍各个参数的作用。
查看面片分割:
添加shader
//曲面细分Demo1
Shader "Unlit/Tess"
{
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
float _TessellationUniform;
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
//这个函数应用在domain shader中,用来空间转换
v2f vert (a2v v)
{
v2f 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;//3就是三角形,4就是矩形等等
float inside : SV_INSIDETESSFACTOR;
};
//顶点着色器函数
TessVertex tessvert (a2v v)
{
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}
//定义曲面细分的参数
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")]//同样需要定义图元
//bary:重心坐标
v2f ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
{
a2v 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;
v2f o = vert (v);
return o;
}
#endif
fixed4 frag (v2f i) : SV_Target
{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
调整细分参数:
2.2.2. 与置换贴图的结合
基本原理:通过置换贴图的深度,来把顶点沿着它的法线方向进行移动,以此来对mash进行形变。
//曲面细分Demo2:与置换贴图结合使用
Shader "Unlit/TessWithDisplayTex"
{
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 a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 worldPos:TEXCOORD1;
half3 tspace0 :TEXCOORD2;
half3 tspace1 :TEXCOORD3;
half3 tspace2 :TEXCOORD4;
};
v2f vert (a2v v)
//这个函数应用在domain函数中,用来空间转换的函数
{
v2f 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);
//通过置换贴图的深度,来把顶点沿着它的法线方向进行移动,以此来对mash进行形变。
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 (a2v 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")]//同样需要定义图元
v2f ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
//bary:重心坐标
{
a2v 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;
v2f o = vert (v);
return o;
}
#endif
float4 frag (v2f 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);
}
ENDCG
}
}
Fallback "Diffuse"
}
补充:
#pragma target 5.0
解释:
要指定 Shader 对象所需的着色器模型或 GPU 功能,可以在 HLSL 代码中使用 pragma 指令。可以使用 #pragma target name 指令来指定着色器模型,可以使用 #pragma require feature … 指令来指定功能。
例如:
pragma target 3.5
pragma require integers 2darray instancing
pragma target 5.0
- DX11 着色器模型 5.0。
- 在早于 SM5.0 的 DX11、早于 4.3 的 OpenGL(即 Mac)、OpenGL ES 2.0/3.0/3.1 和 Metal 上不支持。
- 在 DX11+ SM5.0、OpenGL 4.3+、OpenGL ES 3.1+AEP、Vulkan、Metal(不含几何体)和 PS4/XB1 游戏主机上支持。
tex2d
:采样纹理,在vertex shader中不可用
tex2dlod
:根据采样点坐标和mip级别,计算偏移值,然后采样,在vertex shader/fragmnt shader中均可用 (lod -- Level-of-detail)
tex2dproj
:与tex2d基本一样,只是在采样前,tex2Dproj将输入的UV xy坐标除以其w坐标,将坐标映射到透视投影(裁剪空间坐标分量w归一的时候使用)
tex2dbias
:在偏移mipmap级别(lod bias)后对贴图进行采样, 用于对图像采样结果模糊或锐化
3. 几何着色器GS
3.1. 原理
一直以来我们使用了顶点着色器(vertex shader)和片元着色器(fragment shader),实际上OpenGL还提供了一个可选的几何着色器(geometry shader)。几何着色器位于顶点和片元着色器之间
- 如果没有使用时,则顶点着色器输出到片元着色器;
- 几何着色器在启用后,它将获得顶点着色器以组成一个基础图元为一组的顶点输入,通过对输入的顶点进行处理,几何着色器将决定输出的图元类型和个数。当输出的图元减少或者不输出时,实际上起到了裁剪图形的作用,当输出的图元类型改变或者输出更多图元时起到了产生和改变图元的作用。
几何着色器能够产生0个以上的基础图元(primitive),它能起到一定的裁剪作用、同时也能产生比顶点着色器输入更多的基础图元。
区分:
- 顶点着色器VS是逐顶点操作,可以进行坐标变换等计算。
- 片元着色器FS是逐片元/像素操作,进行最终输出颜色的计算。
- 几何着色器GS是逐图元的操作。它的输入是图元,输出也是图元。可以裁剪、产生和改变图元。
3.2. 基础代码
参考:【Unity Shader入门】4、几何着色器Geometry Shaders之结构解析_shader编译目标级别-CSDN博客
#pragma target 4.0//设定着色器编译目标级别为4.0(4.0以上才能支持)
#pragma geometry GS_Main//设定几何体着色器函数名称为GS_Main
[maxvertexcount(3)] //必不可少,定义输出顶点的最大数量
void GS_Main(point VS_OUTPUT IN[1], inout TriangleStream<GS_OUTPUT> triStream)
{
/*shader body*/
}
函数前添加 [maxvertexcount(num)]
- 首先
[maxvertexcount(num)]
这个必须写在几何着色器前面,是不可少的,主要是定义输出顶点的最大数量,输出顶点可以每次都不同,但是不超过这个数就行,注意这个数不要太大,影响性能。 - [NVIDIA08]指出,当GS输出在1到20个标量之间时,可以实现GS的性能峰值,如果GS输出在27-40个标量之间,则性能下降50%。
3.2.1. 输入
void GS_Main(point VS_OUTPUT IN[1], inout TriangleStream<GS_OUTPUT> triStream)
{
/*shader body*/
}
point
:输入类型,见下表
输入类型 | 描述 | 顶点数量 |
point | 输入图元为点 | 1 |
line | 输入图元为线 | 2 |
triangle | 输入图元为三角形 | 3 |
lineadj | 输入图元为带有邻接信息的直线,由4个顶点构成3条线 | 4 |
triangleadj | 输入图元为带有邻接信息的三角形,由6个顶点构成 | 6 |
VS_OUTPUT
:顶点着色器传进来的类型,可以自定义结构体,v2g、v2f...
IN[1]
:IN为输入变量名,可以自定义,里面的1是顶点数量,根据输入类型变化
3.2.2. 输出
void GS_Main(point VS_OUTPUT IN[1], inout TriangleStream<GS_OUTPUT> triStream)
{
/*shader body*/
}
inout
:关键词
TriangleStream
:输出类型,见下表及样例
输出类型 | 描述 |
PointStream | 输出图元为点 |
LineStream | 输出图元为线 |
TriangleStream | 输出图元为三角形 |
GS_OUTPUT
:几何着色器传出去的类型,可以自定义结构体,g2f、v2f...
triStream
:输出类型变量名
对于GS有以下几点需要注意的:
- 每输出一个点都要Append到输出流中,如:
triStream.Append(o);
- 对于TriangleStream ,如果需要改变输出图元,需要每输出点足够对应相应的图元后都要
RestartStrip()
一下再继续构成下一图元,如:tStream.RestartStrip();
3.3. 案例
基于三角形构建一个草地,从简单的生成三角形开始,涉及Triangle Stream的使用,基于切线空间的顶点操作以及与曲面细分shader的联合使用, 同时可以说一下曲面细分参数的确定方式(基于distance, edge)
3.3.1. 草地
参考:
Unity Grass Shader 教程 (roystan.net)
菜鸡都能学会的Unity草地shader - 知乎 (zhihu.com)
加入偏移
转换到切线坐标
构建UV
颜色渐变
随机朝向
随机前弯,并不总是竖直朝上
随机变化宽度和高度
曲面细分,增加密度
弯曲,添加风
草投射阴影
草接受阴影
可视化法线
添加卡通着色,最终效果: