diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java index 357a168..9bcd9ba 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java @@ -27,9 +27,4 @@ public final class AssetPaths { public static String textureMeta(ResourceLocation id) { return texture(id) + ".mcmeta"; } - - /** {@code assets/minecraft/textures/colormap/.png}. */ - public static String colormap(String name) { - return String.format("assets/%s/textures/colormap/%s.png", ResourceLocation.DEFAULT_NAMESPACE, name.toLowerCase()); - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java index 722d8c1..b8a8f1d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java @@ -19,10 +19,6 @@ public final class AssetReader { this.pack = pack; } - public ResourcePack pack() { - return pack; - } - public Optional readJson(String path, Class type) { return pack.read(path).flatMap(bytes -> { try { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java index ba1dd9e..67f09fd 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java @@ -52,8 +52,7 @@ public final class BlockModelRegistry { List variants = blockStateResolver.resolve(data); List elements = new java.util.ArrayList<>(); - long ar = 0, ag = 0, ab = 0; - int acount = 0; + AverageColor.Accumulator avgColor = new AverageColor.Accumulator(); FlatModel lastFlat = null; for (Variant variant : variants) { @@ -62,17 +61,12 @@ public final class BlockModelRegistry { ModelBaker.BakedGeometry baked = baker.bake(flat, variant); elements.addAll(baked.elements()); if (baked.hasGeometry()) { - int c = baked.averageColor(); - ar += (c >> 16) & 0xFF; - ag += (c >> 8) & 0xFF; - ab += c & 0xFF; - acount++; + avgColor.add(baked.averageColor()); } } if (!elements.isEmpty()) { - int avg = 0xFF000000 | (((int) (ar / acount)) << 16) | (((int) (ag / acount)) << 8) | ((int) (ab / acount)); - return new ResolvedModel(elements, avg, 0, 0, false, true); + return new ResolvedModel(elements, avgColor.average(0xFF7F7F7F), 0, 0, false, true); } int avg = fallbackColor(lastFlat); @@ -172,11 +166,7 @@ public final class BlockModelRegistry { } Face[] faces = new Face[6]; for (Direction d : Direction.values()) { - double[] uv = switch (d) { - case UP, DOWN -> new double[]{0, 0, 1, 1}; - default -> new double[]{0, 0, 1, 1}; - }; - faces[d.ordinal()] = new Face(tex, uv[0], uv[1], uv[2], uv[3], 0, tintIndex); + faces[d.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, tintIndex); } Element cube = new Element(new double[]{0, 0, 0}, new double[]{1, 1, 1}, faces, null, -1, 0, false); int avg = AverageColor.of(tex); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ImageCache.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ImageCache.java new file mode 100644 index 0000000..a580b23 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ImageCache.java @@ -0,0 +1,30 @@ +package eu.mhsl.minecraft.pixelpics.assets; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Thread-safe cache of ARGB pixel grids keyed by {@code K}. Subclasses implement {@link #load} to + * produce the grid (or {@code null} on failure); a failure is cached as a sentinel so it is not + * retried, and {@link #get} returns empty for it. The grid is indexed {@code [y][x]}, top-left origin. + */ +public abstract class ImageCache { + + private static final int[][] MISSING = new int[0][0]; + + private final Map cache = new ConcurrentHashMap<>(); + + /** Loads the pixel grid for {@code key}, or returns {@code null} if it cannot be loaded. */ + protected abstract int[][] load(K key); + + public Optional get(K key) { + int[][] result = cache.computeIfAbsent(key, this::loadOrSentinel); + return result == MISSING ? Optional.empty() : Optional.of(result); + } + + private int[][] loadOrSentinel(K key) { + int[][] loaded = load(key); + return loaded == null ? MISSING : loaded; + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java index 3fa40d1..d5c07a9 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java @@ -36,7 +36,7 @@ public final class ModelBaker { int ySteps = ((variant.y() / 90) % 4 + 4) % 4; List baked = new ArrayList<>(); - long ar = 0, ag = 0, ab = 0, acount = 0; + AverageColor.Accumulator avgColor = new AverageColor.Accumulator(); for (ModelFileDto.ElementDto dto : model.elements()) { if (dto.from == null || dto.to == null) continue; @@ -76,9 +76,9 @@ public final class ModelBaker { faces = rotateFacesX(faces); if (rotAxis >= 0) { rotOrigin = rotatePointX(rotOrigin); - int[] na = rotateAxisX(rotAxis, rotAngle); - rotAxis = na[0]; - rotAngle = na[1] == 0 ? rotAngle : -rotAngle; + AxisRotation na = rotateAxisX(rotAxis); + rotAxis = na.axis(); + if (na.flip()) rotAngle = -rotAngle; } } for (int i = 0; i < ySteps; i++) { @@ -88,9 +88,9 @@ public final class ModelBaker { faces = rotateFacesY(faces); if (rotAxis >= 0) { rotOrigin = rotatePointY(rotOrigin); - int[] na = rotateAxisY(rotAxis, rotAngle); - rotAxis = na[0]; - rotAngle = na[1] == 0 ? rotAngle : -rotAngle; + AxisRotation na = rotateAxisY(rotAxis); + rotAxis = na.axis(); + if (na.flip()) rotAngle = -rotAngle; } } @@ -99,17 +99,11 @@ public final class ModelBaker { // Accumulate average color from the element's face textures. for (Face f : faces) { if (f == null) continue; - int c = AverageColor.of(f.texture); - ar += (c >> 16) & 0xFF; - ag += (c >> 8) & 0xFF; - ab += c & 0xFF; - acount++; + avgColor.add(AverageColor.of(f.texture)); } } - int avg = acount == 0 ? 0xFF7F7F7F - : 0xFF000000 | (((int) (ar / acount)) << 16) | (((int) (ag / acount)) << 8) | ((int) (ab / acount)); - return new BakedGeometry(baked, avg, !baked.isEmpty()); + return new BakedGeometry(baked, avgColor.average(0xFF7F7F7F), !baked.isEmpty()); } private Face buildFace(Direction dir, ModelFileDto.FaceDto dto, double[] from, double[] to, @@ -216,22 +210,24 @@ public final class ModelBaker { } // Rotating an element's own rotation axis under a 90-degree block rotation. - // Returns {newAxisIndex, flipFlag(0/1)}; flip indicates the angle sign should invert. - private int[] rotateAxisY(int axis, double angle) { + // {@code flip} indicates the angle sign should invert. + private record AxisRotation(int axis, boolean flip) {} + + private AxisRotation rotateAxisY(int axis) { // Y rotation maps x<->z; the y axis is unchanged. return switch (axis) { - case 0 -> new int[]{2, 1}; // x -> z - case 2 -> new int[]{0, 0}; // z -> x - default -> new int[]{axis, 0}; + case 0 -> new AxisRotation(2, true); // x -> z + case 2 -> new AxisRotation(0, false); // z -> x + default -> new AxisRotation(axis, false); }; } - private int[] rotateAxisX(int axis, double angle) { + private AxisRotation rotateAxisX(int axis) { // X rotation maps y<->z; the x axis is unchanged. return switch (axis) { - case 1 -> new int[]{2, 1}; // y -> z - case 2 -> new int[]{1, 0}; // z -> y - default -> new int[]{axis, 0}; + case 1 -> new AxisRotation(2, true); // y -> z + case 2 -> new AxisRotation(1, false); // z -> y + default -> new AxisRotation(axis, false); }; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java index efbefec..63d33b7 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java @@ -6,27 +6,23 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; import java.net.URLConnection; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; /** * Downloads and caches player skin textures (by URL) as ARGB pixel grids. Legacy 64x32 skins are * converted to the modern 64x64 layout (so the model's overlay/second-layer bones map correctly). * Downloads happen off the main thread (from the entity baking step) and are cached. */ -public final class SkinCache { - - private final Map cache = new ConcurrentHashMap<>(); - private static final int[][] FAILED = new int[0][0]; +public final class SkinCache extends ImageCache { + @Override public Optional get(String url) { if (url == null || url.isEmpty()) return Optional.empty(); - int[][] result = cache.computeIfAbsent(url, this::download); - return result == FAILED ? Optional.empty() : Optional.of(result); + return super.get(url); } - private int[][] download(String url) { + @Override + protected int[][] load(String url) { try { URL u = URI.create(url).toURL(); URLConnection conn = u.openConnection(); @@ -37,10 +33,10 @@ public final class SkinCache { try (InputStream in = conn.getInputStream()) { img = ImageIO.read(in); } - if (img == null) return FAILED; + if (img == null) return null; return toModern(img); } catch (Exception e) { - return FAILED; + return null; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java index 7dea003..d0c3848 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java @@ -3,9 +3,6 @@ package eu.mhsl.minecraft.pixelpics.assets; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; /** * Loads and caches block textures as raw ARGB pixel grids. Textures are stored unflipped (vanilla @@ -16,35 +13,25 @@ import java.util.concurrent.ConcurrentHashMap; * without one (e.g. a 64×128 entity texture like the witch/strider) is a real texture, not an animation, * and must be loaded in full. */ -public final class TextureCache { +public final class TextureCache extends ImageCache { private final ResourcePack pack; - private final Map cache = new ConcurrentHashMap<>(); - private static final int[][] MISSING = new int[0][0]; public TextureCache(ResourcePack pack) { this.pack = pack; } - /** - * Returns the texture pixels for the given id, or empty if it cannot be loaded. - * The grid is indexed {@code [y][x]} with {@code [0][0]} at the top-left. - */ - public Optional get(ResourceLocation textureId) { - int[][] result = cache.computeIfAbsent(textureId, this::load); - return result == MISSING ? Optional.empty() : Optional.of(result); - } - - private int[][] load(ResourceLocation id) { - Optional bytes = pack.read(AssetPaths.texture(id)); - if (bytes.isEmpty()) return MISSING; + @Override + protected int[][] load(ResourceLocation id) { + var bytes = pack.read(AssetPaths.texture(id)); + if (bytes.isEmpty()) return null; BufferedImage img; try { img = ImageIO.read(new ByteArrayInputStream(bytes.get())); } catch (Exception e) { - return MISSING; + return null; } - if (img == null) return MISSING; + if (img == null) return null; int width = img.getWidth(); int height = img.getHeight(); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/AverageColor.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/AverageColor.java index 64b17c8..8f5e982 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/AverageColor.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/AverageColor.java @@ -25,4 +25,23 @@ public final class AverageColor { if (count == 0) return 0xFF7F7F7F; return ColorUtil.argb(0xFF, (int) (r / count), (int) (g / count), (int) (b / count)); } + + /** Mutable accumulator that averages a set of already-opaque ARGB colors (e.g. per-face/per-variant). */ + public static final class Accumulator { + private long r, g, b; + private int count; + + public void add(int argb) { + r += ColorUtil.red(argb); + g += ColorUtil.green(argb); + b += ColorUtil.blue(argb); + count++; + } + + /** Opaque average of the added colors, or {@code fallback} if none were added. */ + public int average(int fallback) { + if (count == 0) return fallback; + return ColorUtil.argb(0xFF, (int) (r / count), (int) (g / count), (int) (b / count)); + } + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java index 22ab7f3..ac3cb0c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java @@ -13,7 +13,7 @@ import java.util.List; * not CEM models but simple textured quads, so the geometry is built directly from the captured world * bounding box and the front-facing direction. Added to the {@link EntityScene} like any other entity. */ -public final class DecorationBaker { +public final class DecorationBaker implements EntityBaker { private static final double MIN_THICKNESS = 0.0625; // 1px, so the slab test never degenerates @@ -23,6 +23,7 @@ public final class DecorationBaker { this.textures = textures; } + @Override public RenderedEntity bake(DecorationState s) { return s.kind() == DecorationState.Kind.PAINTING ? bakePainting(s) : bakeItemFrame(s); } @@ -41,7 +42,7 @@ public final class DecorationBaker { faces[front.ordinal()] = frontFace(art, s.facing()); faces[opposite(front).ordinal()] = new Face(back, 0, 0, 1, 1, 0, -1); EntityCube cube = new EntityCube(from, to, faces, Affine.identity()); - return scene(List.of(cube)); + return RenderedEntity.of(List.of(cube)); } /** @@ -78,7 +79,7 @@ public final class DecorationBaker { cubes.add(new EntityCube(px(-4, -4, 2), px(4, 4, 3), f, toWorld)); } } - return scene(cubes); + return RenderedEntity.of(cubes); } /** A local-space corner in model pixels (1/16 block); z is the outward (front) offset from the wall. */ @@ -153,16 +154,4 @@ public final class DecorationBaker { case DOWN -> Direction.UP; }; } - - private static RenderedEntity scene(List cubes) { - double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; - double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; - for (EntityCube c : cubes) { - for (int a = 0; a < 3; a++) { - if (c.aabbMin[a] < min[a]) min[a] = c.aabbMin[a]; - if (c.aabbMax[a] > max[a]) max[a] = c.aabbMax[a]; - } - } - return new RenderedEntity(cubes, min, max); - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityBaker.java new file mode 100644 index 0000000..dd641e5 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityBaker.java @@ -0,0 +1,10 @@ +package eu.mhsl.minecraft.pixelpics.render.entity; + +/** + * Bakes a captured state {@code S} (mob, block-entity or decoration) into a {@link RenderedEntity}, + * or returns {@code null} when it has no renderable geometry. Lets {@link EntityScene} treat all three + * render paths uniformly. + */ +public interface EntityBaker { + RenderedEntity bake(S state); +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java index e2a4f68..d2dc56e 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java @@ -18,30 +18,22 @@ public final class EntityScene { private static final double EPS = 1e-7; private final List entities; - public EntityScene(List states, CemBaker baker) { - this(states, baker, List.of(), null, List.of(), null); - } - public EntityScene(List states, CemBaker baker, List blockEntities, BlockEntityBaker beBaker, List decorations, DecorationBaker decoBaker) { this.entities = new ArrayList<>(states.size() + blockEntities.size() + decorations.size()); - for (EntityState s : states) { + addAll(states, baker); + addAll(blockEntities, beBaker); + addAll(decorations, decoBaker); + } + + /** Bakes each state and keeps the ones with geometry. A null baker (path disabled) is skipped. */ + private void addAll(List states, EntityBaker baker) { + if (baker == null) return; + for (S s : states) { RenderedEntity e = baker.bake(s); if (e != null && !e.cubes.isEmpty()) entities.add(e); } - if (beBaker != null) { - for (BlockEntityState s : blockEntities) { - RenderedEntity e = beBaker.bake(s); - if (e != null && !e.cubes.isEmpty()) entities.add(e); - } - } - if (decoBaker != null) { - for (DecorationState s : decorations) { - RenderedEntity e = decoBaker.bake(s); - if (e != null && !e.cubes.isEmpty()) entities.add(e); - } - } } public boolean isEmpty() { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java index f205848..c695942 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java @@ -7,16 +7,11 @@ package eu.mhsl.minecraft.pixelpics.render.entity; public record EntityState( String typeKey, // e.g. "cow", "zombie", "player" double x, double y, double z, - float bodyYaw, float headYaw, float pitch, - double vx, double vy, double vz, + float bodyYaw, boolean baby, double width, double height, boolean player, String skinUrl, boolean slim, String variant, // texture-selecting variant key (e.g. "ashen", "warm", "tabby"), or null int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none double sizeScale // extra model scale (slime/magma-cube size); 1.0 = default -) { - public double horizontalSpeed() { - return Math.sqrt(vx * vx + vz * vz); - } -} +) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java index eb1c39b..2d1ca81 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java @@ -10,29 +10,19 @@ public final class ModelCube { public final double inflate; // px, expands the box on all sides (overlay layers) public final double[] uv; // 2, box-UV offset (texels) public final boolean mirror; - public final double[] rotation; // 3 (degrees), per-cube rotation around pivot - public final double[] pivot; // 3 (px), per-cube rotation pivot /** Optional modern per-face UV, indexed by {@link Direction#ordinal()}: {u, v, w, h} texels (h/w may be negative for flips). Null = use box-UV. */ public final double[][] faceUv; - public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror, - double[] rotation, double[] pivot) { - this(origin, size, inflate, uv, mirror, rotation, pivot, null); + public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror) { + this(origin, size, inflate, uv, mirror, null); } - public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror, - double[] rotation, double[] pivot, double[][] faceUv) { + public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror, double[][] faceUv) { this.origin = origin; this.size = size; this.inflate = inflate; this.uv = uv; this.mirror = mirror; - this.rotation = rotation; - this.pivot = pivot; this.faceUv = faceUv; } - - public boolean hasRotation() { - return rotation[0] != 0 || rotation[1] != 0 || rotation[2] != 0; - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/RenderedEntity.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/RenderedEntity.java index 086b842..5b4b825 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/RenderedEntity.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/RenderedEntity.java @@ -13,4 +13,17 @@ public final class RenderedEntity { this.aabbMin = aabbMin; this.aabbMax = aabbMax; } + + /** Wraps baked world-space cubes, computing the overall broad-phase AABB from their per-cube AABBs. */ + public static RenderedEntity of(List cubes) { + double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; + double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; + for (EntityCube c : cubes) { + for (int a = 0; a < 3; a++) { + if (c.aabbMin[a] < min[a]) min[a] = c.aabbMin[a]; + if (c.aabbMax[a] > max[a]) max[a] = c.aabbMax[a]; + } + } + return new RenderedEntity(cubes, min, max); + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java new file mode 100644 index 0000000..cc70593 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java @@ -0,0 +1,55 @@ +package eu.mhsl.minecraft.pixelpics.render.entity; + +/** + * In-place ARGB pixel-grid compositing shared by the entity/block-entity bakers: deep copy, tint and + * src-over alpha overlay. Grids are indexed {@code [y][x]}; all operations assume the ARGB layout used + * throughout the renderer. + */ +public final class TextureOps { + + private TextureOps() {} + + /** A row-by-row copy so callers can tint/overlay without mutating the cached source texture. */ + public static int[][] deepCopy(int[][] src) { + int[][] out = new int[src.length][]; + for (int y = 0; y < src.length; y++) out[y] = src[y].clone(); + return out; + } + + /** Multiplies every non-transparent texel by an ARGB tint (RGB channels only), in place. */ + public static void tint(int[][] tex, int argb) { + int tr = (argb >> 16) & 0xFF, tg = (argb >> 8) & 0xFF, tb = argb & 0xFF; + for (int[] row : tex) { + for (int x = 0; x < row.length; x++) { + int p = row[x]; + int a = (p >>> 24) & 0xFF; + if (a == 0) continue; + int r = ((p >> 16) & 0xFF) * tr / 255, g = ((p >> 8) & 0xFF) * tg / 255, b = (p & 0xFF) * tb / 255; + row[x] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + } + + /** Standard src-over alpha composite of {@code src} onto {@code dst} (clipped to the overlap), in place. */ + public static void overlay(int[][] dst, int[][] src) { + int h = Math.min(dst.length, src.length); + for (int y = 0; y < h; y++) { + int w = Math.min(dst[y].length, src[y].length); + for (int x = 0; x < w; x++) { + int sp = src[y][x]; + int sa = (sp >>> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { dst[y][x] = sp; continue; } + int dp = dst[y][x]; + int da = (dp >>> 24) & 0xFF; + int outA = sa + da * (255 - sa) / 255; + int sr = (sp >> 16) & 0xFF, sg = (sp >> 8) & 0xFF, sb = sp & 0xFF; + int dr = (dp >> 16) & 0xFF, dg = (dp >> 8) & 0xFF, db = dp & 0xFF; + int r = (sr * sa + dr * da * (255 - sa) / 255) / Math.max(1, outA); + int g = (sg * sa + dg * da * (255 - sa) / 255) / Math.max(1, outA); + int b = (sb * sa + db * da * (255 - sa) / 255) / Math.max(1, outA); + dst[y][x] = (outA << 24) | (r << 16) | (g << 8) | b; + } + } + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java index 925791b..6a1f03a 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java @@ -6,8 +6,10 @@ import eu.mhsl.minecraft.pixelpics.assets.TextureCache; import eu.mhsl.minecraft.pixelpics.render.entity.Affine; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityModels; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState; +import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker; import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube; import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity; +import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps; import java.util.ArrayList; import java.util.List; @@ -23,7 +25,7 @@ import java.util.Set; * block scale), so the placement is {@code T(cell centre) · rotY(yaw) · T(localOffset)} and most types * only need defaults. Wall-mounted and scaled types (signs, wall heads, beds) override via {@link Place}. */ -public final class BlockEntityBaker { +public final class BlockEntityBaker implements EntityBaker { private static final double SIGN_SCALE = 0.6666667; // vanilla SignRenderer model scale @@ -38,6 +40,7 @@ public final class BlockEntityBaker { } /** Returns the baked block-entity, or null when it has no model/texture (then nothing renders). */ + @Override public RenderedEntity bake(BlockEntityState s) { List layers = layers(s); if (layers.isEmpty()) return null; @@ -58,7 +61,7 @@ public final class BlockEntityBaker { cubes.add(new EntityCube(b.from(), b.to(), b.faces(), placement.mul(b.world()))); } } - return cubes.isEmpty() ? null : CemGeometry.finish(cubes); + return cubes.isEmpty() ? null : RenderedEntity.of(cubes); } /** The CEM model name; for heads it depends on the texture's aspect (64x32 vs square 64x64). */ @@ -193,44 +196,15 @@ public final class BlockEntityBaker { * alpha-overlay each pattern mask ({@code entity/banner/}) dyed with its own colour, in order. */ private int[][] bakeBanner(int[][] base, BlockEntityState s) { - int[][] out = deepCopy(base); - if (s.baseColorArgb() != 0) CemGeometry.tint(out, s.baseColorArgb()); + int[][] out = TextureOps.deepCopy(base); + if (s.baseColorArgb() != 0) TextureOps.tint(out, s.baseColorArgb()); for (BlockEntityState.BannerPattern pat : s.patterns()) { int[][] mask = textures.get(ResourceLocation.parse("entity/banner/" + pat.patternKey())).orElse(null); if (mask == null) continue; - int[][] dyed = deepCopy(mask); - CemGeometry.tint(dyed, pat.colorArgb()); - overlay(out, dyed); + int[][] dyed = TextureOps.deepCopy(mask); + TextureOps.tint(dyed, pat.colorArgb()); + TextureOps.overlay(out, dyed); } return out; } - - /** Standard src-over alpha composite of {@code src} onto {@code dst} (same dimensions), in place. */ - private static void overlay(int[][] dst, int[][] src) { - int h = Math.min(dst.length, src.length); - for (int y = 0; y < h; y++) { - int w = Math.min(dst[y].length, src[y].length); - for (int x = 0; x < w; x++) { - int sp = src[y][x]; - int sa = (sp >>> 24) & 0xFF; - if (sa == 0) continue; - if (sa == 255) { dst[y][x] = sp; continue; } - int dp = dst[y][x]; - int da = (dp >>> 24) & 0xFF; - int outA = sa + da * (255 - sa) / 255; - int sr = (sp >> 16) & 0xFF, sg = (sp >> 8) & 0xFF, sb = sp & 0xFF; - int dr = (dp >> 16) & 0xFF, dg = (dp >> 8) & 0xFF, db = dp & 0xFF; - int r = (sr * sa + dr * da * (255 - sa) / 255) / Math.max(1, outA); - int g = (sg * sa + dg * da * (255 - sa) / 255) / Math.max(1, outA); - int b = (sb * sa + db * da * (255 - sa) / 255) / Math.max(1, outA); - dst[y][x] = (outA << 24) | (r << 16) | (g << 8) | b; - } - } - } - - private static int[][] deepCopy(int[][] src) { - int[][] out = new int[src.length][]; - for (int y = 0; y < src.length; y++) out[y] = src[y].clone(); - return out; - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java index 0e5d102..52ea9eb 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java @@ -6,11 +6,13 @@ import eu.mhsl.minecraft.pixelpics.assets.TextureCache; import eu.mhsl.minecraft.pixelpics.assets.model.Face; import eu.mhsl.minecraft.pixelpics.render.entity.Affine; import eu.mhsl.minecraft.pixelpics.render.entity.BoxUv; +import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker; import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube; import eu.mhsl.minecraft.pixelpics.render.entity.EntityModels; import eu.mhsl.minecraft.pixelpics.render.entity.EntityState; import eu.mhsl.minecraft.pixelpics.render.entity.ModelCube; import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity; +import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps; import java.util.ArrayList; import java.util.List; @@ -22,7 +24,7 @@ import java.util.List; * + px→block scale + an outer Y-flip (upright); the model is then dropped onto the ground and placed at * the entity's position/yaw. Calibrated against fox/pig/cow. */ -public final class CemBaker { +public final class CemBaker implements EntityBaker { // Parts representing an alternate state (rolled-up, sleeping, …) that must not render in the idle pose. private static final java.util.Map> HIDDEN_PARTS = java.util.Map.of( @@ -40,9 +42,11 @@ public final class CemBaker { this.skins = skins; } + @Override public RenderedEntity bake(EntityState s) { int[][] tex = resolveTexture(s); - CemModelLoader.CemModel model = models.get(EntityModels.cemModel(s.typeKey())); + String cem = EntityModels.cemModel(s.typeKey()); + CemModelLoader.CemModel model = models.get(cem); if (model == null || tex == null) return fallbackBox(s, tex); double sc = (s.baby() ? 0.5 : 1.0) * s.sizeScale(); @@ -50,24 +54,23 @@ public final class CemBaker { // rotations and handedness; only px->block scaling is applied. Affine pre = Affine.scale(sc / 16.0); - java.util.Set hidden = HIDDEN_PARTS.getOrDefault(EntityModels.cemModel(s.typeKey()), java.util.Set.of()); + java.util.Set hidden = HIDDEN_PARTS.getOrDefault(cem, java.util.Set.of()); List baked = new ArrayList<>(CemGeometry.bakeModel(model, tex, pre, hidden)); // Sheep: render the inflated, dye-tinted wool fur layer over the body (transparent where the face shows). if (s.typeKey().equals("sheep")) { CemModelLoader.CemModel wool = models.get("sheep_wool"); int[][] woolTex = textures.get(ResourceLocation.parse("entity/sheep/sheep_wool")).orElse(null); if (wool != null && woolTex != null) { - int[][] t = new int[woolTex.length][]; - for (int y = 0; y < woolTex.length; y++) t[y] = woolTex[y].clone(); - if (s.tint() != 0) CemGeometry.tint(t, s.tint()); + int[][] t = TextureOps.deepCopy(woolTex); + if (s.tint() != 0) TextureOps.tint(t, s.tint()); baked.addAll(CemGeometry.bakeModel(wool, t, pre, hidden)); } } // Guardian: the CEM model ships a RIGHT body side-panel but no left one, and the main body box's // left face is transparent in the texture → a see-through hole on the left. Add the mirrored left panel. - if (EntityModels.cemModel(s.typeKey()).equals("guardian")) { + if (cem.equals("guardian")) { double[] org = {-8, 2, -6}, size = {2, 12, 12}; - ModelCube mc = new ModelCube(org, size, 0, new double[]{0, 28}, true, new double[]{0,0,0}, new double[]{0,0,0}); + ModelCube mc = new ModelCube(org, size, 0, new double[]{0, 28}, true); Face[] faces = BoxUv.build(mc, tex, model.texW(), model.texH()); baked.add(new CemGeometry.Baked(org, new double[]{org[0]+size[0], org[1]+size[1], org[2]+size[2]}, faces, pre)); } @@ -82,7 +85,7 @@ public final class CemBaker { List cubes = new ArrayList<>(baked.size()); for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()))); - return CemGeometry.finish(cubes); + return RenderedEntity.of(cubes); } // --- texture resolution (player skin, dyed sheep wool, variant candidates) --- @@ -107,14 +110,14 @@ public final class CemBaker { double[] to = {w / 2, h, w / 2}; int[][] t = tex != null ? tex : flat(0xFF8C8C8C); ModelCube box = new ModelCube(new double[]{-w/2, 0, -w/2}, new double[]{w, h, w}, 0, - new double[]{0, 0}, false, new double[]{0, 0, 0}, new double[]{0, 0, 0}); + new double[]{0, 0}, false); Face[] faces = BoxUv.build(box, t, Math.max(64, (int) (2 * (w + w))), Math.max(64, (int) (2 * (w + h)))); Affine place = Affine.translation(s.x(), s.y(), s.z()) .mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw()))) .mul(Affine.scale(1.0 / 16.0)); List cubes = new ArrayList<>(); cubes.add(new EntityCube(from, to, faces, place)); - return CemGeometry.finish(cubes); + return RenderedEntity.of(cubes); } private static int[][] flat(int argb) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java index 06dd9a7..8f2d163 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java @@ -3,9 +3,7 @@ package eu.mhsl.minecraft.pixelpics.render.entity.cem; import eu.mhsl.minecraft.pixelpics.assets.model.Face; import eu.mhsl.minecraft.pixelpics.render.entity.Affine; import eu.mhsl.minecraft.pixelpics.render.entity.BoxUv; -import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube; import eu.mhsl.minecraft.pixelpics.render.entity.ModelCube; -import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity; import java.util.ArrayList; import java.util.List; @@ -37,24 +35,15 @@ final class CemGeometry { /** Bake all parts of a model with the given pre-transform; parts in {@code hidden} are skipped. */ static List bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set hidden) { - return bakeModel(model, tex, pre, hidden, 0, 0); + return bakeModel(model, tex, pre, hidden, 0, 0, false); } /** - * As {@link #bakeModel(CemModelLoader.CemModel, int[][], Affine, Set)} but with an explicit texture - * size for UV normalisation (use when the applied texture's size differs from the model's declared - * {@code textureSize} in a non-proportional way, e.g. a 16x16 sherd on a 32x32-authored pot face). - * {@code texW}/{@code texH} of 0 fall back to the model's declared size. - */ - static List bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set hidden, - int texW, int texH) { - return bakeModel(model, tex, pre, hidden, texW, texH, false); - } - - /** - * As above, but {@code ignoreFaceUv} forces box-UV even when the model declares per-face UV — used when - * applying a standalone texture (e.g. the conduit cage's own {@code cage.png}) whose layout is box-UV, - * not the combined-sheet layout the model's per-face UV assumes. + * Bake all parts with an explicit texture size for UV normalisation ({@code texW}/{@code texH} of 0 + * fall back to the model's declared size; use a real size when the applied texture differs from the + * model's declared {@code textureSize}, e.g. a 16x16 sherd on a 32x32-authored pot face). When + * {@code ignoreFaceUv} is set, box-UV is forced even if the model declares per-face UV — used for a + * standalone texture (e.g. the conduit cage) whose layout is box-UV, not the combined-sheet layout. */ static List bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set hidden, int texW, int texH, boolean ignoreFaceUv) { @@ -91,7 +80,7 @@ final class CemGeometry { double[] from = {org[0] - inf, org[1] - inf, org[2] - inf}; double[] to = {org[0] + b.size()[0] + inf, org[1] + b.size()[1] + inf, org[2] + b.size()[2] + inf}; double[][] faceUv = ignoreFaceUv ? null : b.faceUv(); - ModelCube mc = new ModelCube(org, b.size(), inf, b.uv(), b.mirror(), new double[]{0, 0, 0}, new double[]{0, 0, 0}, faceUv); + ModelCube mc = new ModelCube(org, b.size(), inf, b.uv(), b.mirror(), faceUv); Face[] faces = BoxUv.build(mc, tex, texW, texH); out.add(new Baked(from, to, faces, world)); } @@ -103,30 +92,4 @@ final class CemGeometry { } } - /** Multiplies every non-transparent texel by an ARGB tint (in place). */ - static void tint(int[][] tex, int argb) { - int tr = (argb >> 16) & 0xFF, tg = (argb >> 8) & 0xFF, tb = argb & 0xFF; - for (int[] row : tex) { - for (int x = 0; x < row.length; x++) { - int p = row[x]; - int a = (p >>> 24) & 0xFF; - if (a == 0) continue; - int r = ((p >> 16) & 0xFF) * tr / 255, g = ((p >> 8) & 0xFF) * tg / 255, b = (p & 0xFF) * tb / 255; - row[x] = (a << 24) | (r << 16) | (g << 8) | b; - } - } - } - - /** Wraps baked world-space cubes into a {@link RenderedEntity}, computing the overall AABB. */ - static RenderedEntity finish(List cubes) { - double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; - double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; - for (EntityCube c : cubes) { - for (int a = 0; a < 3; a++) { - if (c.aabbMin[a] < min[a]) min[a] = c.aabbMin[a]; - if (c.aabbMax[a] > max[a]) max[a] = c.aabbMax[a]; - } - } - return new RenderedEntity(cubes, min, max); - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java index 6b5a381..3d3ec4e 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java @@ -95,7 +95,7 @@ public final class SnapshotRaytracer { int color = shadeAndTint(hit, data, snapshot, bx, by, bz); if (!reflected && model.reflection > 0 && depth > 0) { - Vector reflectDir = MathUtil.reflectVector(origin, direction, hit.point(), hit.normal()); + Vector reflectDir = MathUtil.reflectVector(direction, hit.normal()); Vector reflectStart = hit.point().clone().add(hit.normal().clone().multiply(1e-3)); reflectionColor = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1); reflectionFactor = model.reflection; @@ -143,7 +143,7 @@ public final class SnapshotRaytracer { } if (transparencyStart != null) { - baseColor = MathUtil.weightedColorSum( + baseColor = ColorUtil.mix( baseColor, transparencyColor, transparencyFactor, @@ -151,13 +151,13 @@ public final class SnapshotRaytracer { * (1 + transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint) / 5.0)); } if (reflected) { - baseColor = MathUtil.weightedColorSum(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor); + baseColor = ColorUtil.mix(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor); } // Distance fog (atmospheric perspective): fade distant geometry toward the sky color. if (finalPoint != null) { double fog = fogFactor(origin.distance(finalPoint)); - if (fog > 0) baseColor = MathUtil.weightedColorSum(baseColor, skyColor, 1 - fog, fog); + if (fog > 0) baseColor = ColorUtil.mix(baseColor, skyColor, 1 - fog, fog); } return baseColor & 0xFFFFFF; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java index da6242f..c8c7acf 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java @@ -2,19 +2,15 @@ package eu.mhsl.minecraft.pixelpics.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); + this( + (int) Math.round(Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio), + pixels.height); } public Resolution(int width, int height) { @@ -34,29 +30,25 @@ public final class Resolution { } public enum Pixels { - _128P(128, "128p"), - _256P(256, "256p"); + _128P(128), + _256P(256); private final int height; - private final List aliases; - Pixels(int height, String... aliases) { + Pixels(int height) { 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"); + _1_1(1), + _2_1(2), + _3_2(3 / 2.0); private final double ratio; - private final List aliases; - AspectRatio(double ratio, String... aliases) { + AspectRatio(double ratio) { 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/pixelpics/render/sky/SkyRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java index b0fbd90..fd0ce29 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java @@ -54,16 +54,16 @@ public final class SkyRenderer { double dayFactor = smoothstep(-0.20, 0.25, sunY); // Base vertical gradient, blended day<->night. - double up = clamp01(dy); + double up = Math.clamp(dy, 0, 1); int dayColor = lerp(DAY_HORIZON, DAY_ZENITH, up); int nightColor = lerp(NIGHT_HORIZON, NIGHT_ZENITH, up); int color = lerp(nightColor, dayColor, dayFactor); // Sunrise/sunset: a full-sky warm wash (orange at the horizon -> red -> purple at the zenith), // strongest while the sun is near the horizon and warmer toward its azimuth. Matches vanilla. - double twilight = clamp01(1 - Math.abs(sunY) / 0.45); + double twilight = Math.clamp(1 - Math.abs(sunY) / 0.45, 0, 1); if (twilight > 0) { - double az = clamp01(dx * Math.signum(sunX) * 0.5 + 0.5); // 1 toward sun .. 0 away + double az = Math.clamp(dx * Math.signum(sunX) * 0.5 + 0.5, 0, 1); // 1 toward sun .. 0 away int grad = up < 0.40 ? lerp(SUNSET_ORANGE, SUNSET_RED, up / 0.40) : lerp(SUNSET_RED, TWI_PURPLE, (up - 0.40) / 0.60); @@ -87,7 +87,7 @@ public final class SkyRenderer { if (sunY > -0.20) { double cosSun = dx * sunX + dy * sunY; if (cosSun > 0) { - double bloom = Math.pow(clamp01(cosSun), 16) * clamp01(sunY + 0.3); + double bloom = Math.pow(Math.clamp(cosSun, 0, 1), 16) * Math.clamp(sunY + 0.3, 0, 1); color = lerp(color, rgb(255, 235, 190), bloom * 0.7); } } @@ -112,7 +112,7 @@ public final class SkyRenderer { if (coverage > 0) { int cloudColor = lerp(rgb(45, 48, 60), rgb(236, 240, 248), dayFactor); if (twilight > 0) cloudColor = lerp(cloudColor, rgb(150, 95, 85), twilight * 0.45); - double fade = clamp01((dy - 0.02) * 4); // fade out near the horizon (single-plane sampling) + double fade = Math.clamp((dy - 0.02) * 4, 0, 1); // fade out near the horizon (single-plane sampling) color = lerp(color, cloudColor, coverage * fade); } } @@ -233,13 +233,8 @@ public final class SkyRenderer { private static int rgb(int r, int g, int b) { return (r << 16) | (g << 8) | b; } private static int lerp(int a, int b, double t) { - t = clamp01(t); - int ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF; - int br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF; - int r = (int) (ar + (br - ar) * t); - int g = (int) (ag + (bg - ag) * t); - int bl = (int) (ab + (bb - ab) * t); - return rgb(r, g, bl); + t = Math.clamp(t, 0, 1); + return ColorUtil.mix(a, b, 1 - t, t); } private static int add(int c, int r, int g, int b) { @@ -261,11 +256,9 @@ public final class SkyRenderer { } private static double smoothstep(double edge0, double edge1, double x) { - double t = clamp01((x - edge0) / (edge1 - edge0)); + double t = Math.clamp((x - edge0) / (edge1 - edge0), 0, 1); return t * t * (3 - 2 * t); } - private static double clamp01(double v) { return v < 0 ? 0 : Math.min(v, 1); } - private static int clamp(int v, int lo, int hi) { return v < lo ? lo : Math.min(v, hi); } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java index 9df7aa0..a83b48f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java @@ -6,7 +6,7 @@ import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BedPart; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BellAttach; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.Kind; -import org.bukkit.Color; +import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.DyeColor; import org.bukkit.Location; import org.bukkit.Material; @@ -39,21 +39,11 @@ public final class BlockEntitySnapshotBuilder { public static List build(Location eye, List rayMap, double maxDistance) { World world = eye.getWorld(); - Vector o = eye.toVector(); - double minX = o.getX(), minY = o.getY(), minZ = o.getZ(); - double maxX = o.getX(), maxY = o.getY(), maxZ = o.getZ(); - for (Vector ray : rayMap) { - minX = Math.min(minX, o.getX() + ray.getX() * maxDistance); - maxX = Math.max(maxX, o.getX() + ray.getX() * maxDistance); - minY = Math.min(minY, o.getY() + ray.getY() * maxDistance); - maxY = Math.max(maxY, o.getY() + ray.getY() * maxDistance); - minZ = Math.min(minZ, o.getZ() + ray.getZ() * maxDistance); - maxZ = Math.max(maxZ, o.getZ() + ray.getZ() * maxDistance); - } + FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance); // 1-block margin so block-entities straddling the frustum edge are still captured. - int bMinX = (int) Math.floor(minX) - 1, bMaxX = (int) Math.ceil(maxX) + 1; - int bMinY = (int) Math.floor(minY) - 1, bMaxY = (int) Math.ceil(maxY) + 1; - int bMinZ = (int) Math.floor(minZ) - 1, bMaxZ = (int) Math.ceil(maxZ) + 1; + int bMinX = (int) Math.floor(bounds.minX) - 1, bMaxX = (int) Math.ceil(bounds.maxX) + 1; + int bMinY = (int) Math.floor(bounds.minY) - 1, bMaxY = (int) Math.ceil(bounds.maxY) + 1; + int bMinZ = (int) Math.floor(bounds.minZ) - 1, bMaxZ = (int) Math.ceil(bounds.maxZ) + 1; int minCX = bMinX >> 4, maxCX = bMaxX >> 4, minCZ = bMinZ >> 4, maxCZ = bMaxZ >> 4; @@ -117,11 +107,11 @@ public final class BlockEntitySnapshotBuilder { Builder b = base(wall ? Kind.WALL_BANNER : Kind.BANNER, bx, by, bz, yaw); if (ts instanceof Banner banner) { DyeColor base = banner.getBaseColor(); - b.baseColorArgb(dyeArgb(base)); + b.baseColorArgb(ColorUtil.dyeArgb(base, 0xFFFFFFFF)); List pats = new ArrayList<>(); for (Pattern p : banner.getPatterns()) { String key = p.getPattern().key().value(); - pats.add(new BannerPattern(key, dyeArgb(p.getColor()))); + pats.add(new BannerPattern(key, ColorUtil.dyeArgb(p.getColor(), 0xFFFFFFFF))); } b.patterns(pats); } @@ -185,16 +175,17 @@ public final class BlockEntitySnapshotBuilder { // --- facing / rotation helpers --- + /** Yaw preferring the block's {@link Directional} facing (wall-mounted block-entities). */ private static float facingYaw(BlockData data) { if (data instanceof Directional d) return faceToYaw(d.getFacing()); if (data instanceof Rotatable r) return faceToYaw(r.getRotation()); return 0; } + /** Yaw preferring the block's {@link Rotatable} rotation (free-standing block-entities). */ private static float rotationYaw(BlockData data) { if (data instanceof Rotatable r) return faceToYaw(r.getRotation()); - if (data instanceof Directional d) return faceToYaw(d.getFacing()); - return 0; + return facingYaw(data); } /** Yaw in degrees for a block face, 0 = south increasing clockwise (vanilla rotation convention). */ @@ -276,13 +267,6 @@ public final class BlockEntitySnapshotBuilder { return null; } - /** Opaque ARGB for a dye colour. */ - private static int dyeArgb(DyeColor dye) { - if (dye == null) return 0xFFFFFFFF; - Color c = dye.getColor(); - return 0xFF000000 | (c.getRed() << 16) | (c.getGreen() << 8) | c.getBlue(); - } - // --- small fluent builder to keep the 15-field record construction readable --- private static Builder base(Kind kind, int bx, int by, int bz, float yaw) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java index db0f0d7..44043be 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java @@ -24,21 +24,8 @@ public final class DecorationSnapshotBuilder { private DecorationSnapshotBuilder() {} public static List build(Location eye, List rayMap, double maxDistance) { - Vector o = eye.toVector(); - double minX = o.getX(), minY = o.getY(), minZ = o.getZ(); - double maxX = o.getX(), maxY = o.getY(), maxZ = o.getZ(); - for (Vector ray : rayMap) { - minX = Math.min(minX, o.getX() + ray.getX() * maxDistance); - maxX = Math.max(maxX, o.getX() + ray.getX() * maxDistance); - minY = Math.min(minY, o.getY() + ray.getY() * maxDistance); - maxY = Math.max(maxY, o.getY() + ray.getY() * maxDistance); - minZ = Math.min(minZ, o.getZ() + ray.getZ() * maxDistance); - maxZ = Math.max(maxZ, o.getZ() + ray.getZ() * maxDistance); - } - Location center = new Location(eye.getWorld(), (minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2); - double hx = (maxX - minX) / 2 + 2, hy = (maxY - minY) / 2 + 2, hz = (maxZ - minZ) / 2 + 2; - - Collection nearby = eye.getWorld().getNearbyEntities(center, hx, hy, hz); + FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance); + Collection nearby = bounds.nearbyEntities(eye.getWorld(), 2); List out = new ArrayList<>(); for (Entity e : nearby) { try { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java index 762edde..e11683d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java @@ -1,6 +1,7 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot; import eu.mhsl.minecraft.pixelpics.render.entity.EntityState; +import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.Location; import org.bukkit.entity.Ageable; import org.bukkit.entity.Entity; @@ -33,21 +34,8 @@ public final class EntitySnapshotBuilder { ); public static List build(Location eye, List rayMap, double maxDistance, UUID shooter) { - Vector o = eye.toVector(); - double minX = o.getX(), minY = o.getY(), minZ = o.getZ(); - double maxX = o.getX(), maxY = o.getY(), maxZ = o.getZ(); - for (Vector ray : rayMap) { - minX = Math.min(minX, o.getX() + ray.getX() * maxDistance); - maxX = Math.max(maxX, o.getX() + ray.getX() * maxDistance); - minY = Math.min(minY, o.getY() + ray.getY() * maxDistance); - maxY = Math.max(maxY, o.getY() + ray.getY() * maxDistance); - minZ = Math.min(minZ, o.getZ() + ray.getZ() * maxDistance); - maxZ = Math.max(maxZ, o.getZ() + ray.getZ() * maxDistance); - } - Location center = new Location(eye.getWorld(), (minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2); - double hx = (maxX - minX) / 2 + 2, hy = (maxY - minY) / 2 + 2, hz = (maxZ - minZ) / 2 + 2; - - Collection nearby = eye.getWorld().getNearbyEntities(center, hx, hy, hz); + FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance); + Collection nearby = bounds.nearbyEntities(eye.getWorld(), 2); List states = new ArrayList<>(); for (Entity e : nearby) { if (shooter != null && e.getUniqueId().equals(shooter)) continue; @@ -65,21 +53,15 @@ public final class EntitySnapshotBuilder { if (NON_RENDERABLE.contains(type) || type.endsWith("_raft")) return null; float bodyYaw = loc.getYaw(); - float headYaw = loc.getYaw(); - float pitch = loc.getPitch(); if (e instanceof LivingEntity le) { bodyYaw = le.getBodyYaw(); - Location eyeLoc = le.getEyeLocation(); - headYaw = eyeLoc.getYaw(); - pitch = eyeLoc.getPitch(); } boolean baby = (e instanceof Ageable a && !a.isAdult()) || (e instanceof org.bukkit.entity.Zombie z && z.isBaby()); - Vector v = e.getVelocity(); - double width = safeWidth(e); - double height = safeHeight(e); + double width = safeDim(e::getWidth, () -> e.getBoundingBox().getWidthX()); + double height = safeDim(e::getHeight, () -> e.getBoundingBox().getHeight()); boolean player = e instanceof Player; String skinUrl = null; @@ -98,7 +80,7 @@ public final class EntitySnapshotBuilder { if (e instanceof org.bukkit.entity.Slime sl) sizeScale = sl.getSize(); // MushroomCow extends Cow, ZombieVillager does not extend Villager — order matters. if (e instanceof org.bukkit.entity.Sheep sh) { - tint = dyeArgb(sh.getColor()); + tint = ColorUtil.dyeArgb(sh.getColor(), 0); } else if (e instanceof org.bukkit.entity.Cat c) { variant = keyOf(c.getCatType()); } else if (e instanceof org.bukkit.entity.Wolf w) { @@ -139,7 +121,7 @@ public final class EntitySnapshotBuilder { } return new EntityState(type, loc.getX(), loc.getY(), loc.getZ(), - bodyYaw, headYaw, pitch, v.getX(), v.getY(), v.getZ(), baby, width, height, + bodyYaw, baby, width, height, player, skinUrl, slim, variant, tint, sizeScale); } @@ -151,13 +133,6 @@ public final class EntitySnapshotBuilder { return o.toString().toLowerCase(java.util.Locale.ROOT); } - /** ARGB wool-tint multiplier for a dye colour (opaque); never returns 0 so it stays "set". */ - private static int dyeArgb(org.bukkit.DyeColor dye) { - if (dye == null) return 0; - org.bukkit.Color c = dye.getColor(); - return 0xFF000000 | (c.getRed() << 16) | (c.getGreen() << 8) | c.getBlue(); - } - /** Returns {skinUrl, model} from the player's profile texture property, or {null, null}. */ private static String[] resolveSkin(Player player) { try { @@ -179,19 +154,12 @@ public final class EntitySnapshotBuilder { return new String[]{null, null}; } - private static double safeWidth(Entity e) { + /** Reads a dimension via {@code primary}, falling back to {@code fallback} on any version mismatch. */ + private static double safeDim(java.util.function.DoubleSupplier primary, java.util.function.DoubleSupplier fallback) { try { - return e.getWidth(); + return primary.getAsDouble(); } catch (Throwable t) { - return e.getBoundingBox().getWidthX(); - } - } - - private static double safeHeight(Entity e) { - try { - return e.getHeight(); - } catch (Throwable t) { - return e.getBoundingBox().getHeight(); + return fallback.getAsDouble(); } } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java new file mode 100644 index 0000000..a04d2a7 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java @@ -0,0 +1,48 @@ +package eu.mhsl.minecraft.pixelpics.render.snapshot; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; + +import java.util.Collection; +import java.util.List; + +/** + * Axis-aligned world-space bounds of the camera frustum: the min/max corner of the camera origin + * together with every ray endpoint at {@code maxDistance}. Shared by all snapshot builders to size + * their chunk/entity queries identically. + */ +final class FrustumBounds { + + final double minX, minY, minZ; + final double maxX, maxY, maxZ; + + private FrustumBounds(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + this.minX = minX; this.minY = minY; this.minZ = minZ; + this.maxX = maxX; this.maxY = maxY; this.maxZ = maxZ; + } + + static FrustumBounds of(Vector origin, List rayMap, double maxDistance) { + double minX = origin.getX(), minY = origin.getY(), minZ = origin.getZ(); + double maxX = origin.getX(), maxY = origin.getY(), maxZ = origin.getZ(); + for (Vector ray : rayMap) { + double fx = origin.getX() + ray.getX() * maxDistance; + double fy = origin.getY() + ray.getY() * maxDistance; + double fz = origin.getZ() + ray.getZ() * maxDistance; + minX = Math.min(minX, fx); maxX = Math.max(maxX, fx); + minY = Math.min(minY, fy); maxY = Math.max(maxY, fy); + minZ = Math.min(minZ, fz); maxZ = Math.max(maxZ, fz); + } + return new FrustumBounds(minX, minY, minZ, maxX, maxY, maxZ); + } + + /** All entities whose centre falls within these bounds expanded by {@code margin} on every side. */ + Collection nearbyEntities(World world, double margin) { + Location center = new Location(world, (minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2); + double hx = (maxX - minX) / 2 + margin; + double hy = (maxY - minY) / 2 + margin; + double hz = (maxZ - minZ) / 2 + margin; + return world.getNearbyEntities(center, hx, hy, hz); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java index 71dc2c9..3f75106 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java @@ -26,29 +26,17 @@ public final class SnapshotBuilder { public static WorldSnapshot build(Location eye, List rayMap, double maxDistance, Logger logger) { World world = eye.getWorld(); - Vector origin = eye.toVector(); - - double minX = origin.getX(), minY = origin.getY(), minZ = origin.getZ(); - double maxX = origin.getX(), maxY = origin.getY(), maxZ = origin.getZ(); - - for (Vector ray : rayMap) { - double fx = origin.getX() + ray.getX() * maxDistance; - double fy = origin.getY() + ray.getY() * maxDistance; - double fz = origin.getZ() + ray.getZ() * maxDistance; - minX = Math.min(minX, fx); maxX = Math.max(maxX, fx); - minY = Math.min(minY, fy); maxY = Math.max(maxY, fy); - minZ = Math.min(minZ, fz); maxZ = Math.max(maxZ, fz); - } + FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance); int worldMinY = world.getMinHeight(); int worldMaxY = world.getMaxHeight(); - int clampedMinY = Math.max(worldMinY, (int) Math.floor(minY) - 1); - int clampedMaxY = Math.min(worldMaxY, (int) Math.ceil(maxY) + 1); + int clampedMinY = Math.max(worldMinY, (int) Math.floor(bounds.minY) - 1); + int clampedMaxY = Math.min(worldMaxY, (int) Math.ceil(bounds.maxY) + 1); - int minCX = (int) Math.floor(minX) >> 4; - int maxCX = (int) Math.floor(maxX) >> 4; - int minCZ = (int) Math.floor(minZ) >> 4; - int maxCZ = (int) Math.floor(maxZ) >> 4; + int minCX = (int) Math.floor(bounds.minX) >> 4; + int maxCX = (int) Math.floor(bounds.maxX) >> 4; + int minCZ = (int) Math.floor(bounds.minZ) >> 4; + int maxCZ = (int) Math.floor(bounds.maxZ) >> 4; Map chunks = new HashMap<>(); int captured = 0; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java index dbb8392..42ec760 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java @@ -65,8 +65,8 @@ public final class BiomeTintProvider { /** Vanilla colormap lookup: x = (1-temp)*255, y = (1-downfall*temp)*255. */ private int sample(int[][] colormap, double temperature, double downfall, int fallback) { if (colormap == null || colormap.length == 0) return fallback; - double temp = clamp01(temperature); - double down = clamp01(downfall) * temp; + double temp = Math.clamp(temperature, 0, 1); + double down = Math.clamp(downfall, 0, 1) * temp; int x = (int) ((1.0 - temp) * 255.0); int y = (int) ((1.0 - down) * 255.0); int h = colormap.length; @@ -75,8 +75,4 @@ public final class BiomeTintProvider { y = Math.max(0, Math.min(h - 1, y)); return 0xFF000000 | (colormap[y][x] & 0xFFFFFF); } - - private double clamp01(double v) { - return v < 0 ? 0 : Math.min(v, 1); - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java index 2c0d7e6..ca0c1bc 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java @@ -16,6 +16,13 @@ public final class ColorUtil { return (a << 24) | (r << 16) | (g << 8) | b; } + /** Opaque ARGB for a Bukkit dye colour, or {@code fallback} when {@code dye} is null. */ + public static int dyeArgb(org.bukkit.DyeColor dye, int fallback) { + if (dye == null) return fallback; + org.bukkit.Color c = dye.getColor(); + return argb(0xFF, c.getRed(), c.getGreen(), c.getBlue()); + } + /** Multiplies the RGB channels of {@code base} by {@code tint} (per-channel, 0..255), keeping base alpha. */ public static int multiply(int base, int tint) { int a = alpha(base); @@ -38,6 +45,18 @@ public final class ColorUtil { return v < 0 ? 0 : Math.min(v, 255); } + /** + * Normalized weighted average of two RGB colors (alpha ignored, result opaque-RGB in the low 24 bits). + * Weights need not sum to 1; they are normalized by their total. + */ + public static int mix(int rgbA, int rgbB, double weightA, double weightB) { + double total = weightA + weightB; + int r = (int) ((red(rgbA) * weightA + red(rgbB) * weightB) / total); + int g = (int) ((green(rgbA) * weightA + green(rgbB) * weightB) / total); + int b = (int) ((blue(rgbA) * weightA + blue(rgbB) * weightB) / total); + return (r << 16) | (g << 8) | b; + } + // --- Gamma-correct (linear-light) averaging --- private static final float[] SRGB_TO_LINEAR = new float[256]; 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 7bf9016..54a76f5 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 @@ -1,6 +1,5 @@ package eu.mhsl.minecraft.pixelpics.render.util; -import org.bukkit.Color; import org.bukkit.block.BlockFace; import org.bukkit.util.Vector; @@ -8,7 +7,7 @@ public class MathUtil { private MathUtil() {} - public static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) { + private static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) { double oldX = base.getX(); double oldY = base.getY(); double oldZ = base.getZ(); @@ -30,39 +29,12 @@ public class MathUtil { 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))); + /** 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))); } 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/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java index 8b63913..27f45fd 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java @@ -1,5 +1,6 @@ package eu.mhsl.minecraft.pixelpics.utils; +import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.map.MapPalette; import java.awt.Color; @@ -80,7 +81,7 @@ public final class MapColorPalette { } private static double[] rgbToLab(int r, int g, int b) { - double rl = pivotSrgb(r), gl = pivotSrgb(g), bl = pivotSrgb(b); + double rl = ColorUtil.toLinear(r), gl = ColorUtil.toLinear(g), bl = ColorUtil.toLinear(b); // sRGB -> XYZ (D65) double x = rl * 0.4124 + gl * 0.3576 + bl * 0.1805; double y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722; @@ -91,11 +92,6 @@ public final class MapColorPalette { return new double[]{116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)}; } - private static double pivotSrgb(int c) { - double v = c / 255.0; - return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); - } - private static double pivotXyz(double t) { return t > 0.008856 ? Math.cbrt(t) : (7.787 * t + 16.0 / 116.0); } diff --git a/tools/EntityTestRender.java b/tools/EntityTestRender.java index 9049ca2..2eb30a1 100644 --- a/tools/EntityTestRender.java +++ b/tools/EntityTestRender.java @@ -125,7 +125,7 @@ public class EntityTestRender { static BufferedImage renderEntity(DefaultScreenRenderer renderer, eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker, WorldSnapshot world, SkyContext sky, String key, float yaw) { boolean isPlayer = key.equals("player"); - EntityState s = new EntityState(key, 0, 0, 0, yaw, yaw, 0, 0, 0, 0, false, 0.8, 1.0, + EntityState s = new EntityState(key, 0, 0, 0, yaw, false, 0.8, 1.0, isPlayer, null, false, VAR.get(key), 0, 1.0); RenderedEntity re = baker.bake(s); double cx = (re.aabbMin[0] + re.aabbMax[0]) / 2;