add Spotless plugin: enforce code formatting
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'com.diffplug.spotless' version '7.0.2'
|
||||
}
|
||||
|
||||
group = 'eu.mhsl.minecraft.pixelpics'
|
||||
@@ -48,6 +49,14 @@ processResources {
|
||||
}
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target 'src/main/java/**/*.java'
|
||||
palantirJavaFormat()
|
||||
removeUnusedImports()
|
||||
}
|
||||
}
|
||||
|
||||
if (file("local.gradle").exists()) {
|
||||
apply from: "local.gradle"
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ import eu.mhsl.minecraft.pixelpics.survival.CraftingListener;
|
||||
import eu.mhsl.minecraft.pixelpics.survival.JoinListener;
|
||||
import eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class Main extends JavaPlugin {
|
||||
|
||||
@@ -75,8 +74,9 @@ public final class Main extends JavaPlugin {
|
||||
int timeoutSeconds = Math.max(1, getConfig().getInt("render.timeout-seconds", 30));
|
||||
|
||||
this.renderManager = new RenderManager(this, threads, maxConcurrent, queueSize, timeoutSeconds);
|
||||
getLogger().info("Render pool: " + threads + " core(s), max " + maxConcurrent
|
||||
+ " concurrent, queue " + queueSize + ", timeout " + timeoutSeconds + "s.");
|
||||
getLogger()
|
||||
.info("Render pool: " + threads + " core(s), max " + maxConcurrent + " concurrent, queue " + queueSize
|
||||
+ ", timeout " + timeoutSeconds + "s.");
|
||||
}
|
||||
|
||||
private void initRenderer() {
|
||||
@@ -87,9 +87,10 @@ public final class Main extends JavaPlugin {
|
||||
|
||||
Optional<ResourcePack> pack = ResourcePackLoader.load(resourcePackDir, getLogger());
|
||||
if (pack.isEmpty()) {
|
||||
getLogger().severe("No resource pack found in " + resourcePackDir.getPath()
|
||||
+ " — place a vanilla resource pack (directory with assets/minecraft/... or a .zip) there. "
|
||||
+ "/pixelPic is disabled until a pack is available.");
|
||||
getLogger()
|
||||
.severe("No resource pack found in " + resourcePackDir.getPath()
|
||||
+ " — place a vanilla resource pack (directory with assets/minecraft/... or a .zip) there. "
|
||||
+ "/pixelPic is disabled until a pack is available.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,8 +100,7 @@ public final class Main extends JavaPlugin {
|
||||
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
|
||||
BiomeTintProvider tintProvider = new BiomeTintProvider(textures);
|
||||
|
||||
CemModelLoader cemLoader =
|
||||
new CemModelLoader();
|
||||
CemModelLoader cemLoader = new CemModelLoader();
|
||||
try (InputStream in = getResource("cem/cem_template_models.json")) {
|
||||
int n = in == null ? 0 : cemLoader.load(in, getLogger());
|
||||
getLogger().info("Loaded " + n + " CEM entity models.");
|
||||
@@ -108,16 +108,19 @@ public final class Main extends JavaPlugin {
|
||||
getLogger().severe("Failed to load CEM entity models: " + e.getMessage());
|
||||
}
|
||||
SkinCache skinCache = new SkinCache();
|
||||
BitmapFont font =
|
||||
FontLoader.load(resourcePack, textures, getLogger());
|
||||
BitmapFont font = FontLoader.load(resourcePack, textures, getLogger());
|
||||
getLogger().info("Loaded sign font (" + (font.isEmpty() ? "no glyphs — text disabled" : "ok") + ").");
|
||||
CemBaker entityBaker =
|
||||
new CemBaker(cemLoader, textures, skinCache);
|
||||
BlockEntityBaker blockEntityBaker =
|
||||
new BlockEntityBaker(cemLoader, textures, skinCache, font);
|
||||
CemBaker entityBaker = new CemBaker(cemLoader, textures, skinCache);
|
||||
BlockEntityBaker blockEntityBaker = new BlockEntityBaker(cemLoader, textures, skinCache, font);
|
||||
|
||||
this.screenRenderer = new DefaultScreenRenderer(registry, tintProvider, textures, entityBaker,
|
||||
blockEntityBaker, getLogger(), renderManager.tracePool());
|
||||
this.screenRenderer = new DefaultScreenRenderer(
|
||||
registry,
|
||||
tintProvider,
|
||||
textures,
|
||||
entityBaker,
|
||||
blockEntityBaker,
|
||||
getLogger(),
|
||||
renderManager.tracePool());
|
||||
// Warm the map palette on the main thread so off-thread dithering never triggers its first init.
|
||||
MapColorPalette.size();
|
||||
getLogger().info("PixelPics renderer initialized with resource pack assets.");
|
||||
|
||||
@@ -10,7 +10,8 @@ public final class AssetPaths {
|
||||
|
||||
/** {@code assets/<ns>/blockstates/<name>.json} for a plain block name (no {@code block/} prefix). */
|
||||
public static String blockState(String blockName) {
|
||||
return String.format("assets/%s/blockstates/%s.json", ResourceLocation.DEFAULT_NAMESPACE, blockName.toLowerCase());
|
||||
return String.format(
|
||||
"assets/%s/blockstates/%s.json", ResourceLocation.DEFAULT_NAMESPACE, blockName.toLowerCase());
|
||||
}
|
||||
|
||||
/** {@code assets/<ns>/models/<path>.json}. The id path already contains e.g. {@code block/stone}. */
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.mhsl.minecraft.pixelpics.assets;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -32,7 +31,8 @@ public final class AssetReader {
|
||||
public Optional<JsonObject> readJsonObject(String path) {
|
||||
return pack.read(path).flatMap(bytes -> {
|
||||
try {
|
||||
return Optional.of(JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject());
|
||||
return Optional.of(JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject());
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Element;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.ResolvedModel;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
/**
|
||||
* Top-level entry point that turns a {@link BlockData} into a baked {@link ResolvedModel}, combining
|
||||
@@ -88,8 +87,12 @@ public final class BlockModelRegistry {
|
||||
|
||||
private static EnumSet<Material> buildInvisibleMaterials() {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (String n : new String[]{"BARRIER", "LIGHT", "STRUCTURE_VOID"}) {
|
||||
try { set.add(Material.valueOf(n)); } catch (IllegalArgumentException ignored) { /* older/newer server */ }
|
||||
for (String n : new String[] {"BARRIER", "LIGHT", "STRUCTURE_VOID"}) {
|
||||
try {
|
||||
set.add(Material.valueOf(n));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
/* older/newer server */
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
@@ -107,14 +110,17 @@ public final class BlockModelRegistry {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (Material m : Material.values()) {
|
||||
String n = m.name();
|
||||
boolean match =
|
||||
n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging
|
||||
|| n.endsWith("_BANNER") // banners: standing/wall
|
||||
|| n.endsWith("_BED")
|
||||
|| n.endsWith("SHULKER_BOX") // SHULKER_BOX + <color>_SHULKER_BOX
|
||||
|| ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD)
|
||||
|| m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.ENDER_CHEST
|
||||
|| m == Material.CONDUIT || m == Material.DECORATED_POT || m == Material.BELL;
|
||||
boolean match = n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging
|
||||
|| n.endsWith("_BANNER") // banners: standing/wall
|
||||
|| n.endsWith("_BED")
|
||||
|| n.endsWith("SHULKER_BOX") // SHULKER_BOX + <color>_SHULKER_BOX
|
||||
|| ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD)
|
||||
|| m == Material.CHEST
|
||||
|| m == Material.TRAPPED_CHEST
|
||||
|| m == Material.ENDER_CHEST
|
||||
|| m == Material.CONDUIT
|
||||
|| m == Material.DECORATED_POT
|
||||
|| m == Material.BELL;
|
||||
if (match) set.add(m);
|
||||
}
|
||||
return set;
|
||||
@@ -127,7 +133,7 @@ public final class BlockModelRegistry {
|
||||
for (Direction d : Direction.values()) {
|
||||
faces[d.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, -1);
|
||||
}
|
||||
return new Element(new double[]{0, 0, 0}, new double[]{1, 1, 1}, faces, null, -1, 0, false);
|
||||
return new Element(new double[] {0, 0, 0}, new double[] {1, 1, 1}, faces, null, -1, 0, false);
|
||||
}
|
||||
|
||||
private int fallbackColor(FlatModel flat) {
|
||||
@@ -169,7 +175,7 @@ public final class BlockModelRegistry {
|
||||
for (Direction d : Direction.values()) {
|
||||
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);
|
||||
Element cube = new Element(new double[] {0, 0, 0}, new double[] {1, 1, 1}, faces, null, -1, 0, false);
|
||||
int avg = AverageColor.of(tex);
|
||||
return new ResolvedModel(List.of(cube), avg, transparency, reflection, true, true);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package eu.mhsl.minecraft.pixelpics.assets;
|
||||
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
/**
|
||||
* Parses the property map and plain block name out of a {@link BlockData} string such as
|
||||
|
||||
@@ -3,11 +3,10 @@ package eu.mhsl.minecraft.pixelpics.assets;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
/**
|
||||
* Resolves a {@link BlockData} to the list of model variants vanilla would render, by reading the
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.mhsl.minecraft.pixelpics.assets;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.dto.ModelFileDto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import eu.mhsl.minecraft.pixelpics.assets.model.AverageColor;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Element;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -61,8 +60,9 @@ public final class ModelBaker {
|
||||
double rotAngle = 0;
|
||||
boolean rescale = false;
|
||||
if (dto.rotation != null && dto.rotation.angle != 0 && dto.rotation.origin != null) {
|
||||
rotOrigin = new double[]{
|
||||
dto.rotation.origin[0] / 16.0, dto.rotation.origin[1] / 16.0, dto.rotation.origin[2] / 16.0};
|
||||
rotOrigin = new double[] {
|
||||
dto.rotation.origin[0] / 16.0, dto.rotation.origin[1] / 16.0, dto.rotation.origin[2] / 16.0
|
||||
};
|
||||
rotAxis = axisIndex(dto.rotation.axis);
|
||||
rotAngle = Math.toRadians(dto.rotation.angle);
|
||||
rescale = dto.rotation.rescale;
|
||||
@@ -106,8 +106,8 @@ public final class ModelBaker {
|
||||
return new BakedGeometry(baked, avgColor.average(0xFF7F7F7F), !baked.isEmpty());
|
||||
}
|
||||
|
||||
private Face buildFace(Direction dir, ModelFileDto.FaceDto dto, double[] from, double[] to,
|
||||
Map<String, String> textureVars) {
|
||||
private Face buildFace(
|
||||
Direction dir, ModelFileDto.FaceDto dto, double[] from, double[] to, Map<String, String> textureVars) {
|
||||
int[][] tex = resolveTexture(dto.texture, textureVars);
|
||||
if (tex == null) return null;
|
||||
|
||||
@@ -134,9 +134,9 @@ public final class ModelBaker {
|
||||
*/
|
||||
private double[] defaultUv(Direction dir, double[] f, double[] t) {
|
||||
return switch (dir) {
|
||||
case UP, DOWN -> new double[]{f[0], f[2], t[0], t[2]};
|
||||
case NORTH, SOUTH -> new double[]{f[0], 1 - t[1], t[0], 1 - f[1]};
|
||||
case WEST, EAST -> new double[]{f[2], 1 - t[1], t[2], 1 - f[1]};
|
||||
case UP, DOWN -> new double[] {f[0], f[2], t[0], t[2]};
|
||||
case NORTH, SOUTH -> new double[] {f[0], 1 - t[1], t[0], 1 - f[1]};
|
||||
case WEST, EAST -> new double[] {f[2], 1 - t[1], t[2], 1 - f[1]};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,18 +179,18 @@ public final class ModelBaker {
|
||||
|
||||
private double[] rotatePointY(double[] p) {
|
||||
double x = p[0] - 0.5, z = p[2] - 0.5;
|
||||
return new double[]{0.5 + z, p[1], 0.5 - x};
|
||||
return new double[] {0.5 + z, p[1], 0.5 - x};
|
||||
}
|
||||
|
||||
private double[] rotatePointX(double[] p) {
|
||||
double y = p[1] - 0.5, z = p[2] - 0.5;
|
||||
return new double[]{p[0], 0.5 + z, 0.5 - y};
|
||||
return new double[] {p[0], 0.5 + z, 0.5 - y};
|
||||
}
|
||||
|
||||
private double[][] minMax(double[] a, double[] b) {
|
||||
double[] from = {Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.min(a[2], b[2])};
|
||||
double[] to = {Math.max(a[0], b[0]), Math.max(a[1], b[1]), Math.max(a[2], b[2])};
|
||||
return new double[][]{from, to};
|
||||
return new double[][] {from, to};
|
||||
}
|
||||
|
||||
private Face[] rotateFacesY(Face[] faces) {
|
||||
@@ -216,7 +216,7 @@ public final class ModelBaker {
|
||||
private AxisRotation rotateAxisY(int axis) {
|
||||
// Y rotation maps x<->z; the y axis is unchanged.
|
||||
return switch (axis) {
|
||||
case 0 -> new AxisRotation(2, true); // x -> z
|
||||
case 0 -> new AxisRotation(2, true); // x -> z
|
||||
case 2 -> new AxisRotation(0, false); // z -> x
|
||||
default -> new AxisRotation(axis, false);
|
||||
};
|
||||
@@ -225,7 +225,7 @@ public final class ModelBaker {
|
||||
private AxisRotation rotateAxisX(int axis) {
|
||||
// X rotation maps y<->z; the x axis is unchanged.
|
||||
return switch (axis) {
|
||||
case 1 -> new AxisRotation(2, true); // y -> z
|
||||
case 1 -> new AxisRotation(2, true); // y -> z
|
||||
case 2 -> new AxisRotation(1, false); // z -> y
|
||||
default -> new AxisRotation(axis, false);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.mhsl.minecraft.pixelpics.assets;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.dto.ModelFileDto;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -31,7 +30,8 @@ public final class ModelResolver {
|
||||
}
|
||||
|
||||
private FlatModel resolve(ResourceLocation modelId, int depth) {
|
||||
ModelFileDto dto = reader.readJson(AssetPaths.model(modelId), ModelFileDto.class).orElse(null);
|
||||
ModelFileDto dto =
|
||||
reader.readJson(AssetPaths.model(modelId), ModelFileDto.class).orElse(null);
|
||||
if (dto == null) {
|
||||
return new FlatModel(new HashMap<>(), null);
|
||||
}
|
||||
|
||||
@@ -45,15 +45,13 @@ public final class ResourcePackLoader {
|
||||
|
||||
// Zip packs anywhere under the resourcepack folder.
|
||||
try (Stream<Path> walk = Files.walk(resourcePackDir.toPath())) {
|
||||
List<Path> zips = walk
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.getFileName().toString().toLowerCase().endsWith(".zip"))
|
||||
.toList();
|
||||
List<Path> zips = walk.filter(Files::isRegularFile)
|
||||
.filter(p -> p.getFileName().toString().toLowerCase().endsWith(".zip"))
|
||||
.toList();
|
||||
for (Path zip : zips) {
|
||||
try {
|
||||
ZipResourcePack pack = new ZipResourcePack(zip);
|
||||
if (pack.exists(MARKER + "/blockstates") || pack.exists("pack.mcmeta")
|
||||
|| hasAnyBlockstate(pack)) {
|
||||
if (pack.exists(MARKER + "/blockstates") || pack.exists("pack.mcmeta") || hasAnyBlockstate(pack)) {
|
||||
packs.add(pack);
|
||||
logger.info("Loaded resource pack zip: " + zip);
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.mhsl.minecraft.pixelpics.assets;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Optional;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Downloads and caches player skin textures (by URL) as ARGB pixel grids. Legacy 64x32 skins are
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package eu.mhsl.minecraft.pixelpics.assets;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Loads and caches block textures as raw ARGB pixel grids. Textures are stored unflipped (vanilla
|
||||
|
||||
@@ -4,5 +4,4 @@ package eu.mhsl.minecraft.pixelpics.assets;
|
||||
* A resolved blockstate variant: which model to use plus its {@code x}/{@code y} rotation (in
|
||||
* degrees, multiples of 90) and {@code uvlock}.
|
||||
*/
|
||||
public record Variant(ResourceLocation model, int x, int y, boolean uvlock) {
|
||||
}
|
||||
public record Variant(ResourceLocation model, int x, int y, boolean uvlock) {}
|
||||
|
||||
@@ -22,9 +22,7 @@ public final class ZipResourcePack implements ResourcePack {
|
||||
|
||||
public ZipResourcePack(Path zipPath) throws IOException {
|
||||
this.zipFile = new ZipFile(zipPath.toFile());
|
||||
zipFile.stream()
|
||||
.filter(entry -> !entry.isDirectory())
|
||||
.forEach(entry -> entries.put(entry.getName(), entry));
|
||||
zipFile.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> entries.put(entry.getName(), entry));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,24 +13,24 @@ public class ModelFileDto {
|
||||
public List<ElementDto> elements;
|
||||
|
||||
public static class ElementDto {
|
||||
public double[] from; // 0..16
|
||||
public double[] to; // 0..16
|
||||
public RotationDto rotation; // optional
|
||||
public double[] from; // 0..16
|
||||
public double[] to; // 0..16
|
||||
public RotationDto rotation; // optional
|
||||
public Map<String, FaceDto> faces; // keys: down/up/north/south/west/east
|
||||
}
|
||||
|
||||
public static class FaceDto {
|
||||
public double[] uv; // optional, 0..16 (x1,y1,x2,y2)
|
||||
public String texture; // e.g. "#side" or "minecraft:block/oak_planks"
|
||||
public double[] uv; // optional, 0..16 (x1,y1,x2,y2)
|
||||
public String texture; // e.g. "#side" or "minecraft:block/oak_planks"
|
||||
public Integer tintindex;
|
||||
public String cullface; // ignored by the renderer
|
||||
public int rotation; // 0/90/180/270
|
||||
public int rotation; // 0/90/180/270
|
||||
}
|
||||
|
||||
public static class RotationDto {
|
||||
public double[] origin; // 0..16
|
||||
public String axis; // "x" | "y" | "z"
|
||||
public double angle; // -45..45 in 22.5 steps
|
||||
public String axis; // "x" | "y" | "z"
|
||||
public double angle; // -45..45 in 22.5 steps
|
||||
public boolean rescale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import eu.mhsl.minecraft.pixelpics.assets.AssetPaths;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourcePack;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -63,7 +62,8 @@ public final class FontLoader {
|
||||
if (bytes.isEmpty()) return;
|
||||
JsonObject root;
|
||||
try {
|
||||
root = JsonParser.parseString(new String(bytes.get(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
root = JsonParser.parseString(new String(bytes.get(), StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
if (log != null) log.warning("PixelPics: failed to parse font " + path + ": " + e.getMessage());
|
||||
return;
|
||||
@@ -84,7 +84,9 @@ public final class FontLoader {
|
||||
}
|
||||
case "space" -> space(p);
|
||||
case "bitmap" -> bitmap(p);
|
||||
default -> { /* unihex, legacy_unicode, ttf, … — out of scope */ }
|
||||
default -> {
|
||||
/* unihex, legacy_unicode, ttf, … — out of scope */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +119,9 @@ public final class FontLoader {
|
||||
JsonArray rows = p.getAsJsonArray("chars");
|
||||
int nRows = rows.size();
|
||||
if (nRows == 0) return;
|
||||
int nCols = rows.get(0).getAsString().codePointCount(0, rows.get(0).getAsString().length());
|
||||
int nCols = rows.get(0)
|
||||
.getAsString()
|
||||
.codePointCount(0, rows.get(0).getAsString().length());
|
||||
if (nCols == 0) return;
|
||||
int cellW = imgW / nCols;
|
||||
int cellH = imgH / nRows;
|
||||
@@ -131,12 +135,12 @@ public final class FontLoader {
|
||||
int cp = row.codePointAt(ci);
|
||||
ci += Character.charCount(cp);
|
||||
int thisCol = col++;
|
||||
if (cp == 0 || thisCol >= nCols) continue; // 0x0000 = empty slot
|
||||
if (glyphs.containsKey(cp)) continue; // earlier provider wins
|
||||
if (cp == 0 || thisCol >= nCols) continue; // 0x0000 = empty slot
|
||||
if (glyphs.containsKey(cp)) continue; // earlier provider wins
|
||||
int srcX = thisCol * cellW;
|
||||
int srcY = r * cellH;
|
||||
int glyphPx = rightmostOpaqueColumn(tex, srcX, srcY, cellW, cellH) + 1;
|
||||
if (glyphPx <= 0) continue; // blank glyph (handled by space provider)
|
||||
if (glyphPx <= 0) continue; // blank glyph (handled by space provider)
|
||||
int advance = (int) (glyphPx * scale + 0.5) + 1;
|
||||
glyphs.put(cp, new Glyph(tex, srcX, srcY, cellW, cellH, glyphPx, height, ascent, advance));
|
||||
maxAscent = Math.max(maxAscent, ascent);
|
||||
@@ -151,7 +155,10 @@ public final class FontLoader {
|
||||
for (int x = 0; x < cellW; x++) {
|
||||
for (int y = 0; y < cellH; y++) {
|
||||
int argb = tex[srcY + y][srcX + x];
|
||||
if (((argb >>> 24) & 0xFF) != 0) { last = x; break; }
|
||||
if (((argb >>> 24) & 0xFF) != 0) {
|
||||
last = x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return last;
|
||||
|
||||
@@ -9,8 +9,8 @@ package eu.mhsl.minecraft.pixelpics.assets.font;
|
||||
* <p>Glyphs from different providers may declare different cell sizes and ascents (e.g. ascii 8px/ascent
|
||||
* 7 vs accented 12px/ascent 10); rendering aligns them on a common baseline so umlauts and Latin mix.
|
||||
*/
|
||||
public record Glyph(int[][] tex, int srcX, int srcY, int cellW, int cellH,
|
||||
int glyphPx, int height, int ascent, int advance) {
|
||||
public record Glyph(
|
||||
int[][] tex, int srcX, int srcY, int cellW, int cellH, int glyphPx, int height, int ascent, int advance) {
|
||||
|
||||
/** Vertical pixels below the baseline (font px). */
|
||||
public int descent() {
|
||||
|
||||
@@ -11,7 +11,6 @@ public enum Direction {
|
||||
SOUTH(0, 0, 1),
|
||||
WEST(-1, 0, 0),
|
||||
EAST(1, 0, 0);
|
||||
|
||||
public final int nx, ny, nz;
|
||||
|
||||
Direction(int nx, int ny, int nz) {
|
||||
@@ -42,8 +41,8 @@ public enum Direction {
|
||||
case EAST -> SOUTH;
|
||||
case SOUTH -> WEST;
|
||||
case WEST -> NORTH;
|
||||
default -> d; // up/down unchanged
|
||||
};
|
||||
// up/down unchanged
|
||||
default -> d;};
|
||||
}
|
||||
return d;
|
||||
}
|
||||
@@ -58,8 +57,8 @@ public enum Direction {
|
||||
case NORTH -> DOWN;
|
||||
case DOWN -> SOUTH;
|
||||
case SOUTH -> UP;
|
||||
default -> d; // east/west unchanged
|
||||
};
|
||||
// east/west unchanged
|
||||
default -> d;};
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -10,17 +10,23 @@ package eu.mhsl.minecraft.pixelpics.assets.model;
|
||||
public final class Element {
|
||||
|
||||
public final double[] from; // length 3, 0..1
|
||||
public final double[] to; // length 3, 0..1
|
||||
public final Face[] faces; // length 6, indexed by Direction.ordinal()
|
||||
public final double[] to; // length 3, 0..1
|
||||
public final Face[] faces; // length 6, indexed by Direction.ordinal()
|
||||
|
||||
// Element rotation (0..1 origin), null/zero when axis-aligned.
|
||||
public final double[] rotOrigin; // length 3, 0..1, may be null
|
||||
public final int rotAxis; // 0=x,1=y,2=z, -1 when none
|
||||
public final int rotAxis; // 0=x,1=y,2=z, -1 when none
|
||||
public final double rotAngleRad; // radians
|
||||
public final boolean rescale;
|
||||
|
||||
public Element(double[] from, double[] to, Face[] faces,
|
||||
double[] rotOrigin, int rotAxis, double rotAngleRad, boolean rescale) {
|
||||
public Element(
|
||||
double[] from,
|
||||
double[] to,
|
||||
Face[] faces,
|
||||
double[] rotOrigin,
|
||||
int rotAxis,
|
||||
double rotAngleRad,
|
||||
boolean rescale) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.faces = faces;
|
||||
|
||||
@@ -29,10 +29,21 @@ public final class Face {
|
||||
// Apply face rotation by rotating the (s,t) lookup.
|
||||
double rs = s, rt = t;
|
||||
switch (((rotation % 360) + 360) % 360) {
|
||||
case 90 -> { rs = t; rt = 1.0 - s; }
|
||||
case 180 -> { rs = 1.0 - s; rt = 1.0 - t; }
|
||||
case 270 -> { rs = 1.0 - t; rt = s; }
|
||||
default -> { /* 0 */ }
|
||||
case 90 -> {
|
||||
rs = t;
|
||||
rt = 1.0 - s;
|
||||
}
|
||||
case 180 -> {
|
||||
rs = 1.0 - s;
|
||||
rt = 1.0 - t;
|
||||
}
|
||||
case 270 -> {
|
||||
rs = 1.0 - t;
|
||||
rt = s;
|
||||
}
|
||||
default -> {
|
||||
/* 0 */
|
||||
}
|
||||
}
|
||||
|
||||
double u = u1 + (u2 - u1) * rs;
|
||||
|
||||
@@ -15,12 +15,17 @@ public final class ResolvedModel {
|
||||
public final List<Element> elements;
|
||||
public final int averageColor; // ARGB
|
||||
public final double transparency; // 0..1
|
||||
public final double reflection; // 0..1
|
||||
public final double reflection; // 0..1
|
||||
public final boolean occluding;
|
||||
public final boolean hasGeometry;
|
||||
|
||||
public ResolvedModel(List<Element> elements, int averageColor,
|
||||
double transparency, double reflection, boolean occluding, boolean hasGeometry) {
|
||||
public ResolvedModel(
|
||||
List<Element> elements,
|
||||
int averageColor,
|
||||
double transparency,
|
||||
double reflection,
|
||||
boolean occluding,
|
||||
boolean hasGeometry) {
|
||||
this.elements = elements;
|
||||
this.averageColor = averageColor;
|
||||
this.transparency = transparency;
|
||||
|
||||
@@ -2,6 +2,8 @@ package eu.mhsl.minecraft.pixelpics.commands;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.survival.PhotoService;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -10,31 +12,31 @@ import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class PixelPicsCommand implements CommandExecutor {
|
||||
|
||||
private static final int DEFAULT_CLEANUP_DAYS = 30;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
|
||||
@NotNull String @NotNull [] args) {
|
||||
public boolean onCommand(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String @NotNull [] args) {
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("cleanup")) {
|
||||
return cleanup(sender, args);
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!",
|
||||
NamedTextColor.RED));
|
||||
sender.sendMessage(
|
||||
Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
if (args.length > 0) return false;
|
||||
|
||||
// Debug shortcut: render a photo from the player's view without needing a camera or film.
|
||||
if (!player.hasPermission("pixelpic.admin")) {
|
||||
player.sendActionBar(Component.text("Dafür fehlt dir die Berechtigung — nutze eine Kamera.",
|
||||
NamedTextColor.RED));
|
||||
player.sendActionBar(
|
||||
Component.text("Dafür fehlt dir die Berechtigung — nutze eine Kamera.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -45,7 +47,8 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
/** {@code /pixelPic cleanup [days]} dry-run; {@code /pixelPic cleanup confirm [days]} deletes orphans. */
|
||||
private boolean cleanup(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("pixelpic.admin") && !sender.isOp()) {
|
||||
sender.sendMessage(Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED));
|
||||
sender.sendMessage(
|
||||
Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,21 +67,25 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
Set<Integer> inUse = MapManager.collectInUseMapIds();
|
||||
long cutoff = System.currentTimeMillis() - days * 86_400_000L;
|
||||
List<MapManager.StoredMap> candidates = MapManager.listStored().stream()
|
||||
.filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff)
|
||||
.toList();
|
||||
.filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff)
|
||||
.toList();
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
sender.sendMessage(Component.text("Keine aufräumbaren Aufnahmen gefunden (älter als " + days
|
||||
+ " Tage und nicht in Benutzung).", NamedTextColor.YELLOW));
|
||||
sender.sendMessage(Component.text(
|
||||
"Keine aufräumbaren Aufnahmen gefunden (älter als " + days + " Tage und nicht in Benutzung).",
|
||||
NamedTextColor.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
sender.sendMessage(Component.text(candidates.size() + " Aufnahme(n) könnten gelöscht werden "
|
||||
+ "(älter als " + days + " Tage, nicht in geladenen Itemframes/Online-Inventaren).",
|
||||
NamedTextColor.YELLOW));
|
||||
sender.sendMessage(Component.text("Achtung: Maps in lange ungeladenen Bereichen werden hierbei nicht "
|
||||
+ "erkannt. Zum Löschen: /pixelPic cleanup confirm " + days, NamedTextColor.GRAY));
|
||||
sender.sendMessage(Component.text(
|
||||
candidates.size() + " Aufnahme(n) könnten gelöscht werden " + "(älter als " + days
|
||||
+ " Tage, nicht in geladenen Itemframes/Online-Inventaren).",
|
||||
NamedTextColor.YELLOW));
|
||||
sender.sendMessage(Component.text(
|
||||
"Achtung: Maps in lange ungeladenen Bereichen werden hierbei nicht "
|
||||
+ "erkannt. Zum Löschen: /pixelPic cleanup confirm " + days,
|
||||
NamedTextColor.GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ package eu.mhsl.minecraft.pixelpics.listeners;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||
import java.awt.image.BufferedImage;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.server.MapInitializeEvent;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class OnMapInitialize implements Listener {
|
||||
|
||||
@EventHandler
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -19,6 +16,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* Server-friendly scheduling for photo renders. Bounds CPU use and protects the server tick by:
|
||||
@@ -37,7 +36,11 @@ import java.util.function.Function;
|
||||
*/
|
||||
public final class RenderManager {
|
||||
|
||||
public enum Outcome { ACCEPTED, USER_BUSY, QUEUE_FULL }
|
||||
public enum Outcome {
|
||||
ACCEPTED,
|
||||
USER_BUSY,
|
||||
QUEUE_FULL
|
||||
}
|
||||
|
||||
private final Plugin plugin;
|
||||
private final ForkJoinPool tracePool;
|
||||
@@ -54,9 +57,12 @@ public final class RenderManager {
|
||||
this.timeoutMillis = Math.max(1L, timeoutSeconds) * 1000L;
|
||||
this.tracePool = new ForkJoinPool(threads, lowPriorityForkJoinFactory(), null, false);
|
||||
this.dispatcher = new ThreadPoolExecutor(
|
||||
maxConcurrent, maxConcurrent, 30, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(), // capacity is enforced by inFlight, not this queue
|
||||
lowPriorityThreadFactory());
|
||||
maxConcurrent,
|
||||
maxConcurrent,
|
||||
30,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(), // capacity is enforced by inFlight, not this queue
|
||||
lowPriorityThreadFactory());
|
||||
this.dispatcher.allowCoreThreadTimeOut(true);
|
||||
this.watchdog = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "PixelPics-render-watchdog");
|
||||
@@ -98,7 +104,8 @@ public final class RenderManager {
|
||||
public <T> void dispatch(UUID user, Function<AtomicBoolean, T> work, Consumer<T> onSuccess, Runnable onFailure) {
|
||||
dispatcher.execute(() -> {
|
||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||
ScheduledFuture<?> deadline = watchdog.schedule(() -> cancelled.set(true), timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> deadline =
|
||||
watchdog.schedule(() -> cancelled.set(true), timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
T result = null;
|
||||
boolean ok = false;
|
||||
try {
|
||||
@@ -109,8 +116,9 @@ public final class RenderManager {
|
||||
} finally {
|
||||
deadline.cancel(false);
|
||||
if (cancelled.get()) {
|
||||
plugin.getLogger().warning("Render for " + user + " aborted after "
|
||||
+ (timeoutMillis / 1000) + "s (timeout).");
|
||||
plugin.getLogger()
|
||||
.warning(
|
||||
"Render for " + user + " aborted after " + (timeoutMillis / 1000) + "s (timeout).");
|
||||
}
|
||||
release(user);
|
||||
}
|
||||
|
||||
@@ -17,30 +17,30 @@ public final class Affine {
|
||||
}
|
||||
|
||||
public static Affine identity() {
|
||||
return new Affine(new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[]{0, 0, 0});
|
||||
return new Affine(new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[] {0, 0, 0});
|
||||
}
|
||||
|
||||
public static Affine translation(double x, double y, double z) {
|
||||
return new Affine(new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[]{x, y, z});
|
||||
return new Affine(new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[] {x, y, z});
|
||||
}
|
||||
|
||||
public static Affine scale(double s) {
|
||||
return new Affine(new double[]{s, 0, 0, 0, s, 0, 0, 0, s}, new double[]{0, 0, 0});
|
||||
return new Affine(new double[] {s, 0, 0, 0, s, 0, 0, 0, s}, new double[] {0, 0, 0});
|
||||
}
|
||||
|
||||
public static Affine rotX(double a) {
|
||||
double c = Math.cos(a), s = Math.sin(a);
|
||||
return new Affine(new double[]{1, 0, 0, 0, c, -s, 0, s, c}, new double[]{0, 0, 0});
|
||||
return new Affine(new double[] {1, 0, 0, 0, c, -s, 0, s, c}, new double[] {0, 0, 0});
|
||||
}
|
||||
|
||||
public static Affine rotY(double a) {
|
||||
double c = Math.cos(a), s = Math.sin(a);
|
||||
return new Affine(new double[]{c, 0, s, 0, 1, 0, -s, 0, c}, new double[]{0, 0, 0});
|
||||
return new Affine(new double[] {c, 0, s, 0, 1, 0, -s, 0, c}, new double[] {0, 0, 0});
|
||||
}
|
||||
|
||||
public static Affine rotZ(double a) {
|
||||
double c = Math.cos(a), s = Math.sin(a);
|
||||
return new Affine(new double[]{c, -s, 0, s, c, 0, 0, 0, 1}, new double[]{0, 0, 0});
|
||||
return new Affine(new double[] {c, -s, 0, s, c, 0, 0, 0, 1}, new double[] {0, 0, 0});
|
||||
}
|
||||
|
||||
/** this ∘ o (apply o first, then this). */
|
||||
@@ -53,7 +53,7 @@ public final class Affine {
|
||||
}
|
||||
}
|
||||
double[] ot = o.t;
|
||||
double[] nt = new double[]{
|
||||
double[] nt = new double[] {
|
||||
a[0] * ot[0] + a[1] * ot[1] + a[2] * ot[2] + this.t[0],
|
||||
a[3] * ot[0] + a[4] * ot[1] + a[5] * ot[2] + this.t[1],
|
||||
a[6] * ot[0] + a[7] * ot[1] + a[8] * ot[2] + this.t[2]
|
||||
@@ -62,7 +62,7 @@ public final class Affine {
|
||||
}
|
||||
|
||||
public double[] apply(double x, double y, double z) {
|
||||
return new double[]{
|
||||
return new double[] {
|
||||
r[0] * x + r[1] * y + r[2] * z + t[0],
|
||||
r[3] * x + r[4] * y + r[5] * z + t[1],
|
||||
r[6] * x + r[7] * y + r[8] * z + t[2]
|
||||
@@ -71,10 +71,8 @@ public final class Affine {
|
||||
|
||||
/** Linear part only (for directions). */
|
||||
public double[] applyLinear(double x, double y, double z) {
|
||||
return new double[]{
|
||||
r[0] * x + r[1] * y + r[2] * z,
|
||||
r[3] * x + r[4] * y + r[5] * z,
|
||||
r[6] * x + r[7] * y + r[8] * z
|
||||
return new double[] {
|
||||
r[0] * x + r[1] * y + r[2] * z, r[3] * x + r[4] * y + r[5] * z, r[6] * x + r[7] * y + r[8] * z
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,12 +81,12 @@ public final class Affine {
|
||||
double a = r[0], b = r[1], c = r[2], d = r[3], e = r[4], f = r[5], g = r[6], h = r[7], i = r[8];
|
||||
double det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
||||
double inv = Math.abs(det) < 1e-12 ? 0 : 1.0 / det;
|
||||
double[] ir = new double[]{
|
||||
double[] ir = new double[] {
|
||||
(e * i - f * h) * inv, (c * h - b * i) * inv, (b * f - c * e) * inv,
|
||||
(f * g - d * i) * inv, (a * i - c * g) * inv, (c * d - a * f) * inv,
|
||||
(d * h - e * g) * inv, (b * g - a * h) * inv, (a * e - b * d) * inv
|
||||
};
|
||||
double[] it = new double[]{
|
||||
double[] it = new double[] {
|
||||
-(ir[0] * t[0] + ir[1] * t[1] + ir[2] * t[2]),
|
||||
-(ir[3] * t[0] + ir[4] * t[1] + ir[5] * t[2]),
|
||||
-(ir[6] * t[0] + ir[7] * t[1] + ir[8] * t[2])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -17,7 +16,9 @@ public final class BlockEntityModels {
|
||||
/** The CEM ({@code .jem}) model name for a block-entity. */
|
||||
public static String cemModel(BlockEntityState s) {
|
||||
return switch (s.kind()) {
|
||||
case CHEST, TRAPPED_CHEST, ENDER_CHEST -> switch (s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind()) {
|
||||
case CHEST, TRAPPED_CHEST, ENDER_CHEST -> switch (s.chestKind() == null
|
||||
? BlockEntityState.ChestKind.SINGLE
|
||||
: s.chestKind()) {
|
||||
case LEFT -> "chest_left";
|
||||
case RIGHT -> "chest_right";
|
||||
case SINGLE -> "chest";
|
||||
@@ -56,7 +57,7 @@ public final class BlockEntityModels {
|
||||
case SIGN, WALL_SIGN -> {
|
||||
String wood = s.wood() == null ? "oak" : s.wood();
|
||||
paths.add("entity/signs/" + wood);
|
||||
paths.add("entity/sign"); // legacy single-texture fallback
|
||||
paths.add("entity/sign"); // legacy single-texture fallback
|
||||
}
|
||||
case HANGING_SIGN -> {
|
||||
String wood = s.wood() == null ? "oak" : s.wood();
|
||||
@@ -69,7 +70,7 @@ public final class BlockEntityModels {
|
||||
}
|
||||
case SHULKER_BOX -> {
|
||||
if (s.colorName() != null) paths.add("entity/shulker/shulker_" + s.colorName());
|
||||
paths.add("entity/shulker/shulker"); // uncoloured (purpur) default
|
||||
paths.add("entity/shulker/shulker"); // uncoloured (purpur) default
|
||||
}
|
||||
case CONDUIT -> paths.add("entity/conduit/base");
|
||||
case DECORATED_POT -> paths.add("entity/decorated_pot/decorated_pot_base");
|
||||
@@ -92,7 +93,10 @@ public final class BlockEntityModels {
|
||||
}
|
||||
|
||||
private static void headTextures(List<String> paths, String headType) {
|
||||
if (headType == null) { paths.add("entity/skeleton/skeleton"); return; }
|
||||
if (headType == null) {
|
||||
paths.add("entity/skeleton/skeleton");
|
||||
return;
|
||||
}
|
||||
switch (headType) {
|
||||
case "wither_skeleton" -> paths.add("entity/skeleton/wither_skeleton");
|
||||
case "zombie" -> paths.add("entity/zombie/zombie");
|
||||
|
||||
@@ -12,36 +12,59 @@ import java.util.List;
|
||||
* facing/rotation, vanilla convention). Type-specific fields are null/0/empty when unused.
|
||||
*/
|
||||
public record BlockEntityState(
|
||||
Kind kind,
|
||||
int bx, int by, int bz,
|
||||
float facingDeg,
|
||||
ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/ENDER_CHEST)
|
||||
int baseColorArgb, // banner base tint (white texture); 0 = none
|
||||
String colorName, // bed/shulker/banner colour variant ("red", "white", …); null = default
|
||||
String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default
|
||||
BedPart bedPart, // bed half
|
||||
String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player"
|
||||
String skinUrl, // player-head owner skin URL; null otherwise
|
||||
List<BannerPattern> patterns, // banner overlay patterns (may be empty)
|
||||
List<String> sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty)
|
||||
BellAttach bellAttach, // bell attachment; null when not a bell
|
||||
SignText frontText, // sign front-side text; null when not a sign or blank
|
||||
SignText backText // sign back-side text; null when not a sign or blank
|
||||
) {
|
||||
Kind kind,
|
||||
int bx,
|
||||
int by,
|
||||
int bz,
|
||||
float facingDeg,
|
||||
ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/ENDER_CHEST)
|
||||
int baseColorArgb, // banner base tint (white texture); 0 = none
|
||||
String colorName, // bed/shulker/banner colour variant ("red", "white", …); null = default
|
||||
String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default
|
||||
BedPart bedPart, // bed half
|
||||
String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player"
|
||||
String skinUrl, // player-head owner skin URL; null otherwise
|
||||
List<BannerPattern> patterns, // banner overlay patterns (may be empty)
|
||||
List<String> sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty)
|
||||
BellAttach bellAttach, // bell attachment; null when not a bell
|
||||
SignText frontText, // sign front-side text; null when not a sign or blank
|
||||
SignText backText // sign back-side text; null when not a sign or blank
|
||||
) {
|
||||
public enum Kind {
|
||||
CHEST, TRAPPED_CHEST, ENDER_CHEST,
|
||||
SIGN, WALL_SIGN, HANGING_SIGN,
|
||||
BANNER, WALL_BANNER,
|
||||
BED, SHULKER_BOX,
|
||||
HEAD, WALL_HEAD,
|
||||
CONDUIT, DECORATED_POT, BELL
|
||||
CHEST,
|
||||
TRAPPED_CHEST,
|
||||
ENDER_CHEST,
|
||||
SIGN,
|
||||
WALL_SIGN,
|
||||
HANGING_SIGN,
|
||||
BANNER,
|
||||
WALL_BANNER,
|
||||
BED,
|
||||
SHULKER_BOX,
|
||||
HEAD,
|
||||
WALL_HEAD,
|
||||
CONDUIT,
|
||||
DECORATED_POT,
|
||||
BELL
|
||||
}
|
||||
|
||||
public enum ChestKind { SINGLE, LEFT, RIGHT }
|
||||
public enum ChestKind {
|
||||
SINGLE,
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
public enum BedPart { HEAD, FOOT }
|
||||
public enum BedPart {
|
||||
HEAD,
|
||||
FOOT
|
||||
}
|
||||
|
||||
public enum BellAttach { FLOOR, CEILING, SINGLE_WALL, DOUBLE_WALL }
|
||||
public enum BellAttach {
|
||||
FLOOR,
|
||||
CEILING,
|
||||
SINGLE_WALL,
|
||||
DOUBLE_WALL
|
||||
}
|
||||
|
||||
/** One banner overlay layer: a pattern key (e.g. "stripe_top") and the ARGB colour it is dyed with. */
|
||||
public record BannerPattern(String patternKey, int colorArgb) {}
|
||||
|
||||
@@ -46,8 +46,13 @@ public final class BoxUv {
|
||||
double[] down = {u + dz + 2 * dx, v, -dx, dz};
|
||||
|
||||
if (cube.mirror) {
|
||||
for (double[] f : new double[][]{east, west, up, down, south, north}) { f[0] += f[2]; f[2] = -f[2]; }
|
||||
double[] tmp = east; east = west; west = tmp; // mirror swaps the left/right faces
|
||||
for (double[] f : new double[][] {east, west, up, down, south, north}) {
|
||||
f[0] += f[2];
|
||||
f[2] = -f[2];
|
||||
}
|
||||
double[] tmp = east;
|
||||
east = west;
|
||||
west = tmp; // mirror swaps the left/right faces
|
||||
}
|
||||
|
||||
Face[] faces = new Face[6];
|
||||
|
||||
@@ -4,7 +4,6 @@ import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,7 +28,8 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
||||
}
|
||||
|
||||
private RenderedEntity bakePainting(DecorationState s) {
|
||||
int[][] art = textures.get(ResourceLocation.parse("painting/" + s.paintingArt())).orElse(null);
|
||||
int[][] art = textures.get(ResourceLocation.parse("painting/" + s.paintingArt()))
|
||||
.orElse(null);
|
||||
if (art == null) return null;
|
||||
int[][] back = textures.get(ResourceLocation.parse("painting/back")).orElse(art);
|
||||
|
||||
@@ -53,10 +53,12 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
||||
private RenderedEntity bakeItemFrame(DecorationState s) {
|
||||
String frameTex = s.glow() ? "block/glow_item_frame" : "block/item_frame";
|
||||
int[][] leather = textures.get(ResourceLocation.parse(frameTex)).orElse(null);
|
||||
int[][] wood = textures.get(ResourceLocation.parse("block/birch_planks")).orElse(leather);
|
||||
int[][] wood =
|
||||
textures.get(ResourceLocation.parse("block/birch_planks")).orElse(leather);
|
||||
if (leather == null) return null;
|
||||
|
||||
Affine toWorld = Affine.translation(faceCenterX(s), faceCenterY(s), faceCenterZ(s)).mul(facingRotation(s.facing()));
|
||||
Affine toWorld = Affine.translation(faceCenterX(s), faceCenterY(s), faceCenterZ(s))
|
||||
.mul(facingRotation(s.facing()));
|
||||
int si = Direction.SOUTH.ordinal();
|
||||
|
||||
List<EntityCube> cubes = new ArrayList<>(3);
|
||||
@@ -83,7 +85,7 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
||||
|
||||
/** A local-space corner in model pixels (1/16 block); z is the outward (front) offset from the wall. */
|
||||
private static double[] px(double x, double y, double z) {
|
||||
return new double[]{x / 16.0, y / 16.0, z / 16.0};
|
||||
return new double[] {x / 16.0, y / 16.0, z / 16.0};
|
||||
}
|
||||
|
||||
/** Item sprite: generated items live under item/, block items fall back to block/. */
|
||||
|
||||
@@ -7,19 +7,31 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
* already encodes vanilla's awkward multi-block painting offset), avoiding placement math.
|
||||
*/
|
||||
public record DecorationState(
|
||||
Kind kind,
|
||||
double minX, double minY, double minZ,
|
||||
double maxX, double maxY, double maxZ,
|
||||
Facing facing, // direction the front faces (away from the wall)
|
||||
String paintingArt, // painting art asset key (texture painting/<art>); null for frames
|
||||
String itemId, // item-frame contents material key (e.g. "diamond"); null if empty
|
||||
int itemRotationDeg, // item-frame content rotation (0/45/…/315)
|
||||
boolean glow // glow item frame
|
||||
) {
|
||||
public enum Kind { PAINTING, ITEM_FRAME }
|
||||
Kind kind,
|
||||
double minX,
|
||||
double minY,
|
||||
double minZ,
|
||||
double maxX,
|
||||
double maxY,
|
||||
double maxZ,
|
||||
Facing facing, // direction the front faces (away from the wall)
|
||||
String paintingArt, // painting art asset key (texture painting/<art>); null for frames
|
||||
String itemId, // item-frame contents material key (e.g. "diamond"); null if empty
|
||||
int itemRotationDeg, // item-frame content rotation (0/45/…/315)
|
||||
boolean glow // glow item frame
|
||||
) {
|
||||
public enum Kind {
|
||||
PAINTING,
|
||||
ITEM_FRAME
|
||||
}
|
||||
|
||||
public enum Facing {
|
||||
NORTH, SOUTH, EAST, WEST, UP, DOWN;
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST,
|
||||
UP,
|
||||
DOWN;
|
||||
|
||||
/** The axis this face's normal lies on: 0=x, 1=y, 2=z. */
|
||||
public int axis() {
|
||||
|
||||
@@ -8,11 +8,11 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
* broad-phase culling.
|
||||
*/
|
||||
public final class EntityCube {
|
||||
public final double[] from; // local min (px, inflated)
|
||||
public final double[] to; // local max
|
||||
public final Face[] faces; // by Direction.ordinal()
|
||||
public final double[] from; // local min (px, inflated)
|
||||
public final double[] to; // local max
|
||||
public final Face[] faces; // by Direction.ordinal()
|
||||
public final Affine toWorld;
|
||||
public final Affine toLocal; // inverse
|
||||
public final Affine toLocal; // inverse
|
||||
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||||
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ public final class EntityIntersector {
|
||||
|
||||
private EntityIntersector() {}
|
||||
|
||||
public static FaceHit intersect(EntityCube cube, double ox, double oy, double oz,
|
||||
double dx, double dy, double dz) {
|
||||
public static FaceHit intersect(EntityCube cube, double ox, double oy, double oz, double dx, double dy, double dz) {
|
||||
double[] o = cube.toLocal.apply(ox, oy, oz);
|
||||
double[] d = cube.toLocal.applyLinear(dx, dy, dz);
|
||||
|
||||
@@ -36,23 +35,33 @@ public final class EntityIntersector {
|
||||
double t2 = (cube.to[a] - o[a]) * inv;
|
||||
boolean n = true;
|
||||
if (t1 > t2) {
|
||||
double tmp = t1; t1 = t2; t2 = tmp;
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
n = false;
|
||||
}
|
||||
if (t1 > tmin) { tmin = t1; axis = a; neg = n; }
|
||||
if (t1 > tmin) {
|
||||
tmin = t1;
|
||||
axis = a;
|
||||
neg = n;
|
||||
}
|
||||
if (t2 < tmax) tmax = t2;
|
||||
if (tmin > tmax) return null;
|
||||
}
|
||||
if (axis < 0) return null;
|
||||
double t = tmin;
|
||||
if (t < EPS) { t = tmax; if (t < EPS) return null; }
|
||||
if (t < EPS) {
|
||||
t = tmax;
|
||||
if (t < EPS) return null;
|
||||
}
|
||||
|
||||
double px = o[0] + d[0] * t, py = o[1] + d[1] * t, pz = o[2] + d[2] * t;
|
||||
Direction dir = switch (axis) {
|
||||
case 0 -> neg ? Direction.WEST : Direction.EAST;
|
||||
case 1 -> neg ? Direction.DOWN : Direction.UP;
|
||||
default -> neg ? Direction.NORTH : Direction.SOUTH;
|
||||
};
|
||||
Direction dir =
|
||||
switch (axis) {
|
||||
case 0 -> neg ? Direction.WEST : Direction.EAST;
|
||||
case 1 -> neg ? Direction.DOWN : Direction.UP;
|
||||
default -> neg ? Direction.NORTH : Direction.SOUTH;
|
||||
};
|
||||
Face face = cube.faces[dir.ordinal()];
|
||||
if (face == null) return null;
|
||||
|
||||
@@ -63,12 +72,30 @@ public final class EntityIntersector {
|
||||
// run their horizontal axis opposite to back/left (they're viewed from the other side).
|
||||
double s, tt;
|
||||
switch (dir) {
|
||||
case UP -> { s = fx; tt = fz; }
|
||||
case DOWN -> { s = fx; tt = 1 - fz; }
|
||||
case NORTH -> { s = 1 - fx; tt = 1 - fy; }
|
||||
case SOUTH -> { s = fx; tt = 1 - fy; }
|
||||
case EAST -> { s = 1 - fz; tt = 1 - fy; }
|
||||
default -> { s = fz; tt = 1 - fy; } // WEST
|
||||
case UP -> {
|
||||
s = fx;
|
||||
tt = fz;
|
||||
}
|
||||
case DOWN -> {
|
||||
s = fx;
|
||||
tt = 1 - fz;
|
||||
}
|
||||
case NORTH -> {
|
||||
s = 1 - fx;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
case SOUTH -> {
|
||||
s = fx;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
case EAST -> {
|
||||
s = 1 - fz;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
default -> {
|
||||
s = fz;
|
||||
tt = 1 - fy;
|
||||
} // WEST
|
||||
}
|
||||
int color = face.sample(s, tt);
|
||||
if (ColorUtil.alpha(color) <= ALPHA_THRESHOLD) return null;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -18,33 +17,32 @@ public final class EntityModels {
|
||||
// Type key -> CEM (.jem) model name. Default is the type key itself; these are the exceptions
|
||||
// (mob reuses another mob's model, or the CEM set only ships a version-suffixed/renamed name).
|
||||
private static final Map<String, String> CEM_OVERRIDE = Map.ofEntries(
|
||||
Map.entry("husk", "zombie"),
|
||||
Map.entry("giant", "zombie"),
|
||||
Map.entry("mooshroom", "cow"),
|
||||
Map.entry("ocelot", "cat"),
|
||||
Map.entry("cave_spider", "spider"),
|
||||
Map.entry("elder_guardian", "guardian"),
|
||||
Map.entry("glow_squid", "squid"),
|
||||
Map.entry("mule", "donkey"),
|
||||
Map.entry("skeleton_horse", "horse"),
|
||||
Map.entry("zombie_horse", "horse"),
|
||||
Map.entry("trader_llama", "llama"),
|
||||
Map.entry("stray", "skeleton"),
|
||||
Map.entry("wither_skeleton", "skeleton"),
|
||||
Map.entry("zoglin", "hoglin"),
|
||||
Map.entry("piglin_brute", "piglin"),
|
||||
Map.entry("zombified_piglin", "piglin"),
|
||||
Map.entry("evoker", "illager"),
|
||||
Map.entry("vindicator", "illager"),
|
||||
Map.entry("illusioner", "illager"),
|
||||
Map.entry("wandering_trader", "villager"),
|
||||
Map.entry("ender_dragon", "dragon"),
|
||||
Map.entry("mannequin", "player"),
|
||||
Map.entry("camel_husk", "camel"),
|
||||
Map.entry("rabbit", "rabbit_21.11"),
|
||||
Map.entry("pufferfish", "puffer_fish_big"),
|
||||
Map.entry("tropical_fish", "tropical_fish_a")
|
||||
);
|
||||
Map.entry("husk", "zombie"),
|
||||
Map.entry("giant", "zombie"),
|
||||
Map.entry("mooshroom", "cow"),
|
||||
Map.entry("ocelot", "cat"),
|
||||
Map.entry("cave_spider", "spider"),
|
||||
Map.entry("elder_guardian", "guardian"),
|
||||
Map.entry("glow_squid", "squid"),
|
||||
Map.entry("mule", "donkey"),
|
||||
Map.entry("skeleton_horse", "horse"),
|
||||
Map.entry("zombie_horse", "horse"),
|
||||
Map.entry("trader_llama", "llama"),
|
||||
Map.entry("stray", "skeleton"),
|
||||
Map.entry("wither_skeleton", "skeleton"),
|
||||
Map.entry("zoglin", "hoglin"),
|
||||
Map.entry("piglin_brute", "piglin"),
|
||||
Map.entry("zombified_piglin", "piglin"),
|
||||
Map.entry("evoker", "illager"),
|
||||
Map.entry("vindicator", "illager"),
|
||||
Map.entry("illusioner", "illager"),
|
||||
Map.entry("wandering_trader", "villager"),
|
||||
Map.entry("ender_dragon", "dragon"),
|
||||
Map.entry("mannequin", "player"),
|
||||
Map.entry("camel_husk", "camel"),
|
||||
Map.entry("rabbit", "rabbit_21.11"),
|
||||
Map.entry("pufferfish", "puffer_fish_big"),
|
||||
Map.entry("tropical_fish", "tropical_fish_a"));
|
||||
|
||||
/** The CEM model name for an entity type (boats/rafts share the boat hull). */
|
||||
public static String cemModel(String typeKey) {
|
||||
@@ -53,64 +51,63 @@ public final class EntityModels {
|
||||
}
|
||||
|
||||
// Type key -> texture path override (where the first derived candidate is wrong).
|
||||
private static final Map<String, String> TEX_OVERRIDE = Map.ofEntries(
|
||||
Map.entry("cow", "entity/cow/cow_temperate"),
|
||||
Map.entry("mooshroom", "entity/cow/mooshroom_red"),
|
||||
Map.entry("zombie", "entity/zombie/zombie"),
|
||||
Map.entry("husk", "entity/zombie/husk"),
|
||||
Map.entry("drowned", "entity/zombie/drowned"),
|
||||
Map.entry("zombified_piglin", "entity/piglin/zombified_piglin"),
|
||||
Map.entry("skeleton", "entity/skeleton/skeleton"),
|
||||
Map.entry("stray", "entity/skeleton/stray"),
|
||||
Map.entry("wither_skeleton", "entity/skeleton/wither_skeleton"),
|
||||
Map.entry("creeper", "entity/creeper/creeper"),
|
||||
Map.entry("spider", "entity/spider/spider"),
|
||||
Map.entry("enderman", "entity/enderman/enderman"),
|
||||
Map.entry("player", "entity/player/wide/steve"),
|
||||
// Textures whose folder/name doesn't follow the "entity/<key>/<key>" pattern.
|
||||
Map.entry("iron_golem", "entity/iron_golem/iron_golem"),
|
||||
Map.entry("polar_bear", "entity/bear/polarbear"),
|
||||
Map.entry("ender_dragon", "entity/enderdragon/dragon"),
|
||||
Map.entry("magma_cube", "entity/slime/magmacube"),
|
||||
Map.entry("tropical_fish", "entity/fish/tropical_a"),
|
||||
Map.entry("bogged", "entity/skeleton/bogged"),
|
||||
Map.entry("donkey", "entity/horse/donkey"),
|
||||
Map.entry("mule", "entity/horse/mule"),
|
||||
Map.entry("skeleton_horse", "entity/horse/horse_skeleton"),
|
||||
Map.entry("zombie_horse", "entity/horse/horse_zombie"),
|
||||
Map.entry("trader_llama", "entity/llama/llama_creamy"),
|
||||
Map.entry("cave_spider", "entity/spider/cave_spider"),
|
||||
Map.entry("guardian", "entity/guardian/guardian"),
|
||||
Map.entry("elder_guardian", "entity/guardian/guardian_elder"),
|
||||
Map.entry("piglin_brute", "entity/piglin/piglin_brute"),
|
||||
Map.entry("zoglin", "entity/hoglin/zoglin"),
|
||||
Map.entry("illusioner", "entity/illager/illusioner"),
|
||||
Map.entry("giant", "entity/zombie/zombie"),
|
||||
// Illagers share one texture folder; none follow the entity/<key>/<key> pattern.
|
||||
Map.entry("pillager", "entity/illager/pillager"),
|
||||
Map.entry("vindicator", "entity/illager/vindicator"),
|
||||
Map.entry("evoker", "entity/illager/evoker"),
|
||||
Map.entry("ravager", "entity/illager/ravager"),
|
||||
Map.entry("vex", "entity/illager/vex"),
|
||||
// Fish share entity/fish/; squids share entity/squid/.
|
||||
Map.entry("cod", "entity/fish/cod"),
|
||||
Map.entry("salmon", "entity/fish/salmon"),
|
||||
Map.entry("pufferfish", "entity/fish/pufferfish"),
|
||||
Map.entry("glow_squid", "entity/squid/glow_squid"),
|
||||
// Variant-only textures with no plain base file — pick a sensible default variant.
|
||||
Map.entry("cat", "entity/cat/cat_tabby"),
|
||||
Map.entry("ocelot", "entity/cat/ocelot"), // ocelot texture lives in the cat folder now
|
||||
Map.entry("axolotl", "entity/axolotl/axolotl_wild"),
|
||||
Map.entry("parrot", "entity/parrot/parrot_red_blue"),
|
||||
Map.entry("turtle", "entity/turtle/turtle"),
|
||||
Map.entry("wind_charge", "entity/projectiles/wind_charge"),
|
||||
Map.entry("camel_husk", "entity/camel/camel_husk"),
|
||||
Map.entry("armor_stand", "entity/armorstand/armorstand"), // texture folder is "armorstand"
|
||||
Map.entry("happy_ghast", "entity/ghast/happy_ghast"),
|
||||
Map.entry("parched", "entity/skeleton/parched"), // husk-style skeleton, texture in skeleton/
|
||||
Map.entry("zombie_nautilus_coral", "entity/nautilus/zombie_nautilus_coral"),
|
||||
Map.entry("mannequin", "entity/player/wide/steve")
|
||||
);
|
||||
private static final Map<String, String> TEX_OVERRIDE = Map.<String, String>ofEntries(
|
||||
Map.entry("cow", "entity/cow/cow_temperate"),
|
||||
Map.entry("mooshroom", "entity/cow/mooshroom_red"),
|
||||
Map.entry("zombie", "entity/zombie/zombie"),
|
||||
Map.entry("husk", "entity/zombie/husk"),
|
||||
Map.entry("drowned", "entity/zombie/drowned"),
|
||||
Map.entry("zombified_piglin", "entity/piglin/zombified_piglin"),
|
||||
Map.entry("skeleton", "entity/skeleton/skeleton"),
|
||||
Map.entry("stray", "entity/skeleton/stray"),
|
||||
Map.entry("wither_skeleton", "entity/skeleton/wither_skeleton"),
|
||||
Map.entry("creeper", "entity/creeper/creeper"),
|
||||
Map.entry("spider", "entity/spider/spider"),
|
||||
Map.entry("enderman", "entity/enderman/enderman"),
|
||||
Map.entry("player", "entity/player/wide/steve"),
|
||||
// Textures whose folder/name doesn't follow the "entity/<key>/<key>" pattern.
|
||||
Map.entry("iron_golem", "entity/iron_golem/iron_golem"),
|
||||
Map.entry("polar_bear", "entity/bear/polarbear"),
|
||||
Map.entry("ender_dragon", "entity/enderdragon/dragon"),
|
||||
Map.entry("magma_cube", "entity/slime/magmacube"),
|
||||
Map.entry("tropical_fish", "entity/fish/tropical_a"),
|
||||
Map.entry("bogged", "entity/skeleton/bogged"),
|
||||
Map.entry("donkey", "entity/horse/donkey"),
|
||||
Map.entry("mule", "entity/horse/mule"),
|
||||
Map.entry("skeleton_horse", "entity/horse/horse_skeleton"),
|
||||
Map.entry("zombie_horse", "entity/horse/horse_zombie"),
|
||||
Map.entry("trader_llama", "entity/llama/llama_creamy"),
|
||||
Map.entry("cave_spider", "entity/spider/cave_spider"),
|
||||
Map.entry("guardian", "entity/guardian/guardian"),
|
||||
Map.entry("elder_guardian", "entity/guardian/guardian_elder"),
|
||||
Map.entry("piglin_brute", "entity/piglin/piglin_brute"),
|
||||
Map.entry("zoglin", "entity/hoglin/zoglin"),
|
||||
Map.entry("illusioner", "entity/illager/illusioner"),
|
||||
Map.entry("giant", "entity/zombie/zombie"),
|
||||
// Illagers share one texture folder; none follow the entity/<key>/<key> pattern.
|
||||
Map.entry("pillager", "entity/illager/pillager"),
|
||||
Map.entry("vindicator", "entity/illager/vindicator"),
|
||||
Map.entry("evoker", "entity/illager/evoker"),
|
||||
Map.entry("ravager", "entity/illager/ravager"),
|
||||
Map.entry("vex", "entity/illager/vex"),
|
||||
// Fish share entity/fish/; squids share entity/squid/.
|
||||
Map.entry("cod", "entity/fish/cod"),
|
||||
Map.entry("salmon", "entity/fish/salmon"),
|
||||
Map.entry("pufferfish", "entity/fish/pufferfish"),
|
||||
Map.entry("glow_squid", "entity/squid/glow_squid"),
|
||||
// Variant-only textures with no plain base file — pick a sensible default variant.
|
||||
Map.entry("cat", "entity/cat/cat_tabby"),
|
||||
Map.entry("ocelot", "entity/cat/ocelot"), // ocelot texture lives in the cat folder now
|
||||
Map.entry("axolotl", "entity/axolotl/axolotl_wild"),
|
||||
Map.entry("parrot", "entity/parrot/parrot_red_blue"),
|
||||
Map.entry("turtle", "entity/turtle/turtle"),
|
||||
Map.entry("wind_charge", "entity/projectiles/wind_charge"),
|
||||
Map.entry("camel_husk", "entity/camel/camel_husk"),
|
||||
Map.entry("armor_stand", "entity/armorstand/armorstand"), // texture folder is "armorstand"
|
||||
Map.entry("happy_ghast", "entity/ghast/happy_ghast"),
|
||||
Map.entry("parched", "entity/skeleton/parched"), // husk-style skeleton, texture in skeleton/
|
||||
Map.entry("zombie_nautilus_coral", "entity/nautilus/zombie_nautilus_coral"),
|
||||
Map.entry("mannequin", "entity/player/wide/steve"));
|
||||
|
||||
/** Ordered texture-path candidates; the baker uses the first that loads. */
|
||||
public static List<ResourceLocation> textureCandidates(String typeKey, String variant) {
|
||||
@@ -139,30 +136,30 @@ public final class EntityModels {
|
||||
*/
|
||||
private static List<String> variantPaths(String typeKey, String v) {
|
||||
return switch (typeKey) {
|
||||
case "cat" -> List.of("entity/cat/cat_" + v);
|
||||
case "axolotl" -> List.of("entity/axolotl/axolotl_" + v);
|
||||
case "wolf" -> List.of("entity/wolf/wolf_" + v, "entity/wolf/wolf");
|
||||
case "horse" -> List.of("entity/horse/horse_" + HORSE_COLOR.getOrDefault(v, v));
|
||||
case "llama" -> List.of("entity/llama/llama_" + v);
|
||||
case "cow" -> List.of("entity/cow/cow_" + v);
|
||||
case "pig" -> List.of("entity/pig/pig_" + v);
|
||||
case "chicken" -> List.of("entity/chicken/chicken_" + v);
|
||||
case "frog" -> List.of("entity/frog/frog_" + v);
|
||||
case "panda" -> List.of(v.equals("normal") ? "entity/panda/panda" : "entity/panda/panda_" + v);
|
||||
case "fox" -> List.of(v.equals("snow") ? "entity/fox/fox_snow" : "entity/fox/fox");
|
||||
case "parrot" -> List.of("entity/parrot/parrot_" + PARROT_COLOR.getOrDefault(v, v));
|
||||
case "rabbit" -> List.of("entity/rabbit/rabbit_" + RABBIT_TYPE.getOrDefault(v, v));
|
||||
case "mooshroom" -> List.of("entity/cow/mooshroom_" + v);
|
||||
case "shulker" -> List.of("entity/shulker/shulker_" + v);
|
||||
// villager/zombie_villager: type/<biome> and profession are transparent OVERLAYS (clothing
|
||||
// only); the opaque base body is entity/<folder>/<folder> — handled by the generic candidates.
|
||||
default -> List.of();
|
||||
case "cat" -> List.of("entity/cat/cat_" + v);
|
||||
case "axolotl" -> List.of("entity/axolotl/axolotl_" + v);
|
||||
case "wolf" -> List.of("entity/wolf/wolf_" + v, "entity/wolf/wolf");
|
||||
case "horse" -> List.of("entity/horse/horse_" + HORSE_COLOR.getOrDefault(v, v));
|
||||
case "llama" -> List.of("entity/llama/llama_" + v);
|
||||
case "cow" -> List.of("entity/cow/cow_" + v);
|
||||
case "pig" -> List.of("entity/pig/pig_" + v);
|
||||
case "chicken" -> List.of("entity/chicken/chicken_" + v);
|
||||
case "frog" -> List.of("entity/frog/frog_" + v);
|
||||
case "panda" -> List.of(v.equals("normal") ? "entity/panda/panda" : "entity/panda/panda_" + v);
|
||||
case "fox" -> List.of(v.equals("snow") ? "entity/fox/fox_snow" : "entity/fox/fox");
|
||||
case "parrot" -> List.of("entity/parrot/parrot_" + PARROT_COLOR.getOrDefault(v, v));
|
||||
case "rabbit" -> List.of("entity/rabbit/rabbit_" + RABBIT_TYPE.getOrDefault(v, v));
|
||||
case "mooshroom" -> List.of("entity/cow/mooshroom_" + v);
|
||||
case "shulker" -> List.of("entity/shulker/shulker_" + v);
|
||||
// villager/zombie_villager: type/<biome> and profession are transparent OVERLAYS (clothing
|
||||
// only); the opaque base body is entity/<folder>/<folder> — handled by the generic candidates.
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private static final Map<String, String> HORSE_COLOR = Map.of("dark_brown", "darkbrown");
|
||||
private static final Map<String, String> PARROT_COLOR = Map.of(
|
||||
"red", "red_blue", "cyan", "yellow_blue", "gray", "grey");
|
||||
private static final Map<String, String> RABBIT_TYPE = Map.of(
|
||||
"black_and_white", "white_splotched", "salt_and_pepper", "salt", "the_killer_bunny", "caerbannog");
|
||||
private static final Map<String, String> PARROT_COLOR =
|
||||
Map.of("red", "red_blue", "cyan", "yellow_blue", "gray", "grey");
|
||||
private static final Map<String, String> RABBIT_TYPE =
|
||||
Map.of("black_and_white", "white_splotched", "salt_and_pepper", "salt", "the_killer_bunny", "caerbannog");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.FaceHit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -18,9 +17,13 @@ public final class EntityScene {
|
||||
private static final double EPS = 1e-7;
|
||||
private final List<RenderedEntity> entities;
|
||||
|
||||
public EntityScene(List<EntityState> states, CemBaker baker,
|
||||
List<BlockEntityState> blockEntities, BlockEntityBaker beBaker,
|
||||
List<DecorationState> decorations, DecorationBaker decoBaker) {
|
||||
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());
|
||||
addAll(states, baker);
|
||||
addAll(blockEntities, beBaker);
|
||||
@@ -58,8 +61,8 @@ public final class EntityScene {
|
||||
return best;
|
||||
}
|
||||
|
||||
private static boolean rayAabb(double[] min, double[] max, double ox, double oy, double oz,
|
||||
double dx, double dy, double dz, double maxT) {
|
||||
private static boolean rayAabb(
|
||||
double[] min, double[] max, double ox, double oy, double oz, double dx, double dy, double dz, double maxT) {
|
||||
double tmin = 0, tmax = maxT;
|
||||
double[] o = {ox, oy, oz}, d = {dx, dy, dz};
|
||||
for (int a = 0; a < 3; a++) {
|
||||
@@ -69,7 +72,11 @@ public final class EntityScene {
|
||||
double inv = 1.0 / d[a];
|
||||
double t1 = (min[a] - o[a]) * inv;
|
||||
double t2 = (max[a] - o[a]) * inv;
|
||||
if (t1 > t2) { double tmp = t1; t1 = t2; t2 = tmp; }
|
||||
if (t1 > t2) {
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
if (t1 > tmin) tmin = t1;
|
||||
if (t2 < tmax) tmax = t2;
|
||||
if (tmin > tmax) return false;
|
||||
|
||||
@@ -5,24 +5,31 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
* off-thread. Angles are in degrees (Minecraft convention).
|
||||
*/
|
||||
public record EntityState(
|
||||
String typeKey, // e.g. "cow", "zombie", "player"
|
||||
double x, double y, double z,
|
||||
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"); for villagers the biome type, 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
|
||||
String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null
|
||||
int villagerLevel, // villager profession level 1-5 (badge tier); 0 = none/unknown
|
||||
String markings, // horse coat markings style (e.g. "white", "whitefield", "whitedots", "blackdots"), or null
|
||||
boolean saddle, // horse/donkey/mule is saddled
|
||||
boolean chest, // donkey/mule/llama is carrying a chest
|
||||
String bodyEquip, // horse armor material (iron/gold/diamond/leather) OR llama carpet colour OR "trader_llama"; null = none
|
||||
Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer
|
||||
boolean invisible // entity is invisible -> render only its equipment (like vanilla), not the body
|
||||
) {
|
||||
String typeKey, // e.g. "cow", "zombie", "player"
|
||||
double x,
|
||||
double y,
|
||||
double z,
|
||||
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"); for villagers the biome type,
|
||||
// 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
|
||||
String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null
|
||||
int villagerLevel, // villager profession level 1-5 (badge tier); 0 = none/unknown
|
||||
String markings, // horse coat markings style (e.g. "white", "whitefield", "whitedots", "blackdots"), or null
|
||||
boolean saddle, // horse/donkey/mule is saddled
|
||||
boolean chest, // donkey/mule/llama is carrying a chest
|
||||
String bodyEquip, // horse armor material (iron/gold/diamond/leather) OR llama carpet colour OR "trader_llama";
|
||||
// null = none
|
||||
Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer
|
||||
boolean invisible // entity is invisible -> render only its equipment (like vanilla), not the body
|
||||
) {
|
||||
|
||||
/** Worn armor (4 slots) of a humanoid wearer; any field may be null. */
|
||||
public record Equipment(EquipPiece head, EquipPiece chest, EquipPiece legs, EquipPiece feet) {
|
||||
@@ -33,10 +40,10 @@ public record EntityState(
|
||||
|
||||
/** One worn armor piece. */
|
||||
public record EquipPiece(
|
||||
String asset, // equipment asset id, e.g. "diamond", "leather", "elytra", "turtle_scute"
|
||||
int dyeColor, // ARGB tint for dyeable (leather) armor; 0 = undyed/not dyeable
|
||||
String trimMaterial, // armor-trim material key (e.g. "diamond"); null = no trim
|
||||
String trimPattern, // armor-trim pattern key (e.g. "coast"); null = no trim
|
||||
boolean glint // item is enchanted -> render the enchantment glint
|
||||
) {}
|
||||
String asset, // equipment asset id, e.g. "diamond", "leather", "elytra", "turtle_scute"
|
||||
int dyeColor, // ARGB tint for dyeable (leather) armor; 0 = undyed/not dyeable
|
||||
String trimMaterial, // armor-trim material key (e.g. "diamond"); null = no trim
|
||||
String trimPattern, // armor-trim pattern key (e.g. "coast"); null = no trim
|
||||
boolean glint // item is enchanted -> render the enchantment glint
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
* {@code origin} is the minimum corner, {@code uv} is the box-UV texture offset.
|
||||
*/
|
||||
public final class ModelCube {
|
||||
public final double[] origin; // 3, min corner (px)
|
||||
public final double[] size; // 3 (px)
|
||||
public final double inflate; // px, expands the box on all sides (overlay layers)
|
||||
public final double[] uv; // 2, box-UV offset (texels)
|
||||
public final double[] origin; // 3, min corner (px)
|
||||
public final double[] size; // 3 (px)
|
||||
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;
|
||||
/** Optional modern per-face UV, indexed by {@code Direction.ordinal()}: {u, v, w, h} texels (h/w may be negative for flips). Null = use box-UV. */
|
||||
public final double[][] faceUv;
|
||||
|
||||
@@ -39,7 +39,10 @@ public final class TextureOps {
|
||||
int sp = src[y][x];
|
||||
int sa = (sp >>> 24) & 0xFF;
|
||||
if (sa == 0) continue;
|
||||
if (sa == 255) { dst[y][x] = sp; 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;
|
||||
@@ -73,7 +76,10 @@ public final class TextureOps {
|
||||
int c = from[i];
|
||||
int dr = pr - ((c >> 16) & 0xFF), dg = pg - ((c >> 8) & 0xFF), db = pb - (c & 0xFF);
|
||||
int d = dr * dr + dg * dg + db * db;
|
||||
if (d < bestD) { bestD = d; best = i; }
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
row[x] = (a << 24) | (to[best] & 0xFFFFFF);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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.HashSet;
|
||||
import java.util.List;
|
||||
@@ -36,13 +35,13 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
// Sign-text placement (model px in the board's local frame; calibrated against the test harness).
|
||||
// The text block is scaled to fill the board's writable area (W×H) so few-line signs are rendered as
|
||||
// large as possible (legibility on low-res maps), capped so a single line isn't blown up absurdly.
|
||||
private static final double SIGN_TEXT_W = 22.0, SIGN_TEXT_H = 11.0, SIGN_TEXT_CAP = 0.5; // board 24x12
|
||||
private static final double HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10
|
||||
private static final double SIGN_TEXT_CY = 20.0; // board Y∈[14,26], centre 20
|
||||
private static final double HANGING_TEXT_CY = 5.0; // board Y∈[0,10], centre 5
|
||||
private static final double BOARD_FRONT_Z = 1.0; // both boards: front/back faces at Z=±1
|
||||
private static final double TEXT_Z_EPS = 0.05; // lift the text just off the board face
|
||||
private static final double TEXT_THICK = 0.5; // slab thickness so the ray test never degenerates
|
||||
private static final double SIGN_TEXT_W = 22.0, SIGN_TEXT_H = 11.0, SIGN_TEXT_CAP = 0.5; // board 24x12
|
||||
private static final double HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10
|
||||
private static final double SIGN_TEXT_CY = 20.0; // board Y∈[14,26], centre 20
|
||||
private static final double HANGING_TEXT_CY = 5.0; // board Y∈[0,10], centre 5
|
||||
private static final double BOARD_FRONT_Z = 1.0; // both boards: front/back faces at Z=±1
|
||||
private static final double TEXT_Z_EPS = 0.05; // lift the text just off the board face
|
||||
private static final double TEXT_THICK = 0.5; // slab thickness so the ray test never degenerates
|
||||
|
||||
private final CemModelLoader models;
|
||||
private final TextureCache textures;
|
||||
@@ -69,12 +68,13 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
Place p = place(s);
|
||||
Affine pre = Affine.scale(p.scale / 16.0);
|
||||
Affine placement = Affine.translation(s.bx() + 0.5, s.by(), s.bz() + 0.5)
|
||||
.mul(Affine.rotY(Math.toRadians(p.yaw)))
|
||||
.mul(Affine.translation(p.lx, p.ly, p.lz));
|
||||
.mul(Affine.rotY(Math.toRadians(p.yaw)))
|
||||
.mul(Affine.translation(p.lx, p.ly, p.lz));
|
||||
|
||||
List<EntityCube> cubes = new ArrayList<>();
|
||||
for (Layer layer : layers) {
|
||||
for (CemGeometry.Baked b : CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) {
|
||||
for (CemGeometry.Baked b :
|
||||
CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) {
|
||||
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), placement.mul(b.world())));
|
||||
}
|
||||
}
|
||||
@@ -106,10 +106,18 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
addSide(s.backText(), Direction.SOUTH, tw, th, cap, cy, toWorld, cubes);
|
||||
}
|
||||
|
||||
private void addSide(BlockEntityState.SignText t, Direction faceDir, double tw, double th, double cap,
|
||||
double cy, Affine toWorld, List<EntityCube> cubes) {
|
||||
private void addSide(
|
||||
BlockEntityState.SignText t,
|
||||
Direction faceDir,
|
||||
double tw,
|
||||
double th,
|
||||
double cap,
|
||||
double cy,
|
||||
Affine toWorld,
|
||||
List<EntityCube> cubes) {
|
||||
if (t == null) return;
|
||||
int[][] bmp = SignTextRasterizer.rasterize(trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing());
|
||||
int[][] bmp = SignTextRasterizer.rasterize(
|
||||
trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing());
|
||||
if (bmp == null) return;
|
||||
// Scale so the text block fills the board's writable area, whichever dimension binds, capped.
|
||||
double fpm = Math.min(cap, Math.min(tw / bmp[0].length, th / bmp.length));
|
||||
@@ -117,10 +125,10 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
double blockH = bmp.length * fpm;
|
||||
|
||||
double z0, z1;
|
||||
if (faceDir == Direction.NORTH) { // front: −Z face, text sits just past −1
|
||||
if (faceDir == Direction.NORTH) { // front: −Z face, text sits just past −1
|
||||
z1 = -BOARD_FRONT_Z - TEXT_Z_EPS;
|
||||
z0 = z1 - TEXT_THICK;
|
||||
} else { // back: +Z face, text sits just past +1
|
||||
} else { // back: +Z face, text sits just past +1
|
||||
z0 = BOARD_FRONT_Z + TEXT_Z_EPS;
|
||||
z1 = z0 + TEXT_THICK;
|
||||
}
|
||||
@@ -156,7 +164,9 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
/** A single bake pass: one texture applied to the parts not in {@code hidden}, with optional UV size
|
||||
* and a flag forcing box-UV (for standalone part textures not matching the model's per-face UV). */
|
||||
private record Layer(int[][] tex, Set<String> hidden, int texW, int texH, boolean boxUv) {
|
||||
Layer(int[][] tex, Set<String> hidden) { this(tex, hidden, 0, 0, false); }
|
||||
Layer(int[][] tex, Set<String> hidden) {
|
||||
this(tex, hidden, 0, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Some types paint different parts with different textures (pot sherds, conduit cage/heart). */
|
||||
@@ -175,13 +185,17 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
|
||||
/** The conduit's cage and inner heart use separate textures; the eye/wind (active state) are skipped. */
|
||||
private List<Layer> conduitLayers() {
|
||||
int[][] cage = textures.get(ResourceLocation.parse("entity/conduit/cage")).orElse(null);
|
||||
int[][] base = textures.get(ResourceLocation.parse("entity/conduit/base")).orElse(null);
|
||||
int[][] cage =
|
||||
textures.get(ResourceLocation.parse("entity/conduit/cage")).orElse(null);
|
||||
int[][] base =
|
||||
textures.get(ResourceLocation.parse("entity/conduit/base")).orElse(null);
|
||||
// Each part's texture is authored at its own native size (32x16) with a box-UV layout, so force
|
||||
// box-UV and normalise by the texture's own size (the model's per-face UV assumes a combined sheet).
|
||||
List<Layer> layers = new ArrayList<>(2);
|
||||
if (cage != null) layers.add(new Layer(cage, onlyPart("cage", CONDUIT_PARTS), cage[0].length, cage.length, true));
|
||||
if (base != null) layers.add(new Layer(base, onlyPart("base", CONDUIT_PARTS), base[0].length, base.length, true));
|
||||
if (cage != null)
|
||||
layers.add(new Layer(cage, onlyPart("cage", CONDUIT_PARTS), cage[0].length, cage.length, true));
|
||||
if (base != null)
|
||||
layers.add(new Layer(base, onlyPart("base", CONDUIT_PARTS), base[0].length, base.length, true));
|
||||
return layers;
|
||||
}
|
||||
|
||||
@@ -196,8 +210,10 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
private static final String[] POT_FACES = {"front", "left", "right", "back"}; // matches sherd capture order
|
||||
|
||||
private List<Layer> potLayers(BlockEntityState s) {
|
||||
int[][] base = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_base")).orElse(null);
|
||||
int[][] side = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_side")).orElse(null);
|
||||
int[][] base = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_base"))
|
||||
.orElse(null);
|
||||
int[][] side = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_side"))
|
||||
.orElse(null);
|
||||
if (base == null) return List.of();
|
||||
List<Layer> layers = new ArrayList<>();
|
||||
// Structure (rim/neck/foot) comes from the combined base texture; the four sides are NOT in it.
|
||||
@@ -210,7 +226,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
String sherd = s.sherds().get(i);
|
||||
if (sherd != null && sherd.endsWith("_pottery_sherd")) {
|
||||
int[][] pat = textures.get(ResourceLocation.parse(
|
||||
"entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern"))).orElse(null);
|
||||
"entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern")))
|
||||
.orElse(null);
|
||||
if (pat != null) tex = pat;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +247,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
return switch (s.kind()) {
|
||||
case SIGN -> new Place(yaw, SIGN_SCALE, 0, 0, 0);
|
||||
case WALL_SIGN -> new Place(yaw, SIGN_SCALE, 0, -0.3125, 0.4375); // drop to mid-block, push to wall
|
||||
case WALL_HEAD -> new Place(yaw, 1.0, 0, 0.25, 0.25); // mid-height, against the wall
|
||||
case WALL_HEAD -> new Place(yaw, 1.0, 0, 0.25, 0.25); // mid-height, against the wall
|
||||
case WALL_BANNER -> new Place(yaw, 1.0, 0, -0.16, 0.4375);
|
||||
default -> new Place(yaw, 1.0, 0, 0, 0);
|
||||
};
|
||||
@@ -240,8 +257,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
private Set<String> hiddenParts(BlockEntityState s) {
|
||||
return switch (s.kind()) {
|
||||
case BED -> s.bedPart() == BlockEntityState.BedPart.HEAD
|
||||
? Set.of("foot", "leg3", "leg4")
|
||||
: Set.of("head", "leg1", "leg2");
|
||||
? Set.of("foot", "leg3", "leg4")
|
||||
: Set.of("head", "leg1", "leg2");
|
||||
case CONDUIT -> Set.of("eye", "wind");
|
||||
default -> Set.of();
|
||||
};
|
||||
@@ -251,7 +268,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
|
||||
private int[][] resolveTexture(BlockEntityState s) {
|
||||
// Player heads use the owner's skin when available.
|
||||
if (s.skinUrl() != null && (s.kind() == BlockEntityState.Kind.HEAD || s.kind() == BlockEntityState.Kind.WALL_HEAD)) {
|
||||
if (s.skinUrl() != null
|
||||
&& (s.kind() == BlockEntityState.Kind.HEAD || s.kind() == BlockEntityState.Kind.WALL_HEAD)) {
|
||||
int[][] skin = skins.get(s.skinUrl()).orElse(null);
|
||||
if (skin != null) return skin;
|
||||
}
|
||||
@@ -273,7 +291,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
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);
|
||||
int[][] mask = textures.get(ResourceLocation.parse("entity/banner/" + pat.patternKey()))
|
||||
.orElse(null);
|
||||
if (mask == null) continue;
|
||||
int[][] dyed = TextureOps.deepCopy(mask);
|
||||
TextureOps.tint(dyed, pat.colorArgb());
|
||||
|
||||
@@ -13,7 +13,6 @@ 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.HashSet;
|
||||
import java.util.List;
|
||||
@@ -31,9 +30,8 @@ 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 Map<String, Set<String>> HIDDEN_PARTS = Map.of(
|
||||
"armadillo", Set.of("cube"), // the rolled-up ball
|
||||
"illager", Set.of("left_arm", "right_arm")
|
||||
);
|
||||
"armadillo", Set.of("cube"), // the rolled-up ball
|
||||
"illager", Set.of("left_arm", "right_arm"));
|
||||
|
||||
private final CemModelLoader models;
|
||||
private final TextureCache textures;
|
||||
@@ -53,11 +51,11 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (s.typeKey().equals("villager") || s.typeKey().equals("zombie_villager")) {
|
||||
tex = compositeVillager(s, tex);
|
||||
} else if (cem.equals("horse")) {
|
||||
tex = compositeHorse(s, tex); // coat markings + horse armor
|
||||
tex = compositeHorse(s, tex); // coat markings + horse armor
|
||||
} else if (cem.equals("llama")) {
|
||||
tex = compositeLlama(s, tex); // dyed/trader carpet decor
|
||||
tex = compositeLlama(s, tex); // dyed/trader carpet decor
|
||||
} else if (cem.equals("nautilus")) {
|
||||
tex = compositeNautilus(s, tex); // body armor + saddle (same-UV overlays)
|
||||
tex = compositeNautilus(s, tex); // body armor + saddle (same-UV overlays)
|
||||
}
|
||||
CemModelLoader.CemModel model = models.get(cem);
|
||||
// A visible entity needs its body model+texture; an invisible one renders only its equipment
|
||||
@@ -73,13 +71,18 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
Set<String> hidden = new HashSet<>(HIDDEN_PARTS.getOrDefault(cem, Set.of()));
|
||||
// Donkeys/llamas carry the chest boxes inside the base model; hide them unless a chest is equipped.
|
||||
if (!s.chest()) {
|
||||
if (cem.equals("donkey")) { hidden.add("left_chest"); hidden.add("right_chest"); }
|
||||
else if (cem.equals("llama")) { hidden.add("chest_left"); hidden.add("chest_right"); }
|
||||
if (cem.equals("donkey")) {
|
||||
hidden.add("left_chest");
|
||||
hidden.add("right_chest");
|
||||
} else if (cem.equals("llama")) {
|
||||
hidden.add("chest_left");
|
||||
hidden.add("chest_right");
|
||||
}
|
||||
}
|
||||
// The body model is baked even when invisible — not drawn, but used as the ground-snap reference
|
||||
// so equipment stays at body height (e.g. a lone helmet sits at the head, not on the floor).
|
||||
List<CemGeometry.Baked> body = (model != null && tex != null)
|
||||
? CemGeometry.bakeModel(model, tex, pre, hidden) : List.of();
|
||||
List<CemGeometry.Baked> body =
|
||||
(model != null && tex != null) ? CemGeometry.bakeModel(model, tex, pre, hidden) : List.of();
|
||||
|
||||
List<CemGeometry.Baked> baked = new ArrayList<>();
|
||||
if (!invisible) {
|
||||
@@ -87,7 +90,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// 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);
|
||||
int[][] woolTex = textures.get(ResourceLocation.parse("entity/sheep/sheep_wool"))
|
||||
.orElse(null);
|
||||
if (wool != null && woolTex != null) {
|
||||
int[][] t = TextureOps.deepCopy(woolTex);
|
||||
if (s.tint() != 0) TextureOps.tint(t, s.tint());
|
||||
@@ -98,9 +102,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// left face is transparent in the texture → a see-through hole on the left. Add the mirrored left panel.
|
||||
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);
|
||||
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));
|
||||
baked.add(new CemGeometry.Baked(
|
||||
org, new double[] {org[0] + size[0], org[1] + size[1], org[2] + size[2]}, faces, pre));
|
||||
}
|
||||
// Saddle: an extra inflated layer from the *_saddle CEM model, showing only its saddle-specific parts.
|
||||
if (s.saddle()) addSaddleLayer(s, cem, model, pre, baked);
|
||||
@@ -116,8 +121,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
for (CemGeometry.Baked b : ref) minY = Math.min(minY, b.minWorldY());
|
||||
|
||||
Affine place = Affine.translation(s.x(), s.y(), s.z())
|
||||
.mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw())))
|
||||
.mul(Affine.translation(0, -minY, 0));
|
||||
.mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw())))
|
||||
.mul(Affine.translation(0, -minY, 0));
|
||||
|
||||
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())));
|
||||
@@ -129,8 +134,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (s.player()) {
|
||||
int[][] skin = skins.get(s.skinUrl()).orElse(null);
|
||||
if (skin != null) return skin;
|
||||
int[][] def = textures.get(ResourceLocation.parse(
|
||||
s.slim() ? "entity/player/slim/steve" : "entity/player/wide/steve")).orElse(null);
|
||||
int[][] def = textures.get(
|
||||
ResourceLocation.parse(s.slim() ? "entity/player/slim/steve" : "entity/player/wide/steve"))
|
||||
.orElse(null);
|
||||
if (def != null) return def;
|
||||
}
|
||||
for (ResourceLocation rl : EntityModels.textureCandidates(s.typeKey(), s.variant())) {
|
||||
@@ -195,12 +201,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
|
||||
/** Bake the saddle as a separate inflated layer; only the saddle-specific parts (those not in the base model). */
|
||||
private void addSaddleLayer(EntityState s, String cem, CemModelLoader.CemModel base, Affine pre, List<CemGeometry.Baked> baked) {
|
||||
private void addSaddleLayer(
|
||||
EntityState s, String cem, CemModelLoader.CemModel base, Affine pre, List<CemGeometry.Baked> baked) {
|
||||
String saddleModel = cem.equals("donkey") ? "donkey_saddle" : (cem.equals("horse") ? "horse_saddle" : null);
|
||||
if (saddleModel == null) return; // llamas and other mounts have no saddle model
|
||||
CemModelLoader.CemModel sm = models.get(saddleModel);
|
||||
if (sm == null) return;
|
||||
int[][] saddleTex = textures.get(ResourceLocation.parse("entity/equipment/" + s.typeKey() + "_saddle/saddle")).orElse(null);
|
||||
int[][] saddleTex = textures.get(ResourceLocation.parse("entity/equipment/" + s.typeKey() + "_saddle/saddle"))
|
||||
.orElse(null);
|
||||
if (saddleTex == null) return;
|
||||
// Show only the saddle-specific parts: hide every part the base body model also defines.
|
||||
Set<String> hideBase = new HashSet<>();
|
||||
@@ -214,10 +222,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// shoes=boots;
|
||||
// armor_layer_2 (texture entity/equipment/humanoid_leggings/<mat>): waist+legs=leggings.
|
||||
// Each slot may use a different material, so each is baked separately, showing only its parts.
|
||||
private static final Set<String> ARMOR1_HEAD = Set.of("head");
|
||||
private static final Set<String> ARMOR1_HEAD = Set.of("head");
|
||||
private static final Set<String> ARMOR1_CHEST = Set.of("body", "left_arm", "right_arm");
|
||||
private static final Set<String> ARMOR1_FEET = Set.of("left_shoe", "right_shoe");
|
||||
private static final Set<String> ARMOR2_LEGS = Set.of("waist", "left_leg", "right_leg");
|
||||
private static final Set<String> ARMOR1_FEET = Set.of("left_shoe", "right_shoe");
|
||||
private static final Set<String> ARMOR2_LEGS = Set.of("waist", "left_leg", "right_leg");
|
||||
private static final int GLINT_COLOR = 0xFF8040CC; // approximated enchantment-glint purple
|
||||
|
||||
private void addArmorLayers(EntityState s, Affine pre, List<CemGeometry.Baked> baked) {
|
||||
@@ -234,8 +242,13 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
|
||||
/** Bake one armor slot: its layer model with only {@code show} parts, textured for the material. */
|
||||
private void bakeArmorPiece(EntityState.EquipPiece piece, String modelName, Set<String> show,
|
||||
String layerFolder, Affine pre, List<CemGeometry.Baked> baked) {
|
||||
private void bakeArmorPiece(
|
||||
EntityState.EquipPiece piece,
|
||||
String modelName,
|
||||
Set<String> show,
|
||||
String layerFolder,
|
||||
Affine pre,
|
||||
List<CemGeometry.Baked> baked) {
|
||||
if (piece == null) return;
|
||||
CemModelLoader.CemModel model = models.get(modelName);
|
||||
if (model == null) return;
|
||||
@@ -249,7 +262,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
/** Resolve + composite a single armor slot's texture: material base (+leather dye/overlay), trim, glint. */
|
||||
private int[][] buildArmorTexture(EntityState.EquipPiece piece, String layerFolder) {
|
||||
String asset = piece.asset();
|
||||
int[][] base = textures.get(ResourceLocation.parse("entity/equipment/" + layerFolder + "/" + asset)).orElse(null);
|
||||
int[][] base = textures.get(ResourceLocation.parse("entity/equipment/" + layerFolder + "/" + asset))
|
||||
.orElse(null);
|
||||
if (base == null) return null;
|
||||
int[][] out = TextureOps.deepCopy(base);
|
||||
// Leather is dyeable: tint the base layer, then composite the (undyed) overlay layer on top.
|
||||
@@ -266,13 +280,15 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
/** Armor trim: recolour the grayscale pattern via the material palette (same UV) and overlay it. */
|
||||
private void applyTrim(int[][] armorTex, String layerFolder, String pattern, String material, String armorAsset) {
|
||||
int[][] mask = textures.get(ResourceLocation.parse("trims/entity/" + layerFolder + "/" + pattern)).orElse(null);
|
||||
int[][] mask = textures.get(ResourceLocation.parse("trims/entity/" + layerFolder + "/" + pattern))
|
||||
.orElse(null);
|
||||
if (mask == null || mask.length != armorTex.length || mask[0].length != armorTex[0].length) return;
|
||||
int[] from = palette8("trims/color_palettes/trim_palette");
|
||||
// Vanilla uses the darker palette variant when the trim material matches the armor material.
|
||||
String palette = material;
|
||||
if (material.equals(armorAsset)
|
||||
&& textures.get(ResourceLocation.parse("trims/color_palettes/" + material + "_darker")).isPresent()) {
|
||||
&& textures.get(ResourceLocation.parse("trims/color_palettes/" + material + "_darker"))
|
||||
.isPresent()) {
|
||||
palette = material + "_darker";
|
||||
}
|
||||
int[] to = palette8("trims/color_palettes/" + palette);
|
||||
@@ -291,14 +307,15 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
/** Static enchantment-glint approximation over an armor/elytra/item texture (in place). */
|
||||
private void applyGlint(int[][] tex) {
|
||||
textures.get(ResourceLocation.parse("misc/enchanted_glint_armor"))
|
||||
.ifPresent(glint -> TextureOps.addGlint(tex, glint, GLINT_COLOR, 0.6));
|
||||
.ifPresent(glint -> TextureOps.addGlint(tex, glint, GLINT_COLOR, 0.6));
|
||||
}
|
||||
|
||||
/** Elytra in the chest slot: bake the wing model with the default elytra texture (no chestplate). */
|
||||
private void bakeElytra(EntityState.EquipPiece piece, Affine pre, List<CemGeometry.Baked> baked) {
|
||||
CemModelLoader.CemModel model = models.get("elytra");
|
||||
if (model == null) return;
|
||||
int[][] tex = textures.get(ResourceLocation.parse("entity/equipment/wings/elytra")).orElse(null);
|
||||
int[][] tex = textures.get(ResourceLocation.parse("entity/equipment/wings/elytra"))
|
||||
.orElse(null);
|
||||
if (tex == null) return;
|
||||
int[][] out = TextureOps.deepCopy(tex);
|
||||
if (piece.glint()) applyGlint(out);
|
||||
@@ -310,12 +327,12 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
double[] from = {-w / 2, 0, -w / 2};
|
||||
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);
|
||||
ModelCube box =
|
||||
new ModelCube(new double[] {-w / 2, 0, -w / 2}, new double[] {w, h, w}, 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));
|
||||
.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 RenderedEntity.of(cubes);
|
||||
|
||||
@@ -4,7 +4,6 @@ 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.ModelCube;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -45,8 +44,14 @@ final class CemGeometry {
|
||||
* {@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) {
|
||||
static List<Baked> bakeModel(
|
||||
CemModelLoader.CemModel model,
|
||||
int[][] tex,
|
||||
Affine pre,
|
||||
Set<String> hidden,
|
||||
int texW,
|
||||
int texH,
|
||||
boolean ignoreFaceUv) {
|
||||
int nw = texW > 0 ? texW : model.texW();
|
||||
int nh = texH > 0 ? texH : model.texH();
|
||||
List<Baked> out = new ArrayList<>();
|
||||
@@ -63,15 +68,24 @@ final class CemGeometry {
|
||||
* parent origin from the 2nd nesting level on). Top-level boxes are absolute; nested boxes are offset
|
||||
* by their group origin. The group transform is {@code parent · T(O) · R · T(-O)}.
|
||||
*/
|
||||
private static void bakePart(CemModelLoader.CemPart part, Affine parentWorld, double[] o, int depth,
|
||||
Set<String> hidden, int texW, int texH, int[][] tex, boolean ignoreFaceUv, List<Baked> out) {
|
||||
private static void bakePart(
|
||||
CemModelLoader.CemPart part,
|
||||
Affine parentWorld,
|
||||
double[] o,
|
||||
int depth,
|
||||
Set<String> hidden,
|
||||
int texW,
|
||||
int texH,
|
||||
int[][] tex,
|
||||
boolean ignoreFaceUv,
|
||||
List<Baked> out) {
|
||||
if (hidden.contains(part.name())) return;
|
||||
Affine world = parentWorld
|
||||
.mul(Affine.translation(o[0], o[1], o[2]))
|
||||
.mul(Affine.rotZ(Math.toRadians(part.rotate()[2])))
|
||||
.mul(Affine.rotY(Math.toRadians(part.rotate()[1])))
|
||||
.mul(Affine.rotX(Math.toRadians(part.rotate()[0])))
|
||||
.mul(Affine.translation(-o[0], -o[1], -o[2]));
|
||||
.mul(Affine.translation(o[0], o[1], o[2]))
|
||||
.mul(Affine.rotZ(Math.toRadians(part.rotate()[2])))
|
||||
.mul(Affine.rotY(Math.toRadians(part.rotate()[1])))
|
||||
.mul(Affine.rotX(Math.toRadians(part.rotate()[0])))
|
||||
.mul(Affine.translation(-o[0], -o[1], -o[2]));
|
||||
|
||||
double ox = depth > 0 ? o[0] : 0, oy = depth > 0 ? o[1] : 0, oz = depth > 0 ? o[2] : 0;
|
||||
for (CemModelLoader.CemBox b : part.boxes()) {
|
||||
@@ -87,9 +101,9 @@ final class CemGeometry {
|
||||
for (CemModelLoader.CemPart child : part.children()) {
|
||||
double[] t = child.translate();
|
||||
// submodel origin = its translate, accumulated with this group's origin from the 2nd level on.
|
||||
double[] co = depth >= 1 ? new double[]{t[0] + o[0], t[1] + o[1], t[2] + o[2]} : new double[]{t[0], t[1], t[2]};
|
||||
double[] co =
|
||||
depth >= 1 ? new double[] {t[0] + o[0], t[1] + o[1], t[2] + o[2]} : new double[] {t[0], t[1], t[2]};
|
||||
bakePart(child, world, co, depth + 1, hidden, texW, texH, tex, ignoreFaceUv, out);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -29,11 +28,13 @@ public final class CemModelLoader {
|
||||
* and optional per-face UV ({@code faceUv}, indexed by {@link eu.mhsl.minecraft.pixelpics.assets.model.Direction}
|
||||
* ordinal, each {@code {x, y, w, h}} texels; null = use box-UV).
|
||||
*/
|
||||
public record CemBox(double[] origin, double[] size, double inflate, double[] uv, boolean mirror, double[][] faceUv) {}
|
||||
public record CemBox(
|
||||
double[] origin, double[] size, double inflate, double[] uv, boolean mirror, double[][] faceUv) {}
|
||||
|
||||
/** A model part: its (raw) translate, rotation (deg), boxes and nested submodels. The rotation pivot
|
||||
* is {@code -(sum of translates from the root to this part)} — accumulated by the baker. */
|
||||
public record CemPart(String name, double[] translate, double[] rotate, List<CemBox> boxes, List<CemPart> children) {}
|
||||
public record CemPart(
|
||||
String name, double[] translate, double[] rotate, List<CemBox> boxes, List<CemPart> children) {}
|
||||
|
||||
/** A whole model: declared texture size and its top-level parts. */
|
||||
public record CemModel(int texW, int texH, List<CemPart> parts) {}
|
||||
@@ -50,13 +51,15 @@ public final class CemModelLoader {
|
||||
|
||||
/** Parse the CEM template-models JSON stream. Returns the number of models loaded. */
|
||||
public int load(InputStream in, Logger logger) {
|
||||
JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
JsonObject modelsObj = root.getAsJsonObject("models");
|
||||
for (Map.Entry<String, JsonElement> e : modelsObj.entrySet()) {
|
||||
try {
|
||||
JsonObject entry = e.getValue().getAsJsonObject();
|
||||
if (!entry.has("model")) continue;
|
||||
JsonObject model = JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject();
|
||||
JsonObject model =
|
||||
JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject();
|
||||
int tw = model.getAsJsonArray("textureSize").get(0).getAsInt();
|
||||
int th = model.getAsJsonArray("textureSize").get(1).getAsInt();
|
||||
List<CemPart> parts = new ArrayList<>();
|
||||
@@ -72,26 +75,36 @@ public final class CemModelLoader {
|
||||
private CemPart parsePart(JsonObject p) {
|
||||
double[] translate = arr3(p, "translate");
|
||||
double[] rotate = arr3(p, "rotate");
|
||||
boolean partMirror = mirrorsU(p); // mirrorTexture "u" — applies to all of the part's boxes
|
||||
boolean partMirror = mirrorsU(p); // mirrorTexture "u" — applies to all of the part's boxes
|
||||
List<CemBox> boxes = new ArrayList<>();
|
||||
if (p.has("boxes")) {
|
||||
for (JsonElement be : p.getAsJsonArray("boxes")) {
|
||||
JsonObject b = be.getAsJsonObject();
|
||||
if (!b.has("coordinates")) continue;
|
||||
JsonArray c = b.getAsJsonArray("coordinates");
|
||||
double[] origin = {c.get(0).getAsDouble(), c.get(1).getAsDouble(), c.get(2).getAsDouble()};
|
||||
double[] size = {c.get(3).getAsDouble(), c.get(4).getAsDouble(), c.get(5).getAsDouble()};
|
||||
double[] origin = {
|
||||
c.get(0).getAsDouble(), c.get(1).getAsDouble(), c.get(2).getAsDouble()
|
||||
};
|
||||
double[] size = {
|
||||
c.get(3).getAsDouble(), c.get(4).getAsDouble(), c.get(5).getAsDouble()
|
||||
};
|
||||
double inflate = b.has("sizeAdd") ? b.get("sizeAdd").getAsDouble() : 0;
|
||||
double[] uv = b.has("textureOffset")
|
||||
? new double[]{b.getAsJsonArray("textureOffset").get(0).getAsDouble(), b.getAsJsonArray("textureOffset").get(1).getAsDouble()}
|
||||
: new double[]{0, 0};
|
||||
? new double[] {
|
||||
b.getAsJsonArray("textureOffset").get(0).getAsDouble(),
|
||||
b.getAsJsonArray("textureOffset").get(1).getAsDouble()
|
||||
}
|
||||
: new double[] {0, 0};
|
||||
boxes.add(new CemBox(origin, size, inflate, uv, partMirror || mirrorsU(b), parseFaceUv(b)));
|
||||
}
|
||||
}
|
||||
List<CemPart> children = new ArrayList<>();
|
||||
if (p.has("submodels")) for (JsonElement se : p.getAsJsonArray("submodels")) children.add(parsePart(se.getAsJsonObject()));
|
||||
if (p.has("submodels"))
|
||||
for (JsonElement se : p.getAsJsonArray("submodels")) children.add(parsePart(se.getAsJsonObject()));
|
||||
if (p.has("submodel")) children.add(parsePart(p.getAsJsonObject("submodel")));
|
||||
String name = p.has("part") ? p.get("part").getAsString() : (p.has("id") ? p.get("id").getAsString() : "");
|
||||
String name = p.has("part")
|
||||
? p.get("part").getAsString()
|
||||
: (p.has("id") ? p.get("id").getAsString() : "");
|
||||
return new CemPart(name, translate, rotate, boxes, children);
|
||||
}
|
||||
|
||||
@@ -101,7 +114,11 @@ public final class CemModelLoader {
|
||||
/** Parses per-face UV ({@code uvNorth} etc., each {@code [u1,v1,u2,v2]}) into {@code {x,y,w,h}}, or null. */
|
||||
private static double[][] parseFaceUv(JsonObject b) {
|
||||
boolean any = false;
|
||||
for (String k : FACE_UV_KEYS) if (b.has(k)) { any = true; break; }
|
||||
for (String k : FACE_UV_KEYS)
|
||||
if (b.has(k)) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
if (!any) return null;
|
||||
double[][] faces = new double[6][];
|
||||
for (int i = 0; i < FACE_UV_KEYS.length; i++) {
|
||||
@@ -109,7 +126,7 @@ public final class CemModelLoader {
|
||||
JsonArray a = b.getAsJsonArray(FACE_UV_KEYS[i]);
|
||||
double u1 = a.get(0).getAsDouble(), v1 = a.get(1).getAsDouble();
|
||||
double u2 = a.get(2).getAsDouble(), v2 = a.get(3).getAsDouble();
|
||||
faces[i] = new double[]{u1, v1, u2 - u1, v2 - v1};
|
||||
faces[i] = new double[] {u1, v1, u2 - u1, v2 - v1};
|
||||
}
|
||||
return faces;
|
||||
}
|
||||
@@ -119,8 +136,10 @@ public final class CemModelLoader {
|
||||
}
|
||||
|
||||
private static double[] arr3(JsonObject o, String key) {
|
||||
if (!o.has(key) || !o.get(key).isJsonArray()) return new double[]{0, 0, 0};
|
||||
if (!o.has(key) || !o.get(key).isJsonArray()) return new double[] {0, 0, 0};
|
||||
JsonArray a = o.getAsJsonArray(key);
|
||||
return new double[]{a.get(0).getAsDouble(), a.get(1).getAsDouble(), a.get(2).getAsDouble()};
|
||||
return new double[] {
|
||||
a.get(0).getAsDouble(), a.get(1).getAsDouble(), a.get(2).getAsDouble()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity.cem;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.font.Glyph;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -22,8 +21,8 @@ final class SignTextRasterizer {
|
||||
if (font.isEmpty() || lines.isEmpty()) return null;
|
||||
|
||||
int ascent = font.maxAscent();
|
||||
int pitch = font.lineHeight(); // ascent + descent
|
||||
int margin = glow ? 1 : 0; // room for the outline
|
||||
int pitch = font.lineHeight(); // ascent + descent
|
||||
int margin = glow ? 1 : 0; // room for the outline
|
||||
|
||||
int contentW = 1;
|
||||
for (String line : lines) contentW = Math.max(contentW, lineWidth(line, font));
|
||||
|
||||
@@ -5,10 +5,9 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Element;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.ResolvedModel;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Intersects a ray (in block coordinates) with a {@link ResolvedModel}'s element boxes and returns
|
||||
@@ -28,16 +27,23 @@ public final class ElementIntersector {
|
||||
* @param dx,dy,dz ray direction (need not be normalized)
|
||||
* @param bx,by,bz block min corner in world coordinates (for reconstructing the world hit point)
|
||||
*/
|
||||
public static FaceHit intersect(ResolvedModel model,
|
||||
double ox, double oy, double oz,
|
||||
double dx, double dy, double dz,
|
||||
int bx, int by, int bz) {
|
||||
public static FaceHit intersect(
|
||||
ResolvedModel model,
|
||||
double ox,
|
||||
double oy,
|
||||
double oz,
|
||||
double dx,
|
||||
double dy,
|
||||
double dz,
|
||||
int bx,
|
||||
int by,
|
||||
int bz) {
|
||||
List<Candidate> candidates = new ArrayList<>(model.elements.size());
|
||||
for (int i = 0; i < model.elements.size(); i++) {
|
||||
Element element = model.elements.get(i);
|
||||
Candidate c = element.isAxisAligned()
|
||||
? intersectAabb(element, ox, oy, oz, dx, dy, dz)
|
||||
: intersectObb(element, ox, oy, oz, dx, dy, dz);
|
||||
? intersectAabb(element, ox, oy, oz, dx, dy, dz)
|
||||
: intersectObb(element, ox, oy, oz, dx, dy, dz);
|
||||
if (c != null) candidates.add(new Candidate(c.element, c.t, c.dir, c.s, c.t2, c.normal, i));
|
||||
}
|
||||
if (candidates.isEmpty()) return null;
|
||||
@@ -61,16 +67,16 @@ public final class ElementIntersector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private record Candidate(Element element, double t, Direction dir, double s, double t2, double[] normal, int order) {}
|
||||
private record Candidate(
|
||||
Element element, double t, Direction dir, double s, double t2, double[] normal, int order) {}
|
||||
|
||||
private static Candidate intersectAabb(Element e, double ox, double oy, double oz,
|
||||
double dx, double dy, double dz) {
|
||||
private static Candidate intersectAabb(
|
||||
Element e, double ox, double oy, double oz, double dx, double dy, double dz) {
|
||||
return slab(e, ox, oy, oz, dx, dy, dz, e.from, e.to, null);
|
||||
}
|
||||
|
||||
/** Rotated element: transform the ray into the element's local (unrotated) frame, then slab-test. */
|
||||
private static Candidate intersectObb(Element e, double ox, double oy, double oz,
|
||||
double dx, double dy, double dz) {
|
||||
private static Candidate intersectObb(Element e, double ox, double oy, double oz, double dx, double dy, double dz) {
|
||||
double[] o = rotate(ox - e.rotOrigin[0], oy - e.rotOrigin[1], oz - e.rotOrigin[2], e.rotAxis, -e.rotAngleRad);
|
||||
o[0] += e.rotOrigin[0];
|
||||
o[1] += e.rotOrigin[1];
|
||||
@@ -79,9 +85,17 @@ public final class ElementIntersector {
|
||||
return slab(e, o[0], o[1], o[2], d[0], d[1], d[2], e.from, e.to, e);
|
||||
}
|
||||
|
||||
private static Candidate slab(Element e, double ox, double oy, double oz,
|
||||
double dx, double dy, double dz,
|
||||
double[] from, double[] to, Element obb) {
|
||||
private static Candidate slab(
|
||||
Element e,
|
||||
double ox,
|
||||
double oy,
|
||||
double oz,
|
||||
double dx,
|
||||
double dy,
|
||||
double dz,
|
||||
double[] from,
|
||||
double[] to,
|
||||
Element obb) {
|
||||
double tmin = Double.NEGATIVE_INFINITY;
|
||||
double tmax = Double.POSITIVE_INFINITY;
|
||||
int axis = -1;
|
||||
@@ -99,7 +113,9 @@ public final class ElementIntersector {
|
||||
double t2 = (to[a] - o[a]) * inv;
|
||||
boolean neg = true;
|
||||
if (t1 > t2) {
|
||||
double tmp = t1; t1 = t2; t2 = tmp;
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
neg = false;
|
||||
}
|
||||
if (t1 > tmin) {
|
||||
@@ -136,11 +152,20 @@ public final class ElementIntersector {
|
||||
|
||||
double s, t;
|
||||
switch (dir) {
|
||||
// Texture V is top-down (0 = texture top). For side faces the texture top is the block
|
||||
// top (high Y), so t = 1 - fracY.
|
||||
case UP, DOWN -> { s = fracX; t = fracZ; }
|
||||
case NORTH, SOUTH -> { s = fracX; t = 1 - fracY; }
|
||||
default -> { s = fracZ; t = 1 - fracY; } // WEST, EAST
|
||||
// Texture V is top-down (0 = texture top). For side faces the texture top is the block
|
||||
// top (high Y), so t = 1 - fracY.
|
||||
case UP, DOWN -> {
|
||||
s = fracX;
|
||||
t = fracZ;
|
||||
}
|
||||
case NORTH, SOUTH -> {
|
||||
s = fracX;
|
||||
t = 1 - fracY;
|
||||
}
|
||||
default -> {
|
||||
s = fracZ;
|
||||
t = 1 - fracY;
|
||||
} // WEST, EAST
|
||||
}
|
||||
return new Candidate(e, tEntry, dir, s, t, normal, 0);
|
||||
}
|
||||
@@ -165,9 +190,9 @@ public final class ElementIntersector {
|
||||
double cos = Math.cos(angle);
|
||||
double sin = Math.sin(angle);
|
||||
return switch (axis) {
|
||||
case 0 -> new double[]{x, y * cos - z * sin, y * sin + z * cos};
|
||||
case 1 -> new double[]{x * cos + z * sin, y, -x * sin + z * cos};
|
||||
default -> new double[]{x * cos - y * sin, x * sin + y * cos, z};
|
||||
case 0 -> new double[] {x, y * cos - z * sin, y * sin + z * cos};
|
||||
case 1 -> new double[] {x * cos + z * sin, y, -x * sin + z * cos};
|
||||
default -> new double[] {x * cos - y * sin, x * sin + y * cos, z};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,4 @@ import org.bukkit.util.Vector;
|
||||
* The result of intersecting a ray with a block's geometry: the world-space hit point and normal,
|
||||
* the sampled ARGB color (before shading/tinting) and the face's tint index ({@code -1} = none).
|
||||
*/
|
||||
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) {
|
||||
}
|
||||
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) {}
|
||||
|
||||
@@ -11,14 +11,13 @@ import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.MathUtil;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Traces a single ray against a {@link WorldSnapshot}, sampling block models via the
|
||||
* {@link ElementIntersector} and applying biome tint, directional face shading, transparency and
|
||||
@@ -44,8 +43,12 @@ public final class SnapshotRaytracer {
|
||||
private final int maxSteps;
|
||||
private final Map<Long, BiomeTint> tintCache = new ConcurrentHashMap<>();
|
||||
|
||||
public SnapshotRaytracer(BlockModelRegistry registry, BiomeTintProvider tintProvider,
|
||||
SkyRenderer skyRenderer, double maxDistance, int reflectionDepth) {
|
||||
public SnapshotRaytracer(
|
||||
BlockModelRegistry registry,
|
||||
BiomeTintProvider tintProvider,
|
||||
SkyRenderer skyRenderer,
|
||||
double maxDistance,
|
||||
int reflectionDepth) {
|
||||
this.registry = registry;
|
||||
this.tintProvider = tintProvider;
|
||||
this.skyRenderer = skyRenderer;
|
||||
@@ -58,7 +61,8 @@ public final class SnapshotRaytracer {
|
||||
return trace(snapshot, origin, direction, sky, scene, reflectionDepth);
|
||||
}
|
||||
|
||||
private int trace(WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) {
|
||||
private int trace(
|
||||
WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) {
|
||||
double ox = origin.getX(), oy = origin.getY(), oz = origin.getZ();
|
||||
double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ();
|
||||
|
||||
@@ -99,7 +103,8 @@ public final class SnapshotRaytracer {
|
||||
|
||||
if (!reflected && model.reflection > 0 && depth > 0) {
|
||||
Vector reflectDir = MathUtil.reflectVector(direction, hit.normal());
|
||||
Vector reflectStart = hit.point().clone().add(hit.normal().clone().multiply(1e-3));
|
||||
Vector reflectStart =
|
||||
hit.point().clone().add(hit.normal().clone().multiply(1e-3));
|
||||
reflectionColor = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1);
|
||||
reflectionFactor = model.reflection;
|
||||
reflected = true;
|
||||
@@ -147,11 +152,13 @@ public final class SnapshotRaytracer {
|
||||
|
||||
if (transparencyStart != null) {
|
||||
baseColor = ColorUtil.mix(
|
||||
baseColor,
|
||||
transparencyColor,
|
||||
transparencyFactor,
|
||||
(1 - transparencyFactor)
|
||||
* (1 + transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint) / 5.0));
|
||||
baseColor,
|
||||
transparencyColor,
|
||||
transparencyFactor,
|
||||
(1 - transparencyFactor)
|
||||
* (1
|
||||
+ transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint)
|
||||
/ 5.0));
|
||||
}
|
||||
if (reflected) {
|
||||
baseColor = ColorUtil.mix(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor);
|
||||
@@ -191,7 +198,9 @@ public final class SnapshotRaytracer {
|
||||
* across the face. Only applied to axis-aligned faces.
|
||||
*/
|
||||
private double ambientOcclusion(FaceHit hit, WorldSnapshot snapshot, int bx, int by, int bz) {
|
||||
double nx = hit.normal().getX(), ny = hit.normal().getY(), nz = hit.normal().getZ();
|
||||
double nx = hit.normal().getX(),
|
||||
ny = hit.normal().getY(),
|
||||
nz = hit.normal().getZ();
|
||||
double ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz);
|
||||
if (Math.max(ax, Math.max(ay, az)) < 0.99) return 1.0; // skip rotated/diagonal faces
|
||||
|
||||
@@ -203,12 +212,33 @@ public final class SnapshotRaytracer {
|
||||
int ofx = (int) Math.round(nx), ofy = (int) Math.round(ny), ofz = (int) Math.round(nz);
|
||||
int ux, uy, uz, vx, vy, vz;
|
||||
double su, sv;
|
||||
if (ay > 0.5) { // up/down
|
||||
ux = 1; uy = 0; uz = 0; vx = 0; vy = 0; vz = 1; su = lx; sv = lz;
|
||||
} else if (ax > 0.5) { // east/west
|
||||
ux = 0; uy = 0; uz = 1; vx = 0; vy = 1; vz = 0; su = lz; sv = ly;
|
||||
} else { // north/south
|
||||
ux = 1; uy = 0; uz = 0; vx = 0; vy = 1; vz = 0; su = lx; sv = ly;
|
||||
if (ay > 0.5) { // up/down
|
||||
ux = 1;
|
||||
uy = 0;
|
||||
uz = 0;
|
||||
vx = 0;
|
||||
vy = 0;
|
||||
vz = 1;
|
||||
su = lx;
|
||||
sv = lz;
|
||||
} else if (ax > 0.5) { // east/west
|
||||
ux = 0;
|
||||
uy = 0;
|
||||
uz = 1;
|
||||
vx = 0;
|
||||
vy = 1;
|
||||
vz = 0;
|
||||
su = lz;
|
||||
sv = ly;
|
||||
} else { // north/south
|
||||
ux = 1;
|
||||
uy = 0;
|
||||
uz = 0;
|
||||
vx = 0;
|
||||
vy = 1;
|
||||
vz = 0;
|
||||
su = lx;
|
||||
sv = ly;
|
||||
}
|
||||
|
||||
double b00 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, -1, -1);
|
||||
@@ -221,13 +251,26 @@ public final class SnapshotRaytracer {
|
||||
return top + (bottom - top) * sv;
|
||||
}
|
||||
|
||||
private double aoCorner(WorldSnapshot snapshot, int bx, int by, int bz,
|
||||
int ofx, int ofy, int ofz, int ux, int uy, int uz, int vx, int vy, int vz,
|
||||
int du, int dv) {
|
||||
private double aoCorner(
|
||||
WorldSnapshot snapshot,
|
||||
int bx,
|
||||
int by,
|
||||
int bz,
|
||||
int ofx,
|
||||
int ofy,
|
||||
int ofz,
|
||||
int ux,
|
||||
int uy,
|
||||
int uz,
|
||||
int vx,
|
||||
int vy,
|
||||
int vz,
|
||||
int du,
|
||||
int dv) {
|
||||
boolean side1 = solid(snapshot, bx + ofx + du * ux, by + ofy + du * uy, bz + ofz + du * uz);
|
||||
boolean side2 = solid(snapshot, bx + ofx + dv * vx, by + ofy + dv * vy, bz + ofz + dv * vz);
|
||||
boolean corner = solid(snapshot,
|
||||
bx + ofx + du * ux + dv * vx, by + ofy + du * uy + dv * vy, bz + ofz + du * uz + dv * vz);
|
||||
boolean corner = solid(
|
||||
snapshot, bx + ofx + du * ux + dv * vx, by + ofy + du * uy + dv * vy, bz + ofz + du * uz + dv * vz);
|
||||
int level = (side1 && side2) ? 0 : 3 - (side1 ? 1 : 0) - (side2 ? 1 : 0) - (corner ? 1 : 0);
|
||||
return AO_BRIGHTNESS[Math.clamp(level, 0, 3)];
|
||||
}
|
||||
|
||||
+42
-22
@@ -2,13 +2,13 @@ package eu.mhsl.minecraft.pixelpics.render.render;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.DecorationBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityScene;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.SnapshotRaytracer;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyRenderer;
|
||||
@@ -20,20 +20,18 @@ import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.MathUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.IntStream;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Renders the scene by capturing a world snapshot on the main thread ({@link #prepare}) and then
|
||||
@@ -43,6 +41,7 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
|
||||
/** Horizontal half field-of-view; the vertical half is derived from the output aspect ratio. */
|
||||
private static final double H_FOV_HALF_RAD = Math.toRadians(35);
|
||||
|
||||
private static final Vector BASE_VEC = new Vector(1, 0, 0);
|
||||
|
||||
private static final double MAX_DISTANCE = 256;
|
||||
@@ -59,15 +58,24 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
/** Bounds parallel ray tracing to a fixed, low-priority pool; {@code null} = use the common pool. */
|
||||
private final ForkJoinPool tracePool;
|
||||
|
||||
public DefaultScreenRenderer(BlockModelRegistry registry, BiomeTintProvider tintProvider,
|
||||
TextureCache textures, CemBaker entityBaker,
|
||||
BlockEntityBaker blockEntityBaker, Logger logger) {
|
||||
public DefaultScreenRenderer(
|
||||
BlockModelRegistry registry,
|
||||
BiomeTintProvider tintProvider,
|
||||
TextureCache textures,
|
||||
CemBaker entityBaker,
|
||||
BlockEntityBaker blockEntityBaker,
|
||||
Logger logger) {
|
||||
this(registry, tintProvider, textures, entityBaker, blockEntityBaker, logger, null);
|
||||
}
|
||||
|
||||
public DefaultScreenRenderer(BlockModelRegistry registry, BiomeTintProvider tintProvider,
|
||||
TextureCache textures, CemBaker entityBaker,
|
||||
BlockEntityBaker blockEntityBaker, Logger logger, ForkJoinPool tracePool) {
|
||||
public DefaultScreenRenderer(
|
||||
BlockModelRegistry registry,
|
||||
BiomeTintProvider tintProvider,
|
||||
TextureCache textures,
|
||||
CemBaker entityBaker,
|
||||
BlockEntityBaker blockEntityBaker,
|
||||
Logger logger,
|
||||
ForkJoinPool tracePool) {
|
||||
SkyRenderer skyRenderer = new SkyRenderer(textures);
|
||||
this.raytracer = new SnapshotRaytracer(registry, tintProvider, skyRenderer, MAX_DISTANCE, REFLECTION_DEPTH);
|
||||
this.entityBaker = entityBaker;
|
||||
@@ -99,8 +107,16 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
int moonPhase = (int) (fullTime / 24000L % 8L);
|
||||
SkyContext sky = new SkyContext(dayTime, moonPhase, fullTime);
|
||||
|
||||
return new RenderJob(snapshot, rayMap, eyeLocation.toVector(),
|
||||
resolution.getWidth(), resolution.getHeight(), sky, entities, blockEntities, decorations);
|
||||
return new RenderJob(
|
||||
snapshot,
|
||||
rayMap,
|
||||
eyeLocation.toVector(),
|
||||
resolution.getWidth(),
|
||||
resolution.getHeight(),
|
||||
sky,
|
||||
entities,
|
||||
blockEntities,
|
||||
decorations);
|
||||
}
|
||||
|
||||
/** Traces every (super)ray in parallel, then downsamples gamma-correctly. Safe off the main thread. */
|
||||
@@ -121,8 +137,8 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
WorldSnapshot snapshot = job.snapshot();
|
||||
Vector origin = job.origin();
|
||||
SkyContext sky = job.sky();
|
||||
EntityScene scene = new EntityScene(job.entities(), entityBaker, job.blockEntities(), blockEntityBaker,
|
||||
job.decorations(), decorationBaker);
|
||||
EntityScene scene = new EntityScene(
|
||||
job.entities(), entityBaker, job.blockEntities(), blockEntityBaker, job.decorations(), decorationBaker);
|
||||
|
||||
int[] superBuf = new int[rayMap.size()];
|
||||
runParallel(() -> IntStream.range(0, rayMap.size()).parallel().forEach(i -> {
|
||||
@@ -183,15 +199,19 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
List<Vector> rayMap = new ArrayList<>(width * height);
|
||||
|
||||
Vector leftFraction = upperLeftCorner.clone().subtract(lowerLeftCorner).multiply(1.0 / (height - 1));
|
||||
Vector rightFraction = upperRightCorner.clone().subtract(lowerRightCorner).multiply(1.0 / (height - 1));
|
||||
Vector rightFraction =
|
||||
upperRightCorner.clone().subtract(lowerRightCorner).multiply(1.0 / (height - 1));
|
||||
|
||||
for (int pitch = 0; pitch < height; pitch++) {
|
||||
Vector leftPitch = upperLeftCorner.clone().subtract(leftFraction.clone().multiply(pitch));
|
||||
Vector rightPitch = upperRightCorner.clone().subtract(rightFraction.clone().multiply(pitch));
|
||||
Vector leftPitch =
|
||||
upperLeftCorner.clone().subtract(leftFraction.clone().multiply(pitch));
|
||||
Vector rightPitch =
|
||||
upperRightCorner.clone().subtract(rightFraction.clone().multiply(pitch));
|
||||
Vector yawFraction = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1));
|
||||
|
||||
for (int yaw = 0; yaw < width; yaw++) {
|
||||
Vector ray = leftPitch.clone().add(yawFraction.clone().multiply(yaw)).normalize();
|
||||
Vector ray =
|
||||
leftPitch.clone().add(yawFraction.clone().multiply(yaw)).normalize();
|
||||
rayMap.add(ray);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,47 @@ import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
|
||||
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* A prepared render: the world snapshot (captured on the main thread) plus the ray map, camera origin,
|
||||
* the sky context (time of day / moon phase) and the captured entity, block-entity and decoration
|
||||
* (painting / item frame) states. {@link DefaultScreenRenderer#execute} can run this off the main thread.
|
||||
*/
|
||||
public record RenderJob(WorldSnapshot snapshot, List<Vector> rayMap, Vector origin,
|
||||
int width, int height, SkyContext sky, List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities, List<DecorationState> decorations) {
|
||||
public record RenderJob(
|
||||
WorldSnapshot snapshot,
|
||||
List<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities,
|
||||
List<DecorationState> decorations) {
|
||||
|
||||
/** Backwards-compatible constructor (no block-entities/decorations), used by the standalone harness. */
|
||||
public RenderJob(WorldSnapshot snapshot, List<Vector> rayMap, Vector origin,
|
||||
int width, int height, SkyContext sky, List<EntityState> entities) {
|
||||
public RenderJob(
|
||||
WorldSnapshot snapshot,
|
||||
List<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> entities) {
|
||||
this(snapshot, rayMap, origin, width, height, sky, entities, List.of(), List.of());
|
||||
}
|
||||
|
||||
/** Convenience for callers that supply entities + block-entities but no decorations. */
|
||||
public RenderJob(WorldSnapshot snapshot, List<Vector> rayMap, Vector origin,
|
||||
int width, int height, SkyContext sky, List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities) {
|
||||
public RenderJob(
|
||||
WorldSnapshot snapshot,
|
||||
List<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities) {
|
||||
this(snapshot, rayMap, origin, width, height, sky, entities, blockEntities, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.render;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import org.bukkit.Location;
|
||||
|
||||
public interface Renderer {
|
||||
BufferedImage render(Location eyeLocation, Resolution resolution);
|
||||
|
||||
@@ -9,8 +9,9 @@ public final class Resolution {
|
||||
|
||||
public Resolution(Pixels pixels, AspectRatio aspectRatio) {
|
||||
this(
|
||||
(int) Math.round(Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio),
|
||||
pixels.height);
|
||||
(int) Math.round(
|
||||
Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio),
|
||||
pixels.height);
|
||||
}
|
||||
|
||||
public Resolution(int width, int height) {
|
||||
|
||||
@@ -5,5 +5,4 @@ package eu.mhsl.minecraft.pixelpics.render.sky;
|
||||
* (0..7) and the absolute world time (for continuous cloud drift). Immutable so it can be read from
|
||||
* worker threads.
|
||||
*/
|
||||
public record SkyContext(long dayTime, int moonPhase, long fullTime) {
|
||||
}
|
||||
public record SkyContext(long dayTime, int moonPhase, long fullTime) {}
|
||||
|
||||
@@ -14,10 +14,10 @@ public final class SkyRenderer {
|
||||
|
||||
private static final double TICKS_PER_DAY = 24000.0;
|
||||
private static final double CLOUD_HEIGHT = 192.0;
|
||||
private static final double CLOUD_CELL = 12.0; // world blocks per cloud texel
|
||||
private static final double CLOUD_SPEED = 0.03; // blocks per tick, drift along +X
|
||||
private static final double CLOUD_CELL = 12.0; // world blocks per cloud texel
|
||||
private static final double CLOUD_SPEED = 0.03; // blocks per tick, drift along +X
|
||||
|
||||
private static final double SUN_HALF = 0.085; // angular half-size (radians)
|
||||
private static final double SUN_HALF = 0.085; // angular half-size (radians)
|
||||
private static final double MOON_HALF = 0.075;
|
||||
|
||||
// Gradient endpoints (RGB).
|
||||
@@ -34,16 +34,21 @@ public final class SkyRenderer {
|
||||
private final int[][] cloudTexture;
|
||||
|
||||
public SkyRenderer(TextureCache textures) {
|
||||
this.sunTexture = textures.get(ResourceLocation.parse("environment/sun")).orElse(null);
|
||||
this.moonTexture = textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null);
|
||||
this.cloudTexture = textures.get(ResourceLocation.parse("environment/clouds")).orElse(null);
|
||||
this.sunTexture =
|
||||
textures.get(ResourceLocation.parse("environment/sun")).orElse(null);
|
||||
this.moonTexture =
|
||||
textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null);
|
||||
this.cloudTexture =
|
||||
textures.get(ResourceLocation.parse("environment/clouds")).orElse(null);
|
||||
}
|
||||
|
||||
public int colorFor(Vector direction, Vector origin, SkyContext ctx) {
|
||||
double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ();
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len < 1e-9) return DAY_ZENITH;
|
||||
dx /= len; dy /= len; dz /= len;
|
||||
dx /= len;
|
||||
dy /= len;
|
||||
dz /= len;
|
||||
|
||||
// Sun/moon position, derived exactly from Minecraft's sky transforms:
|
||||
// celestialAngle ca = getTimeOfDay(dayTime); the sun is rotated by ca*360deg about the X axis
|
||||
@@ -65,8 +70,8 @@ public final class SkyRenderer {
|
||||
if (twilight > 0) {
|
||||
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);
|
||||
? lerp(SUNSET_ORANGE, SUNSET_RED, up / 0.40)
|
||||
: lerp(SUNSET_RED, TWI_PURPLE, (up - 0.40) / 0.60);
|
||||
int twiColor = lerp(lerp(TWI_PURPLE, grad, 0.55), grad, az); // cooler away from the sun
|
||||
color = lerp(color, twiColor, twilight * 0.85);
|
||||
}
|
||||
@@ -98,7 +103,8 @@ public final class SkyRenderer {
|
||||
}
|
||||
// Moon disc (phase shape from the texture's alpha).
|
||||
if (-sunY > -0.15) {
|
||||
color = overlayDisc(color, dx, dy, dz, -sunX, -sunY, 0, MOON_HALF, moonTexture, rgb(228, 228, 238), ctx.moonPhase());
|
||||
color = overlayDisc(
|
||||
color, dx, dy, dz, -sunX, -sunY, 0, MOON_HALF, moonTexture, rgb(228, 228, 238), ctx.moonPhase());
|
||||
}
|
||||
|
||||
// Cloud layer: the ray crosses the cloud plane at y = CLOUD_HEIGHT; the world hit point is
|
||||
@@ -121,8 +127,18 @@ public final class SkyRenderer {
|
||||
}
|
||||
|
||||
/** Draws a sun/moon disc, sampling a texture when available (moonPhase ≥ 0 picks the phase tile). */
|
||||
private int overlayDisc(int base, double dx, double dy, double dz,
|
||||
double cx, double cy, double cz, double half, int[][] texture, int solid, int moonPhase) {
|
||||
private int overlayDisc(
|
||||
int base,
|
||||
double dx,
|
||||
double dy,
|
||||
double dz,
|
||||
double cx,
|
||||
double cy,
|
||||
double cz,
|
||||
double half,
|
||||
int[][] texture,
|
||||
int solid,
|
||||
int moonPhase) {
|
||||
double cos = dx * cx + dy * cy + dz * cz;
|
||||
if (cos <= 0) return base;
|
||||
double sinHalf = Math.sin(half);
|
||||
@@ -130,8 +146,15 @@ public final class SkyRenderer {
|
||||
// right = normalize(body x worldUp); discUp = right x body
|
||||
double crx = cz, cry = 0, crz = -cx;
|
||||
double crl = Math.sqrt(crx * crx + crz * crz);
|
||||
if (crl < 1e-6) { crx = 1; cry = 0; crz = 0; crl = 1; }
|
||||
crx /= crl; cry /= crl; crz /= crl;
|
||||
if (crl < 1e-6) {
|
||||
crx = 1;
|
||||
cry = 0;
|
||||
crz = 0;
|
||||
crl = 1;
|
||||
}
|
||||
crx /= crl;
|
||||
cry /= crl;
|
||||
crz /= crl;
|
||||
// discUp = right cross body
|
||||
double ux = cry * cz - crz * cy;
|
||||
double uy = crz * cx - crx * cz;
|
||||
@@ -201,8 +224,7 @@ public final class SkyRenderer {
|
||||
return alpha > 16 ? 0.85 : 0.0;
|
||||
}
|
||||
double scale = 0.012;
|
||||
double n = valueNoise(x * scale, z * scale) * 0.6
|
||||
+ valueNoise(x * scale * 2.3, z * scale * 2.3) * 0.4;
|
||||
double n = valueNoise(x * scale, z * scale) * 0.6 + valueNoise(x * scale * 2.3, z * scale * 2.3) * 0.4;
|
||||
return smoothstep(0.52, 0.72, n) * 0.8;
|
||||
}
|
||||
|
||||
@@ -230,7 +252,9 @@ public final class SkyRenderer {
|
||||
|
||||
// --- small color/math helpers ---
|
||||
|
||||
private static int rgb(int r, int g, int b) { return (r << 16) | (g << 8) | b; }
|
||||
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 = Math.clamp(t, 0, 1);
|
||||
@@ -260,5 +284,7 @@ public final class SkyRenderer {
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
private static int clamp(int v, int lo, int hi) { return v < lo ? lo : Math.min(v, hi); }
|
||||
private static int clamp(int v, int lo, int hi) {
|
||||
return v < lo ? lo : Math.min(v, hi);
|
||||
}
|
||||
}
|
||||
|
||||
+115
-38
@@ -7,6 +7,11 @@ 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 eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -29,12 +34,6 @@ import org.bukkit.block.sign.SignSide;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Captures block-entities (chests, signs, banners, beds, heads, …) near the view frustum into immutable
|
||||
* {@link BlockEntityState}s. MUST run on the main thread (live {@link BlockState} access). Reads each
|
||||
@@ -82,15 +81,15 @@ public final class BlockEntitySnapshotBuilder {
|
||||
|
||||
// --- chests ---
|
||||
if (mat == Material.CHEST || mat == Material.TRAPPED_CHEST || mat == Material.ENDER_CHEST) {
|
||||
Kind kind = mat == Material.TRAPPED_CHEST ? Kind.TRAPPED_CHEST
|
||||
: mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST;
|
||||
Kind kind = mat == Material.TRAPPED_CHEST
|
||||
? Kind.TRAPPED_CHEST
|
||||
: mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST;
|
||||
ChestKind ck = ChestKind.SINGLE;
|
||||
if (data instanceof Chest cd) {
|
||||
ck = switch (cd.getType()) {
|
||||
case LEFT -> ChestKind.LEFT;
|
||||
case RIGHT -> ChestKind.RIGHT;
|
||||
case SINGLE -> ChestKind.SINGLE;
|
||||
};
|
||||
case SINGLE -> ChestKind.SINGLE;};
|
||||
}
|
||||
return base(kind, bx, by, bz, facingYaw(data)).chestKind(ck).build();
|
||||
}
|
||||
@@ -99,13 +98,17 @@ public final class BlockEntitySnapshotBuilder {
|
||||
if (data instanceof Bed bed) {
|
||||
BedPart part = bed.getPart() == Bed.Part.HEAD ? BedPart.HEAD : BedPart.FOOT;
|
||||
return base(Kind.BED, bx, by, bz, faceToYaw(bed.getFacing()))
|
||||
.bedPart(part).colorName(stripColor(n, "_BED")).build();
|
||||
.bedPart(part)
|
||||
.colorName(stripColor(n, "_BED"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// --- shulker boxes ---
|
||||
if (n.endsWith("SHULKER_BOX")) {
|
||||
String color = n.equals("SHULKER_BOX") ? null : stripColor(n, "_SHULKER_BOX");
|
||||
return base(Kind.SHULKER_BOX, bx, by, bz, facingYaw(data)).colorName(color).build();
|
||||
return base(Kind.SHULKER_BOX, bx, by, bz, facingYaw(data))
|
||||
.colorName(color)
|
||||
.build();
|
||||
}
|
||||
|
||||
// --- banners ---
|
||||
@@ -131,12 +134,14 @@ public final class BlockEntitySnapshotBuilder {
|
||||
Kind kind;
|
||||
float yaw;
|
||||
if (n.endsWith("_WALL_SIGN")) {
|
||||
kind = Kind.WALL_SIGN; yaw = facingYaw(data);
|
||||
kind = Kind.WALL_SIGN;
|
||||
yaw = facingYaw(data);
|
||||
} else if (n.endsWith("_HANGING_SIGN")) {
|
||||
kind = Kind.HANGING_SIGN;
|
||||
yaw = n.endsWith("_WALL_HANGING_SIGN") ? facingYaw(data) : rotationYaw(data);
|
||||
} else {
|
||||
kind = Kind.SIGN; yaw = rotationYaw(data);
|
||||
kind = Kind.SIGN;
|
||||
yaw = rotationYaw(data);
|
||||
}
|
||||
Builder b = base(kind, bx, by, bz, yaw).wood(wood);
|
||||
if (ts instanceof Sign sign) {
|
||||
@@ -178,10 +183,11 @@ public final class BlockEntitySnapshotBuilder {
|
||||
case FLOOR -> BellAttach.FLOOR;
|
||||
case CEILING -> BellAttach.CEILING;
|
||||
case SINGLE_WALL -> BellAttach.SINGLE_WALL;
|
||||
case DOUBLE_WALL -> BellAttach.DOUBLE_WALL;
|
||||
};
|
||||
case DOUBLE_WALL -> BellAttach.DOUBLE_WALL;};
|
||||
}
|
||||
return base(Kind.BELL, bx, by, bz, facingYaw(data)).bellAttach(attach).build();
|
||||
return base(Kind.BELL, bx, by, bz, facingYaw(data))
|
||||
.bellAttach(attach)
|
||||
.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -250,14 +256,17 @@ public final class BlockEntitySnapshotBuilder {
|
||||
if (!any) return null;
|
||||
DyeColor dye = side.getColor();
|
||||
boolean glow = side.isGlowingText();
|
||||
return new BlockEntityState.SignText(lines, ColorUtil.signFillArgb(dye, glow),
|
||||
ColorUtil.signOutlineArgb(dye), glow);
|
||||
return new BlockEntityState.SignText(
|
||||
lines, ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow);
|
||||
}
|
||||
|
||||
private static String signWood(String name) {
|
||||
String s = name;
|
||||
for (String suf : new String[]{"_WALL_HANGING_SIGN", "_HANGING_SIGN", "_WALL_SIGN", "_SIGN"}) {
|
||||
if (s.endsWith(suf)) { s = s.substring(0, s.length() - suf.length()); break; }
|
||||
for (String suf : new String[] {"_WALL_HANGING_SIGN", "_HANGING_SIGN", "_WALL_SIGN", "_SIGN"}) {
|
||||
if (s.endsWith(suf)) {
|
||||
s = s.substring(0, s.length() - suf.length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return s.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
@@ -278,8 +287,9 @@ public final class BlockEntitySnapshotBuilder {
|
||||
private static List<String> sherds(DecoratedPot pot) {
|
||||
// Order: front, left, right, back — matches the CEM decorated_pot face parts.
|
||||
List<String> out = new ArrayList<>(4);
|
||||
for (DecoratedPot.Side side : new DecoratedPot.Side[]{
|
||||
DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK}) {
|
||||
for (DecoratedPot.Side side : new DecoratedPot.Side[] {
|
||||
DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK
|
||||
}) {
|
||||
Material m = pot.getSherd(side);
|
||||
out.add(m.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
@@ -328,25 +338,92 @@ public final class BlockEntitySnapshotBuilder {
|
||||
private BlockEntityState.SignText backText;
|
||||
|
||||
Builder(Kind kind, int bx, int by, int bz, float yaw) {
|
||||
this.kind = kind; this.bx = bx; this.by = by; this.bz = bz; this.yaw = yaw;
|
||||
this.kind = kind;
|
||||
this.bx = bx;
|
||||
this.by = by;
|
||||
this.bz = bz;
|
||||
this.yaw = yaw;
|
||||
}
|
||||
|
||||
Builder chestKind(ChestKind v) { this.chestKind = v; return this; }
|
||||
Builder baseColorArgb(int v) { this.baseColorArgb = v; return this; }
|
||||
Builder colorName(String v) { this.colorName = v; return this; }
|
||||
Builder wood(String v) { this.wood = v; return this; }
|
||||
Builder bedPart(BedPart v) { this.bedPart = v; return this; }
|
||||
Builder headType(String v) { this.headType = v; return this; }
|
||||
Builder skinUrl(String v) { this.skinUrl = v; return this; }
|
||||
Builder patterns(List<BannerPattern> v) { this.patterns = v; return this; }
|
||||
Builder sherds(List<String> v) { this.sherds = v; return this; }
|
||||
Builder bellAttach(BellAttach v) { this.bellAttach = v; return this; }
|
||||
Builder frontText(BlockEntityState.SignText v) { this.frontText = v; return this; }
|
||||
Builder backText(BlockEntityState.SignText v) { this.backText = v; return this; }
|
||||
Builder chestKind(ChestKind v) {
|
||||
this.chestKind = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder baseColorArgb(int v) {
|
||||
this.baseColorArgb = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder colorName(String v) {
|
||||
this.colorName = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder wood(String v) {
|
||||
this.wood = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder bedPart(BedPart v) {
|
||||
this.bedPart = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder headType(String v) {
|
||||
this.headType = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder skinUrl(String v) {
|
||||
this.skinUrl = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder patterns(List<BannerPattern> v) {
|
||||
this.patterns = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder sherds(List<String> v) {
|
||||
this.sherds = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder bellAttach(BellAttach v) {
|
||||
this.bellAttach = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder frontText(BlockEntityState.SignText v) {
|
||||
this.frontText = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder backText(BlockEntityState.SignText v) {
|
||||
this.backText = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
BlockEntityState build() {
|
||||
return new BlockEntityState(kind, bx, by, bz, yaw, chestKind, baseColorArgb, colorName, wood,
|
||||
bedPart, headType, skinUrl, patterns, sherds, bellAttach, frontText, backText);
|
||||
return new BlockEntityState(
|
||||
kind,
|
||||
bx,
|
||||
by,
|
||||
bz,
|
||||
yaw,
|
||||
chestKind,
|
||||
baseColorArgb,
|
||||
colorName,
|
||||
wood,
|
||||
bedPart,
|
||||
headType,
|
||||
skinUrl,
|
||||
patterns,
|
||||
sherds,
|
||||
bellAttach,
|
||||
frontText,
|
||||
backText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-12
@@ -1,6 +1,9 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
@@ -11,10 +14,6 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Captures flat wall decorations (paintings + item frames) near the view frustum into immutable
|
||||
* {@link DecorationState}s. MUST run on the main thread. Uses each entity's world bounding box directly,
|
||||
@@ -42,19 +41,39 @@ public final class DecorationSnapshotBuilder {
|
||||
if (e instanceof Painting painting) {
|
||||
BoundingBox bb = e.getBoundingBox();
|
||||
String art = painting.getArt().assetId().value();
|
||||
return new DecorationState(DecorationState.Kind.PAINTING,
|
||||
bb.getMinX(), bb.getMinY(), bb.getMinZ(), bb.getMaxX(), bb.getMaxY(), bb.getMaxZ(),
|
||||
facing(e.getFacing()), art, null, 0, false);
|
||||
return new DecorationState(
|
||||
DecorationState.Kind.PAINTING,
|
||||
bb.getMinX(),
|
||||
bb.getMinY(),
|
||||
bb.getMinZ(),
|
||||
bb.getMaxX(),
|
||||
bb.getMaxY(),
|
||||
bb.getMaxZ(),
|
||||
facing(e.getFacing()),
|
||||
art,
|
||||
null,
|
||||
0,
|
||||
false);
|
||||
}
|
||||
if (e instanceof ItemFrame frame) {
|
||||
BoundingBox bb = e.getBoundingBox();
|
||||
boolean glow = e instanceof GlowItemFrame
|
||||
|| e.getType().getKey().getKey().equals("glow_item_frame");
|
||||
boolean glow =
|
||||
e instanceof GlowItemFrame || e.getType().getKey().getKey().equals("glow_item_frame");
|
||||
String itemId = itemId(frame.getItem());
|
||||
int rot = frame.getRotation().ordinal() * 45;
|
||||
return new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||||
bb.getMinX(), bb.getMinY(), bb.getMinZ(), bb.getMaxX(), bb.getMaxY(), bb.getMaxZ(),
|
||||
facing(e.getFacing()), null, itemId, rot, glow);
|
||||
return new DecorationState(
|
||||
DecorationState.Kind.ITEM_FRAME,
|
||||
bb.getMinX(),
|
||||
bb.getMinY(),
|
||||
bb.getMinZ(),
|
||||
bb.getMaxX(),
|
||||
bb.getMaxY(),
|
||||
bb.getMaxZ(),
|
||||
facing(e.getFacing()),
|
||||
null,
|
||||
itemId,
|
||||
rot,
|
||||
glow);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+82
-39
@@ -6,6 +6,15 @@ import com.google.gson.JsonParser;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import io.papermc.paper.datacomponent.DataComponentTypes;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.DoubleSupplier;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -47,16 +56,6 @@ import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.DoubleSupplier;
|
||||
|
||||
/**
|
||||
* Captures entities near the view frustum into immutable {@link EntityState}s. MUST run on the main
|
||||
* thread (live entity access). The camera entity is skipped.
|
||||
@@ -68,22 +67,46 @@ public final class EntitySnapshotBuilder {
|
||||
// Technical / non-mob entity types that have no meaningful geometry; rendering them would only
|
||||
// produce stray fallback boxes. Markers, displays, item frames, paintings, projectiles, drops, etc.
|
||||
private static final Set<String> NON_RENDERABLE = Set.of(
|
||||
"area_effect_cloud", "marker", "interaction",
|
||||
"item_frame", "glow_item_frame", "painting",
|
||||
"block_display", "item_display", "text_display",
|
||||
"fishing_bobber", "lightning_bolt", "eye_of_ender",
|
||||
"experience_orb", "experience_bottle", "egg", "snowball",
|
||||
"potion", "ender_pearl", "tnt", "falling_block", "item"
|
||||
);
|
||||
"area_effect_cloud",
|
||||
"marker",
|
||||
"interaction",
|
||||
"item_frame",
|
||||
"glow_item_frame",
|
||||
"painting",
|
||||
"block_display",
|
||||
"item_display",
|
||||
"text_display",
|
||||
"fishing_bobber",
|
||||
"lightning_bolt",
|
||||
"eye_of_ender",
|
||||
"experience_orb",
|
||||
"experience_bottle",
|
||||
"egg",
|
||||
"snowball",
|
||||
"potion",
|
||||
"ender_pearl",
|
||||
"tnt",
|
||||
"falling_block",
|
||||
"item");
|
||||
|
||||
// Entities whose vanilla renderer draws the humanoid armor layers (HumanoidArmorLayer) and held
|
||||
// items. Their CEM bodies share standard humanoid proportions, so the armor_layer_1/2 models align.
|
||||
private static final Set<String> HUMANOID_ARMOR_WEARERS = Set.of(
|
||||
"player", "mannequin", "armor_stand", "giant",
|
||||
"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin",
|
||||
"skeleton", "stray", "wither_skeleton", "bogged",
|
||||
"piglin", "piglin_brute"
|
||||
);
|
||||
"player",
|
||||
"mannequin",
|
||||
"armor_stand",
|
||||
"giant",
|
||||
"zombie",
|
||||
"husk",
|
||||
"drowned",
|
||||
"zombie_villager",
|
||||
"zombified_piglin",
|
||||
"skeleton",
|
||||
"stray",
|
||||
"wither_skeleton",
|
||||
"bogged",
|
||||
"piglin",
|
||||
"piglin_brute");
|
||||
|
||||
public static List<EntityState> build(Location eye, List<Vector> rayMap, double maxDistance, UUID shooter) {
|
||||
FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance);
|
||||
@@ -109,14 +132,13 @@ public final class EntitySnapshotBuilder {
|
||||
bodyYaw = le.getBodyYaw();
|
||||
}
|
||||
|
||||
boolean baby = (e instanceof Ageable a && !a.isAdult())
|
||||
|| (e instanceof Zombie z && z.isAdult());
|
||||
boolean baby = (e instanceof Ageable a && !a.isAdult()) || (e instanceof Zombie z && z.isAdult());
|
||||
|
||||
// Invisible entities render only their equipment (like vanilla): the generic invisible flag, an
|
||||
// invisibility potion effect, or an explicitly-hidden armor stand.
|
||||
boolean invisible = e.isInvisible()
|
||||
|| (e instanceof ArmorStand as && !as.isVisible())
|
||||
|| (e instanceof LivingEntity inv && inv.hasPotionEffect(PotionEffectType.INVISIBILITY));
|
||||
|| (e instanceof ArmorStand as && !as.isVisible())
|
||||
|| (e instanceof LivingEntity inv && inv.hasPotionEffect(PotionEffectType.INVISIBILITY));
|
||||
|
||||
double width = safeDim(e::getWidth, () -> e.getBoundingBox().getWidthX());
|
||||
double height = safeDim(e::getHeight, () -> e.getBoundingBox().getHeight());
|
||||
@@ -211,10 +233,29 @@ public final class EntitySnapshotBuilder {
|
||||
equipment = captureEquipment(wearer);
|
||||
}
|
||||
|
||||
return new EntityState(type, loc.getX(), loc.getY(), loc.getZ(),
|
||||
bodyYaw, baby, width, height,
|
||||
player, skinUrl, slim, variant, tint, sizeScale, profession, villagerLevel,
|
||||
markings, saddle, chest, bodyEquip, equipment, invisible);
|
||||
return new EntityState(
|
||||
type,
|
||||
loc.getX(),
|
||||
loc.getY(),
|
||||
loc.getZ(),
|
||||
bodyYaw,
|
||||
baby,
|
||||
width,
|
||||
height,
|
||||
player,
|
||||
skinUrl,
|
||||
slim,
|
||||
variant,
|
||||
tint,
|
||||
sizeScale,
|
||||
profession,
|
||||
villagerLevel,
|
||||
markings,
|
||||
saddle,
|
||||
chest,
|
||||
bodyEquip,
|
||||
equipment,
|
||||
invisible);
|
||||
}
|
||||
|
||||
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
|
||||
@@ -223,8 +264,8 @@ public final class EntitySnapshotBuilder {
|
||||
EntityEquipment eq = le.getEquipment();
|
||||
if (eq == null) return null;
|
||||
EntityState.Equipment equip = new EntityState.Equipment(
|
||||
armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()),
|
||||
armorPiece(eq.getLeggings()), armorPiece(eq.getBoots()));
|
||||
armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()),
|
||||
armorPiece(eq.getLeggings()), armorPiece(eq.getBoots()));
|
||||
return equip.isEmpty() ? null : equip;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
@@ -257,8 +298,11 @@ public final class EntitySnapshotBuilder {
|
||||
if (key.equals("elytra")) return "elytra";
|
||||
if (key.equals("turtle_helmet")) return "turtle_scute";
|
||||
String base = null;
|
||||
for (String suf : new String[]{"_helmet", "_chestplate", "_leggings", "_boots"}) {
|
||||
if (key.endsWith(suf)) { base = key.substring(0, key.length() - suf.length()); break; }
|
||||
for (String suf : new String[] {"_helmet", "_chestplate", "_leggings", "_boots"}) {
|
||||
if (key.endsWith(suf)) {
|
||||
base = key.substring(0, key.length() - suf.length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (base == null) return null;
|
||||
return base.equals("golden") ? "gold" : base;
|
||||
@@ -305,7 +349,7 @@ public final class EntitySnapshotBuilder {
|
||||
|
||||
/** Registry/Keyed values yield their key path; plain enums yield their lower-case name. */
|
||||
private static String keyOf(Object o) {
|
||||
return switch(o) {
|
||||
return switch (o) {
|
||||
case null -> null;
|
||||
case Keyed k -> k.getKey().getKey();
|
||||
case Enum<?> en -> en.name().toLowerCase(Locale.ROOT);
|
||||
@@ -318,8 +362,7 @@ public final class EntitySnapshotBuilder {
|
||||
try {
|
||||
for (ProfileProperty prop : player.getPlayerProfile().getProperties()) {
|
||||
if (!prop.getName().equals("textures")) continue;
|
||||
String json = new String(Base64.getDecoder().decode(prop.getValue()),
|
||||
StandardCharsets.UTF_8);
|
||||
String json = new String(Base64.getDecoder().decode(prop.getValue()), StandardCharsets.UTF_8);
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
JsonObject skin = root.getAsJsonObject("textures").getAsJsonObject("SKIN");
|
||||
String url = skin.get("url").getAsString();
|
||||
@@ -327,11 +370,11 @@ public final class EntitySnapshotBuilder {
|
||||
if (skin.has("metadata") && skin.getAsJsonObject("metadata").has("model")) {
|
||||
model = skin.getAsJsonObject("metadata").get("model").getAsString();
|
||||
}
|
||||
return new String[]{url, model};
|
||||
return new String[] {url, model};
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return new String[]{null, null};
|
||||
return new String[] {null, null};
|
||||
}
|
||||
|
||||
/** Reads a dimension via {@code primary}, falling back to {@code fallback} on any version mismatch. */
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
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
|
||||
@@ -19,8 +18,12 @@ final class FrustumBounds {
|
||||
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;
|
||||
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) {
|
||||
@@ -30,9 +33,12 @@ final class FrustumBounds {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
||||
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Captures the world region covered by the camera frustum into a {@link WorldSnapshot}.
|
||||
@@ -58,7 +57,8 @@ public final class SnapshotBuilder {
|
||||
}
|
||||
|
||||
if (skipped > 0) {
|
||||
logger.fine(String.format("Snapshot captured %d chunks, skipped %d (unloaded or over cap)", captured, skipped));
|
||||
logger.fine(
|
||||
String.format("Snapshot captured %d chunks, skipped %d (unloaded or over cap)", captured, skipped));
|
||||
}
|
||||
|
||||
return new WorldSnapshot(chunks, clampedMinY, clampedMaxY, Material.AIR.createBlockData());
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
||||
|
||||
import java.util.Map;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An immutable, thread-safe view of a bounded region of the world, backed by {@link ChunkSnapshot}s.
|
||||
* Block/biome lookups outside the captured region return air/null so rays simply terminate there.
|
||||
@@ -42,6 +41,11 @@ public final class WorldSnapshot {
|
||||
return cs.getBiome(x & 15, y, z & 15);
|
||||
}
|
||||
|
||||
public int minY() { return minY; }
|
||||
public int maxY() { return maxY; }
|
||||
public int minY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public int maxY() {
|
||||
return maxY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,13 @@ public final class BiomeClimate {
|
||||
|
||||
private static final Map<String, Climate> TABLE = new HashMap<>();
|
||||
|
||||
private static void put(String key, double t, double d) { TABLE.put(key, new Climate(t, d, DEFAULT_WATER)); }
|
||||
private static void put(String key, double t, double d, int water) { TABLE.put(key, new Climate(t, d, water)); }
|
||||
private static void put(String key, double t, double d) {
|
||||
TABLE.put(key, new Climate(t, d, DEFAULT_WATER));
|
||||
}
|
||||
|
||||
private static void put(String key, double t, double d, int water) {
|
||||
TABLE.put(key, new Climate(t, d, water));
|
||||
}
|
||||
|
||||
static {
|
||||
put("plains", 0.8, 0.4);
|
||||
|
||||
@@ -3,5 +3,4 @@ package eu.mhsl.minecraft.pixelpics.render.tint;
|
||||
/**
|
||||
* The biome-dependent tint colors (RGB) for the colormap-driven channels.
|
||||
*/
|
||||
public record BiomeTint(int grass, int foliage, int dryFoliage, int water) {
|
||||
}
|
||||
public record BiomeTint(int grass, int foliage, int dryFoliage, int water) {}
|
||||
|
||||
@@ -2,10 +2,9 @@ package eu.mhsl.minecraft.pixelpics.render.tint;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
/**
|
||||
* Computes per-biome grass/foliage tint colors by sampling the resource pack's colormaps using the
|
||||
@@ -20,8 +19,10 @@ public final class BiomeTintProvider {
|
||||
|
||||
public BiomeTintProvider(TextureCache textures) {
|
||||
this.grassMap = textures.get(ResourceLocation.parse("colormap/grass")).orElse(null);
|
||||
this.foliageMap = textures.get(ResourceLocation.parse("colormap/foliage")).orElse(null);
|
||||
this.dryFoliageMap = textures.get(ResourceLocation.parse("colormap/dry_foliage")).orElse(null);
|
||||
this.foliageMap =
|
||||
textures.get(ResourceLocation.parse("colormap/foliage")).orElse(null);
|
||||
this.dryFoliageMap =
|
||||
textures.get(ResourceLocation.parse("colormap/dry_foliage")).orElse(null);
|
||||
}
|
||||
|
||||
public BiomeTint forBiome(Biome biome) {
|
||||
@@ -57,7 +58,7 @@ public final class BiomeTintProvider {
|
||||
grass = 0xFF000000 | (((grass & 0xFEFEFE) + 0x28340A) >> 1);
|
||||
foliage = 0xFF000000 | (((foliage & 0xFEFEFE) + 0x28340A) >> 1);
|
||||
}
|
||||
default -> { }
|
||||
default -> {}
|
||||
}
|
||||
return new BiomeTint(grass, foliage, dry, 0xFF000000 | climate.water());
|
||||
}
|
||||
|
||||
@@ -32,8 +32,11 @@ public final class TintResolver {
|
||||
if (name.endsWith("stem")) return STEM;
|
||||
|
||||
// grass_block (top/overlay), short_grass, tall_grass, fern, large_fern, sugar_cane, ...
|
||||
if (name.contains("grass") || name.equals("fern") || name.equals("large_fern")
|
||||
|| name.equals("sugar_cane") || name.equals("potted_fern")) {
|
||||
if (name.contains("grass")
|
||||
|| name.equals("fern")
|
||||
|| name.equals("large_fern")
|
||||
|| name.equals("sugar_cane")
|
||||
|| name.equals("potted_fern")) {
|
||||
return biomeTint.grass();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,21 @@ public final class ColorUtil {
|
||||
|
||||
private ColorUtil() {}
|
||||
|
||||
public static int alpha(int argb) { return (argb >> 24) & 0xFF; }
|
||||
public static int red(int argb) { return (argb >> 16) & 0xFF; }
|
||||
public static int green(int argb) { return (argb >> 8) & 0xFF; }
|
||||
public static int blue(int argb) { return argb & 0xFF; }
|
||||
public static int alpha(int argb) {
|
||||
return (argb >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
public static int red(int argb) {
|
||||
return (argb >> 16) & 0xFF;
|
||||
}
|
||||
|
||||
public static int green(int argb) {
|
||||
return (argb >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
public static int blue(int argb) {
|
||||
return argb & 0xFF;
|
||||
}
|
||||
|
||||
public static int argb(int a, int r, int g, int b) {
|
||||
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||
@@ -31,24 +42,25 @@ public final class ColorUtil {
|
||||
* table (the firework/text colours, NOT the cloth colours). Null = black (the default sign ink).
|
||||
*/
|
||||
public static int signTextColor(DyeColor dye) {
|
||||
int rgb = switch (dye == null ? DyeColor.BLACK : dye) {
|
||||
case WHITE -> 0xF9FFFE;
|
||||
case ORANGE -> 0xF9801D;
|
||||
case MAGENTA -> 0xC74EBD;
|
||||
case LIGHT_BLUE -> 0x3AB3DA;
|
||||
case YELLOW -> 0xFED83D;
|
||||
case LIME -> 0x80C71F;
|
||||
case PINK -> 0xF38BAA;
|
||||
case GRAY -> 0x474F52;
|
||||
case LIGHT_GRAY -> 0x9D9D97;
|
||||
case CYAN -> 0x169C9C;
|
||||
case PURPLE -> 0x8932B8;
|
||||
case BLUE -> 0x3C44AA;
|
||||
case BROWN -> 0x835432;
|
||||
case GREEN -> 0x5E7C16;
|
||||
case RED -> 0xB02E26;
|
||||
case BLACK -> 0x1D1D21;
|
||||
};
|
||||
int rgb =
|
||||
switch (dye == null ? DyeColor.BLACK : dye) {
|
||||
case WHITE -> 0xF9FFFE;
|
||||
case ORANGE -> 0xF9801D;
|
||||
case MAGENTA -> 0xC74EBD;
|
||||
case LIGHT_BLUE -> 0x3AB3DA;
|
||||
case YELLOW -> 0xFED83D;
|
||||
case LIME -> 0x80C71F;
|
||||
case PINK -> 0xF38BAA;
|
||||
case GRAY -> 0x474F52;
|
||||
case LIGHT_GRAY -> 0x9D9D97;
|
||||
case CYAN -> 0x169C9C;
|
||||
case PURPLE -> 0x8932B8;
|
||||
case BLUE -> 0x3C44AA;
|
||||
case BROWN -> 0x835432;
|
||||
case GREEN -> 0x5E7C16;
|
||||
case RED -> 0xB02E26;
|
||||
case BLACK -> 0x1D1D21;
|
||||
};
|
||||
return 0xFF000000 | rgb;
|
||||
}
|
||||
|
||||
@@ -108,6 +120,7 @@ public final class ColorUtil {
|
||||
// --- Gamma-correct (linear-light) averaging ---
|
||||
|
||||
private static final float[] SRGB_TO_LINEAR = new float[256];
|
||||
|
||||
static {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
double c = i / 255.0;
|
||||
|
||||
@@ -5,36 +5,36 @@ import org.bukkit.util.Vector;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
private MathUtil() {}
|
||||
private MathUtil() {}
|
||||
|
||||
private static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) {
|
||||
double oldX = base.getX();
|
||||
double oldY = base.getY();
|
||||
double oldZ = base.getZ();
|
||||
private static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) {
|
||||
double oldX = base.getX();
|
||||
double oldY = base.getY();
|
||||
double oldZ = base.getZ();
|
||||
|
||||
double sinOne = Math.sin(angleYaw);
|
||||
double sinTwo = Math.sin(anglePitch);
|
||||
double cosOne = Math.cos(angleYaw);
|
||||
double cosTwo = Math.cos(anglePitch);
|
||||
double sinOne = Math.sin(angleYaw);
|
||||
double sinTwo = Math.sin(anglePitch);
|
||||
double cosOne = Math.cos(angleYaw);
|
||||
double cosTwo = Math.cos(anglePitch);
|
||||
|
||||
double newX = oldX * cosOne * cosTwo - oldY * cosOne * sinTwo - oldZ * sinOne;
|
||||
double newY = oldX * sinTwo + oldY * cosTwo;
|
||||
double newZ = oldX * sinOne * cosTwo - oldY * sinOne * sinTwo + oldZ * cosOne;
|
||||
double newX = oldX * cosOne * cosTwo - oldY * cosOne * sinTwo - oldZ * sinOne;
|
||||
double newY = oldX * sinTwo + oldY * cosTwo;
|
||||
double newZ = oldX * sinOne * cosTwo - oldY * sinOne * sinTwo + oldZ * cosOne;
|
||||
|
||||
return new Vector(newX, newY, newZ);
|
||||
}
|
||||
return new Vector(newX, newY, newZ);
|
||||
}
|
||||
|
||||
public static Vector doubleYawPitchRotation(Vector base, double firstYaw, double firstPitch, double secondYaw,
|
||||
double secondPitch) {
|
||||
return yawPitchRotation(yawPitchRotation(base, firstYaw, firstPitch), secondYaw, secondPitch);
|
||||
}
|
||||
public static Vector doubleYawPitchRotation(
|
||||
Vector base, double firstYaw, double firstPitch, double secondYaw, double secondPitch) {
|
||||
return yawPitchRotation(yawPitchRotation(base, firstYaw, firstPitch), secondYaw, secondPitch);
|
||||
}
|
||||
|
||||
/** Reflects {@code direction} across the plane with the given (unit) {@code normal}. */
|
||||
public static Vector reflectVector(Vector direction, Vector normal) {
|
||||
return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal)));
|
||||
}
|
||||
/** 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 Vector toVector(BlockFace face) {
|
||||
return new Vector(face.getModX(), face.getModY(), face.getModZ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package eu.mhsl.minecraft.pixelpics.survival;
|
||||
import com.destroystokyo.paper.profile.PlayerProfile;
|
||||
import com.destroystokyo.paper.profile.ProfileProperty;
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
@@ -17,10 +20,6 @@ import org.bukkit.inventory.meta.components.EquippableComponent;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Factory and identity helpers for the survival items: the camera and the film roll are custom-skinned
|
||||
* {@code PLAYER_HEAD}s (the texture renders client-side without a resource pack), the photo is the
|
||||
@@ -35,8 +34,10 @@ public final class CameraItems {
|
||||
private static final int CAMERA_MODEL_DATA = 1001;
|
||||
private static final int FILM_MODEL_DATA = 1002;
|
||||
|
||||
static final String CAMERA_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDlkMmNiZjAyZDMwOGI2MDY1YTZmZThjNjU3MWI2MzU2NjMzZjQxOTJlOGVjNzEyMTNjNzcwNzgwZTNkZTRlMiJ9fX0=";
|
||||
static final String FILM_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTVkMGY0OGJlNzNkYmIwZDJjYjE1NTRjMmUzODZiNWNjM2FiMjFhNGRjYWU4ZmYzOGI3NzRhZDNkMDFkMGE1OSJ9fX0=";
|
||||
static final String CAMERA_TEXTURE_B64 =
|
||||
"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDlkMmNiZjAyZDMwOGI2MDY1YTZmZThjNjU3MWI2MzU2NjMzZjQxOTJlOGVjNzEyMTNjNzcwNzgwZTNkZTRlMiJ9fX0=";
|
||||
static final String FILM_TEXTURE_B64 =
|
||||
"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTVkMGY0OGJlNzNkYmIwZDJjYjE1NTRjMmUzODZiNWNjM2FiMjFhNGRjYWU4ZmYzOGI3NzRhZDNkMDFkMGE1OSJ9fX0=";
|
||||
|
||||
private CameraItems() {}
|
||||
|
||||
@@ -48,8 +49,7 @@ public final class CameraItems {
|
||||
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) item.getItemMeta();
|
||||
applyHead(meta, "pixelpics:camera", CAMERA_TEXTURE_B64);
|
||||
meta.displayName(Component.text("Kamera", NamedTextColor.AQUA)
|
||||
.decoration(TextDecoration.ITALIC, false));
|
||||
meta.displayName(Component.text("Kamera", NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false));
|
||||
meta.setCustomModelData(CAMERA_MODEL_DATA);
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().cameraMarker, PersistentDataType.BYTE, (byte) 1);
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count);
|
||||
@@ -64,11 +64,10 @@ public final class CameraItems {
|
||||
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) item.getItemMeta();
|
||||
applyHead(meta, "pixelpics:film", FILM_TEXTURE_B64);
|
||||
meta.displayName(Component.text("Filmrolle", NamedTextColor.GREEN)
|
||||
.decoration(TextDecoration.ITALIC, false));
|
||||
meta.displayName(Component.text("Filmrolle", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false));
|
||||
meta.setCustomModelData(FILM_MODEL_DATA);
|
||||
meta.lore(List.of(Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY)
|
||||
.decoration(TextDecoration.ITALIC, false)));
|
||||
meta.lore(List.of(
|
||||
Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false)));
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().filmMarker, PersistentDataType.BYTE, (byte) 1);
|
||||
makeUnwearable(meta);
|
||||
item.setItemMeta(meta);
|
||||
@@ -99,15 +98,17 @@ public final class CameraItems {
|
||||
/** A photo is a filled map carrying our picture id. */
|
||||
public static boolean isPhoto(ItemStack item) {
|
||||
if (item == null || item.getType() != Material.FILLED_MAP || !item.hasItemMeta()) return false;
|
||||
return item.getItemMeta().getPersistentDataContainer()
|
||||
.has(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
return item.getItemMeta()
|
||||
.getPersistentDataContainer()
|
||||
.has(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
/** Loaded film on a camera, or 0 if the item is not a camera. */
|
||||
public static int getFilmCount(ItemStack item) {
|
||||
if (!isCamera(item)) return 0;
|
||||
Integer v = item.getItemMeta().getPersistentDataContainer()
|
||||
.get(Main.getInstance().filmCountKey, PersistentDataType.INTEGER);
|
||||
Integer v = item.getItemMeta()
|
||||
.getPersistentDataContainer()
|
||||
.get(Main.getInstance().filmCountKey, PersistentDataType.INTEGER);
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
@@ -121,11 +122,12 @@ public final class CameraItems {
|
||||
|
||||
private static void applyCameraLore(ItemMeta meta, int count) {
|
||||
meta.lore(List.of(
|
||||
Component.text("Film: " + count + " / " + MAX_FILM,
|
||||
count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED).decoration(TextDecoration.ITALIC, false),
|
||||
Component.text("Rechtsklick: Foto aufnehmen", NamedTextColor.DARK_GRAY)
|
||||
.decoration(TextDecoration.ITALIC, false)
|
||||
));
|
||||
Component.text(
|
||||
"Film: " + count + " / " + MAX_FILM,
|
||||
count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED)
|
||||
.decoration(TextDecoration.ITALIC, false),
|
||||
Component.text("Rechtsklick: Foto aufnehmen", NamedTextColor.DARK_GRAY)
|
||||
.decoration(TextDecoration.ITALIC, false)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Sound;
|
||||
@@ -11,10 +14,6 @@ import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Takes a photo when a player right-clicks while holding a camera. Consumes one loaded film per shot;
|
||||
* with no film loaded it gives a short fail feedback. Guards against the well-known double-fire of
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import java.util.function.Predicate;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -19,8 +20,6 @@ import org.bukkit.inventory.Recipe;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Drives the two dynamic recipes whose inputs carry variable NBT: <em>load film</em>
|
||||
* (camera + film → camera+1) and <em>copy photo</em> (photo + film → 2 photos sharing the same map).
|
||||
@@ -31,7 +30,11 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public class CraftingListener implements Listener {
|
||||
|
||||
private enum Kind { LOAD, COPY, NONE }
|
||||
private enum Kind {
|
||||
LOAD,
|
||||
COPY,
|
||||
NONE
|
||||
}
|
||||
|
||||
/** Scanned crafting grid: the relevant items and whether any of our marked items are present. */
|
||||
private record Scan(Kind kind, ItemStack camera, ItemStack film, ItemStack photo, boolean hasOurItems) {}
|
||||
@@ -44,9 +47,8 @@ public class CraftingListener implements Listener {
|
||||
switch (scan.kind()) {
|
||||
case LOAD -> {
|
||||
int count = CameraItems.getFilmCount(scan.camera());
|
||||
inv.setResult(count >= CameraItems.MAX_FILM
|
||||
? null
|
||||
: CameraItems.withFilmCount(scan.camera(), count + 1));
|
||||
inv.setResult(
|
||||
count >= CameraItems.MAX_FILM ? null : CameraItems.withFilmCount(scan.camera(), count + 1));
|
||||
}
|
||||
case COPY -> inv.setResult(buildPhotoCopy(scan.photo()));
|
||||
case NONE -> {
|
||||
@@ -109,10 +111,16 @@ public class CraftingListener implements Listener {
|
||||
ItemStack camera = null, film = null, photo = null;
|
||||
for (ItemStack it : matrix) {
|
||||
if (it == null || it.getType().isAir()) continue;
|
||||
if (CameraItems.isCamera(it)) { cameras++; camera = it; }
|
||||
else if (CameraItems.isFilm(it)) { films++; film = it; }
|
||||
else if (CameraItems.isPhoto(it)) { photos++; photo = it; }
|
||||
else others++;
|
||||
if (CameraItems.isCamera(it)) {
|
||||
cameras++;
|
||||
camera = it;
|
||||
} else if (CameraItems.isFilm(it)) {
|
||||
films++;
|
||||
film = it;
|
||||
} else if (CameraItems.isPhoto(it)) {
|
||||
photos++;
|
||||
photo = it;
|
||||
} else others++;
|
||||
}
|
||||
boolean ours = cameras + films + photos > 0;
|
||||
Kind kind = Kind.NONE;
|
||||
@@ -136,11 +144,9 @@ public class CraftingListener implements Listener {
|
||||
ItemStack copy = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta dst = (MapMeta) copy.getItemMeta();
|
||||
dst.setMapView(src.getMapView());
|
||||
String pid = src.getPersistentDataContainer()
|
||||
.get(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
String pid = src.getPersistentDataContainer().get(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
if (pid != null) {
|
||||
dst.getPersistentDataContainer()
|
||||
.set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid);
|
||||
dst.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid);
|
||||
}
|
||||
copy.setItemMeta(dst);
|
||||
return copy;
|
||||
@@ -152,7 +158,10 @@ public class CraftingListener implements Listener {
|
||||
if (it != null && !it.getType().isAir() && pred.test(it)) {
|
||||
int amt = it.getAmount();
|
||||
if (amt <= 1) matrix[i] = null;
|
||||
else { it.setAmount(amt - 1); matrix[i] = it; }
|
||||
else {
|
||||
it.setAmount(amt - 1);
|
||||
matrix[i] = it;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -163,8 +172,8 @@ public class CraftingListener implements Listener {
|
||||
if (cursor.getType().isAir()) {
|
||||
player.setItemOnCursor(item);
|
||||
} else {
|
||||
player.getInventory().addItem(item).values()
|
||||
.forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left));
|
||||
player.getInventory().addItem(item).values().forEach(left -> player.getWorld()
|
||||
.dropItemNaturally(player.getLocation(), left));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -22,9 +24,6 @@ import org.bukkit.map.MapView;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Renders a photo of a player's current view and delivers it as a developing {@code FILLED_MAP},
|
||||
* with a shutter sound + flash particles on capture and a chime when the picture finishes developing.
|
||||
@@ -47,8 +46,8 @@ public final class PhotoService {
|
||||
Main plugin = Main.getInstance();
|
||||
DefaultScreenRenderer renderer = plugin.getScreenRenderer();
|
||||
if (renderer == null) {
|
||||
player.sendActionBar(Component.text("PixelPics ist nicht einsatzbereit (kein Resource-Pack).",
|
||||
NamedTextColor.RED));
|
||||
player.sendActionBar(
|
||||
Component.text("PixelPics ist nicht einsatzbereit (kein Resource-Pack).", NamedTextColor.RED));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,9 +55,11 @@ public final class PhotoService {
|
||||
UUID user = player.getUniqueId();
|
||||
RenderManager.Outcome outcome = manager.tryReserve(user);
|
||||
if (outcome != RenderManager.Outcome.ACCEPTED) {
|
||||
player.sendActionBar(Component.text(outcome == RenderManager.Outcome.USER_BUSY
|
||||
? "Deine letzte Aufnahme wird noch entwickelt …"
|
||||
: "Zu viele Aufnahmen gerade — versuch es gleich erneut.", NamedTextColor.RED));
|
||||
player.sendActionBar(Component.text(
|
||||
outcome == RenderManager.Outcome.USER_BUSY
|
||||
? "Deine letzte Aufnahme wird noch entwickelt …"
|
||||
: "Zu viele Aufnahmen gerade — versuch es gleich erneut.",
|
||||
NamedTextColor.RED));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -77,8 +78,11 @@ public final class PhotoService {
|
||||
|
||||
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
|
||||
MapMeta meta = (MapMeta) map.getItemMeta();
|
||||
meta.getPersistentDataContainer().set(plugin.pictureIdFlag,
|
||||
PersistentDataType.STRING, UUID.randomUUID().toString());
|
||||
meta.getPersistentDataContainer()
|
||||
.set(
|
||||
plugin.pictureIdFlag,
|
||||
PersistentDataType.STRING,
|
||||
UUID.randomUUID().toString());
|
||||
meta.setMapView(mapView);
|
||||
map.setItemMeta(meta);
|
||||
player.getInventory().addItem(map);
|
||||
@@ -88,19 +92,20 @@ public final class PhotoService {
|
||||
|
||||
// Trace + dither off-thread (queued), then develop on the main thread. The cancel token is
|
||||
// tripped by the watchdog on timeout; we bail out before the (now pointless) dithering.
|
||||
manager.dispatch(user,
|
||||
cancelled -> {
|
||||
BufferedImage image = renderer.execute(job, cancelled);
|
||||
if (cancelled.get()) return null;
|
||||
return new RenderOutput(image, MapImageDither.dither(image));
|
||||
},
|
||||
out -> {
|
||||
MapManager.saveImage(out.image(), id);
|
||||
MapManager.saveIndices(out.indices(), id);
|
||||
mapRenderer.develop(out.indices());
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
|
||||
},
|
||||
() -> player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
|
||||
manager.dispatch(
|
||||
user,
|
||||
cancelled -> {
|
||||
BufferedImage image = renderer.execute(job, cancelled);
|
||||
if (cancelled.get()) return null;
|
||||
return new RenderOutput(image, MapImageDither.dither(image));
|
||||
},
|
||||
out -> {
|
||||
MapManager.saveImage(out.image(), id);
|
||||
MapManager.saveIndices(out.indices(), id);
|
||||
mapRenderer.develop(out.indices());
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
|
||||
},
|
||||
() -> player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
|
||||
dispatched = true;
|
||||
} finally {
|
||||
// prepare()/map setup threw before handing off — free the reserved slot.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import java.util.List;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -9,8 +10,6 @@ import org.bukkit.inventory.RecipeChoice;
|
||||
import org.bukkit.inventory.ShapedRecipe;
|
||||
import org.bukkit.inventory.ShapelessRecipe;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Registers the four survival recipes and makes them discoverable in the recipe book. Camera and
|
||||
* film have fixed results; load-film and copy-photo use {@code MaterialChoice} ingredients (their
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package eu.mhsl.minecraft.pixelpics.utils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapCanvas;
|
||||
import org.bukkit.map.MapRenderer;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Draws precomputed map palette indices onto the canvas. Supports a Polaroid-style "developing"
|
||||
* animation: the picture dissolves in over {@link #DEVELOP_MILLIS} once the render is ready, like an
|
||||
@@ -24,9 +23,9 @@ public class ImageMapRenderer extends MapRenderer {
|
||||
private static byte filmIndex = 0;
|
||||
private static boolean filmResolved = false;
|
||||
|
||||
private volatile byte[] indices; // MAP_SIZE*MAP_SIZE, null while still rendering
|
||||
private volatile byte[] indices; // MAP_SIZE*MAP_SIZE, null while still rendering
|
||||
private final boolean animate;
|
||||
private volatile long developStart = 0; // 0 until indices are available
|
||||
private volatile long developStart = 0; // 0 until indices are available
|
||||
private boolean finished = false;
|
||||
private boolean blankDrawn = false;
|
||||
|
||||
@@ -88,8 +87,7 @@ public class ImageMapRenderer extends MapRenderer {
|
||||
byte film = film();
|
||||
for (int y = 0; y < MAP_SIZE; y++) {
|
||||
for (int x = 0; x < MAP_SIZE; x++) {
|
||||
byte value = (progress >= 1.0 || revealThreshold(x, y) <= progress)
|
||||
? data[y * MAP_SIZE + x] : film;
|
||||
byte value = (progress >= 1.0 || revealThreshold(x, y) <= progress) ? data[y * MAP_SIZE + x] : film;
|
||||
canvas.setPixel(x, y, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package eu.mhsl.minecraft.pixelpics.utils;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import org.bukkit.map.MapPalette;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bukkit.map.MapPalette;
|
||||
|
||||
/**
|
||||
* The set of usable Minecraft map colors, with nearest-color matching in CIELAB (perceptual) space.
|
||||
@@ -92,7 +91,7 @@ public final class MapColorPalette {
|
||||
double fx = pivotXyz(x / 0.95047);
|
||||
double fy = pivotXyz(y);
|
||||
double fz = pivotXyz(z / 1.08883);
|
||||
return new double[]{116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)};
|
||||
return new double[] {116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)};
|
||||
}
|
||||
|
||||
private static double pivotXyz(double t) {
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
package eu.mhsl.minecraft.pixelpics.utils;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -19,6 +9,15 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.imageio.ImageIO;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
/**
|
||||
* Persists rendered images (PNG, source of truth) and their dithered map-color indices (cache) to
|
||||
|
||||
Reference in New Issue
Block a user