在我翻译过的OpenGL和实时渲染相关文章中,简要介绍过几何着色器,它的执行顺序位于细分曲面着色器、光栅化与片元着色器之间,有时不会使用细分曲面着色器,且常不表示固定阶段,所以简要来说,顶点着色器的输出到几何着色器,接着进行某些增减基本体的操作,然后进入片元着色器进行光照计算操作。
几何着色器本质上最常用的是增加基本体,可以用于生成粒子,毛发等。
基本的几何着色器
Unity中,和顶点与片元着色器一样,使用一个预编译指令来标明函数,且额外定义一个结构体用于变量的输入输出,供着色器之间通信。这里给出一个简单的几何着色器例子:
Shader "Unlit/SimpleGeometryShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2g
{
float vertex: SV_POSITION;
float uv : TEXCOORD0;
};
struct g2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2g vert (appdata v)
{
v2g o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
{
g2f o;
for(int i = 0;i < 3; i++)
{
o.vertex = UnityObjectToClipPos(IN[i].vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
triStream.Append(o);
}
triStream.RestartStrip();
}
fixed4 frag (g2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
注意三个结构体:
- appdata,将物体的属性传入顶点着色器。
- v2g,将数据从顶点着色器传入几何着色器。
- g2f,将数据从几何着色器传入片元着色器。
对于将顶点转换到裁剪空间,以及变换UV的操作,我们在几何着色器中执行了,实际上也可以在顶点着色器中执行,只是这么做的话,更灵活。
几何着色器讲解
对于几何着色器函数,这里复制一下:
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
{
g2f o;
for(int i = 0;i < 3; i++)
{
o.vertex = UnityObjectToClipPos(IN[i].vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
triStream.Append(o);
}
triStream.RestartStrip();
}
maxvertexcount
代表几何着色器会增加的顶点的最大数量。由于我们在物体上实际添加一个三角形,所以这里设置为3.
接下来讲解几何着色器函数的参数:
-
triangle v2g IN[3]
:这是有3个v2g元素的数组,每个元素与我们所编辑的三角形的一个顶点绑定。triangle
标签表明几何着色器会将一个三角形作为输入。也可以用line
(数组大小为2)或point
(数组大小为1)。 -
inout TriangleStream<g2f> triStream
:我们可以看到,该函数的返回值为空,所以实际上我们没有返回一个物体。几何着色器实际上将每个三角形添加到一个TriangleStream
列表中,类型为g2f
。如果想要输出线或点的话,可以用inout LineStream<g2f> lineStream
或inout PointStream<g2f> pointStream
。
接着讲解函数体。首先我们定义一个g2f
的结构体对象,我们会对其进行操作然后加入列表中。
然后,我们进行一个简单的循环,将每个输入的顶点添加到流中,创建三角形。因为数据要传入片元着色器,因此我们将顶点坐标转换到裁剪空间,并且按系数偏移UV。
最后,我们使用triStream.Append(o)
将一个修改过的g2f
结构体添加到三角形流中。在结束循环后,使用RestartStrip
函数,这可以让流明白一个独立的三角形要在之后添加。这里并没有额外生成顶点等,一切保持原状。
挤出金字塔
现在我们扩展这个简单的几何着色器,我们从每个三角形上挤出一个金字塔的形状,即在三角形中心添加一个顶点,然后沿法线方向挤出面。
Shader "Unlit/SimpleGeometryShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ExtrusionFactor("Extrusion factor", float)=0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2g
{
float4 vertex: SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct g2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _ExtrusionFactor;
v2g vert (appdata v)
{
v2g o;
o.vertex = v.vertex;
o.uv = v.uv;
o.normal = v.normal;
return o;
}
[maxvertexcount(12)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
{
g2f o;
// 重心
float4 barycenter = (IN[0].vertex + IN[1].vertex + IN[2].vertex)/3;
float3 normal = (IN[0].normal + IN[1].normal + IN[2].normal)/3;
// 构成金字塔的三个三角形
for(int i = 0;i < 3; i++)
{
// i=0:1;i=1:2;i=2:1;
// 即计算相邻顶点索引
int next = (i+1)%3;
o.vertex = UnityObjectToClipPos(IN[i].vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
o.color = fixed4(0.0,0.0,0.0,1.0);
triStream.Append(o);
// 金字塔三角形顶尖顶点
o.vertex = UnityObjectToClipPos(barycenter + float4(normal, 0.0)*_ExtrusionFactor);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
o.color = fixed4(1.0,1.0,1.0,1.0);
triStream.Append(o);
o.vertex = UnityObjectToClipPos(IN[next].vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
o.color = fixed4(0.0,0.0,0.0,1.0);
triStream.Append(o);
}
triStream.RestartStrip();
// 组装最基本的三角形
for(int i = 0;i < 3;i++)
{
o.vertex = UnityObjectToClipPos(IN[i].vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
o.color = fixed4(0.0,0.0,0.0,1.0);
triStream.Append(o);
}
triStream.RestartStrip();
}
fixed4 frag (g2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
在最开始,我们添加了_ExtrusionFactor
属性,用于控制挤出的高度。接着开启Cull Off
,关闭面消隐。
在几何着色器函数中,我们首先设置最大输出顶点数量,相当于生成4个三角形,即在每个三角形的基础上生成3个三角形来组成锥体。
对于挤出操作,我们需要每个三角形的中心作为法线,因此,我们通过对三角形各个顶点的坐标做平均计算来得到重心,然后同样通过平均计算来得到三角形的法线。
然后我们生成锥体,算法过程为:
对于每个三角形的点
得到下一个点的索引
在当前点的位置处增加一个顶点
在三角形重心处添加一个顶点,然后沿沿面的法线挤出,乘等于`_ExtrusionFactor`
在下一个点处添加一个顶点
这就是第一个循环所做的事情。第二个循环中,我们将最开始的三角形组装起来,然后就可以得到想要的三角锥了。