瀏覽代碼

Merge remote-tracking branch 'remotes/origin/lods'

TothBenoit 1 年之前
父節點
當前提交
96ee74a3fc
共有 10 個文件被更改,包括 393 次插入106 次删除
  1. 83 0
      h3d/Camera.hx
  2. 170 97
      h3d/prim/HMDModel.hx
  3. 3 1
      h3d/prim/ModelCache.hx
  4. 12 1
      h3d/prim/Primitive.hx
  5. 45 1
      h3d/scene/Mesh.hx
  6. 3 2
      h3d/scene/MultiMaterial.hx
  7. 3 0
      h3d/scene/Object.hx
  8. 2 0
      h3d/scene/RenderContext.hx
  9. 2 1
      h3d/scene/Skin.hx
  10. 70 3
      hxd/fmt/hmd/Library.hx

+ 83 - 0
h3d/Camera.hx

@@ -47,6 +47,7 @@ class Camera {
 	var mcamInv : Matrix;
 	var mprojInv : Matrix;
 	var needInv : Bool;
+	var directions : Matrix;
 
 	public function new( fovY = 25., zoom = 1., screenRatio = 1.333333, zNear = 0.02, zFar = 4000., rightHanded = false ) {
 		this.fovY = fovY;
@@ -130,6 +131,87 @@ class Camera {
 		return mcamInv;
 	}
 
+	function calcDirections() {
+		var cameraForward = ( target - pos ).normalized();
+		var cameraRight = up.cross(cameraForward).normalized();
+		var cameraUp = cameraForward.cross(cameraRight);
+
+		directions._11 = cameraForward.x;
+		directions._12 = cameraForward.y;
+		directions._13 = cameraForward.z;
+
+		directions._21 = cameraRight.x;
+		directions._22 = cameraRight.y;
+		directions._23 = cameraRight.z;
+
+		directions._31 = cameraUp.x;
+		directions._32 = cameraUp.y;
+		directions._33 = cameraUp.z;
+
+		directions._44 = 1;
+	}
+
+	/**
+		Returns the forward of the camera. Cache the result until the next update().
+	**/
+	public function getForward( ?forward : h3d.Vector ) : h3d.Vector {
+		if ( forward == null)
+			forward = new h3d.Vector();
+
+		if ( directions == null ) {
+			directions = new h3d.Matrix();
+			directions._44 = 0;
+		}
+		if ( directions._44 == 0 )
+			calcDirections();
+
+		forward.x = directions._11;
+		forward.y = directions._12;
+		forward.z = directions._13;
+
+		return forward;
+	}
+
+	/**
+		Returns the right of the camera. Cache the result until the next update().
+	**/
+	public function getRight( ?right : h3d.Vector ) : h3d.Vector {
+		if ( right == null)
+			right = new h3d.Vector();
+		if ( directions == null ) {
+			directions = new h3d.Matrix();
+			directions._44 = 0;
+		}
+		if ( directions._44 == 0 )
+			calcDirections();
+
+		right.x = directions._21;
+		right.y = directions._22;
+		right.z = directions._23;
+
+		return right;
+	}
+
+	/**
+		Returns the up of the camera. Cache the result until the next update().
+	**/
+	public function getUp( ?up : h3d.Vector ) : h3d.Vector {
+		if ( up == null)
+			up = new h3d.Vector();
+		if ( directions == null ) {
+			directions = new h3d.Matrix();
+			directions._44 = 0;
+		}
+		if ( directions._44 == 0 )
+			calcDirections();
+		
+		up.x = directions._31;
+		up.y = directions._32;
+		up.z = directions._33;
+
+		return up;
+	}
+
 	/**
 		Setup camera for cubemap rendering on the given face.
 	**/
@@ -199,6 +281,7 @@ class Camera {
 		needInv = true;
 		if( mcamInv != null ) mcamInv._44 = 0;
 		if( mprojInv != null ) mprojInv._44 = 0;
+		if( directions != null ) directions._44 = 0;
 
 		frustum.loadMatrix(m);
 	}

+ 170 - 97
h3d/prim/HMDModel.hx

@@ -2,7 +2,9 @@ package h3d.prim;
 
 class HMDModel extends MeshPrimitive {
 
-	var data : hxd.fmt.hmd.Data.Geometry;
+	var data (get, never) : hxd.fmt.hmd.Data.Geometry;
+	function get_data() { return lods[0]; }
+	var lods : Array<hxd.fmt.hmd.Data.Geometry>;
 	var dataPosition : Int;
 	var indexCount : Int;
 	var indexesTriPos : Array<Int>;
@@ -12,8 +14,10 @@ class HMDModel extends MeshPrimitive {
 	var normalsRecomputed : String;
 	var blendshape : Blendshape;
 
-	public function new(data, dataPos, lib) {
-		this.data = data;
+	public function new( data : hxd.fmt.hmd.Data.Geometry, dataPos, lib, lods = null ) {		
+		this.lods = [data];
+		if (lods != null)
+			this.lods = this.lods.concat(lods);			
 		this.dataPosition = dataPos;
 		this.lib = lib;
 
@@ -37,8 +41,8 @@ class HMDModel extends MeshPrimitive {
 		return data.bounds;
 	}
 
-	override function selectMaterial( i : Int ) {
-		curMaterial = i;
+	override function selectMaterial( material : Int, lod : Int ) {
+		curMaterial = material + lod * data.indexCounts.length;
 	}
 
 	override function getMaterialIndexes(material:Int):{count:Int, start:Int} {
@@ -55,26 +59,53 @@ class HMDModel extends MeshPrimitive {
 
 	override function alloc(engine:h3d.Engine) {
 		dispose();
-		buffer = new h3d.Buffer(data.vertexCount, data.vertexFormat);
-
-		var entry = lib.resource.entry;
-
-		var size = data.vertexCount * data.vertexFormat.strideBytes;
-		var bytes = entry.fetchBytes(dataPosition + data.vertexPosition, size);
-		buffer.uploadBytes(bytes, 0, data.vertexCount);
 
+		var vertexCount : Int = 0;
+		var vertexFormat : hxd.BufferFormat = data.vertexFormat;
 		indexCount = 0;
 		indexesTriPos = [];
-		for( n in data.indexCounts ) {
-			indexesTriPos.push(Std.int(indexCount/3));
-			indexCount += n;
+		for ( lod in lods ) {
+			vertexCount += lod.vertexCount;
+			for( n in lod.indexCounts ) {
+				indexesTriPos.push(Std.int(indexCount/3));
+				indexCount += n;
+			}
 		}
-		var is32 = data.vertexCount > 0x10000;
+
+		buffer = new h3d.Buffer(vertexCount, vertexFormat);
+
+		var is32 : Bool = vertexCount > 0x10000;
 		indexes = new h3d.Indexes(indexCount, is32);
+		var indexStride : Int = (is32 ? 4 : 2);
+		
+		var entry = lib.resource.entry;
+		var curVertexCount : Int = 0;
+		var curIndexCount : Int = 0;
+		
+		for ( lod in lods ) {
+			if (lod.vertexFormat != vertexFormat)
+				throw "LOD has a different vertex format";
+			
+			var size = lod.vertexCount * vertexFormat.strideBytes;
+			var bytes = entry.fetchBytes(dataPosition + lod.vertexPosition, size);			
+			engine.driver.uploadBufferBytes(buffer, curVertexCount, lod.vertexCount, bytes, 0);
 
-		var size = (is32 ? 4 : 2) * indexCount;
-		var bytes = entry.fetchBytes(dataPosition + data.indexPosition, size);
-		indexes.uploadBytes(bytes, 0, indexCount);
+			var indexCount = lod.indexCount;
+			size = indexStride * indexCount;
+			var bytes = entry.fetchBytes(dataPosition + lod.indexPosition, size);
+			if ( curIndexCount != 0 ) {
+				if (is32)
+					for ( i in 0...indexCount )
+						bytes.setInt32(i << 2, bytes.getInt32(i << 2) + curVertexCount);
+				else 
+					for ( i in 0...indexCount )
+						bytes.setUInt16(i << 1, bytes.getUInt16(i << 1) + curVertexCount);
+			}
+			engine.driver.uploadBufferBytes(indexes, curIndexCount, indexCount, bytes, 0);
+
+			curVertexCount += lod.vertexCount;
+			curIndexCount += indexCount;
+		}
 
 		if( normalsRecomputed != null ) {
 			var name = normalsRecomputed;
@@ -92,55 +123,57 @@ class HMDModel extends MeshPrimitive {
 
 		if( name == null ) name = "normal";
 
-
-		var pos = lib.getBuffers(data, hxd.BufferFormat.POS3D);
-		var ids = new Array();
-		var pts : Array<h3d.col.Point> = [];
-		var mpts = new Map();
-
-		for( i in 0...data.vertexCount ) {
-			var added = false;
-			var px = pos.vertexes[i * 3];
-			var py = pos.vertexes[i * 3 + 1];
-			var pz = pos.vertexes[i * 3 + 2];
-			var pid = Std.int((px + py + pz) * 10.01);
-			var arr = mpts.get(pid);
-			if( arr == null ) {
-				arr = [];
-				mpts.set(pid, arr);
-			} else {
-				for( idx in arr ) {
-					var p = pts[idx];
-					if( p.x == px && p.y == py && p.z == pz ) {
-						ids.push(idx);
-						added = true;
-						break;
+		var v = new hxd.FloatBuffer();
+		for ( lod in lods ) {
+			var pos = lib.getBuffers(lod, hxd.BufferFormat.POS3D);
+			var ids = new Array();
+			var pts : Array<h3d.col.Point> = [];
+			var mpts = new Map();
+	
+			for( i in 0...lod.vertexCount ) {
+				var added = false;
+				var px = pos.vertexes[i * 3];
+				var py = pos.vertexes[i * 3 + 1];
+				var pz = pos.vertexes[i * 3 + 2];
+				var pid = Std.int((px + py + pz) * 10.01);
+				var arr = mpts.get(pid);
+				if( arr == null ) {
+					arr = [];
+					mpts.set(pid, arr);
+				} else {
+					for( idx in arr ) {
+						var p = pts[idx];
+						if( p.x == px && p.y == py && p.z == pz ) {
+							ids.push(idx);
+							added = true;
+							break;
+						}
 					}
 				}
+				if( !added ) {
+					ids.push(pts.length);
+					arr.push(pts.length);
+					pts.push(new h3d.col.Point(px,py,pz));
+				}
 			}
-			if( !added ) {
-				ids.push(pts.length);
-				arr.push(pts.length);
-				pts.push(new h3d.col.Point(px,py,pz));
+	
+			var idx = new hxd.IndexBuffer();
+			for( i in pos.indexes )
+				idx.push(ids[i]);
+	
+			var pol = new Polygon(pts, idx);
+			pol.addNormals();
+	
+			var startOffset : Int = v.length;
+			v.grow(lod.vertexCount*3);
+			var k = 0;
+			for( i in 0...lod.vertexCount ) {
+				var n = pol.normals[ids[i]];
+				v[startOffset + k++] = n.x;
+				v[startOffset + k++] = n.y;
+				v[startOffset + k++] = n.z;
 			}
 		}
-
-		var idx = new hxd.IndexBuffer();
-		for( i in pos.indexes )
-			idx.push(ids[i]);
-
-		var pol = new Polygon(pts, idx);
-		pol.addNormals();
-
-		var v = new hxd.FloatBuffer();
-		v.grow(data.vertexCount*3);
-		var k = 0;
-		for( i in 0...data.vertexCount ) {
-			var n = pol.normals[ids[i]];
-			v[k++] = n.x;
-			v[k++] = n.y;
-			v[k++] = n.z;
-		}
 		var buf = h3d.Buffer.ofFloats(v, hxd.BufferFormat.make([{ name : name, type : DVec3 }]));
 		addBuffer(buf);
 		normalsRecomputed = name;
@@ -149,42 +182,47 @@ class HMDModel extends MeshPrimitive {
 	public function addTangents() {
 		if( hasInput("tangent") )
 			return;
-		var pos = lib.getBuffers(data, hxd.BufferFormat.POS3D);
-		var ids = new Array();
-		var pts : Array<h3d.col.Point> = [];
-		for( i in 0...data.vertexCount ) {
-			var added = false;
-			var px = pos.vertexes[i * 3];
-			var py = pos.vertexes[i * 3 + 1];
-			var pz = pos.vertexes[i * 3 + 2];
-			for(i in 0...pts.length) {
-				var p = pts[i];
-				if(p.x == px && p.y == py && p.z == pz) {
-					ids.push(i);
-					added = true;
-					break;
+		var v = new hxd.FloatBuffer();
+		for ( lod in lods ) {
+			var pos = lib.getBuffers(lod, hxd.BufferFormat.POS3D);
+			var ids = new Array();
+			var pts : Array<h3d.col.Point> = [];
+			for( i in 0...lod.vertexCount ) {
+				var added = false;
+				var px = pos.vertexes[i * 3];
+				var py = pos.vertexes[i * 3 + 1];
+				var pz = pos.vertexes[i * 3 + 2];
+				for(i in 0...pts.length) {
+					var p = pts[i];
+					if(p.x == px && p.y == py && p.z == pz) {
+						ids.push(i);
+						added = true;
+						break;
+					}
+				}
+				if( !added ) {
+					ids.push(pts.length);
+					pts.push(new h3d.col.Point(px,py,pz));
 				}
 			}
-			if( !added ) {
-				ids.push(pts.length);
-				pts.push(new h3d.col.Point(px,py,pz));
+			var idx = new hxd.IndexBuffer();
+			for( i in pos.indexes )
+				idx.push(ids[i]);
+			var pol = new Polygon(pts, idx);
+			pol.addNormals();
+			pol.addTangents();
+	
+			var startOffset : Int = v.length;
+			v.grow(lod.vertexCount*3);
+			var k = 0;
+			for( i in 0...lod.vertexCount ) {
+				var t = pol.tangents[ids[i]];
+				v[startOffset + k++] = t.x;
+				v[startOffset + k++] = t.y;
+				v[startOffset + k++] = t.z;
 			}
 		}
-		var idx = new hxd.IndexBuffer();
-		for( i in pos.indexes )
-			idx.push(ids[i]);
-		var pol = new Polygon(pts, idx);
-		pol.addNormals();
-		pol.addTangents();
-		var v = new hxd.FloatBuffer();
-		v.grow(data.vertexCount*3);
-		var k = 0;
-		for( i in 0...data.vertexCount ) {
-			var t = pol.tangents[ids[i]];
-			v[k++] = t.x;
-			v[k++] = t.y;
-			v[k++] = t.z;
-		}
+		
 		var buf = h3d.Buffer.ofFloats(v, hxd.BufferFormat.make([{ name : "tangent", type : DVec3 }]));
 		addBuffer(buf);
 	}
@@ -194,12 +232,16 @@ class HMDModel extends MeshPrimitive {
 			super.render(engine);
 			return;
 		}
+
+		var materialCount = data.indexCounts.length;
+		var lodLevel = Std.int(curMaterial / data.indexCounts.length);
+
 		if( indexes == null || indexes.isDisposed() )
 			alloc(engine);
 		if( buffers == null )
-			engine.renderIndexed(buffer, indexes, indexesTriPos[curMaterial], Std.int(data.indexCounts[curMaterial]/3));
+			engine.renderIndexed(buffer, indexes, indexesTriPos[curMaterial], Std.int(lods[lodLevel].indexCounts[curMaterial % materialCount]/3));
 		else
-			engine.renderMultiBuffers(formats, buffers, indexes, indexesTriPos[curMaterial], Std.int(data.indexCounts[curMaterial]/3));
+			engine.renderMultiBuffers(formats, buffers, indexes, indexesTriPos[curMaterial], Std.int(lods[lodLevel].indexCounts[curMaterial % materialCount]/3));
 		curMaterial = -1;
 	}
 
@@ -228,4 +270,35 @@ class HMDModel extends MeshPrimitive {
 		initCollider(poly);
 		return collider;
 	}
+		
+	override public function lodCount() : Int {
+		return lods.length;
+	}
+	
+	public static var lodExportKeyword : String = "LOD";
+	static var lodConfig : Array<Float> = [0.02, 0.002, 0.0002];
+	public static function loadLodConfig( config : Array<Float> ) {
+		lodConfig = config;
+	}
+
+	override public function screenRatioToLod( screenRatio : Float ) : Int {
+		var lodCount = lodCount();
+
+		if ( lodCount == 1 )
+			return 0;
+
+		if ( lodConfig != null && lodConfig.length >= lodCount - 1) {
+			var lodLevel : Int = 0; 
+			var maxIter = ( ( lodConfig.length > lodCount - 1 ) ? lodCount - 1: lodConfig.length );
+			for ( i in 0...maxIter ) {
+				if ( lodConfig[i] > screenRatio )
+					lodLevel++;
+				else 
+					break;
+			}
+			return lodLevel;
+		}
+
+		return 0;
+	}
 }

+ 3 - 1
h3d/prim/ModelCache.hx

@@ -60,6 +60,9 @@ class ModelCache {
 			var colliders = [];
 			for( m in lib.header.models ) {
 				if( m.geometry < 0 ) continue;
+				var prim = @:privateAccess lib.makePrimitive(m);
+				if (prim == null)
+					continue;
 				var pos = m.position.toMatrix();
 				var parent = lib.header.models[m.parent];
 				while( parent != null ) {
@@ -67,7 +70,6 @@ class ModelCache {
 					pos.multiply3x4(pos, pp);
 					parent = lib.header.models[parent.parent];
 				}
-				var prim = @:privateAccess lib.makePrimitive(m.geometry);
 				var col = cast(prim.getCollider(), h3d.col.Collider.OptimizedCollider);
 				colliders.push(new h3d.col.TransformCollider(pos,col));
 			}

+ 12 - 1
h3d/prim/Primitive.hx

@@ -83,7 +83,7 @@ class Primitive {
 	/**
 		Select the specified sub material before drawin. Used for internal usage.
 	**/
-	public function selectMaterial( material : Int ) {
+	public function selectMaterial( material : Int, lod : Int ) {
 	}
 
 	/**
@@ -130,4 +130,15 @@ class Primitive {
 		return Type.getClassName(Type.getClass(this)).split(".").pop();
 	}
 
+	/**
+	 	Return the LOD count.
+	**/
+	public function lodCount() {
+		return 1;
+	}
+
+	public function screenRatioToLod ( screenRatio : Float ) : Int {
+		return 0;
+	}
+
 }

+ 45 - 1
h3d/scene/Mesh.hx

@@ -16,6 +16,11 @@ class Mesh extends Object {
 	**/
 	public var material : h3d.mat.Material;
 
+	/**
+		When enabled, the lod level is inherited by children objects.
+	**/
+	public var inheritLod : Bool = false;
+
 	/**
 		Creates a new mesh with given primitive, material and parent object.
 		If material is not specified, a new default material is created for the current renderer.
@@ -37,7 +42,6 @@ class Mesh extends Object {
 		return [material];
 	}
 
-
 	static var tmpMat = new h3d.Matrix();
 	override function addBoundsRec( b : h3d.col.Bounds, relativeTo : h3d.Matrix ) {
 		super.addBoundsRec(b, relativeTo);
@@ -64,11 +68,51 @@ class Mesh extends Object {
 		return primitive.getCollider();
 	}
 
+	var curScreenRatio : Float = 1.0;
 	override function draw( ctx : RenderContext ) {
+		primitive.selectMaterial(0,	primitive.screenRatioToLod(curScreenRatio));
 		primitive.render(ctx.engine);
 	}
 
+	function calcScreenRatio( ctx : RenderContext ) {
+		if ( primitive.lodCount() == 1 )
+			return;
+
+		if ( ctx.forcedScreenRatio >= 0.0 ) {
+			curScreenRatio = ctx.forcedScreenRatio;
+			return;
+		}
+
+		var bounds = primitive.getBounds();
+		if ( bounds == null ) {
+			curScreenRatio = 1.0;
+			return;
+		}
+
+		var absPos = getAbsPos();
+		var worldCenter = absPos.getPosition();
+		var worldScale = absPos.getScale(); 
+		var worldRadius = bounds.dimension() * hxd.Math.max( worldScale.x, hxd.Math.max(worldScale.y, worldScale.z) ) / 2.0;
+
+		var cameraRight = ctx.camera.getRight();
+		var cameraUp = ctx.camera.getUp();
+		var cameraTopLeft = (cameraUp - cameraRight).normalized();
+		var worldTopLeft = worldCenter + cameraTopLeft * worldRadius;
+		var worldBottomRight = worldCenter - cameraTopLeft * worldRadius;
+
+		var screenTopLeft = ctx.camera.project( worldTopLeft.x, worldTopLeft.y, worldTopLeft.z, 1.0, 1.0, false );
+		var screenBottomRight = ctx.camera.project( worldBottomRight.x, worldBottomRight.y, worldBottomRight.z, 1.0, 1.0, false );
+
+		var screenArea = hxd.Math.max( screenBottomRight.x - screenTopLeft.x, screenBottomRight.y - screenTopLeft.y );
+
+		curScreenRatio = screenArea * screenArea;
+
+		if ( inheritLod )
+			ctx.forcedScreenRatio = curScreenRatio;
+	}
+
 	override function emit( ctx : RenderContext ) {
+		calcScreenRatio(ctx);
 		ctx.emit(material, this);
 	}
 

+ 3 - 2
h3d/scene/MultiMaterial.hx

@@ -24,6 +24,7 @@ class MultiMaterial extends Mesh {
 	}
 
 	override function emit( ctx : RenderContext ) {
+		calcScreenRatio(ctx);
 		for( i in 0...materials.length ) {
 			var m = materials[i];
 			if( m != null )
@@ -52,8 +53,8 @@ class MultiMaterial extends Mesh {
 
 	override function draw( ctx : RenderContext ) {
 		if( materials.length > 1 )
-			primitive.selectMaterial(ctx.drawPass.index);
-		super.draw(ctx);
+			primitive.selectMaterial(ctx.drawPass.index, primitive.screenRatioToLod(curScreenRatio));
+		primitive.render(ctx.engine);
 	}
 
 }

+ 3 - 0
h3d/scene/Object.hx

@@ -797,11 +797,14 @@ class Object {
 			for( c in children )
 				c.posChanged = true;
 		}
+
+		var prevForcedScreenRatio : Float = ctx.forcedScreenRatio;
 		if( !culled || ctx.computingStatic )
 			emit(ctx);
 
 		for( c in children )
 			c.emitRec(ctx);
+		ctx.forcedScreenRatio = prevForcedScreenRatio;
 	}
 
 	inline function set_x(v) {

+ 2 - 0
h3d/scene/RenderContext.hx

@@ -24,6 +24,7 @@ class RenderContext extends h3d.impl.RenderContext {
 	public var debugCulling : Bool;
 	public var wasContextLost : Bool;
 	public var cullingCollider : h3d.col.Collider;
+	public var forcedScreenRatio : Float = -1;	
 
 	@global("camera.view") var cameraView : h3d.Matrix;
 	@global("camera.zNear") var cameraNear : Float;
@@ -94,6 +95,7 @@ class RenderContext extends h3d.impl.RenderContext {
 		lights = null;
 		cachedPos = 0;
 		visibleFlag = true;
+		forcedScreenRatio = -1;
 		time += elapsedTime;
 		frame++;
 		setCurrent();

+ 2 - 1
h3d/scene/Skin.hx

@@ -252,6 +252,7 @@ class Skin extends MultiMaterial {
 	}
 
 	override function emit( ctx : RenderContext ) {
+		calcScreenRatio(ctx);
 		syncJoints(); // In case sync was not called because of culling (eg fixedPosition)
 		if( splitPalette == null )
 			super.emit(ctx);
@@ -294,7 +295,7 @@ class Skin extends MultiMaterial {
 		} else {
 			var i = ctx.drawPass.index;
 			skinShader.bonesMatrixes = splitPalette[i];
-			primitive.selectMaterial(i);
+			primitive.selectMaterial(i, primitive.screenRatioToLod(curScreenRatio));
 			ctx.uploadParams();
 			primitive.render(ctx.engine);
 		}

+ 70 - 3
hxd/fmt/hmd/Library.hx

@@ -1,4 +1,5 @@
 package hxd.fmt.hmd;
+import h3d.prim.HMDModel;
 import hxd.fmt.hmd.Data;
 
 private class FormatMap {
@@ -266,10 +267,21 @@ class Library {
 		return buf;
 	}
 
-	function makePrimitive( id : Int ) {
+	function makePrimitive( model : Model ) {
+		var id : Int = model.geometry;
 		var p = cachedPrimitives[id];
 		if( p != null ) return p;
-		p = new h3d.prim.HMDModel(header.geometries[id], header.dataPosition, this);
+
+		var lodModelName : String = "";
+		var lodLevel = getLODLevel( model, lodModelName ) ;
+		if ( lodLevel > 0)
+			return null;
+
+		var lods : Array<Geometry> = null;
+		if (lodLevel == 0 )
+			lods = findLODs( lodModelName );
+		
+		p = new h3d.prim.HMDModel(header.geometries[id], header.dataPosition, this, lods);
 		p.incref(); // Prevent from auto-disposing
 		cachedPrimitives[id] = p;
 		return p;
@@ -363,6 +375,59 @@ class Library {
 		return def;
 	}
 
+	function getLODLevel( model : Model, ?outModelName : String ) : Int {
+		var modelName : String = model.name;
+		var keyword = h3d.prim.HMDModel.lodExportKeyword;
+		if (modelName == null || modelName.length <= keyword.length)
+			return -1;
+	
+		// Test prefix
+		if ( modelName.substr(0, keyword.length) == keyword) {
+			var parsedInt = Std.parseInt(modelName.substr( keyword.length, 1 ));
+			if (parsedInt != null) {
+				if ( Std.parseInt( modelName.substr( keyword.length + 1, 1 ) ) != null )
+					throw 'Did not expect a second number after LOD in ${modelName}';
+				outModelName = modelName.substr(keyword.length);
+				return parsedInt;
+			}
+		}
+
+		// 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 ) {
+				outModelName = modelName.substr( 0, maxCursor );
+				return parsedInt;
+			}
+		}
+
+		return -1;
+	}
+
+	function findLODs( modelName : String ) : Array<Geometry> {
+		if ( modelName == null )
+			return null;
+
+		var lods : Array<Geometry> = [];
+		for ( curModel in header.models ) {			
+			var curModelName : String = "";
+			var lodLevel = getLODLevel(curModel, curModelName);
+			if ( lodLevel < 1 )
+				continue;	
+			if ( curModelName == modelName ) {
+				var capacityNeeded = lodLevel;
+				if ( capacityNeeded > lods.length )
+					lods.resize(capacityNeeded);
+				if ( lods[lodLevel - 1] != null )
+					throw 'Multiple LODs with the same level : ${curModel.name}';
+				lods[lodLevel - 1] = header.geometries[curModel.geometry];
+			}
+		}
+
+		return lods;
+	}
+
 	#if !dataOnly
 	public function makeObject( ?loadTexture : String -> h3d.mat.Texture ) : h3d.scene.Object {
 		if( loadTexture == null )
@@ -375,7 +440,9 @@ class Library {
 			if( m.geometry < 0 ) {
 				obj = new h3d.scene.Object();
 			} else {
-				var prim = makePrimitive(m.geometry);
+				var prim = makePrimitive(m);
+				if (prim == null)
+					continue;
 				if( m.skin != null ) {
 					var skinData = makeSkin(m.skin, header.geometries[m.geometry]);
 					skinData.primitive = prim;