(图1:正常渲染)
(图2:几何着色器粒子化特效进行中)
(图3:几何着色器粒子化特效进行中)
1,用几何着色器进行图元转换
在OpenGL渲染管线中,几何着色器Geometry Shader有一个独一无二的功能,既是图元转换。可简单理解为对基本图元点,线,三角形等等之间的转换。基本图元是由顶点组成的,所以几何着色器可以在函数内拿到一个基本图元的所有组成顶点,例如输入图元如果是三角形,它可以拿到三个顶点。
几何着色器相关语法很简单,以本文中Shader为例:
声明着色器:
#pragma geometry geom
设置输出顶点数量:
[maxvertexcount(120)]
声明输入与输出struct:
struct v2g //vertex to geometry
{
float4 vertex : SV_POSITION;
fixed4 color:COLOR;
float3 normal:NORMAL;
};
struct g2f //geometry to fragment
{
float4 vertex : SV_POSITION;
fixed4 color:COLOR;
};
配置几何着色器的输入参数与输出值:“PointStream”决定了几何着色器的输出数据结构,“triangle v2g”必须与顶点着色器输出(既是几何着色器的输入)的图元类型一致,数组长度也要一致,例如当它为三角形图元时,结构数组长度必须为[3],既是每次同时被输入一个三角形的三个顶点。
void geom(inout PointStream<g2f> OutputStream,triangle v2g input[3])
{
...
}
在本案例中,一个三角形分解为3个点,需要一个方法将输出的点收集起来:
OutputStream.Append(o);
其他图元之间的转换在语法上大同小异。
2,用几何着色器对图元进行细分
除了细分着色器(见另一篇文章),几何着色器也可以用来进行细分工作。
例如本例中将模型网格进行了粒子化,为了加强效果,增加粒子数量,对每个三角形图元都进行了细分。几何着色器对图元进行细分后输出的顶点数是有上限的,根据输出stream的结构体(例如本例中的g2f)的大小,Shader会对输出顶点数做出限制,本例中,每个三角形最多可以转换为120个粒子顶点,“[maxvertexcount(120)]”。
细分的算法
首先需要三个向量,一个位置向量V0作为起点,以及从起点至另两个顶点的方向向量V1,V2:
V1 = (input[1].vertex - input[0].vertex).xyz;
V2 = (input[2].vertex - input[0].vertex).xyz;
V0 = input[0].vertex.xyz;
接下来,利用V1,V2对三角形进行参数化。
(图4:三角形参数化。原图来自http://web.engr.oregonstate.edu/~mjb/cs519/Handouts/geometry_shaders.1pp.pdf)
我对三角形参数化的理解是:从V0出发,以V1为方向行进x个单位,再以V2为方向行进y个单位,可到达三角形内任意一点:
(图5:从V0出发按照V1,V2方向行进到达点P0,P1)
Shader代码实现
int numLayers =1<<_Level; //2^_Level
float dt = 1.0f / float( numLayers );
float t = 1.0f;
for( int it = 0; it < numLayers; it++ )
{
float smax = 1.0f - t;
int nums = it + 1;
float ds = smax / float( nums - 1 );
float s = 0;
for( int is = 0; is < nums; is++ )
{
float3 v = V0 + s*V1 + t*V2;
//略......
s += ds;
}
t -= dt;
}
上面代码删去与细分无关的部分,核心思想既是在双重循环中等距的向V1,V2方向移动,移动过程中找出的所有点既是三角形细分后的点的集合。
(图6:一个待细分的quad)
(图7:用此算法对quad进行细分后,由于输出顶点数达到了极限120,中间部分为空白)
_Level变量可用来控制细分中t方向的密度:
(图8:_Level=4)
(图9:_Level=1)
3,用几何着色器构建粒子系统
使用了几何着色器后,它就成为了面向顶点编程的最后阶段,可在此对顶点进行动画编程。例如案例中的Shader在几何着色器中实现了一个位移动画和淡出效果。
粒子位移
在顶点属性准备阶段,C#脚本不断更新一个unityTime全局变量。
Shader.SetGlobalFloat ("unityTime", Time.time);
在Shader内计算动画累计时间:
float time_SinceBirth=(unityTime-_ShaderStartTime)*0.1f;
计算重心坐标:
CG=(input[0].vertex.xyz + input[1].vertex.xyz+ input[2].vertex.xyz)/3.0f;
位移:根据动画时间进行位移加速。此行代码决定了此Shader中粒子的移动效果,如果想模拟真实物理效果可套入一些公式。此行代码没什么特殊思想,只是根据时间有一个越飞越快的效果。
v = CG + vel*(_Speed*time_SinceBirth+1.0f) + 0.5f*_DispDir.xyz*sin(it*is)*(_Speed*time_SinceBirth)*(_Speed*time_SinceBirth);
淡出效果
根据动画累计时间对alpha值进行递减让粒子逐渐消失:
o.color=_FinalColor;
o.color.w=1.0f-smoothstep(0,1.0f,time_SinceBirth);
除了通过alpha进行淡出处理也可以通过语义ponitsize对粒子size进行缩小处理以达到淡出(但目前在unity 5.x版本中使用此语义无法通过编译)。
4,案例源码:
Shader:
Shader "Unlit/ParticleExp_Beta"
{
Properties
{
//细分相关变量
_Level("Level",int)=0
_DispDir("Displacement Direction",Vector)=(0,0,0)
_uVelScale("VelScale",float)=2
//粒子化特效相关变量
_Speed("Speed",Range(0,1))=1
_ShaderStartTime("Shader Start Time",float)=0
_FinalColor("Final Color",color)=(1,1,1,1)
}
SubShader
{
Tags{"RenderType"="Transparent" "Queue" = "Transparent"}
LOD 100
Pass
{
Blend SrcAlpha OneMinusSrcAlpha // use alpha blending
cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
细分相关变量
uniform int _Level;
uniform float3 _DispDir;
uniform float _uVelScale;
粒子化特效相关变量
uniform float _Speed; //粒子位移速度
uniform float _ShaderStartTime; //粒子化起始时间
uniform fixed4 _FinalColor; //粒子颜色
float3 V0, V1, V2;
float3 CG;
float unityTime;
struct appdata
{
float4 vertex : POSITION;
float3 normal:NORMAL;
};
struct v2g
{
float4 vertex : SV_POSITION;
fixed4 color:COLOR;
float3 normal:NORMAL;
};
struct g2f
{
float4 vertex : SV_POSITION;
fixed4 color:COLOR;
};
v2g vert (appdata v)
{
v2g o;
o.vertex = v.vertex;
o.normal=UnityObjectToWorldNormal(v.normal);
return o;
}
[maxvertexcount(120)]//v2g input[3]
void geom(inout PointStream<g2f> OutputStream,triangle v2g input[3])
{
float time_SinceBirth=(unityTime-_ShaderStartTime)*0.1f;
g2f o = (g2f)0;
V1 = (input[1].vertex - input[0].vertex).xyz;
V2 = (input[2].vertex - input[0].vertex).xyz;
V0 = input[0].vertex.xyz;
CG=(input[0].vertex.xyz + input[1].vertex.xyz+ input[2].vertex.xyz)/3.0f;
int numLayers =1<<_Level; //2^_Level
float dt = 1.0f / float( numLayers );
float t = 1.0f;
for( int it = 0; it < numLayers; it++ )
{
float smax = 1.0f - t;
int nums = it + 1;
float ds = smax / float( nums - 1 );
float s = 0;
for( int is = 0; is < nums; is++ )
{
float3 v = V0 + s*V1 + t*V2;
float3 vel = _uVelScale * ( v - CG );
v = CG + vel*(_Speed*time_SinceBirth+1.0f) + 0.5f*_DispDir.xyz*sin(it*is)*(_Speed*time_SinceBirth)*(_Speed*time_SinceBirth);
o.vertex = UnityObjectToClipPos(float4( v, 1.0f ));
o.color=_FinalColor;
o.color.w=1.0f-smoothstep(0,1.0f,time_SinceBirth);
OutputStream.Append(o);
s += ds;
}
t -= dt;
}
}
fixed4 frag (g2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
C#动画触发控制脚本:
挂在场景中任意gameObject上。主要功能是收集model上的所有材质球并对其进行逐渐替换以实现逐渐粒子化的效果。另一个思路是同时替换所有材质球,然后根据uv或位置坐标进行更细腻的粒子化渐变。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ParticleExpController : MonoBehaviour {
public Material particleExp;
public MeshRenderer[] smRs;
private Material[] originalMaterial;
public GameObject model;
// Use this for initialization
void Start () {
}
IEnumerator EXP(){
smRs=model.GetComponentsInChildren<MeshRenderer>();
Material p_exp = new Material (particleExp);
p_exp.SetFloat ("_ShaderStartTime", Time.time);
for (int i = 0; i < smRs.Length; i++) {
Material[] temp=smRs[i].materials;
for(int j=0;j<smRs[i].materials.Length;j++){
temp [j] = p_exp;
}
smRs [i].materials = temp;
yield return new WaitForSeconds (0.5f);
}
}
// Update is called once per frame
void Update () {
if(Input.GetKeyDown(KeyCode.E)){
StartCoroutine (EXP ());
}
Shader.SetGlobalFloat ("unityTime", Time.time);
}
}
参考:
OPENGL编程指南–Khronos Group
GLSL Geometry Shader–Mike Bailey–
(http://web.engr.oregonstate.edu/~mjb/cs519/Handouts/geometry_shaders.1pp.pdf)
射线和三角形的相交检测–DirectX
(http://www.cnblogs.com/graphics/archive/2010/08/09/1795348.html)
维护日志:
2018-2-24:填词改句
2018-6-10:改标题
2020-8-16:review