糍粑大叔的独游之旅-u3d中2D轮廓的生成(中)
创建RTCamera
先创建RTCamera,设置一个非常远的地方(99999,99999),在这里开展渲染到纹理的工作。当然,也可以通过culling mask来通过layer来保障只有target被渲染,但要单独设置layer,会比较麻烦,所有在非常远的地方,不会有其他对象,可以起到相同目的。可以根据实际情况灵活选择。
这里必须要先设置一个target texture,否则,u3d会将他当一般的摄像机处理。
(这里吐槽一下,个人觉得,u3d在代码动态控制这一方面用的很别扭,基于场景和节点的方式,有时候很不灵活)
创建rt
需要提供rt的长和宽,因为轮廓纹理一定大于原始图像,建议对原始图像放大1.5-2倍作为rt的长和宽,我的游戏中,单位是很多sprite组合而成,所以,算的公共的bounds。
// 根据输入的x和y,创建一个rt
RenderTexture rt = new RenderTexture((int)x, (int)y, 16);
rt.Create();
GetComponent<Camera>().targetTexture = rt;
// 调整cemara的范围以适应rt
float cameraSize = y / 2;
GetComponent<Camera>().orthographicSize = cameraSize;
设置位置或layer,保证RTCamera只“看”目标对象
保存原始位置和旋转。
如果使用layer,这里用设置target的layer
// 移动目标到摄像机处,保证只有目标在摄像机的范围内
// 由于此用一个极远的位置,此处只有目标,所有可以不用设置layer
Vector3 pos = transform.position;
pos.z = 0;
target.transform.position = pos;
target.transform.eulerAngles = Vector3.zero;
//target.layer = RTGenLayer;
进行Solid渲染
使用SolidShader进行渲染,SolidShader是一个无光照、无纹理计算,尽绘制白色的shader。在u3d中,使用ShaderLab的固定管线即可实现,但由于我的资源图像不那么规范,所以只能自己做一些特殊处理。
除了以下设置和fragment外,没有什么特殊的:
Cull Off
Lighting Off
ZWrite Off
Blend Off
fragment shader:
fixed4 frag(v2f IN) : SV_Target
{
fixed4 c = tex2D (_MainTex,IN.texcoord);
if( c.a > 0.2 )
return fixed4(1,1,1,1);
else
return fixed4(0,0,0,0);
}
用SolidShader渲染
m_SolidMat = new Material(Shader.Find("Outterline/Solid"));
GetComponent<Camera>().RenderWithShader(m_SolidMat.shader, "");
RenderWithShader可以外出一个独立的渲染,这里没有用到第二个参数(替换某些场景中的shader)
渲染完成后,可以将target的位置和旋转还原
对rt进行扩大
这部分对于不熟悉render to texture的人来说比较复杂,standard assets和网上有很多例子,这里简单讲一下原理。
RenderTexture buffer = RenderTexture.GetTemporary(rt.width, rt.height, 0);
RenderTexture buffer2 = RenderTexture.GetTemporary(rt.width, rt.height, 0);
Graphics.Blit(rt, buffer);
bool oddEven = true;
for (int i = 0; i < iterations; i++)
{
if (oddEven)
_FourTapCone(buffer, buffer2, i, spread);
else
_FourTapCone(buffer2, buffer, i, spread);
oddEven = !oddEven;
}
if (oddEven)
{
Graphics.Blit(rt, buffer, m_CutoffMaterial);
Graphics.Blit(buffer, rt, m_CompositeMaterial);
}
else
{
Graphics.Blit(rt, buffer2, m_CutoffMaterial);
Graphics.Blit(buffer2, rt, m_CompositeMaterial);
}
RenderTexture.ReleaseTemporary(buffer);
RenderTexture.ReleaseTemporary(buffer2);
其中
void _FourTapCone(RenderTexture source, RenderTexture dest, int iteration, float spread)
{
float off = 0.5f + iteration * spread;
Graphics.BlitMultiTap(source, dest, m_BlurMaterial,
new Vector2(off, off),
new Vector2(-off, off),
new Vector2(off, -off),
new Vector2(-off, -off)
);
}
过程为:
1、创建2个等大小的临时rt:buffer和buffer2
2、把rt给blit到buffer,blit可以认为是u3d里的rt到rt的像素复制
3、buffer和buffer2交互做BlitMultiTap,我认为,对于BlitMultiTap不用太深究,它就是u3d做image effect的方法,设置了shader里的相关的参数。这个步骤的目的就是通过周围(由offset指定)的4个点来,来实现模糊。offset越大,采样距离越大,模糊扩大程度越大。
4、根据复制的次数,判断从buffer或buffer2作为源,生成rt,这里有两个步骤:a、从rt进行blit到buffer或buffer2,使用cutoffbodyshder,即消除rt里本身的那部分;b、再从buffer或buffer2复制回rt,得到最终rt。
几个关键的shader:
m_BlurMaterial = new Material(Shader.Find("Outterline/ShapeBlur"));
m_CutoffMaterial = new Material(Shader.Find("Outterline/CutoffBody"));
m_CompositeMaterial = new Material(Shader.Find("Outterline/Composer"));
ShapeBlue的关键点:
v2f vert (appdata_img v)
{
v2f o;
float offX = _MainTex_TexelSize.x * _BlurOffsets.x;
float offY = _MainTex_TexelSize.y * _BlurOffsets.y;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
float2 uv = MultiplyUV (UNITY_MATRIX_TEXTURE0, v.texcoord.xy-float2(offX, offY));
o.uv[0].xy = uv + float2( offX, offY);
o.uv[0].zw = uv + float2(-offX, offY);
o.uv[1].xy = uv + float2( offX,-offY);
o.uv[1].zw = uv + float2(-offX,-offY);
return o;
}
sampler2D _MainTex;
fixed4 frag( v2f i ) : COLOR
{
fixed4 c;
c = tex2D( _MainTex, i.uv[0].xy );
c += tex2D( _MainTex, i.uv[0].zw );
c += tex2D( _MainTex, i.uv[1].xy );
c += tex2D( _MainTex, i.uv[1].zw );
return c /2 ;
}
用uv存储offset,栅格化后,用offset对每个像素周围采样
CutoffBody的关键点:
Blend One One
BlendOp RevSub,Add
ZTest Always
Cull Off
ZWrite Off
Fog { Mode Off }
...
...
half4 frag(v2f i) : COLOR
{
fixed4 c = tex2D(_MainTex, i.uv);
if( c.r > 0 )
return fixed4(1,1,1,1);
else
return fixed4(0,0,0,0);
}
使用RevSub将rt里像素减去
创建轮廓绘制对象
至此,rt已经保存了白色的轮廓图像。现在需要把他在target上绘制出来。
这个步骤本来很简单,用sprite就行。
但对rendertexture好像不能用sprite绘制。。。(可能是我没找到办法)
所以,必须自己做一个支持rt的sprite,即创建一个四边形mesh,设置material和rt。
Mesh mesh;
MeshRenderer renderer = gameObject.GetComponent<MeshRenderer>();
mesh = GetComponent<MeshFilter>().mesh;
/*
* 2 3
* 0 1
*/
int sectionCount = 1;
int[] triangles = new int[ sectionCount * 6];
Vector3[] vertices = new Vector3[ sectionCount * 4];
Color[] colors = new Color[ sectionCount * 4];
Vector2[] uv = new Vector2[ sectionCount * 4];
for (int i = 0; i < sectionCount; i++)
{
vertices[4 * i] = new Vector2(-x / 2, -y / 2);
vertices[4 * i + 1] = new Vector2(x / 2, -y / 2);
vertices[4 * i + 2] = new Vector2(-x / 2, y / 2);
vertices[4 * i + 3] = new Vector2(x / 2, y / 2);
colors[4 * i] = Color.white;
colors[4 * i + 1] = Color.white;
colors[4 * i + 2] = Color.white;
colors[4 * i + 3] = Color.white;
uv[4 * i] = new Vector2(0, 0);
uv[4 * i + 1] = new Vector2(1, 0);
uv[4 * i + 2] = new Vector2(0, 1);
uv[4 * i + 3] = new Vector2(1, 1);
}
for (int i = 0; i < sectionCount; i++)
{
triangles[6 * i] = (i * 4) + 0;
triangles[6 * i + 1] = (i * 4) + 3;
triangles[6 * i + 2] = (i * 4) + 1;
triangles[6 * i + 3] = (i * 4) + 0;
triangles[6 * i + 4] = (i * 4) + 2;
triangles[6 * i + 5] = (i * 4) + 3;
}
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.colors = colors;
mesh.uv = uv;
m_RT = rt;
m_Material = new Material(Shader.Find("Outterline/Render"));
m_Material.hideFlags = HideFlags.HideAndDontSave;
renderer.material = m_Material;
renderer.material.color = color;
renderer.material.mainTexture = rt;
代码里创建4个顶点,两个三角形索引,需要注重索引的顺序,u3d的Z方向采用左手准则。
m_RT为生成的轮廓纹理,m_Material使用Outterline/Render作为shader,
通过设置color,控制轮廓的颜色。
Outterline/Render没有什么特殊的,使用tex2D (_MainTex,IN.texcoord) * _Color作为像素输出。
总结
生成rt后,rt作为纹理一直使用,可以不用实时生成,通过enable、disable控制轮廓是否显示,设置材质的color属性,改变颜色。这样大幅提升了效率,避免了反复的像素处理。
一次动态生成,静态使用。缺点是,如果有动画或target子节点的transform变化,轮廓就失效。但这些问题也不难解决。
本文说明了u3d中2D轮廓生成的技术实现细节,下篇中,将对这些过程进行提炼和封装,形成易用的结构