Przeglądaj źródła

hmd v3: reorganize animation data, BufferAnimation replaces LinearAnimation
hmd-v2 still accessible with -D hmd_version=2
see #560

Nicolas Cannasse 6 lat temu
rodzic
commit
71b6750562
5 zmienionych plików z 517 dodań i 51 usunięć
  1. 287 0
      h3d/anim/BufferAnimation.hx
  2. 86 27
      hxd/fmt/fbx/HMDOut.hx
  3. 12 2
      hxd/fmt/hmd/Data.hx
  4. 39 9
      hxd/fmt/hmd/Dump.hx
  5. 93 13
      hxd/fmt/hmd/Library.hx

+ 287 - 0
h3d/anim/BufferAnimation.hx

@@ -0,0 +1,287 @@
+package h3d.anim;
+import h3d.anim.Animation;
+import hxd.impl.Float32;
+
+enum DataLayout {
+	Position;
+	Rotation;
+	Scale;
+	UV;
+	Alpha;
+	Property;
+	SingleFrame;
+}
+
+class BufferObject extends AnimatedObject {
+
+	public var layout : haxe.EnumFlags<DataLayout>;
+	public var dataOffset : Int;
+	public var propCurrentValue : Float;
+	public var propName:  String;
+	public var matrix : h3d.Matrix;
+
+	public function new( objectName, dataOffset ) {
+		super(objectName);
+		this.dataOffset = dataOffset;
+	}
+
+	public function getStride() {
+		var stride = 0;
+		if( layout.has(Position) ) stride += 3;
+		if( layout.has(Rotation) ) stride += 4;
+		if( layout.has(Scale) ) stride += 3;
+		if( layout.has(UV) ) stride += 2;
+		if( layout.has(Alpha) ) stride += 1;
+		if( layout.has(Property) ) stride += 1;
+		return stride;
+	}
+
+	override function clone() : AnimatedObject {
+		var o = new BufferObject(objectName, dataOffset);
+		o.layout = layout;
+		o.propName = propName;
+		return o;
+	}
+}
+
+class BufferAnimation extends Animation {
+
+	var syncFrame : Float;
+	var data : #if hl hl.BytesAccess<hl.F32> #else hxd.impl.TypedArray.Float32Array #end;
+	var stride : Int;
+
+	public function new(name,frame,sampling) {
+		super(name,frame,sampling);
+		syncFrame = -1;
+	}
+
+	public function setData( data, stride ) {
+		this.data = data;
+		this.stride = stride;
+	}
+
+	public function addObject( objName, offset ) {
+		var f = new BufferObject(objName, offset);
+		objects.push(f);
+		return f;
+	}
+
+	override function getPropValue( objName, propName ) : Null<Float> {
+		for( o in getFrames() )
+			if( o.objectName == objName && o.propName == propName )
+				return o.propCurrentValue;
+		return null;
+	}
+
+	inline function getFrames() : Array<BufferObject> {
+		return cast objects;
+	}
+
+	override function clone(?a:Animation) {
+		if( a == null )
+			a = new BufferAnimation(name, frameCount, sampling);
+		super.clone(a);
+		var la = Std.instance(a, BufferAnimation);
+		la.setData(data, stride);
+		return a;
+	}
+
+	override function endFrame() {
+		return loop ? frameCount : frameCount - 1;
+	}
+
+	#if !(dataOnly || macro)
+
+	override function initInstance() {
+		super.initInstance();
+		var frames = getFrames();
+		for( a in frames ) {
+			if( a.layout.has(Property) )
+				a.propCurrentValue = data[a.dataOffset];
+			if( a.layout.has(Alpha) && (a.targetObject == null || !a.targetObject.isMesh()) )
+				throw a.objectName + " should be a mesh (for alpha animation)";
+			if( a.layout.has(Position) || a.layout.has(Rotation) || a.layout.has(Scale) ) {
+				a.matrix = new h3d.Matrix();
+				a.matrix.identity();
+			}
+		}
+		// makes sure that all single frame anims are at the end so we can break early when isSync=true
+		frames.sort(sortByFrameCountDesc);
+	}
+
+	function sortByFrameCountDesc( o1 : BufferObject, o2 : BufferObject ) {
+		return (o2.layout.has(SingleFrame) ? 0 : 1) - (o1.layout.has(SingleFrame) ? 0 : 1);
+	}
+
+	inline function uvLerp( v1 : Float, v2 : Float, k : Float ) {
+		v1 %= 1.;
+		v2 %= 1.;
+		if( v1 < v2 - 0.5 )
+			v1 += 1;
+		else if( v1 > v2 + 0.5 )
+			v1 -= 1;
+		return v1 * (1 - k) + v2 * k;
+	}
+
+	@:access(h3d.scene.Skin)
+	@:noDebug
+	override function sync( decompose = false ) {
+		if( frame == syncFrame && !decompose )
+			return;
+		var frame1 = getIFrame();
+		var frame2 = (frame1 + 1) % frameCount;
+		var k2 : Float32 = frame - frame1;
+		var k1 : Float32 = 1 - k2;
+		if( frame1 < 0 ) frame1 = frame2 = 0 else if( frame >= frameCount ) frame1 = frame2 = frameCount - 1 else if( !loop && frame2 == 0 ) frame2 = frameCount - 1;
+		syncFrame = frame;
+		if( decompose ) isSync = false;
+		for( o in getFrames() ) {
+
+			if( o.targetObject == null && o.targetSkin == null ) continue;
+
+			var layout = o.layout;
+			var offset1 = stride * frame1 + o.dataOffset;
+			var offset2 = stride * frame2 + o.dataOffset;
+			inline function lerpValue() {
+				return data[offset1++] * k1 + data[offset2++] * k2;
+			}
+
+			var frame1 = frame1, frame2 = frame2;
+
+			// if we have a single frame
+			if( layout.has(SingleFrame) ) {
+				if( isSync )
+					break;
+				frame1 = frame2 = 0;
+				offset1 = offset2 = o.dataOffset;
+			}
+
+			var m = o.matrix;
+
+			if( layout.has(Position) ) {
+				m._41 = lerpValue();
+				m._42 = lerpValue();
+				m._43 = lerpValue();
+			}
+
+			if( layout.has(Rotation) ) {
+				var q1x : Float32 = data[offset1++];
+				var q1y : Float32 = data[offset1++];
+				var q1z : Float32 = data[offset1++];
+				var q1w : Float32 = data[offset1++];
+				var q2x : Float32 = data[offset2++];
+				var q2y : Float32 = data[offset2++];
+				var q2z : Float32 = data[offset2++];
+				var q2w : Float32 = data[offset2++];
+				// qlerp nearest
+				var dot = q1x * q1x + q1y * q2y + q1z * q2z + q1w * q2w;
+				var q2 = dot < 0 ? -k2 : k2;
+				var qx = q1x * k1 + q2x * q2;
+				var qy = q1y * k1 + q2y * q2;
+				var qz = q1z * k1 + q2z * q2;
+				var qw = q1w * k1 + q2w * q2;
+				// make sure the resulting quaternion is normalized
+				var ql = 1 / Math.sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
+				qx *= ql;
+				qy *= ql;
+				qz *= ql;
+				qw *= ql;
+
+				if( decompose ) {
+					m._12 = qx;
+					m._13 = qy;
+					m._21 = qz;
+					m._23 = qw;
+					if( layout.has(Scale) ) {
+						m._11 = lerpValue();
+						m._22 = lerpValue();
+						m._33 = lerpValue();
+					} else {
+						m._11 = 1;
+						m._22 = 1;
+						m._33 = 1;
+					}
+				} else {
+					// quaternion to matrix
+					var xx = qx * qx;
+					var xy = qx * qy;
+					var xz = qx * qz;
+					var xw = qx * qw;
+					var yy = qy * qy;
+					var yz = qy * qz;
+					var yw = qy * qw;
+					var zz = qz * qz;
+					var zw = qz * qw;
+					m._11 = 1 - 2 * ( yy + zz );
+					m._12 = 2 * ( xy + zw );
+					m._13 = 2 * ( xz - yw );
+					m._21 = 2 * ( xy - zw );
+					m._22 = 1 - 2 * ( xx + zz );
+					m._23 = 2 * ( yz + xw );
+					m._31 = 2 * ( xz + yw );
+					m._32 = 2 * ( yz - xw );
+					m._33 = 1 - 2 * ( xx + yy );
+					if( layout.has(Scale) ) {
+						var sx = lerpValue();
+						var sy = lerpValue();
+						var sz = lerpValue();
+						m._11 *= sx;
+						m._12 *= sx;
+						m._13 *= sx;
+						m._21 *= sy;
+						m._22 *= sy;
+						m._23 *= sy;
+						m._31 *= sz;
+						m._32 *= sz;
+						m._33 *= sz;
+					}
+				}
+
+			} else if( m != null ) {
+				m._12 = 0;
+				m._13 = 0;
+				m._21 = 0;
+				m._23 = decompose ? 1 : 0;
+
+				if( layout.has(Scale) ) {
+					m._11 = lerpValue();
+					m._22 = lerpValue();
+					m._33 = lerpValue();
+				} else {
+					m._11 = 1;
+					m._22 = 1;
+					m._33 = 1;
+				}
+			}
+
+			if( m != null ) {
+				if( o.targetSkin != null ) {
+					o.targetSkin.currentRelPose[o.targetJoint] = m;
+					o.targetSkin.jointsUpdated = true;
+				} else
+					o.targetObject.defaultTransform = m;
+			}
+
+			if( layout.has(UV) ) {
+				var mat = o.targetObject.toMesh().material;
+				var s = mat.mainPass.getShader(h3d.shader.UVDelta);
+				if( s == null ) {
+					s = mat.mainPass.addShader(new h3d.shader.UVDelta());
+					mat.texture.wrap = Repeat;
+				}
+				s.uvDelta.x = uvLerp(data[offset1++],data[offset2++],k2);
+				s.uvDelta.y = uvLerp(data[offset1++],data[offset2++],k2);
+			}
+			if( layout.has(Alpha) ) {
+				var mat = o.targetObject.toMesh().material;
+				if( mat.blendMode == None ) mat.blendMode = Alpha;
+				mat.color.w = lerpValue();
+			}
+			if( layout.has(Property) )
+				o.propCurrentValue = lerpValue();
+		}
+		if( !decompose ) isSync = true;
+	}
+	#end
+
+}

+ 86 - 27
hxd/fmt/fbx/HMDOut.hx

@@ -710,6 +710,38 @@ class HMDOut extends BaseLibrary {
 		dataOut.writeFloat( f == 0 ? 0 : f ); // prevent negative zero
 	}
 
+	function writeFrame( o : h3d.anim.LinearAnimation.LinearObject, fid : Int ) {
+		if( d.version < 3 ) return;
+		if( o.frames != null ) {
+			var f = o.frames[fid];
+			if( o.hasPosition ) {
+				writeFloat(f.tx);
+				writeFloat(f.ty);
+				writeFloat(f.tz);
+			}
+			if( o.hasRotation ) {
+				var ql = Math.sqrt(f.qx * f.qx + f.qy * f.qy + f.qz * f.qz + f.qw * f.qw);
+				writeFloat(round(f.qx / ql));
+				writeFloat(round(f.qy / ql));
+				writeFloat(round(f.qz / ql));
+				writeFloat(round(f.qw / ql));
+			}
+			if( o.hasScale ) {
+				writeFloat(f.sx);
+				writeFloat(f.sy);
+				writeFloat(f.sz);
+			}
+		}
+		if( o.uvs != null ) {
+			writeFloat(o.uvs[fid<<1]);
+			writeFloat(o.uvs[(fid<<1)+1]);
+		}
+		if( o.alphas != null )
+			writeFloat(o.alphas[fid]);
+		if( o.propValues != null )
+			writeFloat(o.propValues[fid]);
+	}
+
 	function makeAnimation( anim : h3d.anim.Animation ) {
 		var a = new Animation();
 		a.name = anim.name;
@@ -723,57 +755,80 @@ class HMDOut extends BaseLibrary {
 			a.events = [for( a in animationEvents ) { var e = new AnimationEvent(); e.frame = a.frame; e.data = a.data; e; } ];
 		var objects : Array<h3d.anim.LinearAnimation.LinearObject> = cast @:privateAccess anim.objects;
 		objects.sort(function(o1, o2) return Reflect.compare(o1.objectName, o2.objectName));
+
+		var animatedObjects = [];
 		for( obj in objects ) {
 			var o = new AnimationObject();
+			var count = 0;
 			o.name = obj.objectName;
 			o.flags = new haxe.EnumFlags();
 			o.props = [];
 			if( obj.frames != null ) {
-				o.flags.set(HasPosition);
+				count = obj.frames.length;
+				if( obj.hasPosition || d.version < 3 )
+					o.flags.set(HasPosition);
 				if( obj.hasRotation )
 					o.flags.set(HasRotation);
 				if( obj.hasScale )
 					o.flags.set(HasScale);
-				if( obj.frames.length == 1 )
-					o.flags.set(SinglePosition);
-				for( f in obj.frames ) {
-					if( o.flags.has(HasPosition) ) {
-						writeFloat(f.tx);
-						writeFloat(f.ty);
-						writeFloat(f.tz);
-					}
-					if( o.flags.has(HasRotation) ) {
-						var ql = Math.sqrt(f.qx * f.qx + f.qy * f.qy + f.qz * f.qz + f.qw * f.qw);
-						if( f.qw < 0 ) ql = -ql;
-						writeFloat(round(f.qx / ql));
-						writeFloat(round(f.qy / ql));
-						writeFloat(round(f.qz / ql));
-					}
-					if( o.flags.has(HasScale) ) {
-						writeFloat(f.sx);
-						writeFloat(f.sy);
-						writeFloat(f.sz);
+				if( d.version < 3 ) {
+					for( f in obj.frames ) {
+						if( o.flags.has(HasPosition) ) {
+							writeFloat(f.tx);
+							writeFloat(f.ty);
+							writeFloat(f.tz);
+						}
+						if( o.flags.has(HasRotation) ) {
+							var ql = Math.sqrt(f.qx * f.qx + f.qy * f.qy + f.qz * f.qz + f.qw * f.qw);
+							if( f.qw < 0 ) ql = -ql;
+							writeFloat(round(f.qx / ql));
+							writeFloat(round(f.qy / ql));
+							writeFloat(round(f.qz / ql));
+						}
+						if( o.flags.has(HasScale) ) {
+							writeFloat(f.sx);
+							writeFloat(f.sy);
+							writeFloat(f.sz);
+						}
 					}
 				}
 			}
 			if( obj.uvs != null ) {
 				o.flags.set(HasUV);
-				for( f in obj.uvs )
-					writeFloat(f);
-			}
+				if( count == 0 ) count = obj.uvs.length else if( count != obj.uvs.length ) throw "assert";
+				if( d.version < 3 )
+					for( f in obj.uvs )
+						writeFloat(f);
+				}
 			if( obj.alphas != null ) {
 				o.flags.set(HasAlpha);
-				for( f in obj.alphas )
-					writeFloat(f);
+				if( count == 0 ) count = obj.alphas.length else if( count != obj.alphas.length ) throw "assert";
+				if( d.version < 3 )
+					for( f in obj.alphas )
+						writeFloat(f);
 			}
 			if( obj.propValues != null ) {
 				o.flags.set(HasProps);
 				o.props.push(obj.propName);
-				for( f in obj.propValues )
-					writeFloat(f);
+				if( count == 0 ) count = obj.propValues.length else if( count != obj.propValues.length ) throw "assert";
+				if( d.version < 3 )
+					for( f in obj.propValues )
+						writeFloat(f);
+			}
+			if( count == 0 )
+				throw "assert"; // no data ?
+			if( count == 1 ) {
+				o.flags.set(SingleFrame);
+				writeFrame(obj,0);
+			} else {
+				if( count != anim.frameCount ) throw "assert";
+				animatedObjects.push(obj);
 			}
 			a.objects.push(o);
 		}
+		for( i in 0...anim.frameCount )
+			for( obj in animatedObjects )
+				writeFrame(obj,i);
 		return a;
 	}
 
@@ -795,7 +850,11 @@ class HMDOut extends BaseLibrary {
 		this.filePath = filePath;
 
 		d = new Data();
+		#if hmd_version
+		d.version = Std.parseInt(#if macro haxe.macro.Context.definedValue("hmd_version") #else haxe.macro.Compiler.getDefine("hmd_version") #end);
+		#else
 		d.version = Data.CURRENT_VERSION;
+		#end
 		d.geometries = [];
 		d.materials = [];
 		d.models = [];

+ 12 - 2
hxd/fmt/hmd/Data.hx

@@ -178,7 +178,7 @@ enum AnimationFlag {
 	HasScale;
 	HasUV;
 	HasAlpha;
-	SinglePosition;
+	SingleFrame;
 	HasProps;
 	Reserved;
 }
@@ -189,6 +189,16 @@ class AnimationObject {
 	public var props : Array<String>;
 	public function new() {
 	}
+	public function getStride() {
+		var stride = 0;
+		if( flags.has(HasPosition) ) stride += 3;
+		if( flags.has(HasRotation) ) stride += 4;
+		if( flags.has(HasScale) ) stride += 3;
+		if( flags.has(HasUV) ) stride += 2;
+		if( flags.has(HasAlpha) ) stride += 1;
+		if( flags.has(HasProps) ) stride += props.length;
+		return stride;
+	}
 }
 
 class AnimationEvent {
@@ -214,7 +224,7 @@ class Animation {
 
 class Data {
 
-	public static inline var CURRENT_VERSION = 2;
+	public static inline var CURRENT_VERSION = 3;
 
 	public var version : Int;
 	public var props : Properties;

+ 39 - 9
hxd/fmt/hmd/Dump.hx

@@ -184,27 +184,57 @@ class Dump {
 			add('@$k ANIMATION');
 			prefix += '\t';
 			d.position = a.dataPosition;
+			var animStride = 0;
+			var prefixTotal = 0;
+			for( o in a.objects ) {
+				var stride = o.getStride();
+				if( o.flags.has(SingleFrame) )
+					prefixTotal += stride;
+				else
+					animStride += stride;
+			}
+			var firstFramePos = 0;
+			var animPos = 0;
 			for( o in a.objects ) {
 				var frames = a.frames;
 				var stride = 0;
 				if( o.flags.has(HasPosition) )
 					stride += 3;
 				if( o.flags.has(HasRotation) )
-					stride += 3;
+					stride += h.version < 3 ? 3 : 4;
 				if( o.flags.has(HasScale) )
 					stride += 3;
-				if( o.flags.has(SinglePosition) )
+				if( o.flags.has(SingleFrame) )
 					frames = 1;
+				inline function setFrame(f,offset) {
+					if( h.version < 3 ) return;
+					if( o.flags.has(SingleFrame) )
+						d.position = a.dataPosition + (firstFramePos + offset) * 4;
+					else
+						d.position = a.dataPosition + (animStride * f + prefixTotal + offset + animPos) * 4;
+				}
 				if( stride > 0 )
-					add('${o.name} Position : '+Std.string([for( i in 0...frames ) [for( j in 0...stride ) d.readFloat()]]));
-				if( o.flags.has(HasUV) )
-					add('${o.name} UV : '+Std.string([for( i in 0...a.frames ) [for( j in 0...2 ) d.readFloat()]]));
-				if( o.flags.has(HasAlpha) )
-					add('${o.name} Alpha : '+Std.string([for( i in 0...a.frames ) d.readFloat()]));
+					add('${o.name} Position : '+Std.string([for( i in 0...frames ) { setFrame(i,0); [for( j in 0...stride ) d.readFloat()]; }]));
+				if( h.version < 3 )
+					frames = a.frames;
+				if( o.flags.has(HasUV) ) {
+					add('${o.name} UV : '+Std.string([for( i in 0...frames ) { setFrame(i,stride); [for( j in 0...2 ) d.readFloat()]; }]));
+					stride += 2;
+				}
+				if( o.flags.has(HasAlpha) ) {
+					add('${o.name} Alpha : '+Std.string([for( i in 0...frames ) { setFrame(i,stride); d.readFloat(); }]));
+					stride += 1;
+				}
 				if( o.flags.has(HasProps) ) {
-					for( p in o.props )
-						add('${o.name} $p : '+Std.string([for( i in 0...a.frames ) d.readFloat()]));
+					for( p in o.props ) {
+						add('${o.name} $p : '+Std.string([for( i in 0...frames ) { setFrame(i,stride); d.readFloat(); }]));
+						stride += 1;
+					}
 				}
+				if( o.flags.has(SingleFrame) )
+					firstFramePos += stride;
+				else
+					animPos += stride;
 			}
 			prefix = '';
 		}

+ 93 - 13
hxd/fmt/hmd/Library.hx

@@ -388,27 +388,107 @@ class Library {
 				throw 'Animation $name not found !';
 		}
 
-		var l = makeAnimation(a);
-		l.resPath = resource.entry.path;
-		cachedAnimations.set(a.name, l);
-		if( name == null ) cachedAnimations.set("", l);
-		return l;
-	}
-
-
-	function makeAnimation( a : Animation ) {
-
-		var l = new h3d.anim.LinearAnimation(a.name, a.frames, a.sampling);
+		var l = header.version <= 2 ? makeLinearAnimation(a) : makeAnimation(a);
 		l.speed = a.speed;
 		l.loop = a.loop;
 		if( a.events != null ) l.setEvents(a.events);
-
 		if( hideData != null ) {
 			var name = resource.entry.name.split(".")[0];
 			if( StringTools.startsWith(name,"Anim_") ) name = name.substr(5);
 			if(hideData.animations.exists(name))
 				l.setEvents(hideData.animations.get(name).events);
 		}
+		l.resourcePath = resource.entry.path;
+		cachedAnimations.set(a.name, l);
+		if( name == null ) cachedAnimations.set("", l);
+		return l;
+	}
+
+	function makeAnimation( a : Animation ) {
+		var b = new h3d.anim.BufferAnimation(a.name, a.frames, a.sampling);
+
+		var stride = 0;
+		var singleFrames = [];
+		var otherFrames = [];
+		for( o in a.objects ) {
+			var c = b.addObject(o.name, 0);
+			var sm = 1;
+			if( o.flags.has(SingleFrame) ) {
+				c.layout.set(SingleFrame);
+				singleFrames.push(c);
+				sm = 0;
+			} else
+				otherFrames.push(c);
+			if( o.flags.has(HasPosition) ) {
+				c.layout.set(Position);
+				stride += 3 * sm;
+			}
+			if( o.flags.has(HasRotation) ) {
+				c.layout.set(Rotation);
+				stride += 4 * sm;
+			}
+			if( o.flags.has(HasScale) ) {
+				c.layout.set(Scale);
+				stride += 3 * sm;
+			}
+			if( o.flags.has(HasUV) ) {
+				c.layout.set(UV);
+				stride += 2 * sm;
+			}
+			if( o.flags.has(HasAlpha) ) {
+				c.layout.set(Alpha);
+				stride += sm;
+			}
+			if( o.flags.has(HasProps) ) {
+				for( i in 0...o.props.length ) {
+					var c = c;
+					if( i > 0 ) {
+						c = b.addObject(o.name, 0);
+						if( sm == 0 ) singleFrames.push(c) else otherFrames.push(c);
+					}
+					c.layout.set(Property);
+					c.propName = o.props[i];
+					stride += sm;
+				}
+			}
+		}
+
+		// assign data offsets
+		var pos = 0;
+		for( b in singleFrames ) {
+			b.dataOffset = pos;
+			pos += b.getStride();
+		}
+		var singleStride = pos;
+		for( b in otherFrames ) {
+			b.dataOffset = pos;
+			pos += b.getStride();
+		}
+
+		var entry = resource.entry;
+		entry.open();
+		entry.skip(header.dataPosition + a.dataPosition);
+		var count = stride * a.frames + singleStride;
+		var data = haxe.io.Bytes.alloc(count * 4);
+		entry.read(data,0,data.length);
+		entry.close();
+
+		#if hl
+		b.setData(data, stride);
+		#elseif js
+		b.setData(new hxd.impl.TypedArray.Float32Array(@:privateAccess data.b.buffer), stride);
+		#else
+		var v = new hxd.impl.TypedArray.Float32Array(count);
+		for( i in 0...count )
+			v[i] = data.getFloat(i << 2);
+		b.setData(v, stride);
+		#end
+
+		return b;
+	}
+
+	function makeLinearAnimation( a : Animation ) {
+		var l = new h3d.anim.LinearAnimation(a.name, a.frames, a.sampling);
 
 		var entry = resource.entry;
 		entry.open();
@@ -418,7 +498,7 @@ class Library {
 			var pos = o.flags.has(HasPosition), rot = o.flags.has(HasRotation), scale = o.flags.has(HasScale);
 			if( pos || rot || scale ) {
 				var frameCount = a.frames;
-				if( o.flags.has(SinglePosition) )
+				if( o.flags.has(SingleFrame) )
 					frameCount = 1;
 				var fl = new haxe.ds.Vector<h3d.anim.LinearAnimation.LinearFrame>(frameCount);
 				var size = ((pos ? 3 : 0) + (rot ? 3 : 0) + (scale?3:0)) * 4 * frameCount;