开源项目LiquidEffect渲染分析
项目传送门
实现流程
一共三个步骤:
- 创建若干个圆形Sprite作为“液体粒子”,每个“粒子”都是一个Dynamic的RigidBody2D,并赋予合适的物理材质和2D Collider
- 利用Blur着色器将所有的液体粒子图像模糊化并渲染到一张RenderTexture上
- 将RenderTexture使用UnlitMod Shader(选择Alpha值在一定范围内的像素渲染成同一颜色)渲染到一个片儿上。
步骤一 创建刚体
作者用了一个比较讨巧的方式来模拟水,使用大量带物理属性的小球来模拟流体的流动,只要“粒子”足够多,足够小,就可以拟态流体
先看看他的场景里都有什么

- Scene节点下存放平台黑色阻挡物体
- Effect Camera为特效处理摄像机
- 设置了Culling Mask,只观察小球
- 设置了Target Texture,将渲染结果输出到一张RenderTexture
- MeshWithTextureFromCamera节点为Effect Camera渲染的RT输出
- MainCamera为主摄像机
- 设置了Culling Mask,观察阻挡物和MeshWithTextureFromCamera
- Particles Batch节点下是图中拟态用的蓝色小球,调整好合适的物理属性,摩擦力和弹性如下图:
![在这里插入图片描述]()
步骤二 降采样与模糊处理
首先排除其他观测障碍,关掉EffectCamera的HDR和抗锯齿,以便观察结果

先看OnRenderImage做了什么,上代码
// Called by the camera to apply the image effect
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
int rtW = source.width / 4;
int rtH = source.height / 4;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// Copy source to the 4x4 smaller texture.
DownSample4x(source, buffer);
// Blur the small texture
for (int i = 0; i < iterations; i++)
{
RenderTexture buffer2 = RenderTexture.GetTemporary(rtW, rtH, 0);
FourTapCone(buffer, buffer2, i);
RenderTexture.ReleaseTemporary(buffer);
buffer = buffer2;
}
Graphics.Blit(buffer, destination);
RenderTexture.ReleaseTemporary(buffer);
}
整体模糊处理一共三步
- 把本摄像机渲染的source数据降采样到四分之一
- 对降采样后对结果进行几次模糊迭代
- 把模糊处理的结果输出到destination,也就是屏幕
先看结果再看分析,打开FrameDebug窗口,点击播放,点到这个位置,如下图:

我的iterations迭代次数赋值为7,Draw GL数量为8,是因为第一个为DownSample4x(source, buffer)降采样操作,依次点击Draw GL会发现图片越来越模糊,如下图:

降采样分析
因为作者写法很的很有歧义,很不好懂,这里详细解释一下
C#部分
上代码,先是C#部分
// Downsamples the texture to a quarter resolution.
private void DownSample4x(RenderTexture source, RenderTexture dest)
{
float off = 1.0f;
Graphics.BlitMultiTap(source, dest, material,
new Vector2(-off, -off),
new Vector2(-off, off),
new Vector2(off, off),
new Vector2(off, -off));
}
Graphics.BlitMultiTap函数
- 第一个参数source为2048x2048尺寸的RenderTexture
- 第二个参数dest为1/4尺寸的512x512的RenderTexture
- 第三个参数是可变Vec2数组,官方解释是偏移了多少像素的UV坐标,并且依次填充到Quad方块的纹理单元中
Each vertex of the quad has multiple texture coordinates set up, offset by offsets pixels.
顶点着色器
然后是shader部分,原作者是这么写的
structv2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 taps[4] : TEXCOORD1;
};
//顶点着色器
v2f vert( appdata_img v ) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord - _BlurOffsets.xy * _MainTex_TexelSize.xy;
o.taps[0] = o.uv + _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[1] = o.uv - _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[2] = o.uv + _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
o.taps[3] = o.uv - _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
return o;
}
- _MainTex_TexelSize为Unity默认赋值,其为
Vector4(1 / width, 1 / height, width, height) - _BlurOffsets为Unity的黑魔法,默认赋值为
Graphics.BlitMultiTap函数的Vector2[] offsets的第一个值 - 顶点着色器里的_MainTex_TexelSize * _BlurOffsets.xy这个函数,Unity做了转化,实际为_MainTex_TexelSize.xy * _BlurOffsets.xy
模糊处理是采用的均值模糊,一个像素的颜色为上下左右的颜色平均值,中心像素为目标像素,他的值是周围四个格子颜色的平均数!
在这里插入图片描述
顶点着色器他就是想取到像素周围四个顶点的纹理坐标,这里看到作者只用了offset的第一个值,其他传入的参数并没有用到,估计也是参考的,所以我改了一下代码,让参数利用率高一些,如下:
v2f vert( appdata_full v ) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.taps[0] = v.texcoord;
o.taps[1] = v.texcoord1;
o.taps[2] = v.texcoord2;
o.taps[3] = v.texcoord3;
return o;
}
改了顶点输入结构appdata_full为Unity内置结构,贴出来给大家参考,如下,因为之前传入了四个偏移量,并且已经依次填入顶点数据并且已经算好偏移uv,所以直接赋值就好。(因为顶点到片元数据为差值计算,可以直接赋值在顶点数据里)
struct appdata_full
{
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
half4 texcoord4 : TEXCOORD4;
half4 texcoord5 : TEXCOORD5;
#endif
fixed4 color : COLOR;
};
片元着色器
//片元着色器
half4 frag(v2f i) : SV_Target {
half4 color = tex2D(_MainTex, i.taps[0]);
color += tex2D(_MainTex, i.taps[1]);
color += tex2D(_MainTex, i.taps[2]);
color += tex2D(_MainTex, i.taps[3]);
return color * 0.25;
}
- _MainTex为传入source图像
这个很好理解,算好周围颜色加起来做平均数
分析
输入为2048 * 2048的全屏RenderTexture,然后创建一个1/4大小的Rt buffer,实际就是创建一个1/4 Rt四边形,在片元着色器采样_MainTex,采样到四边形,则为GPU里的RT buffer镜像,存入真实采样color结果,根据选择的Rt格式为准(譬如float浮点数),此时这个Rt buffer为512 * 512,完成降采样
模糊分析
上代码
// Performs one blur iteration.
public void FourTapCone(RenderTexture source, RenderTexture dest, int iteration)
{
float off = 0.5f + iteration * blurSpread;
Graphics.BlitMultiTap(source, dest, material,
new Vector2(-off, -off),
new Vector2(-off, off),
new Vector2(off, off),
new Vector2(off, -off));
}
这里使用的shader和降采样使用的是同一shader,所以算纹理坐标是一样的,只不过是改了offsets的偏移值,之前降采样是取上下左右四个值,现在是根据模糊迭代次数取不同偏移量的值,offsets随着迭代次数递增,取越远的位置颜色,blurSpread为预先定义好的模糊系数,可以尝试调整观察结果。执行7次模糊结果如下图,然后将此模糊图像渲染到摄像机目标RT(2048x2048),这时如果设置纹理采样模式为双线性差值采样,模糊效果会更进一步。而且适当的降采样也会对模糊有帮助。

步骤三 Alpha过滤
这步骤思路很简单,就是把上面模糊处理的图像,把模糊的颜色也涂成纯色,这样就把小球周围扩大的模糊颜色链接起来,就变成了水。
- MeshWithTextureFromCamera为一个Quad方块,使用的材质贴图为刚才的RT
- 通过shader将Quad渲染出来
上代码,主要看片元着色器
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.texcoord);
clip(col.a - _Cutoff);
if(col.a < _Stroke) col = _StrokeColor;
else col = _Color;
return col;
}
操作非常简单,因为模糊处理后,小球中央不透明度最大,随着发散越边缘部分模糊越大,不透明度也越小,所以就可以通过Alpha进行过滤判断
- clip(col.a - _Cutoff); 将特别微弱的颜色剔除掉
- if(col.a < _Stroke) col = _StrokeColor; 将剔除后的最边缘部分显示为一个指定颜色
- else col = _Color; 将剩余部分填充指定颜色
最后,让我们看一个有描边的黄色液体,嘿嘿
![在这里插入图片描述]()
PS:欢迎评论(。^▽^)


浙公网安备 33010602011771号