From b7ed72b0feb25f4c2061243ca51dc49d4054c17f Mon Sep 17 00:00:00 2001 From: Joo200 Date: Thu, 12 Oct 2023 13:57:56 +0200 Subject: [PATCH] Improve SnowSmooth Brush and add utests for GaussianKernel (#2407) * Fix flaw in GaussianKernel layout The sum of the data should be close to one. The GaussianKernel creates the data based on the radius. This would sum up to one if it would be a perfect circle. * Fix snowsmooth brush for multiple iterations --- .../math/convolution/GaussianKernel.java | 10 ++- .../math/convolution/SnowHeightMap.java | 7 +- .../math/convolution/GaussianKernelTest.java | 61 +++++++++++++++ .../math/convolution/HeightMapFilterTest.java | 74 +++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/GaussianKernelTest.java create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/HeightMapFilterTest.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/GaussianKernel.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/GaussianKernel.java index 31dec0cda..7059adf0c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/GaussianKernel.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/GaussianKernel.java @@ -40,12 +40,20 @@ private static float[] createKernel(int radius, double sigma) { double sigma22 = 2 * sigma * sigma; double constant = Math.PI * sigma22; + float sum = 0; for (int y = -radius; y <= radius; ++y) { for (int x = -radius; x <= radius; ++x) { - data[(y + radius) * diameter + x + radius] = (float) (Math.exp(-(x * x + y * y) / sigma22) / constant); + float value = (float) (Math.exp(-(x * x + y * y) / sigma22) / constant); + data[(y + radius) * diameter + x + radius] = value; + sum += value; } } + // GaussianKernel assumes a circle, however we have whole blocks. Normalize the array. + for (int i = 0; i < data.length; i++) { + data[i] = data[i] / sum; + } + return data; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/SnowHeightMap.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/SnowHeightMap.java index bbcbf25aa..b04719a1b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/SnowHeightMap.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/convolution/SnowHeightMap.java @@ -107,8 +107,11 @@ public float[] applyFilter(HeightMapFilter filter, int iterations) { float[] newData = data.clone(); for (int i = 0; i < iterations; ++i) { - // add an offset from 0.0625F to the values (snowlayer half) - newData = filter.filter(newData, width, height, 0.0625F); + newData = filter.filter(newData, width, height, 0); + } + // add an offset from 0.0625F to the values (half snowlayer) + for (int i = 0; i < newData.length; ++i) { + newData[i] = newData[i] + 0.0625F; } return newData; } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/GaussianKernelTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/GaussianKernelTest.java new file mode 100644 index 000000000..ecd91fa18 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/GaussianKernelTest.java @@ -0,0 +1,61 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.math.convolution; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("Gaussian Kernel for HeightMaps") +public class GaussianKernelTest { + private void testGaussian(GaussianKernel kernel) { + float[] data = kernel.getKernelData(null); + float sum = 0; + for (float datum : data) { + assertTrue(datum >= 0); + sum += datum; + } + // The sum has to be 1 + assertEquals(1f, sum, 0.01); + } + + /** + * Test the creation of the gaussian kernel with Sigma 1. + * @param radius the radius to test. + */ + @ParameterizedTest(name = "radius={0}") + @ValueSource(ints = { 1, 2, 5, 10 }) + public void testGaussianKernelSigma1(int radius) { + testGaussian(new GaussianKernel(radius, 1)); + } + + /** + * Test the creation of the gaussian kernel with Sigma 5. + * @param radius the radius to test. + */ + @ParameterizedTest(name = "radius={0}") + @ValueSource(ints = { 1, 2, 5, 10 }) + public void testGaussianKernelSigma5(int radius) { + testGaussian(new GaussianKernel(radius, 5)); + } +} diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/HeightMapFilterTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/HeightMapFilterTest.java new file mode 100644 index 000000000..e6fdeea24 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/math/convolution/HeightMapFilterTest.java @@ -0,0 +1,74 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.math.convolution; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("A heightmap") +public class HeightMapFilterTest { + + /** + * A simple kernel test to validate the kernel on flat world works fine. + * + *

The kernel should not change the height because everything is flat!

+ * + * @param height The height to test + * @param kernel The used kernel + */ + private void testKernelOnFlat(float height, Kernel kernel) { + HeightMapFilter filter = new HeightMapFilter(kernel); + + float[] data = new float[9 * 9]; + Arrays.fill(data, height); + + float[] output = filter.filter(data, 1, 1, 0); + assertEquals(height, output[0], 0.05); + } + + /** + * Test the Gaussian kernel with the HeightMapFilter. + * @param height the height to test, parameterized + */ + @ParameterizedTest + @ValueSource(floats = {-25.0f, -10.0f, 0f, 10f, 25f}) + public void testGaussianHeightMap(float height) { + testKernelOnFlat(height, new GaussianKernel(1, 1)); + + testKernelOnFlat(height, new GaussianKernel(3, 1)); + } + + /** + * Test the linear kernel with the HeightMapFilter. + * @param height the height to test, parameterized + */ + @ParameterizedTest + @ValueSource(floats = {-25.0f, -10.0f, 0f, 10f, 25f}) + public void testLinearHeightMap(float height) { + testKernelOnFlat(height, new LinearKernel(1)); + + testKernelOnFlat(height, new LinearKernel(3)); + } +}