草庐IT

拼接贴图接缝问题的解决方案

duanoldfive 2023-03-28 原文

烦人的接缝

游戏中经常会用到在一个区域平铺某个贴图的需求,比如用碎石铺就的广场,我们通常会根据 顶点/像素 的世界坐标动态的计算UV值,达到贴图密度可调的目的。URP下Shader代码如下:

    float gridScale = 0.5;
    float2 roadUV = abs(frac(input.positionWS * gridScale).xz);
    surfaceData.albedo = SAMPLE_TEXTURE2D(_RoadTex, sampler_RoadTex, roadUV).rgb;
广场

细心的你一定注意到了,广场整体效果看起来还可以,但是有的地方有接缝的感觉,特别是摄像机移动或者旋转的过程中,尤其明显。(如红色箭头所指的地方)。

原因和解决方案

造成这个现象的原因主要由两个,一个是贴图的Mipmap,一个是贴图的过滤方式。我们导入贴图,一般默认是产生Mipmap, 过滤方式默认双线性过滤。
这种过滤方式会采样距离当前像素最近的四个纹素,然后根据像素到四个纹素点的距离进行插值来确定最终颜色,但是当UV值到达边缘值0或者1的时候,由于边缘像素对应的纹素少了一边或者两边,造成采样到的颜色和中间的像素颜色不一样,从而出现接缝现象。
Mip map 是根据距离摄像机远近不同,采用不同分辨率的贴图,而确定用哪个级别的贴图,需要用到UV的偏导数,而我们的UV是通过像素的世界坐标frac得到的,会导致偏导数不连续,从而采用了错误的Mipmap等级,使接缝变的更加明显。详见Unity Shader 关于tex2D中 dx dy 的猜想
知道了原因,我们就去修改一下试试效果,选中贴图,把Generate Mip Maps后面的勾选去掉,Filter Mode改成Point.别忘了点击Apply按钮应用设置。

设置贴图

再来看看效果:
效果

仔细观察刚才有接缝的地方,现在果然没有接缝了。

"就这样就好了吗?这也太简单了点吧?"

你的直觉是对的,事情肯定不是这么简单。
一般我们的地面会有多种材质,比如有石头路面,沙地,草地等等,怎么做呢?一般我们会把几种贴图合成到一张贴图中,类似Atlas,然后根据Mask贴图做混合,具体的可以研究一下刷地表的功能,这里主要讲接缝,就不展开了,我们为了实验,把两种贴图合成到一块看看会出现什么效果:

合成图片

效果

看起来不错,也没有接缝。以为万事大吉,可是等打包到手机平台,你就会发现还是出现了明显的接缝。
我分析可能是因为点过滤方式的采样,是取距离像素最近的纹素进行采样,像素落在两张贴图中间的时候(U值0.5的时候),会采样到另一侧的像素,所以接缝处有点土黄色,所以有一个解决方案是把每个贴图都外扩一定的像素,再合成一张贴图,然后对采样UV做一个clamp.这里就不详细介绍了,感兴趣的同学可以参考: 地形纹理合并

Textrue2DArray

今天我们要说的是另一种解决方案: Texture2DArray, 贴图数组,可以把它直接传给Shader,采样的时候可以指定index,然后就像采样单张贴图一样,系统会自动根据FilterMode,WrapMode处理采样中的各种问题,不会出现上面的两张贴图接缝处采样到另一边的问题,处理UV也简单很多。
详细文档地址: Texture2DArray

支持平台

可见该技术对平台有一定的要求,可以在运行时通过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(表示线性空间)。最好自己加个参数,给用户选择。


创建窗口

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字段处,运行项目,看看效果吧:


效果

完全看不到接缝了,打包到手机,同样完美。

感谢您的阅读,如果有什么意见建议欢迎联系我,共同进步。


最后给出项目地址 接缝项目

有关拼接贴图接缝问题的解决方案的更多相关文章

  1. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  4. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  5. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  6. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  7. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  8. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  9. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  10. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

随机推荐