unity曲面细分着色器(上):原理和基本实现
目录
前言
最近在学习百人计划的曲面细分着色器的时候,看着大佬提供的源码一头雾水,课上的老师讲得也比较快,感觉无法理解,就去看了英文教程
真的好长啊这个文章,一边自己翻译一边做了两天,都是我自己翻译的,看完之后感觉英语水平又提高了好多
参考
Flat and Wireframe Shading Derivatives and Geometry:https://catlikecoding.com/unity/tutorials/advanced-rendering/flat-and-wireframe-shading/
Tessellation Subdividing Triangles:https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
3.3 曲面细分与几何着色器—大规模草渲染:https://www.yuque.com/sugelameiyoudi-jadcc/okgm7e/xyx5h5#eI5Sb
【技术美术百人计划】图形 3.3 曲面细分与几何着色器 大规模草渲染https://www.bilibili.com/video/BV1XX4y1A7Ns/?spm_id_from=333.1387.collection.video_card.click&vd_source=75cfca612334135de761351e88587faa
原理
着色器顺序
- 整体顺序:顶点 → 曲面细分 → 几何 → 片元
- 曲面细分又分为:Hull shader 、Tessellation Primitive Generator 、 Domain shader
- Hull shader主要作用:定义一些细分的参数(如:每条边上如何细分,内部三角形如何细分)
- Tessellation Primitive Generator,不可编程的
- Domain shader:经过曲面细分着色器细分后的点是位于重心空间的,这部分的作用就是把它转化到我们要用的空间。
- 曲面细分又分为:Hull shader 、Tessellation Primitive Generator 、 Domain shader
- 在D3D11 和 OpenGL中,名字/叫法有差异,问题不大
输入和输出
流程
Hull shader → Tessellation Primitive Generator → Domain shader
Hull shader
- 定义细分的参数
- 设定Tessellation factor以及Inside Tessellation factor
- (如果需要的话)可以对输入的Patch参数进行改变
Tessellation Primitive Generator
- 这部分是不可编程、无法控制的
- 进行细分操作
Domain shader
- 对细分后的点进行处理,从重心空间(Barycentric coordinate system)转换到屏幕空间
曲面细分hull shader各参数解析
Tessellation Factor
- 定义把一条边分为几个部分
- 切分的方法有三种:
- equal_Spacing
-
把一条边等分(二、三分等等…)
-
- fractional_even_spacing
-
向上取最近的偶数
-
最小值是2
-
会把周长分为n-2的等长部分、以及两端不等长的部分(两端部分和小数有关,具体看gif)
-
- fractional_odd_spacing
-
向上取最近的奇数
-
最小值是1
-
会把周长分为n-2的等长部分、以及两端不等长的部分
-
目的:让细分更加平滑
-
inner tesselation factor:
- 定义内部的三角形/矩形是怎么画出来的
- 三角形情况
假设我们这个参数设置为三,不管之前的这个factor它是怎么切分这个边的,我们就按当前这个三来把这个三角形切分成三个等份
我们从它一个端点开始,分别找它最近的这两个切分的点,做它们的延长线,他们的交点就是新的内部三角形的一个点,以此类推做出这内部的这个三角形
如果多一个点,就在内部这个三角形再多一个点,然后再用这两个点延长线,然后做交点
- 例如上图三等分的情况:
- 将三条边三等分,然后从一个端点开始,取邻近的两个切分点做延长线,两者的交点就是新三角形的一个端点。以此类推就是左图的效果。
- 上图四等分、甚至更多点的情况:
- 上述三等分步骤之后,内部三角形的每个边的等分点做延长线
矩形类似
- 同样的,做延长线,交点,直到没有交点或者交于重心一个点
hull program
为了看得更清楚,这个曲面细分着色器是基于平面线框着色器的
详情可以看我的上一节,最后有全部源码:unity模型平直着色、线框效果https://blog.youkuaiyun.com/nidayeaaaaaaaaa/article/details/146956340?spm=1001.2014.3001.5501
复制这个shader然后重命名一下,在这基础上实现曲面细分
创建自己的cginc,把核心代码写到这里,这样其他shader想要用这个功能直接引用这个cginc文件就行
注意cginc命名,如果直接命名为Tessellation,他会覆盖unity自带的cginc
注意target level最低4.6
曲面细分着色器步骤
声明hull program
和几何着色器一样,曲面细分着色器也很灵活,可以对三角形、四边形、等值线进行操作,但是我们必须在hull shader中告知他要操作的表面的类型,并且输入必要的参数
添加hull program,hull program是对一个面片起作用,所以需要传入InputPatch参数,他同样需要模版来规定类型
所谓的patch就是一组mesh顶点的集合
这里我们新建一个结构体用于输入模版参数
因为一个三角形有3个顶点,所以模版的第二个参数写3,意味着传入的一个patch有3个顶点,每个顶点都携带着VertexData结构体类型的信息
hull program的作用是传递顶点信息给曲面细分阶段,尽管他被传入了一整个patch,但是他一次只能输出一个顶点
对于patch中的每个顶点,hullprogram都会被调用一次,所以每次调用都需要一个参数指示他是对哪个控制点生效,这个参数是一个uint类型,语义为SV_OutputControlPointID
返回指定的patch
在shader中声明hull program,现在hull program还不完全,所以会报错
hull program所需的属性
和几何着色器一样,他也需要具体指定对triangles生效,使用UNITY_domain属性:[UNITY_domain("tri")]
而且我们还要具体告知他,他每次输出3个控制点,分别对应三角形的三个点:[UNITY_outputcontrolpoints(3)]
gpu在创建新的三角形的时候,还需要知道这个三角形的三个点的定义顺序是顺时针还是逆时针,unity中默认是顺时针定义(clockwise):[UNITY_outputtopology("triangle_cw")]
gpu还要知道要怎么对这个patch切分,使用UNITY_partitioning属性,有很多partitioning方法,之后会做介绍,现在先用integer方法:[UNITY_patitioning("integer")]
其次,gpu还要知道一个patch被切割为多少份,这个份数可能每个patch都不一样,所以我们需要写一个方法来计算它,这个就是patch constant function,目前我们还没定义,先写着:[UNITY_patchconstantfunc("MyPatchConstantFuncition")]
最后,hullProgram会返回VertexData
patch constant function
patch的属性之一就是他会被如何分割,patch constant function只会被每个patch调用一次,而不是每个控制点都调用一次
所以他才会被叫做Constant function,因为他在整个patch中都是const的
事实上,patch constant function是和hull program并行运行的子阶段
对于三角形,gpu使用4个参数来对他进行划分,其中3个分别控制三角形的三个边,使用SV_TessFactor
语义,剩下一个控制三角形内部,控制内部的使用**SV_InsideTessFactor
**语义
创建patch constant function,他以patch作为输入,输出上面的这个TessellationFactors结构体
先让他每个边都设置为1,这样他就不会对patch做切分
Domain shader
创建domain Program
hull和domain都是对三角形作用的,所以再次声明作用域为triangle
domain Program传入TessellationFactors和一个OutputPatch模版
虽然Tessellation阶段会分割patch,但是他实际上不生成任何新的顶点
他为这些顶点创建重心坐标,然后交由domain Program,domain Program会根据这些重心坐标来派生顶点
每个顶点都会调用一次domain Program,还需要传入重心坐标,语义为**SV_DomainLocation
**
在function 内部,创建一个VertexData,然后通过重心坐标在原始三角形中进行插值,重心坐标的xyz分别决定了第123个控制点的权重
后面还要对vertex data里面的每个值做这个操作,这段代码比较难写,所以直接把他封装成一个宏定义函数
返回值,调用顶点着色器
最后domain shader是要返回数据,要么送到几何着色器,要么送到插值器,这就需要他输出VertexOutput,也就是顶点着色器输出的v2f
要让他输出这个,就让他调用顶点着色器,这时候使用cginc的弊端就出现了,他引用不到shader中的顶点着色器
所以再创建一个cginc,把和处理顶点相关的代码都放进去,其他的再引用这个cginc
vertexProcess.cginc
#if !defined(VERTEXPROCESS_INCLUDED)
#define VERTEXPROCESS_INCLUDED
struct VertexInput
{
float2 uv : TEXCOORD0;
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent:TANGENT;
};
struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertexWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
UNITY_SHADOW_COORDS(3)
float3 vertexColor : TEXCOORD4;
float3 tangentWS:TEXCOORD5;
float2 barycentric : TEXCOORD9;
};
float4 _MainTex_ST;
VertexOutput vert(VertexInput v)
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertexWS = mul(UNITY_MATRIX_M, v.vertex);
o.pos = mul(UNITY_MATRIX_VP, o.vertexWS);
o.normalWS = UnityObjectToWorldNormal(v.normal);
o.tangentWS=UnityObjectToWorldDir(v.tangent);
TRANSFER_SHADOW(o)
return o;
}
#endif
原本的Tessellation.cginc中的VertexData结构体其实没必要,因为他本质就是VertexInput结构体,所以删去替换
然后调用顶点着色器,返回VertexOutput
MyTessellation.cginc
#if !defined(MYTESSELLATION_INCLUDED)
#define MYTESSELLATION_INCLUDED
#include "vertexProcess.cginc"
struct TessellationFactors
{
float edge[3]:SV_TessFactor;
float inside:SV_InsideTessFactor;
};
TessellationFactors MyPatchConstantFuncition(
InputPatch<VertexInput,3> patch
){
TessellationFactors f;
f.edge[0]=1;
f.edge[1]=1;
f.edge[2]=1;
f.inside=1;
return f;
}
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("MyPatchConstantFuncition")]
VertexInput MyHullProgram(InputPatch<VertexInput,3> patch,
uint id: SV_OutputControlPointID)
{
return patch[id];
}
#define MY_DOMAIN_POGRAM_INTERPOLATE(fieldName) data.fieldName= \
patch[0].fieldName*barycentricCoordinates.x+ \
patch[1].fieldName*barycentricCoordinates.y+ \
patch[2].fieldName*barycentricCoordinates.z;
[UNITY_domain("tri")]
VertexOutput MyDomainProgram(
TessellationFactors factors,
OutputPatch<VertexInput,3> patch,
float3 barycentricCoordinates:SV_DomainLocation
)
{
VertexInput data;
MY_DOMAIN_POGRAM_INTERPOLATE(vertex)
MY_DOMAIN_POGRAM_INTERPOLATE(normal)
MY_DOMAIN_POGRAM_INTERPOLATE(tangent)
MY_DOMAIN_POGRAM_INTERPOLATE(uv)
return vert(data);
}
#endif
然后在shader中声明domain
现在依然有bug,这么做了之后,物体直接消失不见了
控制点
顶点着色器vert只会被调用一次,刚才已经被domain shader调用了,所以失效了
我们还要再定义一个顶点着色器,但是他实际上不用做任何事情,只需要传递参数信息就行了,这里还是把他写在vertexProcess.cginc
同时在shader中修改定义
这又会造成报错,所以再创造一个结构体TessellationControlPoint,里面的vertex的语义为INTERNALTESSPOS
,其余的和vertexOutput保持一致,这个结构体的顶点信息用于为曲面细分提供控制点
hull Program和 MyPatchConstantFuncition和MyDomainProgram中也做修改
现在他可以正常显示了但是实际上并没有做曲面细分
代码
vertexProcess.cginc
#if !defined(VERTEXPROCESS_INCLUDED)
#define VERTEXPROCESS_INCLUDED
struct VertexInput
{
float2 uv : TEXCOORD0;
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent:TANGENT;
};
struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertexWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
UNITY_SHADOW_COORDS(3)
float3 vertexColor : TEXCOORD4;
float3 tangentWS:TEXCOORD5;
float2 barycentric : TEXCOORD9;
};
float4 _MainTex_ST;
VertexOutput vert(VertexInput v)
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertexWS = mul(UNITY_MATRIX_M, v.vertex);
o.pos = mul(UNITY_MATRIX_VP, o.vertexWS);
o.normalWS = UnityObjectToWorldNormal(v.normal);
o.tangentWS=UnityObjectToWorldDir(v.tangent);
TRANSFER_SHADOW(o)
return o;
}
struct TessellationControlPoint {
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
TessellationControlPoint MyTessellationVertexProgram(VertexInput v)
{
TessellationControlPoint o;
o.vertex=v.vertex;
o.normal=v.normal;
o.uv=v.uv;
o.tangent=v.tangent;
return o;
}
#endif
MyTessellation.cginc
#if !defined(MYTESSELLATION_INCLUDED)
#define MYTESSELLATION_INCLUDED
#include "vertexProcess.cginc"
struct TessellationFactors
{
float edge[3]:SV_TessFactor;
float inside:SV_InsideTessFactor;
};
TessellationFactors MyPatchConstantFuncition(
InputPatch<TessellationControlPoint,3> patch
){
TessellationFactors f;
f.edge[0]=1;
f.edge[1]=1;
f.edge[2]=1;
f.inside=1;
return f;
}
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("MyPatchConstantFuncition")]
TessellationControlPoint MyHullProgram(InputPatch<TessellationControlPoint,3> patch,
uint id: SV_OutputControlPointID)
{
return patch[id];
}
#define MY_DOMAIN_POGRAM_INTERPOLATE(fieldName) data.fieldName= \
patch[0].fieldName*barycentricCoordinates.x+ \
patch[1].fieldName*barycentricCoordinates.y+ \
patch[2].fieldName*barycentricCoordinates.z;
[UNITY_domain("tri")]
VertexOutput MyDomainProgram(
TessellationFactors factors,
OutputPatch<TessellationControlPoint,3> patch,
float3 barycentricCoordinates:SV_DomainLocation
)
{
VertexInput data;
MY_DOMAIN_POGRAM_INTERPOLATE(vertex)
MY_DOMAIN_POGRAM_INTERPOLATE(normal)
MY_DOMAIN_POGRAM_INTERPOLATE(tangent)
MY_DOMAIN_POGRAM_INTERPOLATE(uv)
return vert(data);
}
#endif
切分三角形
曲面细分规律
一个三角形patch是怎么切分的取决于他的**TessellationFactors
**,如图,我们把它改成2,三角形的所有边都被切成了2份,一个三角形就会多出3个顶点
同时在三角形的中心也会增加一个顶点,然后这个顶点会和其余顶点相连,这样一来,原本一个三角形就被切分成了6个,而一个quad有2个三角形,就被切成12个
如果是切分3条子边,那么三角形内部还会多出3个点,构成一个内部的小三角形,然后连接
如果让他inside为7,其余的都为1
f.edge[0] = 1;
f.edge[1] = 1;
f.edge[2] = 1;
f.inside = 7;
简而言之,在MyPatchConstantFuncition
里面edge[n]分别指示了三角形的每条边被分成几份,而inside则指示了三角形内部会多出多少个顶点
控制曲面细分参数
添加一个参数用于控制曲面细分的个数,注意范围,最低为1
分数因数Fractional Factors
眼下我们做的细分都是等价细分,这是因为我们的partitioning mode设置为integer,他在演示的时候比较好看,但是在细分级别切换的时候不够流畅
fractional_odd
修改为*fractional_odd
,结果和integer差不多,但是额外的边缘细分将被分裂和增长,或收缩和合并 *,这样每条边就不再是每次分成等长的段,优点就在于细分级别之间切换流畅
请添加图片描述
fractional_even
对比
这两种Fractional Factors哪个好?
如图,可以看到even一开始就是经过细分了的,尽管TessellationUniform=1,但是他的最低级别强制为2
所以其实odd更加常用
具体代码
TessellationShader
Shader "Unlit/TessellationShader"
{
Properties {
_MainTex("MainTex",2D)="white"{}
_DiffuseIntensity("DiffuseIntensity",float)=1
_SpecularIntensity("SpecularIntensity",float)=1
_Shineness("Shineness",float)=0.5
[Header(WireFrame)]
[Toggle]_WireFrameEnable("WireFrameEnable",float)=1
[Toggle]_FlatShading("FlatShading",float)=1
_WireFrameColor("WireFrameColor",Color)=(0,0,0,1)
_WireframeSmoothing ("Wireframe Smoothing", Range(0, 10)) = 1
_WireframeThickness ("Wireframe Thickness", Range(0, 10)) = 1
[Header(Tessellation)]
_TessellationUniform("TessellationUniform",Range(1,64))=1
}
SubShader
{
pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "FlatWireFrame.cginc"
#include "MyTessellation.cginc"
#include "vertexProcess.cginc"
#pragma shader_feature _WIREFRAMEENABLE_ON
#pragma vertex MyTessellationVertexProgram
#pragma fragment frag
#pragma hull MyHullProgram
#pragma domain MyDomainProgram
#pragma geometry MyGeometryProgram
#pragma multi_compile_fwdbase
#pragma target 4.6
void InitializeFragmentNormal(inout VertexOutput i)
{
float3 dpdx = ddx(i.vertexWS);
float3 dpdy = ddy(i.vertexWS);
i.normalWS = normalize(cross(dpdy, dpdx));
}
sampler2D _MainTex;
float _DiffuseIntensity,_SpecularIntensity,_Shineness;
float4 _WireFrameColor;
float _WireframeThickness,_WireframeSmoothing,_WireFrameEnable;
float4 frag(VertexOutput i) : SV_TARGET {
// InitializeFragmentNormal(i);
// 阴影
UNITY_LIGHT_ATTENUATION(atten,i,i.vertexWS);
float4 ambient=tex2D(_MainTex,i.uv)*atten*unity_AmbientSky;
// Lambert:ambient+kd*dot(N,L)
float3 lightDir=_WorldSpaceLightPos0;
float3 normalDir=normalize(i.normalWS);
float4 Lambert=ambient+_DiffuseIntensity*max(0,dot(normalDir,lightDir))*_LightColor0;
// Blinn-Phong:ks*dot(N,H)
float3 viewDir=normalize(_WorldSpaceCameraPos-i.vertexWS);
float3 H=normalize(lightDir+viewDir);
float4 BlPhong=_SpecularIntensity*pow(max(0,dot(normalDir,H)),_Shineness);
float4 result=Lambert+BlPhong;
#if _WIREFRAMEENABLE_ON
float3 barys=getBarysWithWireframe(i);
float3 deltas = fwidth(barys);
float3 smoothing = deltas * _WireframeSmoothing;
float3 thickness = deltas * _WireframeThickness;
barys = smoothstep(thickness, thickness + smoothing, barys);
float minBary = min(barys.x, min(barys.y, barys.z));
float4 finalBary=lerp(_WireFrameColor, (Lambert+BlPhong), minBary);
result=finalBary;
#endif
return result;
}
ENDCG
}
pass
{
Tags{"LightMode"="ShadowCaster"}
CGPROGRAM
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
struct VertexInput
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(VertexInput v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
fixed4 frag(v2f i):SV_TARGET
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
MyTessellation.cginc
#if !defined(MYTESSELLATION_INCLUDED)
#define MYTESSELLATION_INCLUDED
#include "vertexProcess.cginc"
struct TessellationFactors
{
float edge[3]:SV_TessFactor;
float inside:SV_InsideTessFactor;
};
float _TessellationUniform;
TessellationFactors MyPatchConstantFuncition(
InputPatch<TessellationControlPoint,3> patch
){
TessellationFactors f;
f.edge[0]=_TessellationUniform;
f.edge[1]=_TessellationUniform;
f.edge[2]=_TessellationUniform;
f.inside=_TessellationUniform;
return f;
}
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("fractional_odd")]
[UNITY_patchconstantfunc("MyPatchConstantFuncition")]
TessellationControlPoint MyHullProgram(InputPatch<TessellationControlPoint,3> patch,
uint id: SV_OutputControlPointID)
{
return patch[id];
}
#define MY_DOMAIN_POGRAM_INTERPOLATE(fieldName) data.fieldName= \
patch[0].fieldName*barycentricCoordinates.x+ \
patch[1].fieldName*barycentricCoordinates.y+ \
patch[2].fieldName*barycentricCoordinates.z;
[UNITY_domain("tri")]
VertexOutput MyDomainProgram(
TessellationFactors factors,
OutputPatch<TessellationControlPoint,3> patch,
float3 barycentricCoordinates:SV_DomainLocation
)
{
VertexInput data;
MY_DOMAIN_POGRAM_INTERPOLATE(vertex)
MY_DOMAIN_POGRAM_INTERPOLATE(normal)
MY_DOMAIN_POGRAM_INTERPOLATE(tangent)
MY_DOMAIN_POGRAM_INTERPOLATE(uv)
return vert(data);
}
#endif
vertexProcess.cginc
#if !defined(VERTEXPROCESS_INCLUDED)
#define VERTEXPROCESS_INCLUDED
struct VertexInput
{
float2 uv : TEXCOORD0;
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent:TANGENT;
};
struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertexWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
UNITY_SHADOW_COORDS(3)
float3 vertexColor : TEXCOORD4;
float3 tangentWS:TEXCOORD5;
float2 barycentric : TEXCOORD9;
};
float4 _MainTex_ST;
VertexOutput vert(VertexInput v)
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertexWS = mul(UNITY_MATRIX_M, v.vertex);
o.pos = mul(UNITY_MATRIX_VP, o.vertexWS);
o.normalWS = UnityObjectToWorldNormal(v.normal);
o.tangentWS=UnityObjectToWorldDir(v.tangent);
TRANSFER_SHADOW(o)
return o;
}
struct TessellationControlPoint {
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
TessellationControlPoint MyTessellationVertexProgram(VertexInput v)
{
TessellationControlPoint o;
o.vertex=v.vertex;
o.normal=v.normal;
o.uv=v.uv;
o.tangent=v.tangent;
return o;
}
#endif
注意
unity不同时支持gpu实例化和曲面细分着色器,所以把gpu实例化有关的代码删除否则报错
毕竟gpu实例化和曲面细分在性能上的影响是完全相反的
如果实在要用很多要用到曲面细分的实例化物体,那就使用LOD级别吧:LOD0级使用一个无实例化的曲面细分材质,其他的LOD级别使用实例化的、没有曲面细分的材质