package codechicken.lib.render;
import codechicken.lib.render.uv.UV;
import codechicken.lib.render.uv.UVScale;
import codechicken.lib.vec.*;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.ResourceLocation;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
public class QBImporter {
private static class ImagePackNode {
Rectangle4i rect;
ImagePackNode child1;
ImagePackNode child2;
QBImage packed;
public ImagePackNode(int x, int y, int w, int h) {
rect = new Rectangle4i(x, y, w, h);
}
public boolean pack(QBImage img) {
if (child1 != null) {
return child1.pack(img) || child2.pack(img);
}
if (packed != null) {
return false;
}
int fit = getFit(img.width(), img.height());
if (fit == 0) {
return false;
}
if ((fit & 2) != 0) {//exact fit
packed = img;
img.packSlot = rect;
img.packT = new ImageTransform((fit & 1) << 2);
return true;
}
int w = (fit & 1) == 0 ? img.width() : img.height();
int h = (fit & 1) == 0 ? img.height() : img.width();
if (rect.w - w > rect.h - h) {//create split with biggest leftover space
child1 = new ImagePackNode(rect.x, rect.y, w, rect.h);
child2 = new ImagePackNode(rect.x + w, rect.y, rect.w - w, rect.h);
} else {
child1 = new ImagePackNode(rect.x, rect.y, rect.w, h);
child2 = new ImagePackNode(rect.x, rect.y + h, rect.w, rect.h - h);
}
return child1.pack(img);
}
private int getFit(int w, int h) {
if (w == rect.w && h == rect.h) {
return 2;
}
if (w == rect.h && h == rect.w) {
return 3;
}
if (rect.w >= w && rect.h >= h) {
return 4;
}
if (rect.w >= h && rect.h >= w) {
return 5;
}
return 0;
}
private static void nextSize(Rectangle4i rect, boolean square) {
if (square) {
rect.w <<= 1;
rect.h <<= 1;
} else {
if (rect.w == rect.h) {
rect.w *= 2;
} else {
rect.h *= 2;
}
}
}
public static ImagePackNode pack(List<QBImage> images, boolean square) {
Collections.sort(images);
int area = 0;
for (QBImage img : images) {
area += img.area();
}
ImagePackNode node = new ImagePackNode(0, 0, 2, 2);
while (node.rect.area() < area) {
nextSize(node.rect, square);
}
while (true) {
boolean packed = true;
for (QBImage img : images) {
if (!node.pack(img)) {
packed = false;
break;
}
}
if (packed) {
return node;
}
node.child1 = node.child2 = null;
nextSize(node.rect, square);
}
}
public BufferedImage toImage() {
BufferedImage img = new BufferedImage(rect.w, rect.h, BufferedImage.TYPE_INT_ARGB);
write(img);
return img;
}
private void write(BufferedImage img) {
if (child1 != null) {
child1.write(img);
child2.write(img);
} else if (packed != null) {
ImageTransform t = packed.packT;
for (int u = 0; u < rect.w; u++) {
for (int v = 0; v < rect.h; v++) {
int rgba = t.access(packed, u, v);
img.setRGB(u + rect.x, v + rect.y, rgba >>> 8 | rgba << 24);
}
}
}
}
}
private static class ImageTransform {
int transform;
public ImageTransform(int i) {
transform = i;
}
public ImageTransform() {
this(0);
}
public boolean transpose() {
return (transform & 4) != 0;
}
public boolean flipU() {
return (transform & 1) != 0;
}
public boolean flipV() {
return (transform & 2) != 0;
}
public int access(QBImage img, int u, int v) {
if (transpose()) {
int tmp = u;
u = v;
v = tmp;
}
if (flipU()) {
u = img.width() - 1 - u;
}
if (flipV()) {
v = img.height() - 1 - v;
}
return img.data[u][v];
}
public UV transform(UV uv) {
if (transpose()) {
double tmp = uv.u;
uv.u = uv.v;
uv.v = tmp;
}
if (flipU()) {
uv.u = 1 - uv.u;
}
if (flipV()) {
uv.v = 1 - uv.v;
}
return uv;
}
}
public static class QBImage implements Comparable<QBImage> {
int[][] data;
ImageTransform packT;
Rectangle4i packSlot;
public int width() {
return data.length;
}
public int height() {
return data[0].length;
}
public int area() {
return width() * height();
}
@Override
public int compareTo(QBImage o) {
int a = area();
int b = o.area();
return a > b ? -1 : a == b ? 0 : 1;
}
public ImageTransform transformTo(QBImage img) {
if (width() == img.width() && height() == img.height()) {
for (int i = 0; i < 4; i++) {
ImageTransform t = new ImageTransform(i);
if (equals(img, t)) {
return t;
}
}
}
if (width() == img.height() && height() == img.width()) {
for (int i = 4; i < 8; i++) {
ImageTransform t = new ImageTransform(i);
if (equals(img, t)) {
return t;
}
}
}
return null;
}
public boolean equals(QBImage img, ImageTransform t) {
for (int u = 0; u < img.width(); u++) {
for (int v = 0; v < img.height(); v++) {
if (t.access(this, u, v) != img.data[u][v]) {
return false;
}
}
}
return true;
}
public void transform(UV uv) {
packT.transform(uv);
uv.u *= packSlot.w;
uv.v *= packSlot.h;
uv.u += packSlot.x;
uv.v += packSlot.y;
}
}
private static final int[][] vertOrder = new int[][] {//clockwise because MC is left handed
{ 3, 0 }, { 1, 0 }, { 1, 2 }, { 3, 2 } };
public static class QBQuad {
public Vertex5[] verts = new Vertex5[4];
public QBImage image = new QBImage();
public ImageTransform t = new ImageTransform();
public int side;
public QBQuad(int side) {
this.side = side;
}
public void applyImageT() {
for (Vertex5 vert : verts) {
t.transform(vert.uv);
image.transform(vert.uv);
}
}
public static QBQuad restore(Rectangle4i flat, int side, double d, QBImage img) {
QBQuad quad = new QBQuad(side);
quad.image = img;
Transformation t = new Scale(-1, 1, -1).with(Rotation.sideOrientation(side, 0)).with(new Translation(new Vector3().setSide(side, d)));
quad.verts[0] = new Vertex5(flat.x, 0, flat.y, 0, 0);
quad.verts[1] = new Vertex5(flat.x + flat.w, 0, flat.y, 1, 0);
quad.verts[2] = new Vertex5(flat.x + flat.w, 0, flat.y + flat.h, 1, 1);
quad.verts[3] = new Vertex5(flat.x, 0, flat.y + flat.h, 0, 1);
for (Vertex5 vert : quad.verts) {
vert.apply(t);
}
return quad;
}
public Rectangle4i flatten() {
Transformation t = Rotation.sideOrientation(side, 0).inverse().with(new Scale(-1, 0, -1));
Vector3 vmin = verts[0].vec.copy().apply(t);
Vector3 vmax = verts[2].vec.copy().apply(t);
return new Rectangle4i((int) vmin.x, (int) vmin.z, (int) (vmax.x - vmin.x), (int) (vmax.z - vmin.z));
}
}
public static class QBCuboid {
public QBMatrix mat;
public CuboidCoord c;
public int sides;
public QBCuboid(QBMatrix mat, CuboidCoord c) {
this.mat = mat;
this.c = c;
sides = 0;
}
public static boolean intersects(QBCuboid a, QBCuboid b) {
CuboidCoord c = a.c;
CuboidCoord d = b.c;
return c.min.x <= d.max.x &&
d.min.x <= c.max.x &&
c.min.y <= d.max.y &&
d.min.y <= c.max.y &&
c.min.z <= d.max.z &&
d.min.z <= c.max.z;
}
public static void clip(QBCuboid a, QBCuboid b) {
if (intersects(a, b)) {
a.clip(b);
b.clip(a);
}
}
public void clip(QBCuboid o) {
CuboidCoord d = o.c;
for (int a = 0; a < 6; a += 2) {
int a1 = (a + 2) % 6;
int a2 = (a + 4) % 6;
if (c.getSide(a1 + 1) <= d.getSide(a1 + 1) &&
c.getSide(a1) >= d.getSide(a1) &&
c.getSide(a2 + 1) <= d.getSide(a2 + 1) &&
c.getSide(a2) >= d.getSide(a2)) {
if (c.getSide(a) <= d.getSide(a + 1) && c.getSide(a) >= d.getSide(a)) {
c.setSide(a, d.getSide(a + 1) + 1);
sides |= 1 << a;
}
if (c.getSide(a + 1) >= d.getSide(a) && c.getSide(a + 1) <= d.getSide(a + 1)) {
c.setSide(a + 1, d.getSide(a) - 1);
sides |= 2 << a;
}
}
}
}
public void extractQuads(List<QBQuad> quads) {
Cuboid6 box = c.bounds();
for (int s = 0; s < 6; s++) {
if ((sides & 1 << s) == 0) {
quads.add(extractQuad(s, box));
}
}
}
private QBQuad extractQuad(int side, Cuboid6 box) {
double[] da = new double[3];
da[side >> 1] = box.getSide(side);
QBQuad quad = new QBQuad(side);
for (int i = 0; i < 4; i++) {
int rU = vertOrder[i][0];
int rV = vertOrder[i][1];
int sideU = Rotation.rotateSide(side, rU);
int sideV = Rotation.rotateSide(side, rV);
da[sideU >> 1] = box.getSide(sideU);
da[sideV >> 1] = box.getSide(sideV);
quad.verts[i] = new Vertex5(Vector3.fromAxes(da), (3 - rU) / 2, rV / 2);
}
int sideU = Rotation.rotateSide(side, 1);
int sideV = Rotation.rotateSide(side, 2);
quad.image.data = new int[c.size(sideU)][c.size(sideV)];
QBImage image = quad.image;
int[] ia = new int[3];
ia[side >> 1] = c.getSide(side);
ia[sideU >> 1] = c.getSide(sideU ^ 1);
ia[sideV >> 1] = c.getSide(sideV ^ 1);
BlockCoord b = BlockCoord.fromAxes(ia);
BlockCoord bU = BlockCoord.sideOffsets[sideU];
BlockCoord bV = BlockCoord.sideOffsets[sideV];
for (int u = 0; u < image.width(); u++) {
for (int v = 0; v < image.height(); v++) {
image.data[u][v] = mat.matrix[b.x + bU.x * u + bV.x * v][b.y + bU.y * u + bV.y * v][b.z + bU.z * u + bV.z * v];
}
}
return quad;
}
}
public static class QBMatrix {
public String name;
public BlockCoord pos;
public BlockCoord size;
public int[][][] matrix;
public void readMatrix(DataInputStream din, boolean compressed) throws IOException {
if (compressed) {
int z = 0;
while (z < size.z) {
int index = 0;
while (true) {
int data = din.readInt();
if (data == NEXTSLICEFLAG) {
break;
}
if (data == CODEFLAG) {
int count = readTni(din);
data = din.readInt();
for (int j = 0; j < count; j++, index++) {
matrix[index % size.x][index / size.x][z] = data;
}
} else {
matrix[index % size.x][index / size.x][z] = data;
index++;
}
}
z++;
}
} else {
for (int z = 0; z < size.z; z++) {
for (int y = 0; y < size.y; y++) {
for (int x = 0; x < size.x; x++) {
matrix[x][y][z] = din.readInt();
}
}
}
}
}
public void convertBGRAtoRGBA() {
for (int z = 0; z < size.z; z++) {
for (int y = 0; y < size.y; y++) {
for (int x = 0; x < size.x; x++) {
int i = matrix[x][y][z];
matrix[x][y][z] = Integer.reverseBytes(i >>> 8) | i & 0xFF;
}
}
}
}
private boolean voxelFull(boolean[][][] solid, CuboidCoord c) {
for (BlockCoord b : c) {
if (matrix[b.x][b.y][b.z] == 0) {
return false;
}
}
for (BlockCoord b : c) {
solid[b.x][b.y][b.z] = false;
}
return true;
}
private QBCuboid expand(boolean[][][] solid, BlockCoord b) {
CuboidCoord c = new CuboidCoord(b);
solid[b.x][b.y][b.z] = false;
for (int s = 0; s < 6; s++) {
CuboidCoord slice = c.copy();
slice.expand(s ^ 1, -(slice.size(s) - 1));
slice.expand(s, 1);
while (slice.getSide(s) >= 0 && slice.getSide(s) < size.getSide(s)) {
if (!voxelFull(solid, slice)) {
break;
}
slice.expand(s ^ 1, -1);
slice.expand(s, 1);
c.expand(s, 1);
}
}
return new QBCuboid(this, c);
}
public List<QBCuboid> rectangulate() {
List<QBCuboid> list = new ArrayList<QBCuboid>();
boolean[][][] solid = new boolean[size.x][size.y][size.z];
for (int z = 0; z < size.z; z++) {
for (int y = 0; y < size.y; y++) {
for (int x = 0; x < size.x; x++) {
solid[x][y][z] = matrix[x][y][z] != 0;
}
}
}
for (int x = 0; x < size.x; x++) {
for (int z = 0; z < size.z; z++) {
for (int y = 0; y < size.y; y++) {
if (solid[x][y][z]) {
list.add(expand(solid, new BlockCoord(x, y, z)));
}
}
}
}
for (int i = 0; i < list.size(); i++) {
for (int j = i + 1; j < list.size(); j++) {
QBCuboid.clip(list.get(i), list.get(j));
}
}
return list;
}
public List<QBQuad> extractQuads(boolean texturePlanes) {
List<QBQuad> quads = new LinkedList<QBQuad>();
for (QBCuboid c : rectangulate()) {
c.extractQuads(quads);
}
if (texturePlanes) {
optimisePlanes(quads);
}
return quads;
}
private void optimisePlanes(List<QBQuad> quads) {
Multimap<Integer, QBQuad> map = HashMultimap.create();
for (QBQuad quad : quads) {
map.put(quad.side | ((int) quad.verts[0].vec.getSide(quad.side)) << 3, quad);
}
quads.clear();
for (Integer key : map.keySet()) {
Collection<QBQuad> plane = map.get(key);
if (plane.size() == 1) {
quads.add(plane.iterator().next());
continue;
}
int side = key & 7;
Rectangle4i rect = null;
for (QBQuad q : plane) {
if (rect == null) {
rect = q.flatten();
} else {
rect.include(q.flatten());
}
}
QBImage img = new QBImage();
img.data = new int[rect.w][rect.h];
for (QBQuad q : plane) {
QBImage from = q.image;
Rectangle4i r = q.flatten();
int du = r.x - rect.x;
int dv = r.y - rect.y;
for (int u = 0; u < from.width(); u++) {
for (int v = 0; v < from.height(); v++) {
img.data[du + u][dv + v] = from.data[u][v];
}
}
}
quads.add(QBQuad.restore(rect, side, key >> 3, img));
}
}
public CCModel buildModel(List<QBQuad> quads, BufferedImage img, boolean scaleMC) {
CCModel m = CCModel.quadModel(quads.size() * 4);
int i = 0;
for (QBQuad quad : quads) {
quad.applyImageT();
m.verts[i++] = quad.verts[0];
m.verts[i++] = quad.verts[1];
m.verts[i++] = quad.verts[2];
m.verts[i++] = quad.verts[3];
}
m.apply(new UVScale(1D / img.getWidth(), 1D / img.getHeight()));
m.apply(new Translation(pos.x, pos.y, pos.z));
if (scaleMC) {
m.apply(new Scale(1 / 16D));
}
m.computeNormals();
return m;
}
private static void addImages(List<QBQuad> quads, List<QBImage> images) {
for (QBQuad q : quads) {
QBImage img = q.image;
boolean matched = false;
for (QBImage img2 : images) {
ImageTransform t = img.transformTo(img2);
if (t != null) {
q.t = t;
q.image = img2;
matched = true;
break;
}
}
if (!matched) {
images.add(img);
}
}
}
}
public static final int TEXTUREPLANES = 1;
public static final int SQUARETEXTURE = 2;
public static final int MERGETEXTURES = 4;
public static final int SCALEMC = 8;
public static class QBModel {
public QBMatrix[] matrices;
public boolean rightHanded;
public RasterisedModel toRasterisedModel(int flags) {
List<QBImage> qbImages = new ArrayList<QBImage>();
List<List<QBQuad>> modelQuads = new ArrayList<List<QBQuad>>();
List<BufferedImage> images = new ArrayList<BufferedImage>();
boolean texturePlanes = (flags & TEXTUREPLANES) != 0;
boolean squareTextures = (flags & SQUARETEXTURE) != 0;
boolean mergeTextures = (flags & MERGETEXTURES) != 0;
boolean scaleMC = (flags & SCALEMC) != 0;
for (QBMatrix mat : matrices) {
List<QBQuad> quads = mat.extractQuads(texturePlanes);
modelQuads.add(quads);
QBMatrix.addImages(quads, qbImages);
if (!mergeTextures) {
images.add(ImagePackNode.pack(qbImages, squareTextures).toImage());
qbImages.clear();
}
}
if (mergeTextures) {
images.add(ImagePackNode.pack(qbImages, squareTextures).toImage());
}
RasterisedModel m = new RasterisedModel(images);
for (int i = 0; i < matrices.length; i++) {
QBMatrix mat = matrices[i];
BufferedImage img = images.get(mergeTextures ? 0 : i);
m.add(mat.name, mat.buildModel(modelQuads.get(i), img, scaleMC));
}
return m;
}
}
public static class RasterisedModel {
private class Holder {
CCModel m;
int img;
public Holder(CCModel m, int img) {
this.m = m;
this.img = img;
}
}
private Map<String, Holder> map = new HashMap<String, Holder>();
private List<BufferedImage> images;
public RasterisedModel(List<BufferedImage> images) {
this.images = images;
}
public void add(String name, CCModel m) {
map.put(name, new Holder(m, Math.min(map.size(), images.size() - 1)));
}
public CCModel getModel(String key) {
return map.get(key).m;
}
public TextureAtlasSprite getIcon(String key, TextureMap textureMap) {
int img = map.get(key).img;
String iconName = "QBModel" + hashCode() + "_img";
TextureAtlasSprite icon = textureMap.getTextureExtry(iconName);
if (icon != null) {
return icon;
}
return TextureUtils.getTextureSpecial(textureMap, iconName).addTexture(new TextureDataHolder(images.get(img)));
}
private void exportImg(BufferedImage img, File imgFile) throws IOException {
if (!imgFile.exists()) {
imgFile.createNewFile();
}
ImageIO.write(img, "PNG", imgFile);
}
public void export(File objFile, File imgDir) {
try {
if (!objFile.exists()) {
objFile.createNewFile();
}
if (!imgDir.exists()) {
imgDir.mkdirs();
}
Map<String, CCModel> modelMap = new HashMap<String, CCModel>();
for (Map.Entry<String, Holder> e : map.entrySet()) {
modelMap.put(e.getKey(), e.getValue().m);
}
PrintWriter p = new PrintWriter(objFile);
CCModel.exportObj(modelMap, p);
p.close();
if (images.size() < map.size()) {
exportImg(images.get(0), new File(imgDir, objFile.getName().replaceAll("(.+)\\..+", "$1.png")));
} else {
for (Map.Entry<String, Holder> e : map.entrySet()) {
exportImg(images.get(e.getValue().img), new File(imgDir, e.getKey() + ".png"));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private static String readAsciiString(DataInputStream din) throws IOException {
byte[] bytes = new byte[din.readByte() & 0xFF];
din.readFully(bytes);
return new String(bytes, "US-ASCII");
}
private static int readTni(DataInputStream din) throws IOException {
return Integer.reverseBytes(din.readInt());
}
private static final int CODEFLAG = Integer.reverseBytes(2);
private static final int NEXTSLICEFLAG = Integer.reverseBytes(6);
public static QBModel loadQB(InputStream input) throws IOException {
DataInputStream din = new DataInputStream(input);
QBModel m = new QBModel();
int version = din.readInt();
int colorFormat = din.readInt();
m.rightHanded = din.readInt() != 0;
boolean compressed = din.readInt() != 0;
boolean visEncoded = din.readInt() != 0;
if (visEncoded) {
throw new IllegalArgumentException("Encoded Visiblity States not supported");
}
m.matrices = new QBMatrix[readTni(din)];
for (int i = 0; i < m.matrices.length; i++) {
QBMatrix mat = new QBMatrix();
m.matrices[i] = mat;
mat.name = readAsciiString(din);
mat.size = new BlockCoord(readTni(din), readTni(din), readTni(din));
mat.pos = new BlockCoord(readTni(din), readTni(din), readTni(din));
mat.matrix = new int[mat.size.x][mat.size.y][mat.size.z];
mat.readMatrix(din, compressed);
if (colorFormat == 1) {
mat.convertBGRAtoRGBA();
}
}
return m;
}
public static QBModel loadQB(ResourceLocation res) {
try {
return loadQB(Minecraft.getMinecraft().getResourceManager().getResource(res).getInputStream());
} catch (Exception e) {
throw new RuntimeException("failed to load model: " + res, e);
}
}
public static QBModel loadQB(File file) {
try {
FileInputStream fin = new FileInputStream(file);
try {
return loadQB(fin);
} finally {
fin.close();
}
} catch (Exception e) {
throw new RuntimeException("failed to load model: " + file.getPath(), e);
}
}
}