Unity中实现纹理色块替换

项目开发笔记(十二)

哈,又一年了。在小时候过年总是非常有感觉,也满心期待。长大一点到了17 18岁的阶段,慢慢感觉过年失去味道 慢慢变成形式化,觉得过年传统做的节目都太无聊。又长大一点 到了25 26岁,过着常年背井离乡的生活 反而小时候对过年的期待又回来了,这几年我甚至非常热忱的参与到 杀猪 贴门神 拜天地等活动中…说回正题,我最近遇到一个需求.

一个小需求

对纹理中的指定色块,使用另一个纹理替换,如下图,这个图我们叫它原纹理

途中指定的颜色例如红色替换成如下纹理 ,这个图我们叫它目标纹理

效果大概变成如下图 ,这个图我们叫它效果纹理

原纹理中所有的色块均需用不同的目标纹理替换。(最终效果图可以拉到文末观看)

解决方案

要解决这个问题 需分两步。

  • 对纹理进行分色

其实就是找到纹理中主要的色块具体的颜色值,例如上图中的红色 RGBA(1,0,0,1).这里做了一回拷贝先生,朋友推荐了一个很好的实现方案 – C#版图片分色.经过很少的修改之后 让它适应于Unity。细微修改后的源码被我放到了github上,需要的请传送 – EICTextureColor,简单的使用代码如下:

    //-------------------------------------------------------------------------
    private void __ParserTextureColor()
    {
        // m_txtMain 为 Texture,请自行赋值
        if (null == m_txtMain)
        {
            return;
        }

        List<int ?[]> pColors = EICTextureColor.GetPalette(m_txtMain as Texture2D);
        if (null == pColors || 0 == pColors.Count)
        {
            return;
        }

        List<color> txtColors = new List</color><color>();
        List<string> strColors = new List</string><string>();
        foreach (var v in pColors)
        {
            if (null == v || 3 < v.Length)
            {
                continue;
            }
            txtColors.Add(new Color((float)v[0] / 255.0f, (float)v[1] / 255.0f, (float)v[2] / 255.0f));
            strColors.Add(__GetColor(v));
        }
        StringBuilder sb = new StringBuilder();

        int nIndex = 0;
        sb.AppendLine("该纹理的主要颜色为:");
        foreach (var v in txtColors)
        {
            sb.Append("<color=#" + strColors[nIndex ++ ] + ">");
            sb.Append(v.ToString());
            sb.Append("</string></color>,");
        }
        Debug.LogWarning(sb.ToString());

        if (null != m_text)
        {
            m_text.text = sb.ToString();
        }

    }
    //-------------------------------------------------------------------------

简单的效果如图:

注意,在Unity中如果需要对Texture的像素进行读取,需要设置原图片的属性,如图所示.

  1. 设置Texture Type为Advanced
  2. 勾选 Read/Write Enabled


详细看上图提示信息,基本可以找到纹理中主要的颜色值。

  • 使用目标纹理的颜色值 替换 原纹理的颜色值

根据需要 要实现这个效果,我想到一下两个方案

0x01.多个纹理叠加

第一反应想到的解决方案就是 每一种颜色做一个纹理,然后根据不同色块进行一个深度排列,最后把所有纹理根据排列一个一个贴上去。这个方案其实在3D游戏中叫地表贴花,典型的多层纹理混合。但是这种方案有一个明显的缺陷,越复杂的需要的纹理层数越多。这对于手机上的应用程序来说 性能上是一个灾难。

0x02.使用shader进行颜色判断和替换

大家都知道通过shader可以在渲染之前获得每一个像素的RGBA和UV值,我们可以在这个地方 根据UV和颜色值去取得目标纹理对应的颜色值,然后替换之。中和考虑后 我决定使用这种方式。

使用shader进行颜色判断和替换

基本步骤流程如下:

0x01. 通过EICTextureColor进行纹理分色取得原纹理某个颜色A

0x02. 通过shader获取原纹理的每个像素的颜色B 和 UV

0x03. 通过对AB颜色进行对比,如果符合则进入 0x04

0x04. 通过UV获取 目标纹理 的颜色值C

0x05. 用目标纹理上的颜色C 替换 原文里上的颜色A

0x06. Over

以下是Shader代码详解:

我们知道在shader中可以轻易的获取原纹理的每个像素的颜色 和 UV,如下代码中

    //-------------------------------------------------------------------------
    // _MainTex 为当前对象的纹理
    fixed4 frag(v2f IN) : SV_Target
    {
        half4 color = (tex2D(_MainTex, IN.texcoord)) * IN.color;
        return color;
    }
    //-------------------------------------------------------------------------

当前像素的的UV为 IN.texcoord,当前像素点的颜色值为 color,根据这个我们可以对每一个像素的的颜色值进行对比,和什么对比?和我们上一步通过纹理分色获得的颜色值进行对比,代码大概如下:

    //-------------------------------------------------------------------------
    //  source - 原纹理的颜色,就是在frag函数中获取的那个color,由调用者传入
    //  avg - 通过EICTextureColor进行纹理分色取得的某个颜色,由调用者传入
    //  _fTolerance - 容差值,由外部传入
    bool __CMPPiexl(fixed4 source, fixed4 avg)
    {
        float r = abs(source.r - avg.r);
        float g = abs(source.g - avg.g);
        float b = abs(source.b - avg.b);
        return r < _fTolerance && g < _fTolerance && b < _fTolerance;
    }
    //-------------------------------------------------------------------------

这里值得注意的是容差变量 _fTolerance , 所谓容差,就是误差。由于我们纹理分色取得的颜色值是一个大概的平均值,而原纹理中的色块中的颜色不可能绝对平整,这个时候这个误差就非常有用了。如果这个对比返回了true 则可以获取目标纹理身上的颜色值了,代码大概如下:

    //-------------------------------------------------------------------------
    // _nUVTileCount - 平铺参数
    // _AvgColor001 - 通过EICTextureColor进行纹理分色取得的某个颜色,由外部传入
    // _TagTex001 - 目标纹理 ,由外部传入
    fixed4 __CheckPiexl(float2 uv, fixed4 c)
    {
        bool bChange = false;
        float2 f = (float2)(uv * _nUVTileCount); 

        bChange = __CMPPiexl( c, _AvgColor001);
        if (true == bChange)
        {
            fixed4 target = tex2D(_TagTex001, f) * _Color;
            return target;
        }
        return c;
    }
    //-------------------------------------------------------------------------

关键部分完整串联起来就是如此:

    //-------------------------------------------------------------------------
    //  source - 原纹理的颜色,就是在frag函数中获取的那个color,由调用者传入
    //  avg - 通过EICTextureColor进行纹理分色取得的某个颜色,由调用者传入
    //  _fTolerance - 容差值,由外部传入
    bool __CMPPiexl(fixed4 source, fixed4 avg)
    {
        float r = abs(source.r - avg.r);
        float g = abs(source.g - avg.g);
        float b = abs(source.b - avg.b);
        return r < _fTolerance && g < _fTolerance && b < _fTolerance;
    }
    //-------------------------------------------------------------------------
    // _nUVTileCount - 平铺参数
    // _AvgColor001 - 通过EICTextureColor进行纹理分色取得的某个颜色,由外部传入
    // _TagTex001 - 目标纹理 ,由外部传入
    fixed4 __CheckPiexl(float2 uv, fixed4 c)
    {
        bool bChange = false;
        float2 f = (float2)(uv * _nUVTileCount); 

        bChange = __CMPPiexl( c, _AvgColor001);
        if (true == bChange)
        {
            fixed4 target = tex2D(_TagTex001, f) * _Color;
            return target;
        }
        return c;
    }
    //-------------------------------------------------------------------------
    // 容差,此参数由外部传入
    float _fTolerance;
    // 纹理平铺数量,此参数由外部传入
    int _nUVTileCount;
    // 原纹理,此参数由外部传入
    sampler2D _MainTex;
    // 分色出来的颜色值,此参数由外部传入
    fixed4 _AvgColor001;
    // 目标纹理,此参数由外部传入
    sampler2D _TagTex001;
    //-------------------------------------------------------------------------
    fixed4 frag(v2f IN) : SV_Target
    {
        half4 color = (tex2D(_MainTex, IN.texcoord)) * IN.color;

        color = __CheckPiexl(IN.texcoord, color);

        return color;
    }
    //------------------------------------------------------------------------- 

到此基本实现了整个流程,Shader关键部分和关键代码已经分享,稍微了解一点shader的同学应该很快可以照着源码写一个出来,由于是公司内部项目导致暂时无法完整分享Shader源码,请见谅。最终的效果如图

2D UI 上的效果图

3D 物体上的效果图

参考的文章们

  1. http://www.cnblogs.com/xinglizhenchu/p/5164816.html
  2. http://blog.csdn.net/u011416077/article/details/47665383

《Unity中实现纹理色块替换》有5个想法

发表评论

邮箱地址不会被公开。 必填项已用*标注