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); } } }