拼接贴图接缝问题的解决方案
烦人的接缝
游戏中经常会用到在一个区域平铺某个贴图的需求,比如用碎石铺就的广场,我们通常会根据 顶点/像素 的世界坐标动态的计算UV值,达到贴图密度可调的目的。URP下Shader代码如下:
float gridScale = 0.5;
float2 roadUV = abs(frac(input.positionWS * gridScale).xz);
surfaceData.albedo = SAMPLE_TEXTURE2D(_RoadTex, sampler_RoadTex, roadUV).rgb;
![](https://img.haomeiwen.com/i13524317/229e163e4f487d10.png)
细心的你一定注意到了,广场整体效果看起来还可以,但是有的地方有接缝的感觉,特别是摄像机移动或者旋转的过程中,尤其明显。(如红色箭头所指的地方)。
原因和解决方案
造成这个现象的原因主要由两个,一个是贴图的Mipmap,一个是贴图的过滤方式。我们导入贴图,一般默认是产生Mipmap, 过滤方式默认双线性过滤。
这种过滤方式会采样距离当前像素最近的四个纹素,然后根据像素到四个纹素点的距离进行插值来确定最终颜色,但是当UV值到达边缘值0或者1的时候,由于边缘像素对应的纹素少了一边或者两边,造成采样到的颜色和中间的像素颜色不一样,从而出现接缝现象。
Mip map 是根据距离摄像机远近不同,采用不同分辨率的贴图,而确定用哪个级别的贴图,需要用到UV的偏导数,而我们的UV是通过像素的世界坐标frac得到的,会导致偏导数不连续,从而采用了错误的Mipmap等级,使接缝变的更加明显。详见Unity Shader 关于tex2D中 dx dy 的猜想
知道了原因,我们就去修改一下试试效果,选中贴图,把Generate Mip Maps后面的勾选去掉,Filter Mode改成Point.别忘了点击Apply按钮应用设置。
![](https://img.haomeiwen.com/i13524317/a2c03ec1b4a9d45f.png)
再来看看效果:
![](https://img.haomeiwen.com/i13524317/1fa003ecffc0241e.png)
仔细观察刚才有接缝的地方,现在果然没有接缝了。
"就这样就好了吗?这也太简单了点吧?"
你的直觉是对的,事情肯定不是这么简单。
一般我们的地面会有多种材质,比如有石头路面,沙地,草地等等,怎么做呢?一般我们会把几种贴图合成到一张贴图中,类似Atlas,然后根据Mask贴图做混合,具体的可以研究一下刷地表的功能,这里主要讲接缝,就不展开了,我们为了实验,把两种贴图合成到一块看看会出现什么效果:
![](https://img.haomeiwen.com/i13524317/aa18d9cfb77234ef.png)
![](https://img.haomeiwen.com/i13524317/6552cf0395a5a0ba.png)
看起来不错,也没有接缝。以为万事大吉,可是等打包到手机平台,你就会发现还是出现了明显的接缝。
我分析可能是因为点过滤方式的采样,是取距离像素最近的纹素进行采样,像素落在两张贴图中间的时候(U值0.5的时候),会采样到另一侧的像素,所以接缝处有点土黄色,所以有一个解决方案是把每个贴图都外扩一定的像素,再合成一张贴图,然后对采样UV做一个clamp.这里就不详细介绍了,感兴趣的同学可以参考: 地形纹理合并
Textrue2DArray
今天我们要说的是另一种解决方案: Texture2DArray, 贴图数组,可以把它直接传给Shader,采样的时候可以指定index,然后就像采样单张贴图一样,系统会自动根据FilterMode,WrapMode处理采样中的各种问题,不会出现上面的两张贴图接缝处采样到另一边的问题,处理UV也简单很多。
详细文档地址: Texture2DArray
![](https://img.haomeiwen.com/i13524317/87ed582139d5162c.png)
可见该技术对平台有一定的要求,可以在运行时通过SystemInfo.supports2DArrayTextures来判断是否支持,不支持的可以按照之前的做法用拼图方式进行处理,不过随着硬件的发展,大部分设备都能够支持了。
创建资源
Texture2DArray没有办法通过Potoshop创建,也没有办法通过Unity Create菜单直接创建,只能通过脚本创建,所以需要写一个工具类,放到Editor文件夹下:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
//创建Texture2DArray
public class TextureArray : EditorWindow
{
public int PropertyNum = 10;
public List<Texture2D> textures = new List<Texture2D>();
[MenuItem("Tools/Texture2DArray")]
static void Init()
{
TextureArray window = (TextureArray)EditorWindow.GetWindow(typeof(TextureArray), false, "TextureArray", true);
window.Show();
}
private float spaceNumber = 10f;
private void OnGUI()
{
GUILayout.Space(spaceNumber);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField("要合成的贴图:", GUILayout.Width(100), GUILayout.Height(30));
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
for (int i=0; i<textures.Count; i++)
{
EditorGUILayout.BeginHorizontal();
textures[i] = (Texture2D)EditorGUILayout.ObjectField(textures[i], typeof(Texture2D), true, GUILayout.Width(64), GUILayout.Height(64));
if (GUILayout.Button("-", GUILayout.Width(30), GUILayout.Height(30)))
{
textures.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
GUILayout.Space(spaceNumber);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+", GUILayout.Height(30)))
{
textures.Add(null);
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(spaceNumber);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("创 建", GUILayout.Height(30)))
{
CreateTextureArray();
}
EditorGUILayout.EndHorizontal();
}
public void CreateTextureArray()
{
//如果没有指定要合成的贴图,或者都为空,则直接返回
textures.RemoveAll(tex => tex == null);
if (textures.Count == 0)
{
Debug.LogError("Please select textures for combine");
return;
}
Texture2D firstTex = textures[0];
//Create texture2DArray
Texture2DArray texture2DArray = new Texture2DArray(firstTex.width,firstTex.height, textures.Count, firstTex.format, false, false);
// Apply settings
//texture2DArray.filterMode = firstTex.filterMode;
//texture2DArray.wrapMode = firstTex.wrapMode;
texture2DArray.filterMode = FilterMode.Point;
texture2DArray.wrapMode = TextureWrapMode.Clamp;
int index = 0;
foreach(Texture2D tex in textures)
{
for (int m = 0; m < tex.mipmapCount; m++)
{
Graphics.CopyTexture(tex, 0, m, texture2DArray, index, m);
}
index++;
}
//Save
string path = EditorUtility.SaveFilePanel("Save As", "Assets", "texArray", "asset");
if (path.Length > 0)
{
path = path.Substring(Application.dataPath.Length - 6);
AssetDatabase.CreateAsset(texture2DArray, path);
}
}
}
然后Unity菜单中点击 Tools->Textrue2DArray,会弹出一个窗口,点击加号按钮,把要合成的贴图拖到对应的框内,等把所有要合成到一起的贴图全部处理好,点击合成按钮,选择位置,文件名,就会创建出一个Texture2DArray的资源了。
这里要注意的是,合成在一起的所有贴图要有一样的大小,格式,Import选项。
另外由于法线贴图都是线性空间的而不像普通贴图的gamma空间,所以要用这个工具处理法线贴图,请在new texture2DArray的时候,最后一个参数传true(表示线性空间)。最好自己加个参数,给用户选择。
![](https://img.haomeiwen.com/i13524317/892645d6fc5dda45.png)
Texture2DArray的使用
资源有了,现在就是怎么使用了,给要使用的Shader添加代码:
Properties
{
...
_RoadTex("Road texture", 2DArray) = "" {}
}
SubShader
{
...
Pass
{
...
//声明变量
TEXTURE2D_ARRAY(_RoadTex); SAMPLER(sampler_RoadTex);
...
half4 LitPassFragment(Varyings input) : SV_Target
{
int index = 1; //贴图索引,请根据项目需求自行设置,这里只是演示,固定取索引1
//计算UV坐标
float gridScale = 0.25;
float2 roadUV = abs(frac(input.positionWS * gridScale).xz);
//采样
surfaceData.albedo = SAMPLE_TEXTURE2D_ARRAY(_RoadTex, sampler_RoadTex, roadUV, 1).rgb;
...
}
}
}
Shader准备好以后,把刚才创建的Array资源拖到材质面板的Road texture字段处,运行项目,看看效果吧:
![](https://img.haomeiwen.com/i13524317/692b664d593e2cba.png)
完全看不到接缝了,打包到手机,同样完美。
感谢您的阅读,如果有什么意见建议欢迎联系我,共同进步。
最后给出项目地址 接缝项目