From fe17434d000899a78cf93a0e76c50d225f6390f4 Mon Sep 17 00:00:00 2001 From: Jesse Boyd Date: Fri, 11 May 2018 17:59:32 +1000 Subject: [PATCH] Add image brush --- .../boydti/fawe/object/brush/ImageBrush.java | 147 +++++++----- .../boydti/fawe/object/brush/LayerBrush.java | 54 ++--- .../fawe/object/brush/StencilBrush.java | 65 ++--- .../object/collection/SummedAreaTable.java | 1 - .../object/collection/SummedColorTable.java | 227 ++++++++++++++++++ .../com/boydti/fawe/util/TextureUtil.java | 2 +- .../com/boydti/fawe/util/image/ImageUtil.java | 62 +++++ .../worldedit/command/BrushCommands.java | 42 ++-- 8 files changed, 456 insertions(+), 144 deletions(-) create mode 100644 core/src/main/java/com/boydti/fawe/object/collection/SummedColorTable.java diff --git a/core/src/main/java/com/boydti/fawe/object/brush/ImageBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/ImageBrush.java index 9d16e29d..862442df 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/ImageBrush.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/ImageBrush.java @@ -1,96 +1,129 @@ package com.boydti.fawe.object.brush; -import com.boydti.fawe.object.PseudoRandom; -import com.boydti.fawe.object.brush.heightmap.HeightMap; -import com.boydti.fawe.object.mask.AdjacentAnyMask; +import com.boydti.fawe.object.collection.SummedColorTable; +import com.boydti.fawe.util.TextureUtil; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.Vector; -import com.sk89q.worldedit.WorldEditException; -import com.sk89q.worldedit.entity.Player; -import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.function.RegionFunction; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.command.tool.brush.Brush; +import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.mask.Mask; -import com.sk89q.worldedit.function.mask.Masks; -import com.sk89q.worldedit.function.mask.RegionMask; import com.sk89q.worldedit.function.mask.SolidBlockMask; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.function.visitor.RecursiveVisitor; -import com.sk89q.worldedit.regions.CuboidRegion; -import java.io.InputStream; +import com.sk89q.worldedit.math.transform.AffineTransform; +import com.sk89q.worldedit.util.Location; +import java.awt.image.BufferedImage; +import java.io.IOException; import java.util.Arrays; -public class ImageBrush extends HeightBrush { - private final boolean onlyWhite; - private final Player player; +public class ImageBrush implements Brush { + private final LocalSession session; + private final SummedColorTable table; + private final int width, height; + private final double centerX, centerZ; - public ImageBrush(Player player, InputStream stream, int rotation, double yscale, boolean onlyWhite, Clipboard clipboard) { - super(stream, rotation, yscale, false, true, clipboard); - this.onlyWhite = onlyWhite; - this.player = player; + private final ColorFunction colorFunction; + + public ImageBrush(BufferedImage image, LocalSession session, boolean alpha /*, boolean glass */) throws IOException { + this.session = session; + this.table = new SummedColorTable(image, alpha); + this.width = image.getWidth(); + this.height = image.getHeight(); + this.centerX = width / 2d; + this.centerZ = height / 2d; + + if (alpha) { + colorFunction = (x1, z1, x2, z2, extent, pos) -> { + int color = table.averageRGBA(x1, z1, x2, z2); + int alpha1 = (color >> 24) & 0xFF; + switch (alpha1) { + case 0: + return 0; + case 255: + return color; + default: + BaseBlock block = extent.getBlock(pos); + TextureUtil tu = session.getTextureUtil(); + int existingColor = tu.getColor(block); + return tu.combineTransparency(color, existingColor); + + } + }; + } else { + colorFunction = (x1, z1, x2, z2, extent, pos) -> table.averageRGB(x1, z1, x2, z2); + } + } + + private interface ColorFunction { + int call(int x1, int z1, int x2, int z2, Extent extent, Vector pos); + } + + private interface BlockFunction { + void apply(int color, Extent extent, Vector pos); } @Override public void build(EditSession editSession, Vector position, Pattern pattern, double sizeDouble) throws MaxChangedBlocksException { + TextureUtil texture = session.getTextureUtil(); + final int cx = position.getBlockX(); final int cy = position.getBlockY(); final int cz = position.getBlockZ(); - int size = (int) sizeDouble; - int maxY = editSession.getMaxY(); - int add; - if (yscale < 0) { - add = maxY; - } else { - add = 0; - } - double scale = (yscale / sizeDouble) * (maxY + 1); - final HeightMap map = getHeightMap(); - map.setSize(size); - int cutoff = onlyWhite ? maxY : 0; final SolidBlockMask solid = new SolidBlockMask(editSession); - final AdjacentAnyMask adjacent = new AdjacentAnyMask(Masks.negate(solid)); - RegionMask region = new RegionMask(new CuboidRegion(editSession.getWorld(), position.subtract(size, size, size), position.add(size, size, size))); + double scale = Math.max(width, height) / sizeDouble; + Location loc = editSession.getPlayer().getPlayer().getLocation(); + float yaw = loc.getYaw(); + float pitch = loc.getPitch(); + AffineTransform transform = new AffineTransform().rotateY((-yaw) % 360).rotateX(pitch - 90).inverse(); RecursiveVisitor visitor = new RecursiveVisitor(new Mask() { + private final Vector mutable = new Vector(); @Override public boolean test(Vector vector) { - if (solid.test(vector) && region.test(vector)) { + if (solid.test(vector)) { int dx = vector.getBlockX() - cx; int dy = vector.getBlockY() - cy; int dz = vector.getBlockZ() - cz; + Vector pos1 = transform.apply(mutable.setComponents(dx - 0.5, dy - 0.5, dz - 0.5)); + int x1 = (int) (pos1.getX() * scale + centerX); + int z1 = (int) (pos1.getZ() * scale + centerZ); - - if (dir != null) { - if (dy != 0) { - if (dir.getBlockX() != 0) { - dx += dir.getBlockX() * dy; - } else if (dir.getBlockZ() != 0) { - dz += dir.getBlockZ() * dy; - } - } - double raise = map.getHeight(dx, dz); - int val = (int) Math.ceil(raise * scale) + add; - if (val < cutoff) { - return true; - } - if (val >= 255 || PseudoRandom.random.random(maxY) < val) { - editSession.setBlock(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ(), pattern); - } - return true; + Vector pos2 = transform.apply(mutable.setComponents(dx + 0.5, dy + 0.5, dz + 0.5)); + int x2 = (int) (pos2.getX() * scale + centerX); + int z2 = (int) (pos2.getZ() * scale + centerZ); + if (x2 < x1) { + int tmp = x1; + x1 = x2; + x2 = tmp; } + if (z2 < z1) { + int tmp = z1; + z1 = z2; + z2 = tmp; + } + + if (x1 >= width || x2 < 0 || z1 >= height || z2 < 0) return false; + + + int color = colorFunction.call(x1, z1, x2, z2, editSession, vector); + if (color != 0) { + BaseBlock block = texture.getNearestBlock(color); + if (block != null) { + editSession.setBlock(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ(), block); + } + } + return true; } return false; } - }, new RegionFunction() { - @Override - public boolean apply(Vector vector) throws WorldEditException { - return true; - } - }, Integer.MAX_VALUE, editSession); + }, vector -> true, Integer.MAX_VALUE, editSession); visitor.setDirections(Arrays.asList(visitor.DIAGONAL_DIRECTIONS)); visitor.visit(position); Operations.completeBlindly(visitor); diff --git a/core/src/main/java/com/boydti/fawe/object/brush/LayerBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/LayerBrush.java index 883e446a..38b49479 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/LayerBrush.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/LayerBrush.java @@ -8,10 +8,8 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.MutableBlockVector; import com.sk89q.worldedit.Vector; -import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.command.tool.brush.Brush; -import com.sk89q.worldedit.function.RegionFunction; import com.sk89q.worldedit.function.mask.BlockMask; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.mask.SolidBlockMask; @@ -43,39 +41,33 @@ public class LayerBrush implements Brush { Operations.completeBlindly(visitor); BlockVectorSet visited = visitor.getVisited(); BaseBlock firstPattern = layers[0]; - visitor = new RecursiveVisitor(new Mask() { - @Override - public boolean test(Vector pos) { - int depth = visitor.getDepth() + 1; - if (depth > 1) { - boolean found = false; - int previous = layers[depth - 1].getCombined(); - int previous2 = layers[depth - 2].getCombined(); - for (Vector dir : BreadthFirstSearch.DEFAULT_DIRECTIONS) { - mutable.setComponents(pos.getBlockX() + dir.getBlockX(), pos.getBlockY() + dir.getBlockY(), pos.getBlockZ() + dir.getBlockZ()); - if (visitor.isVisited(mutable) && queue.getCachedCombinedId4Data(mutable.getBlockX(), mutable.getBlockY(), mutable.getBlockZ()) == previous) { - mutable.setComponents(pos.getBlockX() + dir.getBlockX() * 2, pos.getBlockY() + dir.getBlockY() * 2, pos.getBlockZ() + dir.getBlockZ() * 2); - if (visitor.isVisited(mutable) && queue.getCachedCombinedId4Data(mutable.getBlockX(), mutable.getBlockY(), mutable.getBlockZ()) == previous2) { - found = true; - break; - } else { - return false; - } + visitor = new RecursiveVisitor((Mask) pos -> { + int depth = visitor.getDepth() + 1; + if (depth > 1) { + boolean found = false; + int previous = layers[depth - 1].getCombined(); + int previous2 = layers[depth - 2].getCombined(); + for (Vector dir : BreadthFirstSearch.DEFAULT_DIRECTIONS) { + mutable.setComponents(pos.getBlockX() + dir.getBlockX(), pos.getBlockY() + dir.getBlockY(), pos.getBlockZ() + dir.getBlockZ()); + if (visitor.isVisited(mutable) && queue.getCachedCombinedId4Data(mutable.getBlockX(), mutable.getBlockY(), mutable.getBlockZ()) == previous) { + mutable.setComponents(pos.getBlockX() + dir.getBlockX() * 2, pos.getBlockY() + dir.getBlockY() * 2, pos.getBlockZ() + dir.getBlockZ() * 2); + if (visitor.isVisited(mutable) && queue.getCachedCombinedId4Data(mutable.getBlockX(), mutable.getBlockY(), mutable.getBlockZ()) == previous2) { + found = true; + break; + } else { + return false; } } - if (!found) { - return false; - } } - return !adjacent.test(pos); - } - }, new RegionFunction() { - @Override - public boolean apply(Vector pos) throws WorldEditException { - int depth = visitor.getDepth(); - BaseBlock currentPattern = layers[depth]; - return editSession.setBlockFast(pos, currentPattern); + if (!found) { + return false; + } } + return !adjacent.test(pos); + }, pos -> { + int depth = visitor.getDepth(); + BaseBlock currentPattern = layers[depth]; + return editSession.setBlockFast(pos, currentPattern); }, layers.length - 1, editSession); for (Vector pos : visited) { visitor.visit(pos); diff --git a/core/src/main/java/com/boydti/fawe/object/brush/StencilBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/StencilBrush.java index 50277f22..e263e923 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/StencilBrush.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/StencilBrush.java @@ -5,18 +5,18 @@ import com.boydti.fawe.object.brush.heightmap.HeightMap; import com.boydti.fawe.object.mask.AdjacentAnyMask; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.MutableBlockVector; import com.sk89q.worldedit.Vector; -import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.function.RegionFunction; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.mask.Masks; -import com.sk89q.worldedit.function.mask.RegionMask; import com.sk89q.worldedit.function.mask.SolidBlockMask; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.function.visitor.RecursiveVisitor; -import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.math.transform.AffineTransform; +import com.sk89q.worldedit.util.Location; import java.io.InputStream; import java.util.Arrays; @@ -34,6 +34,7 @@ public class StencilBrush extends HeightBrush { final int cy = position.getBlockY(); final int cz = position.getBlockZ(); int size = (int) sizeDouble; + int size2 = (int) (sizeDouble * sizeDouble); int maxY = editSession.getMaxY(); int add; if (yscale < 0) { @@ -47,42 +48,48 @@ public class StencilBrush extends HeightBrush { int cutoff = onlyWhite ? maxY : 0; final SolidBlockMask solid = new SolidBlockMask(editSession); final AdjacentAnyMask adjacent = new AdjacentAnyMask(Masks.negate(solid)); - RegionMask region = new RegionMask(new CuboidRegion(editSession.getWorld(), position.subtract(size, size, size), position.add(size, size, size))); + + + Player player = editSession.getPlayer().getPlayer(); + Vector pos = player.getPosition(); + + + + Location loc = editSession.getPlayer().getPlayer().getLocation(); + float yaw = loc.getYaw(); + float pitch = loc.getPitch(); + AffineTransform transform = new AffineTransform().rotateY((-yaw) % 360).rotateX(pitch - 90).inverse(); + + RecursiveVisitor visitor = new RecursiveVisitor(new Mask() { + private final MutableBlockVector mutable = new MutableBlockVector(); @Override public boolean test(Vector vector) { - if (solid.test(vector) && region.test(vector)) { + if (solid.test(vector)) { int dx = vector.getBlockX() - cx; int dy = vector.getBlockY() - cy; int dz = vector.getBlockZ() - cz; - Vector dir = adjacent.direction(vector); - if (dir != null) { - if (dy != 0) { - if (dir.getBlockX() != 0) { - dx += dir.getBlockX() * dy; - } else if (dir.getBlockZ() != 0) { - dz += dir.getBlockZ() * dy; - } - } - double raise = map.getHeight(dx, dz); - int val = (int) Math.ceil(raise * scale) + add; - if (val < cutoff) { - return true; - } - if (val >= 255 || PseudoRandom.random.random(maxY) < val) { - editSession.setBlock(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ(), pattern); - } + + Vector srcPos = transform.apply(mutable.setComponents(dx, dy, dz)); + dx = srcPos.getBlockX(); + dz = srcPos.getBlockZ(); + + int distance = dx * dx + dz * dz; + if (distance > size2 || Math.abs(dx) > 256 || Math.abs(dz) > 256) return false; + + double raise = map.getHeight(dx, dz); + int val = (int) Math.ceil(raise * scale) + add; + if (val < cutoff) { return true; } + if (val >= 255 || PseudoRandom.random.random(maxY) < val) { + editSession.setBlock(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ(), pattern); + } + return true; } return false; } - }, new RegionFunction() { - @Override - public boolean apply(Vector vector) throws WorldEditException { - return true; - } - }, Integer.MAX_VALUE, editSession); + }, vector -> true, Integer.MAX_VALUE, editSession); visitor.setDirections(Arrays.asList(visitor.DIAGONAL_DIRECTIONS)); visitor.visit(position); Operations.completeBlindly(visitor); diff --git a/core/src/main/java/com/boydti/fawe/object/collection/SummedAreaTable.java b/core/src/main/java/com/boydti/fawe/object/collection/SummedAreaTable.java index f4127b30..b3c4d0d5 100644 --- a/core/src/main/java/com/boydti/fawe/object/collection/SummedAreaTable.java +++ b/core/src/main/java/com/boydti/fawe/object/collection/SummedAreaTable.java @@ -44,7 +44,6 @@ public class SummedAreaTable { } public int average(int x, int z, int index) { - long centerHeight = source[index]; int minX = Math.max(0, x - radius) - x; int minZ = Math.max(0, z - radius) - z; int maxX = Math.min(width - 1, x + radius) - x; diff --git a/core/src/main/java/com/boydti/fawe/object/collection/SummedColorTable.java b/core/src/main/java/com/boydti/fawe/object/collection/SummedColorTable.java new file mode 100644 index 00000000..d3b07bed --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/collection/SummedColorTable.java @@ -0,0 +1,227 @@ +package com.boydti.fawe.object.collection; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +public class SummedColorTable { + private static float inv256 = 1/256f; + private final long[] reds, greens, blues, alpha; + private final int[] hasAlpha; + private final int length; + private final int width; + private final float[] areaInverses; + private final float[] alphaInverse; + + public SummedColorTable(BufferedImage image, final boolean calculateAlpha) { + int[] raw = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + this.width = image.getWidth(); + this.length = image.getHeight(); + + this.reds = new long[raw.length]; + this.greens = new long[raw.length]; + this.blues = new long[raw.length]; + this.hasAlpha = new int[raw.length]; + this.alpha = calculateAlpha ? new long[raw.length] : null; + this.alphaInverse = calculateAlpha ? new float[256] : null; + this.areaInverses = new float[Character.MAX_VALUE]; + for (int i = 0; i < areaInverses.length; i++) { + areaInverses[i] = 1f / (i + 1); + } + + int index = 0; + if (calculateAlpha) { + for (int i = 0; i < length; i++) { + for (int j = 0; j < width; j++, index++) { + int color = raw[index]; + int alpha = (color >> 24) & 0xFF; + int red, green, blue; + switch (alpha) { + case 0: + red = green = blue = 0; + break; + case 255: + red = (color >> 16) & 0xFF; + green = (color >> 8) & 0xFF; + blue = (color >> 0) & 0xFF; + break; + default: + red = (((color >> 16) & 0xFF) * alpha) >> 8; + green = (((color >> 8) & 0xFF) * alpha) >> 8; + blue = (((color >> 0) & 0xFF) * alpha) >> 8; + break; + } + this.reds[index] = getVal(i, j, index, red, this.reds); + this.greens[index] = getVal(i, j, index, green, this.greens); + this.blues[index] = getVal(i, j, index, blue, this.blues); + this.alpha[index] = getVal(i, j, index, alpha, this.alpha); + this.hasAlpha[index] = getVal(i, j, index, alpha > 0 ? 1 : 0, this.hasAlpha); + } + } + for (int i = 1; i < alphaInverse.length; i++) { + alphaInverse[i] = 256f / i; + } + } else { + for (int i = 0; i < length; i++) { + for (int j = 0; j < width; j++, index++) { + int color = raw[index]; + int red, green, blue, alpha; + if (((color >> 24) != 0)) { + alpha = 1; + red = (color >> 16) & 0xFF; + green = (color >> 8) & 0xFF; + blue = (color >> 0) & 0xFF; + } else { + alpha = red = green = blue = 0; + } + this.reds[index] = getVal(i, j, index, red, this.reds); + this.greens[index] = getVal(i, j, index, green, this.greens); + this.blues[index] = getVal(i, j, index, blue, this.blues); + this.hasAlpha[index] = getVal(i, j, index, alpha, this.hasAlpha); + + } + } + } + } + + private long getSum(int index, long[] summed) { + if (index < 0) return 0; + return summed[index]; + } + + public int averageRGB(int x1, int z1, int x2, int z2) { + int minX = Math.max(0, x1); + int minZ = Math.max(0, z1); + int maxX = Math.min(width - 1, x2); + int maxZ = Math.min(length - 1, z2); + + int XZ = maxZ * width + maxX; + long totRed = reds[XZ]; + long totGreen = greens[XZ]; + long totBlue = blues[XZ]; + int area = hasAlpha[XZ]; + + if (minX > 0) { + int pos = maxZ * width + minX - 1; + totRed -= reds[pos]; + totGreen -= greens[pos]; + totBlue -= blues[pos]; + area -= hasAlpha[pos]; + } + if (minZ > 0) { + int pos = minZ * width - width + maxX; + totRed -= reds[pos]; + totGreen -= greens[pos]; + totBlue -= blues[pos]; + area -= hasAlpha[pos]; + } + + if (minZ > 0 && minX > 0) { + int pos = minZ * width - width + minX - 1; + totRed += reds[pos]; + totGreen += greens[pos]; + totBlue += blues[pos]; + area += hasAlpha[pos]; + } + + if (area == 0) return 0; + float factor = this.areaInverses[area - 1]; + return (255 << 24) + (((int) (totRed * factor)) << 16) + (((int) (totGreen * factor)) << 8) + (((int) (totBlue * factor)) << 0); + } + + public int averageRGBA(int x1, int z1, int x2, int z2) { + int minX = Math.max(0, x1); + int minZ = Math.max(0, z1); + int maxX = Math.min(width - 1, x2); + int maxZ = Math.min(length - 1, z2); + + int XZ = maxZ * width + maxX; + long totRed = reds[XZ]; + long totGreen = greens[XZ]; + long totBlue = blues[XZ]; + long totAlpha = alpha[XZ]; + int area = hasAlpha[XZ]; + + if (minX > 0) { + int pos = maxZ * width + minX - 1; + totRed -= reds[pos]; + totGreen -= greens[pos]; + totBlue -= blues[pos]; + totAlpha -= alpha[pos]; + area -= hasAlpha[pos]; + } + if (minZ > 0) { + int pos = minZ * width - width + maxX; + totRed -= reds[pos]; + totGreen -= greens[pos]; + totBlue -= blues[pos]; + totAlpha -= alpha[pos]; + area -= hasAlpha[pos]; + } + + if (minZ > 0 && minX > 0) { + int pos = minZ * width - width + minX - 1; + totRed += reds[pos]; + totGreen += greens[pos]; + totBlue += blues[pos]; + totAlpha += alpha[pos]; + area += hasAlpha[pos]; + } + + if (totAlpha == 0) return 0; + + float factor = this.areaInverses[area - 1]; + float alpha = (totAlpha * factor); + factor = (factor * 256) / alpha; + return ((int) alpha << 24) + (((int) (totRed * factor)) << 16) + (((int) (totGreen * factor)) << 8) + (((int) (totBlue * factor)) << 0); + } + + private long getVal(int row, int col, int index, long curr, long[] summed) { + long leftSum; // sub matrix sum of left matrix + long topSum; // sub matrix sum of top matrix + long topLeftSum; // sub matrix sum of top left matrix + /* top left value is itself */ + if (index == 0) { + return curr; + } + /* top row */ + else if (row == 0 && col != 0) { + leftSum = summed[index - 1]; + return curr + leftSum; + } + /* left-most column */ + else if (row != 0 && col == 0) { + topSum = summed[index - width]; + return curr + topSum; + } else { + leftSum = summed[index - 1]; + topSum = summed[index - width]; + topLeftSum = summed[index - width - 1]; // overlap between leftSum and topSum + return curr + leftSum + topSum - topLeftSum; + } + } + + private int getVal(int row, int col, int index, int curr, int[] summed) { + int leftSum; // sub matrix sum of left matrix + int topSum; // sub matrix sum of top matrix + int topLeftSum; // sub matrix sum of top left matrix + /* top left value is itself */ + if (index == 0) { + return curr; + } + /* top row */ + else if (row == 0 && col != 0) { + leftSum = summed[index - 1]; + return curr + leftSum; + } + /* left-most column */ + else if (row != 0 && col == 0) { + topSum = summed[index - width]; + return curr + topSum; + } else { + leftSum = summed[index - 1]; + topSum = summed[index - width]; + topLeftSum = summed[index - width - 1]; // overlap between leftSum and topSum + return curr + leftSum + topSum - topLeftSum; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/util/TextureUtil.java b/core/src/main/java/com/boydti/fawe/util/TextureUtil.java index f5ed4d90..ee79b19d 100644 --- a/core/src/main/java/com/boydti/fawe/util/TextureUtil.java +++ b/core/src/main/java/com/boydti/fawe/util/TextureUtil.java @@ -838,7 +838,7 @@ public class TextureUtil implements TextureHolder { /** * Assumes the top layer is a transparent color and the bottom is opaque */ - protected int combineTransparency(int top, int bottom) { + public int combineTransparency(int top, int bottom) { int alpha1 = (top >> 24) & 0xFF; int alpha2 = 255 - alpha1; int red1 = (top >> 16) & 0xFF; diff --git a/core/src/main/java/com/boydti/fawe/util/image/ImageUtil.java b/core/src/main/java/com/boydti/fawe/util/image/ImageUtil.java index 3e09bbda..c8092dd2 100644 --- a/core/src/main/java/com/boydti/fawe/util/image/ImageUtil.java +++ b/core/src/main/java/com/boydti/fawe/util/image/ImageUtil.java @@ -2,11 +2,13 @@ package com.boydti.fawe.util.image; import com.boydti.fawe.Fawe; import com.boydti.fawe.util.MainUtil; +import com.boydti.fawe.util.MathMan; import com.sk89q.worldedit.util.command.parametric.ParameterException; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; import java.io.File; import java.io.IOException; import java.net.URL; @@ -68,6 +70,66 @@ public class ImageUtil { return ret; } + public static void fadeAlpha(BufferedImage image) { + int[] raw = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + int width = image.getWidth(); + int height = image.getHeight(); + int centerX = width / 2; + int centerZ = height / 2; + + float invRadiusX = 1f / centerX; + float invRadiusZ = 1f / centerZ; + + float[] sqrX = new float[width]; + float[] sqrZ = new float[height]; + for (int x = 0; x < width; x++) { + float distance = Math.abs(x - centerX) * invRadiusX; + sqrX[x] = distance * distance; + } + for (int z = 0; z < height; z++) { + float distance = Math.abs(z - centerZ) * invRadiusZ; + sqrZ[z] = distance * distance; + } + + for (int z = 0, index = 0; z < height; z++) { + float dz2 = sqrZ[z]; + for (int x = 0; x < width; x++, index++) { + int color = raw[index]; + int alpha = (color >> 24) & 0xFF; + if (alpha != 0) { + float dx2 = sqrX[x]; + float distSqr = dz2 + dx2; + if (distSqr > 1) raw[index] = 0; + else { + alpha = (int) (alpha * (1 - distSqr)); + raw[index] = (color & 0x00FFFFFF) + (alpha << 24); + } + } + } + } + } + + public static void scaleAlpha(BufferedImage image, double alphaScale) { + int[] raw = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int defined = (MathMan.clamp((int) (255 * alphaScale), 0, 255)) << 24; + for (int i = 0; i < raw.length; i++) { + int color = raw[i]; + int alpha = ((color >> 24) & 0xFF); + switch (alpha) { + case 0: + continue; + case 255: + raw[i] = (color & 0x00FFFFFF) + defined; + continue; + default: + alpha = MathMan.clamp((int) (alpha * alphaScale), 0, 255); + raw[i] = (color & 0x00FFFFFF) + (alpha << 24); + continue; + } + } + } + public static int getColor(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); diff --git a/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java b/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java index 70f9ddd4..87754f44 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -32,6 +32,7 @@ import com.boydti.fawe.object.clipboard.MultiClipboardHolder; import com.boydti.fawe.object.mask.IdMask; import com.boydti.fawe.util.ColorUtil; import com.boydti.fawe.util.MathMan; +import com.boydti.fawe.util.image.ImageUtil; import com.sk89q.minecraft.util.commands.*; import com.sk89q.worldedit.*; import com.sk89q.worldedit.blocks.BaseBlock; @@ -55,6 +56,7 @@ import com.sk89q.worldedit.util.command.binding.Range; import com.sk89q.worldedit.util.command.binding.Switch; import com.sk89q.worldedit.util.command.parametric.Optional; import java.awt.Color; +import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; import java.nio.channels.Channels; @@ -362,7 +364,7 @@ public class BrushCommands extends MethodCommands { } @Command( - aliases = {"stencil", "color"}, + aliases = {"stencil"}, usage = " [radius=5] [file|#clipboard|imgur=null] [rotation=360] [yscale=1.0]", desc = "Use a height map to paint a surface", help = @@ -392,44 +394,34 @@ public class BrushCommands extends MethodCommands { } @Command( - aliases = {"stencil", "color"}, - usage = " [radius=5] [file|#clipboard|imgur=null] [rotation=360] [yscale=1.0]", + aliases = {"image", "color"}, + usage = " [yscale=1]", desc = "Use a height map to paint a surface", + flags = "a", help = "Use a height map to paint any surface.\n" + - "The -w flag will only apply at maximum saturation\n" + - "The -r flag will apply random rotation", + "The -a flag will use image alpha\n" + + "The -f blends the image with the existing terrain", min = 1, max = -1 ) @CommandPermissions("worldedit.brush.stencil") - public BrushSettings stencilBrush(Player player, EditSession editSession, LocalSession session, Pattern fill, @Optional("5") double radius, @Optional("") final String image, @Optional("0") @Step(90) @Range(min=0, max=360) final int rotation, @Optional("1") final double yscale, @Switch('w') boolean onlyWhite, @Switch('r') boolean randomRotate, CommandContext context) throws WorldEditException { + public BrushSettings imageBrush(Player player, EditSession editSession, LocalSession session, @Optional("5") double radius, BufferedImage image, @Optional("1") @Range(min=Double.MIN_NORMAL) final double yscale, @Switch('a') boolean alpha, @Switch('f') boolean fadeOut, CommandContext context) throws WorldEditException, IOException { worldEdit.checkMaxBrushRadius(radius); - InputStream stream = getHeightmapStream(image); - HeightBrush brush; - try { - brush = new StencilBrush(stream, rotation, yscale, onlyWhite, image.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null); - } catch (EmptyClipboardException ignore) { - brush = new StencilBrush(stream, rotation, yscale, onlyWhite, null); + if (yscale != 1) { + ImageUtil.scaleAlpha(image, yscale); + alpha = true; } - if (randomRotate) { - brush.setRandomRotate(true); + if (fadeOut) { + ImageUtil.fadeAlpha(image); + alpha = true; } + ImageBrush brush = new ImageBrush(image, session, alpha); return set(session, context, brush) - .setSize(radius) - .setFill(fill); + .setSize(radius); } -// @Command( -// aliases = {"image", "img"} -// // TODO directional image coloring -// ) -// @CommandPermissions("worldedit.brush.stencil") -// public BrushSettings imageBrush(Player player, EditSession editSession, LocalSession session, Pattern fill, @Optional("5") double radius, @Optional("") final String image, @Optional("0") @Step(90) @Range(min=0, max=360) final int rotation, @Optional("1") final double yscale, @Switch('w') boolean onlyWhite, @Switch('r') boolean randomRotate, CommandContext context) throws WorldEditException { -// -// } - @Command( aliases = {"surface", "surf"}, usage = " [radius=5]",