diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java index ed9dec8..871cbb1 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java @@ -24,7 +24,6 @@ import eu.mhsl.minecraft.pixelpics.render.snapshot.SnapshotBuilder; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; -import eu.mhsl.minecraft.pixelpics.render.util.MathUtil; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.util.ArrayList; @@ -46,10 +45,11 @@ import org.bukkit.util.Vector; */ public class DefaultScreenRenderer implements Renderer { - /** Horizontal half field-of-view; the vertical half is derived from the output aspect ratio. */ - private static final double H_FOV_HALF_RAD = Math.toRadians(35); - - private static final Vector BASE_VEC = new Vector(1, 0, 0); + /** + * Vertical field of view in degrees (vanilla's default FOV option, which is vertical too); the + * horizontal FOV follows from the image aspect ratio. Square photos: 70° both ways. + */ + public static final double V_FOV_DEG = 70; private static final double MAX_DISTANCE = 256; private static final int REFLECTION_DEPTH = 4; @@ -203,45 +203,36 @@ public class DefaultScreenRenderer implements Renderer { } } - private List buildRayMap(Location eyeLocation, int width, int height) { - Vector lineDirection = eyeLocation.getDirection(); + /** + * Pinhole camera like vanilla's projection: the rays go through a flat image plane one unit in front + * of the eye, {@code ray = forward + u·tan(fovX/2)·right + v·tan(fovY/2)·up} with u, v ∈ [-1, 1] at + * the pixel centres, row 0 = top. The vertical FOV is fixed ({@link #V_FOV_DEG}, like vanilla's + * default 70°), the horizontal one follows from the aspect ratio ({@code tan(fovX/2) = tan(fovY/2)·w/h}), + * so one pixel covers the same angle (at the centre) both ways and squares stay square. Right/up come + * from the yaw/pitch of {@code eye} (no roll), so looking straight up or down keeps the yaw orientation. + */ + public static List buildRayMap(Location eye, int width, int height) { + double yaw = Math.toRadians(eye.getYaw()); + Vector forward = eye.getDirection(); + // Player's right in Minecraft's coordinates (yaw 0 = south/+Z → right = west/−X). + Vector right = new Vector(-Math.cos(yaw), 0, -Math.sin(yaw)); + Vector up = right.getCrossProduct(forward).normalize(); - 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)); - - // Derive the vertical half-FOV from the horizontal one so square output is not distorted. - double yawHalf = H_FOV_HALF_RAD; - double pitchHalf = Math.atan(Math.tan(yawHalf) * ((double) height / width)); - - Vector lowerLeftCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, -yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector upperLeftCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, -yawHalf, pitchHalf, angleYaw, anglePitch); - Vector lowerRightCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector upperRightCorner = MathUtil.doubleYawPitchRotation(BASE_VEC, yawHalf, pitchHalf, angleYaw, anglePitch); + double tanY = Math.tan(Math.toRadians(V_FOV_DEG) / 2); + double tanX = tanY * width / height; 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); + for (int row = 0; row < height; row++) { + double v = (1 - 2 * (row + 0.5) / height) * tanY; + for (int col = 0; col < width; col++) { + double u = (2 * (col + 0.5) / width - 1) * tanX; + rayMap.add(new Vector( + forward.getX() + u * right.getX() + v * up.getX(), + forward.getY() + u * right.getY() + v * up.getY(), + forward.getZ() + u * right.getZ() + v * up.getZ()) + .normalize()); } } - return rayMap; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java index b19a6aa..c7bdd3c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java @@ -7,28 +7,6 @@ public class MathUtil { private MathUtil() {} - private 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); - } - /** Reflects {@code direction} across the plane with the given (unit) {@code normal}. */ public static Vector reflectVector(Vector direction, Vector normal) { return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal))); diff --git a/tools/BlockEntityTestRender.java b/tools/BlockEntityTestRender.java index 61024d1..fd584d1 100644 --- a/tools/BlockEntityTestRender.java +++ b/tools/BlockEntityTestRender.java @@ -6,7 +6,6 @@ import eu.mhsl.minecraft.pixelpics.render.render.*; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; -import eu.mhsl.minecraft.pixelpics.render.util.MathUtil; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.data.BlockData; @@ -25,8 +24,7 @@ import java.util.logging.Logger; public class BlockEntityTestRender { static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - static final double H_FOV_HALF = Math.toRadians(35); - static final Vector BASE = new Vector(1, 0, 0); + static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2); static final int SSAA = 3; static final int TW = 220, TH = 240; @@ -271,7 +269,7 @@ public class BlockEntityTestRender { double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2; double ext = 0.5; for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); - double dist = ext / (2 * Math.tan(H_FOV_HALF)) * 1.4 + 0.6; + double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6; Vector center = new Vector(cx, cy, cz); Vector cam = new Vector(cx - dist, cy + dist * 0.45, cz - dist * 0.35); @@ -300,7 +298,7 @@ public class BlockEntityTestRender { double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2; double ext = 0.5; for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); - double dist = ext / (2 * Math.tan(H_FOV_HALF)) * 1.4 + 0.6; + double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6; // View from the front side (along +facing) so the picture is visible, not the back. DecorationState.Facing f = states.get(0).facing(); double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0; @@ -328,24 +326,6 @@ public class BlockEntityTestRender { } static List buildRayMap(Location eye, int width, int height) { - Vector dir = eye.getDirection(); - double angleYaw = Math.atan2(dir.getZ(), dir.getX()); - double anglePitch = Math.atan2(dir.getY(), Math.sqrt(dir.getX() * dir.getX() + dir.getZ() * dir.getZ())); - double yawHalf = H_FOV_HALF; - double pitchHalf = Math.atan(Math.tan(yawHalf) * ((double) height / width)); - Vector ll = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector ul = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, pitchHalf, angleYaw, anglePitch); - Vector lr = MathUtil.doubleYawPitchRotation(BASE, yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector ur = MathUtil.doubleYawPitchRotation(BASE, yawHalf, pitchHalf, angleYaw, anglePitch); - List rayMap = new ArrayList<>(width * height); - Vector leftFrac = ul.clone().subtract(ll).multiply(1.0 / (height - 1)); - Vector rightFrac = ur.clone().subtract(lr).multiply(1.0 / (height - 1)); - for (int pitch = 0; pitch < height; pitch++) { - Vector leftPitch = ul.clone().subtract(leftFrac.clone().multiply(pitch)); - Vector rightPitch = ur.clone().subtract(rightFrac.clone().multiply(pitch)); - Vector yawFrac = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1)); - for (int yaw = 0; yaw < width; yaw++) rayMap.add(leftPitch.clone().add(yawFrac.clone().multiply(yaw)).normalize()); - } - return rayMap; + return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height); } } diff --git a/tools/EntityTestRender.java b/tools/EntityTestRender.java index f492894..7f6eede 100644 --- a/tools/EntityTestRender.java +++ b/tools/EntityTestRender.java @@ -4,7 +4,6 @@ import eu.mhsl.minecraft.pixelpics.render.render.*; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; -import eu.mhsl.minecraft.pixelpics.render.util.MathUtil; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.data.BlockData; @@ -23,8 +22,7 @@ import java.util.logging.Logger; public class EntityTestRender { static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - static final double H_FOV_HALF = Math.toRadians(35); - static final Vector BASE = new Vector(1, 0, 0); + static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2); static final int SSAA = 3; static final int TW = 200, TH = 230; // per-view tile size @@ -232,7 +230,7 @@ public class EntityTestRender { double ext = 0; for (int a = 0; a < 3; a++) ext = Math.max(ext, re.aabbMax[a] - re.aabbMin[a]); if (ext < 0.5) ext = 0.5; - double dist = ext / (2 * Math.tan(H_FOV_HALF)) * 1.25 + 0.4; + double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.25 + 0.4; Vector center = new Vector(cx, cy, cz); Vector cam = new Vector(cx - dist, cy + dist * 0.42, cz - dist * 0.15); @@ -293,28 +291,7 @@ public class EntityTestRender { return cell; } - // Replicated from DefaultScreenRenderer.buildRayMap. static List buildRayMap(Location eye, int width, int height) { - Vector dir = eye.getDirection(); - double angleYaw = Math.atan2(dir.getZ(), dir.getX()); - double anglePitch = Math.atan2(dir.getY(), Math.sqrt(dir.getX() * dir.getX() + dir.getZ() * dir.getZ())); - double yawHalf = H_FOV_HALF; - double pitchHalf = Math.atan(Math.tan(yawHalf) * ((double) height / width)); - Vector ll = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector ul = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, pitchHalf, angleYaw, anglePitch); - Vector lr = MathUtil.doubleYawPitchRotation(BASE, yawHalf, -pitchHalf, angleYaw, anglePitch); - Vector ur = MathUtil.doubleYawPitchRotation(BASE, yawHalf, pitchHalf, angleYaw, anglePitch); - List rayMap = new ArrayList<>(width * height); - Vector leftFrac = ul.clone().subtract(ll).multiply(1.0 / (height - 1)); - Vector rightFrac = ur.clone().subtract(lr).multiply(1.0 / (height - 1)); - for (int pitch = 0; pitch < height; pitch++) { - Vector leftPitch = ul.clone().subtract(leftFrac.clone().multiply(pitch)); - Vector rightPitch = ur.clone().subtract(rightFrac.clone().multiply(pitch)); - Vector yawFrac = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1)); - for (int yaw = 0; yaw < width; yaw++) { - rayMap.add(leftPitch.clone().add(yawFrac.clone().multiply(yaw)).normalize()); - } - } - return rayMap; + return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height); } } diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index f89681b..d4e5754 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -136,6 +136,15 @@ public class PPTest extends JavaPlugin { scenes.put("frames_side", new Scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6)); scenes.put("frames_dirs_ne", new Scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5)); scenes.put("frames_dirs_sw", new Scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5)); + // Camera projection (pinhole, vertical FOV 70): frontal checkerboard wall / item-frame map (squares must be + // square, same size everywhere), ceiling straight up, floor straight down at yaw 30, oblique wall with + // stripes (edges must stay straight, the vertical stripe through the view centre vertical). *_sq = 384x384. + scenes.put("cam_checker_sq", new Scene("cam_checker_sq", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6)); + scenes.put("cam_checker", new Scene("cam_checker", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6)); + scenes.put("cam_frame_sq", new Scene("cam_frame_sq", this::camFrame, 5.5, 5.5, 3, 5.5, 5.5, 6)); + scenes.put("cam_up_sq", new Scene("cam_up_sq", this::camChecker, 5.5, 6, 0.5, 5.5, 11, 0.5)); + scenes.put("cam_down_sq", new Scene("cam_down_sq", this::camChecker, 5.5, 4, 0.5, 5.4995, 0, 0.500866)); + scenes.put("cam_oblique", new Scene("cam_oblique", this::camStripes, 2, 11, 1.5, 5.5, 5.5, 6)); // Performance (entity BVH): a 15x18 animal farm (270 mobs, randomizeData=false, fixed body yaw) and 300 // item displays from a seeded RNG -> deterministic, pixel-diffable; the render time is logged. scenes.put("perf_farm", new Scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0)); @@ -274,7 +283,7 @@ public class PPTest extends JavaPlugin { Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ); Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector()); eye.setDirection(look); - RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null); + RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null); if (sc.name.equals("dbg")) { try { java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker"); @@ -1253,6 +1262,36 @@ public class PPTest extends JavaPlugin { } } + /** Black/white concrete checkerboard: wall z=6 (x -6..17, y 0..11), ceiling y=11 and floor y=-1. */ + private void camChecker(Location o) { + for (int a = -6; a < 18; a++) + for (int b = 0; b < 12; b++) { + Material m = ((a + b) & 1) == 0 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE; + w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6).setType(m, false); + } + for (int a = -6; a < 18; a++) + for (int c = -12; c < 6; c++) { + Material m = ((a + c) & 1) == 0 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE; + w.getBlockAt(o.getBlockX() + a, o.getBlockY() + 11, o.getBlockZ() + c).setType(m, false); + w.getBlockAt(o.getBlockX() + a, o.getBlockY() - 1, o.getBlockZ() + c).setType(m, false); + } + } + + /** Black wall z=6 with one item frame (synthetic map, 1x1 block) facing north at cell (5,5,5). */ + private void camFrame(Location o) { + for (int a = -6; a < 18; a++) + for (int b = 0; b < 12; b++) w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6).setType(Material.BLACK_CONCRETE, false); + frame(o, 5, 5, 5, BlockFace.NORTH, false, synthMap(), 0, true); + } + + /** Black wall z=6 with a white vertical stripe (x=5) and a white horizontal stripe (y=5). */ + private void camStripes(Location o) { + for (int a = -6; a < 18; a++) + for (int b = 0; b < 12; b++) + w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6) + .setType(a == 5 || b == 5 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE, false); + } + private ItemFrame frame(Location o, int x, int y, int z, BlockFace face, boolean glow, ItemStack item, int rot, boolean visible) { Location l = o.clone().add(x + 0.5, y + 0.5, z + 0.5); Class c = glow ? GlowItemFrame.class : ItemFrame.class;