文章

Unity中实现Hex Tiling

1 Motivation

传统的方形纹理平铺通常会带来很明显的纹理重复的视觉问题,尤其是在地形渲染的情况下,如下图所示:

通过Hex Tiling的手段,则可以在很大程度上减弱该问题,如下图所示:

传统的纹理平铺基于方形网格),而 Hex Tiling 使用六边形网格。在六边形的边缘区域,通过计算像素到相邻六边形中心的距离,动态混合多个纹理变体,实现平滑过渡

此外,使用哈希函数或随机噪声为每个六边形单元生成唯一的种子值决定其纹理变体、旋转角度或缩放比例,进一步隐藏重复的纹理特征。


2 实现细节

2.1 Hash Noise Function

这个函数的作用是,给定一个Vector2,返回一个随机的范围在$[0,1]$之间Vector3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float3 Hash23(float2 input)
{
    float3 xyx = float3(input.x, input.y, input.x);
    float3 yxx = float3(input.y, input.x, input.x);
    float3 xyy = float3(input.x, input.y, input.y);

    float x = dot(xyx, float3(127.1, 311.7, 74.7));
    float y = dot(yxx, float3(269.5, 183.3, 246.1));
    float z = dot(xyy, float3(113.5, 271.9, 124.6));

    float3 hash = frac(sin(float3(x, y, z)) * 43758.5453);
    
    return hash;
}

2.2 UV Transform

通过这个函数,我们可以控制UV的旋转、缩放、平移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float2 UVTransform(float2 uv, float rotation, float scale, float2 offset, float2 pivot = float2(0.5, 0.5))
{
    uv -= pivot;
    // rotation
    // --------
    float rad = radians(rotation);
    float2 newUV;
    newUV.x = uv.x * cos(rad) - uv.y * sin(rad);
    newUV.y = uv.x * sin(rad) + uv.y * cos(rad);
    // scaling
    // -------
    newUV *= scale;
    // translation
    // -----------
    newUV += pivot;
    newUV += offset;
    return newUV;
}

2.3 Random UV Transform

我们已经有了UV变换的函数,并且可以通过Hash23来获取“随机”的变换值,那么我们就可以构建一个用于产生UV随机变换的函数:

1
2
3
4
5
6
7
8
9
10
float2 RandomUVTransform(float2 uv, float rotationMin, float rotationMax, float scaleMin, float scaleMax, float2 randomSeed)
{
    float3 hash = Hash23(randomSeed);
    
    float rotation = lerp(rotationMin, rotationMax, frac(hash.z * 16.0));
    float scale = lerp(scaleMin, scaleMax, hash.z);
    float2 offset = hash.xy;

    return UVTransform(uv, rotation, scale, offset);
}

2.4 Hex Grid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
HexGridResults HexGrid(float2 uv, float gridSize, float gridFocus)
{
    HexGridResults results;
    
    // step 1: transform the UV coordinates
    // ------------------------------------
    const float magicNumber = 1.732 * 0.5;
    float uvX = uv.x - uv.y * (0.5 / magicNumber);
    float uvY = uv.y * (1.0 / magicNumber);
    uv = float2(uvX, uvY) / gridSize;

    // step 2: generate a rgb square-checkerboard pattern
    // --------------------------------------------------
    float2 flooredUV = floor(uv);
    float step = flooredUV.x - flooredUV.y;
    float3 rgbCheckerboard = float3(step, step, step);
    rgbCheckerboard += float3(0.0, 1.0, 2.0);
    rgbCheckerboard *= 1.0 / 3.0;
    rgbCheckerboard += float3(1.66667, 1.66667, 1.66667);
    rgbCheckerboard = frac(rgbCheckerboard);
    rgbCheckerboard = round(rgbCheckerboard);

    // step 3: generate a diagonal-checkerboard pattern
    // ------------------------------------------------
    float3 diagonalCheckerboard = 0;
    float2 fracUV = frac(uv);
    step = fracUV.x + fracUV.y- 1.0;
    diagonalCheckerboard.x = abs(step);
    float2 swizzledFracUV = float2(fracUV.y, fracUV.x);
    swizzledFracUV = float2(1.0, 1.0) - swizzledFracUV;
    if (step > 0.0)
        diagonalCheckerboard.yz = swizzledFracUV;
    else
        diagonalCheckerboard.yz = fracUV;

    // step 4: generate a hexagonal pattern
    // ------------------------------------
    float3 R = float3(rgbCheckerboard.b, rgbCheckerboard.r, rgbCheckerboard.g);
    float3 G = float3(rgbCheckerboard.g, rgbCheckerboard.b, rgbCheckerboard.r);
    float3 B = rgbCheckerboard;
    float hexR = dot(R, diagonalCheckerboard);
    float hexG = dot(G, diagonalCheckerboard);
    float hexB = dot(B, diagonalCheckerboard);
    float3 weights = float3(hexR, hexG, hexB);
    weights = pow(weights, gridFocus);
    weights = weights / dot(weights, float3(1.0, 1.0, 1.0));

    // generate three seeds
    // --------------------
    if (step > 0.0) step = 1;
    else step = 0;
    float3 steppedCheckerboard = rgbCheckerboard * float3(step, step, step);
    float2 seed1 = rgbCheckerboard.xy;
    float2 seed2 = rgbCheckerboard.zx;
    float2 seed3 = rgbCheckerboard.yz;
    seed1 += flooredUV + float2(steppedCheckerboard.z, steppedCheckerboard.z);
    seed2 += flooredUV + float2(steppedCheckerboard.y, steppedCheckerboard.y);
    seed3 += flooredUV + float2(steppedCheckerboard.x, steppedCheckerboard.x);

    results.seed1 = seed1;
    results.seed2 = seed2;
    results.seed3 = seed3;
    results.weights = weights;
    return results;
}

2.5 Sample Textures

1
2
3
4
5
6
7
8
9
10
11
12
HexGridResults results = HexGrid(input.uv, _HexGridSize, _HexGridFocus);

float2 uv1 = RandomUVTransform(input.uv, _RotationMin, _RotationMax, _ScaleMin, _ScaleMax, results.seed1);
float2 uv2 = RandomUVTransform(input.uv, _RotationMin, _RotationMax, _ScaleMin, _ScaleMax, results.seed2);
float2 uv3 = RandomUVTransform(input.uv, _RotationMin, _RotationMax, _ScaleMin, _ScaleMax, results.seed3);

float4 sample1 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv1);
float4 sample2 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv2);
float4 sample3 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv3);

float4 color = sample1 * results.weights.x + sample2 * results.weights.y + sample3 * results.weights.z;
return color;

3 More

3.1 Normal Maps

3.2 Work with Triplanar Mapping

本文由作者按照 CC BY 4.0 进行授权