在制作游戏时,经常需要用到将较小的重复循环的纹理图像拼成一个大图,比如地面上的尖刺,或是墙面砖块背景。遇到这种问题时通常的处理手段是像tilemap那样用单张纹理图片作为一个tile(unity中一般用sprite),将多个tile拼接起来形成一张大图。
例如有一张256x256的无缝墙面纹理,我希望用这张图铺满一个512x512大小的墙面, 512x512的墙面正好需要4个256x256的tile来拼接,拼接完成时如图1所示。
图1
但是这样处理要求拼接后的图像大小的宽和高需要分别为原图宽和高的整数倍,这样比较好处理。为了尽量满足这个要求,通常会将用作tile的图片做的非常小,这样可以满足尽量多的拼接图片不同大小的情况。
有没有一种更灵活的方式来处理这种问题,只用一个sprite就可以做出任意大小的墙面呢?
那必须有啊!
其实我们可以借助shader来完成这样一个效果。在opengl中有一个参数叫做GL_TEXTURE_WRAP,就是在纹理超出边界怎么处理。有一种模式是GL_REPEAT,就是可以将纹理进行重复。受到这个启发,我们是不是也可以利用这种重复模式,来实现缩放sprite时,纹理的大小不变,多出的部分则自动用重复的纹理进行铺满呢?
当然可以!只不过会麻烦一些......
unity中,纹理有一个wrap mode属性,可以设置成repeat或是clamp,其中repeat就是我们想要的重复模式。但是unity2d会自动将导入的纹理转换成sprite类型纹理,在sprite类型纹理的属性中,我们无法调整纹理的wrap mode属性所以我们首先需要将导入的纹理变成texture类型,如图2所示。修改完之后记得点apply进行保存。
图2
之后我们在unity编辑器新建一个sprite,这是我们发现新建的sprite不能直接使用我们修改的纹理了,所以我们需要通过脚本来用纹理生成一个sprite对象,赋给新建的sprite。像这样:
// 将你的纹理YourTextureForSprite变成sprite Sprite sprite = Sprite.Create((Texture2D)YourTextureForSprite, new Rect(0, 0, YourTextureForSprite.width, YourTextureForSprite.height), new Vector2(0.5f, 0.5f)); GetComponent().sprite = sprite;
有了纹理,下一步我们希望sprite在缩放时并不改变原始纹理的大小,而是将纹理进行重复。默认的shader是无法完成这项工作的,这时候就需要我们自己去写一个shader了。我们需要将sprite的scale值告诉shader,以便进行处理。这里我将unity自带的默认的sprite shader改写了一下,代码如下:
Shader "Custom/RepeatShader"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap("Pixel snap", Float) = 0
_ScaleX("ScaleX", Float) = 1
_ScaleY("ScaleY", Float) = 1
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
Fog{ Mode Off }
Blend One OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile DUMMY PIXELSNAP_ON
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
half2 texcoord : TEXCOORD0;
};
fixed4 _Color;
fixed _ScaleX;
fixed _ScaleY;
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap(OUT.vertex);
#endif
return OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN) : SV_Target
{
IN.texcoord.x *= _ScaleX;
IN.texcoord.y *= _ScaleY;
fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
c.rgb *= c.a;
return c;
}
ENDCG
}
}
}
这里简单解释一下,纹理坐标(x,y)是两个0到1的值,与纹理的实际大小无关,(0,0)代表纹理的左下角,(1,1)代表纹理的右上角,其实可以理解成该位置与纹理宽高所对应比例。举个例子,假设是一个1x2的纹理,shader中纹理坐标(0,0)代表纹理中(0,0)的像素位置,shader中纹理坐标(1,1)代表纹理中(1,2)的像素位置。
当sprite被缩放时,取到纹理坐标仍然是是两个0到1的值不变,所以这里将纹理坐标乘上缩放值,实际上就是将缩放后绘制出来的纹理再放缩回去,还原成原来的大小。但是多出来部分怎么办呢?比如纹理坐标(0.6,0.7)都乘上2,变成了(1.2,1.4),两个值都大于1了,shader会怎么处理呢?这里由于纹理的wrap mode是repeat,shader就会自动取超出来的部分重新作为纹理坐标来处理,从而使纹理重复出来,(1.2,1.4)两个值都减去取值范围最大值1,就变成了(0.2,0.4)成为了新的纹理坐标。
有了shader,我们自然要新建一个材质,并将shader选为我们自定义的shader。如图3:
图3
纹理和材质都有了,下一步就该完成我们的脚本了,同纹理一样,我们也需要将material赋给SpriteRenderer组件。
这里有一个问题,我们不能在属性面板里直接给SpriteRenderer组件赋值,而需要在脚本里实例化一个材质赋给SpriteRenderer组件。为什么呢?因为unity默认情况下是多个sprite共享同一个material的,如果使用该material的对象有多个,那么当其中一个改变的material中的变量时,其他的对象中的material也会改变。这当然不是我们想要看到的,对吧?所以我们需要实例化material,相当于新建一个同样的material赋给每个使用该material的对象,这样就互不影响了。(注意使用下面的脚本时需要现将SpriteRenderer在属性面板中的material置空,当然你也可以修改脚本......)
最后脚本的完整代码如下:
using UnityEngine;
using System.Collections;
public class RepeatSprite : MonoBehaviour {
// 用作tile的纹理
public Texture TextureForSprite;
// 纹理所使用的自定义材质
public Material MaterialOrigin;
// 初始缩放
public Vector3 scale = Vector3.one;
// SpriteRenderer组件
SpriteRenderer spriteRenderer;
// Use this for initialization
public void Start()
{
transform.localScale = scale;
spriteRenderer = GetComponent();
// 若tile纹理不为空,则使用tile纹理创建相应的sprite纹理赋给SpriteRenderer组件
if (TextureForSprite != null)
{
Sprite sprite = Sprite.Create((Texture2D)TextureForSprite, new Rect(0, 0, TextureForSprite.width, TextureForSprite.height), new Vector2(0.5f, 0.5f));
spriteRenderer.sprite = sprite;
}
// 若自定义使用的材质不为空,则实例化一个材质赋给SpriteRenderer组件
if (MaterialOrigin != null)
{
spriteRenderer.sharedMaterial = Instantiate(MaterialOrigin);
}
// 若自定义材质创建成功,则设置shader获取的缩放值为sprite的缩放值
if (spriteRenderer.sharedMaterial)
{
spriteRenderer.sharedMaterial.SetFloat("_ScaleX", transform.lossyScale.x);
spriteRenderer.sharedMaterial.SetFloat("_ScaleY", transform.lossyScale.y);
}
}
#if UNITY_EDITOR
// 当在编辑器中改变脚本中的缩放值时,同时改变shader获取的缩放值和sprite的大小
void OnValidate()
{
// 在编辑器中做同Start()初始化时相同的工作
if (spriteRenderer == null)
{
spriteRenderer = GetComponent();
}
if (TextureForSprite != null && spriteRenderer.sprite == null)
{
Sprite sprite = Sprite.Create((Texture2D)TextureForSprite, new Rect(0, 0, TextureForSprite.width, TextureForSprite.height), new Vector2(0.5f, 0.5f));
spriteRenderer.sprite = sprite;
}
if (MaterialOrigin != null && spriteRenderer.sharedMaterial == null)
{
spriteRenderer.sharedMaterial = Instantiate(MaterialOrigin);
}
// 改变shader获取的缩放值和sprite的大小为脚本设置的缩放值
if (transform.localScale != scale)
{
transform.localScale = scale;
if (spriteRenderer.sharedMaterial)
{
spriteRenderer.sharedMaterial.SetFloat("_ScaleX", transform.lossyScale.x);
spriteRenderer.sharedMaterial.SetFloat("_ScaleY", transform.lossyScale.y);
}
}
}
#endif
}
最后注意,我们加上脚本之后,只能够通过修改脚本的缩放值才能获得我们想要的结果,在transform组件中修改是无效的哦!下面放出一张效果图:
图4