糍粑大叔的独游之旅-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轮廓生成的技术实现细节,下篇中,将对这些过程进行提炼和封装,形成易用的结构

posted @ 2016-07-26 17:34  糍粑大叔  阅读(199)  评论(0编辑  收藏  举报