Snow heightmap!

This commit is contained in:
Jesse Boyd 2017-03-14 11:23:50 +11:00
parent 1b71bcd4a1
commit eedc3f4069
No known key found for this signature in database
GPG Key ID: 59F1DE6293AF6E1F
8 changed files with 361 additions and 35 deletions

View File

@ -99,6 +99,7 @@ import com.sk89q.worldedit.function.visitor.RecursiveVisitor;
import com.sk89q.worldedit.function.visitor.RegionVisitor;
import com.sk89q.worldedit.history.change.EntityCreate;
import com.sk89q.worldedit.history.change.EntityRemove;
import com.sk89q.worldedit.math.convolution.HeightMap;
import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation;
import com.sk89q.worldedit.math.transform.AffineTransform;
import com.sk89q.worldedit.regions.CuboidRegion;
@ -432,6 +433,7 @@ public class Fawe {
ExtentEntityCopy.inject(); // Async entity create fix
// Transforms
FlattenedClipboardTransform.inject(); // public access
HeightMap.inject(); // Optimizations + Features
// Entity create/remove
EntityCreate.inject(); // Optimizations
EntityRemove.inject(); // Optimizations

View File

@ -13,8 +13,8 @@ import java.io.InputStream;
public class FlattenBrush extends HeightBrush {
public FlattenBrush(InputStream stream, int rotation, double yscale, Clipboard clipboard, ScalableHeightMap.Shape shape) {
super(stream, rotation, yscale, clipboard, shape);
public FlattenBrush(InputStream stream, int rotation, double yscale, boolean layers, Clipboard clipboard, ScalableHeightMap.Shape shape) {
super(stream, rotation, yscale, layers, clipboard, shape);
}
@Override
@ -26,6 +26,6 @@ public class FlattenBrush extends HeightBrush {
}
HeightMap map = getHeightMap();
map.setSize(size);
map.perform(editSession, mask, position, size, rotation, yscale, true, true);
map.perform(editSession, mask, position, size, rotation, yscale, true, true, layers);
}
}

View File

@ -23,14 +23,16 @@ public class HeightBrush implements Brush {
private boolean randomRotate;
public final int rotation;
public final double yscale;
public final boolean layers;
public HeightBrush(InputStream stream, int rotation, double yscale, Clipboard clipboard) {
this(stream, rotation, yscale, clipboard, ScalableHeightMap.Shape.CONE);
public HeightBrush(InputStream stream, int rotation, double yscale, boolean layers, Clipboard clipboard) {
this(stream, rotation, yscale, layers, clipboard, ScalableHeightMap.Shape.CONE);
}
public HeightBrush(InputStream stream, int rotation, double yscale, Clipboard clipboard, ScalableHeightMap.Shape shape) {
public HeightBrush(InputStream stream, int rotation, double yscale, boolean layers, Clipboard clipboard, ScalableHeightMap.Shape shape) {
this.rotation = (rotation / 90) % 4;
this.yscale = yscale;
this.layers = layers;
if (stream != null) {
try {
heightMap = ScalableHeightMap.fromPNG(stream);
@ -68,6 +70,6 @@ public class HeightBrush implements Brush {
}
HeightMap map = getHeightMap();
map.setSize(size);
map.perform(editSession, mask, position, size, rotation, yscale, true, false);
map.perform(editSession, mask, position, size, rotation, yscale, true, false, layers);
}
}

View File

@ -24,7 +24,7 @@ public class StencilBrush extends HeightBrush {
private final boolean onlyWhite;
public StencilBrush(InputStream stream, int rotation, double yscale, boolean onlyWhite, Clipboard clipboard) {
super(stream, rotation, yscale, clipboard);
super(stream, rotation, yscale, false, clipboard);
this.onlyWhite = onlyWhite;
}

View File

@ -5,6 +5,7 @@ import com.boydti.fawe.util.MainUtil;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.WorldVector;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.internal.LocalWorldAdapter;
@ -19,12 +20,12 @@ public interface HeightMap {
public void setSize(int size);
default void perform(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) throws MaxChangedBlocksException {
int[] data = generateHeightData(session, mask, pos, size, rotationMode, yscale, smooth, towards);
applyHeightMapData(data, session, mask, pos, size, rotationMode, yscale, smooth, towards);
default void perform(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards, boolean layers) throws MaxChangedBlocksException {
int[][] data = generateHeightData(session, mask, pos, size, rotationMode, yscale, smooth, towards, layers);
applyHeightMapData(data, session, mask, pos, size, rotationMode, yscale, smooth, towards, layers);
}
default void applyHeightMapData(int[] data, EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) throws MaxChangedBlocksException {
default void applyHeightMapData(int[][] data, EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards, boolean layers) throws MaxChangedBlocksException {
Vector top = session.getMaximumPoint();
int maxY = top.getBlockY();
int diameter = 2 * size + 1;
@ -32,19 +33,29 @@ public interface HeightMap {
WorldVector min = new WorldVector(LocalWorldAdapter.adapt(session.getWorld()), pos.subtract(size, maxY, size));
Vector max = pos.add(size, maxY, size);
Region region = new CuboidRegion(session.getWorld(), min, max);
com.sk89q.worldedit.math.convolution.HeightMap heightMap = new com.sk89q.worldedit.math.convolution.HeightMap(session, region, false);
com.sk89q.worldedit.math.convolution.HeightMap heightMap = new com.sk89q.worldedit.math.convolution.HeightMap(session, region, data[0]);
if (smooth) {
try {
HeightMapFilter filter = (HeightMapFilter) HeightMapFilter.class.getConstructors()[0].newInstance(GaussianKernel.class.getConstructors()[0].newInstance(5, 1));
data = filter.filter(data, diameter, diameter);
data[1] = filter.filter(data[1], diameter, diameter);
} catch (Throwable e) {
MainUtil.handleError(e);
}
}
heightMap.apply(data);
try {
if (layers) {
heightMap.applyLayers(data[1]);
} else {
heightMap.apply(data[1]);
}
} catch (MaxChangedBlocksException e) {
throw e;
} catch (WorldEditException e2) {
throw new RuntimeException(e2);
}
}
default int[] generateHeightData(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) {
default int[][] generateHeightData(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards, final boolean layers) {
Vector top = session.getMaximumPoint();
int maxY = top.getBlockY();
int diameter = 2 * size + 1;
@ -53,7 +64,12 @@ public interface HeightMap {
int centerY = pos.getBlockY();
int endY = pos.getBlockY() + size;
int startY = pos.getBlockY() - size;
int[] newData = new int[diameter * diameter];
int[] oldData = new int[diameter * diameter];
int[] newData = new int[oldData.length];
if (layers) { // Pixel accuracy
centerY <<= 3;
maxY <<= 3;
}
Vector mutablePos = new Vector(0, 0, 0);
if (towards) {
double sizePow = Math.pow(size, yscale);
@ -79,7 +95,13 @@ public interface HeightMap {
raise = getHeight(-z, -x);
break;
}
int height = session.getNearestSurfaceTerrainBlock(xx, zz, pos.getBlockY(), 0, 255);
int height;
if (layers) {
height = session.getNearestSurfaceLayer(xx, zz, pos.getBlockY(), 0, maxY);
} else {
height = session.getNearestSurfaceTerrainBlock(xx, zz, pos.getBlockY(), 0, maxY);
}
oldData[index] = height;
if (height == 0) {
newData[index] = centerY;
continue;
@ -115,18 +137,24 @@ public interface HeightMap {
raise = getHeight(-z, -x);
break;
}
int height = session.getNearestSurfaceTerrainBlock(xx, zz, pos.getBlockY(), 0, maxY);
int height;
if (layers) {
height = session.getNearestSurfaceLayer(xx, zz, pos.getBlockY(), 0, maxY);
} else {
height = session.getNearestSurfaceTerrainBlock(xx, zz, pos.getBlockY(), 0, 255);
}
oldData[index] = height;
if (height == 0) {
newData[index] = centerY;
continue;
}
raise = (yscale * raise);
int random = PseudoRandom.random.random(maxY + 1) < (int) ((raise - (int) raise) * (maxY + 1)) ? 1 : 0;
int random = PseudoRandom.random.random(256) < (int) ((raise - (int) raise) * (256)) ? 1 : 0;
int newHeight = height + (int) raise + random;
newData[index] = newHeight;
}
}
}
return newData;
return new int[][] {oldData, newData};
}
}

View File

@ -981,6 +981,52 @@ public class EditSession extends AbstractWorld implements HasFaweQueue, Lighting
return this.getHighestTerrainBlock(x, z, minY, maxY, false);
}
public int getNearestSurfaceLayer(int x, int z, int y, int minY, int maxY) {
int clearanceAbove = maxY - y;
int clearanceBelow = y - minY;
int clearance = Math.min(clearanceAbove, clearanceBelow);
BaseBlock block = getBlock(x, y, z);
boolean state = FaweCache.isLiquidOrGas(block.getId());
int data1 = block.getData();
int data2 = block.getData();
int offset = state ? 0 : 1;
for (int d = 0; d <= clearance; d++) {
int y1 = y + d;
block = getLazyBlock(x, y1, z);
if (FaweCache.isLiquidOrGas(block.getId()) != state) {
return ((y1 - offset) << 3) - (7 - (state ? block.getData() : data1));
}
data1 = block.getData();
int y2 = y - d;
block = getLazyBlock(x, y2, z);
if (FaweCache.isLiquidOrGas(block.getId()) != state) {
return ((y2 + offset) << 3) - (7 - (state ? block.getData() : data2));
}
data2 = block.getData();
}
if (clearanceAbove != clearanceBelow) {
if (clearanceAbove < clearanceBelow) {
for (int layer = y - clearance - 1; layer >= minY; layer--) {
block = getLazyBlock(x, layer, z);
if (FaweCache.isLiquidOrGas(block.getId()) != state) {
return ((layer + offset) << 3) - (7 - (state ? block.getData() : data1));
}
data1 = block.getData();
}
} else {
for (int layer = y + clearance + 1; layer <= maxY; layer++) {
block = getLazyBlock(x, layer, z);
if (FaweCache.isLiquidOrGas(block.getId()) != state) {
return ((layer - offset) << 3) - (7 - (state ? block.getData() : data2));
}
data2 = block.getData();
}
}
}
return maxY << 4;
}
public int getNearestSurfaceTerrainBlock(int x, int z, int y, int minY, int maxY) {
int clearanceAbove = maxY - y;
int clearanceBelow = y - minY;

View File

@ -714,13 +714,15 @@ public class BrushCommands {
flags = "h",
desc = "Height brush",
help =
"This brush raises and lowers land.\n",
"This brush raises and lowers land.\n" +
"The -r flag enables random off-axis rotation\n" +
"The -l flag will work on snow layers",
min = 1,
max = 4
)
@CommandPermissions("worldedit.brush.height")
public void heightBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, false, randomRotate, ScalableHeightMap.Shape.CONE);
public void heightBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate, @Switch('l') boolean layers) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, false, randomRotate, layers, ScalableHeightMap.Shape.CONE);
}
@Command(
@ -729,28 +731,32 @@ public class BrushCommands {
flags = "h",
desc = "Cliff brush",
help =
"This brush flattens terrain and creates cliffs.\n",
"This brush flattens terrain and creates cliffs.\n" +
"The -r flag enables random off-axis rotation\n" +
"The -l flag will work on snow layers",
min = 1,
max = 4
)
@CommandPermissions("worldedit.brush.height")
public void cliffBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, true, randomRotate, ScalableHeightMap.Shape.CYLINDER);
public void cliffBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate, @Switch('l') boolean layers) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, true, randomRotate, layers, ScalableHeightMap.Shape.CYLINDER);
}
@Command(
aliases = { "flatten", "flatmap", "flat" },
usage = "[radius] [file|#clipboard|null] [rotation] [yscale]",
flags = "h",
desc = "Flatten brush",
desc = "Flatten brush makes terrain flatter\n" +
"The -r flag enables random off-axis rotation\n" +
"The -l flag will work on snow layers",
help =
"This brush raises and lowers land towards the clicked point\n",
min = 1,
max = 4
)
@CommandPermissions("worldedit.brush.height")
public void flattenBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, true, randomRotate, ScalableHeightMap.Shape.CONE);
public void flattenBrush(Player player, LocalSession session, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('r') boolean randomRotate, @Switch('l') boolean layers) throws WorldEditException {
terrainBrush(player, session, radius, filename, rotation, yscale, true, randomRotate, layers, ScalableHeightMap.Shape.CONE);
}
private InputStream getHeightmapStream(String filename) {
@ -784,7 +790,7 @@ public class BrushCommands {
return null;
}
private void terrainBrush(Player player, LocalSession session, double radius, String filename, int rotation, double yscale, boolean flat, boolean randomRotate, ScalableHeightMap.Shape shape) throws WorldEditException {
private void terrainBrush(Player player, LocalSession session, double radius, String filename, int rotation, double yscale, boolean flat, boolean randomRotate, boolean layers, ScalableHeightMap.Shape shape) throws WorldEditException {
worldEdit.checkMaxBrushRadius(radius);
InputStream stream = getHeightmapStream(filename);
BrushTool tool = session.getBrushTool(player);
@ -792,15 +798,15 @@ public class BrushCommands {
HeightBrush brush;
if (flat) {
try {
brush = new FlattenBrush(stream, rotation, yscale, filename.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null, shape);
brush = new FlattenBrush(stream, rotation, yscale, layers, filename.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null, shape);
} catch (EmptyClipboardException ignore) {
brush = new FlattenBrush(stream, rotation, yscale, null, shape);
brush = new FlattenBrush(stream, rotation, yscale, layers, null, shape);
}
} else {
try {
brush = new HeightBrush(stream, rotation, yscale, filename.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null);
brush = new HeightBrush(stream, rotation, yscale, layers, filename.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null);
} catch (EmptyClipboardException ignore) {
brush = new HeightBrush(stream, rotation, yscale, null);
brush = new HeightBrush(stream, rotation, yscale, layers, null);
}
}
tool.setBrush(brush, "worldedit.brush.height", player);

View File

@ -0,0 +1,242 @@
package com.sk89q.worldedit.math.convolution;
import com.boydti.fawe.FaweCache;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.blocks.BaseBlock;
import com.sk89q.worldedit.regions.Region;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Allows applications of Kernels onto the region's height map.
*
* <p>Currently only used for smoothing (with a GaussianKernel)</p>.
*/
public class HeightMap {
private int[] data;
private int width;
private int height;
private Region region;
private EditSession session;
/**
* Constructs the HeightMap
*
* @param session an edit session
* @param region the region
*/
public HeightMap(EditSession session, Region region) {
this(session, region, false);
}
public HeightMap(EditSession session, Region region, boolean naturalOnly) {
checkNotNull(session);
checkNotNull(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, naturalOnly);
}
}
}
public HeightMap(EditSession session, Region region, int[] data) {
this.session = session;
this.region = region;
this.width = region.getWidth();
this.height = region.getLength();
this.data = data;
}
/**
* Apply the filter 'iterations' amount times.
*
* @param filter the filter
* @param iterations the number of iterations
* @return number of blocks affected
* @throws MaxChangedBlocksException
*/
public int applyFilter(HeightMapFilter filter, int iterations) throws WorldEditException {
checkNotNull(filter);
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);
}
public int applyLayers(int[] data) throws WorldEditException {
checkNotNull(data);
Vector minY = region.getMinimumPoint();
int originX = minY.getBlockX();
int originY = minY.getBlockY();
int originZ = minY.getBlockZ();
int maxY = region.getMaximumPoint().getBlockY();
BaseBlock fillerAir = EditSession.nullBlock;
int blocksChanged = 0;
// Apply heightmap
int maxY4 = maxY << 4;
int index = 0;
for (int z = 0; z < height; ++z) {
int zr = z + originZ;
for (int x = 0; x < width; ++x) {
int curHeight = this.data[index];
int newHeight = Math.min(maxY4, data[index++]);
int curBlock = (curHeight) >> 3;
int newBlock = (newHeight + 7) >> 3;
int xr = x + originX;
// 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)
BaseBlock existing = session.getBlock(xr, curBlock, zr);
// Skip water/lava
if (!FaweCache.isLiquidOrGas(existing.getId())) {
// Grow -- start from 1 below top replacing airblocks
for (int y = newBlock - 1 - originY; y >= curBlock; --y) {
int copyFrom = (int) (y * scale);
session.setBlock(xr, originY + y, zr, session.getBlock(xr, originY + copyFrom, zr));
++blocksChanged;
}
int setData = newHeight & 7;
if (setData != 0) {
existing = FaweCache.getBlock(existing.getId(), setData - 1);
session.setBlock(xr, newBlock, zr, existing);
++blocksChanged;
} else {
existing = FaweCache.getBlock(existing.getId(), 7);
session.setBlock(xr, newBlock, zr, existing);
++blocksChanged;
}
}
} else if (curHeight > newHeight) {
// Fill rest with air
for (int y = newBlock + 1; y <= ((curHeight + 7) >> 3); ++y) {
session.setBlock(xr, y, zr, fillerAir);
++blocksChanged;
}
// Set the top block of the column to be the same type
// (this could otherwise go wrong with rounding)
int setData = newHeight & 7;
BaseBlock existing = session.getBlock(xr, curBlock, zr);
if (setData != 0) {
existing = FaweCache.getBlock(existing.getId(), setData - 1);
session.setBlock(xr, newBlock, zr, existing);
} else {
existing = FaweCache.getBlock(existing.getId(), 7);
session.setBlock(xr, newBlock, zr, existing);
}
++blocksChanged;
}
}
}
return blocksChanged;
}
public int apply(int[] data) throws WorldEditException {
checkNotNull(data);
Vector minY = region.getMinimumPoint();
int originX = minY.getBlockX();
int originY = minY.getBlockY();
int originZ = minY.getBlockZ();
int maxY = region.getMaximumPoint().getBlockY();
BaseBlock fillerAir = EditSession.nullBlock;
int blocksChanged = 0;
// Apply heightmap
int index = 0;
for (int z = 0; z < height; ++z) {
int zr = z + originZ;
for (int x = 0; x < width; ++x) {
int curHeight = this.data[index];
int newHeight = Math.min(maxY, data[index++]);
int xr = x + originX;
// 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)
BaseBlock existing = session.getBlock(xr, curHeight, zr);
// Skip water/lava
if (!FaweCache.isLiquidOrGas(existing.getId())) {
session.setBlock(xr, newHeight, zr, existing);
++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(xr, originY + y, zr, session.getBlock(xr, originY + copyFrom, zr));
++blocksChanged;
}
}
} else if (curHeight > newHeight) {
// Shrink -- start from bottom
for (int y = 0; y < newHeight - originY; ++y) {
int copyFrom = (int) (y * scale);
session.setBlock(xr, originY + y, zr, session.getBlock(xr, originY + copyFrom, zr));
++blocksChanged;
}
// Set the top block of the column to be the same type
// (this could otherwise go wrong with rounding)
session.setBlock(xr, newHeight, zr, session.getBlock(xr, curHeight, zr));
++blocksChanged;
// Fill rest with air
for (int y = newHeight + 1; y <= curHeight; ++y) {
session.setBlock(xr, y, zr, fillerAir);
++blocksChanged;
}
}
}
}
return blocksChanged;
}
public static Class<?> inject() {
return HeightMap.class;
}
}