Pārlūkot izejas kodu

Added LODs in HMD format. Added optimization step when converting FBX to HMD that can optimize mesh and generate LODs.

TothBenoit 8 mēneši atpakaļ
vecāks
revīzija
ad4a8163fd
6 mainītis faili ar 223 papildinājumiem un 13 dzēšanām
  1. 179 2
      hxd/fmt/fbx/HMDOut.hx
  2. 2 0
      hxd/fmt/hmd/Data.hx
  3. 21 11
      hxd/fmt/hmd/Library.hx
  4. 9 0
      hxd/fmt/hmd/Reader.hx
  5. 6 0
      hxd/fmt/hmd/Writer.hx
  6. 6 0
      hxd/fs/Convert.hx

+ 179 - 2
hxd/fmt/fbx/HMDOut.hx

@@ -5,6 +5,7 @@ import hxd.fmt.hmd.Data;
 import hxd.BufferFormat;
 
 class HMDOut extends BaseLibrary {
+	public static var lodExportKeyword : String = "LOD";
 
 	var d : Data;
 	var dataOut : haxe.io.BytesOutput;
@@ -13,9 +14,11 @@ class HMDOut extends BaseLibrary {
 	var midsSortRemap : Map<Int, Int>;
 	public var absoluteTexturePath : Bool;
 	public var optimizeSkin = true;
+	public var optimizeMesh = false;
 	public var generateNormals = false;
 	public var generateTangents = false;
 	public var lowPrecConfig : Map<String,Precision>;
+	public var lodsDecimation : Array<Float>;
 
 	function int32tof( v : Int ) : Float {
 		tmp.set(0, v & 0xFF);
@@ -113,7 +116,7 @@ class HMDOut extends BaseLibrary {
 		for( i in 0...index.vidx.length )
 			outputData.addInt32(i);
 		sys.io.File.saveBytes(fileName, outputData.getBytes());
-		var ret = try Sys.command("mikktspace",[fileName,outFile]) catch( e : Dynamic ) -1;
+		var ret = try Sys.command("meshTools",["mikktspace",fileName,outFile]) catch( e : Dynamic ) -1;
 		if( ret != 0 ) {
 			sys.FileSystem.deleteFile(fileName);
 			throw "Failed to call 'mikktspace' executable required to generate tangent data. Please ensure it's in your PATH"+(filePath == null ? "" : ' ($filePath)');
@@ -221,7 +224,91 @@ class HMDOut extends BaseLibrary {
 		return inputName;
 	}
 
-	function buildGeom( geom : hxd.fmt.fbx.Geometry, skin : h3d.anim.Skin, dataOut : haxe.io.BytesOutput, genTangents : Bool ) {
+	function optimize( vbuf : hxd.FloatBuffer, vertexFormat : hxd.BufferFormat, ibuf : Array<Int>, startIndex : Int, decimationFactor : Float ) {
+		var optimizedVbuf : hxd.FloatBuffer;
+		decimationFactor = hxd.Math.clamp(decimationFactor);
+
+		#if ( hl && hl_ver >= version("1.15.0") )
+		var vertexSize = vertexFormat.stride << 2;
+		var vertexCount = Std.int(vbuf.length / vertexFormat.stride);
+		var vertices = new hl.Bytes(vertexCount * vertexSize);
+		for ( i in 0...vbuf.length )
+			vertices.setF32(i << 2, cast(vbuf[i], Single));
+		var indexCount = ibuf.length;
+		var indices = new hl.Bytes(indexCount * 4);
+		for ( i => idx in ibuf )
+			indices.setI32(i << 2, idx);
+
+		var remap = new hl.Bytes(vertexCount * 4);
+		var uniqueVertexCount = hxd.tools.MeshOptimizer.generateVertexRemap(remap, indices, indexCount, vertices, vertexCount, vertexSize);
+		hxd.tools.MeshOptimizer.remapIndexBuffer(indices, indices, indexCount, remap);
+		hxd.tools.MeshOptimizer.remapVertexBuffer(vertices, vertices, vertexCount, vertexSize, remap);
+		vertexCount = uniqueVertexCount;
+		if ( decimationFactor > 0.0 )
+			indexCount = hxd.tools.MeshOptimizer.simplify(indices, indices, indexCount, vertices, vertexCount, vertexSize, Std.int(indexCount * (1.0 - decimationFactor)), 0.05, 0, null);
+		hxd.tools.MeshOptimizer.optimizeVertexCache(indices, indices, indexCount, vertexCount);
+		hxd.tools.MeshOptimizer.optimizeOverdraw(indices, indices, indexCount, vertices, vertexCount, vertexSize, 1.05);
+		vertexCount = hxd.tools.MeshOptimizer.optimizeVertexFetch(vertices, indices, indexCount, vertices, vertexCount, vertexSize);
+
+		optimizedVbuf = new hxd.FloatBuffer();
+		optimizedVbuf.resize(vertexCount * vertexFormat.stride);
+		for ( i in 0...vertexCount * vertexFormat.stride )
+			optimizedVbuf[i] = vertices.getF32(i << 2);
+		ibuf.resize(indexCount);
+		for ( i in 0...indexCount )
+			ibuf[i] = indices.getI32(i << 2) + startIndex;
+
+		#elseif (sys || nodejs)
+		var tmp = Sys.getEnv("TMPDIR");
+		if( tmp == null ) tmp = Sys.getEnv("TMP");
+		if( tmp == null ) tmp = Sys.getEnv("TEMP");
+		if( tmp == null ) tmp = ".";
+		var fileName = tmp+"/meshTools_data"+Date.now().getTime()+"_"+Std.random(0x1000000)+".bin";
+		var outFile = fileName+".out";
+
+		var vertexSize = vertexFormat.stride << 2;
+		var vertexCount = Std.int(vbuf.length / vertexFormat.stride);
+
+		var outputData = new haxe.io.BytesBuffer();
+		outputData.addInt32(vertexCount);
+		outputData.addInt32(vertexSize);
+		for( v in vbuf )
+			outputData.addFloat(v);
+		var indexCount = ibuf.length;
+		outputData.addInt32(indexCount);
+		for( i in ibuf )
+			outputData.addInt32(i);
+		sys.io.File.saveBytes(fileName, outputData.getBytes());
+		var ret = if (decimationFactor > 0.0)
+			try Sys.command("meshTools",["simplify",fileName,outFile,'${Std.int(indexCount * (1.0 - decimationFactor))}','${decimationFactor}']) catch( e : Dynamic ) -1;
+		else
+			try Sys.command("meshTools",["optimize",fileName,outFile]) catch( e : Dynamic ) -1;
+
+		if( ret != 0 ) {
+			sys.FileSystem.deleteFile(fileName);
+			throw "Failed to call 'meshTools' executable required to generate optimized mesh. Please ensure it's in your PATH"+(filePath == null ? "" : ' ($filePath)');
+		}
+		var input = sys.io.File.getBytes(outFile);
+		var pos = 1;
+		vertexCount = input.getInt32(0);
+		optimizedVbuf = new hxd.FloatBuffer();
+		optimizedVbuf.resize(vertexCount * vertexFormat.stride);
+		for ( i in 0...vertexCount * vertexFormat.stride )
+			optimizedVbuf[i] = input.getFloat(4 * pos++);
+		indexCount = input.getInt32(4 * pos++);
+		ibuf.resize(indexCount);
+		for ( i in 0...indexCount )
+			ibuf[i] = input.getInt32(4 * pos++) + startIndex;
+		sys.FileSystem.deleteFile(fileName);
+		sys.FileSystem.deleteFile(outFile);
+		#else
+		optimizedVbuf = vbuf;
+		#end
+
+		return optimizedVbuf;
+	}
+
+	function buildGeom( geom : hxd.fmt.fbx.Geometry, skin : h3d.anim.Skin, dataOut : haxe.io.BytesOutput, genTangents : Bool, decimationFactor : Float = 0.0 ) {
 		var g = new Geometry();
 
 		var verts = geom.getVertices();
@@ -478,6 +565,24 @@ class HMDOut extends BaseLibrary {
 		if( generateNormals )
 			updateNormals(g,vbuf,ibufs);
 
+		if ( optimizeMesh || decimationFactor > 0.0 ) {
+			var optimizedVbuf = new hxd.FloatBuffer();
+			for( idx in ibufs ) {
+				if ( idx == null )
+					continue;
+				var start = optimizedVbuf.length;
+				var buf = optimize(vbuf, g.vertexFormat, idx, Std.int(start / g.vertexFormat.stride), decimationFactor );
+				var length = buf.length;
+				optimizedVbuf.resize(start + length);
+				for ( i in 0...length )
+					optimizedVbuf[start + i] = buf[i];
+			}
+			vbuf = optimizedVbuf;
+			g.vertexCount = Std.int(optimizedVbuf.length / g.vertexFormat.stride);
+			if ( g.vertexCount == 0 )
+				return null;
+		}
+
 		// write data
 		g.vertexPosition = dataOut.length;
 		if( lowPrecConfig == null ) {
@@ -661,6 +766,34 @@ class HMDOut extends BaseLibrary {
 		return { g : g, materials : matMap };
 	}
 
+	function getLODInfos( modelName : String ) : { lodLevel : Int , modelName : String } {
+
+		var keyword = lodExportKeyword;
+		if ( modelName == null || modelName.length <= keyword.length )
+			return { lodLevel : -1, modelName : null };
+
+		// Test prefix
+		if ( modelName.substr(0, keyword.length) == keyword ) {
+			var parsedInt = Std.parseInt(modelName.charAt( keyword.length ));
+			if (parsedInt != null) {
+				if ( Std.parseInt( modelName.charAt( keyword.length + 1 ) ) != null )
+					throw 'Did not expect a second number after LOD in ${modelName}';
+				return { lodLevel : parsedInt, modelName : modelName.substr(keyword.length) };
+			}
+		}
+
+		// Test suffix
+		var maxCursor = modelName.length - keyword.length - 1;
+		if ( modelName.substr( maxCursor, keyword.length ) == keyword ) {
+			var parsedInt = Std.parseInt( modelName.charAt( modelName.length - 1) );
+			if ( parsedInt != null ) {
+				return { lodLevel : parsedInt, modelName : modelName.substr( 0, maxCursor ) };
+			}
+		}
+
+		return { lodLevel : -1, modelName : null };
+	}
+
 	function addModels(includeGeometry) {
 
 		var root = buildHierarchy().root;
@@ -773,6 +906,7 @@ class HMDOut extends BaseLibrary {
 
 		var hgeom = new Map();
 		var hmat = new Map<Int,Int>();
+		var hlods = new Map<String, Array<Index<Model>>>();
 		var index = 0;
 		for( o in objects ) {
 
@@ -944,6 +1078,49 @@ class HMDOut extends BaseLibrary {
 				model.materials = mids;
 			else
 				model.materials = [for( id in gdata.materials ) mids[id]];
+
+			var lodsInfos = getLODInfos(model.name);
+			var lodIndex = lodsInfos.lodLevel;
+			var key = lodsInfos.modelName;
+			if ( lodIndex >= 0 ) {
+				var lods = hlods.get(key);
+				if ( lods == null ) {
+					lods = [];
+					hlods.set(key, lods);
+				}
+				if ( lodIndex > 0 ) {
+					lods[lodIndex - 1] = d.models.length-1;
+					model.lods = [];
+				} else {
+					model.lods = lods;
+				}
+				if( model.props == null ) model.props = [];
+				model.props.push(HasLod);
+			} else if ( lodsDecimation != null && model.skin == null ) {
+				var modelName = model.name;
+				model.name = modelName + "LOD0";
+				if( model.props == null ) model.props = [];
+				model.props.push(HasLod);
+				model.lods = [];
+				for ( i => lods in lodsDecimation ) {
+					var geom = buildGeom(new hxd.fmt.fbx.Geometry(this, g), skin, dataOut, hasNormalMap || generateTangents, lods);
+					if ( geom == null )
+						continue;
+					var lodModel = new Model();
+					lodModel.name = modelName + 'LOD${i}';
+					lodModel.props = model.props;
+					lodModel.parent = model.parent;
+					lodModel.follow = model.follow;
+					lodModel.position = model.position;
+					lodModel.materials = model.materials;
+					lodModel.skin = model.skin;
+					lodModel.lods = [];
+					lodModel.geometry = d.geometries.length;
+					d.geometries.push(geom.g);
+					model.lods.push(d.models.length);
+					d.models.push(lodModel);
+				}
+			}
 		}
 	}
 

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

@@ -11,6 +11,7 @@ enum Property<T> {
 	Unused_HasMaterialFlags; // TODO: Removing this will offset property indices
 	HasExtraTextures;
 	FourBonesByVertex;
+	HasLod;
 }
 
 typedef Properties = Null<Array<Property<Dynamic>>>;
@@ -139,6 +140,7 @@ class Model {
 	public var geometry : Index<Geometry>;
 	public var materials : Null<Array<Index<Material>>>;
 	public var skin : Null<Skin>;
+	public var lods : Array<Index<Model>>;
 	public function new() {
 	}
 }

+ 21 - 11
hxd/fmt/hmd/Library.hx

@@ -272,25 +272,35 @@ class Library {
 		var p = cachedPrimitives[id];
 		if( p != null ) return p;
 
-		var lodInfos = getLODInfos( model );
-		if ( lodInfos.lodLevel > 0) {
-			for ( m in header.models )
-				if ( m.name != null && StringTools.contains(m.name, lodInfos.modelName) && StringTools.contains(m.name, "LOD0"))
-					return null;
-			throw "No LOD0 found for " + lodInfos.modelName + " in " + resource.name;
-		}
-
 		var lods : Array<Model> = null;
-		if (lodInfos.lodLevel == 0 ) {
-			lods = findLODs( lodInfos.modelName, model );
+		var hasLod = model.lods != null;
+		if ( hasLod ) {
+			var isLod = model.name.indexOf("LOD0") < 0;
+			if ( isLod )
+				return null;
+			lods = [for ( lod in model.lods) header.models[lod]];
 			patchLodsMaterials(model, lods);
+		} else {
+			var lodInfos = getLODInfos( model );
+			if ( lodInfos.lodLevel > 0) {
+				for ( m in header.models )
+					if ( m.name != null && StringTools.contains(m.name, lodInfos.modelName) && StringTools.contains(m.name, "LOD0"))
+						return null;
+				throw "No LOD0 found for " + lodInfos.modelName + " in " + resource.name;
+			}
+
+			if (lodInfos.lodLevel == 0 ) {
+				lods = findLODs( lodInfos.modelName, model );
+				patchLodsMaterials(model, lods);
+				hasLod = true;
+			}
 		}
 
 		p = new h3d.prim.HMDModel(header.geometries[id], header.dataPosition, this, lods);
 		p.incref(); // Prevent from auto-disposing
 		cachedPrimitives[id] = p;
 
-		if (lodInfos.lodLevel == 0)
+		if ( hasLod )
 			h3d.prim.ModelDatabase.current.loadModelProps(model.name, p);
 
 		return p;

+ 9 - 0
hxd/fmt/hmd/Reader.hx

@@ -23,6 +23,8 @@ class Reader {
 			return HasExtraTextures;
 		case 3:
 			return FourBonesByVertex;
+		case 4:
+			return HasLod;
 		case unk:
 			throw "Unknown property #" + unk;
 		}
@@ -125,6 +127,11 @@ class Reader {
 		return s;
 	}
 
+	function readLods() {
+		var lodCount = i.readInt32();
+		return [for (_ in 0...lodCount) i.readInt32()];
+	}
+
 	public function readHeader( fast = false ) : Data {
 		var d = new Data();
 		var h = i.readString(3);
@@ -199,6 +206,8 @@ class Reader {
 			for( k in 0...matCount )
 				m.materials.push(i.readInt32());
 			m.skin = readSkin();
+			if ( m.props != null )
+				m.lods = m.props.indexOf(HasLod) >= 0 ? readLods() : null;
 		}
 
 		d.animations = [];

+ 6 - 0
hxd/fmt/hmd/Writer.hx

@@ -18,6 +18,7 @@ class Writer {
 		case Unused_HasMaterialFlags:
 		case HasExtraTextures:
 		case FourBonesByVertex:
+		case HasLod:
 		}
 	}
 
@@ -169,6 +170,11 @@ class Writer {
 				writeName(null);
 			else
 				writeSkin(m.skin);
+			if ( m.lods != null ) {
+				out.writeInt32(m.lods.length);
+				for ( lod in m.lods )
+					out.writeInt32(lod);
+			}
 		}
 
 		out.writeInt32(d.animations.length);

+ 6 - 0
hxd/fs/Convert.hx

@@ -101,6 +101,12 @@ class ConvertFBX2HMD extends Convert {
 						case x: throw "Invalid precision '" + x + "' should be u8|s8|f16";
 					});
 			}
+			if ( params.optimizeMesh != null )
+				hmdout.optimizeMesh = params.optimizeMesh;
+			if (params.lodsDecimation != null) {
+				var config: Array<Float> = params.lodsDecimation;
+				hmdout.lodsDecimation = [for(lod in config) lod];
+			}
 		}
 		hmdout.load(fbx);
 		var isAnim = StringTools.startsWith(originalFilename, "Anim_") || originalFilename.toLowerCase().indexOf("_anim_") > 0;