现在的游戏中很多特效都喜欢用到扭曲效果,常见的实现方案都是在Shader中使用GrabPass,来获取屏幕的画面然后去做计算。关于获取屏幕画面的几种方案的性能分析可以参考我上篇文章。
idleworm:Unity中GrabPass、OnRenderImage、TargetTexture性能测试zhuanlan.zhihu.com我们先搭建一个简单的测试场景,背景是几个贴了数字格的面片,一个球体在最前面,三个面片作为扭曲的区域,测试机还是ViVO X5ProV。
我们先来看下GrabPass的实现方案在低端机上的性能表现
下面来看下使用CommandBuffer实现方案在测试机上的性能表现
可以看到性能还是提升了很多的,接下来我们就来看下怎么通过CommandBuffer来实现扭曲效果。
实现的思路:
要把相机渲染的当前画面当做贴图传递给shader,然后通过一张动态的noise贴图来采样这张纹理,最后输出到屏幕上。我们每个相机都有一个Render Target,可以通过Camera.targetTexture来设置,如果这个目标为空的时候那就是直接写入到Back-Buffer里面,也就是显示在了屏幕上。GrabPass的实现就是在相机渲染完后,从Back-Buffer中把显示的内容拷贝到一张RenderTexture上面,再通过采样这种RT来扭曲画面。经过测试发现把Back-Buffer中的内容拷贝到RT上,这个操作的开销就不小了,所以我们用CommandBuffer的实现方案是,先将相机渲染的画面直接存到一张RT上面,再把RT传递给shader做计算,然后把结果通过CommandBuffer绘制到屏幕上。
需要注意的几个地方:
1.当Camera设置了TargetTexture后就,原本渲染到的Back-Buffer就没有东西了,而是储存在了RT里面,所以屏幕上会显示黑色并提示没有相机在执行渲染。
我们可以新建一个相机和一个Quad来负责专门显示最后输出的屏幕的结果,但是这样可能会比较麻烦。如果项目中本来就要将主相机的画面渲染到一张RenderTexture上,然后再把它显示在绘制UI的相机上,有些场景为了解决UI和三维场景穿插显示层级的时候,客户端会选择这样的方案。
这里我是在OnPreRender中设置TargetTexture,然后在OnPostRender中将TargetTexture设为null,再通过CommandBuffer在屏幕上绘制一个Quad来显示相机渲染的画面。
private void OnPreRender()
{
renderCam.targetTexture = screenCopyRT;
}
private void OnPostRender()
{
renderCam.targetTexture = null;
}
2.使用CommandBuffer来绘制最后显示的画面和扭曲的区域,扭曲的区域只能显示在相机原始画面的上面,这样扭曲的遮挡关系就没有办法处理了。所以我们还要把场景的深度信息也传递给Shader,在Shader里面通过深度的比较来确定遮挡关系。这里我们也不使用Camera.depthTextureMode = DepthTextureMode.Depth,然后在Shader中通过_CameraDepthTexture这样来拿到相机的深度信息。而是在设置相机的RenderTarget的时候把ColorBuffer和DepthBuffer分别输出到两张RT上,再把这两张RT传递给Shader就行了。
private void OnPreRender()
{
renderCam.SetTargetBuffers(screenColorRT.colorBuffer , screenDepthRT.depthBuffer);
}
3.用CommandBuffer在绘制最终显示画面的时候一定要先ClearRenderTarget,把之前缓冲区的颜色和深度都清除掉,否则在移动端会有一些显示Bug。
CommandBuffer.ClearRenderTarget(true , true , Color.black);
完整的项目使用如下:
先将DistortManager脚本挂在场景的相机上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;
[RequireComponent(typeof(Camera))]
public class LX_DistortManager : MonoBehaviour
{
//用Dictionary来管理场景中需要做扭曲效果的物体,通过挂在物体身上的脚本来添加或者移除
private Dictionary<Renderer , Material> rendererDict = new Dictionary<Renderer , Material>();
private const string commandBufferName = "DistortCommandBuffer";
private const string screenColorName = "_ScreenColorTexture";
private const string screenDepthName = "_ScreenDepthTexture";
//CameraEvent用来指定CommandBuffer在什么时候执行
private CameraEvent cameraEvent = CameraEvent.AfterImageEffects;
//RT的分辨率,也会影响到最终输出到屏幕的分辨率
public int DownSample = 0;
//显示相机原本画面的Quad绘制距离,要在所有扭曲区域之后
public float ShowMeshDistance = 100;
private CommandBuffer commandBuffer;
private Camera renderCam;
private int screenColorID = -1;
private int screenDepthID = -1;
private Resolution currentResolution = new Resolution();
private RenderTexture screenColorRT;
private RenderTexture screenDepthRT;
//用来显示相机原本画面的Quad
private Mesh camShowMesh;
private Material camShowMaterial;
private static LX_DistortManager _distortManager;
public static LX_DistortManager GetInstance
{
get
{
if (!_distortManager)
{
_distortManager = GameObject.FindObjectOfType<LX_DistortManager>();
}
return _distortManager;
}
}
private Camera GetCamera
{
get
{
if (!renderCam)
{
renderCam = GetComponent<Camera>();
}
return renderCam;
}
}
private void OnEnable()
{
Initialize();
SetResolution();
CreateCommandBuffer();
}
private void OnPreRender()
{
//当Dictionary里面有需要扭曲的物体时才执行,如果场景中没有需要扭曲的物体就会继续按照原来的方式渲染
if (rendererDict.Count > 0)
{
//当屏幕分辨率发生变化是更新显示
if (currentResolution.width != renderCam.pixelWidth || currentResolution.height != renderCam.pixelHeight)
{
SetResolution();
CreateCommandBuffer();
}
//设置相机渲染目标的ColorBuffer和DepthBuffer分别设置到两张RenderTexture上
renderCam.SetTargetBuffers(screenColorRT.colorBuffer , screenDepthRT.depthBuffer);
}
}
private void OnPostRender()
{
if (rendererDict.Count > 0)
renderCam.targetTexture = null;
}
private void OnDestroy()
{
//删除CommandBuffer
DestroyCommandBuffer();
//释放申请的两张RenderTexture
RenderTexture.ReleaseTemporary(screenColorRT);
RenderTexture.ReleaseTemporary(screenDepthRT);
//清空Dictionary
rendererDict.Clear();
//删除QuadMesh
Destroy(camShowMesh);
}
private void CreateCommandBuffer()
{
DestroyCommandBuffer();
if (rendererDict.Count > 0)
{
RenderTexture.ReleaseTemporary(screenColorRT);
RenderTexture.ReleaseTemporary(screenDepthRT);
//申请两张RT,存储ColorBuffer的不需要深度值所以Depth值为0,存储DepthBuffer的格式为RenderTextureFormat.Depth
screenColorRT = RenderTexture.GetTemporary(renderCam.pixelWidth >> DownSample , renderCam.pixelHeight >> DownSample , 0 , RenderTextureFormat.Default);
screenDepthRT = RenderTexture.GetTemporary(renderCam.pixelWidth >> DownSample , renderCam.pixelHeight >> DownSample , 16 , RenderTextureFormat.Depth);
screenColorRT.name = "CopyColorTempRT";
screenDepthRT.name = "CopyDepthTempRT";
commandBuffer = new CommandBuffer();
commandBuffer.name = commandBufferName;
commandBuffer.Clear();
//清除之前缓冲区中的Color和Depth
commandBuffer.ClearRenderTarget(true , true , Color.black);
//将存储了Color和Depth的两张RT传递给shader
commandBuffer.SetGlobalTexture(screenColorID , screenColorRT);
commandBuffer.SetGlobalTexture(screenDepthID , screenDepthRT);
//绘制一个Quad来显示相机原本渲染的画面
camShowMesh = CreateCamShowMesh(ShowMeshDistance);
commandBuffer.DrawMesh(camShowMesh , Matrix4x4.identity , camShowMaterial);
//遍历Dictionary将场景中需要扭曲的物体绘制出来
foreach (var dict in rendererDict)
{
commandBuffer.DrawRenderer(dict.Key , dict.Value);
}
renderCam.AddCommandBuffer(cameraEvent , commandBuffer);
}
}
private void SetResolution()
{
currentResolution.width = renderCam.pixelWidth;
currentResolution.height = renderCam.pixelHeight;
}
private void DestroyCommandBuffer()
{
if (commandBuffer != null)
{
GetCamera.RemoveCommandBuffer(cameraEvent , commandBuffer);
commandBuffer.Clear();
commandBuffer = null;
}
}
private void Initialize()
{
if (!renderCam)
{
renderCam = GetComponent<Camera>();
}
if (screenColorID == -1)
{
screenColorID = Shader.PropertyToID(screenColorName);
}
if (screenDepthID == -1)
{
screenDepthID = Shader.PropertyToID(screenDepthName);
}
if (camShowMaterial == null)
{
camShowMaterial = new Material(Shader.Find("Custom/Common/LX_CommandBufferTex"));
}
}
//往Dictionary中添加需要扭曲的物体
public void AddRenderer(Renderer renderer , Material material)
{
rendererDict.Add(renderer , material);
CreateCommandBuffer();
//Debug.Log(rendererDict.Count);
}
//从Dictionary中移除不需要扭曲的物体
public void RemoveRenderer(Renderer renderer)
{
rendererDict.Remove(renderer);
CreateCommandBuffer();
//Debug.Log(rendererDict.Count);
}
//通过屏幕四个点和distance在场景中绘制一个Quad
private Mesh CreateCamShowMesh(float distance)
{
Vector3[] vertices = new Vector3[4];
vertices[0] = renderCam.ScreenToWorldPoint(new Vector3(0 , 0 , distance));
vertices[1] = renderCam.ScreenToWorldPoint(new Vector3(0 , renderCam.pixelHeight , distance));
vertices[2] = renderCam.ScreenToWorldPoint(new Vector3(renderCam.pixelWidth , renderCam.pixelHeight , distance));
vertices[3] = renderCam.ScreenToWorldPoint(new Vector3(renderCam.pixelWidth , 0 , distance));
Vector2[] uvs = new Vector2[4];
uvs[0] = new Vector2(0 , 0);
uvs[1] = new Vector2(0 , 1);
uvs[2] = new Vector2(1 , 1);
uvs[3] = new Vector2(1 , 0);
int[] triangleID = new int[6];
triangleID[0] = 0;
triangleID[1] = 1;
triangleID[2] = 2;
triangleID[3] = 2;
triangleID[4] = 3;
triangleID[5] = 0;
Mesh drawMesh = new Mesh();
drawMesh.vertices = vertices;
drawMesh.uv = uvs;
drawMesh.triangles = triangleID;
return drawMesh;
}
}
将AddDistort脚本挂在需要扭曲的物体上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Renderer))]
public class LX_AddDistort : MonoBehaviour
{
private Renderer _renderer;
private Material _material;
private LX_DistortManager _DistortManager;
void Start()
{
_renderer = this.GetComponent<Renderer>();
_material = _renderer.sharedMaterial;
_DistortManager = LX_DistortManager.GetInstance;
_DistortManager.AddRenderer(_renderer , _material);
_renderer.enabled = false;
}
private void OnEnable()
{
if (_DistortManager)
_DistortManager.AddRenderer(_renderer , _material);
}
private void OnDisable()
{
if (_DistortManager)
_DistortManager.RemoveRenderer(_renderer);
}
}
用来渲染QuadMesh的Shader
Shader "Custom/Common/LX_CommandBufferTex"
{
Properties
{
}
SubShader
{
Tags
{
"RenderType"="Opaque"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _ScreenColorTexture;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_ScreenColorTexture, i.uv);
return col;
}
ENDCG
}
}
}
用来渲染扭曲物体的Shader
Shader "Custom/FX/LX_FX_DistortCommandBuffer"
{
Properties
{
_NoiseTex ("NoiseTexture", 2D) = "white" {}
_DistortStrength ("DistortStrength", Range(0,2)) = 0.2
_DistortTimeFactor ("DistortTimeFactor", Range(0,2)) = 1
_Offset ("Offset", float) = 0
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"IgnoreProjector"="True"
"PreviewType"="Plane"
"DisableBatching" = "True"
}
LOD 100
Cull Off Lighting Off ZWrite Off
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 grabPos : TEXCOORD1;
float4 pos : SV_POSITION;
};
sampler2D _NoiseTex; float4 _NoiseTex_ST;
sampler2D _ScreenColorTexture;
sampler2D _ScreenDepthTexture;
fixed _DistortStrength;
fixed _DistortTimeFactor;
fixed _Offset;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _NoiseTex);
o.color = v.color;
o.grabPos = ComputeScreenPos(o.pos);
COMPUTE_EYEDEPTH(o.grabPos.z);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float Zbuffer = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_ScreenDepthTexture, UNITY_PROJ_COORD(i.grabPos)));
float2 offset = tex2D(_NoiseTex, i.uv.xy - _Time.y * _DistortTimeFactor).xy;
i.grabPos.xy -= offset.xy * _DistortStrength * i.color.a;
fixed4 col = tex2Dproj(_ScreenColorTexture, i.grabPos + _Offset) * saturate(Zbuffer - i.grabPos.z);
return col;
}
ENDCG
}
}
}