/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA 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.
*
* NOVA 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 NOVA. If not, see <http://www.gnu.org/licenses/>.
*/package nova.core.render.model;
import nova.core.render.RenderException;
import nova.core.render.pipeline.BlockRenderPipeline;
import nova.core.render.pipeline.CubeTextureCoordinates;
import nova.core.util.math.MatrixStack;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
/**
* A Techne model importer.
* You must load your .tcn file and then bind the Techne texture yourself.
* @author Calclavia
*/
public class TechneModelProvider extends ModelProvider {
//Identifiers for cubes
public static final List<String> cubeIDs = Arrays.asList("d9e621f7-957f-4b77-b1ae-20dcd0da7751", "de81aa14-bd60-4228-8d8d-5238bcd3caaa");
//A map of all models generated with their names
private final MeshModel model = new MeshModel();
/**
* Creates new ModelProvider
* @param domain dolain of the assets.
* @param name name of the model.
*/
public TechneModelProvider(String domain, String name) {
super(domain, name);
}
@Override
public void load(InputStream stream) {
try {
Map<String, byte[]> zipContents = new HashMap<>();
ZipInputStream zipInput = new ZipInputStream(stream);
ZipEntry entry;
while ((entry = zipInput.getNextEntry()) != null) {
byte[] data = new byte[(int) entry.getSize()];
// For some reason, using read(byte[]) makes reading stall upon reaching a 0x1E byte
int i = 0;
while (zipInput.available() > 0 && i < data.length) {
data[i++] = (byte) zipInput.read();
}
zipContents.put(entry.getName(), data);
}
byte[] modelXml = zipContents.get("model.xml");
if (modelXml == null) {
throw new RenderException("Model " + this.name + " contains no model.xml file");
}
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(new ByteArrayInputStream(modelXml));
NodeList nodeListTechne = document.getElementsByTagName("Techne");
if (nodeListTechne.getLength() < 1) {
throw new RenderException("Model " + this.name + " contains no Techne tag");
}
NodeList nodeListModel = document.getElementsByTagName("Model");
if (nodeListModel.getLength() < 1) {
throw new RenderException("Model " + this.name + " contains no Model tag");
}
NamedNodeMap modelAttributes = nodeListModel.item(0).getAttributes();
if (modelAttributes == null) {
throw new RenderException("Model " + this.name + " contains a Model tag with no attributes");
}
NodeList textureSize = document.getElementsByTagName("TextureSize");
if (textureSize.getLength() == 0)
throw new RenderException("Model has no texture size");
String[] textureDimensions = textureSize.item(0).getTextContent().split(",");
double textureWidth = Integer.parseInt(textureDimensions[0]);
double textureHeight = Integer.parseInt(textureDimensions[1]);
NodeList shapes = document.getElementsByTagName("Shape");
for (int i = 0; i < shapes.getLength(); i++) {
Node shape = shapes.item(i);
NamedNodeMap shapeAttributes = shape.getAttributes();
if (shapeAttributes == null) {
throw new RenderException("Shape #" + (i + 1) + " in " + this.name + " has no attributes");
}
Node name = shapeAttributes.getNamedItem("name");
String shapeName = null;
if (name != null) {
shapeName = name.getNodeValue();
}
if (shapeName == null) {
shapeName = "Shape #" + (i + 1);
}
String shapeType = null;
Node type = shapeAttributes.getNamedItem("type");
if (type != null) {
shapeType = type.getNodeValue();
}
if (shapeType != null && !cubeIDs.contains(shapeType)) {
System.out.println("Model shape [" + shapeName + "] in " + this.name + " is not a cube, ignoring");
continue;
}
boolean mirrored = false;
String[] offset = new String[3];
String[] position = new String[3];
String[] rotation = new String[3];
String[] size = new String[3];
String[] textureOffset = new String[2];
NodeList shapeChildren = shape.getChildNodes();
for (int j = 0; j < shapeChildren.getLength(); j++) {
Node shapeChild = shapeChildren.item(j);
String shapeChildName = shapeChild.getNodeName();
String shapeChildValue = shapeChild.getTextContent();
if (shapeChildValue != null) {
shapeChildValue = shapeChildValue.trim();
switch (shapeChildName) {
case "IsMirrored":
mirrored = !shapeChildValue.equals("False");
break;
case "Offset":
offset = shapeChildValue.split(",");
break;
case "Position":
position = shapeChildValue.split(",");
break;
case "Rotation":
rotation = shapeChildValue.split(",");
break;
case "Size":
size = shapeChildValue.split(",");
break;
case "TextureOffset":
textureOffset = shapeChildValue.split(",");
break;
}
}
}
/*
Generate new models
Models in Techne are based on cubes.
Each cube is, by default, skewed to the side. They are not centered.
Everything is scaled by a factor of 16.
The y coordinate is inversed, y = 24 is the surface
The z coordinate is inverted, too.
*/
double positionX = Double.parseDouble(position[0]) / 16d;
double positionY = (16 - Double.parseDouble(position[1])) / 16d;
double positionZ = -Double.parseDouble(position[2]) / 16d;
double sizeX = Double.parseDouble(size[0]) / 16d;
double sizeY = Double.parseDouble(size[1]) / 16d;
double sizeZ = Double.parseDouble(size[2]) / 16d;
double offsetX = Double.parseDouble(offset[0]) / 16d;
double offsetY = -Double.parseDouble(offset[1]) / 16d;
double offsetZ = -Double.parseDouble(offset[2]) / 16d;
double angleX = -Math.toRadians(Double.parseDouble(rotation[0]));
double angleY = Math.toRadians(Double.parseDouble(rotation[1]));
double angleZ = Math.toRadians(Double.parseDouble(rotation[2]));
double textureOffsetU = Double.parseDouble(textureOffset[0]);
double textureOffsetV = Double.parseDouble(textureOffset[1]);
CubeTextureCoordinates textureCoordinates = new TechneCubeTextureCoordinates(
textureWidth, textureHeight,
textureOffsetU, textureOffsetV,
sizeX, sizeY, sizeZ);
final String modelName = shapeName;
MeshModel modelPart = new MeshModel(modelName);
BlockRenderPipeline.drawCube(
modelPart,
offsetX,
offsetY - sizeY,
offsetZ - sizeZ,
offsetX + sizeX,
offsetY,
offsetZ,
textureCoordinates);
MatrixStack ms = new MatrixStack();
ms.translate(positionX, positionY, positionZ);
ms.rotate(Vector3D.PLUS_J, angleY);
ms.rotate(Vector3D.PLUS_I, angleX);
ms.rotate(Vector3D.PLUS_K, angleZ);
modelPart.matrix = ms;
modelPart.textureOffset = new Vector2D(Integer.parseInt(textureOffset[0]), Integer.parseInt(textureOffset[1]));
if (model.children.stream().anyMatch(m -> m.name.equals(modelName))) {
throw new RenderException("Model contained duplicate part name: '" + shapeName + "' node #" + i);
}
model.children.add(modelPart);
}
} catch (ZipException e) {
throw new RenderException("Model " + this.name + " is not a valid zip file");
} catch (IOException e) {
throw new RenderException("Model " + this.name + " could not be read", e);
} catch (SAXException e) {
throw new RenderException("Model " + this.name + " contains invalid XML", e);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public MeshModel getModel() {
return model.clone();
}
@Override
public String getType() {
return "tcn";
}
private static class TechneCubeTextureCoordinates implements CubeTextureCoordinates {
private final double textureWidth;
private final double textureHeight;
private final double offsetU;
private final double offsetV;
private final double sizeX;
private final double sizeY;
private final double sizeZ;
private TechneCubeTextureCoordinates(
double textureWidth, double textureHeight,
double offsetU, double offsetV,
double sizeX, double sizeY, double sizeZ) {
this.textureWidth = textureWidth;
this.textureHeight = textureHeight;
this.offsetU = offsetU;
this.offsetV = offsetV;
this.sizeX = sizeX * 16;
this.sizeY = sizeY * 16;
this.sizeZ = sizeZ * 16;
}
private double translateU(double pixelsU) {
return (offsetU + pixelsU) / textureWidth;
}
private double translateV(double pixelsV) {
return (offsetV + pixelsV) / textureHeight;
}
@Override
public double getTopMinU() {
return translateU(sizeZ + sizeX);
}
@Override
public double getTopMinV() {
return translateV(sizeZ);
}
@Override
public double getTopMaxU() {
return translateU(sizeZ);
}
@Override
public double getTopMaxV() {
return translateV(0);
}
@Override
public double getBottomMinU() {
return translateU(sizeZ + 2 * sizeX);
}
@Override
public double getBottomMinV() {
return translateV(0);
}
@Override
public double getBottomMaxU() {
return translateU(sizeZ + sizeX);
}
@Override
public double getBottomMaxV() {
return translateV(sizeZ);
}
@Override
public double getWestMinU() {
return translateU(0);
}
@Override
public double getWestMinV() {
return translateV(sizeZ);
}
@Override
public double getWestMaxU() {
return translateU(sizeZ);
}
@Override
public double getWestMaxV() {
return translateV(sizeZ + sizeY);
}
@Override
public double getEastMinU() {
return translateU(sizeX + sizeZ * 2);
}
@Override
public double getEastMinV() {
return translateV(sizeZ);
}
@Override
public double getEastMaxU() {
return translateU(sizeX + sizeZ);
}
@Override
public double getEastMaxV() {
return translateV(sizeZ + sizeY);
}
@Override
public double getNorthMinU() {
return translateU(sizeX + 2 * sizeZ);
}
@Override
public double getNorthMinV() {
return translateV(sizeZ);
}
@Override
public double getNorthMaxU() {
return translateU(2 * sizeX + 2 * sizeZ);
}
@Override
public double getNorthMaxV() {
return translateV(sizeZ + sizeY);
}
@Override
public double getSouthMinU() {
return translateU(sizeZ);
}
@Override
public double getSouthMinV() {
return translateV(sizeZ);
}
@Override
public double getSouthMaxU() {
return translateU(sizeX + sizeZ);
}
@Override
public double getSouthMaxV() {
return translateV(sizeZ + sizeY);
}
}
}