mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Draw every block in the Objects view the way Minecraft models it
The 3D object view guessed block shapes: stairs, gates, lanterns and similar became full cubes, fences and walls showed only their post, glass fell back to grey, and vines and ladders became crossed plants. It now reads the real models: every matching multipart part, each box with its own texture, UVs and face rotation, tilted and rescaled boxes, blockstate turns with uvlock, cullfaces against covering neighbours, and Minecraft's side shading. Chests, heads, banners and the bell use their entity textures; fluids and other code-drawn blocks get simple shapes. See-through textures like water, ice and stained glass are blended back to front. Texture objects with a "sprite" key (glass in 26.x) are read, which also fixes them in the palette map, and grey PNGs such as stone are no longer brightened by Java's colour conversion. Model used: Claude Opus 5.5
This commit is contained in:
1 parent
a02b4386a4
commit
b97bd51f20
11 files changed
+1262
-343
No files matched your search
@@ -16,112 +16,198 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
|
||||||
import java.util.EnumMap;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A block as the object viewer draws it: a box with a texture on each side, or two crossed sprites for plants.
|
* A block as the object viewer draws it: the textured faces of its model, in block units, already turned the way the
|
||||||
* Models made of several boxes, like stairs, are drawn as a full block.
|
* blockstate faces.
|
||||||
*
|
*
|
||||||
* @param box the box in block units, from 0 to 1 on every axis for a full block
|
* @param faces every face of every box in the model
|
||||||
* @param faces the texture on each side of the box; a side the model leaves open is missing
|
* @param coveredSides the block's sides that a solid face fills completely, which hide whatever touches them
|
||||||
* @param crossSprite the sprite drawn on two crossed planes, or null for a box
|
* @param hidesSameBlockSides whether the model fills the whole block, so like glass it hides the sides between two of
|
||||||
|
* the same blocks
|
||||||
*/
|
*/
|
||||||
record BlockModel(Box box, Map<CubeFace, BlockTexture> faces, BlockTexture crossSprite) {
|
record BlockModel(List<Face> faces, Set<CubeFace> coveredSides, boolean hidesSameBlockSides) {
|
||||||
static BlockModel box(Box box, Map<CubeFace, BlockTexture> faces) {
|
static final BlockModel EMPTY = new BlockModel(List.of(), Set.of(), false);
|
||||||
return new BlockModel(box, Map.copyOf(faces), null);
|
private static final int SIDE_CELLS = 16;
|
||||||
}
|
private static final double ON_SIDE_TOLERANCE = 1e-4D;
|
||||||
|
|
||||||
static BlockModel cross(BlockTexture sprite) {
|
static BlockModel of(List<Face> faces) {
|
||||||
return new BlockModel(Box.FULL, Map.of(), sprite);
|
Set<CubeFace> coveredSides = EnumSet.noneOf(CubeFace.class);
|
||||||
|
boolean fillsEverySide = true;
|
||||||
|
for (CubeFace side : CubeFace.values()) {
|
||||||
|
if (sideIsFilled(faces, side, true)) {
|
||||||
|
coveredSides.add(side);
|
||||||
|
}
|
||||||
|
fillsEverySide &= sideIsFilled(faces, side, false);
|
||||||
|
}
|
||||||
|
return new BlockModel(List.copyOf(faces), Set.copyOf(coveredSides), fillsEverySide);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BlockModel solidColor(int color) {
|
static BlockModel solidColor(int color) {
|
||||||
BlockTexture texture = new BlockTexture(1, new int[]{color}, new boolean[]{true}, color, null);
|
BlockTexture texture = BlockTexture.solid(color);
|
||||||
Map<CubeFace, BlockTexture> faces = new EnumMap<>(CubeFace.class);
|
List<Face> faces = new ArrayList<>();
|
||||||
for (CubeFace face : CubeFace.values()) {
|
for (CubeFace side : CubeFace.values()) {
|
||||||
faces.put(face, texture);
|
faces.add(Face.boxSide(Vec3.ZERO, new Vec3(1D, 1D, 1D), side, texture, side));
|
||||||
}
|
}
|
||||||
return box(Box.FULL, faces);
|
return of(faces);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isCross() {
|
boolean isEmpty() {
|
||||||
return crossSprite != null;
|
return faces.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
BlockModel withTextures(UnaryOperator<BlockTexture> change) {
|
||||||
* Whether a neighbour's side touching this block can never be seen.
|
Map<BlockTexture, BlockTexture> changed = new IdentityHashMap<>();
|
||||||
*/
|
List<Face> changedFaces = new ArrayList<>(faces.size());
|
||||||
boolean hidesTouchingSides() {
|
for (Face face : faces) {
|
||||||
if (isCross() || !box.isFull() || faces.size() < CubeFace.values().length) {
|
changedFaces.add(face.withTexture(changed.computeIfAbsent(face.texture(), change)));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
for (BlockTexture texture : faces.values()) {
|
return new BlockModel(List.copyOf(changedFaces), coveredSides, hidesSameBlockSides);
|
||||||
for (boolean opaque : texture.opaque()) {
|
}
|
||||||
if (!opaque) {
|
|
||||||
return false;
|
// The side is split into a grid, and every cell has to lie under a face pressed flat against it.
|
||||||
}
|
private static boolean sideIsFilled(List<Face> faces, CubeFace side, boolean onlySolidFaces) {
|
||||||
|
boolean[] filled = new boolean[SIDE_CELLS * SIDE_CELLS];
|
||||||
|
for (Face face : faces) {
|
||||||
|
if (face.liesOn(side) && (!onlySolidFaces || face.texture().isFullyOpaque())) {
|
||||||
|
face.fillCells(side, filled, SIDE_CELLS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (boolean cell : filled) {
|
||||||
|
if (!cell) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
record Box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
|
/**
|
||||||
static final Box FULL = new Box(0D, 0D, 0D, 1D, 1D, 1D);
|
* A parallelogram from {@code origin} along {@code edgeU} and {@code edgeV}. The texture point at the origin and
|
||||||
private static final double BLOCK_CENTER = 0.5D;
|
* how it moves along each edge are given as fractions of the texture.
|
||||||
|
*
|
||||||
|
* @param normal the way the face looks out; it is hidden when seen from behind
|
||||||
|
* @param cullFace the block side whose neighbour hides this face when it covers that side, or null
|
||||||
|
*/
|
||||||
|
record Face(Vec3 origin, Vec3 edgeU, Vec3 edgeV, Vec3 normal, BlockTexture texture, TextureMapping mapping,
|
||||||
|
double shade, CubeFace cullFace) {
|
||||||
|
|
||||||
static Box ofModelElement(double[] from, double[] to) {
|
// Laid out as seen from outside: u to the right and v downwards, matching Minecraft's UVs for a whole texture.
|
||||||
double pixels = 16D;
|
static Face boxSide(Vec3 from, Vec3 to, CubeFace side, BlockTexture texture, CubeFace cullFace) {
|
||||||
return new Box(from[0] / pixels, from[1] / pixels, from[2] / pixels, to[0] / pixels, to[1] / pixels, to[2] / pixels);
|
Vec3[] corners = BoxCorners.of(from, to, side);
|
||||||
|
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
|
||||||
|
texture, TextureMapping.WHOLE, side.shade(), cullFace);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isFull() {
|
Face withTexture(BlockTexture changedTexture) {
|
||||||
return equals(FULL);
|
return new Face(origin, edgeU, edgeV, normal, changedTexture, mapping, shade, cullFace);
|
||||||
}
|
}
|
||||||
|
|
||||||
double min(int axis) {
|
Face transformed(Transform transform) {
|
||||||
return switch (axis) {
|
Vec3 turnedNormal = transform.direction(normal);
|
||||||
case 0 -> minX;
|
double length = Math.sqrt(turnedNormal.x() * turnedNormal.x() + turnedNormal.y() * turnedNormal.y() + turnedNormal.z() * turnedNormal.z());
|
||||||
case 1 -> minY;
|
return new Face(transform.point(origin), transform.direction(edgeU), transform.direction(edgeV),
|
||||||
default -> minZ;
|
length == 0D ? turnedNormal : turnedNormal.times(1D / length), texture, mapping, shade,
|
||||||
};
|
cullFace == null ? null : CubeFace.nearest(transform.direction(cullFace.direction())));
|
||||||
}
|
}
|
||||||
|
|
||||||
double max(int axis) {
|
Face withCullFace(CubeFace side) {
|
||||||
return switch (axis) {
|
return new Face(origin, edgeU, edgeV, normal, texture, mapping, shade, side);
|
||||||
case 0 -> maxX;
|
|
||||||
case 1 -> maxY;
|
|
||||||
default -> maxZ;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blockstates turn a model around the block's centre, the x axis first.
|
Face withShade(double changedShade) {
|
||||||
Box rotated(int rotationX, int rotationY) {
|
return new Face(origin, edgeU, edgeV, normal, texture, mapping, changedShade, cullFace);
|
||||||
if (rotationX % 360 == 0 && rotationY % 360 == 0) {
|
}
|
||||||
return this;
|
|
||||||
|
private boolean liesOn(CubeFace side) {
|
||||||
|
int axis = side.axis();
|
||||||
|
double plane = side.pointsPositive() ? 1D : 0D;
|
||||||
|
return CubeFace.nearest(normal) == side
|
||||||
|
&& Math.abs(normal.component(axis)) > 1D - ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(origin.component(axis) - plane) < ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(edgeU.component(axis)) < ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(edgeV.component(axis)) < ON_SIDE_TOLERANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillCells(CubeFace side, boolean[] filled, int cells) {
|
||||||
|
int firstAxis = side.axis() == 0 ? 1 : 0;
|
||||||
|
int secondAxis = side.axis() == 2 ? 1 : 2;
|
||||||
|
double[] firstRange = range(firstAxis);
|
||||||
|
double[] secondRange = range(secondAxis);
|
||||||
|
for (int first = 0; first < cells; first++) {
|
||||||
|
double firstCenter = (first + 0.5D) / cells;
|
||||||
|
if (firstCenter < firstRange[0] || firstCenter > firstRange[1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int second = 0; second < cells; second++) {
|
||||||
|
double secondCenter = (second + 0.5D) / cells;
|
||||||
|
if (secondCenter >= secondRange[0] && secondCenter <= secondRange[1]) {
|
||||||
|
filled[first * cells + second] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
double[] first = turned(minX, minY, minZ, rotationX, rotationY);
|
|
||||||
double[] second = turned(maxX, maxY, maxZ, rotationX, rotationY);
|
|
||||||
return new Box(Math.min(first[0], second[0]), Math.min(first[1], second[1]), Math.min(first[2], second[2]),
|
|
||||||
Math.max(first[0], second[0]), Math.max(first[1], second[1]), Math.max(first[2], second[2]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double[] turned(double x, double y, double z, int rotationX, int rotationY) {
|
private double[] range(int axis) {
|
||||||
int[] axisX = CubeFace.turnAroundY(CubeFace.turnAroundX(new int[]{1, 0, 0}, rotationX), rotationY);
|
double start = origin.component(axis);
|
||||||
int[] axisY = CubeFace.turnAroundY(CubeFace.turnAroundX(new int[]{0, 1, 0}, rotationX), rotationY);
|
double alongU = edgeU.component(axis);
|
||||||
int[] axisZ = CubeFace.turnAroundY(CubeFace.turnAroundX(new int[]{0, 0, 1}, rotationX), rotationY);
|
double alongV = edgeV.component(axis);
|
||||||
double offsetX = x - BLOCK_CENTER;
|
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
|
||||||
double offsetY = y - BLOCK_CENTER;
|
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
|
||||||
double offsetZ = z - BLOCK_CENTER;
|
return new double[]{low, high};
|
||||||
return new double[]{
|
}
|
||||||
BLOCK_CENTER + axisX[0] * offsetX + axisY[0] * offsetY + axisZ[0] * offsetZ,
|
}
|
||||||
BLOCK_CENTER + axisX[1] * offsetX + axisY[1] * offsetY + axisZ[1] * offsetZ,
|
|
||||||
BLOCK_CENTER + axisX[2] * offsetX + axisY[2] * offsetY + axisZ[2] * offsetZ
|
/**
|
||||||
|
* Where a face's corner sits in its texture and how the texture point moves along the face's two edges, all as
|
||||||
|
* fractions of the texture's width and height.
|
||||||
|
*/
|
||||||
|
record TextureMapping(double u, double v, double uAlongU, double vAlongU, double uAlongV, double vAlongV) {
|
||||||
|
static final TextureMapping WHOLE = new TextureMapping(0D, 0D, 1D, 0D, 0D, 1D);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box side's corners in Minecraft's order: top left, bottom left, bottom right and top right, as seen from
|
||||||
|
* outside the box.
|
||||||
|
*/
|
||||||
|
static final class BoxCorners {
|
||||||
|
private BoxCorners() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vec3[] of(Vec3 from, Vec3 to, CubeFace side) {
|
||||||
|
double minX = from.x();
|
||||||
|
double minY = from.y();
|
||||||
|
double minZ = from.z();
|
||||||
|
double maxX = to.x();
|
||||||
|
double maxY = to.y();
|
||||||
|
double maxZ = to.z();
|
||||||
|
return switch (side) {
|
||||||
|
case DOWN -> corners(minX, minY, maxZ, minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ);
|
||||||
|
case UP -> corners(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ);
|
||||||
|
case NORTH -> corners(maxX, maxY, minZ, maxX, minY, minZ, minX, minY, minZ, minX, maxY, minZ);
|
||||||
|
case SOUTH -> corners(minX, maxY, maxZ, minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ);
|
||||||
|
case WEST -> corners(minX, maxY, minZ, minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ);
|
||||||
|
case EAST -> corners(maxX, maxY, maxZ, maxX, minY, maxZ, maxX, minY, minZ, maxX, maxY, minZ);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vec3[] corners(double... coordinates) {
|
||||||
|
Vec3[] corners = new Vec3[4];
|
||||||
|
for (int corner = 0; corner < 4; corner++) {
|
||||||
|
corners[corner] = new Vec3(coordinates[corner * 3], coordinates[corner * 3 + 1], coordinates[corner * 3 + 2]);
|
||||||
|
}
|
||||||
|
return corners;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.BoxCorners;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a block's look the way Minecraft does: every model part the blockstate picks, every box with its own texture
|
||||||
|
* and UVs, tilted boxes, the blockstate's turn with or without locked textures, and which faces a neighbour hides.
|
||||||
|
*/
|
||||||
|
final class BlockModelBuilder {
|
||||||
|
private static final double MODEL_UNITS = 16D;
|
||||||
|
private static final double BLOCK_CENTER = 0.5D;
|
||||||
|
private static final int QUARTER_TURN = 90;
|
||||||
|
|
||||||
|
private final BlockTextures textures;
|
||||||
|
private final EntityBlockShapes entityShapes;
|
||||||
|
|
||||||
|
BlockModelBuilder(BlockTextures textures) {
|
||||||
|
this.textures = textures;
|
||||||
|
this.entityShapes = new EntityBlockShapes(textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<BlockModel> build(String blockState) {
|
||||||
|
try {
|
||||||
|
BlockStateId state = BlockStateId.parse(blockState);
|
||||||
|
List<JsonObject> placements = placementsOf(state);
|
||||||
|
if (placements == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (JsonObject placement : placements) {
|
||||||
|
faces.addAll(placedFaces(state, placement));
|
||||||
|
}
|
||||||
|
faces.addAll(entityShapes.facesAddedTo(state));
|
||||||
|
return Optional.of(BlockModel.of(faces));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box with Minecraft's default UVs on each side, for shapes the game draws in code.
|
||||||
|
*/
|
||||||
|
static List<Face> box(Vec3 fromPixels, Vec3 toPixels, BlockTexture texture, boolean culledBySides) {
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (CubeFace side : CubeFace.values()) {
|
||||||
|
FaceUv uv = FaceUv.byDefault(side, fromPixels, toPixels);
|
||||||
|
Face face = boxFace(fromPixels, toPixels, side, texture, uv).withShade(side.shade());
|
||||||
|
faces.add(culledBySides ? face.withCullFace(side) : face);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A variant names one model; a multipart blockstate, like a fence's, adds a part for every condition that holds.
|
||||||
|
private List<JsonObject> placementsOf(BlockStateId state) throws IOException {
|
||||||
|
JsonObject definition = textures.readJson(BlockTextures.assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
|
||||||
|
if (definition == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<JsonObject> placements = new ArrayList<>();
|
||||||
|
if (definition.has("variants")) {
|
||||||
|
for (Map.Entry<String, JsonElement> variant : definition.getAsJsonObject("variants").entrySet()) {
|
||||||
|
if (BlockTextures.variantMatches(variant.getKey(), state.properties())) {
|
||||||
|
addPlacement(placements, variant.getValue());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (definition.has("multipart")) {
|
||||||
|
for (JsonElement part : definition.getAsJsonArray("multipart")) {
|
||||||
|
JsonObject rule = part.getAsJsonObject();
|
||||||
|
if (!rule.has("when") || BlockTextures.conditionMatches(rule.getAsJsonObject("when"), state.properties())) {
|
||||||
|
addPlacement(placements, rule.get("apply"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPlacement(List<JsonObject> placements, JsonElement models) {
|
||||||
|
JsonObject first = BlockTextures.firstModel(models);
|
||||||
|
if (first != null && first.has("model")) {
|
||||||
|
placements.add(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> placedFaces(BlockStateId state, JsonObject placement) throws IOException {
|
||||||
|
ModelDefinition model = textures.readModel(placement.get("model").getAsString());
|
||||||
|
if (model.elements() == null) {
|
||||||
|
return entityShapes.facesOf(state, model);
|
||||||
|
}
|
||||||
|
BlockRotation rotation = BlockRotation.of(placement);
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (JsonElement element : model.elements()) {
|
||||||
|
addElementFaces(faces, state, model, element.getAsJsonObject(), rotation);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addElementFaces(List<Face> faces, BlockStateId state, ModelDefinition model, JsonObject element, BlockRotation rotation) {
|
||||||
|
JsonObject elementFaces = element.getAsJsonObject("faces");
|
||||||
|
if (elementFaces == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vec3 from = vector(element.getAsJsonArray("from"));
|
||||||
|
Vec3 to = vector(element.getAsJsonArray("to"));
|
||||||
|
Transform tilt = tiltOf(element);
|
||||||
|
boolean shaded = !element.has("shade") || element.get("shade").getAsBoolean();
|
||||||
|
CubeFace shadeSide = element.has("shade_direction_override")
|
||||||
|
? CubeFace.named(element.get("shade_direction_override").getAsString()) : null;
|
||||||
|
for (Map.Entry<String, JsonElement> entry : elementFaces.entrySet()) {
|
||||||
|
CubeFace side = CubeFace.named(entry.getKey());
|
||||||
|
JsonObject faceJson = entry.getValue().getAsJsonObject();
|
||||||
|
if (side == null || !faceJson.has("texture")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String textureName = BlockTextures.dereference(faceJson.get("texture").getAsString(), model.textures());
|
||||||
|
if (textureName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<BlockTexture> texture = textures.faceTexture(state, textureName, BlockTextures.tintIndexOf(faceJson));
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FaceUv uv = FaceUv.of(faceJson, side, from, to);
|
||||||
|
if (rotation.locksTextures()) {
|
||||||
|
uv = uv.lockedTo(side, rotation);
|
||||||
|
}
|
||||||
|
Face face = boxFace(from, to, side, texture.get(), uv).transformed(tilt);
|
||||||
|
if (faceJson.has("cullface")) {
|
||||||
|
face = face.withCullFace(CubeFace.named(faceJson.get("cullface").getAsString()));
|
||||||
|
}
|
||||||
|
face = face.transformed(rotation.transform());
|
||||||
|
faces.add(face.withShade(shadeOf(face, shaded, shadeSide, rotation)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Face boxFace(Vec3 fromPixels, Vec3 toPixels, CubeFace side, BlockTexture texture, FaceUv uv) {
|
||||||
|
Vec3[] corners = BoxCorners.of(fromPixels.times(1D / MODEL_UNITS), toPixels.times(1D / MODEL_UNITS), side);
|
||||||
|
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
|
||||||
|
texture, uv.mapping(), side.shade(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double shadeOf(Face face, boolean shaded, CubeFace shadeSide, BlockRotation rotation) {
|
||||||
|
if (!shaded) {
|
||||||
|
return CubeFace.UP.shade();
|
||||||
|
}
|
||||||
|
if (shadeSide != null) {
|
||||||
|
return CubeFace.nearest(rotation.transform().direction(shadeSide.direction())).shade();
|
||||||
|
}
|
||||||
|
return CubeFace.nearest(face.normal()).shade();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A box can be tilted around a point, on one axis or, since 1.21.?, on all three; rescaling stretches a tilted
|
||||||
|
// plant back to the full block width.
|
||||||
|
private static Transform tiltOf(JsonObject element) {
|
||||||
|
if (!element.has("rotation")) {
|
||||||
|
return Transform.IDENTITY;
|
||||||
|
}
|
||||||
|
JsonObject rotation = element.getAsJsonObject("rotation");
|
||||||
|
Vec3 origin = rotation.has("origin") ? vector(rotation.getAsJsonArray("origin")).times(1D / MODEL_UNITS) : new Vec3(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER);
|
||||||
|
Transform around = Transform.IDENTITY.translate(origin.x(), origin.y(), origin.z());
|
||||||
|
Transform back = Transform.IDENTITY.translate(-origin.x(), -origin.y(), -origin.z());
|
||||||
|
if (!rotation.has("axis")) {
|
||||||
|
return around
|
||||||
|
.rotateX(numberOr(rotation, "x", 0D))
|
||||||
|
.rotateY(numberOr(rotation, "y", 0D))
|
||||||
|
.rotateZ(numberOr(rotation, "z", 0D))
|
||||||
|
.then(back);
|
||||||
|
}
|
||||||
|
String axis = rotation.get("axis").getAsString();
|
||||||
|
double angle = numberOr(rotation, "angle", 0D);
|
||||||
|
boolean rescale = rotation.has("rescale") && rotation.get("rescale").getAsBoolean();
|
||||||
|
double stretch = rescale ? 1D / Math.cos(Math.toRadians(angle)) : 1D;
|
||||||
|
Transform stretched = around.scale(axis.equals("x") ? 1D : stretch, axis.equals("y") ? 1D : stretch, axis.equals("z") ? 1D : stretch);
|
||||||
|
Transform tilted = switch (axis) {
|
||||||
|
case "x" -> stretched.rotateX(angle);
|
||||||
|
case "y" -> stretched.rotateY(angle);
|
||||||
|
default -> stretched.rotateZ(angle);
|
||||||
|
};
|
||||||
|
return tilted.then(back);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double numberOr(JsonObject object, String key, double fallback) {
|
||||||
|
return object.has(key) ? object.get(key).getAsDouble() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vec3 vector(JsonArray values) {
|
||||||
|
return new Vec3(values.get(0).getAsDouble(), values.get(1).getAsDouble(), values.get(2).getAsDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a blockstate turns its model: around the x axis first, then the y axis, both in quarter turns against the
|
||||||
|
* right-hand rule. With uvlock the textures stay put while the shape turns.
|
||||||
|
*/
|
||||||
|
private record BlockRotation(int x, int y, boolean uvlock) {
|
||||||
|
static BlockRotation of(JsonObject placement) {
|
||||||
|
int x = placement.has("x") ? Math.floorMod(placement.get("x").getAsInt(), 360) : 0;
|
||||||
|
int y = placement.has("y") ? Math.floorMod(placement.get("y").getAsInt(), 360) : 0;
|
||||||
|
return new BlockRotation(x, y, placement.has("uvlock") && placement.get("uvlock").getAsBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean locksTextures() {
|
||||||
|
return uvlock && (x != 0 || y != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform transform() {
|
||||||
|
return Transform.IDENTITY
|
||||||
|
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
|
||||||
|
.then(turn())
|
||||||
|
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform turn() {
|
||||||
|
return Transform.IDENTITY.rotateY(-y).rotateX(-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform inverseTurn() {
|
||||||
|
return Transform.IDENTITY.rotateX(x).rotateY(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A face's corner UVs in model pixels and the quarter turn of the texture on it.
|
||||||
|
*/
|
||||||
|
private record FaceUv(double u1, double v1, double u2, double v2, int rotation) {
|
||||||
|
static FaceUv of(JsonObject face, CubeFace side, Vec3 from, Vec3 to) {
|
||||||
|
int rotation = face.has("rotation") ? Math.floorMod(face.get("rotation").getAsInt(), 360) : 0;
|
||||||
|
if (!face.has("uv")) {
|
||||||
|
return byDefault(side, from, to).withRotation(rotation);
|
||||||
|
}
|
||||||
|
JsonArray uv = face.getAsJsonArray("uv");
|
||||||
|
return new FaceUv(uv.get(0).getAsDouble(), uv.get(1).getAsDouble(), uv.get(2).getAsDouble(), uv.get(3).getAsDouble(), rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The part of the texture the face would cover if the texture were laid over the whole block side.
|
||||||
|
static FaceUv byDefault(CubeFace side, Vec3 from, Vec3 to) {
|
||||||
|
double full = MODEL_UNITS;
|
||||||
|
return switch (side) {
|
||||||
|
case DOWN -> new FaceUv(from.x(), full - to.z(), to.x(), full - from.z(), 0);
|
||||||
|
case UP -> new FaceUv(from.x(), from.z(), to.x(), to.z(), 0);
|
||||||
|
case NORTH -> new FaceUv(full - to.x(), full - to.y(), full - from.x(), full - from.y(), 0);
|
||||||
|
case SOUTH -> new FaceUv(from.x(), full - to.y(), to.x(), full - from.y(), 0);
|
||||||
|
case WEST -> new FaceUv(from.z(), full - to.y(), to.z(), full - from.y(), 0);
|
||||||
|
case EAST -> new FaceUv(full - to.z(), full - to.y(), full - from.z(), full - from.y(), 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceUv withRotation(int changedRotation) {
|
||||||
|
return new FaceUv(u1, v1, u2, v2, changedRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each corner takes the UV a quarter turn further along for every quarter turn of the texture.
|
||||||
|
TextureMapping mapping() {
|
||||||
|
double[] origin = cornerUv(0);
|
||||||
|
double[] alongU = cornerUv(3);
|
||||||
|
double[] alongV = cornerUv(1);
|
||||||
|
return new TextureMapping(origin[0] / MODEL_UNITS, origin[1] / MODEL_UNITS,
|
||||||
|
(alongU[0] - origin[0]) / MODEL_UNITS, (alongU[1] - origin[1]) / MODEL_UNITS,
|
||||||
|
(alongV[0] - origin[0]) / MODEL_UNITS, (alongV[1] - origin[1]) / MODEL_UNITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] cornerUv(int corner) {
|
||||||
|
return switch ((corner + rotation / QUARTER_TURN) % 4) {
|
||||||
|
case 0 -> new double[]{u1, v1};
|
||||||
|
case 1 -> new double[]{u1, v2};
|
||||||
|
case 2 -> new double[]{u2, v2};
|
||||||
|
default -> new double[]{u2, v1};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minecraft's uvlock: the UVs are carried through the reverse of the blockstate's turn, so the texture keeps
|
||||||
|
* its place in the world while the face moves to another side.
|
||||||
|
*/
|
||||||
|
FaceUv lockedTo(CubeFace side, BlockRotation blockRotation) {
|
||||||
|
CubeFace turnedSide = CubeFace.nearest(blockRotation.turn().direction(side.direction()));
|
||||||
|
Transform uvTurn = Transform.IDENTITY
|
||||||
|
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
|
||||||
|
.then(faceFromWorld(side))
|
||||||
|
.then(blockRotation.inverseTurn())
|
||||||
|
.then(worldFromFace(turnedSide))
|
||||||
|
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
|
||||||
|
Vec3 first = uvTurn.point(new Vec3(u1 / MODEL_UNITS, v1 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
|
||||||
|
Vec3 second = uvTurn.point(new Vec3(u2 / MODEL_UNITS, v2 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
|
||||||
|
boolean keepsUOrder = Math.signum(u2 - u1) == Math.signum(second.x() - first.x());
|
||||||
|
boolean keepsVOrder = Math.signum(v2 - v1) == Math.signum(second.y() - first.y());
|
||||||
|
Vec3 textureRight = uvTurn.direction(new Vec3(Math.cos(Math.toRadians(rotation)), Math.sin(Math.toRadians(rotation)), 0D));
|
||||||
|
long quarterTurns = Math.round(Math.toDegrees(Math.atan2(textureRight.y(), textureRight.x())) / QUARTER_TURN);
|
||||||
|
return new FaceUv(
|
||||||
|
keepsUOrder ? first.x() : second.x(), keepsVOrder ? first.y() : second.y(),
|
||||||
|
keepsUOrder ? second.x() : first.x(), keepsVOrder ? second.y() : first.y(),
|
||||||
|
(int) Math.floorMod(-quarterTurns * QUARTER_TURN, 360L));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft lays every side's texture out as if it were the south side, turned into place.
|
||||||
|
private static Transform worldFromFace(CubeFace side) {
|
||||||
|
return switch (side) {
|
||||||
|
case SOUTH -> Transform.IDENTITY;
|
||||||
|
case EAST -> Transform.IDENTITY.rotateY(90D);
|
||||||
|
case WEST -> Transform.IDENTITY.rotateY(-90D);
|
||||||
|
case NORTH -> Transform.IDENTITY.rotateY(180D);
|
||||||
|
case UP -> Transform.IDENTITY.rotateX(-90D);
|
||||||
|
case DOWN -> Transform.IDENTITY.rotateX(90D);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transform faceFromWorld(CubeFace side) {
|
||||||
|
return switch (side) {
|
||||||
|
case SOUTH -> Transform.IDENTITY;
|
||||||
|
case EAST -> Transform.IDENTITY.rotateY(-90D);
|
||||||
|
case WEST -> Transform.IDENTITY.rotateY(90D);
|
||||||
|
case NORTH -> Transform.IDENTITY.rotateY(180D);
|
||||||
|
case UP -> Transform.IDENTITY.rotateX(90D);
|
||||||
|
case DOWN -> Transform.IDENTITY.rotateX(-90D);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ import java.awt.Graphics2D;
|
|||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -43,7 +42,7 @@ final class BlockTextures {
|
|||||||
private static final String DEFAULT_NAMESPACE = "minecraft";
|
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||||
private static final int MAX_MODEL_PARENTS = 16;
|
private static final int MAX_MODEL_PARENTS = 16;
|
||||||
private static final int MAX_TEXTURE_REFERENCES = 16;
|
private static final int MAX_TEXTURE_REFERENCES = 16;
|
||||||
private static final int NO_TINT_INDEX = -1;
|
static final int NO_TINT_INDEX = -1;
|
||||||
private static final Object SHARED_LOCK = new Object();
|
private static final Object SHARED_LOCK = new Object();
|
||||||
private static CompletableFuture<BlockTextures> shared;
|
private static CompletableFuture<BlockTextures> shared;
|
||||||
|
|
||||||
@@ -51,6 +50,8 @@ final class BlockTextures {
|
|||||||
private final MinecraftBiomeColors biomeColors;
|
private final MinecraftBiomeColors biomeColors;
|
||||||
private final Map<String, Optional<ResolvedTexture>> resolvedTextures = new ConcurrentHashMap<>();
|
private final Map<String, Optional<ResolvedTexture>> resolvedTextures = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Optional<BlockModel>> resolvedModels = new ConcurrentHashMap<>();
|
private final Map<String, Optional<BlockModel>> resolvedModels = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Optional<BlockTexture>> faceTextures = new ConcurrentHashMap<>();
|
||||||
|
private final BlockModelBuilder modelBuilder = new BlockModelBuilder(this);
|
||||||
|
|
||||||
private BlockTextures(ZipFile clientJar, MinecraftBiomeColors biomeColors) {
|
private BlockTextures(ZipFile clientJar, MinecraftBiomeColors biomeColors) {
|
||||||
this.clientJar = clientJar;
|
this.clientJar = clientJar;
|
||||||
@@ -109,7 +110,7 @@ final class BlockTextures {
|
|||||||
* What the block looks like from every side, for drawing it in 3D.
|
* What the block looks like from every side, for drawing it in 3D.
|
||||||
*/
|
*/
|
||||||
Optional<BlockModel> model(BlockData block) {
|
Optional<BlockModel> model(BlockData block) {
|
||||||
return resolvedModels.computeIfAbsent(block.getAsString(false), this::resolveModel);
|
return resolvedModels.computeIfAbsent(block.getAsString(false), modelBuilder::build);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlockData withHalf(BlockData block, Bisected.Half half) {
|
private static BlockData withHalf(BlockData block, Bisected.Half half) {
|
||||||
@@ -140,32 +141,6 @@ final class BlockTextures {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<BlockModel> resolveModel(String blockState) {
|
|
||||||
try {
|
|
||||||
BlockStateId state = BlockStateId.parse(blockState);
|
|
||||||
JsonObject variant = variantOf(state);
|
|
||||||
if (variant == null || !variant.has("model")) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
ModelDefinition model = readModel(variant.get("model").getAsString());
|
|
||||||
if (model.isSprite()) {
|
|
||||||
return readTexture(state, model.spriteFace()).map(BlockModel::cross);
|
|
||||||
}
|
|
||||||
int rotationX = rotationOf(variant, "x");
|
|
||||||
int rotationY = rotationOf(variant, "y");
|
|
||||||
Map<CubeFace, BlockTexture> faces = new EnumMap<>(CubeFace.class);
|
|
||||||
for (CubeFace face : CubeFace.values()) {
|
|
||||||
ModelFace modelFace = model.sideFace(face.beforeRotation(rotationX, rotationY).modelName());
|
|
||||||
if (modelFace != null) {
|
|
||||||
readTexture(state, modelFace).ifPresent(texture -> faces.put(face, texture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return faces.isEmpty() ? Optional.empty() : Optional.of(BlockModel.box(model.box().rotated(rotationX, rotationY), faces));
|
|
||||||
} catch (IOException | RuntimeException error) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonObject variantOf(BlockStateId state) throws IOException {
|
private JsonObject variantOf(BlockStateId state) throws IOException {
|
||||||
JsonObject definition = readJson(assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
|
JsonObject definition = readJson(assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
|
||||||
return definition == null ? null : pickVariant(definition, state.properties());
|
return definition == null ? null : pickVariant(definition, state.properties());
|
||||||
@@ -175,6 +150,35 @@ final class BlockTextures {
|
|||||||
return variant.has(axis) ? Math.floorMod(variant.get(axis).getAsInt(), 360) : 0;
|
return variant.has(axis) ? Math.floorMod(variant.get(axis).getAsInt(), 360) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One model face's texture with the block's tint, shared by every block state that uses it.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> faceTexture(BlockStateId state, String texture, int tintIndex) {
|
||||||
|
int fixedTint = MinecraftBlockTints.fixedTint(state.name(), state.properties(), tintIndex);
|
||||||
|
BiomeTint biomeTint = MinecraftBlockTints.biomeTint(state.name(), tintIndex).orElse(null);
|
||||||
|
return faceTextures.computeIfAbsent(texture + "|" + fixedTint + "|" + biomeTint, key -> {
|
||||||
|
try {
|
||||||
|
return readTexture(state, new ModelFace(List.of(texture), tintIndex, false));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A texture drawn on a block entity, like a chest, whole and tinted with a fixed colour.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> entityTexture(String texture, int tint) {
|
||||||
|
return faceTextures.computeIfAbsent(texture + "|entity|" + tint, key -> {
|
||||||
|
try {
|
||||||
|
BufferedImage image = readImage(assetPath(texture, "textures", ".png"));
|
||||||
|
return image == null ? Optional.empty() : Optional.of(BlockTexture.fromWholeImage(image, tint));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<BlockTexture> readTexture(BlockStateId state, ModelFace face) throws IOException {
|
private Optional<BlockTexture> readTexture(BlockStateId state, ModelFace face) throws IOException {
|
||||||
BufferedImage image = readLayeredImage(face.textures());
|
BufferedImage image = readLayeredImage(face.textures());
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
@@ -191,16 +195,18 @@ final class BlockTextures {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A block rotated on its x axis, like a sideways log, shows a side face on top.
|
// A block rotated on its x axis, like a sideways log, shows a side face on top. Blockstates turn the model
|
||||||
|
// against the right-hand rule, so 90 degrees brings the south side up.
|
||||||
private static String faceSeenFromAbove(JsonObject variant) {
|
private static String faceSeenFromAbove(JsonObject variant) {
|
||||||
return switch (rotationOf(variant, "x")) {
|
return switch (rotationOf(variant, "x")) {
|
||||||
case 90, 270 -> "north";
|
case 90 -> "south";
|
||||||
|
case 270 -> "north";
|
||||||
case 180 -> "down";
|
case 180 -> "down";
|
||||||
default -> "up";
|
default -> "up";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModelDefinition readModel(String modelReference) throws IOException {
|
ModelDefinition readModel(String modelReference) throws IOException {
|
||||||
Map<String, String> textures = new HashMap<>();
|
Map<String, String> textures = new HashMap<>();
|
||||||
JsonArray elements = null;
|
JsonArray elements = null;
|
||||||
String current = modelReference;
|
String current = modelReference;
|
||||||
@@ -211,7 +217,7 @@ final class BlockTextures {
|
|||||||
}
|
}
|
||||||
if (model.has("textures")) {
|
if (model.has("textures")) {
|
||||||
for (Map.Entry<String, JsonElement> texture : model.getAsJsonObject("textures").entrySet()) {
|
for (Map.Entry<String, JsonElement> texture : model.getAsJsonObject("textures").entrySet()) {
|
||||||
textures.putIfAbsent(texture.getKey(), texture.getValue().getAsString());
|
textures.putIfAbsent(texture.getKey(), spriteName(texture.getValue()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (elements == null && model.has("elements")) {
|
if (elements == null && model.has("elements")) {
|
||||||
@@ -222,6 +228,11 @@ final class BlockTextures {
|
|||||||
return new ModelDefinition(textures, elements);
|
return new ModelDefinition(textures, elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since 1.21.? a texture can also be an object naming its sprite, like glass's {"sprite": ..., "force_translucent": true}.
|
||||||
|
private static String spriteName(JsonElement texture) {
|
||||||
|
return texture.isJsonObject() ? texture.getAsJsonObject().get("sprite").getAsString() : texture.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
// Some plants draw a second sprite over the first, like the glowing fireflies on a firefly bush.
|
// Some plants draw a second sprite over the first, like the glowing fireflies on a firefly bush.
|
||||||
private static List<String> spriteLayers(JsonArray elements, Map<String, String> textures) {
|
private static List<String> spriteLayers(JsonArray elements, Map<String, String> textures) {
|
||||||
Set<String> layers = new LinkedHashSet<>();
|
Set<String> layers = new LinkedHashSet<>();
|
||||||
@@ -256,7 +267,7 @@ final class BlockTextures {
|
|||||||
return NO_TINT_INDEX;
|
return NO_TINT_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int tintIndexOf(JsonObject face) {
|
static int tintIndexOf(JsonObject face) {
|
||||||
return face.has("tintindex") ? face.get("tintindex").getAsInt() : NO_TINT_INDEX;
|
return face.has("tintindex") ? face.get("tintindex").getAsInt() : NO_TINT_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +294,7 @@ final class BlockTextures {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String dereference(String reference, Map<String, String> textures) {
|
static String dereference(String reference, Map<String, String> textures) {
|
||||||
String resolved = reference;
|
String resolved = reference;
|
||||||
for (int hop = 0; resolved != null && resolved.startsWith("#") && hop < MAX_TEXTURE_REFERENCES; hop++) {
|
for (int hop = 0; resolved != null && resolved.startsWith("#") && hop < MAX_TEXTURE_REFERENCES; hop++) {
|
||||||
resolved = textures.get(resolved.substring(1));
|
resolved = textures.get(resolved.substring(1));
|
||||||
@@ -311,7 +322,7 @@ final class BlockTextures {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean variantMatches(String variantKey, Map<String, String> properties) {
|
static boolean variantMatches(String variantKey, Map<String, String> properties) {
|
||||||
if (variantKey.isEmpty()) {
|
if (variantKey.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -324,7 +335,7 @@ final class BlockTextures {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean conditionMatches(JsonObject condition, Map<String, String> properties) {
|
static boolean conditionMatches(JsonObject condition, Map<String, String> properties) {
|
||||||
if (condition.has("OR")) {
|
if (condition.has("OR")) {
|
||||||
for (JsonElement option : condition.getAsJsonArray("OR")) {
|
for (JsonElement option : condition.getAsJsonArray("OR")) {
|
||||||
if (conditionMatches(option.getAsJsonObject(), properties)) {
|
if (conditionMatches(option.getAsJsonObject(), properties)) {
|
||||||
@@ -354,7 +365,7 @@ final class BlockTextures {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject firstModel(JsonElement models) {
|
static JsonObject firstModel(JsonElement models) {
|
||||||
if (models.isJsonArray()) {
|
if (models.isJsonArray()) {
|
||||||
JsonArray choices = models.getAsJsonArray();
|
JsonArray choices = models.getAsJsonArray();
|
||||||
return choices.isEmpty() ? null : choices.get(0).getAsJsonObject();
|
return choices.isEmpty() ? null : choices.get(0).getAsJsonObject();
|
||||||
@@ -362,14 +373,14 @@ final class BlockTextures {
|
|||||||
return models.getAsJsonObject();
|
return models.getAsJsonObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String assetPath(String resource, String folder, String extension) {
|
static String assetPath(String resource, String folder, String extension) {
|
||||||
int separator = resource.indexOf(':');
|
int separator = resource.indexOf(':');
|
||||||
String namespace = separator < 0 ? DEFAULT_NAMESPACE : resource.substring(0, separator);
|
String namespace = separator < 0 ? DEFAULT_NAMESPACE : resource.substring(0, separator);
|
||||||
String path = separator < 0 ? resource : resource.substring(separator + 1);
|
String path = separator < 0 ? resource : resource.substring(separator + 1);
|
||||||
return "assets/" + namespace + "/" + folder + "/" + path + extension;
|
return "assets/" + namespace + "/" + folder + "/" + path + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonObject readJson(String path) throws IOException {
|
JsonObject readJson(String path) throws IOException {
|
||||||
return ClientJarFiles.readJson(clientJar, path);
|
return ClientJarFiles.readJson(clientJar, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +418,7 @@ final class BlockTextures {
|
|||||||
/**
|
/**
|
||||||
* A block model with its parents' textures merged in. Models drawn in code, like chests, have no elements.
|
* A block model with its parents' textures merged in. Models drawn in code, like chests, have no elements.
|
||||||
*/
|
*/
|
||||||
private record ModelDefinition(Map<String, String> textures, JsonArray elements) {
|
record ModelDefinition(Map<String, String> textures, JsonArray elements) {
|
||||||
ModelFace visibleFace(String faceName) {
|
ModelFace visibleFace(String faceName) {
|
||||||
ModelFace face = sideFace(faceName);
|
ModelFace face = sideFace(faceName);
|
||||||
if (face != null) {
|
if (face != null) {
|
||||||
@@ -430,14 +441,6 @@ final class BlockTextures {
|
|||||||
return texture == null ? null : new ModelFace(List.of(texture), tintIndexOf(face), false);
|
return texture == null ? null : new ModelFace(List.of(texture), tintIndexOf(face), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plants have only upright planes, so nothing of them faces up or down.
|
|
||||||
boolean isSprite() {
|
|
||||||
return elements != null
|
|
||||||
&& topmostFace(elements, CubeFace.UP.modelName()) == null
|
|
||||||
&& topmostFace(elements, CubeFace.DOWN.modelName()) == null
|
|
||||||
&& !spriteLayers(elements, textures).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelFace spriteFace() {
|
ModelFace spriteFace() {
|
||||||
return new ModelFace(spriteLayers(elements, textures), firstTintIndex(elements), true);
|
return new ModelFace(spriteLayers(elements, textures), firstTintIndex(elements), true);
|
||||||
}
|
}
|
||||||
@@ -447,25 +450,13 @@ final class BlockTextures {
|
|||||||
return particle == null ? null : new ModelFace(List.of(particle), NO_TINT_INDEX, false);
|
return particle == null ? null : new ModelFace(List.of(particle), NO_TINT_INDEX, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One box, like a slab or a carpet, keeps its shape; several, like stairs, become a full block.
|
|
||||||
BlockModel.Box box() {
|
|
||||||
if (elements == null || elements.size() != 1) {
|
|
||||||
return BlockModel.Box.FULL;
|
|
||||||
}
|
|
||||||
JsonObject element = elements.get(0).getAsJsonObject();
|
|
||||||
return BlockModel.Box.ofModelElement(coordinates(element.getAsJsonArray("from")), coordinates(element.getAsJsonArray("to")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double[] coordinates(JsonArray values) {
|
|
||||||
return new double[]{values.get(0).getAsDouble(), values.get(1).getAsDouble(), values.get(2).getAsDouble()};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ResolvedTexture(BlockTexture texture, boolean isSideOnSprite) {
|
private record ResolvedTexture(BlockTexture texture, boolean isSideOnSprite) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record BlockStateId(String namespace, String name, Map<String, String> properties) {
|
record BlockStateId(String namespace, String name, Map<String, String> properties) {
|
||||||
private static BlockStateId parse(String blockState) {
|
static BlockStateId parse(String blockState) {
|
||||||
int propertiesStart = blockState.indexOf('[');
|
int propertiesStart = blockState.indexOf('[');
|
||||||
String id = propertiesStart < 0 ? blockState : blockState.substring(0, propertiesStart);
|
String id = propertiesStart < 0 ? blockState : blockState.substring(0, propertiesStart);
|
||||||
int separator = id.indexOf(':');
|
int separator = id.indexOf(':');
|
||||||
@@ -487,26 +478,45 @@ final class BlockTextures {
|
|||||||
|
|
||||||
// Biome tinted textures keep their grey pixels, because the biome colour changes from column to column.
|
// Biome tinted textures keep their grey pixels, because the biome colour changes from column to column.
|
||||||
// See-through pixels get a darker shade of the average colour, and the opaque mask remembers which they were.
|
// See-through pixels get a darker shade of the average colour, and the opaque mask remembers which they were.
|
||||||
record BlockTexture(int size, int[] pixels, boolean[] opaque, int averageColor, BiomeTint biomeTint) {
|
// Textures with partly see-through pixels, like stained glass, ice and water, keep each pixel's alpha too.
|
||||||
private static final int OPAQUE_ALPHA = 128;
|
record BlockTexture(int width, int height, int[] pixels, boolean[] opaque, int[] alphas, int averageColor,
|
||||||
|
BiomeTint biomeTint) {
|
||||||
private static final double GAP_SHADE = 0.55D;
|
private static final double GAP_SHADE = 0.55D;
|
||||||
|
private static final int FULL_ALPHA = 255;
|
||||||
|
|
||||||
// Animated textures stack their frames vertically, so only the first square frame is used.
|
// Animated textures stack their frames vertically, so only the first square frame is used.
|
||||||
static BlockTexture fromImage(BufferedImage image, int fixedTint, BiomeTint biomeTint) {
|
static BlockTexture fromImage(BufferedImage image, int fixedTint, BiomeTint biomeTint) {
|
||||||
int size = image.getWidth();
|
return fromPixels(image, image.getWidth(), Math.min(image.getWidth(), image.getHeight()), fixedTint, biomeTint);
|
||||||
int[] pixels = new int[size * size];
|
}
|
||||||
|
|
||||||
|
// Entity textures, like a chest's, are laid out for a model and can be wider than they are tall.
|
||||||
|
static BlockTexture fromWholeImage(BufferedImage image, int fixedTint) {
|
||||||
|
return fromPixels(image, image.getWidth(), image.getHeight(), fixedTint, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockTexture solid(int color) {
|
||||||
|
return new BlockTexture(1, 1, new int[]{color}, new boolean[]{true}, null, color, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockTexture fromPixels(BufferedImage image, int width, int height, int fixedTint, BiomeTint biomeTint) {
|
||||||
|
int[] pixels = new int[width * height];
|
||||||
boolean[] opaque = new boolean[pixels.length];
|
boolean[] opaque = new boolean[pixels.length];
|
||||||
|
int[] alphas = new int[pixels.length];
|
||||||
|
boolean translucent = false;
|
||||||
long red = 0;
|
long red = 0;
|
||||||
long green = 0;
|
long green = 0;
|
||||||
long blue = 0;
|
long blue = 0;
|
||||||
int opaqueCount = 0;
|
int opaqueCount = 0;
|
||||||
for (int z = 0; z < size; z++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < size; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
int argb = image.getRGB(x, Math.min(z, image.getHeight() - 1));
|
int argb = image.getRGB(x, y);
|
||||||
int index = z * size + x;
|
int index = y * width + x;
|
||||||
if ((argb >>> 24) < OPAQUE_ALPHA) {
|
int alpha = argb >>> 24;
|
||||||
|
alphas[index] = alpha;
|
||||||
|
if (alpha == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
translucent |= alpha < FULL_ALPHA;
|
||||||
int color = multiply(argb & 0xFFFFFF, fixedTint);
|
int color = multiply(argb & 0xFFFFFF, fixedTint);
|
||||||
pixels[index] = color;
|
pixels[index] = color;
|
||||||
opaque[index] = true;
|
opaque[index] = true;
|
||||||
@@ -526,7 +536,23 @@ final class BlockTextures {
|
|||||||
pixels[index] = gapColor;
|
pixels[index] = gapColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new BlockTexture(size, pixels, opaque, average, biomeTint);
|
return new BlockTexture(width, height, pixels, opaque, translucent ? alphas : null, average, biomeTint);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTranslucent() {
|
||||||
|
return alphas != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFullyOpaque() {
|
||||||
|
if (isTranslucent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (boolean pixelOpaque : opaque) {
|
||||||
|
if (!pixelOpaque) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockTexture flattened() {
|
BlockTexture flattened() {
|
||||||
@@ -534,7 +560,7 @@ final class BlockTextures {
|
|||||||
Arrays.fill(flat, averageColor);
|
Arrays.fill(flat, averageColor);
|
||||||
boolean[] allOpaque = new boolean[pixels.length];
|
boolean[] allOpaque = new boolean[pixels.length];
|
||||||
Arrays.fill(allOpaque, true);
|
Arrays.fill(allOpaque, true);
|
||||||
return new BlockTexture(size, flat, allOpaque, averageColor, biomeTint);
|
return new BlockTexture(width, height, flat, allOpaque, null, averageColor, biomeTint);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockTexture tinted(int tint) {
|
BlockTexture tinted(int tint) {
|
||||||
@@ -542,7 +568,7 @@ final class BlockTextures {
|
|||||||
for (int index = 0; index < pixels.length; index++) {
|
for (int index = 0; index < pixels.length; index++) {
|
||||||
tintedPixels[index] = multiply(pixels[index], tint);
|
tintedPixels[index] = multiply(pixels[index], tint);
|
||||||
}
|
}
|
||||||
return new BlockTexture(size, tintedPixels, opaque, multiply(averageColor, tint), null);
|
return new BlockTexture(width, height, tintedPixels, opaque, alphas, multiply(averageColor, tint), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
int texelAt(double worldX, double worldZ) {
|
int texelAt(double worldX, double worldZ) {
|
||||||
@@ -554,14 +580,14 @@ final class BlockTextures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int texelIndex(double worldX, double worldZ) {
|
private int texelIndex(double worldX, double worldZ) {
|
||||||
int x = Math.min(size - 1, (int) ((worldX - Math.floor(worldX)) * size));
|
int x = Math.min(width - 1, (int) ((worldX - Math.floor(worldX)) * width));
|
||||||
int z = Math.min(size - 1, (int) ((worldZ - Math.floor(worldZ)) * size));
|
int z = Math.min(height - 1, (int) ((worldZ - Math.floor(worldZ)) * height));
|
||||||
return z * size + x;
|
return z * width + x;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedImage toImage() {
|
BufferedImage toImage() {
|
||||||
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
image.setRGB(0, 0, size, size, pixels, 0, size);
|
image.setRGB(0, 0, width, height, pixels, 0, width);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import com.google.gson.JsonObject;
|
|||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.color.ColorSpace;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.Raster;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -51,7 +53,30 @@ final class ClientJarFiles {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try (InputStream input = clientJar.getInputStream(entry)) {
|
try (InputStream input = clientJar.getInputStream(entry)) {
|
||||||
return ImageIO.read(input);
|
BufferedImage image = ImageIO.read(input);
|
||||||
|
return image != null && isGrey(image) ? withGreyKept(image) : image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isGrey(BufferedImage image) {
|
||||||
|
return image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Java treats grey PNGs, like stone's, as linear light and brightens them when converting to RGB; Minecraft reads
|
||||||
|
// the stored values as they are.
|
||||||
|
private static BufferedImage withGreyKept(BufferedImage image) {
|
||||||
|
Raster raster = image.getRaster();
|
||||||
|
boolean hasAlpha = raster.getNumBands() > 1;
|
||||||
|
int greyMax = (1 << raster.getSampleModel().getSampleSize(0)) - 1;
|
||||||
|
int alphaMax = hasAlpha ? (1 << raster.getSampleModel().getSampleSize(1)) - 1 : 1;
|
||||||
|
BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
int grey = raster.getSample(x, y, 0) * 255 / greyMax;
|
||||||
|
int alpha = hasAlpha ? raster.getSample(x, y, 1) * 255 / alphaMax : 255;
|
||||||
|
converted.setRGB(x, y, alpha << 24 | grey << 16 | grey << 8 | grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,18 +16,19 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The six sides of a block, named as in Minecraft's model files, with the fixed light each side gets in the object viewer.
|
* The six sides of a block, named as in Minecraft's model files, with the light Minecraft gives each side.
|
||||||
*/
|
*/
|
||||||
enum CubeFace {
|
enum CubeFace {
|
||||||
UP("up", 0, 1, 0, 1.00D),
|
UP("up", 0, 1, 0, 1.0D),
|
||||||
DOWN("down", 0, -1, 0, 0.48D),
|
DOWN("down", 0, -1, 0, 0.5D),
|
||||||
NORTH("north", 0, 0, -1, 0.62D),
|
NORTH("north", 0, 0, -1, 0.8D),
|
||||||
SOUTH("south", 0, 0, 1, 0.88D),
|
SOUTH("south", 0, 0, 1, 0.8D),
|
||||||
EAST("east", 1, 0, 0, 0.80D),
|
EAST("east", 1, 0, 0, 0.6D),
|
||||||
WEST("west", -1, 0, 0, 0.69D);
|
WEST("west", -1, 0, 0, 0.6D);
|
||||||
|
|
||||||
private final String modelName;
|
private final String modelName;
|
||||||
private final int stepX;
|
private final int stepX;
|
||||||
@@ -43,6 +44,31 @@ enum CubeFace {
|
|||||||
this.shade = shade;
|
this.shade = shade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CubeFace named(String modelName) {
|
||||||
|
for (CubeFace face : values()) {
|
||||||
|
if (face.modelName.equals(modelName)) {
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The side a direction points at most, like Minecraft picks for a tilted face.
|
||||||
|
*/
|
||||||
|
static CubeFace nearest(Vec3 direction) {
|
||||||
|
CubeFace nearest = UP;
|
||||||
|
double best = Double.NEGATIVE_INFINITY;
|
||||||
|
for (CubeFace face : values()) {
|
||||||
|
double alignment = face.stepX * direction.x() + face.stepY * direction.y() + face.stepZ * direction.z();
|
||||||
|
if (alignment > best) {
|
||||||
|
best = alignment;
|
||||||
|
nearest = face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
String modelName() {
|
String modelName() {
|
||||||
return modelName;
|
return modelName;
|
||||||
}
|
}
|
||||||
@@ -63,51 +89,26 @@ enum CubeFace {
|
|||||||
return shade;
|
return shade;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
Vec3 direction() {
|
||||||
* The side of the model that ends up facing this way once a blockstate turns the model, which Minecraft does around
|
return new Vec3(stepX, stepY, stepZ);
|
||||||
* the x axis first and the y axis second, each in steps of 90 degrees.
|
|
||||||
*/
|
|
||||||
CubeFace beforeRotation(int rotationX, int rotationY) {
|
|
||||||
int[] direction = {stepX, stepY, stepZ};
|
|
||||||
direction = turnAroundY(direction, -rotationY);
|
|
||||||
direction = turnAroundX(direction, -rotationX);
|
|
||||||
return pointing(direction[0], direction[1], direction[2]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int[] turnAroundX(int[] vector, int degrees) {
|
CubeFace opposite() {
|
||||||
int cos = cosine(degrees);
|
return switch (this) {
|
||||||
int sin = sine(degrees);
|
case UP -> DOWN;
|
||||||
return new int[]{vector[0], vector[1] * cos - vector[2] * sin, vector[1] * sin + vector[2] * cos};
|
case DOWN -> UP;
|
||||||
}
|
case NORTH -> SOUTH;
|
||||||
|
case SOUTH -> NORTH;
|
||||||
static int[] turnAroundY(int[] vector, int degrees) {
|
case EAST -> WEST;
|
||||||
int cos = cosine(degrees);
|
case WEST -> EAST;
|
||||||
int sin = sine(degrees);
|
|
||||||
return new int[]{vector[0] * cos - vector[2] * sin, vector[1], vector[0] * sin + vector[2] * cos};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int cosine(int degrees) {
|
|
||||||
return switch (Math.floorMod(degrees, 360)) {
|
|
||||||
case 90, 270 -> 0;
|
|
||||||
case 180 -> -1;
|
|
||||||
default -> 1;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int sine(int degrees) {
|
int axis() {
|
||||||
return switch (Math.floorMod(degrees, 360)) {
|
return stepX != 0 ? 0 : stepY != 0 ? 1 : 2;
|
||||||
case 90 -> 1;
|
|
||||||
case 270 -> -1;
|
|
||||||
default -> 0;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CubeFace pointing(int x, int y, int z) {
|
boolean pointsPositive() {
|
||||||
for (CubeFace face : values()) {
|
return stepX + stepY + stepZ > 0;
|
||||||
if (face.stepX == x && face.stepY == y && face.stepZ == z) {
|
|
||||||
return face;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Not a block side: " + x + ", " + y + ", " + z);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shapes for the blocks Minecraft draws in code instead of from a model file, like chests, heads, banners and
|
||||||
|
* fluids, which fill their block. Chests, heads, banners and the bell use their entity textures laid out the way Minecraft's model parts are;
|
||||||
|
* rarer ones are a simple box in their particle texture.
|
||||||
|
*/
|
||||||
|
final class EntityBlockShapes {
|
||||||
|
private static final Set<String> INVISIBLE = Set.of("air", "cave_air", "void_air", "barrier", "light",
|
||||||
|
"structure_void", "moving_piston", "pitcher_crop");
|
||||||
|
private static final int END_PORTAL_COLOR = 0x0E1219;
|
||||||
|
private static final double SKULL_WALL_OFFSET = 0.25D;
|
||||||
|
private static final double ROTATION_SEGMENT_DEGREES = 22.5D;
|
||||||
|
private static final double BANNER_SCALE = 2D / 3D;
|
||||||
|
private static final double MODEL_UNITS = 16D;
|
||||||
|
private static final Map<String, Integer> DYE_COLORS = Map.ofEntries(
|
||||||
|
Map.entry("white", 0xF9FFFE), Map.entry("orange", 0xF9801D), Map.entry("magenta", 0xC74EBD),
|
||||||
|
Map.entry("light_blue", 0x3AB3DA), Map.entry("yellow", 0xFED83D), Map.entry("lime", 0x80C71F),
|
||||||
|
Map.entry("pink", 0xF38BAA), Map.entry("gray", 0x474F52), Map.entry("light_gray", 0x9D9D97),
|
||||||
|
Map.entry("cyan", 0x169C9C), Map.entry("purple", 0x8932B8), Map.entry("blue", 0x3C44AA),
|
||||||
|
Map.entry("brown", 0x835432), Map.entry("green", 0x5E7C16), Map.entry("red", 0xB02E26),
|
||||||
|
Map.entry("black", 0x1D1D21));
|
||||||
|
|
||||||
|
private final BlockTextures textures;
|
||||||
|
|
||||||
|
EntityBlockShapes(BlockTextures textures) {
|
||||||
|
this.textures = textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The faces of a block whose model has no boxes of its own.
|
||||||
|
*/
|
||||||
|
List<Face> facesOf(BlockStateId state, ModelDefinition model) {
|
||||||
|
String name = state.name();
|
||||||
|
if (INVISIBLE.contains(name)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (name.endsWith("chest")) {
|
||||||
|
return chest(state);
|
||||||
|
}
|
||||||
|
if (name.endsWith("_skull") || name.endsWith("_head")) {
|
||||||
|
return head(state, model);
|
||||||
|
}
|
||||||
|
if (name.endsWith("banner")) {
|
||||||
|
return banner(state);
|
||||||
|
}
|
||||||
|
return switch (name) {
|
||||||
|
case "conduit" -> particleBox(state, model, new Vec3(5, 5, 5), new Vec3(11, 11, 11), false);
|
||||||
|
case "decorated_pot" -> decoratedPot(state, model);
|
||||||
|
case "end_portal" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 12, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
|
||||||
|
case "end_gateway" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 16, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
|
||||||
|
default -> name.endsWith("copper_golem_statue")
|
||||||
|
? particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 14, 12), false)
|
||||||
|
: particleBox(state, model, new Vec3(0, 0, 0), new Vec3(16, 16, 16), true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parts a block draws in code on top of its model, like the bell hanging in its frame.
|
||||||
|
*/
|
||||||
|
List<Face> facesAddedTo(BlockStateId state) {
|
||||||
|
if (!state.name().equals("bell")) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Optional<BlockTexture> texture = textures.entityTexture("entity/bell/bell_body", MinecraftBlockTints.NO_TINT);
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(5, 6, 5), new Vec3(6, 7, 6), Transform.IDENTITY));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 13, new Vec3(4, 4, 4), new Vec3(8, 2, 8), Transform.IDENTITY));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> particleBox(BlockStateId state, ModelDefinition model, Vec3 from, Vec3 to, boolean culledBySides) {
|
||||||
|
String particle = BlockTextures.dereference("#particle", model.textures());
|
||||||
|
if (particle == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return textures.faceTexture(state, particle, BlockTextures.NO_TINT_INDEX)
|
||||||
|
.map(texture -> BlockModelBuilder.box(from, to, texture, culledBySides))
|
||||||
|
.orElse(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> decoratedPot(BlockStateId state, ModelDefinition model) {
|
||||||
|
List<Face> faces = new ArrayList<>(particleBox(state, model, new Vec3(1, 0, 1), new Vec3(15, 16, 15), false));
|
||||||
|
faces.addAll(particleBox(state, model, new Vec3(4, 16, 4), new Vec3(12, 20, 12), false));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single chests are 14 pixels wide; each half of a double chest is 15, reaching over to its partner.
|
||||||
|
private List<Face> chest(BlockStateId state) {
|
||||||
|
String type = state.properties().getOrDefault("type", "single");
|
||||||
|
String suffix = type.equals("single") ? "" : "_" + type;
|
||||||
|
Optional<BlockTexture> texture = textures.entityTexture("entity/chest/" + chestTextureName(state.name()) + suffix, MinecraftBlockTints.NO_TINT);
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Transform place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0.5D, 0.5D)
|
||||||
|
.rotateY(-yRotationOf(state.properties().getOrDefault("facing", "north")))
|
||||||
|
.translate(-0.5D, -0.5D, -0.5D);
|
||||||
|
double left = type.equals("left") ? 0D : 1D;
|
||||||
|
double width = type.equals("single") ? 14D : 15D;
|
||||||
|
double lockX = switch (type) {
|
||||||
|
case "left" -> 0D;
|
||||||
|
case "right" -> 15D;
|
||||||
|
default -> 7D;
|
||||||
|
};
|
||||||
|
double lockWidth = type.equals("single") ? 2D : 1D;
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 19, new Vec3(left, 0, 1), new Vec3(width, 10, 14), place));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(left, 9, 1), new Vec3(width, 5, 14), place));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(lockX, 7, 15), new Vec3(lockWidth, 4, 1), place));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String chestTextureName(String blockName) {
|
||||||
|
String name = blockName.startsWith("waxed_") ? blockName.substring("waxed_".length()) : blockName;
|
||||||
|
return switch (name) {
|
||||||
|
case "trapped_chest" -> "trapped";
|
||||||
|
case "ender_chest" -> "ender";
|
||||||
|
case "copper_chest" -> "copper";
|
||||||
|
case "exposed_copper_chest" -> "copper_exposed";
|
||||||
|
case "weathered_copper_chest" -> "copper_weathered";
|
||||||
|
case "oxidized_copper_chest" -> "copper_oxidized";
|
||||||
|
default -> "normal";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heads are drawn upside down and mirrored in their model, and turn in sixteenths like signs.
|
||||||
|
private List<Face> head(BlockStateId state, ModelDefinition model) {
|
||||||
|
boolean onWall = state.name().contains("_wall_");
|
||||||
|
String kind = state.name().replace("_wall_", "_");
|
||||||
|
Optional<HeadLook> look = HeadLook.of(kind);
|
||||||
|
Optional<BlockTexture> texture = look.flatMap(found -> textures.entityTexture(found.texture(), MinecraftBlockTints.NO_TINT));
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return onWall
|
||||||
|
? particleBox(state, model, new Vec3(4, 4, 8), new Vec3(12, 12, 16), false)
|
||||||
|
: particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 8, 12), false);
|
||||||
|
}
|
||||||
|
Transform place;
|
||||||
|
if (onWall) {
|
||||||
|
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D - facing.stepX() * SKULL_WALL_OFFSET, SKULL_WALL_OFFSET, 0.5D - facing.stepZ() * SKULL_WALL_OFFSET)
|
||||||
|
.scale(-1D, -1D, 1D)
|
||||||
|
.rotateY(yRotationOf(facing.opposite().modelName()));
|
||||||
|
} else {
|
||||||
|
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0D, 0.5D)
|
||||||
|
.scale(-1D, -1D, 1D)
|
||||||
|
.rotateY(rotation * ROTATION_SEGMENT_DEGREES);
|
||||||
|
}
|
||||||
|
HeadLook found = look.get();
|
||||||
|
return modelPartBox(texture.get(), 0, 0, new Vec3(-found.width() / 2D, -8, -4), new Vec3(found.width(), 8, 8), place);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> banner(BlockStateId state) {
|
||||||
|
boolean onWall = state.name().endsWith("_wall_banner");
|
||||||
|
String color = state.name().substring(0, state.name().length() - (onWall ? "_wall_banner" : "_banner").length());
|
||||||
|
Optional<BlockTexture> frame = textures.entityTexture("entity/banner/banner_base", MinecraftBlockTints.NO_TINT);
|
||||||
|
Optional<BlockTexture> cloth = textures.entityTexture("entity/banner/base", DYE_COLORS.getOrDefault(color, MinecraftBlockTints.NO_TINT));
|
||||||
|
if (frame.isEmpty() || cloth.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Transform place;
|
||||||
|
if (onWall) {
|
||||||
|
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, -1D / 6D, 0.5D)
|
||||||
|
.rotateY(-yRotationOf(facing.modelName()))
|
||||||
|
.translate(0D, -0.3125D, -0.4375D)
|
||||||
|
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
|
||||||
|
} else {
|
||||||
|
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0.5D, 0.5D)
|
||||||
|
.rotateY(-rotation * ROTATION_SEGMENT_DEGREES)
|
||||||
|
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
if (!onWall) {
|
||||||
|
faces.addAll(modelPartBox(frame.get(), 44, 0, new Vec3(-1, -30, -1), new Vec3(2, 42, 2), place));
|
||||||
|
}
|
||||||
|
faces.addAll(modelPartBox(frame.get(), 0, 42, new Vec3(-10, -32, -1), new Vec3(20, 2, 2), place));
|
||||||
|
faces.addAll(modelPartBox(cloth.get(), 0, 0, new Vec3(-10, -32, -2), new Vec3(20, 40, 1), place));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft's facing angle: south is 0 and each quarter turn clockwise seen from above adds 90 degrees.
|
||||||
|
private static double yRotationOf(String facing) {
|
||||||
|
return switch (facing) {
|
||||||
|
case "west" -> 90D;
|
||||||
|
case "north" -> 180D;
|
||||||
|
case "east" -> 270D;
|
||||||
|
default -> 0D;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box of a block entity's model, in model pixels, with its sides laid out on the texture the way Minecraft's
|
||||||
|
* model parts are: the top and bottom in a row above the four sides.
|
||||||
|
*/
|
||||||
|
static List<Face> modelPartBox(BlockTexture texture, int textureU, int textureV, Vec3 from, Vec3 size, Transform place) {
|
||||||
|
double minX = from.x() / MODEL_UNITS;
|
||||||
|
double minY = from.y() / MODEL_UNITS;
|
||||||
|
double minZ = from.z() / MODEL_UNITS;
|
||||||
|
double maxX = (from.x() + size.x()) / MODEL_UNITS;
|
||||||
|
double maxY = (from.y() + size.y()) / MODEL_UNITS;
|
||||||
|
double maxZ = (from.z() + size.z()) / MODEL_UNITS;
|
||||||
|
Vec3 v0 = new Vec3(minX, minY, minZ);
|
||||||
|
Vec3 v1 = new Vec3(maxX, minY, minZ);
|
||||||
|
Vec3 v2 = new Vec3(maxX, maxY, minZ);
|
||||||
|
Vec3 v3 = new Vec3(minX, maxY, minZ);
|
||||||
|
Vec3 v4 = new Vec3(minX, minY, maxZ);
|
||||||
|
Vec3 v5 = new Vec3(maxX, minY, maxZ);
|
||||||
|
Vec3 v6 = new Vec3(maxX, maxY, maxZ);
|
||||||
|
Vec3 v7 = new Vec3(minX, maxY, maxZ);
|
||||||
|
double u = textureU;
|
||||||
|
double v = textureV;
|
||||||
|
double width = size.x();
|
||||||
|
double height = size.y();
|
||||||
|
double depth = size.z();
|
||||||
|
List<Face> faces = new ArrayList<>(6);
|
||||||
|
faces.add(polygon(texture, place, CubeFace.DOWN, v5, v4, v0, u + depth, v, u + depth + width, v + depth));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.UP, v2, v3, v7, u + depth + width, v + depth, u + depth + width + width, v));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.WEST, v0, v4, v7, u, v + depth, u + depth, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.NORTH, v1, v0, v3, u + depth, v + depth, u + depth + width, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.EAST, v5, v1, v2, u + depth + width, v + depth, u + depth + width + depth, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.SOUTH, v4, v5, v6, u + depth + width + depth, v + depth, u + depth + width + depth + width, v + depth + height));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A model part's polygon gives its first corner (u2, v1), its second (u1, v1) and its third (u1, v2).
|
||||||
|
private static Face polygon(BlockTexture texture, Transform place, CubeFace side, Vec3 first, Vec3 second, Vec3 third,
|
||||||
|
double u1, double v1, double u2, double v2) {
|
||||||
|
double textureWidth = texture.width();
|
||||||
|
double textureHeight = texture.height();
|
||||||
|
TextureMapping mapping = new TextureMapping(u1 / textureWidth, v1 / textureHeight,
|
||||||
|
(u2 - u1) / textureWidth, 0D, 0D, (v2 - v1) / textureHeight);
|
||||||
|
Face face = new Face(second, first.minus(second), third.minus(second), side.direction(), texture, mapping, side.shade(), null)
|
||||||
|
.transformed(place);
|
||||||
|
return face.withShade(CubeFace.nearest(face.normal()).shade());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entity texture of a mob head and how wide its head is.
|
||||||
|
*/
|
||||||
|
private record HeadLook(String texture, double width) {
|
||||||
|
private static final double HEAD_WIDTH = 8D;
|
||||||
|
private static final double PIGLIN_HEAD_WIDTH = 10D;
|
||||||
|
|
||||||
|
static Optional<HeadLook> of(String kind) {
|
||||||
|
return Optional.ofNullable(switch (kind) {
|
||||||
|
case "skeleton_skull" -> new HeadLook("entity/skeleton/skeleton", HEAD_WIDTH);
|
||||||
|
case "wither_skeleton_skull" -> new HeadLook("entity/skeleton/wither_skeleton", HEAD_WIDTH);
|
||||||
|
case "zombie_head" -> new HeadLook("entity/zombie/zombie", HEAD_WIDTH);
|
||||||
|
case "creeper_head" -> new HeadLook("entity/creeper/creeper", HEAD_WIDTH);
|
||||||
|
case "player_head" -> new HeadLook("entity/player/wide/steve", HEAD_WIDTH);
|
||||||
|
case "piglin_head" -> new HeadLook("entity/piglin/piglin", PIGLIN_HEAD_WIDTH);
|
||||||
|
default -> null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ import javax.swing.JComponent;
|
|||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -182,7 +181,7 @@ final class ObjectExplorer {
|
|||||||
viewport.show(loaded.mesh());
|
viewport.show(loaded.mesh());
|
||||||
overview.show(loaded.summary());
|
overview.show(loaded.summary());
|
||||||
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn",
|
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn",
|
||||||
loaded.summary().key(), loaded.summary().blockCount(), loaded.mesh().quads().size()));
|
loaded.summary().key(), loaded.summary().blockCount(), loaded.mesh().faceCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFailure(long revision, String key, Throwable error) {
|
private void showFailure(long revision, String key, Throwable error) {
|
||||||
@@ -228,12 +227,7 @@ final class ObjectExplorer {
|
|||||||
|
|
||||||
// Objects have no biome of their own, so grass and leaves get plains' colours.
|
// Objects have no biome of their own, so grass and leaves get plains' colours.
|
||||||
private BlockModel withBiomeTint(BlockModel model) {
|
private BlockModel withBiomeTint(BlockModel model) {
|
||||||
if (model.isCross()) {
|
return model.withTextures(this::withBiomeTint);
|
||||||
return BlockModel.cross(withBiomeTint(model.crossSprite()));
|
|
||||||
}
|
|
||||||
Map<CubeFace, BlockTexture> faces = new EnumMap<>(CubeFace.class);
|
|
||||||
model.faces().forEach((face, texture) -> faces.put(face, withBiomeTint(texture)));
|
|
||||||
return BlockModel.box(model.box(), faces);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private BlockTexture withBiomeTint(BlockTexture texture) {
|
private BlockTexture withBiomeTint(BlockTexture texture) {
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.util.BlockVector;
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
@@ -28,154 +29,72 @@ import java.util.Map;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sides of an object's blocks that can be seen from outside, as flat textured quads. Sides pressed against a solid
|
* The faces of an object's blocks that can be seen from outside. A face is left out when the neighbour on its side
|
||||||
* neighbour are left out, which is most of them in a large object.
|
* covers that side, which is most of them in a large object. See-through faces, like water and stained glass, are
|
||||||
|
* kept apart because they are drawn last, blended over everything behind them.
|
||||||
*/
|
*/
|
||||||
record ObjectMesh(List<Quad> quads, Bounds bounds) {
|
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bounds) {
|
||||||
private static final double CROSS_SHADE = 0.9D;
|
|
||||||
|
|
||||||
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
|
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
|
||||||
List<Quad> quads = new ArrayList<>();
|
List<Quad> solid = new ArrayList<>();
|
||||||
|
List<Quad> translucent = new ArrayList<>();
|
||||||
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
|
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
|
||||||
BlockVector position = entry.getKey();
|
BlockVector position = entry.getKey();
|
||||||
BlockData block = entry.getValue();
|
BlockData block = entry.getValue();
|
||||||
BlockModel model = modelOf.apply(block);
|
BlockModel model = modelOf.apply(block);
|
||||||
if (model.isCross()) {
|
for (Face face : model.faces()) {
|
||||||
addCross(quads, position, model.crossSprite());
|
if (isHiddenByNeighbour(blocks, modelOf, position, block, model, face)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
for (Map.Entry<CubeFace, BlockTexture> face : model.faces().entrySet()) {
|
|
||||||
if (!isCoveredByNeighbour(blocks, modelOf, position, block, model, face.getKey())) {
|
|
||||||
quads.add(Quad.side(position, model.box(), face.getKey(), face.getValue()));
|
|
||||||
}
|
}
|
||||||
|
Quad quad = new Quad(face, position.getBlockX(), position.getBlockY(), position.getBlockZ());
|
||||||
|
(face.texture().isTranslucent() ? translucent : solid).add(quad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ObjectMesh(List.copyOf(quads), Bounds.of(blocks.keySet()));
|
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), Bounds.of(blocks.keySet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isEmpty() {
|
boolean isEmpty() {
|
||||||
return quads.isEmpty();
|
return solidQuads.isEmpty() && translucentQuads.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaves next to leaves and glass next to glass hide the side between them too, as they do in the game.
|
int faceCount() {
|
||||||
private static boolean isCoveredByNeighbour(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf,
|
return solidQuads.size() + translucentQuads.size();
|
||||||
BlockVector position, BlockData block, BlockModel model, CubeFace face) {
|
}
|
||||||
if (!touchesBlockEdge(model.box(), face)) {
|
|
||||||
|
// Glass next to glass and water next to water hide the side between them too, as they do in the game.
|
||||||
|
private static boolean isHiddenByNeighbour(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf,
|
||||||
|
BlockVector position, BlockData block, BlockModel model, Face face) {
|
||||||
|
CubeFace side = face.cullFace();
|
||||||
|
if (side == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
BlockData neighbour = blocks.get(new BlockVector(
|
BlockData neighbour = blocks.get(new BlockVector(
|
||||||
position.getBlockX() + face.stepX(),
|
position.getBlockX() + side.stepX(),
|
||||||
position.getBlockY() + face.stepY(),
|
position.getBlockY() + side.stepY(),
|
||||||
position.getBlockZ() + face.stepZ()));
|
position.getBlockZ() + side.stepZ()));
|
||||||
if (neighbour == null) {
|
if (neighbour == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (neighbour.equals(block) && model.box().isFull()) {
|
BlockModel neighbourModel = modelOf.apply(neighbour);
|
||||||
|
if (neighbourModel.coveredSides().contains(side.opposite())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return modelOf.apply(neighbour).hidesTouchingSides();
|
return neighbour.getMaterial() == block.getMaterial() && model.hidesSameBlockSides() && neighbourModel.hidesSameBlockSides();
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean touchesBlockEdge(BlockModel.Box box, CubeFace face) {
|
|
||||||
return switch (face) {
|
|
||||||
case UP -> box.maxY() >= 1D;
|
|
||||||
case DOWN -> box.minY() <= 0D;
|
|
||||||
case NORTH -> box.minZ() <= 0D;
|
|
||||||
case SOUTH -> box.maxZ() >= 1D;
|
|
||||||
case EAST -> box.maxX() >= 1D;
|
|
||||||
case WEST -> box.minX() <= 0D;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addCross(List<Quad> quads, BlockVector position, BlockTexture sprite) {
|
|
||||||
double x = position.getBlockX();
|
|
||||||
double y = position.getBlockY();
|
|
||||||
double z = position.getBlockZ();
|
|
||||||
quads.add(new Quad(x, y + 1D, z, 1D, 0D, 1D, 0D, -1D, 0D, sprite, 0D, 0D, 1D, 1D, CROSS_SHADE, 0D, 0D, 0D));
|
|
||||||
quads.add(new Quad(x, y + 1D, z + 1D, 1D, 0D, -1D, 0D, -1D, 0D, sprite, 0D, 0D, 1D, 1D, CROSS_SHADE, 0D, 0D, 0D));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A parallelogram from its corner along two edges. Texture u runs along the first edge and v along the second,
|
* A model face placed at a block in the object.
|
||||||
* starting at {@code textureU}, {@code textureV} and covering {@code textureWidth} by {@code textureHeight} of
|
|
||||||
* the texture. A zero normal marks a quad seen from both sides, like a plant's sprite.
|
|
||||||
*/
|
*/
|
||||||
record Quad(double originX, double originY, double originZ,
|
record Quad(Face face, int x, int y, int z) {
|
||||||
double edgeUX, double edgeUY, double edgeUZ,
|
double originX() {
|
||||||
double edgeVX, double edgeVY, double edgeVZ,
|
return x + face.origin().x();
|
||||||
BlockTexture texture, double textureU, double textureV, double textureWidth, double textureHeight,
|
|
||||||
double shade, double normalX, double normalY, double normalZ) {
|
|
||||||
|
|
||||||
boolean isTwoSided() {
|
|
||||||
return normalX == 0D && normalY == 0D && normalZ == 0D;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each side is laid out as seen from outside the block: u to the right, v downwards, matching Minecraft's UVs.
|
double originY() {
|
||||||
static Quad side(BlockVector position, BlockModel.Box box, CubeFace face, BlockTexture texture) {
|
return y + face.origin().y();
|
||||||
double[] origin = switch (face) {
|
|
||||||
case UP -> new double[]{box.minX(), box.maxY(), box.minZ()};
|
|
||||||
case DOWN -> new double[]{box.minX(), box.minY(), box.maxZ()};
|
|
||||||
case NORTH -> new double[]{box.maxX(), box.maxY(), box.minZ()};
|
|
||||||
case SOUTH -> new double[]{box.minX(), box.maxY(), box.maxZ()};
|
|
||||||
case EAST -> new double[]{box.maxX(), box.maxY(), box.maxZ()};
|
|
||||||
case WEST -> new double[]{box.minX(), box.maxY(), box.minZ()};
|
|
||||||
};
|
|
||||||
Edge u = switch (face) {
|
|
||||||
case UP, DOWN, SOUTH -> Edge.POSITIVE_X;
|
|
||||||
case NORTH -> Edge.NEGATIVE_X;
|
|
||||||
case EAST -> Edge.NEGATIVE_Z;
|
|
||||||
case WEST -> Edge.POSITIVE_Z;
|
|
||||||
};
|
|
||||||
Edge v = switch (face) {
|
|
||||||
case UP -> Edge.POSITIVE_Z;
|
|
||||||
case DOWN -> Edge.NEGATIVE_Z;
|
|
||||||
default -> Edge.NEGATIVE_Y;
|
|
||||||
};
|
|
||||||
double uLength = box.max(u.axis()) - box.min(u.axis());
|
|
||||||
double vLength = box.max(v.axis()) - box.min(v.axis());
|
|
||||||
return new Quad(
|
|
||||||
position.getBlockX() + origin[0], position.getBlockY() + origin[1], position.getBlockZ() + origin[2],
|
|
||||||
u.x() * uLength, u.y() * uLength, u.z() * uLength,
|
|
||||||
v.x() * vLength, v.y() * vLength, v.z() * vLength,
|
|
||||||
texture, u.textureStart(origin), v.textureStart(origin), uLength, vLength,
|
|
||||||
face.shade(), face.stepX(), face.stepY(), face.stepZ());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum Edge {
|
|
||||||
POSITIVE_X(0, 1),
|
|
||||||
NEGATIVE_X(0, -1),
|
|
||||||
NEGATIVE_Y(1, -1),
|
|
||||||
POSITIVE_Z(2, 1),
|
|
||||||
NEGATIVE_Z(2, -1);
|
|
||||||
|
|
||||||
private final int axis;
|
|
||||||
private final int sign;
|
|
||||||
|
|
||||||
Edge(int axis, int sign) {
|
|
||||||
this.axis = axis;
|
|
||||||
this.sign = sign;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int axis() {
|
double originZ() {
|
||||||
return axis;
|
return z + face.origin().z();
|
||||||
}
|
|
||||||
|
|
||||||
double x() {
|
|
||||||
return axis == 0 ? sign : 0D;
|
|
||||||
}
|
|
||||||
|
|
||||||
double y() {
|
|
||||||
return axis == 1 ? sign : 0D;
|
|
||||||
}
|
|
||||||
|
|
||||||
double z() {
|
|
||||||
return axis == 2 ? sign : 0D;
|
|
||||||
}
|
|
||||||
|
|
||||||
// An edge running against its axis starts its texture at the far side, like Minecraft's default UVs.
|
|
||||||
double textureStart(double[] origin) {
|
|
||||||
return sign > 0 ? origin[axis] : 1D - origin[axis];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,21 @@
|
|||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws an object mesh with an orthographic camera into a picture, without a graphics card: each quad is filled pixel
|
* Draws an object mesh with an orthographic camera into a picture, without a graphics card: each quad is filled pixel
|
||||||
* by pixel, and a depth buffer keeps the nearest one.
|
* by pixel, and a depth buffer keeps the nearest one. See-through quads come last, farthest first, each blended over
|
||||||
|
* what is already drawn.
|
||||||
*/
|
*/
|
||||||
final class ObjectRenderer {
|
final class ObjectRenderer {
|
||||||
private static final int BACKGROUND = StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF;
|
private static final int BACKGROUND = StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF;
|
||||||
@@ -37,6 +43,9 @@ final class ObjectRenderer {
|
|||||||
// Shared quad edges land exactly on pixel centres now and then; a little overlap keeps them from leaving cracks.
|
// Shared quad edges land exactly on pixel centres now and then; a little overlap keeps them from leaving cracks.
|
||||||
private static final double EDGE_TOLERANCE = 1e-6D;
|
private static final double EDGE_TOLERANCE = 1e-6D;
|
||||||
private static final double EDGE_ON_DETERMINANT = 1e-9D;
|
private static final double EDGE_ON_DETERMINANT = 1e-9D;
|
||||||
|
// A face's own texels end a hair inside its UV rectangle, so rounding never picks the texel next to it.
|
||||||
|
private static final double TEXEL_EDGE_MARGIN = 1e-6D;
|
||||||
|
private static final double FULL_ALPHA = 255D;
|
||||||
|
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
@@ -62,12 +71,29 @@ final class ObjectRenderer {
|
|||||||
Arrays.fill(renderer.pixels, BACKGROUND);
|
Arrays.fill(renderer.pixels, BACKGROUND);
|
||||||
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
||||||
renderer.drawGrid(mesh.bounds());
|
renderer.drawGrid(mesh.bounds());
|
||||||
for (ObjectMesh.Quad quad : mesh.quads()) {
|
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||||
renderer.drawQuad(quad);
|
renderer.drawQuad(quad, false);
|
||||||
|
}
|
||||||
|
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
|
||||||
|
renderer.drawQuad(quad, true);
|
||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ObjectMesh.Quad> farthestFirst(List<ObjectMesh.Quad> quads) {
|
||||||
|
List<ObjectMesh.Quad> sorted = new ArrayList<>(quads);
|
||||||
|
sorted.sort(Comparator.comparingDouble(this::centerDepth));
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double centerDepth(ObjectMesh.Quad quad) {
|
||||||
|
Face face = quad.face();
|
||||||
|
return projection.depth(
|
||||||
|
quad.originX() + (face.edgeU().x() + face.edgeV().x()) / 2D,
|
||||||
|
quad.originY() + (face.edgeU().y() + face.edgeV().y()) / 2D,
|
||||||
|
quad.originZ() + (face.edgeU().z() + face.edgeV().z()) / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
private void drawGrid(ObjectMesh.Bounds bounds) {
|
private void drawGrid(ObjectMesh.Bounds bounds) {
|
||||||
int reach = (int) Math.ceil(bounds.largestSize() * GRID_REACH);
|
int reach = (int) Math.ceil(bounds.largestSize() * GRID_REACH);
|
||||||
int step = Math.max(1, (int) Math.ceil(reach / (double) GRID_LINES_PER_SIDE));
|
int step = Math.max(1, (int) Math.ceil(reach / (double) GRID_LINES_PER_SIDE));
|
||||||
@@ -104,19 +130,23 @@ final class ObjectRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawQuad(ObjectMesh.Quad quad) {
|
// Later faces win ties, so an overlay drawn on the same box, like grass on a grass block's side, shows on top.
|
||||||
if (!quad.isTwoSided() && !projection.facesCamera(quad.normalX(), quad.normalY(), quad.normalZ())) {
|
private void drawQuad(ObjectMesh.Quad quad, boolean blended) {
|
||||||
|
Face face = quad.face();
|
||||||
|
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Vec3 u = face.edgeU();
|
||||||
|
Vec3 v = face.edgeV();
|
||||||
double originX = projection.screenX(quad.originX(), quad.originY(), quad.originZ());
|
double originX = projection.screenX(quad.originX(), quad.originY(), quad.originZ());
|
||||||
double originY = projection.screenY(quad.originX(), quad.originY(), quad.originZ());
|
double originY = projection.screenY(quad.originX(), quad.originY(), quad.originZ());
|
||||||
double originDepth = projection.depth(quad.originX(), quad.originY(), quad.originZ());
|
double originDepth = projection.depth(quad.originX(), quad.originY(), quad.originZ());
|
||||||
double edgeUX = projection.screenStepX(quad.edgeUX(), quad.edgeUZ());
|
double edgeUX = projection.screenStepX(u.x(), u.z());
|
||||||
double edgeUY = projection.screenStepY(quad.edgeUX(), quad.edgeUY(), quad.edgeUZ());
|
double edgeUY = projection.screenStepY(u.x(), u.y(), u.z());
|
||||||
double edgeUDepth = projection.depthStep(quad.edgeUX(), quad.edgeUY(), quad.edgeUZ());
|
double edgeUDepth = projection.depthStep(u.x(), u.y(), u.z());
|
||||||
double edgeVX = projection.screenStepX(quad.edgeVX(), quad.edgeVZ());
|
double edgeVX = projection.screenStepX(v.x(), v.z());
|
||||||
double edgeVY = projection.screenStepY(quad.edgeVX(), quad.edgeVY(), quad.edgeVZ());
|
double edgeVY = projection.screenStepY(v.x(), v.y(), v.z());
|
||||||
double edgeVDepth = projection.depthStep(quad.edgeVX(), quad.edgeVY(), quad.edgeVZ());
|
double edgeVDepth = projection.depthStep(v.x(), v.y(), v.z());
|
||||||
double determinant = edgeUX * edgeVY - edgeUY * edgeVX;
|
double determinant = edgeUX * edgeVY - edgeUY * edgeVX;
|
||||||
if (Math.abs(determinant) < EDGE_ON_DETERMINANT) {
|
if (Math.abs(determinant) < EDGE_ON_DETERMINANT) {
|
||||||
return;
|
return;
|
||||||
@@ -130,10 +160,14 @@ final class ObjectRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockTexture texture = quad.texture();
|
BlockTexture texture = face.texture();
|
||||||
int textureSize = texture.size();
|
TextureMapping mapping = face.mapping();
|
||||||
|
TexelRange columns = TexelRange.of(mapping.u(), mapping.uAlongU(), mapping.uAlongV(), texture.width());
|
||||||
|
TexelRange rows = TexelRange.of(mapping.v(), mapping.vAlongU(), mapping.vAlongV(), texture.height());
|
||||||
int[] texels = texture.pixels();
|
int[] texels = texture.pixels();
|
||||||
boolean[] opaque = texture.opaque();
|
boolean[] opaque = texture.opaque();
|
||||||
|
int[] alphas = texture.alphas();
|
||||||
|
double shade = face.shade();
|
||||||
double alongUPerPixelX = edgeVY / determinant;
|
double alongUPerPixelX = edgeVY / determinant;
|
||||||
double alongVPerPixelX = -edgeUY / determinant;
|
double alongVPerPixelX = -edgeUY / determinant;
|
||||||
for (int y = top; y <= bottom; y++) {
|
for (int y = top; y <= bottom; y++) {
|
||||||
@@ -148,23 +182,32 @@ final class ObjectRenderer {
|
|||||||
}
|
}
|
||||||
int index = rowStart + x;
|
int index = rowStart + x;
|
||||||
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
|
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
|
||||||
if (depth <= depths[index]) {
|
if (depth < depths[index]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int texelX = texelCoordinate(quad.textureU() + alongU * quad.textureWidth(), textureSize);
|
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
|
||||||
int texelY = texelCoordinate(quad.textureV() + alongV * quad.textureHeight(), textureSize);
|
int texelY = rows.texelAt(mapping.v() + alongU * mapping.vAlongU() + alongV * mapping.vAlongV());
|
||||||
int texel = texelY * textureSize + texelX;
|
int texel = texelY * texture.width() + texelX;
|
||||||
if (!opaque[texel]) {
|
if (!opaque[texel]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
int color = BlockPreviewColors.shade(texels[texel], shade);
|
||||||
|
if (blended) {
|
||||||
|
pixels[index] = blend(color, pixels[index], alphas == null ? 1D : alphas[texel] / FULL_ALPHA);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
depths[index] = depth;
|
depths[index] = depth;
|
||||||
pixels[index] = BlockPreviewColors.shade(texels[texel], quad.shade());
|
pixels[index] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int texelCoordinate(double fraction, int textureSize) {
|
private static int blend(int over, int under, double coverage) {
|
||||||
return Math.max(0, Math.min(textureSize - 1, (int) (fraction * textureSize)));
|
return blendChannel(over, under, coverage, 16) << 16 | blendChannel(over, under, coverage, 8) << 8 | blendChannel(over, under, coverage, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int blendChannel(int over, int under, double coverage, int shift) {
|
||||||
|
return (int) Math.round(((over >>> shift) & 0xFF) * coverage + ((under >>> shift) & 0xFF) * (1D - coverage));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double min4(double a, double b, double c, double d) {
|
private static double min4(double a, double b, double c, double d) {
|
||||||
@@ -175,6 +218,28 @@ final class ObjectRenderer {
|
|||||||
return Math.max(Math.max(a, b), Math.max(c, d));
|
return Math.max(Math.max(a, b), Math.max(c, d));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The texels a face covers along one texture axis, so a pixel on the face's edge never takes its neighbour's.
|
||||||
|
*/
|
||||||
|
private record TexelRange(int first, int last, int size) {
|
||||||
|
static TexelRange of(double start, double alongU, double alongV, int size) {
|
||||||
|
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
|
||||||
|
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
|
||||||
|
int first = clamp((int) Math.floor(low * size + TEXEL_EDGE_MARGIN), size);
|
||||||
|
int last = Math.max(first, clamp((int) Math.ceil(high * size - TEXEL_EDGE_MARGIN) - 1, size));
|
||||||
|
return new TexelRange(first, last, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int texelAt(double fraction) {
|
||||||
|
int texel = (int) Math.floor(fraction * size);
|
||||||
|
return texel < first ? first : Math.min(texel, last);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int texel, int size) {
|
||||||
|
return Math.max(0, Math.min(size - 1, texel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns block positions into pixels. Larger depth is nearer the camera.
|
* Turns block positions into pixels. Larger depth is nearer the camera.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An affine transform built up like Minecraft's pose stack: each step applies to points before the steps added
|
||||||
|
* earlier. Rotations follow the right-hand rule, the way Minecraft turns model parts.
|
||||||
|
*/
|
||||||
|
final class Transform {
|
||||||
|
static final Transform IDENTITY = new Transform(new double[]{1D, 0D, 0D, 0D, 1D, 0D, 0D, 0D, 1D}, Vec3.ZERO);
|
||||||
|
|
||||||
|
private final double[] matrix;
|
||||||
|
private final Vec3 translation;
|
||||||
|
|
||||||
|
private Transform(double[] matrix, Vec3 translation) {
|
||||||
|
this.matrix = matrix;
|
||||||
|
this.translation = translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform translate(double x, double y, double z) {
|
||||||
|
return then(new Transform(IDENTITY.matrix, new Vec3(x, y, z)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform rotateX(double degrees) {
|
||||||
|
double cos = cosine(degrees);
|
||||||
|
double sin = sine(degrees);
|
||||||
|
return then(new Transform(new double[]{1D, 0D, 0D, 0D, cos, -sin, 0D, sin, cos}, Vec3.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform rotateY(double degrees) {
|
||||||
|
double cos = cosine(degrees);
|
||||||
|
double sin = sine(degrees);
|
||||||
|
return then(new Transform(new double[]{cos, 0D, sin, 0D, 1D, 0D, -sin, 0D, cos}, Vec3.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform rotateZ(double degrees) {
|
||||||
|
double cos = cosine(degrees);
|
||||||
|
double sin = sine(degrees);
|
||||||
|
return then(new Transform(new double[]{cos, -sin, 0D, sin, cos, 0D, 0D, 0D, 1D}, Vec3.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform scale(double x, double y, double z) {
|
||||||
|
return then(new Transform(new double[]{x, 0D, 0D, 0D, y, 0D, 0D, 0D, z}, Vec3.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This transform applied after {@code next}.
|
||||||
|
*/
|
||||||
|
Transform then(Transform next) {
|
||||||
|
double[] product = new double[9];
|
||||||
|
for (int row = 0; row < 3; row++) {
|
||||||
|
for (int column = 0; column < 3; column++) {
|
||||||
|
product[row * 3 + column] = matrix[row * 3] * next.matrix[column]
|
||||||
|
+ matrix[row * 3 + 1] * next.matrix[3 + column]
|
||||||
|
+ matrix[row * 3 + 2] * next.matrix[6 + column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Transform(product, direction(next.translation).plus(translation));
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 point(Vec3 point) {
|
||||||
|
return direction(point).plus(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 direction(Vec3 direction) {
|
||||||
|
return new Vec3(
|
||||||
|
matrix[0] * direction.x() + matrix[1] * direction.y() + matrix[2] * direction.z(),
|
||||||
|
matrix[3] * direction.x() + matrix[4] * direction.y() + matrix[5] * direction.z(),
|
||||||
|
matrix[6] * direction.x() + matrix[7] * direction.y() + matrix[8] * direction.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quarter turns stay exact, so faces turned by a blockstate still line up with the block's sides.
|
||||||
|
private static double cosine(double degrees) {
|
||||||
|
return sine(degrees + 90D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double sine(double degrees) {
|
||||||
|
double turned = ((degrees % 360D) + 360D) % 360D;
|
||||||
|
if (turned == 0D || turned == 180D) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
if (turned == 90D) {
|
||||||
|
return 1D;
|
||||||
|
}
|
||||||
|
if (turned == 270D) {
|
||||||
|
return -1D;
|
||||||
|
}
|
||||||
|
return Math.sin(Math.toRadians(turned));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A position or direction in a block, in block units.
|
||||||
|
*/
|
||||||
|
record Vec3(double x, double y, double z) {
|
||||||
|
static final Vec3 ZERO = new Vec3(0D, 0D, 0D);
|
||||||
|
|
||||||
|
Vec3 plus(Vec3 other) {
|
||||||
|
return new Vec3(x + other.x, y + other.y, z + other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 minus(Vec3 other) {
|
||||||
|
return new Vec3(x - other.x, y - other.y, z - other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 times(double factor) {
|
||||||
|
return new Vec3(x * factor, y * factor, z * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
double component(int axis) {
|
||||||
|
return switch (axis) {
|
||||||
|
case 0 -> x;
|
||||||
|
case 1 -> y;
|
||||||
|
default -> z;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user