diff --git a/src/EditSession.java b/src/EditSession.java old mode 100644 new mode 100755 index 79b05ca31..47e08c691 --- a/src/EditSession.java +++ b/src/EditSession.java @@ -1856,4 +1856,44 @@ public class EditSession { return distribution; } + + /** + * Returns the highest solid 'terrain' block which can occur naturally. + * Looks at: 1, 2, 3, 7, 12, 13, 14, 15, 16, 56, 73, 74, 87, 88, 89 + * + * @param x + * @param z + * @param minY minimal height + * @param maxY maximal height + * @return height of highest block found or 'minY' + */ + + public int getHighestTerrainBlock( int x , int z, int minY, int maxY) { + for (int y = maxY; y >= minY; y--) { + Vector pt = new Vector(x, y, z); + int id = getBlock(pt).getID(); + + if (id == 1 // stone + || id == 2 // grass + || id == 3 // dirt + || id == 7 // bedrock + || id == 12 // sand + || id == 13 // gravel + // hell + || id == 87 // netherstone + || id == 88 // slowsand + || id == 89 // lightstone + // ores + || id == 14 // coal ore + || id == 15 // iron ore + || id == 16 // gold ore + || id == 56 // diamond ore + || id == 73 // redstone ore + || id == 74 // redstone ore (active) + ) { + return y; + } + } + return minY; + } } diff --git a/src/HeightMap.java b/src/HeightMap.java new file mode 100755 index 000000000..c9739c500 --- /dev/null +++ b/src/HeightMap.java @@ -0,0 +1,161 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q + * + * 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 . +*/ + +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.filters.HeightMapFilter; +import com.sk89q.worldedit.regions.Region; + +/** + * Allows applications of Kernels onto the region's heightmap. + * Currently only used for smoothing (with a GaussianKernel). + * + * @author Grum + */ + +public class HeightMap { + private int[] data; + private int width; + private int height; + + private Region region; + private EditSession session; + + /** + * Constructs the HeightMap + * + * @param session + * @param region + */ + + public HeightMap(EditSession session, Region region) { + this.session = session; + this.region = region; + + this.width = region.getWidth(); + this.height = region.getLength(); + + int minX = region.getMinimumPoint().getBlockX(); + int minY = region.getMinimumPoint().getBlockY(); + int minZ = region.getMinimumPoint().getBlockZ(); + int maxY = region.getMaximumPoint().getBlockY(); + + // Store current heightmap data + data = new int[width * height]; + for (int z = 0; z < height; z++) { + for (int x = 0; x < width; x++) { + data[z * width + x] = session.getHighestTerrainBlock(x + minX, z + minZ, minY, maxY); + } + } + } + + /** + * Apply the filter 'iterations' amount times. + * + * @param filter + * @param iterations + * @return number of blocks affected + * @throws MaxChangedBlocksException + */ + + public int applyFilter(HeightMapFilter filter, int iterations) throws MaxChangedBlocksException { + int[] newData = new int[data.length]; + System.arraycopy(data, 0, newData, 0, data.length); + + for (int i = 0; i < iterations; i++) + newData = filter.filter(newData, width, height); + + return apply(newData); + } + + /** + * Apply a raw heightmap to the region + * + * @param data + * @return number of blocks affected + * @throws MaxChangedBlocksException + */ + + public int apply(int[] data) throws MaxChangedBlocksException { + Vector minY = region.getMinimumPoint(); + int originX = minY.getBlockX(); + int originY = minY.getBlockY(); + int originZ = minY.getBlockZ(); + + int maxY = region.getMaximumPoint().getBlockY(); + BaseBlock fillerAir = new BaseBlock(0); + + int blocksChanged = 0; + + // Apply heightmap + for (int z = 0; z < height; z++) { + for (int x = 0; x < width; x++) { + int index = z * width + x; + int curHeight = this.data[index]; + + // Clamp newHeight within the selection area + int newHeight = Math.min(maxY, data[index]); + + // Offset x,z to be 'real' coordinates + int X = x + originX; + int Z = z + originZ; + + // We are keeping the topmost blocks so take that in account for the scale + double scale = (double) (curHeight - originY) / (double) (newHeight - originY); + + // Depending on growing or shrinking we need to start at the bottom or top + if (newHeight > curHeight) { + // Set the top block of the column to be the same type (this might go wrong with rounding) + session.setBlock(new Vector(X, newHeight, Z), session.getBlock(new Vector(X, curHeight, Z))); + blocksChanged++; + + // Grow -- start from 1 below top replacing airblocks + for (int y = newHeight - 1 - originY; y >= 0; y--) { + int copyFrom = (int) (y * scale); + session.setBlock(new Vector(X, originY + y, Z), session.getBlock(new Vector(X, originY + copyFrom, Z))); + blocksChanged++; + } + } else if (curHeight > newHeight) { + // Shrink -- start from bottom + for (int y = 0; y < newHeight - originY; y++) { + int copyFrom = (int) (y * scale); + session.setBlock(new Vector(X, originY + y, Z), session.getBlock(new Vector(X, originY + copyFrom, Z))); + blocksChanged++; + } + + // Set the top block of the column to be the same type + // (this could otherwise go wrong with rounding) + session.setBlock(new Vector(X, newHeight, Z), session.getBlock(new Vector(X, curHeight, Z))); + blocksChanged++; + + // Fill rest with air + for (int y = newHeight + 1; y <= curHeight; y++) { + session.setBlock(new Vector(X, y, Z), fillerAir); + blocksChanged++; + } + } + } + } + + // Drop trees to the floor -- TODO + + return blocksChanged; + } +} diff --git a/src/WorldEditListener.java b/src/WorldEditListener.java old mode 100644 new mode 100755 index b1df1b60c..697e0b15e --- a/src/WorldEditListener.java +++ b/src/WorldEditListener.java @@ -31,6 +31,7 @@ import java.io.*; import com.sk89q.worldedit.*; import com.sk89q.worldedit.blocks.*; import com.sk89q.worldedit.data.*; +import com.sk89q.worldedit.filters.*; import com.sk89q.worldedit.snapshots.*; import com.sk89q.worldedit.regions.*; import com.sk89q.worldedit.patterns.*; @@ -165,6 +166,7 @@ public class WorldEditListener extends PluginListener { commands.put("/butcher", " - Kill nearby mobs"); commands.put("//use", "[SnapshotID] - Use a particular snapshot"); commands.put("//restore", " - Restore a particular snapshot"); + commands.put("//smooth", " - Smooth an area's heightmap"); } /** @@ -937,6 +939,21 @@ public class WorldEditListener extends PluginListener { return true; + // Smooth the heightmap of a region + } else if (split[0].equalsIgnoreCase("//smooth")) { + checkArgs(split, 0, 1, split[0]); + + int iterations = 1; + if (split.length >= 2) + iterations = Integer.parseInt(split[1]); + + HeightMap heightMap = new HeightMap(editSession, session.getRegion()); + HeightMapFilter filter = new HeightMapFilter(new GaussianKernel(5, 1.0)); + int affected = heightMap.applyFilter(filter, iterations); + player.print("Terrain's heightmap has been smoothed. " + affected + " block(s) have been changed."); + + return true; + // Set the outline of a region } else if(split[0].equalsIgnoreCase("//outline")) { checkArgs(split, 1, 1, split[0]); diff --git a/src/com/sk89q/worldedit/filters/GaussianKernel.java b/src/com/sk89q/worldedit/filters/GaussianKernel.java new file mode 100755 index 000000000..4241f8b95 --- /dev/null +++ b/src/com/sk89q/worldedit/filters/GaussianKernel.java @@ -0,0 +1,57 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q + * + * 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.filters; + +import java.awt.image.Kernel; + +/** + * A Gaussian Kernel generator (2D bellcurve) + * + * @author Grum + */ + +public class GaussianKernel extends Kernel { + + /** + * Constructor of the kernel + * + * @param radius the resulting diameter will be radius * 2 + 1 + * @param sigma controls 'flatness' + */ + + public GaussianKernel(int radius, double sigma) { + super(radius * 2 + 1, radius * 2 + 1, createKernel(radius, sigma)); + } + + private static float[] createKernel(int radius, double sigma) { + int diameter = radius * 2 + 1; + float[] data = new float[diameter*diameter]; + + double sigma22 = 2 * sigma * sigma; + double constant = Math.PI * sigma22; + 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); + } + } + + return data; + } +} diff --git a/src/com/sk89q/worldedit/filters/HeightMapFilter.java b/src/com/sk89q/worldedit/filters/HeightMapFilter.java new file mode 100755 index 000000000..124dab288 --- /dev/null +++ b/src/com/sk89q/worldedit/filters/HeightMapFilter.java @@ -0,0 +1,118 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q + * + * 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.filters; + +import java.awt.image.Kernel; + +/** + * Allows applications of Kernels onto the region's heightmap. + * Only used for smoothing (with a GaussianKernel). + * + * @author Grum + */ + +public class HeightMapFilter { + private Kernel kernel; + + /** + * Construct the HeightMapFilter object. + * + * @param kernel + */ + public HeightMapFilter(Kernel kernel) { + this.kernel = kernel; + } + + /** + * Construct the HeightMapFilter object. + * + * @param kernelWidth + * @param kernelHeight + * @param kernelData + */ + public HeightMapFilter(int kernelWidth, int kernelHeight, float[] kernelData) { + this.kernel = new Kernel(kernelWidth, kernelHeight, kernelData); + } + + /** + * @return the kernel + */ + public Kernel getKernel() { + return kernel; + } + + /** + * Set Kernel + * + * @param kernel + */ + public void setKernel(Kernel kernel) { + this.kernel = kernel; + } + + /** + * Filter with a 2D kernel + * + * @param inData + * @param width + * @param height + * @return the modified heightmap + */ + public int[] filter(int[] inData, int width, int height ) { + int index = 0; + float[] matrix = kernel.getKernelData(null); + int[] outData = new int[inData.length]; + + int kh = kernel.getHeight(); + int kw = kernel.getWidth(); + int kox = kernel.getXOrigin(); + int koy = kernel.getYOrigin(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float z = 0; + + for (int ky = 0; ky < kh; ky++) { + int offsetY = y + ky - koy; + // Clamp coordinates inside data + if (offsetY < 0 || offsetY >= height) + offsetY = y; + + offsetY *= width; + + int matrixOffset = ky * kw; + for (int kx = 0; kx < kw; kx++) { + float f = matrix[matrixOffset + kx]; + if (f == 0) continue; + + int offsetX = x + kx - kox; + // Clamp coordinates inside data + if (offsetX < 0 || offsetX >= width) + offsetX = x; + + z += f * inData[offsetY + offsetX]; + } + } + outData[index++] = (int) (z + 0.5); + } + } + return outData; + } +} diff --git a/src/com/sk89q/worldedit/filters/LinearKernel.java b/src/com/sk89q/worldedit/filters/LinearKernel.java new file mode 100755 index 000000000..021514b61 --- /dev/null +++ b/src/com/sk89q/worldedit/filters/LinearKernel.java @@ -0,0 +1,44 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q + * + * 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.filters; + +import java.awt.image.Kernel; + +/** + * A linear Kernel generator (all cells weight the same) + * + * @author Grum + */ + +public class LinearKernel extends Kernel { + + public LinearKernel(int radius) { + super(radius * 2 + 1, radius * 2 + 1, createKernel(radius)); + } + + private static float[] createKernel(int radius) { + int diameter = radius * 2 + 1; + float[] data = new float[diameter*diameter]; + + for (int i = 0; i < data.length; data[i++] = 1.0f/data.length) ; + + return data; + } +}