You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Perlin Noise生成异常:C#实现中邻接单元格不匹配问题排查

2D Perlin Noise实现中单元格衔接异常的问题

Perlin Noise输出结果

每个单元格单独显示正常,但相邻单元格无法平滑衔接(少数情况下能匹配)。

以下是原实现的代码:

原Perlin结构体代码

public struct Perlin
{
    public struct _2D
    {
        public static float Simple(float x, float y, int seed)
        {
            Vector2 pos = new Vector2(x % 1, y % 1); // Position in the unit square

            Vector2[] points = new Vector2[4];
            points[0] = new Vector2(Mathf.Floor(x), Mathf.Floor(y)); // Corner positions
            points[1] = new Vector2(Mathf.Floor(x), Mathf.Ceil(y)); // Corner positions
            points[2] = new Vector2(Mathf.Ceil(x), Mathf.Ceil(y)); // Corner positions
            points[3] = new Vector2(Mathf.Ceil(x), Mathf.Floor(y)); // Corner positions

            Vector2[] vectorTable = new Vector2[] { new Vector2(-1, -1), new Vector2(1, -1), new Vector2(1, 1), new Vector2(-1, 1) , new Vector2(Mathf.Sqrt(2), 0), new Vector2(0, Mathf.Sqrt(2)), new Vector2(-Mathf.Sqrt(2), 0), new Vector2(0, -Mathf.Sqrt(2)) };

            Vector2[] gradients = new Vector2[4];
            for (int g = 0; g < gradients.Length; g++)
            {
                gradients[g] = vectorTable[Mathf.RoundToInt(Random(new Vector2(points[g].x + seed, points[g].y + seed)) * (vectorTable.Length-1))]; // Gradient vector
            }

            Vector2[] directions = new Vector2[4];
            for (int d = 0; d < directions.Length; d++)
            {
                directions[d] = (new Vector2(x - points[d].x, y - points[d].y) - pos).normalized; // Direction vector from the x and y position to the corner
            }

            float[] dots = new float[4];
            for (int i = 0; i < points.Length; i++)
            {
                dots[i] = (Vector2.Dot(directions[i], gradients[i])); // Dot products
            }

            float lerp1 = (Mathf.Lerp(dots[0], dots[1], Fade(pos.y)));
            float lerp2 = (Mathf.Lerp(dots[2], dots[3], Fade(pos.y)));
            float lerp3 = (Mathf.Lerp(lerp1, lerp2, Fade(pos.x)));

            return (lerp3 + 1) / 2; // Remapping the output from (-1 to 1) to (0 to 1)

            float Fade(float t)
            {
                return t * t * t * (t * (t * 6 - 15) + 10);
            }
        }
    }
}

原Random函数代码

static float Random(Vector2 seed)
{
    Vector2 v1 = new Vector2(3.1251f, 17.8737f);
    float f1 = 43758.545312f;
    return fract(Mathf.Sin(Vector2.Dot(seed, v1) * f1)) ;

    float fract(float x)
    {
        return Mathf.Abs(x % 1.0f);
    }
}

问题根源与修复方案

1. 方向向量计算错误

当前代码中错误地对偏移向量做了减法和归一化操作:

directions[d] = (new Vector2(x - points[d].x, y - points[d].y) - pos).normalized;

new Vector2(x - points[d].x, y - points[d].y)本身就是当前点到网格顶点的偏移向量,和pos是同一个值,减法后会得到0向量,完全破坏了方向逻辑。同时Perlin Noise不需要对方向向量归一化,这会打乱梯度的分布特性。

修复后代码:

directions[d] = new Vector2(x - points[d].x, y - points[d].y);

2. 梯度索引的精度问题

使用Mathf.RoundToInt可能因浮点数精度问题导致索引越界,且(vectorTable.Length-1)的限制会浪费最后一个梯度向量的使用概率。改用Mathf.FloorToInt并直接乘以向量表长度,能保证索引稳定在合法范围内。

修复后代码:

int index = Mathf.FloorToInt(Random(new Vector2(points[g].x + seed, points[g].y + seed)) * vectorTable.Length);
gradients[g] = vectorTable[index];

3. 插值顺序逻辑错误

原代码的横向插值顺序完全颠倒,导致相邻单元格的衔接逻辑混乱。正确的顺序应该是先对上下边的横向顶点分别插值,再做纵向插值。

修复后插值代码:

float bottomLerp = Mathf.Lerp(dots[0], dots[3], Fade(pos.x));
float topLerp = Mathf.Lerp(dots[1], dots[2], Fade(pos.x));
float finalLerp = Mathf.Lerp(bottomLerp, topLerp, Fade(pos.y));

修复后的完整代码

Perlin结构体

public struct Perlin
{
    public struct _2D
    {
        public static float Simple(float x, float y, int seed)
        {
            Vector2 pos = new Vector2(x % 1, y % 1); // 单元格内相对位置

            Vector2[] points = new Vector2[4];
            points[0] = new Vector2(Mathf.Floor(x), Mathf.Floor(y)); // 左下角顶点
            points[1] = new Vector2(Mathf.Floor(x), Mathf.Ceil(y));  // 左上角顶点
            points[2] = new Vector2(Mathf.Ceil(x), Mathf.Ceil(y));   // 右上角顶点
            points[3] = new Vector2(Mathf.Ceil(x), Mathf.Floor(y));  // 右下角顶点

            Vector2[] vectorTable = new Vector2[] 
            { 
                new Vector2(-1, -1), new Vector2(1, -1), 
                new Vector2(1, 1), new Vector2(-1, 1), 
                new Vector2(Mathf.Sqrt(2), 0), new Vector2(0, Mathf.Sqrt(2)), 
                new Vector2(-Mathf.Sqrt(2), 0), new Vector2(0, -Mathf.Sqrt(2)) 
            };

            Vector2[] gradients = new Vector2[4];
            for (int g = 0; g < gradients.Length; g++)
            {
                int index = Mathf.FloorToInt(Random(new Vector2(points[g].x + seed, points[g].y + seed)) * vectorTable.Length);
                gradients[g] = vectorTable[index];
            }

            Vector2[] directions = new Vector2[4];
            for (int d = 0; d < directions.Length; d++)
            {
                directions[d] = new Vector2(x - points[d].x, y - points[d].y);
            }

            float[] dots = new float[4];
            for (int i = 0; i < points.Length; i++)
            {
                dots[i] = Vector2.Dot(directions[i], gradients[i]);
            }

            float bottomLerp = Mathf.Lerp(dots[0], dots[3], Fade(pos.x));
            float topLerp = Mathf.Lerp(dots[1], dots[2], Fade(pos.x));
            float finalLerp = Mathf.Lerp(bottomLerp, topLerp, Fade(pos.y));

            return (finalLerp + 1) / 2;

            float Fade(float t)
            {
                return t * t * t * (t * (t * 6 - 15) + 10);
            }
        }
    }
}

Random函数

static float Random(Vector2 seed)
{
    Vector2 v1 = new Vector2(3.1251f, 17.8737f);
    float f1 = 43758.545312f;
    return fract(Mathf.Sin(Vector2.Dot(seed, v1) * f1));

    float fract(float x)
    {
        return Mathf.Abs(x % 1.0f);
    }
}

内容的提问来源于stack exchange,提问作者JacksStuff

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 13:21:28