package javaforce.gl;
import java.io.*;
import java.util.*;
import javaforce.*;
/**
* Blender .blend reader
*
* NOTE:
* Supports Blender v2.63+
* Supports objects with multiple UVMaps
* Rotation/Scale on objects are ignored, please rotate/scale in edit mode (the vertex data)
* BHead chunks can have duplicate old pointer addresses in which case they must be used in order.
* See : https://developer.blender.org/T45471
* TODO:
* Animation data
*
* Blender Source : https://git.blender.org/gitweb/gitweb.cgi/blender.git/tree/HEAD:/source/blender
* - look in blenloader and makesdna folders
* - most important to understand DNA : makesdna/intern/dna_genfile.c:init_structDNA()
* - also see doc/blender_file_format/mystery_of_the_blend.html
*
* @author pquiring
*/
public class GL_BLEND {
private byte data[];
private int datapos;
private boolean x64; //64bit file format (else 32bit)
private boolean le; //little endian file format (else big endian)
private GLModel model;
private GLObject obj;
private float org[] = new float[3];
private boolean haveDups;
private HashMap<Long, Chunk> chunks = new HashMap<Long, Chunk>();
private static final int ID_ME = 0x454d; //ME (mesh)
private static final int ID_OB = 0x424f; //OB (object)
private static final int ID_SC = 0x4353; //SCE (scene)
private static final int ID_DNA1 = 0x31414e44; //DNA1
// typedef enum CustomDataType {...}
private static final int CD_MVERT = 0;
private static final int CD_MSTICKY = 1; /* DEPRECATED */
private static final int CD_MDEFORMVERT = 2;
private static final int CD_MEDGE = 3;
private static final int CD_MFACE = 4;
private static final int CD_MTFACE = 5;
private static final int CD_MCOL = 6;
private static final int CD_ORIGINDEX = 7;
private static final int CD_NORMAL = 8;
/* private static final int CD_POLYINDEX = 9; */
private static final int CD_PROP_FLT = 10;
private static final int CD_PROP_INT = 11;
private static final int CD_PROP_STR = 12;
private static final int CD_ORIGSPACE = 13; /* for modifier stack face location mapping */
private static final int CD_ORCO = 14;
private static final int CD_MTEXPOLY = 15;
private static final int CD_MLOOPUV = 16;
private static final int CD_MLOOPCOL = 17;
private static final int CD_TANGENT = 18;
private static final int CD_MDISPS = 19;
private static final int CD_PREVIEW_MCOL = 20; /* for displaying weightpaint colors */
private static final int CD_ID_MCOL = 21;
private static final int CD_TEXTURE_MCOL = 22;
private static final int CD_CLOTH_ORCO = 23;
private static final int CD_RECAST = 24;
/* BMESH ONLY START */
private static final int CD_MPOLY = 25;
private static final int CD_MLOOP = 26;
private static final int CD_SHAPE_KEYINDEX = 27;
private static final int CD_SHAPEKEY = 28;
private static final int CD_BWEIGHT = 29;
private static final int CD_CREASE = 30;
private static final int CD_ORIGSPACE_MLOOP = 31;
private static final int CD_PREVIEW_MLOOPCOL = 32;
private static final int CD_BM_ELEM_PYPTR = 33;
/* BMESH ONLY END */
private static final int CD_PAINT_MASK = 34;
private static final int CD_GRID_PAINT_MASK = 35;
private static final int CD_MVERT_SKIN = 36;
private static final int CD_FREESTYLE_EDGE = 37;
private static final int CD_FREESTYLE_FACE = 38;
private static final int CD_MLOOPTANGENT = 39;
private static final int CD_TESSLOOPNORMAL = 40;
private static final int CD_CUSTOMLOOPNORMAL = 41;
private static final int CD_NUMTYPES = 42;
//DNA stuff
private ArrayList<String> names = new ArrayList<String>(); //member names
private ArrayList<String> types = new ArrayList<String>(); //struct names
private ArrayList<Short> typelen = new ArrayList<Short>();
private class struct {
short typeidx; //index into types
short nr; //# of members
String name;
ArrayList<member> members = new ArrayList<member>();
}
private class member {
short typelenidx; //index into typelen
short nameidx; //index into names
String name;
int typelen;
int size; //total size of member
}
private ArrayList<struct> structs = new ArrayList<struct>();
private struct getStruct(String name) throws Exception {
for(int a=0;a<structs.size();a++) {
struct s = structs.get(a);
if (s.name.equals(name)) return s;
}
throw new Exception("struct not found:" + name);
}
private int calcMemberSize(member m) {
if (m.name.startsWith("*")) {
if (x64) return 8;
return 4;
}
if (m.name.indexOf("[") != -1) {
//array type
String f[] = m.name.replaceAll("\\]", "").split("\\[");
if (f.length == 2) {
//single array
return m.typelen * Integer.valueOf(f[1]);
} else {
//double array
return m.typelen * Integer.valueOf(f[1]) * Integer.valueOf(f[2]);
}
}
return m.typelen;
}
public GLModel load(String filename) {
try {
return loadBlend(new FileInputStream(filename));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public GLModel load(InputStream is) {
try {
return loadBlend(is);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private boolean eof() {
return datapos >= data.length;
}
private byte readuint8() {
byte uint8 = data[datapos++];
return uint8;
}
private short readuint16() {
int uint16;
if (le) uint16 = LE.getuint16(data, datapos);
else uint16 = BE.getuint16(data, datapos);
datapos += 2;
return (short)uint16;
}
private int readuint32() {
int uint32;
if (le) uint32 = LE.getuint32(data, datapos);
else uint32 = BE.getuint32(data, datapos);
datapos += 4;
return uint32;
}
private long readuint64() {
long uint64;
if (le) uint64 = LE.getuint64(data, datapos);
else uint64 = BE.getuint64(data, datapos);
datapos += 8;
return uint64;
}
private long readptr() {
if (x64) return readuint64();
return readuint32();
}
private void readByteArray(byte in[]) {
System.arraycopy(data, datapos, in, 0, in.length);
datapos+=in.length;
}
private void readPtrArray(long in[]) {
for(int a=0;a<in.length;a++) {
in[a] = readptr();
}
}
private void readFloatArray(float in[]) {
for(int a=0;a<in.length;a++) {
in[a] = readfloat();
}
}
private float readfloat() {
return Float.intBitsToFloat(readuint32());
}
/* Read fixed size char[] string */
private String readString(int len) {
int sl = strlen(data, datapos, len);
String str = new String(data, datapos, sl);
datapos+=len;
return str;
}
/* Read C style string (NULL terminated) */
private String readString() {
int sl = strlen(data, datapos, data.length - datapos);
String str = new String(data, datapos, sl);
datapos += sl+1;
return str;
}
private void setData(byte in[]) {
data = in;
datapos = 0;
}
private class Context {
byte data[];
int datapos;
}
private Context pushData() {
Context ctx = new Context();
ctx.data = data;
ctx.datapos = datapos;
return ctx;
}
private void popData(Context ctx) {
data = ctx.data;
datapos = ctx.datapos;
}
private int strlen(byte str[], int offset, int max) {
for(int a=0;a<max;a++) {
if (str[a+offset] == 0) return a;
}
return max;
}
private Chunk findChunkByPtr(long ptr) {
if (ptr == 0) return null;
Chunk chunk = chunks.get(ptr);
if (chunk == null) return null;
if (chunk.dup) {
// JFLog.log("Duplicate:" + Long.toString(ptr, 16) + ",idx=" + chunk.dupidx);
int cnt = chunk.dupidx;
chunk.dupidx++;
for(int a=0;a<cnt;a++) {
chunk = chunk.nextdup;
}
}
return chunk;
}
private class Vertex {
float xyz[];
}
private GLModel loadBlend(InputStream is) throws Exception {
setData(JF.readAll(is));
if (data.length < 12) {
throw new Exception("GL_BLEND:File too small");
}
model = new GLModel();
//load signature (12 bytes) "BLENDER_V100"
if (!new String(data, 0, 7).equals("BLENDER")) {
throw new Exception("Not a blender file");
}
switch (data[7]) {
case '-': x64 = true; break;
case '_': x64 = false; break;
default:
throw new Exception("GL_BLEND:Unknown bit size");
}
switch (data[8]) {
case 'v': le = true; break;
case 'V': le = false; break;
default:
throw new Exception("GL_BLEND:Unknown Endianness");
}
String version = new String(data, 9, 3);
// JFLog.log("Blender file version:" + version);
int ver = Integer.valueOf(version);
if (ver < 263) {
throw new Exception("Error:Blender file too old, can not read.");
}
datapos = 12; //skip main header
//first phase - read raw chunks
while (!eof()) {
Chunk chunk = new Chunk();
chunk.filepos = datapos;
chunk.read();
Chunk ochunk = chunks.get(chunk.ptr);
if (ochunk != null) {
if (!haveDups) {
JFLog.log("Warning:This file contains duplicate BHeads.");
haveDups = true;
}
ochunk.dup = true;
while (ochunk.nextdup != null) {
ochunk = ochunk.nextdup;
}
ochunk.nextdup = chunk;
} else {
chunks.put(chunk.ptr, chunk);
}
}
int chunkCnt = chunks.size();
Chunk chunkArray[] = chunks.values().toArray(new Chunk[chunkCnt]);
Chunk raw;
//2nd phase - parse DNA chunk
for(int i=0;i<chunkCnt;i++) {
if (chunkArray[i].id == ID_DNA1) {
raw = chunkArray[i];
setData(raw.raw);
//SDNA
String SDNA = readString(4);
if (!SDNA.equals("SDNA")) throw new Exception("Bad DNA Struct:SDNA");
//NAME
String NAME = readString(4);
if (!NAME.equals("NAME")) throw new Exception("Bad DNA Struct:NAME");
int nr_names = readuint32();
for(int a=0;a<nr_names;a++) {
String str = readString();
// JFLog.log("name=" + str);
names.add(str);
}
//align pointer
datapos += 3;
datapos &= 0xfffffffc;
//TYPE
String TYPE = readString(4);
if (!TYPE.equals("TYPE")) throw new Exception("Bad DNA Struct:TYPE");
int nr_types = readuint32();
for(int a=0;a<nr_types;a++) {
String str = readString();
// JFLog.log("type=" + str);
types.add(str);
}
//align pointer
datapos += 3;
datapos &= 0xfffffffc;
//TLEN
String TLEN = readString(4);
if (!TLEN.equals("TLEN")) throw new Exception("Bad DNA Struct:TLEN");
for(int a=0;a<nr_types;a++) {
typelen.add(readuint16());
}
//align pointer
datapos += 3;
datapos &= 0xfffffffc;
//STRC
String STRC = readString(4);
if (!STRC.equals("STRC")) throw new Exception("Bad DNA Struct:STRC");
int nr_structs = readuint32();
for(int a=0;a<nr_structs;a++) {
struct s = new struct();
s.typeidx = readuint16();
s.nr = readuint16();
s.name = types.get(s.typeidx);
// JFLog.log("struct:" + s.name + "==" + a);
for(int b=0;b<s.nr;b++) {
member m = new member();
m.typelenidx = readuint16();
m.nameidx = readuint16();
m.name = names.get(m.nameidx);
m.typelen = typelen.get(m.typelenidx);
m.size = calcMemberSize(m);
// JFLog.log(" member:" + m.name + "=" + m.length);
s.members.add(m);
}
structs.add(s);
}
break;
}
}
//3nd phase - now look for objects and piece together chunks
for(int i=0;i<chunkCnt;i++) {
if (chunkArray[i].id == ID_SC) {
setData(chunkArray[i].raw);
Scene scene = new Scene();
scene.read();
long ptr = scene.last;
while (ptr != 0) {
Chunk chunk = findChunkByPtr(ptr);
if (chunk == null) break;
setData(chunk.raw);
Base base = new Base();
base.read();
chunk = findChunkByPtr(base.object);
if (chunk.id == ID_OB) {
readObject(chunk);
}
ptr = base.prev;
}
}
}
return model;
}
private void readObject(Chunk chunk) throws Exception {
ArrayList<Vertex> vertexList = new ArrayList<Vertex>();
ArrayList<Integer> loopList = new ArrayList<Integer>();
setData(chunk.raw);
bObject bObj = new bObject();
bObj.read();
// JFLog.log("object.type=" + bObj.type);
if (bObj.type != 1) return; //not a mesh object (could be camera, light, etc.)
obj = new GLObject();
model.addObject(obj);
obj.name = bObj.id.name.substring(2);
// JFLog.log("object=" + obj.name);
chunk = findChunkByPtr(bObj.data);
if (chunk == null) {
throw new Exception("GL_BLEND:Unable to find Mesh for Object");
}
Mesh mesh = new Mesh();
setData(chunk.raw);
// JFLog.log("Mesh@" + Integer.toString(raw.fileOffset, 16));
mesh.read();
obj.org.x = bObj.loc[0];
org[0] = bObj.loc[0];
obj.org.y = bObj.loc[1];
org[1] = bObj.loc[1];
obj.org.z = bObj.loc[2];
org[2] = bObj.loc[2];
//find mvert
chunk = findChunkByPtr(mesh.mvert);
if (chunk == null) {
throw new Exception("GL_BLEND:Unable to find MVert for Mesh");
}
setData(chunk.raw);
for(int a=0;a<chunk.nr;a++) {
MVert mvert = new MVert();
mvert.read();
// obj.addVertex(mvert.co);
Vertex v = new Vertex();
v.xyz = mvert.v;
vertexList.add(v);
}
//find mloop
chunk = findChunkByPtr(mesh.mloop);
if (chunk == null) {
throw new Exception("GL_BLEND:Unable to find MLoop for Mesh");
}
setData(chunk.raw);
for(int a=0;a<chunk.nr;a++) {
MLoop mloop = new MLoop();
mloop.read();
loopList.add(mloop.v);
}
//find mloopuv
/* //use the UVMaps in the CustomData instead - this is only the active one
raw = findChunkByPtr(mesh.mloopuv);
if (raw == null) {
throw new Exception("GL_BLEND:Unable to find MLoopUV for Mesh");
}
setData(raw.raw);
JFLog.log("MLoopUV:nr=" + raw.nr);
for(int a=0;a<raw.nr;a++) {
MLoopUV mloopuv = new MLoopUV();
mloopuv.read();
}
*/
//find mpoly
chunk = findChunkByPtr(mesh.mpoly);
if (chunk == null) {
throw new Exception("GL_BLEND:Unable to find MPoly for Mesh");
}
setData(chunk.raw);
//TODO : calc which vertex needed to be dup'ed for each unique uv value (Blender does this in their 3ds export script)
int type = -1;
int pcnt = -1;
int vidx = 0;
//MPoly = faces
for(int a=0;a<chunk.nr;a++) {
MPoly mpoly = new MPoly();
mpoly.read();
switch (mpoly.totloop) {
case 3:
if (type == GL.GL_QUADS) {
throw new Exception("GL_BLEND:Mixed QUADS/TRIANGLES not supported");
}
type = GL.GL_TRIANGLES;
pcnt = 3;
break;
case 4:
if (type == GL.GL_TRIANGLES) {
throw new Exception("GL_BLEND:Mixed QUADS/TRIANGLES not supported");
}
type = GL.GL_QUADS;
pcnt = 4;
break;
default:
throw new Exception("GL_BLEND:Polygon not supported:nr=" + mpoly.totloop);
}
int loopidx = mpoly.loopstart;
for(int p=0;p<pcnt;p++) {
int idx = loopList.get(loopidx++);
obj.addVertex(vertexList.get(idx).xyz);
obj.addPoly(new int[] {vidx++});
}
}
obj.type = type;
//find customdata types
readLayer(mesh.vdata.layers, "vdata");
readLayer(mesh.edata.layers, "edata");
readLayer(mesh.fdata.layers, "fdata");
readLayer(mesh.pdata.layers, "pdata");
readLayer(mesh.ldata.layers, "ldata");
}
private void readLayer(long layers, String name) throws Exception {
if (layers == 0) return;
// JFLog.log(name + ".layers=" + Long.toString(layers, 16));
Chunk raw = findChunkByPtr(layers);
if (raw == null) {
throw new Exception("GL_BLEND:Unable to find " + name + ".layers for Mesh");
}
setData(raw.raw);
// JFLog.log("#layers=" + raw.nr);
for(int a=0;a<raw.nr;a++) {
CustomDataLayer layer = new CustomDataLayer();
layer.read();
String layer_name = layer.name;
if (layer.data == 0) {
// JFLog.log("layer.data == null");
continue;
}
Chunk layer_data = findChunkByPtr(layer.data);
if (layer_data == null) {
throw new Exception("GL_BLEND:Unable to find " + name + ".layers.data for Mesh");
}
Context ctx = pushData();
setData(layer_data.raw);
// JFLog.log("layer.data=" + Long.toString(layer.data, 16) + ",type==" + layer.type + ",a=" + a);
switch (layer.type) {
case CD_MTEXPOLY: { //15
//NOTE:There is a MTexPoly per face, I only read the first
MTexPoly tex = new MTexPoly();
tex.read();
Chunk imageChunk = findChunkByPtr(tex.tpage);
if (imageChunk == null) {
throw new Exception("GL_BLEND:No texture found for UVMap:" + a);
}
setData(imageChunk.raw);
Image image = new Image();
image.read();
GLUVMap map;
if (a < obj.getUVMaps())
map = obj.getUVMap(a);
else
map = obj.createUVMap();
String tn = image.name;
//string texture path for now
int tnidx = tn.lastIndexOf("/");
if (tnidx != -1) {
tn = tn.substring(tnidx+1);
}
tnidx = tn.lastIndexOf("\\");
if (tnidx != -1) {
tn = tn.substring(tnidx+1);
}
int tidx = model.addTexture(tn);
map.textureIndex = tidx;
map.name = layer_name;
// JFLog.log("texpoly=" + map.name);
break;
}
case CD_MLOOPUV: { //16
//There is a UV per face per vertex
if (a >= obj.getUVMaps()) {
obj.createUVMap();
}
// JFLog.log("loopuv.nr=" + layer_data.nr);
for(int b=0;b<layer_data.nr;b++) {
MLoopUV uv = new MLoopUV();
uv.read();
uv.uv[1] = 1.0f - uv.uv[1]; //invert V(y)
obj.addText(uv.uv, a);
}
break;
}
}
popData(ctx);
}
}
private class Chunk {
//BHead
int id;
int len;
long ptr; //the actual memory address of this chunk when it was saved to disk !!!
int SDNAnr;
int nr; //array count of struct
byte raw[];
int filepos; //for debugging
boolean dup;
int dupidx;
Chunk nextdup;
int fileOffset;
void read() {
id = readuint32();
len = readuint32();
ptr = readptr();
SDNAnr = readuint32();
nr = readuint32();
fileOffset = datapos;
if (len == 0) return;
raw = new byte[len];
readByteArray(raw);
}
}
private class ID {
String name;
void read() throws Exception {
struct s = getStruct("ID");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("name[66]")) {
name = readString(m.size);
}
else {
datapos += m.size;
}
}
}
}
private class Scene {
long first; //first Base
long last;
void read() throws Exception {
struct s = getStruct("Scene");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("base")) {
first = readptr();
last = readptr();
}
else {
datapos += m.size;
}
}
}
}
private class Base {
long next;
long prev;
long object;
void read() throws Exception {
struct s = getStruct("Base");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("*next")) {
next = readptr();
}
else if (m.name.equals("*prev")) {
prev = readptr();
}
else if (m.name.equals("*object")) {
object = readptr();
}
else {
datapos += m.size;
}
}
}
}
private class bObject {
ID id = new ID();
int type;
long data; // -> Mesh
float loc[] = new float[3]; //Location (aka Origin)
void read() throws Exception {
struct s = getStruct("Object");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("*data")) {
data = readptr();
}
else if (m.name.equals("loc[3]")) {
readFloatArray(loc);
}
else if (m.name.equals("type")) {
type = readuint16();
}
else if (m.name.equals("id")) {
id.read();
}
else {
datapos += m.size;
}
}
}
}
private class CustomData {
long layers; //->CustomDataLayer
void read(String name) throws Exception {
struct s = getStruct("CustomData");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("*layers")) {
layers = readptr();
}
else {
datapos += m.size;
}
}
}
}
private class CustomDataLayer {
int type; /* type of data in layer */
String name; /* layer name, MAX_CUSTOMDATA_LAYER_NAME */
long data; /* layer data */
void read() throws Exception {
struct s = getStruct("CustomDataLayer");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("type")) {
type = readuint32();
}
else if (m.name.equals("name[64]")) {
name = readString(m.size);
}
else if (m.name.equals("*data")) {
data = readptr();
}
else {
datapos += m.size;
}
}
}
}
private class Mesh {
ID id = new ID();
long mpoly, mloop, mloopuv, mvert;
CustomData vdata = new CustomData();
CustomData edata = new CustomData();
CustomData fdata = new CustomData();
CustomData pdata = new CustomData();
CustomData ldata = new CustomData();
void read() throws Exception {
struct s = getStruct("Mesh");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("*mpoly")) {
mpoly = readptr();
}
else if (m.name.equals("*mloop")) {
mloop = readptr();
}
else if (m.name.equals("*mloopuv")) {
mloopuv = readptr();
}
else if (m.name.equals("*mvert")) {
mvert = readptr();
}
else if (m.name.equals("vdata")) {
vdata.read("vdata");
}
else if (m.name.equals("edata")) {
edata.read("edata");
}
else if (m.name.equals("fdata")) {
fdata.read("fdata");
}
else if (m.name.equals("pdata")) {
pdata.read("pdata");
}
else if (m.name.equals("ldata")) {
ldata.read("ldata");
}
else if (m.name.equals("id")) {
id.read();
}
else {
datapos += m.size;
}
}
}
}
private class MVert {
float v[] = new float[3];
void read() throws Exception {
struct s = getStruct("MVert");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("co[3]")) {
for(int b=0;b<3;b++) {
v[b] = readfloat() + org[b]; //xyz position
}
}
else {
datapos += m.size;
}
}
}
}
private class MPoly {
/* offset into loop array and number of loops in the face */
int loopstart;
int totloop;
void read() throws Exception {
struct s = getStruct("MPoly");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("loopstart")) {
loopstart = readuint32();
}
else if (m.name.equals("totloop")) {
totloop = readuint32();
}
else {
datapos += m.size;
}
}
}
}
private class MLoop {
int v; /* vertex index */
void read() throws Exception {
struct s = getStruct("MLoop");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("v")) {
v = readuint32();
}
else {
datapos += m.size;
}
}
}
}
private class MTexPoly {
long tpage; //Image
void read() throws Exception {
struct s = getStruct("MTexPoly");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("*tpage")) {
tpage = readptr();
}
else {
datapos += m.size;
}
}
}
}
private class MLoopUV {
float uv[] = new float[2];
void read() throws Exception {
struct s = getStruct("MLoopUV");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("uv[2]")) {
uv[0] = readfloat();
uv[1] = readfloat();
}
else {
datapos += m.size;
}
}
}
}
private class Image {
ID id = new ID();
String name;
void read() throws Exception {
struct s = getStruct("Image");
for(int a=0;a<s.nr;a++) {
member m = s.members.get(a);
if (m.name.equals("name[1024]")) {
name = readString(m.size);
}
else if (m.name.equals("id")) {
id.read();
}
else {
datapos += m.size;
}
}
}
}
}