Geomerty Shader 是 SM4.0 Directx 9.0c 中引用的新技术, 它的位置位于 Vertex Shader 和 Fragment Shader 之间, 可以对一组(多个)顶点进行编程
Geometry Shader 可以自定义输出方式
PointStream
LineStream
TriangleStream
分别是 点,线,面
这样如果想做线框模式渲染,就非常的简单
上个线框模式图
请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/06/05/unity3d-geomerty-shader/
一个和 Unlit Shader 一样效果的 GS Shader 如下
Shader "Aoi/GSShader"
{
Properties
{
_SpriteTex("Base (RGB)", 2D) = "white" {}
_Size("Size", Range(0, 3)) = 0.5
_Tess("Tess", float) = 1
}
SubShader
{
Pass
{
Tags{ "RenderType" = "Opaque" }
LOD 200
CGPROGRAM
#pragma target 5.0
#pragma vertex VS_Main
#pragma fragment FS_Main
#pragma geometry GS_Main
#include "UnityCG.cginc"
// **************************************************************
// Data structures *
// **************************************************************
struct GS_INPUT
{
float4 pos : POSITION;
float3 normal : NORMAL;
float2 tex0 : TEXCOORD0;
};
struct FS_INPUT
{
float4 pos : POSITION;
float2 tex0 : TEXCOORD0;
};
// **************************************************************
// Vars *
// **************************************************************
float _Size;
float4x4 _VP;
sampler2D _SpriteTex;
float4 _SpriteTex_ST;
// **************************************************************
// Shader Programs *
// **************************************************************
// Vertex Shader ------------------------------------------------
GS_INPUT VS_Main(appdata_base v)
{
GS_INPUT output = (GS_INPUT)0;
output.pos = v.vertex;
output.normal = v.normal;
output.tex0 = v.texcoord;
return output;
}
// Geometry Shader -----------------------------------------------------
[maxvertexcount(4)]
void GS_Main(triangle GS_INPUT p[3], inout TriangleStream<FS_INPUT> triStream)
{
for (int i = 0; i < 3; i++) {
FS_INPUT pIn;
pIn.pos = mul(UNITY_MATRIX_MVP, p[i].pos);
pIn.tex0 = p[i].tex0;
triStream.Append(pIn);
}
}
// Fragment Shader -----------------------------------------------
float4 FS_Main(FS_INPUT input) : COLOR
{
return tex2D(_SpriteTex, input.tex0);
}
ENDCG
}
}
}
现在来介绍下GS Shader开启的方法
首先要定义2个预处理指令
#pragma target 4.0
#pragma geometry GS_Main
因为 gs shader 是在 sm 4.0 下才支持的, 如果不定义 target,可能会导致 shader.supported api 没效果, 导致部分低端设备的 fallback shader无法调用
之后就是 gs shader 的主体 gs_main 的代码
[maxvertexcount(4)] //当处理的顶点数 >= (数量) 才进行渲染
void GS_Main(triangle GS_INPUT p[3], inout TriangleStream<FS_INPUT> triStream)
//参数0 triangle GS_INPUT p[3] 表示输入数据类型转换成什么格式进行处理
//参数1 inout TriangleStream<FS_INPUT> triStream 输入输出流, 将处理的好的数据 append 到流中,完成处理
i.e
FS_INPUT pIn;
//pIn.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1.0));
pIn.pos = mul(UNITY_MATRIX_MVP, p[i].pos);
pIn.tex0 = p[i].tex0;
triStream.Append(pIn);
在这个 Shader 基础上,我们可以直接通过修改 inout 的输出类型,来输出不同渲染模式, 比如将
inout TriangleStream 修改为 inout PointStream
那么就可以看到 以顶点为单位, 中间没有像素插值的 皮卡丘
GS Shader 应用的场景
1.基于模型形状,以顶点为单位的 Billboard
如图,模型本身不是 Billboard, 但是构成模型的每个 Quad 却是 Billboard
只有VS 和 GS 部分的代码和 上面的 GS Shader 有区别
// Vertex Shader ------------------------------------------------
GS_INPUT VS_Main(appdata_base v)
{
GS_INPUT output = (GS_INPUT)0;
//output.pos = v.vertex;
output.pos = mul(_Object2World, v.vertex);
output.normal = v.normal;
output.tex0 = v.texcoord;
return output;
}
// Geometry Shader -----------------------------------------------------
[maxvertexcount(4)]
void GS_Main(point GS_INPUT p[1], inout TriangleStream<FS_INPUT> triStream)
{
float3 up = float3(0, 1, 0);
float3 look = _WorldSpaceCameraPos - p[0].pos;
look.y = 0;
look = normalize(look);
float3 right = (cross(up, look));
float halfS = 0.5f * 0.1;
float4 v[4];
v[0] = float4(p[0].pos + halfS * right - halfS * up, 1.0f);
v[1] = float4(p[0].pos + halfS * right + halfS * up, 1.0f);
v[2] = float4(p[0].pos - halfS * right - halfS * up, 1.0f);
v[3] = float4(p[0].pos - halfS * right + halfS * up, 1.0f);
float4x4 vp = mul(UNITY_MATRIX_MVP, _World2Object);
FS_INPUT pIn;
pIn.pos = mul(vp, v[0]);
pIn.tex0 = p[0].tex0;
triStream.Append(pIn);
pIn.pos = mul(vp, v[1]);
pIn.tex0 = p[0].tex0;
triStream.Append(pIn);
pIn.pos = mul(vp, v[2]);
pIn.tex0 = p[0].tex0;
triStream.Append(pIn);
pIn.pos = mul(vp, v[3]);
pIn.tex0 = p[0].tex0;
triStream.Append(pIn);
}
分析这个Shader 的做法。 为了制作 Quad Billboard, 输入输出类型 肯定是 TriangleStream, 因为一个Quad 是由2个三角形组成, 也因此 [maxvertexcount(4)] 数值为4, 4个顶点构成2个三角形,处理完4个顶点就可以输出了。
因为是将每个顶点,扩展为4个顶点做Quad 所以 输入类型为 point GS_INPUT p[1] 一次处理一个顶点
float4 v[4];
v[0] = float4(p[0].pos + halfS * right - halfS * up, 1.0f);
v[1] = float4(p[0].pos + halfS * right + halfS * up, 1.0f);
v[2] = float4(p[0].pos - halfS * right - halfS * up, 1.0f);
v[3] = float4(p[0].pos - halfS * right + halfS * up, 1.0f);
已当前顶点位置抽象为中心点,在它周围创建4个新的顶点,构成Quad.
你可能会问我,如果你想把 point GS_INPUT p[1] 改成 point GS_INPUT p[4] 行不行,已自身已有的4个顶点,来组成一个Quad. 我只能说,不行
因为 GS_INPUT p[Number] 的数量是跟 输入格式绑定的 point 格式 只能是1, line 格式 只能是2, triangle 格式只能是 3
至于用自身4个顶点来做quad 行不行呢, 当然是勉强可以,只是要换种方式做, 就是只保留一个顶点,忽略剩下的三个顶点,然后自己重建它们
添加一个结构体,用来创建Quad
static const float4 Quad_POSITION[4] =
{
float4(0, 0, 0, 0),
float4(0, 0, -1, 0),
float4(1, 0, 0, 0),
float4(1, 0, -1, 0),
};
ps:因为我的皮卡丘模型是颠倒的,所以需要 z 轴 * -1. 如果你是正常模型,把 -1 改为 1 即可
在Shader上添加
float4 _SpriteTex_ST_TexelSize;
在构建的Quad 上,也需要重建 uv, 因此需要 uv 的递进值
之后是 GS Shader
// Geometry Shader -----------------------------------------------------
[maxvertexcount(4)]
void GS_Main(triangle GS_INPUT p[3], inout TriangleStream<FS_INPUT> triStream)
{
float4 vertex = float4(p[0].pos.x, p[0].pos.y, p[0].pos.z, 1);
float2 uv = p[0].tex0;
for (int i = 3; i >= 0; i--) {
FS_INPUT pIn;
pIn.pos = mul(UNITY_MATRIX_MVP, vertex + Quad_POSITION[i].xyzw * 0.01);
pIn.tex0.xy = uv + Quad_POSITION[i].xy * _SpriteTex_ST_TexelSize.xy;
triStream.Append(pIn);
}
}
2. 为细分曲面做一些前置计算 (这需要 SM 5.0 dx11)
3. 使用一个Pass 绘制 Cubemap
不过GS 的性能好低,到DX11 还是很低, 目前仅限做粒子和一些变形渲染上用咯
by the way 由于 unity3d 的限制 ,没有修改 PointSize, 因此使用 PointStream 输出模型后,模型像素点大小没法缩放 ╮(╯▽╰)╭