开源项目LiquidEffect渲染分析

在这里插入图片描述

项目传送门

GitHub地址

实现流程

一共三个步骤:

  1. 创建若干个圆形Sprite作为“液体粒子”,每个“粒子”都是一个Dynamic的RigidBody2D,并赋予合适的物理材质和2D Collider
  2. 利用Blur着色器将所有的液体粒子图像模糊化并渲染到一张RenderTexture上
  3. 将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);
}

整体模糊处理一共三步

  1. 把本摄像机渲染的source数据降采样到四分之一
  2. 对降采样后对结果进行几次模糊迭代
  3. 把模糊处理的结果输出到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:欢迎评论(。^▽^)

posted on 2020-04-05 10:27  点赞机器人  阅读(382)  评论(0)    收藏  举报