# 前言

使用 Unity 实现脚印,这种功能很简单,而且之前我也写过一篇相关 Blog 来描述: 链接

过去很久之后,这个功能终于又被 “旧事重提” 了,不过,新的要求是:根据玩家在地形不同的位置,检测地形相应点的贴图,再根据贴图来判断播放的特效、音效及其它处理。因为策划希望能在地形相应的区域,有着不同的效果,而最省力的实现方式,便莫过于如此了。

这个方式的最大难点大约在于:如何取得玩家当前点的地形贴图?包括几张贴图的混合区域等。

Splat Texture

地形混合贴图

# 分析

为了获取到这个信息,我首先调试了整个地形数据,并分析其中 “可能” 有用的几项,主要位于 TerrainData 中:

s
// 摘要:
        //     ///
        //     Height of the alpha map.
        //     ///
        public int alphamapHeight { get; }
        // 摘要:
        //     ///
        //     Width of the alpha map.
        //     ///
        public int alphamapWidth { get; }
        // 摘要:
        //     ///
        //     The total size in world units of the terrain.
        //     ///
        public Vector3 size { get; set; }
        // 摘要:
        //     ///
        //     Splat texture used by the terrain.
        //     ///
        public SplatPrototype[] splatPrototypes { get; set; }

在上边几个参数中,只所以将 AlphaMap 当作重要参数,是因为在 Unity 中,不同的地形贴图,就是全靠这个贴图进行混合完成的,所以它也是获取各贴图的重要线索。
Size 作为地形的大小,用于判断 Alpha 贴图与地形实际大小的差距 (如果有的话)。比如我这儿地形大小是 4096,贴图大小是 2048。为此,在取样的时候,同样必须注意坐标的转换。

TerrainData

splatPrototypes 更不必说,这就是地形所用到的几张贴图了,到时候最终信息肯定就是从这儿取。

在 TerrainData 中,提供了一个 “GetAlphamaps” 的接口,这个接口便可以用于获取指定点指定宽度的 Alpha 贴图信息:

s
// 摘要:
        //     ///
        //     Returns the alpha map at a position x, y given a width and height.
        //     ///
        //
        // 参数:
        //   x:
        //
        //   y:
        //
        //   width:
        //
        //   height:
        [WrapperlessIcall]
        public float[,,] GetAlphamaps(int x, int y, int width, int height);

查询官方文档,可以明白这个接口返回值得意义:第一维即 X 坐标,第二维 Y 坐标,重要的是第三维:地形贴图的贡献值!
刚开始不是很清楚这个接口的使用,还花了些时间做调试。这个第三维,即地形贴图的贡献,大小与 splatPrototypes 的大小一致,简单来说,就是地形贴图有多少,第三维就有多大。并且值位于 0~1 之间。

既然如此,这就好办了,循环输出一次,就可以发现这一维与 splatPrototypes 一一对应,并以值 0~1 代表相应贴图在地形指定点的贡献。并且,所有的总和同样为 1。

# 实现

现在,所有的信息已经就位。改一下上一遍实现脚印的 Blog 中检测地面的代码,从检测地面的 Hit 中获取地形组件:

s
Terrain terrain = hit.transform.GetComponent<Terrain>();

然后将世界坐标转化为贴图坐标,并于 AlphaMap 中进行采样 (这儿的坐标转化方式因地而异,因为不同大小地形与贴图比例自然需要不同的转换方式,我连调试都是直接在大地形上做的,因为数据太大,还费了不少功夫。现在想来,完全可以新建一个小号的哩):

s
Vector3 inTerrainPos = (hit.point - terrain.GetPosition()) * 0.5f;
var maps = terrain.terrainData.GetAlphamaps((int)inTerrainPos.x, (int)inTerrainPos.z, 1, 1);

最后,循环找出贴图中对当前点贡献最大的那张贴图,并返回名字。这样,就可以根据策划填写的数据库,索引相应的音效、特效等等:

s
int length = maps.GetLength(2);
int index = 0;
float alpha = 0;
for (int i = 0; i < length; i++)
{
     float a = maps[0, 0, i];
     if (a > alpha)
     {
         alpha = a;
         index = i;
     }
     if (alpha == 1) break;
}
return terrain.terrainData.splatPrototypes[index].texture.name;

策划给出的表格如下:

序号场景描述跑(走)动音效跳跃音效落地音效跳跃特效(白天)跳跃落地特效(白天)脚印(白天)跳跃特效(晚上)跳跃落地特效(晚上)脚印(晚上)
Terrain01草地420064200742008200022000820014200032000920015
Terrain02草地420064200742008200022000820014200032000920015
Terrain17草地420064200742008200022000820014200032000920015
Terrain04硬地420004200142002200012000720013200042001020016
Terrain06硬地420004200142002200012000720013200042001020016
Terrain09硬地420004200142002200012000720013200042001020016
Terrain14硬地420004200142002200012000720013200042001020016
Terrain16硬地420004200142002200012000720013200042001020016
Terrain19硬地420004200142002200012000720013200042001020016
Terrain03沙漠地形420034200442005200052001120017200062001220018
Terrain23沙漠地形420034200442005200052001120017200062001220018
Terrain24沙漠地形420034200442005200052001120017200062001220018

# 源码

源码就发主要的方法吧,至于获得了贴图信息,然后查表、处理各项功能,就是些简单重复的功能了,这儿就不多赘述。

s
public static string CheckToGround(Transform transform, GameObject footPrint)
    {
        RaycastHit hit;
        Ray ray = new Ray(transform.position + Vector3.up * 0.5f, Vector3.down);
        if (Physics.Raycast(ray, out hit, 0.7f, m_groundLayer))
        {
            if (footPrint)
            {
                footPrint.transform.position = hit.point;
                footPrint.transform.LookAt(hit.point + hit.normal, transform.forward);
            }
            Terrain terrain = hit.transform.GetComponent<Terrain>();
            if (terrain)
            {
                Vector3 inTerrainPos = (hit.point - terrain.GetPosition()) * 0.5f;
                var maps = terrain.terrainData.GetAlphamaps((int)inTerrainPos.x, (int)inTerrainPos.z, 1, 1);
                int length = maps.GetLength(2);
                int index = 0;
                float alpha = 0;
                for (int i = 0; i < length; i++)
                {
                    float a = maps[0, 0, i];
                    if (a > alpha)
                    {
                        alpha = a;
                        index = i;
                    }
                    if (alpha == 1) break;
                }
                return terrain.terrainData.splatPrototypes[index].texture.name;
            }
        }
        return "";
    }