计算地形网格法线

3

我试图通过尽可能保留细节的同时减少三角形数量来优化我的地形。缩减工作得很好,我将顶点数减少了五分之一,没有太多的视觉损失。但是,在这个新的不对称网格上计算法线有问题。

我有每个顶点的法线,这里是计算法线的代码片段:

private void calcNormal(Vector<Triangle_dt> triangles, Point_dt point) {

    Vec3 normal = new Vec3(0, 0, 0);
    for (Triangle_dt triangle : triangles) {
        Vec3 a = getPos(triangle.p1());
        Vec3 b = getPos(triangle.p2());
        Vec3 c = getPos(triangle.p3());

        Vec3 AB = b.subtract(a);
        Vec3 AC = c.subtract(a);

        normal = normal.add(AB.cross(AC));
    }

    setNormal(point, normal.getUnitVector());
}

这里的三角形是与顶点(点)相连的三角形。我将所有三角形法线加在一起(不规格化,以使最终向量按三角形面积加权),然后最后对结果进行规格化。

我认为计算是正确的,但结果中存在令人讨厌的伪影(使用定向光照明):

Unwanted lines where the vertices are sparse

正如您所看到的,稀疏顶点处存在不必要的线条。这是由于紧密聚集的小点群远离下一组点而导致的(请参见下面的图片)。有什么方法可以防止这种情况发生吗?下面是相同视图的点渲染:

Same view with point rendering


你有原始高度图吗?那么最好从地形法线贴图纹理中对每个片段进行法线采样。 - Yakov Galka
我认为这个可以尝试一下,但恐怕载入会比较耗费空间(这个一维高度图以纯文本格式保存,大小达166MB)。 - Johan
(1) 不要存储在文本中!? (2) 使用每个顶点4字节的纹理可能比您现在使用的每个顶点3个浮点数更便宜。 (3) 通常,如果您有一个巨大的地形模型,那么最好根据相机距离将其加载到瓦片中。 (4) 使用统一细分,以相机距离为函数的分辨率和高度和法线的平铺纹理可能比您现在所做的更容易实现并给您更好的结果。 - Yakov Galka
@ybungalobill(1)那只是为了调试目的,只是为了让你了解高度图的大小。非常感谢您的帮助和建议,最终我按照您所说的做法,从原始高度图中渲染出了一张法线贴图(详见下面我的回答)。 Spektre:这就是我已经做过的事情,在您的示例中,顶点是均匀分布的,它可以很好地工作,但由于三角形大小不规则,它在我的示例中无法使其足够平滑。 - Johan
我很高兴我的简要提示对你有所帮助!为你坚定的工作加2分! - Yakov Galka
1个回答

3

感谢ybungalobill的帮助,我做了以下操作使其正常工作:

  1. 使用以下代码从原始高度图(对称网格)创建了一张普通地图:

从高度图计算法线

// Calculating normals from height map
public void calcNormals() {
    Vec3 up = new Vec3(0, 1, 0);
    float sizeFactor = 1.0f / (8.0f * cellSize);
    normals = new Vec3[rows * cols];

    for (int row = 0; row < rows; row++) {
        for (int col = 0; col < cols; col++) {
            Vec3 normal = up;

            if (col > 0 && row > 0 && col < cols - 1 && row < rows - 1) {
                float nw = getValue(row - 1, col - 1);
                float n = getValue(row - 1, col);
                float ne = getValue(row - 1, col + 1);
                float e = getValue(row, col + 1);
                float se = getValue(row + 1, col + 1);
                float s = getValue(row + 1, col);
                float sw = getValue(row + 1, col - 1);
                float w = getValue(row, col - 1);

                float dydx = ((ne + 2 * e + se) - (nw + 2 * w + sw)) * sizeFactor;
                float dydz = ((sw + 2 * s + se) - (nw + 2 * n + ne)) * sizeFactor;

                normal = new Vec3(-dydx, 1.0f, -dydz).getUnitVector();
            }

            normals[row * cols + col] = normal;
        }
    }
}

从法线图创建图像

public static BufferedImage getNormalMap(Terrain terrain) {
    Vec3[] normals = terrain.getNormals();
    float[] pixels = new float[normals.length * 3];

    for (int i = 0; i < normals.length; i++) {
        Vec3 normal = normals[i];
        float x = (1.0f + normal.x) * 0.5f;
        float y = (1.0f + normal.y) * 0.5f;
        float z = (1.0f + normal.z) * 0.5f;
        pixels[i * 3] = x * MAX;
        pixels[i * 3 + 1] = y * MAX;
        pixels[i * 3 + 2] = z * MAX;
    }

    BufferedImage img = new BufferedImage(cols, rows, BufferedImage.TYPE_INT_RGB);
    WritableRaster imgRaster = img.getRaster();
    imgRaster.setPixels(0, 0, cols, rows, pixels);
    return img;
}

应用图像到片元着色器中,并使用顶点着色器中的顶点位置计算纹理坐标:
  1. 在片元着色器中应用图像并使用顶点着色器中的顶点位置计算纹理坐标:

片元着色器的一部分:

void main() {
    vec3 newNormal = texture(normalMap, vec2(worldPos0.x / maxX, worldPos0.z / maxZ)).xyz;
    newNormal = (2.0 * newNormal) - 1.0;
    outputColor = calcColor(normalize(newNormal));
}

结果如下:

以下是结果:

输入图像描述

使用点渲染的相同视图:

输入图像描述

换句话说:少量顶点,但地形细节高。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接