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
This commit is contained in:
Joo200 2023-10-12 13:57:56 +02:00 committed by GitHub
parent f9d5d3b28b
commit b7ed72b0fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 3 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,61 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -0,0 +1,74 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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.
*
* <p>The kernel should not change the height because everything is flat!</p>
*
* @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));
}
}