Files
armorpaint/Sources/arm/format/FbxLibrary.hx
T
2020-02-28 15:56:44 +01:00

789 lines
20 KiB
Haxe

// Adapted fbx parser originally developed by Nicolas Cannasse
// https://github.com/HeapsIO/heaps/tree/master/hxd/fmt/fbx
// The MIT License (MIT)
// Copyright (c) 2013 Nicolas Cannasse
// 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 arm.format;
enum FbxProp {
PInt( v : Int );
PFloat( v : Float );
PString( v : String );
PIdent( i : String );
PInts( v : Array<Int> );
PFloats( v : Array<Float> );
}
typedef FbxNode = {
var name : String;
var props : Array<FbxProp>;
var childs : Array<FbxNode>;
}
class FbxTools {
public static function get( n : FbxNode, path : String, opt = false ) {
var parts = path.split(".");
var cur = n;
for( p in parts ) {
var found = false;
for( c in cur.childs )
if( c.name == p ) {
cur = c;
found = true;
break;
}
if( !found ) {
if( opt )
return null;
throw n.name + " does not have " + path+" ("+p+" not found)";
}
}
return cur;
}
public static function getAll( n : FbxNode, path : String ) {
var parts = path.split(".");
var cur = [n];
for( p in parts ) {
var out = [];
for( n in cur )
for( c in n.childs )
if( c.name == p )
out.push(c);
cur = out;
if( cur.length == 0 )
return cur;
}
return cur;
}
public static function getInts( n : FbxNode ) {
if( n.props.length != 1 )
throw n.name + " has " + n.props + " props";
switch( n.props[0] ) {
case PInts(v):
return v;
default:
throw n.name + " has " + n.props + " props";
}
}
public static function getFloats( n : FbxNode ) {
if( n.props.length != 1 )
throw n.name + " has " + n.props + " props";
switch( n.props[0] ) {
case PFloats(v):
return v;
case PInts(i):
var fl = new Array<Float>();
for( x in i )
fl.push(x);
n.props[0] = PFloats(fl); // keep data synchronized
// this is necessary for merging geometries since we are pushing directly into the
// float buffer
return fl;
default:
throw n.name + " has " + n.props + " props";
}
}
public static function hasProp( n : FbxNode, p : FbxProp ) {
for( p2 in n.props )
if( Type.enumEq(p, p2) )
return true;
return false;
}
static inline function idToInt( f : Float ) {
#if (neko || hl)
// ids are unsigned
f %= 4294967296.;
if( f >= 2147483648. )
f -= 4294967296.;
else if( f < -2147483648. )
f += 4294967296.;
#end
return Std.int(f);
}
public static function toInt( n : FbxProp ) {
if( n == null ) throw "null prop";
return switch( n ) {
case PInt(v): v;
case PFloat(f): idToInt(f);
default: throw "Invalid prop " + n;
}
}
public static function toFloat( n : FbxProp ) {
if( n == null ) throw "null prop";
return switch( n ) {
case PInt(v): v * 1.0;
case PFloat(v): v;
default: throw "Invalid prop " + n;
}
}
public static function toString( n : FbxProp ) {
if( n == null ) throw "null prop";
return switch( n ) {
case PString(v): v;
default: throw "Invalid prop " + n;
}
}
public static function getId( n : FbxNode ) {
if( n.props.length != 3 )
throw n.name + " is not an object";
return switch( n.props[0] ) {
case PInt(id): id;
case PFloat(id) : idToInt(id);
default: throw n.name + " is not an object " + n.props;
}
}
public static function getName( n : FbxNode ) {
if( n.props.length != 3 )
throw n.name + " is not an object";
return switch( n.props[1] ) {
case PString(n): n.split("::").pop();
default: throw n.name + " is not an object";
}
}
public static function getType( n : FbxNode ) {
if( n.props.length != 3 )
throw n.name + " is not an object";
return switch( n.props[2] ) {
case PString(n): n;
default: throw n.name + " is not an object";
}
}
}
private enum Token {
TIdent( s : String );
TNode( s : String );
TInt( s : String );
TFloat( s : String );
TString( s : String );
TLength( v : Int );
TBraceOpen;
TBraceClose;
TColon;
TEof;
}
class Parser {
var line : Int;
var buf : String;
var pos : Int;
var token : Null<Token>;
function new() {
}
function parseText( str ) : FbxNode {
this.buf = str;
this.pos = 0;
this.line = 1;
token = null;
return {
name : "Root",
props : [PInt(0),PString("Root"),PString("Root")],
childs : parseNodes(),
};
}
function parseNodes() {
var nodes = [];
while( true ) {
switch( peek() ) {
case TEof, TBraceClose:
return nodes;
default:
}
nodes.push(parseNode());
}
return nodes;
}
function parseNode() : FbxNode {
var t = next();
var name = switch( t ) {
case TNode(n): n;
default: unexpected(t);
};
var props = [], childs = null;
while( true ) {
t = next();
switch( t ) {
case TFloat(s):
props.push(PFloat(Std.parseFloat(s)));
case TInt(s):
props.push(PInt(Std.parseInt(s)));
case TString(s):
props.push(PString(s));
case TIdent(s):
props.push(PIdent(s));
case TBraceOpen, TBraceClose, TNode(_):
token = t;
case TLength(v):
except(TBraceOpen);
except(TNode("a"));
var ints : Array<Int> = [];
var floats : Array<Float> = null;
var i = 0;
while( i < v ) {
t = next();
switch( t ) {
case TColon:
continue;
case TInt(s):
i++;
if( floats == null )
ints.push(Std.parseInt(s));
else
floats.push(Std.parseInt(s));
case TFloat(s):
i++;
if( floats == null ) {
floats = [];
for( i in ints )
floats.push(i);
ints = null;
}
floats.push(Std.parseFloat(s));
default:
unexpected(t);
}
}
props.push(floats == null ? PInts(ints) : PFloats(floats));
except(TBraceClose);
break;
default:
unexpected(t);
}
t = next();
switch( t ) {
case TNode(_), TBraceClose:
token = t; // next
break;
case TColon:
// next prop
case TBraceOpen:
childs = parseNodes();
except(TBraceClose);
break;
default:
unexpected(t);
}
}
if( childs == null ) childs = [];
return { name : name, props : props, childs : childs };
}
function except( except : Token ) {
var t = next();
if( !Type.enumEq(t, except) )
error("Unexpected '" + tokenStr(t) + "' (" + tokenStr(except) + " expected)");
}
function peek() {
if( token == null )
token = nextToken();
return token;
}
function next() {
if( token == null )
return nextToken();
var tmp = token;
token = null;
return tmp;
}
function error( msg : String ) : Dynamic {
throw msg + " (line " + line + ")";
return null;
}
function unexpected( t : Token ) : Dynamic {
return error("Unexpected "+tokenStr(t));
}
function tokenStr( t : Token ) {
return switch( t ) {
case TEof: "<eof>";
case TBraceOpen: '{';
case TBraceClose: '}';
case TIdent(i): i;
case TNode(i): i+":";
case TFloat(f): f;
case TInt(i): i;
case TString(s): '"' + s + '"';
case TColon: ',';
case TLength(l): '*' + l;
};
}
inline function nextChar() {
return StringTools.fastCodeAt(buf, pos++);
}
inline function getBuf( pos : Int, len : Int ) {
return buf.substr(pos, len);
}
inline function isIdentChar(c) {
return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code) || c == '_'.code || c == '-'.code;
}
@:noDebug
function nextToken() {
var start = pos;
while( true ) {
var c = nextChar();
switch( c ) {
case ' '.code, '\r'.code, '\t'.code:
start++;
case '\n'.code:
line++;
start++;
case ';'.code:
while( true ) {
var c = nextChar();
if( StringTools.isEof(c) || c == '\n'.code ) {
pos--;
break;
}
}
start = pos;
case ','.code:
return TColon;
case '{'.code:
return TBraceOpen;
case '}'.code:
return TBraceClose;
case '"'.code:
start = pos;
while( true ) {
c = nextChar();
if( c == '"'.code )
break;
if( StringTools.isEof(c) || c == '\n'.code )
error("Unclosed string");
}
return TString(getBuf(start, pos - start - 1));
case '*'.code:
start = pos;
do {
c = nextChar();
} while( c >= '0'.code && c <= '9'.code );
pos--;
return TLength(Std.parseInt(getBuf(start, pos - start)));
default:
if( (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || c == '_'.code ) {
do {
c = nextChar();
} while( isIdentChar(c) );
if( c == ':'.code )
return TNode(getBuf(start, pos - start - 1));
pos--;
return TIdent(getBuf(start, pos - start));
}
if( (c >= '0'.code && c <= '9'.code) || c == '-'.code ) {
do {
c = nextChar();
} while( c >= '0'.code && c <= '9'.code );
if( c != '.'.code && c != 'E'.code && c != 'e'.code && pos - start < 10 ) {
pos--;
return TInt(getBuf(start, pos - start));
}
if( c == '.'.code ) {
do {
c = nextChar();
} while( c >= '0'.code && c <= '9'.code );
}
if( c == 'e'.code || c == 'E'.code ) {
c = nextChar();
if( c != '-'.code && c != '+'.code )
pos--;
do {
c = nextChar();
} while( c >= '0'.code && c <= '9'.code );
}
pos--;
return TFloat(getBuf(start, pos - start));
}
if( StringTools.isEof(c) ) {
pos--;
return TEof;
}
error("Unexpected char '" + String.fromCharCode(c) + "'");
}
}
}
public static function parse( text : String ) {
return new Parser().parseText(text);
}
}
class FbxLibrary {
var root : FbxNode;
var ids : Map<Int,FbxNode>;
var connect : Map<Int,Array<Int>>;
var namedConnect : Map<Int,Map<String,Int>>;
var invConnect : Map<Int,Array<Int>>;
// var uvAnims : Map<String, Array<{ t : Float, u : Float, v : Float }>>;
// var animationEvents : Array<{ frame : Int, data : String }>;
/**
The FBX version that was decoded
**/
public var version : Float = 0.;
public function new() {
root = { name : "Root", props : [], childs : [] };
reset();
}
function reset() {
ids = new Map();
connect = new Map();
namedConnect = new Map();
invConnect = new Map();
}
public function load( root : FbxNode ) {
reset();
this.root = root;
version = FbxTools.toInt(FbxTools.get(root, "FBXHeaderExtension.FBXVersion").props[0]) / 1000;
if( Std.int(version) != 7 )
throw "FBX Version 7.x required : use FBX 2010 export";
for( c in root.childs )
init(c);
// init properties
// for( m in FbxTools.getAll(this.root, "Objects.Model") ) {
// for( p in FbxTools.getAll(m, "Properties70.P") )
// switch( FbxTools.toString(p.props[0]) ) {
// case "UDP3DSMAX":
// var userProps = FbxTools.toString(p.props[4]).split("&cr;&lf;");
// for( p in userProps ) {
// var pl = p.split("=");
// var pname = StringTools.trim(pl.shift());
// var pval = StringTools.trim(pl.join("="));
// switch( pname ) {
// case "UV" if( pval != "" ):
// var xml = try Xml.parse(pval) catch( e : Dynamic ) throw "Invalid UV data in " + FbxTools.getName(m);
// var frames = [for( f in new haxe.xml.Access(xml.firstElement()).elements ) { var f = f.innerData.split(" "); { t : Std.parseFloat(f[0]) * 9622116.25, u : Std.parseFloat(f[1]), v : Std.parseFloat(f[2]) }} ];
// if( uvAnims == null ) uvAnims = new Map();
// uvAnims.set(FbxTools.getName(m), frames);
// case "Events":
// var xml = try Xml.parse(pval) catch( e : Dynamic ) throw "Invalid Events data in " + FbxTools.getName(m);
// animationEvents = [for( f in new haxe.xml.Access(xml.firstElement()).elements ) { var f = f.innerData.split(" "); { frame : Std.parseInt(f.shift()), data : StringTools.trim(f.join(" ")) }} ];
// default:
// }
// }
// default:
// }
// }
}
function init( n : FbxNode ) {
switch( n.name ) {
case "Connections":
for( c in n.childs ) {
if( c.name != "C" )
continue;
var child = FbxTools.toInt(c.props[1]);
var parent = FbxTools.toInt(c.props[2]);
// Maya exports invalid references
if( ids.get(child) == null || ids.get(parent) == null ) continue;
var name = c.props[3];
if( name != null ) {
var name = FbxTools.toString(name);
var nc = namedConnect.get(parent);
if( nc == null ) {
nc = new Map();
namedConnect.set(parent, nc);
}
nc.set(name, child);
// don't register as a parent, since the target can also be the child of something else
if( name == "LookAtProperty" ) continue;
}
var c = connect.get(parent);
if( c == null ) {
c = [];
connect.set(parent, c);
}
c.push(child);
if( parent == 0 )
continue;
var c = invConnect.get(child);
if( c == null ) {
c = [];
invConnect.set(child, c);
}
c.push(parent);
}
case "Objects":
for( c in n.childs )
ids.set(FbxTools.getId(c), c);
default:
}
}
public function getGeometry( name : String = "" ) {
var geom = null;
for( g in FbxTools.getAll(root, "Objects.Geometry") )
if( FbxTools.hasProp(g, PString("Geometry::" + name)) ) {
geom = g;
break;
}
if( geom == null )
throw "Geometry " + name + " not found";
return new Geometry(this, geom);
}
public function getFirstGeometry() {
var geom = FbxTools.getAll(root, "Objects.Geometry")[0];
return new Geometry(this, geom);
}
public function getAllGeometries() {
var geoms = FbxTools.getAll(root, "Objects.Geometry");
var res:Array<Geometry> = [];
for (g in geoms) res.push(new Geometry(this, g));
return res;
}
}
class Geometry {
var lib : FbxLibrary;
var root : FbxNode;
public function new(l, root) {
this.lib = l;
this.root = root;
}
public function getRoot() {
return root;
}
public function getVertices() {
return FbxTools.getFloats(FbxTools.get(root, "Vertices"));
}
public function getPolygons() {
return FbxTools.getInts(FbxTools.get(root, "PolygonVertexIndex"));
}
/**
Decode polygon informations into triangle indexes and vertices indexes.
Returns vidx, which is the list of vertices indexes and iout which is the index buffer for the full vertex model
**/
public function getIndexes() {
var count = 0, pos = 0;
var index = getPolygons();
var vout = [], iout = [];
for( i in index ) {
count++;
if( i < 0 ) {
index[pos] = -i - 1;
var start = pos - count + 1;
for( n in 0...count )
vout.push(index[n + start]);
for( n in 0...count - 2 ) {
iout.push(start + n);
iout.push(start + count - 1);
iout.push(start + n + 1);
}
index[pos] = i; // restore
count = 0;
}
pos++;
}
return { vidx : vout, idx : iout };
}
public function getNormals() {
return processVectors("LayerElementNormal", "Normals");
}
public function getTangents( opt = false ) {
return processVectors("LayerElementTangent", "Tangents", opt);
}
function processVectors( layer, name, opt = false ) {
var vect = FbxTools.get(root, layer + "." + name, opt);
if( vect == null ) return null;
var nrm = FbxTools.getFloats(vect);
// if by-vertice (Maya in some cases, unless maybe "Split per-Vertex Normals" is checked)
// let's reindex based on polygon indexes
if( FbxTools.toString(FbxTools.get(root, layer+".MappingInformationType").props[0]) == "ByVertice" ) {
var nout = [];
for( i in getPolygons() ) {
var vid = i;
if( vid < 0 ) vid = -vid - 1;
nout.push(nrm[vid * 3]);
nout.push(nrm[vid * 3 + 1]);
nout.push(nrm[vid * 3 + 2]);
}
nrm = nout;
}
return nrm;
}
public function getColors() {
var color = FbxTools.get(root, "LayerElementColor",true);
return color == null ? null : { values : FbxTools.getFloats(FbxTools.get(color, "Colors")), index : FbxTools.getInts(FbxTools.get(color, "ColorIndex")) };
}
public function getUVs() {
var uvs = [];
for( v in FbxTools.getAll(root, "LayerElementUV") ) {
var index = FbxTools.get(v, "UVIndex", true);
var values = FbxTools.getFloats(FbxTools.get(v, "UV"));
var index = if( index == null ) {
// ByVertice/Direct (Maya sometimes...)
[for( i in getPolygons() ) if( i < 0 ) -i - 1 else i];
} else FbxTools.getInts(index);
uvs.push({ values : values, index : index });
}
return uvs;
}
public function getBuffers(binary:Bool, p:FbxParser) {
// triangulize indexes :
// format is A,B,...,-X : negative values mark the end of the polygon
var pbuf = getVertices();
var nbuf = getNormals();
var tbuf = getUVs()[0];
var cbuf = FbxParser.parseVCols ? getColors() : null;
var polys = getPolygons();
if (FbxParser.parseTransform) {
for (i in 0...Std.int(pbuf.length / 3)) {
pbuf[i * 3 ] *= p.sx;
// q.fromEuler(p.rx, p.ry, p.rz);
// v.applyQuat(q);
pbuf[i * 3 ] += p.tx;
pbuf[i * 3 + 1] *= p.sy;
pbuf[i * 3 + 1] += p.ty;
pbuf[i * 3 + 2] *= p.sz;
pbuf[i * 3 + 2] += p.tz;
}
}
// Pack positions to (-1, 1) range
var scalePos = 0.0;
for (p in pbuf) {
var f = Math.abs(p);
if (scalePos < f) scalePos = f;
}
var inv = 32767 * (1 / scalePos);
var pos = 0;
var count = 0;
var vlen = 0;
var ilen = 0;
for (i in polys) {
count++;
if (i < 0) {
for (n in 0...count) vlen++;
for (n in 0...count - 2) ilen += 3;
count = 0;
}
pos++;
}
// Pack into 16bit
var posa = new kha.arrays.Int16Array(vlen * 4);
var nora = new kha.arrays.Int16Array(vlen * 2);
var texa = tbuf != null ? new kha.arrays.Int16Array(vlen * 2) : null;
var cola = cbuf != null ? new kha.arrays.Int16Array(vlen * 4) : null;
var inda = new kha.arrays.Uint32Array(ilen);
pos = 0;
count = 0;
vlen = 0;
ilen = 0;
for (i in polys) {
count++;
if (i < 0) {
polys[pos] = -i - 1;
var start = pos - count + 1;
for (n in 0...count) {
var k = n + start;
var vidx = polys[k];
posa[vlen * 4 ] = Std.int(pbuf[vidx * 3 ] * inv);
posa[vlen * 4 + 1] = Std.int(pbuf[vidx * 3 + 1] * inv);
posa[vlen * 4 + 2] = Std.int(pbuf[vidx * 3 + 2] * inv);
posa[vlen * 4 + 3] = Std.int(nbuf[k * 3 + 2] * 32767);
nora[vlen * 2 ] = Std.int(nbuf[k * 3 ] * 32767);
nora[vlen * 2 + 1] = Std.int(nbuf[k * 3 + 1] * 32767);
if (tbuf != null) {
var iuv = tbuf.index[k];
texa[vlen * 2 ] = Std.int( tbuf.values[iuv * 2 ] * 32767);
texa[vlen * 2 + 1] = Std.int((1.0 - tbuf.values[iuv * 2 + 1]) * 32767);
}
if (cbuf != null) {
var icol = cbuf.index[k];
cola[vlen * 3 ] = Std.int(cbuf.values[icol * 4 ] * 32767);
cola[vlen * 3 + 1] = Std.int(cbuf.values[icol * 4 + 1] * 32767);
cola[vlen * 3 + 2] = Std.int(cbuf.values[icol * 4 + 2] * 32767);
// cola[vlen * 4 + 3] = Std.int(cbuf.values[icol * 4 + 3] * 32767);
}
vlen++;
}
// polygons are actually triangle fans
for (n in 0...count - 2) {
inda[ilen + 2] = start + n;
inda[ilen + 1] = start + count - 1;
inda[ilen ] = start + n + 1;
ilen += 3;
}
polys[pos] = i; // restore
count = 0;
}
pos++;
}
return { posa: posa, nora: nora, texa: texa, cola: cola, inda: inda, scalePos: scalePos };
}
}