Unity特效在ScrollView中的裁剪
1.序列帧
按照特效的效果,做序列帧动画
问题和缺点
效果会差点
2.添加相机
添加一个Camera,这个Canera只显示ScrollView的实际显示区域
我们先添加一个新的Layer层RewardEffect,并将新添加的Camera的显示层设置为RewardEffect.
Camera中的Viewport Rect是其可见的区域,这里我们要调整为这个区域与ScrollView的显示区域吻合.
特效的Layer要设置为RewardEffect,特效不再会出现在UICamera中,而是由新的Camera负责显示.由于相机区域的设置,特效在区域内的部分会正常显示,拖动超出相机范围后,就看不到了,实现了裁剪的效果。
如何比较方便准确的设置摄像范围,有一个脚本UIViewport,将这个脚本挂在新加的摄像机上.在ScrollView的左上角和右下角位置各放一个Transform,设置到UIViewport
问题和缺点
1.特效所属的Layer层级要高于UI的的Layer层,当这个界面弹出另一个界面(比如奖励领取界面),特效就显示在新界面之上了(可选择在新界面出现的时候监听暂时隐藏特效,但解决方案不够完美).
3.使用RenderTexture
1.创建相机,相机提前做好prefab,然后在使用的时候动态创建.
GameObject camera = (GameObject)Resources.Load(@"UI/Prefabs/Camera/RoleCameraVariateInfo", typeof(GameObject));
if (camera != null)
{
GameObject go = GameObject.Instantiate(camera) as GameObject;
if (go != null &&
go.camera != null)
{
m_Camera = go.camera;
}
}
2.加载特效文件,并设置特效及相机位置
Object model = Resources.Load("Effect/UI/Business/Busi_Effect001Test");
if (null == model)
{
return;
}
m_Model = GameObject.Instantiate(model) as GameObject;
if (null == m_Model)
{
return;
}
var t = m_Model.transform;
t.localPosition = new Vector3(0f, m_nIndex * MODEL_OFFSET, 0f);
t.localScale = modelScale > 0 ? new Vector3(modelScale, modelScale, modelScale) : new Vector3(1f, 1f, 1f);
m_Camera.transform.localPosition = new Vector3(cameraX, cameraY + m_nIndex * MODEL_OFFSET, cameraZ);
3.给相机设置targetTexture
m_Camera.clearFlags = CameraClearFlags.Color;
m_Camera.renderingPath = RenderingPath.DeferredLighting;
RenderTexture rt = RenderTexture.GetTemporary(uiTexture.width, uiTexture.height, 0);
if (rt != null)
{
m_Camera.targetTexture = rt;
m_Camera.mainTexture = rt;
m_Camera.Render();
}
问题和缺点
1.当特效种类较多,不同应用场景多的时候,开发成本偏高.
4.通过Shader实现裁剪
将ScrollView的显示区域传给特效的Shader,超出显示区域部分将透明度设置为0,实现裁剪.这个方案实施分为两个部分:修改特效的Shader和显示区域传给Shader.
1.修改特效的Shader,下面是我们修改后的Shader,其中// add clip的部分是为了实现裁剪添加的部分:
// 裁剪shader
Shader "Effect_Mid/ScrollClipAdditive"
{
Properties
{
......
// add clip
_MinX ("Min X", Float) = -10
_MaxX ("Max X", Float) = 10
_MinY ("Min Y", Float) = -10
_MaxY ("Max Y", Float) = 10
// add clip
}
SubShader
{
Tags
{
......
}
Pass
{
......
// ZTest Off
Fog { Color (0,0,0,0) }
......
// add clip
float _MinX;
float _MaxX;
float _MinY;
float _MaxY;
// add clip
......
struct VS_OUTPUT
{
......
// add clip
float3 vpos : TEXCOORD2;
// add clip
};
VS_OUTPUT vert(VS_INPUT In)
{
......
// add clip
Out.vpos = mul(UNITY_MATRIX_MVP, In.position);
// add clip
......
return Out;
}
float4 frag(VS_OUTPUT In) : COLOR
{
......
// add clip
color.a *= (In.vpos.x >= _MinX );
color.a *= (In.vpos.x <= _MaxX);
color.a *= (In.vpos.y >= _MinY );
color.a *= (In.vpos.y <= _MaxY);
// add clip
return color;
}
ENDCG
}
}
Fallback off
CustomEditor "EffectMaterialEd"
}
另外一个shader
Shader "PJ Particles/PJ Additive 1"
{
Properties
{
_TintColor ("Tint Color", Color) = (0.5,0.5,0.5,0.5)
_MainTex ("Particle Texture (A = Transparency)", 2D) = "white" {}
_InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
//新增 记录裁剪框的四个边界的值
_Area ("Area", Vector) = (0,0,1,1)
//----end----
}
Category
{
Tags { "Queue"="Transparent+300" "IgnoreProjector"="True" "RenderType"="Transparent" }
Blend SrcAlpha One
AlphaTest Greater .01
ColorMask RGB
Cull Off Lighting Off ZWrite Off Fog { Color (0,0,0,0) }
BindChannels
{
Bind "Color", color
Bind "Vertex", vertex
Bind "TexCoord", texcoord
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_particles
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed4 _TintColor;
//新增,对应上面的_Area
float4 _Area;
//----end----
struct appdata_t
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
//新增,记录顶点的世界坐标
float2 worldPos : TEXCOORD1;
//----end----
};
float4 _MainTex_ST;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color;
o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
//新增,计算顶点的世界坐标
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xy;
//----end----
return o;
}
sampler2D _CameraDepthTexture;
float _InvFade;
fixed4 frag (v2f i) : COLOR
{
//新增,判断顶点坐标是否在裁剪框内
bool inArea = i.worldPos.x >= _Area.x && i.worldPos.x <= _Area.z && i.worldPos.y >= _Area.y && i.worldPos.y <= _Area.w;
//----end----
//如果在裁剪框内return原本的效果,否则即隐藏
return inArea? 2.0f * i.color * _TintColor * tex2D(_MainTex, i.texcoord) : fixed4(0,0,0,0);
}
ENDCG
}
}
}
}
2.将显示区域传给Shader,为了方便使用,写了一个脚本,使用时将该脚本挂在特效上或多个特效的父节点上,然后设置UIPanel给脚本.
脚本代码如下:
using UnityEngine;
using System.Collections;
public class EffectClipImpl : MonoBehaviour
{
// 要实现特效参见的UIPanel(UIPanel和ScrollView是结合使用的,真正管理裁剪区域的是UIPanel)
public UIPanel m_RootPanel = null;
// 默认值,写了一个很大的数值,保证不设置Panel时可以正常显示
float m_MinX = -10;
float m_MinY = -10;
float m_MaxX = 10;
float m_MaxY = 10;
void Start()
{
Reset();
}
public void Reset()
{
if (m_RootPanel != null)
{
// 获取UI摄像机,为了获取panel相对于屏幕中心点的位置
GameObject UIRootCamera = GameObject.Find("UI/UIRoot(Clone)/UICamera(Clone)");
if (UIRootCamera != null)
{
// 获取panel相对于屏幕中心点的位置,准确说是获取ScrollView的实际显示区域相对于屏幕中心点的位置
Vector3 toCameraPos = UIRootCamera.transform.InverseTransformPoint(m_RootPanel.transform.position);
// 处理UIPanel的Center偏移
toCameraPos += new Vector3(m_RootPanel.baseClipRegion.x, m_RootPanel.baseClipRegion.y, 0);
// 去除滑动的偏移量
// 添加特效的时候ScrollView可能已经滑动过了,这时候ScrollView的坐标位置发生了变化
// 我们根据ScrollView的坐标位置计算显示区域的位置,要计算这部分滑动的量
toCameraPos += new Vector3(m_RootPanel.clipOffset.x, m_RootPanel.clipOffset.y, 0);
// 获取Panel实际显示区域的大小
Vector2 viewSize = m_RootPanel.GetViewSize();
// 过渡部分处理,panel的过渡部分透明度会渐变消失
// 我们项目使用的Softness基本都是4,比较小,这里处理为Softness的一半区域后消失
// 虽然方案不完美,看上去还可以(有需要的话可以把Softness的值也传给Shader,做过渡消失的处理)
Vector2 clipSoftness = m_RootPanel.clipSoftness;
Vector2 clipSize = viewSize - clipSoftness * 0.5f;
// 获取窗口大小
Vector2 screenViewSize = m_RootPanel.GetWindowSize();
float width = screenViewSize.x;
float height = screenViewSize.y;
// 计算要传送给Shder的四个值,最小X,最大X,最小Y,最大Y
// 这个值的大小是相对于屏幕常和宽的比例值
m_MinX = -((float)(clipSize.x * 0.5f - toCameraPos.x)) / (width * 0.5f);
m_MinY = -((float)(clipSize.y * 0.5f - toCameraPos.y)) / (height * 0.5f);
m_MaxX = ((float)(clipSize.x * 0.5f + toCameraPos.x)) / (width * 0.5f);
m_MaxY = ((float)(clipSize.y * 0.5f + toCameraPos.y)) / (height * 0.5f);
// 传输范围数据
ExcuteChild(gameObject);
}
}
}
// 传输范围数据
void ExcuteChild(GameObject go)
{
if (go.renderer != null)
{
// material与sharedMaterial用法相同,但是效率有差别
// sharedMaterial是共享材质,修改的话内存只占用一份,如果使用material的话,每次属性修改都会new一份新的出来
// 但在编辑器模式下,修改sharedMaterial会导致本地文件发生变化,为防止不必要的文件变动和提交,这里区分平台处理
#if UNITY_EDITOR
Material[] list = go.renderer.materials;
#else
Material[] list = go.renderer.sharedMaterials;
#endif
for (int i = 0; i < list.Length; i++)
{
list[i].SetFloat("_MinX", m_MinX);
list[i].SetFloat("_MinY", m_MinY);
list[i].SetFloat("_MaxX", m_MaxX);
list[i].SetFloat("_MaxY", m_MaxY);
}
}
// 遍历处理子节点
int count = go.transform.childCount;
for (int i = 0; i < count; i++)
{
ExcuteChild(go.transform.GetChild(i).gameObject);
}
}
}
另外一个脚本配合第二个shader
using System.Collections.Generic;
using UnityEngine;
namespace UI
{
public class EffectClip : MonoBehaviour
{
// 遮挡容器,即ScrollView
[SerializeField] RectTransform m_rectTrans;
// 存放需要修改Shader的Material
List<Material> m_materialList = new List<Material>();
// UI的根,Canvas
Transform m_canvas;
float m_halfWidth, m_halfHeight, m_canvasScale;
void Start ()
{
m_canvas = GameObject.Find("Canvas").transform;
// 获取所有需要修改shader的material,并替换shader
var particleSystems = GetComponentsInChildren<ParticleSystem>();
for(int i = 0, j = particleSystems.Length; i < j ; i++)
{
var ps = particleSystems[i];
var mat = ps.GetComponent<Renderer>().material;
m_materialList.Add(mat);
mat.shader = Shader.Find(mat.shader.name + " 1");
}
var renders = GetComponentsInChildren<MeshRenderer>();
for(int i = 0, j = renders.Length; i < j; i++)
{
var ps = renders[i];
var mat = ps.material;
m_materialList.Add(mat);
mat.shader = Shader.Find(mat.shader.name + " 1");
}
// 获取UI的scale,容器的宽高的一半的值
m_canvasScale = m_canvas.localScale.x;
m_halfWidth = m_rectTrans.sizeDelta.x * 0.5f * m_canvasScale;
m_halfHeight = m_rectTrans.sizeDelta.y * 0.5f * m_canvasScale;
// 给shader的容器坐标变量_Area赋值
Vector4 area = CalculateArea(m_rectTrans.position);
for(int i = 0, len = m_materialList.Count; i < len; i++)
{
m_materialList[i].SetVector("_Area", area);
}
}
// 计算容器在世界坐标的Vector4,xz为左右边界的值,yw为下上边界值
Vector4 CalculateArea(Vector3 position)
{
return new Vector4()
{
x = position.x - m_halfWidth,
y = position.y - m_halfHeight,
z = position.x + m_halfWidth,
w = position.y + m_halfHeight
};
}
}
}
总结
第一个方案,效果打了折扣
第二个方案,开发维护成本高,有一些不好避免的问题
第三个方案,显示粒子特效异常,可以有选择的进行使用;
第四个方案,是个比较好的实现方案.如果有裁剪需求的特效使用的是通用的几个Shader,那么这个方案就更划算了.即使有几个特殊的Shader需要修改,成本也不算太高.