BlueMap/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/blockmodel/BlockModelResource.java

494 lines
14 KiB
Java

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resourcepack.blockmodel;
import com.flowpowered.math.TrigMath;
import com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector3i;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resourcepack.blockmodel.BlockModelResource.Element.Face;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.math.Axis;
import de.bluecolored.bluemap.core.util.math.MatrixM4f;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
public class BlockModelResource {
private static final double FIT_TO_BLOCK_SCALE_MULTIPLIER = 2 - Math.sqrt(2);
private ModelType modelType = ModelType.NORMAL;
private boolean culling = false;
private boolean occluding = false;
private final boolean ambientOcclusion = true; //TODO: wat?
private final Collection<Element> elements = new ArrayList<>();
private final Map<String, Texture> textures = new HashMap<>();
private BlockModelResource() {}
public ModelType getType() {
return modelType;
}
public boolean isAmbientOcclusion() {
return ambientOcclusion;
}
public boolean isCulling() {
return culling;
}
public boolean isOccluding() {
return occluding;
}
public Collection<Element> getElements() {
return elements;
}
public Texture getTexture(String key) {
return textures.get(key);
}
public class Element {
private Vector3f from = Vector3f.ZERO, to = new Vector3f(16f, 16f, 16f);
private Rotation rotation = new Rotation();
private MatrixM4f rotationMatrix;
private boolean shade = true;
private EnumMap<Direction, Face> faces = new EnumMap<>(Direction.class);
private boolean fullCube = false;
private Element() {}
public Vector4f getDefaultUV(Direction face) {
switch (face){
case DOWN :
case UP :
return new Vector4f(
from.getX(), from.getZ(),
to.getX(), to.getZ()
);
case NORTH :
case SOUTH :
return new Vector4f(
from.getX(), from.getY(),
to.getX(), to.getY()
);
case WEST :
case EAST :
return new Vector4f(
from.getZ(), from.getY(),
to.getZ(), to.getY()
);
default :
return new Vector4f(
0, 0,
16, 16
);
}
}
public BlockModelResource getModel() {
return BlockModelResource.this;
}
public Vector3f getFrom() {
return from;
}
public Vector3f getTo() {
return to;
}
public Rotation getRotation() {
return rotation;
}
public MatrixM4f getRotationMatrix() {
return rotationMatrix;
}
public boolean isShade() {
return shade;
}
public boolean isFullCube() {
return fullCube;
}
public EnumMap<Direction, Face> getFaces() {
return faces;
}
public class Face {
private Vector4f uv;
private Texture texture;
private Direction cullface;
private int rotation = 0;
private boolean tinted = false;
private Face(Direction dir) {
uv = getDefaultUV(dir);
}
public Element getElement() {
return Element.this;
}
public Vector4f getUv() {
return uv;
}
public Texture getTexture() {
return texture;
}
public Direction getCullface() {
return cullface;
}
public int getRotation() {
return rotation;
}
public boolean isTinted() {
return tinted;
}
}
public class Rotation {
private Vector3f origin = new Vector3f(8, 8, 8);
private Axis axis = Axis.Y;
private float angle = 0;
private boolean rescale = false;
private Rotation() {}
public Vector3f getOrigin() {
return origin;
}
public Axis getAxis() {
return axis;
}
public float getAngle() {
return angle;
}
public boolean isRescale() {
return rescale;
}
}
}
public static Builder builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
return new Builder(sourcesAccess, resourcePack);
}
public static class Builder {
private static final String JSON_COMMENT = "__comment";
private static final Vector3f FULL_CUBE_FROM = Vector3f.ZERO;
private static final Vector3f FULL_CUBE_TO = new Vector3f(16f, 16f, 16f);
private FileAccess sourcesAccess;
private ResourcePack resourcePack;
private HashMap<String, String> textures;
private Builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
this.sourcesAccess = sourcesAccess;
this.resourcePack = resourcePack;
this.textures = new HashMap<>();
}
public synchronized BlockModelResource build(String modelPath) throws IOException, ParseResourceException {
return build(modelPath, Collections.emptyMap());
}
public synchronized BlockModelResource build(String modelPath, Map<String, String> textures) throws IOException, ParseResourceException {
this.textures.clear();
this.textures.putAll(textures);
return buildNoReset(modelPath, true, modelPath);
}
private BlockModelResource buildNoReset(String modelPath, boolean renderElements, String topModelPath) throws IOException, ParseResourceException {
BlockModelResource blockModel = new BlockModelResource();
InputStream fileIn = sourcesAccess.readFile(modelPath);
ConfigurationNode config = GsonConfigurationLoader.builder()
.source(() -> new BufferedReader(new InputStreamReader(fileIn, StandardCharsets.UTF_8)))
.build()
.load();
for (Entry<Object, ? extends ConfigurationNode> entry : config.node("textures").childrenMap().entrySet()) {
if (entry.getKey().equals(JSON_COMMENT)) continue;
String key = entry.getKey().toString();
String value = entry.getValue().getString();
if (("#" + key).equals(value)) continue; // skip direct loop
textures.putIfAbsent(key, value);
}
String parentPath = config.node("parent").getString();
if (parentPath != null) {
if (parentPath.startsWith("builtin")) {
switch (parentPath) {
case "builtin/liquid":
blockModel.modelType = ModelType.LIQUID;
break;
}
} else {
try {
parentPath = ResourcePack.namespacedToAbsoluteResourcePath(parentPath, "models") + ".json";
blockModel = this.buildNoReset(parentPath, renderElements && config.node("elements").virtual(), topModelPath);
} catch (IOException ex) {
throw new ParseResourceException("Failed to load parent model " + parentPath + " of model " + topModelPath, ex);
}
}
}
if (renderElements) {
for (ConfigurationNode elementNode : config.node("elements").childrenList()) {
blockModel.elements.add(buildElement(blockModel, elementNode, topModelPath));
}
}
for (String key : textures.keySet()) {
try {
blockModel.textures.put(key, getTexture("#" + key));
} catch (NoSuchElementException | FileNotFoundException ignore) {
// ignore this so unused textures can remain unresolved. See issue #147
//throw new ParseResourceException("Failed to map Texture key '" + key + "' for model '" + topModelPath + "': " + ex);
}
}
//check block properties
for (Element element : blockModel.elements) {
if (element.isFullCube()) {
blockModel.occluding = true;
blockModel.culling = true;
for (Direction dir : Direction.values()) {
Face face = element.faces.get(dir);
if (face == null) {
blockModel.culling = false;
break;
}
Texture texture = face.getTexture();
if (texture == null) {
blockModel.culling = false;
break;
}
if (texture.getColorStraight().a < 1) {
blockModel.culling = false;
break;
}
}
break;
}
}
return blockModel;
}
private Element buildElement(BlockModelResource model, ConfigurationNode node, String topModelPath) throws ParseResourceException {
Element element = model.new Element();
element.from = readVector3f(node.node("from"));
element.to = readVector3f(node.node("to"));
element.shade = node.node("shade").getBoolean(true);
boolean fullElement = element.from.equals(FULL_CUBE_FROM) && element.to.equals(FULL_CUBE_TO);
if (!node.node("rotation").virtual()) {
element.rotation.angle = node.node("rotation", "angle").getFloat(0);
element.rotation.axis = Axis.fromString(node.node("rotation", "axis").getString("x"));
if (!node.node("rotation", "origin").virtual()) element.rotation.origin = readVector3f(node.node("rotation", "origin"));
element.rotation.rescale = node.node("rotation", "rescale").getBoolean(false);
// rotation matrix
float angle = element.rotation.angle;
Vector3i axis = element.rotation.axis.toVector();
Vector3f origin = element.rotation.origin;
boolean rescale = element.rotation.rescale;
MatrixM4f rot = new MatrixM4f();
if (angle != 0f) {
rot.translate(-origin.getX(), -origin.getY(), -origin.getZ());
rot.rotate(
angle,
axis.getX(),
axis.getY(),
axis.getZ()
);
if (rescale) {
float scale = (float) (Math.abs(TrigMath.sin(angle * TrigMath.DEG_TO_RAD)) * FIT_TO_BLOCK_SCALE_MULTIPLIER);
rot.scale(
(1 - axis.getX()) * scale + 1,
(1 - axis.getY()) * scale + 1,
(1 - axis.getZ()) * scale + 1
);
}
rot.translate(origin.getX(), origin.getY(), origin.getZ());
}
element.rotationMatrix = rot;
} else {
element.rotationMatrix = new MatrixM4f();
}
boolean allDirs = true;
for (Direction direction : Direction.values()) {
ConfigurationNode faceNode = node.node("faces", direction.name().toLowerCase());
if (!faceNode.virtual()) {
try {
Face face = buildFace(element, direction, faceNode);
element.faces.put(direction, face);
} catch (ParseResourceException | IOException ex) {
throw new ParseResourceException("Failed to parse an " + direction + " face for the model " + topModelPath + "!", ex);
}
} else {
allDirs = false;
}
}
if (fullElement && allDirs) element.fullCube = true;
return element;
}
private Face buildFace(Element element, Direction direction, ConfigurationNode node) throws ParseResourceException, IOException {
try {
Face face = element.new Face(direction);
if (!node.node("uv").virtual()) face.uv = readVector4f(node.node("uv"));
face.texture = getTexture(node.node("texture").getString(""));
face.tinted = node.node("tintindex").getInt(-1) >= 0;
face.rotation = node.node("rotation").getInt(0);
if (!node.node("cullface").virtual()) {
String dirString = node.node("cullface").getString("");
if (dirString.equals("bottom")) dirString = "down";
if (dirString.equals("top")) dirString = "up";
try {
face.cullface = Direction.fromString(dirString);
} catch (IllegalArgumentException ignore) {}
}
return face;
} catch (FileNotFoundException ex) {
throw new ParseResourceException("There is no texture with the path: " + node.node("texture").getString(), ex);
} catch (NoSuchElementException ex) {
throw new ParseResourceException("Texture key '" + node.node("texture").getString() + "' has no texture assigned!", ex);
}
}
private Vector3f readVector3f(ConfigurationNode node) throws ParseResourceException {
List<? extends ConfigurationNode> nodeList = node.childrenList();
if (nodeList.size() < 3) throw new ParseResourceException("Failed to load Vector3: Not enough values in list-node!");
return new Vector3f(
nodeList.get(0).getFloat(0),
nodeList.get(1).getFloat(0),
nodeList.get(2).getFloat(0)
);
}
private Vector4f readVector4f(ConfigurationNode node) throws ParseResourceException {
List<? extends ConfigurationNode> nodeList = node.childrenList();
if (nodeList.size() < 4) throw new ParseResourceException("Failed to load Vector4: Not enough values in list-node!");
return new Vector4f(
nodeList.get(0).getFloat(0),
nodeList.get(1).getFloat(0),
nodeList.get(2).getFloat(0),
nodeList.get(3).getFloat(0)
);
}
private Texture getTexture(String key) throws NoSuchElementException, FileNotFoundException, IOException {
return getTexture(key, 0);
}
private Texture getTexture(String key, int depth) throws NoSuchElementException, FileNotFoundException, IOException {
if (key.isEmpty() || key.equals("#")) throw new NoSuchElementException("Empty texture key or name!");
if (depth > 10) throw new NoSuchElementException("Recursive texture-variable!");
if (key.charAt(0) == '#') {
String value = textures.get(key.substring(1));
if (value == null) throw new NoSuchElementException("There is no texture defined for the key " + key);
return getTexture(value, depth + 1);
}
String path = ResourcePack.namespacedToAbsoluteResourcePath(key, "textures") + ".png";
Texture texture;
try {
texture = resourcePack.getTextures().get(path);
} catch (NoSuchElementException ex) {
texture = resourcePack.getTextures().loadTexture(sourcesAccess, path);
}
return texture;
}
}
}