refactor and simplify rendering logic; remove redundant code and consolidate utility methods
This commit is contained in:
@@ -27,9 +27,4 @@ public final class AssetPaths {
|
||||
public static String textureMeta(ResourceLocation id) {
|
||||
return texture(id) + ".mcmeta";
|
||||
}
|
||||
|
||||
/** {@code assets/minecraft/textures/colormap/<name>.png}. */
|
||||
public static String colormap(String name) {
|
||||
return String.format("assets/%s/textures/colormap/%s.png", ResourceLocation.DEFAULT_NAMESPACE, name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ public final class AssetReader {
|
||||
this.pack = pack;
|
||||
}
|
||||
|
||||
public ResourcePack pack() {
|
||||
return pack;
|
||||
}
|
||||
|
||||
public <T> Optional<T> readJson(String path, Class<T> type) {
|
||||
return pack.read(path).flatMap(bytes -> {
|
||||
try {
|
||||
|
||||
@@ -52,8 +52,7 @@ public final class BlockModelRegistry {
|
||||
List<Variant> variants = blockStateResolver.resolve(data);
|
||||
|
||||
List<Element> 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);
|
||||
|
||||
@@ -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<K> {
|
||||
|
||||
private static final int[][] MISSING = new int[0][0];
|
||||
|
||||
private final Map<K, int[][]> 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<int[][]> 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;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public final class ModelBaker {
|
||||
int ySteps = ((variant.y() / 90) % 4 + 4) % 4;
|
||||
|
||||
List<Element> 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, int[][]> cache = new ConcurrentHashMap<>();
|
||||
private static final int[][] FAILED = new int[0][0];
|
||||
public final class SkinCache extends ImageCache<String> {
|
||||
|
||||
@Override
|
||||
public Optional<int[][]> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ResourceLocation> {
|
||||
|
||||
private final ResourcePack pack;
|
||||
private final Map<ResourceLocation, int[][]> 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<int[][]> get(ResourceLocation textureId) {
|
||||
int[][] result = cache.computeIfAbsent(textureId, this::load);
|
||||
return result == MISSING ? Optional.empty() : Optional.of(result);
|
||||
}
|
||||
|
||||
private int[][] load(ResourceLocation id) {
|
||||
Optional<byte[]> 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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DecorationState> {
|
||||
|
||||
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<EntityCube> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<S> {
|
||||
RenderedEntity bake(S state);
|
||||
}
|
||||
@@ -18,30 +18,22 @@ public final class EntityScene {
|
||||
private static final double EPS = 1e-7;
|
||||
private final List<RenderedEntity> entities;
|
||||
|
||||
public EntityScene(List<EntityState> states, CemBaker baker) {
|
||||
this(states, baker, List.of(), null, List.of(), null);
|
||||
}
|
||||
|
||||
public EntityScene(List<EntityState> states, CemBaker baker,
|
||||
List<BlockEntityState> blockEntities, BlockEntityBaker beBaker,
|
||||
List<DecorationState> 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 <S> void addAll(List<S> states, EntityBaker<S> 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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EntityCube> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BlockEntityState> {
|
||||
|
||||
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<Layer> 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/<key>}) 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EntityState> {
|
||||
|
||||
// Parts representing an alternate state (rolled-up, sleeping, …) that must not render in the idle pose.
|
||||
private static final java.util.Map<String, java.util.Set<String>> 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<String> hidden = HIDDEN_PARTS.getOrDefault(EntityModels.cemModel(s.typeKey()), java.util.Set.of());
|
||||
java.util.Set<String> hidden = HIDDEN_PARTS.getOrDefault(cem, java.util.Set.of());
|
||||
List<CemGeometry.Baked> 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<EntityCube> 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<EntityCube> 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) {
|
||||
|
||||
@@ -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<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> 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<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> 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<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> 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<EntityCube> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> 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<String> aliases;
|
||||
|
||||
AspectRatio(double ratio, String... aliases) {
|
||||
AspectRatio(double ratio) {
|
||||
this.ratio = ratio;
|
||||
this.aliases = Collections.unmodifiableList(Arrays.asList(aliases));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
+10
-26
@@ -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<BlockEntityState> build(Location eye, List<Vector> 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<BannerPattern> 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) {
|
||||
|
||||
+2
-15
@@ -24,21 +24,8 @@ public final class DecorationSnapshotBuilder {
|
||||
private DecorationSnapshotBuilder() {}
|
||||
|
||||
public static List<DecorationState> build(Location eye, List<Vector> 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<Entity> nearby = eye.getWorld().getNearbyEntities(center, hx, hy, hz);
|
||||
FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance);
|
||||
Collection<Entity> nearby = bounds.nearbyEntities(eye.getWorld(), 2);
|
||||
List<DecorationState> out = new ArrayList<>();
|
||||
for (Entity e : nearby) {
|
||||
try {
|
||||
|
||||
+11
-43
@@ -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<EntityState> build(Location eye, List<Vector> 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<Entity> nearby = eye.getWorld().getNearbyEntities(center, hx, hy, hz);
|
||||
FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance);
|
||||
Collection<Entity> nearby = bounds.nearbyEntities(eye.getWorld(), 2);
|
||||
List<EntityState> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vector> 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<Entity> 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);
|
||||
}
|
||||
}
|
||||
@@ -26,29 +26,17 @@ public final class SnapshotBuilder {
|
||||
|
||||
public static WorldSnapshot build(Location eye, List<Vector> 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<Long, ChunkSnapshot> chunks = new HashMap<>();
|
||||
int captured = 0;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user