diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/ImageMapRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/ImageMapRenderer.java new file mode 100644 index 0000000..3461005 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/ImageMapRenderer.java @@ -0,0 +1,50 @@ +package eu.mhsl.minecraft.pixelpic; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.map.MapCanvas; +import org.bukkit.map.MapRenderer; +import org.bukkit.map.MapView; +import org.jetbrains.annotations.NotNull; + +import java.awt.image.BufferedImage; + +public class ImageMapRenderer extends MapRenderer { + public final int IMAGE_SIZE = 128; + + private BufferedImage image; + private final int x; + private final int y; + + public ImageMapRenderer(BufferedImage image) { + this(image, 0, 0); + } + + public ImageMapRenderer(BufferedImage image, int x, int y) { + this.x = x; + this.y = y; + recalculateInput(image); + } + + public void recalculateInput(BufferedImage input) { + if (x * IMAGE_SIZE > input.getWidth() || y * IMAGE_SIZE > input.getHeight()) + throw new RuntimeException(String.format("Input image mus match a multiple of x and y with %d", IMAGE_SIZE)); + + int x1 = (int) (double) (x * IMAGE_SIZE); + int y1 = (int) (double) (y * IMAGE_SIZE); + + int x2 = (int) (double) Math.min(input.getWidth(), ((x + 1) * IMAGE_SIZE)); + int y2 = (int) (double) Math.min(input.getHeight(), ((y + 1) * IMAGE_SIZE)); + + if (x2 - x1 <= 0 || y2 - y1 <= 0) + return; + + this.image = input.getSubimage(x1, y1, x2 - x1, y2 - y1); + } + + @Override + public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) { + if(image == null) return; + Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> canvas.drawImage(0, 0, image), 2L); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/Main.java b/src/main/java/eu/mhsl/minecraft/pixelpic/Main.java index 1ccf6c9..25beb2a 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpic/Main.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/Main.java @@ -1,17 +1,123 @@ package eu.mhsl.minecraft.pixelpic; +import eu.mhsl.minecraft.pixelpic.render.render.DefaultScreenRenderer; +import eu.mhsl.minecraft.pixelpic.render.render.Renderer; +import eu.mhsl.minecraft.pixelpic.render.render.Resolution; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.map.MapView; import org.bukkit.plugin.java.JavaPlugin; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; + public final class Main extends JavaPlugin { + private static Main instance; + private Renderer screenRenderer; @Override public void onEnable() { - // Plugin startup logic + this.instance = this; + this.screenRenderer = new DefaultScreenRenderer(); + Bukkit.getPluginCommand("test").setExecutor((sender, command, label, args) -> { + if(!(sender instanceof Player player)) return false; + Resolution.Pixels pixels = Resolution.Pixels._128P; + Resolution.AspectRatio aspectRatio = Resolution.AspectRatio._1_1; + Resolution resolution = new Resolution(pixels, aspectRatio); + BufferedImage image = screenRenderer.render((Player) sender, resolution); + Bukkit.broadcast(Component.text(image.toString())); + + File file = new File(getDataFolder(), "Bild" + ".png"); + try { + getDataFolder().mkdir(); + ImageIO.write(image, "png", file); + } catch (Exception e) { + return true; + } + + ItemStack map = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta = (MapMeta) map.getItemMeta(); + + MapView mapView = Bukkit.createMap(Bukkit.getWorlds().getFirst()); + mapView.addRenderer(new ImageMapRenderer(image)); + + meta.setMapView(mapView); + map.setItemMeta(meta); + + player.getInventory().addItem(map); + player.updateInventory(); + + return true; + }); + + Bukkit.getPluginCommand("test2").setExecutor((sender, command, label, args) -> { + if(!(sender instanceof Player player)) return false; + Bukkit.broadcast(Component.text("HI")); + + Resolution.Pixels pixels = Resolution.Pixels._256P; + Resolution.AspectRatio aspectRatio = Resolution.AspectRatio._1_1; + Resolution resolution = new Resolution(pixels, aspectRatio); + BufferedImage image = screenRenderer.render((Player) sender, resolution); + Bukkit.broadcast(Component.text(image.toString())); + + File file = new File(getDataFolder(), "Bild" + ".png"); + try { + getDataFolder().mkdir(); + ImageIO.write(image, "png", file); + } catch (Exception e) { + return true; + } + + ItemStack map = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta = (MapMeta) map.getItemMeta(); + MapView mapView = Bukkit.createMap(Bukkit.getWorlds().getFirst()); + mapView.addRenderer(new ImageMapRenderer(image, 0, 0)); + meta.setMapView(mapView); + map.setItemMeta(meta); + player.getInventory().addItem(map); + + ItemStack map2 = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta1 = (MapMeta) map2.getItemMeta(); + MapView mapView4 = Bukkit.createMap(Bukkit.getWorlds().getFirst()); + mapView4.addRenderer(new ImageMapRenderer(image, 1, 0)); + meta1.setMapView(mapView4); + map2.setItemMeta(meta1); + player.getInventory().addItem(map2); + + ItemStack map3 = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta2 = (MapMeta) map3.getItemMeta(); + MapView mapView3 = Bukkit.createMap(Bukkit.getWorlds().getFirst()); + mapView3.addRenderer(new ImageMapRenderer(image, 0, 1)); + meta2.setMapView(mapView3); + map3.setItemMeta(meta2); + player.getInventory().addItem(map3); + + ItemStack map4 = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta3 = (MapMeta) map4.getItemMeta(); + MapView mapView2 = Bukkit.createMap(Bukkit.getWorlds().getFirst()); + mapView2.addRenderer(new ImageMapRenderer(image, 1, 1)); + meta3.setMapView(mapView2); + map4.setItemMeta(meta3); + player.getInventory().addItem(map4); + + player.updateInventory(); + + return true; + }); } @Override public void onDisable() { // Plugin shutdown logic } + + public static Main getInstance() { + return instance; + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/AbstractModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/AbstractModel.java new file mode 100644 index 0000000..af5d8ef --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/AbstractModel.java @@ -0,0 +1,108 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import com.google.common.base.Preconditions; + +import eu.mhsl.minecraft.pixelpic.render.model.MultiModel.MultiModelBuilder; +import eu.mhsl.minecraft.pixelpic.render.model.CrossModel.CrossModelBuilder; +import eu.mhsl.minecraft.pixelpic.render.model.StaticModel.StaticModelBuilder; +import eu.mhsl.minecraft.pixelpic.render.model.OctahedronModel.OctahedronModelBuilder; +import eu.mhsl.minecraft.pixelpic.render.model.SphereModel.SphereModelBuilder; + + +public abstract class AbstractModel implements Model { + + final int textureSize; + final int[][] texture; + + private final double transparencyFactor; + private final double reflectionFactor; + private final boolean occluding; + + AbstractModel(int[][] texture, double transparencyFactor, double reflectionFactor, + boolean occluding) { + Preconditions.checkNotNull(texture); + Preconditions.checkArgument(texture.length > 0, "texture cannot be empty"); + Preconditions.checkArgument(texture.length == texture[0].length, "texture must be a square array"); + + this.textureSize = texture.length; + this.texture = texture; + + this.transparencyFactor = transparencyFactor; + this.reflectionFactor = reflectionFactor; + this.occluding = occluding; + } + + @Override + public double getTransparencyFactor() { + return transparencyFactor; + } + + @Override + public double getReflectionFactor() { + return reflectionFactor; + } + + @Override + public boolean isOccluding() { + return occluding; + } + + public static abstract class Builder { + + final int[][] texture; + + double transparencyFactor; + double reflectionFactor; + boolean occluding; + + Builder(int[][] texture) { + this.texture = texture; + + this.transparencyFactor = 0; + this.reflectionFactor = 0; + this.occluding = false; + } + + public static SimpleModel.SimpleModelBuilder createSimple(int[][] texture) { + return new SimpleModel.SimpleModelBuilder(texture); + } + + public static MultiModelBuilder createMulti(int[][] topTexture, int[][] sideTexture, + int[][] bottomTexture) { + return new MultiModelBuilder(topTexture, sideTexture, bottomTexture); + } + + public static StaticModelBuilder createStatic(int color) { + return new StaticModelBuilder(color); + } + + public static CrossModelBuilder createCross(int[][] texture) { + return new CrossModelBuilder(texture); + } + + public static SphereModelBuilder createSphere(int[][] texture) { + return new SphereModelBuilder(texture); + } + + public static OctahedronModelBuilder createOctahedron(int[][] texture) { + return new OctahedronModelBuilder(texture); + } + + public Builder transparency(double transparencyFactor) { + this.transparencyFactor = transparencyFactor; + return this; + } + + public Builder reflection(double reflectionFactor) { + this.reflectionFactor = reflectionFactor; + return this; + } + + public Builder occlusion() { + this.occluding = true; + return this; + } + + public abstract Model build(); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/CrossModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/CrossModel.java new file mode 100644 index 0000000..ae5b62f --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/CrossModel.java @@ -0,0 +1,91 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import eu.mhsl.minecraft.pixelpic.render.util.MathUtil; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; + +public class CrossModel extends AbstractModel { + + private static final Vector NORMAL_ONE = new Vector(1, 0, 1).normalize(); + private static final Vector NORMAL_TWO = new Vector(-1, 0, 1).normalize(); + + private static final Vector POINT_ONE = new Vector(1, 0, 0); + private static final Vector POINT_TWO = new Vector(1, 0, 1); + + private CrossModel(int[][] texture, double transparencyFactor, double reflectionFactor, + boolean occluding) { + super(texture, transparencyFactor, reflectionFactor, occluding); + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + Vector linePoint = currentIntersection.getPoint(); + Vector lineDirection = currentIntersection.getDirection(); + + Vector blockPoint = block.getLocation().toVector(); + Vector planePoint = block.getLocation().add(0.5, 0, 0.5).toVector(); + + double distance = Double.POSITIVE_INFINITY; + int color = 0; + Vector target = null; + + Vector intersectionOne = MathUtil.getLinePlaneIntersection(linePoint, lineDirection, planePoint, NORMAL_ONE, + true); + if (intersectionOne != null) { + intersectionOne.subtract(blockPoint); + if (isInsideBlock(intersectionOne)) { + color = getColor(intersectionOne, POINT_ONE); + distance = linePoint.distanceSquared(intersectionOne.add(blockPoint)); + target = intersectionOne; + } + } + + Vector intersectionTwo = MathUtil.getLinePlaneIntersection(linePoint, lineDirection, planePoint, NORMAL_TWO, + true); + if (intersectionTwo != null) { + intersectionTwo.subtract(blockPoint); + if (isInsideBlock(intersectionTwo)) { + int colorTwo = getColor(intersectionTwo, POINT_TWO); + double distanceTwo = linePoint.distanceSquared(intersectionTwo.add(blockPoint)); + if ((distanceTwo < distance && (colorTwo >> 24) != 0) || (color >> 24) == 0) { + target = intersectionTwo; + color = colorTwo; + } + } + } + + if (target == null) { + target = linePoint; + } + + return Intersection.of(currentIntersection.getNormal(), target, lineDirection, color); + } + + private boolean isInsideBlock(Vector vec) { + return vec.getX() >= 0 && vec.getZ() < 1 && vec.getY() >= 0 && vec.getY() < 1 && vec.getZ() >= 0 + && vec.getZ() < 1; + } + + private int getColor(Vector vec, Vector base) { + double xOffset = Math.sqrt(Math.pow(vec.getX() - base.getX(), 2) + Math.pow(vec.getZ() - base.getZ(), 2)); + double yOffset = vec.getY(); + + int pixelY = (int) Math.floor(yOffset * textureSize); + int pixelX = (int) Math.floor(xOffset / Math.sqrt(2) * textureSize); + + return texture[pixelY][pixelX]; + } + + public static class CrossModelBuilder extends Builder { + + CrossModelBuilder(int[][] texture) { + super(texture); + } + + @Override + public CrossModel build() { + return new CrossModel(texture, transparencyFactor, reflectionFactor, occluding); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/Model.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/Model.java new file mode 100644 index 0000000..830e288 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/Model.java @@ -0,0 +1,15 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import org.bukkit.block.Block; + +public interface Model { + + Intersection intersect(Block block, Intersection currentIntersection); + + double getTransparencyFactor(); + + double getReflectionFactor(); + + boolean isOccluding(); +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/MultiModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/MultiModel.java new file mode 100644 index 0000000..6af8049 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/MultiModel.java @@ -0,0 +1,61 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; + +public class MultiModel extends SimpleModel { + + private final int[][] topTexture; + private final int[][] bottomTexture; + + private MultiModel(int[][] topTexture, int[][] sideTexture, int[][] bottomTexture, + double transparencyFactor, double reflectionFactor, boolean occluding) { + super(sideTexture, transparencyFactor, reflectionFactor, occluding); + + this.topTexture = topTexture; + this.bottomTexture = bottomTexture; + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + if (!currentIntersection.getNormal().equals(UP) && !currentIntersection.getNormal().equals(DOWN)) { + return super.intersect(block, currentIntersection); + } + + Vector normal = currentIntersection.getNormal(); + Vector point = currentIntersection.getPoint(); + Vector direction = currentIntersection.getDirection(); + + double yOffset = point.getX() - (int) point.getX(); + double xOffset = point.getZ() - (int) point.getZ(); + + int pixelY = (int) Math.floor((yOffset < 0 ? yOffset + 1 : yOffset) * textureSize); + int pixelX = (int) Math.floor((xOffset < 0 ? xOffset + 1 : xOffset) * textureSize); + + if (normal.equals(UP)) { + return Intersection.of(normal, point, direction, topTexture[pixelY][pixelX]); + } else { + return Intersection.of(normal, point, direction, bottomTexture[pixelY][pixelX]); + } + } + + public static class MultiModelBuilder extends SimpleModelBuilder { + + private final int[][] topTexture; + private final int[][] bottomTexture; + + MultiModelBuilder(int[][] topTexture, int[][] sideTexture, int[][] bottomTexture) { + super(sideTexture); + + this.topTexture = topTexture; + this.bottomTexture = bottomTexture; + } + + @Override + public MultiModel build() { + return new MultiModel(topTexture, texture, bottomTexture, transparencyFactor, + reflectionFactor, occluding); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/OctahedronModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/OctahedronModel.java new file mode 100644 index 0000000..5cdd731 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/OctahedronModel.java @@ -0,0 +1,100 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import eu.mhsl.minecraft.pixelpic.render.util.MathUtil; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; + +public class OctahedronModel extends AbstractModel { + + private static final double RADIUS = 0.5; + + private static final Vector[] NORMALS = new Vector[]{new Vector(-1, -1, -1), new Vector(-1, -1, 1), + new Vector(-1, 1, -1), new Vector(-1, 1, 1), new Vector(1, -1, -1), new Vector(1, -1, 1), + new Vector(1, 1, -1), new Vector(1, 1, 1)}; + + private OctahedronModel(int[][] texture, double transparencyFactor, double reflectionFactor, + boolean occluding) { + super(texture, transparencyFactor, reflectionFactor, occluding); + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + Vector linePoint = currentIntersection.getPoint(); + Vector lineDirection = currentIntersection.getDirection(); + Vector blockPoint = block.getLocation().toVector(); + Vector centerPoint = blockPoint.clone().add(new Vector(0.5, 0.5, 0.5)); + + Vector lastIntersection = null; + double lastDistance = Double.POSITIVE_INFINITY; + for (int i = 0; i < 8; i++) { + Vector planePoint = new Vector(i < 4 ? -0.5 : 0.5, 0, 0).add(centerPoint); + Vector planeNormal = NORMALS[i]; + + Vector intersection = MathUtil.getLinePlaneIntersection(linePoint, lineDirection, planePoint, planeNormal, + false); + if (intersection == null) { + continue; + } + + if (!isInsideBlock(blockPoint, planeNormal, intersection)) { + continue; + } + + double distance = intersection.distance(linePoint); + if (distance < lastDistance) { + lastIntersection = intersection; + lastDistance = distance; + } + } + + if (lastIntersection == null) { + return currentIntersection; + } + + double dist = linePoint.distance(centerPoint); + double minDist = dist - RADIUS; + double maxDist = dist + RADIUS; + double factor = (lastDistance - minDist) / (maxDist - minDist); + + double yOffset = lastIntersection.getX() - (int) lastIntersection.getX(); + double xOffset = lastIntersection.getZ() - (int) lastIntersection.getZ(); + + int pixelY = (int) Math.floor((yOffset < 0 ? yOffset + 1 : yOffset) * textureSize); + int pixelX = (int) Math.floor((xOffset < 0 ? xOffset + 1 : xOffset) * textureSize); + + return Intersection.of(currentIntersection.getNormal(), lastIntersection, lineDirection, + 0xFF000000 | MathUtil.weightedColorSum(texture[pixelY][pixelX], 0, 1 - factor, factor)); + } + + private boolean isInsideBlock(Vector blockPoint, Vector planeNormal, Vector intersection) { + intersection = intersection.clone().subtract(blockPoint); + + if (intersection.getX() < 0 || intersection.getX() >= 1 || intersection.getY() < 0 || intersection.getY() >= 1 + || intersection.getZ() < 0 || intersection.getZ() >= 1) { + return false; + } + + boolean posX = planeNormal.getX() >= 0; + boolean posY = planeNormal.getY() >= 0; + boolean posZ = planeNormal.getZ() >= 0; + + boolean blockX = intersection.getX() >= 0.5; + boolean blockY = intersection.getY() >= 0.5; + boolean blockZ = intersection.getZ() >= 0.5; + + return posX == blockX && posY == blockY && posZ == blockZ; + } + + public static class OctahedronModelBuilder extends Builder { + + OctahedronModelBuilder(int[][] texture) { + super(texture); + } + + @Override + public Model build() { + return new OctahedronModel(texture, transparencyFactor, reflectionFactor, occluding); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SimpleModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SimpleModel.java new file mode 100644 index 0000000..b74c797 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SimpleModel.java @@ -0,0 +1,58 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; + +public class SimpleModel extends AbstractModel { + + static final Vector UP = new Vector(0, 1, 0); + static final Vector DOWN = new Vector(0, -1, 0); + private static final Vector NORTH = new Vector(0, 0, -1); + private static final Vector SOUTH = new Vector(0, 0, 1); + private static final Vector EAST = new Vector(1, 0, 0); + private static final Vector WEST = new Vector(-1, 0, 0); + + SimpleModel(int[][] texture, double transparencyFactor, double reflectionFactor, + boolean occluding) { + super(texture, transparencyFactor, reflectionFactor, occluding); + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + double yOffset; + double xOffset; + + Vector normal = currentIntersection.getNormal(); + Vector point = currentIntersection.getPoint(); + Vector direction = currentIntersection.getDirection(); + + if (normal.equals(NORTH) || normal.equals(SOUTH)) { + yOffset = point.getY() - (int) point.getY(); + xOffset = point.getX() - (int) point.getX(); + } else if (normal.equals(EAST) || normal.equals(WEST)) { + yOffset = point.getY() - (int) point.getY(); + xOffset = point.getZ() - (int) point.getZ(); + } else { + yOffset = point.getX() - (int) point.getX(); + xOffset = point.getZ() - (int) point.getZ(); + } + + int pixelY = (int) Math.floor((yOffset < 0 ? yOffset + 1 : yOffset) * textureSize); + int pixelX = (int) Math.floor((xOffset < 0 ? xOffset + 1 : xOffset) * textureSize); + + return Intersection.of(normal, point, direction, texture[pixelY][pixelX]); + } + + public static class SimpleModelBuilder extends Builder { + + protected SimpleModelBuilder(int[][] texture) { + super(texture); + } + + @Override + public Model build() { + return new SimpleModel(texture, transparencyFactor, reflectionFactor, occluding); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SphereModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SphereModel.java new file mode 100644 index 0000000..f67c162 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/SphereModel.java @@ -0,0 +1,128 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import eu.mhsl.minecraft.pixelpic.render.util.MathUtil; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; + +public class SphereModel extends AbstractModel { + + private final double radius; + private final Vector offset; + + private SphereModel(int[][] texture, double transparencyFactor, double reflectionFactor, + boolean occluding, double radius, Vector offset) { + super(texture, transparencyFactor, reflectionFactor, occluding); + + this.radius = radius; + this.offset = offset; + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + Vector linePoint = currentIntersection.getPoint(); + Vector lineDirection = currentIntersection.getDirection(); + Vector blockPoint = block.getLocation().toVector(); + Vector centerPoint = block.getLocation().add(0.5, 0.5, 0.5).add(offset).toVector(); + + double a = lineDirection.dot(lineDirection); + double b = 2 * (linePoint.dot(lineDirection) - centerPoint.dot(lineDirection)); + double c = linePoint.dot(linePoint) - 2 * centerPoint.dot(linePoint) + centerPoint.dot(centerPoint) + - Math.pow(radius, 2); + + double delta = Math.pow(b, 2) - 4 * a * c; + if (delta < 0) { + return Intersection.of(currentIntersection.getNormal(), linePoint, lineDirection); + } + + double dist = linePoint.distance(centerPoint); + double minDist = dist - radius; + double maxDist = dist + radius; + + if (delta == 0) { + double t = -b / (2 * a); + Vector intersection = lineDirection.clone().add(lineDirection.clone().multiply(t)); + if (!isInsideBlock(blockPoint, intersection)) { + return currentIntersection; + } + double currentDist = intersection.distance(linePoint); + double factor = (currentDist - minDist) / (maxDist - minDist); + Vector normal = intersection.clone().subtract(centerPoint).normalize(); + return Intersection.of(normal, intersection, lineDirection, getColor(centerPoint, intersection, factor)); + } + + double deltaSqrt = Math.sqrt(delta); + + double tOne = (-b + deltaSqrt) / (2 * a); + double tTwo = (-b - deltaSqrt) / (2 * a); + + Vector intersectionOne = linePoint.clone().add(lineDirection.clone().multiply(tOne)); + Vector intersectionTwo = linePoint.clone().add(lineDirection.clone().multiply(tTwo)); + + boolean first = intersectionOne.distanceSquared(linePoint) < intersectionTwo.distanceSquared(linePoint); + double currentDist = (first ? intersectionOne : intersectionTwo).distance(linePoint); + double factor = (currentDist - minDist) / (maxDist - minDist); + if (first && isInsideBlock(blockPoint, intersectionOne)) { + Vector normal = intersectionOne.clone().subtract(centerPoint).normalize(); + return Intersection.of(normal, intersectionOne, lineDirection, + getColor(centerPoint, intersectionOne, factor)); + } else if (isInsideBlock(blockPoint, intersectionTwo)) { + Vector normal = intersectionTwo.clone().subtract(centerPoint).normalize(); + return Intersection.of(normal, intersectionTwo, lineDirection, + getColor(centerPoint, intersectionTwo, factor)); + } else { + return currentIntersection; + } + } + + private int getColor(Vector base, Vector intersection, double factor) { + Location loc = base.toLocation(null); + loc.setDirection(intersection.clone().subtract(base).normalize()); + + double perimeter = Math.round(2 * Math.PI * radius); + double yawDiv = 360 / perimeter; + double pitchDiv = 180 / perimeter; + + int pixelX = (int) ((loc.getYaw() % yawDiv) / (yawDiv / textureSize)); + int pixelY = (int) (((loc.getPitch() + 90) % pitchDiv) / (pitchDiv / textureSize)); + + return 0xFF000000 | MathUtil.weightedColorSum(texture[pixelY][pixelX], 0, 1 - factor, factor); + } + + private boolean isInsideBlock(Vector blockPoint, Vector intersection) { + intersection = intersection.clone().subtract(blockPoint); + + return intersection.getX() >= 0 && intersection.getX() < 1 && intersection.getY() >= 0 + && intersection.getY() < 1 && intersection.getZ() >= 0 && intersection.getZ() < 1; + } + + public static class SphereModelBuilder extends Builder { + + private double radius; + private Vector offset; + + SphereModelBuilder(int[][] texture) { + super(texture); + + this.radius = 0.5; + this.offset = new Vector(); + } + + public SphereModelBuilder radius(double radius) { + this.radius = radius; + return this; + } + + public SphereModelBuilder offset(Vector offset) { + this.offset = offset.clone(); + return this; + } + + @Override + public Model build() { + return new SphereModel(texture, transparencyFactor, reflectionFactor, occluding, radius, + offset); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/StaticModel.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/StaticModel.java new file mode 100644 index 0000000..75c7be3 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/model/StaticModel.java @@ -0,0 +1,57 @@ +package eu.mhsl.minecraft.pixelpic.render.model; + +import eu.mhsl.minecraft.pixelpic.render.model.AbstractModel.Builder; +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import org.bukkit.block.Block; + +public class StaticModel implements Model { + + private final int color; + private final double transparencyFactor; + private final double reflectionFactor; + private final boolean occluding; + + private StaticModel(int color, double transparencyFactor, double reflectionFactor, boolean occluding) { + this.color = color; + this.transparencyFactor = transparencyFactor; + this.reflectionFactor = reflectionFactor; + this.occluding = occluding; + } + + @Override + public Intersection intersect(Block block, Intersection currentIntersection) { + return Intersection.of(currentIntersection.getNormal(), currentIntersection.getPoint(), + currentIntersection.getDirection(), color); + } + + @Override + public double getTransparencyFactor() { + return transparencyFactor; + } + + @Override + public double getReflectionFactor() { + return reflectionFactor; + } + + @Override + public boolean isOccluding() { + return occluding; + } + + public static class StaticModelBuilder extends Builder { + + private final int color; + + StaticModelBuilder(int color) { + super(new int[1][1]); + + this.color = color; + } + + @Override + public StaticModel build() { + return new StaticModel(color, transparencyFactor, reflectionFactor, occluding); + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/DefaultRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/DefaultRaytracer.java new file mode 100644 index 0000000..134a9f5 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/DefaultRaytracer.java @@ -0,0 +1,141 @@ +package eu.mhsl.minecraft.pixelpic.render.raytrace; + +import eu.mhsl.minecraft.pixelpic.render.model.Model; +import eu.mhsl.minecraft.pixelpic.render.registry.DefaultModelRegistry; +import eu.mhsl.minecraft.pixelpic.render.registry.ModelRegistry; +import eu.mhsl.minecraft.pixelpic.render.util.BlockRaytracer; +import eu.mhsl.minecraft.pixelpic.render.util.Intersection; +import eu.mhsl.minecraft.pixelpic.render.util.MathUtil; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.util.Vector; + +public class DefaultRaytracer implements Raytracer { + + private static final int MAX_DISTANCE = 300; + private static final int REFLECTION_DEPTH = 10; + + private final ModelRegistry textureRegistry; + private Block reflectedBlock; + + public DefaultRaytracer() { + this.textureRegistry = new DefaultModelRegistry(); + this.textureRegistry.initialize(); + + this.reflectedBlock = null; + } + + @Override + public int trace(World world, Vector point, Vector direction) { + return trace(world, point, direction, REFLECTION_DEPTH); + } + + private int trace(World world, Vector point, Vector direction, int reflectionDepth) { + Location loc = point.toLocation(world); + loc.setDirection(direction); + BlockRaytracer iterator = new BlockRaytracer(loc); + int baseColor = Color.fromRGB(65, 89, 252).asRGB(); + Vector finalIntersection = null; + + int reflectionColor = 0; + double reflectionFactor = 0; + boolean reflected = false; + + Vector transparencyStart = null; + int transparencyColor = 0; + double transparencyFactor = 0; + + Material occlusionMaterial = null; + BlockData occlusionData = null; + + for (int i = 0; i < MAX_DISTANCE; i++) { + if (!iterator.hasNext()) { + break; + } + + Block block = iterator.next(); + if (block == null) { + continue; + } + + if (reflectedBlock != null && reflectedBlock.equals(block)) { + continue; + } + reflectedBlock = null; + + Material material = block.getType(); + if (material == Material.AIR) { + occlusionMaterial = null; + occlusionData = null; + continue; + } + + Model textureModel = textureRegistry.getModel(block); + Intersection currentIntersection = Intersection.of(MathUtil.toVector(iterator.getIntersectionFace()), + i == 0 ? point : iterator.getIntersectionPoint(), direction); + Intersection newIntersection = textureModel.intersect(block, currentIntersection); + + if (newIntersection == null) { + continue; + } + + int color = newIntersection.getColor(); + + if (!reflected && textureModel.getReflectionFactor() > 0 && reflectionDepth > 0 && (color >> 24) != 0) { + reflectedBlock = block; + reflectionColor = trace(world, newIntersection.getPoint(), MathUtil.reflectVector(point, direction, + newIntersection.getPoint(), newIntersection.getNormal()), reflectionDepth - 1); + reflectionFactor = textureModel.getReflectionFactor(); + reflected = true; + } + + if (transparencyStart == null && textureModel.getTransparencyFactor() > 0) { + transparencyStart = newIntersection.getPoint(); + transparencyColor = newIntersection.getColor(); + transparencyFactor = textureModel.getTransparencyFactor(); + } + + if (textureModel.isOccluding()) { + BlockData data = block.getBlockData(); + + if (material == occlusionMaterial && data.equals(occlusionData)) { + continue; + } + + occlusionMaterial = material; + occlusionData = data; + } else { + occlusionMaterial = null; + occlusionData = null; + } + + if (transparencyStart != null && textureModel.getTransparencyFactor() > 0) { + continue; + } + + if ((color >> 24) == 0) { + continue; + } + + baseColor = color; + finalIntersection = newIntersection.getPoint(); + break; + } + + if (transparencyStart != null) { + baseColor = MathUtil.weightedColorSum(baseColor, transparencyColor, transparencyFactor, (1 + - transparencyFactor) + * (1 + transparencyStart.distance(finalIntersection == null ? transparencyStart : finalIntersection) + / 5.0)); + } + if (reflected) { + baseColor = MathUtil.weightedColorSum(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor); + } + + return baseColor & 0xFFFFFF; + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/Raytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/Raytracer.java new file mode 100644 index 0000000..fdb5d85 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/raytrace/Raytracer.java @@ -0,0 +1,9 @@ +package eu.mhsl.minecraft.pixelpic.render.raytrace; + +import org.bukkit.World; +import org.bukkit.util.Vector; + +public interface Raytracer { + + int trace(World world, Vector point, Vector direction); +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/DefaultModelRegistry.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/DefaultModelRegistry.java new file mode 100644 index 0000000..641342b --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/DefaultModelRegistry.java @@ -0,0 +1,169 @@ +package eu.mhsl.minecraft.pixelpic.render.registry; + +import eu.mhsl.minecraft.pixelpic.render.model.AbstractModel.Builder; +import eu.mhsl.minecraft.pixelpic.render.model.Model; +import org.bukkit.Color; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +public class DefaultModelRegistry implements ModelRegistry { + + private static final String IMAGE_RESOURCE = "terrain.png"; + private static final int TEXTURE_SIZE = 16; + + private final Map> modelMap; + private BufferedImage textures; + + public DefaultModelRegistry() { + this.modelMap = new HashMap<>(); + } + + @Override + public void initialize() { + URL url = this.getClass().getClassLoader().getResource(IMAGE_RESOURCE); + if (url == null) { + throw new RuntimeException("Default resource \"terrain.png\" is missing"); + } + try (InputStream input = url.openConnection().getInputStream()) { + this.textures = ImageIO.read(input); + } catch (IOException e) { + throw new RuntimeException(e); + } + + registerModel(Material.GRASS_BLOCK, Builder.createMulti(textureIndex(0, 0), textureIndex(0, 3), textureIndex(0, 2)).build()); + registerModel(Material.STONE, Builder.createSimple(textureIndex(0, 1)).build()); + registerModel(Material.DIRT, Builder.createSimple(textureIndex(0, 2)).build()); + registerModel(Material.OAK_PLANKS, Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.SPRUCE_PLANKS, + Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.BIRCH_PLANKS, + Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.JUNGLE_PLANKS, + Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.ACACIA_PLANKS, + Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.DARK_OAK_PLANKS, + Builder.createSimple(textureIndex(0, 4)).build()); + registerModel(Material.BRICK, Builder.createSimple(textureIndex(0, 7)).build()); + registerModel(Material.TNT, Builder.createMulti(textureIndex(0, 9), + textureIndex(0, 8), textureIndex(0, 10)).build()); + registerModel(Material.WATER, Builder.createStatic(0xFF000000 | Color.fromRGB(0, 5, 60).asRGB()) + .transparency(0.60).reflection(0.1).occlusion().build()); + registerModel(Material.DIAMOND_BLOCK, + Builder.createSimple(textureIndex(3, 3)).reflection(0.75).build()); + registerModel(Material.POPPY, Builder.createCross(textureIndex(0, 12)).build()); + registerModel(Material.DANDELION, Builder.createCross(textureIndex(0, 13)).build()); + registerModel(Material.OAK_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + registerModel(Material.SPRUCE_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + registerModel(Material.BIRCH_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + registerModel(Material.JUNGLE_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + registerModel(Material.ACACIA_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + registerModel(Material.DARK_OAK_SAPLING, + Builder.createCross(textureIndex(0, 15)).build()); + + registerModel(Material.COBBLESTONE, + Builder.createSimple(textureIndex(1, 0)).build()); + registerModel(Material.BEDROCK, Builder.createSimple(textureIndex(1, 1)).build()); + registerModel(Material.SAND, Builder.createSimple(textureIndex(1, 2)).build()); + registerModel(Material.GRAVEL, Builder.createSimple(textureIndex(1, 3)).build()); + registerModel(Material.OAK_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.SPRUCE_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.BIRCH_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.JUNGLE_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.ACACIA_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.DARK_OAK_LOG, Builder.createMulti(textureIndex(1, 5), + textureIndex(1, 4), textureIndex(1, 5)).build()); + registerModel(Material.OAK_WOOD, Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.SPRUCE_WOOD, + Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.BIRCH_WOOD, Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.JUNGLE_WOOD, + Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.ACACIA_WOOD, + Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.DARK_OAK_WOOD, + Builder.createSimple(textureIndex(1, 4)).build()); + registerModel(Material.OAK_LEAVES, Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.SPRUCE_LEAVES, + Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.BIRCH_LEAVES, + Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.JUNGLE_LEAVES, + Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.ACACIA_LEAVES, + Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.DARK_OAK_LEAVES, + Builder.createSimple(textureIndex(1, 6)).build()); + registerModel(Material.IRON_BLOCK, + Builder.createMulti(textureIndex(1, 7), + textureIndex(2, 7), textureIndex(3, 7)).build()); + registerModel(Material.GOLD_BLOCK, Builder.createMulti(textureIndex(1, 8), + textureIndex(2, 8), textureIndex(3, 8)).build()); + registerModel(Material.RED_MUSHROOM, + Builder.createCross(textureIndex(1, 12)).build()); + registerModel(Material.BROWN_MUSHROOM, + Builder.createCross(textureIndex(1, 13)).build()); + registerModel(Material.LAVA, Builder.createSimple(textureIndex(2, 14)) + .transparency(0.15).reflection(0.05).occlusion().build()); + + registerModel(Material.GOLD_ORE, Builder.createSimple(textureIndex(2, 0)).build()); + registerModel(Material.IRON_ORE, Builder.createSimple(textureIndex(2, 1)).build()); + registerModel(Material.COAL_ORE, Builder.createSimple(textureIndex(2, 2)).build()); + + registerModel(Material.GLASS, + Builder.createSimple(textureIndex(3, 1)).occlusion().build()); + + registerModel(Material.GRASS_BLOCK, Builder.createCross(textureIndex(5, 6)).build()); + registerModel(Material.SUGAR_CANE, Builder.createCross(textureIndex(5, 5)).build()); + } + + @Override + public Model getModel(Material material, BlockData blockData) { + return modelMap.computeIfAbsent(material, key -> new HashMap<>()).getOrDefault(blockData, + blockData == null ? getDefaultModel() + : modelMap.get(material).getOrDefault(null, getDefaultModel())); + } + + @Override + public Model getDefaultModel() { + return Builder.createStatic(Color.PURPLE.asRGB()).build(); + } + + private void registerModel(Material material, Model blockModel) { + modelMap.computeIfAbsent(material, key -> new HashMap<>()) + .put(null, blockModel); + } + + private int[][] textureIndex(int verticalIndex, int horizontalIndex) { + int[][] texture = new int[TEXTURE_SIZE][TEXTURE_SIZE]; + + int offsetY = verticalIndex * TEXTURE_SIZE + (TEXTURE_SIZE - 1); + int offsetX = horizontalIndex * TEXTURE_SIZE; + + for (int pixelY = 0; pixelY < TEXTURE_SIZE; pixelY++) { + for (int pixelX = 0; pixelX < TEXTURE_SIZE; pixelX++) { + texture[pixelY][pixelX] = textures.getRGB(offsetX + pixelX, offsetY - pixelY); + } + } + + return texture; + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/ModelRegistry.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/ModelRegistry.java new file mode 100644 index 0000000..d304671 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/registry/ModelRegistry.java @@ -0,0 +1,23 @@ +package eu.mhsl.minecraft.pixelpic.render.registry; + +import eu.mhsl.minecraft.pixelpic.render.model.Model; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +public interface ModelRegistry { + + void initialize(); + + default Model getModel(Block block) { + return getModel(block.getType(), block.getBlockData()); + } + + default Model getModel(Material material) { + return getModel(material, null); + } + + Model getModel(Material material, BlockData blockData); + + Model getDefaultModel(); +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/DefaultScreenRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/DefaultScreenRenderer.java new file mode 100644 index 0000000..8df6ac3 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/DefaultScreenRenderer.java @@ -0,0 +1,87 @@ +package eu.mhsl.minecraft.pixelpic.render.render; + +import eu.mhsl.minecraft.pixelpic.render.raytrace.DefaultRaytracer; +import eu.mhsl.minecraft.pixelpic.render.raytrace.Raytracer; +import eu.mhsl.minecraft.pixelpic.render.util.MathUtil; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.ArrayList; +import java.util.List; + +public class DefaultScreenRenderer implements Renderer { + + private static final double FOV_YAW_DEG = 53; + private static final double FOV_PITCH_DEG = 23; + + private static final double FOV_YAW_RAD = Math.toRadians(FOV_YAW_DEG); + private static final double FOV_PITCH_RAD = Math.toRadians(FOV_PITCH_DEG); + + private static final Vector BASE_VEC = new Vector(1, 0, 0); + + private final Raytracer raytracer; + + public DefaultScreenRenderer() { + this.raytracer = new DefaultRaytracer(); + } + + @Override + public BufferedImage render(Player player, Resolution resolution) { + int width = resolution.getWidth(); + int height = resolution.getHeight(); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + int[] imageData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + World world = player.getWorld(); + Vector linePoint = player.getEyeLocation().toVector(); + List rayMap = buildRayMap(player, resolution); + for (int i = 0; i < rayMap.size(); i++) { + imageData[i] = raytracer.trace(world, linePoint, rayMap.get(i)); + } + + return image; + } + + private List buildRayMap(Player p, Resolution resolution) { + Location eyeLocation = p.getEyeLocation(); + Vector lineDirection = eyeLocation.getDirection(); + + double x = lineDirection.getX(); + double y = lineDirection.getY(); + double z = lineDirection.getZ(); + + double angleYaw = Math.atan2(z, x); + double anglePitch = Math.atan2(y, Math.sqrt(x * x + z * z)); + + Vector lowerLeftCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, -FOV_YAW_RAD, -FOV_PITCH_RAD, angleYaw, anglePitch); + Vector upperLeftCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, -FOV_YAW_RAD, FOV_PITCH_RAD, angleYaw, anglePitch); + Vector lowerRightCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, FOV_YAW_RAD, -FOV_PITCH_RAD, angleYaw, anglePitch); + Vector upperRightCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, FOV_YAW_RAD, FOV_PITCH_RAD, angleYaw, anglePitch); + + int width = resolution.getWidth(); + int height = resolution.getHeight(); + List rayMap = new ArrayList<>(width * height); + + Vector leftFraction = upperLeftCorner.clone().subtract(lowerLeftCorner).multiply(1.0 / (height - 1)); + Vector rightFraction = upperRightCorner.clone().subtract(lowerRightCorner).multiply(1.0 / (height - 1)); + + for (int pitch = 0; pitch < height; pitch++) { + + Vector leftPitch = upperLeftCorner.clone().subtract(leftFraction.clone().multiply(pitch)); + Vector rightPitch = upperRightCorner.clone().subtract(rightFraction.clone().multiply(pitch)); + Vector yawFraction = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1)); + + for (int yaw = 0; yaw < width; yaw++) { + Vector ray = leftPitch.clone().add(yawFraction.clone().multiply(yaw)).normalize(); + rayMap.add(ray); + } + } + + return rayMap; + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Renderer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Renderer.java new file mode 100644 index 0000000..2a47ac2 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Renderer.java @@ -0,0 +1,10 @@ +package eu.mhsl.minecraft.pixelpic.render.render; + +import org.bukkit.entity.Player; + +import java.awt.image.BufferedImage; + +public interface Renderer { + + BufferedImage render(Player player, Resolution resolution); +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Resolution.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Resolution.java new file mode 100644 index 0000000..2a32f2d --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/render/Resolution.java @@ -0,0 +1,62 @@ +package eu.mhsl.minecraft.pixelpic.render.render; + +import com.google.common.base.Preconditions; + +import java.util.*; + +public final class Resolution { + + private final int width; + private final int height; + + public Resolution(Pixels pixels, AspectRatio aspectRatio) { + Preconditions.checkNotNull(pixels); + Preconditions.checkNotNull(aspectRatio); + + this.height = pixels.height; + this.width = (int) Math.round(pixels.height * aspectRatio.ratio); + } + + public Resolution(int width, int height) { + Preconditions.checkArgument(width > 0); + Preconditions.checkArgument(height > 0); + + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public enum Pixels { + _128P(128, "128p"), + _256P(256, "256p"); + + private final int height; + private final List aliases; + + Pixels(int height, String... aliases) { + this.height = height; + this.aliases = Collections.unmodifiableList(Arrays.asList(aliases)); + } + } + + public enum AspectRatio { + _1_1(1, "1:1"), + _2_1(2, "2:1"), + _3_2(3 / 2.0, "3:2"); + + private final double ratio; + private final List aliases; + + AspectRatio(double ratio, String... aliases) { + this.ratio = ratio; + this.aliases = Collections.unmodifiableList(Arrays.asList(aliases)); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/BlockRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/BlockRaytracer.java new file mode 100644 index 0000000..fbfe63e --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/BlockRaytracer.java @@ -0,0 +1,48 @@ +package eu.mhsl.minecraft.pixelpic.render.util; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.util.BlockIterator; +import org.bukkit.util.Vector; + +public class BlockRaytracer extends BlockIterator { + + private final Vector position; + private final Vector direction; + + private Block lastBlock; + private BlockFace currentFace; + + public BlockRaytracer(Location loc) { + super(loc); + + this.position = loc.toVector(); + this.direction = loc.getDirection(); + } + + public BlockFace getIntersectionFace() { + if (currentFace == null) { + throw new IllegalStateException("Called before next()"); + } + + return currentFace; + } + + public Vector getIntersectionPoint() { + BlockFace lastFace = getIntersectionFace(); + Vector planeNormal = new Vector(lastFace.getModX(), lastFace.getModY(), lastFace.getModZ()); + Vector planePoint = lastBlock.getLocation().add(0.5, 0.5, 0.5).toVector() + .add(planeNormal.clone().multiply(0.5)); + + return MathUtil.getLinePlaneIntersection(position, direction, planePoint, planeNormal, true); + } + + @Override + public Block next() { + Block currentBlock = super.next(); + currentFace = lastBlock == null ? BlockFace.SELF : currentBlock.getFace(lastBlock); + + return (lastBlock = currentBlock); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/Intersection.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/Intersection.java new file mode 100644 index 0000000..725b8f0 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/Intersection.java @@ -0,0 +1,42 @@ +package eu.mhsl.minecraft.pixelpic.render.util; + +import org.bukkit.util.Vector; + +public final class Intersection { + + private final Vector normal; + private final Vector point; + private final Vector direction; + private final int color; + + private Intersection(Vector normal, Vector point, Vector direction, int color) { + this.normal = normal; + this.point = point; + this.direction = direction; + this.color = color; + } + + public Vector getNormal() { + return normal; + } + + public Vector getPoint() { + return point; + } + + public Vector getDirection() { + return direction; + } + + public int getColor() { + return color; + } + + public static Intersection of(Vector normal, Vector point, Vector direction) { + return of(normal, point, direction, 0); + } + + public static Intersection of(Vector normal, Vector point, Vector direction, int color) { + return new Intersection(normal, point, direction, color); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/MathUtil.java b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/MathUtil.java new file mode 100644 index 0000000..c0ed7d2 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpic/render/util/MathUtil.java @@ -0,0 +1,68 @@ +package eu.mhsl.minecraft.pixelpic.render.util; + +import org.bukkit.Color; +import org.bukkit.block.BlockFace; +import org.bukkit.util.Vector; + +public class MathUtil { + + private MathUtil() {} + + public static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) { + double oldX = base.getX(); + double oldY = base.getY(); + double oldZ = base.getZ(); + + double sinOne = Math.sin(angleYaw); + double sinTwo = Math.sin(anglePitch); + double cosOne = Math.cos(angleYaw); + double cosTwo = Math.cos(anglePitch); + + double newX = oldX * cosOne * cosTwo - oldY * cosOne * sinTwo - oldZ * sinOne; + double newY = oldX * sinTwo + oldY * cosTwo; + double newZ = oldX * sinOne * cosTwo - oldY * sinOne * sinTwo + oldZ * cosOne; + + return new Vector(newX, newY, newZ); + } + + public static Vector doubleYawPitchRotation(Vector base, double firstYaw, double firstPitch, double secondYaw, + double secondPitch) { + return yawPitchRotation(yawPitchRotation(base, firstYaw, firstPitch), secondYaw, secondPitch); + } + + public static Vector reflectVector(Vector linePoint, Vector lineDirection, Vector planePoint, Vector planeNormal) { + return lineDirection.clone().subtract(planeNormal.clone().multiply(2 * lineDirection.dot(planeNormal))); + } + + public static Vector toVector(BlockFace face) { + return new Vector(face.getModX(), face.getModY(), face.getModZ()); + } + + public static int weightedColorSum(int rgbOne, int rgbTwo, double weightOne, double weightTwo) { + Color colorOne = Color.fromRGB(rgbOne & 0xFFFFFF); + Color colorTwo = Color.fromRGB(rgbTwo & 0xFFFFFF); + + double total = weightOne + weightTwo; + int newRed = (int) ((colorOne.getRed() * weightOne + colorTwo.getRed() * weightTwo) / total); + int newGreen = (int) ((colorOne.getGreen() * weightOne + colorTwo.getGreen() * weightTwo) / total); + int newBlue = (int) ((colorOne.getBlue() * weightOne + colorTwo.getBlue() * weightTwo) / total); + + return Color.fromRGB(newRed, newGreen, newBlue).asRGB(); + } + + public static Vector getLinePlaneIntersection(Vector linePoint, Vector lineDirection, Vector planePoint, + Vector planeNormal, boolean allowBackwards) { + double d = planePoint.dot(planeNormal); + double t = (d - planeNormal.dot(linePoint)) / planeNormal.dot(lineDirection); + + if (t < 0 && !allowBackwards) { + return null; + } + + double x = linePoint.getX() + lineDirection.getX() * t; + double y = linePoint.getY() + lineDirection.getY() * t; + double z = linePoint.getZ() + lineDirection.getZ() * t; + + return new Vector(x, y, z); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d7fb166..afd0bad 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,3 +2,6 @@ name: PixelPic version: '1.0-SNAPSHOT' main: eu.mhsl.minecraft.pixelpic.Main api-version: '1.21' +commands: + test: + test2: \ No newline at end of file diff --git a/src/main/resources/terrain.png b/src/main/resources/terrain.png new file mode 100644 index 0000000..d2cf26c Binary files /dev/null and b/src/main/resources/terrain.png differ