几何着色器作为一个可选项,位于顶点着色器和片元着色器之间。
几何着色器获得顶点着色器组成一个基础图元为一组的顶点输入,对输入的顶点进行处理,这些顶点组可以来自一个点或者一个三角面。进行的是逐图元的操作。
所以基于GS能轻松实现线框着色等效果。
类似OpenGL中GS的输入和输出
输入类型有:
point
line
triangle
lineadj
triangleadj
输出类型有:
PointStream
LineStream
TriangleStream
类似于顶点片元,需要先定义几何着色器geometry,由于GS是第四代显卡着色架构后才支持的,所以着色器编译目标级别最低4.0
#pragma target 4.0
#pragma geometry geom
函数前设置输出最大顶点数量
[maxvertexcount(21)]
实例:
10年世博会的英国馆让当时还在初一的我印象深刻,会馆由内向外伸出由无数根带种子的“条”,非常美
为了将一个面转化为一个三菱体,我们选择输入triangle,输出TriangleStream,所以须绘制7个三角面21个点。
核心代码如下
#define ADD_VERT(v) \
o.vertex = UnityObjectToClipPos(v); \
tristream.Append(o);
#define ADD_TRI(p0, p1, p2) \
ADD_VERT(p0) ADD_VERT(p1) \
ADD_VERT(p2) \
tristream.RestartStrip();
[maxvertexcount(21)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream)
{
g2f o;
float3 edgeA = IN[1].vertex - IN[0].vertex;
float3 edgeB = IN[2].vertex - IN[0].vertex;
float3 normalFace = normalize(cross(edgeA, edgeB));
float2 centerTex = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;
o.uv = centerTex;
float3 v0 = IN[0].vertex;
float3 v1 = IN[1].vertex;
float3 v2 = IN[2].vertex;
float3 v3 = IN[0].vertex + normalFace * _Length;
float3 v4 = IN[1].vertex + normalFace * _Length;
float3 v5 = IN[2].vertex + normalFace * _Length;
ADD_TRI(v0,v3,v2);
ADD_TRI(v3,v5,v2);
ADD_TRI(v3,v4,v0);
ADD_TRI(v4,v1,v0);
ADD_TRI(v5,v2,v1);
ADD_TRI(v5,v4,v1);
ADD_TRI(v3,v4,v5);
}
先通过点找到底边向量,通过叉积求出法线方向后找到新的三个点,然后组成面。需注意的是,GS输出的依然是图元,还没有进行光栅化插值,接下来的输出传递到片元着色器中完成。每输出一个点Append到输出流中 ,每输出点足够对应相应的图元后都要RestartStrip()一下再继续构成下一图元。完整代码如下:
Shader "MyShader/GS Test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Length("Length", Range(0.01, 10)) = 0.02
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Off
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2g
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct g2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Length;
v2g vert(appdata_base v)
{
v2g o;
o.vertex = v.vertex;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
#define ADD_VERT(v) \
o.vertex = UnityObjectToClipPos(v); \
tristream.Append(o);
#define ADD_TRI(p0, p1, p2) \
ADD_VERT(p0) ADD_VERT(p1) \
ADD_VERT(p2) \
tristream.RestartStrip();
[maxvertexcount(21)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream)
{
g2f o;
float3 edgeA = IN[1].vertex - IN[0].vertex;
float3 edgeB = IN[2].vertex - IN[0].vertex;
float3 normalFace = normalize(cross(edgeA, edgeB));
float2 centerTex = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;
o.uv = centerTex;
float3 v0 = IN[0].vertex;
float3 v1 = IN[1].vertex;
float3 v2 = IN[2].vertex;
float3 v3 = IN[0].vertex + normalFace * _Length;
float3 v4 = IN[1].vertex + normalFace * _Length;
float3 v5 = IN[2].vertex + normalFace * _Length;
ADD_TRI(v0,v3,v2);
ADD_TRI(v3,v5,v2);
ADD_TRI(v3,v4,v0);
ADD_TRI(v4,v1,v0);
ADD_TRI(v5,v2,v1);
ADD_TRI(v5,v4,v1);
ADD_TRI(v3,v4,v5);
}
fixed4 frag (g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
实例2:
最近在RIME中看到这样一个效果,不得不说RIME真的是一款非常赞的游戏,BGM更是首首经典。
这个台阶消失的效果必然是用粒子来做的,但是学到GS我们当然要用它来稍微模拟一下这个效果了。
思路:首先是溶解的边缘有一段纯色,这个很容易实现。其次是溶解途中散发出的小粒子,这里我们使用GS用每一个三角面生成一个立方体。所以这就要求模型的面数不要太高,否则=。=
Shader "My/DoublePass"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_DissolveColor("Dissolve Color", Color) = (0,0,0,0)
_MainTex("Base 2D", 2D) = "white"{}
_ColorFactor("ColorFactor", Range(0,1)) = 0.7
_DissolveThreshold("DissolveThreshold", Float) = 0
_BoxScale("BoxScale",Range(0,1)) = 0.001
_BoxColor("BoxColor",Color) = (1,1,1,1)
}
SubShader
{
Tags{ "RenderType" = "Opaque" }
Pass{
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
uniform fixed4 _Diffuse;
uniform fixed4 _DissolveColor;
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float _ColorFactor;
uniform float _DissolveThreshold;
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float2 uv : TEXCOORD1;
float4 objPos : TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.objPos = v.vertex;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float factor = _DissolveThreshold - i.objPos.y;
clip(factor);
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 lambert = saturate(dot(worldNormal, worldLightDir));
fixed3 albedo = lambert * _Diffuse.xyz * _LightColor0.xyz + UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 color = tex2D(_MainTex, i.uv).rgb * albedo;
if (factor < _ColorFactor)
{
return _DissolveColor;
}
return fixed4(color, 1);
}
ENDCG
}
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Pass {
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma geometry geo
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2g
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float4 objPos: TEXCOORD1;
};
struct g2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _BoxScale;
fixed4 _BoxColor;
float _ColorFactor;
float _DissolveThreshold;
float _Alpha;
v2g vert (appdata v) {
v2g o;
o.vertex = v.vertex;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.objPos = v.vertex;
return o;
}
#define ADD_VERT(v) \
o.vertex = UnityObjectToClipPos(v); \
TriStream.Append(o);
#define ADD_TRI(p0, p1, p2) \
ADD_VERT(p0) ADD_VERT(p1) \
ADD_VERT(p2) \
TriStream.RestartStrip();
[maxvertexcount(36)]
void geo(triangle v2g v[3], inout TriangleStream<g2f> TriStream) {
float4 vertex = (v[0].vertex + v[1].vertex + v[2].vertex) / 3;
float2 uv = (v[0].uv + v[1].uv + v[2].uv) / 3;
float3 edgeA = v[1].vertex - v[0].vertex;
float3 edgeB = v[2].vertex - v[0].vertex;
float3 normalFace = normalize(cross(edgeA, edgeB));
float factor = _DissolveThreshold - vertex.y;
//if(factor < 0) return;
if(factor < _ColorFactor){
//这里存粹是为效果而设置的,让它看上去是对的。
vertex.xyz += normalFace * clamp(-0.5f + vertex.y + _Time.y * 0.2f,0,5);
g2f o;
o.uv = uv;
float scale = _BoxScale;
float4 v0 = float4( 1, 1, 1,1)*scale + float4(vertex.xyz,0);
float4 v1 = float4( 1, 1,-1,1)*scale + float4(vertex.xyz,0);
float4 v2 = float4( 1,-1, 1,1)*scale + float4(vertex.xyz,0);
float4 v3 = float4( 1,-1,-1,1)*scale + float4(vertex.xyz,0);
float4 v4 = float4(-1, 1, 1,1)*scale + float4(vertex.xyz,0);
float4 v5 = float4(-1, 1,-1,1)*scale + float4(vertex.xyz,0);
float4 v6 = float4(-1,-1, 1,1)*scale + float4(vertex.xyz,0);
float4 v7 = float4(-1,-1,-1,1)*scale + float4(vertex.xyz,0);
ADD_TRI(v0, v2, v3);
ADD_TRI(v3, v1, v0);
ADD_TRI(v5, v7, v6);
ADD_TRI(v6, v4, v5);
ADD_TRI(v4, v0, v1);
ADD_TRI(v1, v5, v4);
ADD_TRI(v7, v3, v2);
ADD_TRI(v2, v6, v7);
ADD_TRI(v6, v2, v0);
ADD_TRI(v0, v4, v6);
ADD_TRI(v5, v1, v3);
ADD_TRI(v3, v7, v5);
}
}
fixed4 frag (g2f i) : SV_Target {
float4 col = _BoxColor;
col.a = _Alpha;
return col;
}
ENDCG
}
}
}
在这里选择用两个Pass,第一个绘制溶解边缘部分,注意这里是基于自身坐标的溶解。第二个用GS绘制立方体,类似于之前的三菱体。先找到面的中心点后视为坐标原点创建相应点和面。在生成立方体后移动中心点位置即可实现粒子散开的效果,这里简单起见沿着法线移动。
为了实现从上到下逐渐散开的效果,最好的想法是当到达溶解区域后粒子再向外运动而不是同步扩散,但这个shader反馈是脚本接收不到的,所以我弄了这个纯粹为效果的公式,不同模型还得改不同参数。然后在脚本中控制溶解速度来配合其扩散。
vertex.xyz += normalFace * clamp(-0.5f + vertex.y + _Time.y * 0.2f,0,5);
最终效果如下:
学习资料:
https://www.khronos.org/opengl/wiki/Geometry_Shader
https://github.com/n0mimono/CustomRendering