Przeglądaj źródła

New particle format & optimize physics a bit

Panagiotis Christopoulos Charitos 7 lat temu
rodzic
commit
1b3baa2f47

BIN
samples/physics_playground/assets/floor.ankimesh


+ 21 - 22
samples/physics_playground/assets/smoke.ankipart

@@ -1,26 +1,25 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
 <particleEmitter>
-	<life>10.5</life>
-	<lifeDeviation>0.0</lifeDeviation>
-	<mass>2.0</mass>
-	<massDeviation>0.0</massDeviation>
-	<gravity>0.0 -9.5 0.0</gravity>
-	<alpha>1.0</alpha>
-	<alphaDeviation>0.1</alphaDeviation>
-	<alphaAnimationEnabled>0</alphaAnimationEnabled>
-	<emissionPeriod>0.001</emissionPeriod>
-	<particlesPerEmission>8</particlesPerEmission>
-	<startingPosition>0.0 0.0 0.8</startingPosition>
-	<startingPositionDeviation>1.5 1.5 0.0</startingPositionDeviation>
-	<material>assets/smoke.ankimtl</material>
-	<size>1.4</size>
-	<sizeDeviation>0.2</sizeDeviation>
-	<sizeAnimation>0.0</sizeAnimation>
-	<usePhysicsEngine>1</usePhysicsEngine>
-	<maxNumberOfParticles>8</maxNumberOfParticles>
-	<forceDirection>0 0 1</forceDirection>
-	<forceDirectionDeviation>1.0 1.0 0</forceDirectionDeviation>
-	<forceMagnitude>900</forceMagnitude>
-	<forceMagnitudeDeviation>200</forceMagnitudeDeviation>
+	<life value="7.5"/>
+
+	<mass value="2"/>
+
+	<gravity value="0.0 -9.5 0.0"/>
+	
+	<initialAlpha min="0.8" max="0.9"/>
+	<finalAlpha value="0"/>
+	
+	<startingPosition min="-1.5 -1.5 0.8" max="1.5 1.5 0.8"/>
+
+	<size value="2.4"/>
+
+	<forceDirection min="-0.5 -0.5 1" max="0.5 0.5 1"/>
+	<forceMagnitude min="700" max="900"/>
+
+	<maxNumberOfParticles value="8"/>
+	<emissionPeriod value="0.001"/>
+	<particlesPerEmission value="8"/>
+	<material value="assets/smoke.ankimtl"/>
+	<usePhysicsEngine value="1"/>
 </particleEmitter>

BIN
samples/physics_playground/assets/wall.ankimesh


+ 15 - 17
samples/sponza/assets/fire.ankipart

@@ -1,21 +1,19 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
 <particleEmitter>
-	<life>1.5</life>
-	<lifeDeviation>0.5</lifeDeviation>
-	<mass>1.0</mass>
-	<massDeviation>0.0</massDeviation>
-	<gravity>0.0 0.5 0.0</gravity>
-	<alpha>0.5</alpha>
-	<alphaDeviation>0.5</alphaDeviation>
-	<alphaAnimationEnabled>1</alphaAnimationEnabled>
-	<emissionPeriod>0.05</emissionPeriod>
-	<particlesPerEmission>1</particlesPerEmission>
-	<startingPosition>0.0 0.1 0.0</startingPosition>
-	<startingPositionDeviation>0.04 0.0 0.04</startingPositionDeviation>
-	<material>assets/fire.ankimtl</material>
-	<size>0.80</size>
-	<sizeDeviation>0.02</sizeDeviation>
-	<sizeAnimation>-0.2</sizeAnimation>
-	<usePhysicsEngine>0</usePhysicsEngine>
+	<life min="1" max="2"/>
+	<gravity value="0.0 0.5 0.0"/>
+
+	<initialAlpha min="0.5" max="1"/>
+	<finalAlpha value="0"/>
+
+	<initialSize min="0.78" max="0.82"/>
+	<finalSize min="0.4" max="0.5"/>
+
+	<startingPosition min="-0.04 0.1 -0.04" max="0.04 0.1 0.04"/>
+
+	<maxNumberOfParticles value="10"/>
+	<emissionPeriod value="0.05"/>
+	<particlesPerEmission value="1"/>
+	<material value="assets/fire.ankimtl"/>
 </particleEmitter>

+ 15 - 18
samples/sponza/assets/smoke.ankipart

@@ -1,22 +1,19 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
 <particleEmitter>
-	<life>5.0</life>
-	<lifeDeviation>1.0</lifeDeviation>
-	<mass>0.0</mass>
-	<massDeviation>0.0</massDeviation>
-	<gravity>0.0 0.095 0.0</gravity>
-	<alpha>0.2</alpha>
-	<alphaDeviation>0.1</alphaDeviation>
-	<alphaAnimationEnabled>1</alphaAnimationEnabled>
-	<emissionPeriod>0.5</emissionPeriod>
-	<particlesPerEmission>1</particlesPerEmission>
-	<startingPosition>0.0 0.0 0.0</startingPosition>
-	<startingPositionDeviation>0.0 0.2 0.0</startingPositionDeviation>
-	<material>assets/smoke.ankimtl</material>
-	<size>0.4</size>
-	<sizeDeviation>0.2</sizeDeviation>
-	<sizeAnimation>0.5</sizeAnimation>
-	<usePhysicsEngine>0</usePhysicsEngine>
-	<maxNumberOfParticles>128</maxNumberOfParticles>
+	<life min="6" max="8"/>
+	<gravity value="0.0 0.095 0.0"/>
+
+	<initialAlpha min="0.2" max="0.3"/>
+	<finalAlpha value="0"/>
+
+	<initialSize min="0.4" max="1.2"/>
+	<finalSize min="0.7" max="1.9"/>
+
+	<startingPosition min="0 -0.2 0" max="0 0.2 0"/>
+
+	<maxNumberOfParticles value="128"/>
+	<emissionPeriod value="0.5"/>
+	<particlesPerEmission value="1"/>
+	<material value="assets/smoke.ankimtl"/>
 </particleEmitter>

+ 4 - 1
src/anki/core/App.cpp

@@ -75,6 +75,7 @@ public:
 	BufferedValue<Second> m_lightBinTime;
 	BufferedValue<Second> m_sceneUpdateTime;
 	BufferedValue<Second> m_visTestsTime;
+	BufferedValue<Second> m_physicsTime;
 
 	PtrSize m_allocatedCpuMem = 0;
 	U64 m_allocCount = 0;
@@ -110,7 +111,7 @@ public:
 
 		nk_style_push_style_item(ctx, &ctx->style.window.fixed_background, nk_style_item_color(nk_rgba(0, 0, 0, 128)));
 
-		if(nk_begin(ctx, "Stats", nk_rect(5, 5, 230, 350), 0))
+		if(nk_begin(ctx, "Stats", nk_rect(5, 5, 230, 380), 0))
 		{
 			nk_layout_row_dynamic(ctx, 17, 1);
 
@@ -120,6 +121,7 @@ public:
 			labelTime(ctx, m_lightBinTime.get(false), "Light bin");
 			labelTime(ctx, m_sceneUpdateTime.get(flush), "Scene update");
 			labelTime(ctx, m_visTestsTime.get(flush), "Visibility");
+			labelTime(ctx, m_physicsTime.get(flush), "Physics");
 
 			nk_label(ctx, " ", NK_TEXT_ALIGN_LEFT);
 			nk_label(ctx, "Memory:", NK_TEXT_ALIGN_LEFT);
@@ -618,6 +620,7 @@ Error App::mainLoop()
 			statsUi.m_lightBinTime.set(m_renderer->getStats().m_lightBinTime);
 			statsUi.m_sceneUpdateTime.set(m_scene->getStats().m_updateTime);
 			statsUi.m_visTestsTime.set(m_scene->getStats().m_visibilityTestsTime);
+			statsUi.m_physicsTime.set(m_scene->getStats().m_physicsUpdate);
 			statsUi.m_allocatedCpuMem = m_memStats.m_allocatedMem.load();
 			statsUi.m_allocCount = m_memStats.m_allocCount.load();
 			statsUi.m_freeCount = m_memStats.m_freeCount.load();

+ 35 - 18
src/anki/physics/PhysicsCollisionShape.cpp

@@ -32,28 +32,38 @@ PhysicsBox::PhysicsBox(PhysicsWorld* world, const Vec3& extend)
 }
 
 PhysicsTriangleSoup::PhysicsTriangleSoup(
-	PhysicsWorld* world, ConstWeakArray<Vec3> positions, ConstWeakArray<U32> indices)
+	PhysicsWorld* world, ConstWeakArray<Vec3> positions, ConstWeakArray<U32> indices, Bool convex)
 	: PhysicsCollisionShape(world)
 {
-	ANKI_ASSERT((indices.getSize() % 3) == 0);
-
-	m_mesh = getAllocator().newInstance<btTriangleMesh>();
-	for(U i = 0; i < indices.getSize(); i += 3)
+	if(!convex)
 	{
-		m_mesh->addTriangle(
-			toBt(positions[indices[i]]), toBt(positions[indices[i + 1]]), toBt(positions[indices[i + 2]]));
-	}
+		ANKI_ASSERT((indices.getSize() % 3) == 0);
+
+		m_mesh = getAllocator().newInstance<btTriangleMesh>();
+		for(U i = 0; i < indices.getSize(); i += 3)
+		{
+			m_mesh->addTriangle(
+				toBt(positions[indices[i]]), toBt(positions[indices[i + 1]]), toBt(positions[indices[i + 2]]));
+		}
 
-	// Create the dynamic shape
-	btGImpactMeshShape* shape = getAllocator().newInstance<btGImpactMeshShape>(m_mesh);
-	shape->setMargin(getWorld().getCollisionMargin());
-	shape->updateBound();
-	m_shape = shape;
+		// Create the dynamic shape
+		btGImpactMeshShape* shape = getAllocator().newInstance<btGImpactMeshShape>(m_mesh);
+		shape->setMargin(getWorld().getCollisionMargin());
+		shape->updateBound();
+		m_shape = shape;
 
-	// And the static one
-	btBvhTriangleMeshShape* triShape = getAllocator().newInstance<btBvhTriangleMeshShape>(m_mesh, true);
-	m_staticShape = triShape;
-	m_staticShape->setMargin(getWorld().getCollisionMargin());
+		// And the static one
+		btBvhTriangleMeshShape* triShape = getAllocator().newInstance<btBvhTriangleMeshShape>(m_mesh, true);
+		m_staticShape = triShape;
+		m_staticShape->setMargin(getWorld().getCollisionMargin());
+	}
+	else
+	{
+		btConvexHullShape* shape =
+			getAllocator().newInstance<btConvexHullShape>(&positions[0][0], positions.getSize(), sizeof(Vec3));
+
+		m_shape = shape;
+	}
 
 	m_shape->setUserPointer(static_cast<PhysicsObject*>(this));
 }
@@ -68,7 +78,14 @@ PhysicsTriangleSoup::~PhysicsTriangleSoup()
 
 btCollisionShape* PhysicsTriangleSoup::getBtShape(Bool forDynamicBodies) const
 {
-	return (forDynamicBodies) ? m_shape : m_staticShape;
+	if(!forDynamicBodies && m_staticShape)
+	{
+		return m_staticShape;
+	}
+	else
+	{
+		return m_shape;
+	}
 }
 
 } // end namespace anki

+ 2 - 1
src/anki/physics/PhysicsCollisionShape.h

@@ -74,7 +74,8 @@ private:
 	btTriangleMesh* m_mesh = nullptr;
 	btCollisionShape* m_staticShape = nullptr;
 
-	PhysicsTriangleSoup(PhysicsWorld* world, ConstWeakArray<Vec3> positions, ConstWeakArray<U32> indices);
+	PhysicsTriangleSoup(
+		PhysicsWorld* world, ConstWeakArray<Vec3> positions, ConstWeakArray<U32> indices, Bool convex = false);
 
 	~PhysicsTriangleSoup();
 

+ 1 - 1
src/anki/physics/PhysicsWorld.cpp

@@ -190,7 +190,7 @@ Error PhysicsWorld::update(Second dt)
 	// Update world
 	{
 		auto lock = lockBtWorld();
-		m_world->stepSimulation(dt, 2, 1.0 / 60.0);
+		m_world->stepSimulation(dt, 1, 1.0 / 60.0);
 	}
 
 	// Process trigger contacts

+ 3 - 1
src/anki/resource/CollisionResource.cpp

@@ -54,7 +54,9 @@ Error CollisionResource::load(const ResourceFilename& filename, Bool async)
 		DynamicArrayAuto<Vec3> positions(getTempAllocator());
 		ANKI_CHECK(loader.storeIndicesAndPosition(indices, positions));
 
-		m_physicsShape = physics.newInstance<PhysicsTriangleSoup>(positions, indices);
+		const Bool convex = !!(loader.getHeader().m_flags & MeshBinaryFile::Flag::CONVEX);
+
+		m_physicsShape = physics.newInstance<PhysicsTriangleSoup>(positions, indices, convex);
 	}
 	else
 	{

+ 1 - 1
src/anki/resource/ImageLoader.cpp

@@ -558,7 +558,7 @@ Error ImageLoader::load(ResourceFilePtr file, const CString& filename, U32 maxTe
 {
 	// get the extension
 	StringAuto ext(m_alloc);
-	getFilepathExtension(filename, m_alloc, ext);
+	getFilepathExtension(filename, ext);
 
 	if(ext.isEmpty())
 	{

+ 2 - 1
src/anki/resource/MeshLoader.h

@@ -27,8 +27,9 @@ public:
 	{
 		NONE = 0,
 		QUAD = 1 << 0,
+		CONVEX = 1 << 1,
 
-		ALL = QUAD,
+		ALL = QUAD | CONVEX,
 	};
 	ANKI_ENUM_ALLOW_NUMERIC_OPERATIONS(Flag, friend)
 

+ 68 - 123
src/anki/resource/ParticleEmitterResource.cpp

@@ -13,61 +13,16 @@
 namespace anki
 {
 
-static ANKI_USE_RESULT Error xmlVec3(const XmlElement& el_, const CString& str, Vec3& out)
+template<typename T>
+static ANKI_USE_RESULT Error getXmlVal(const XmlElement& el, const CString& tag, T& out, Bool& found)
 {
-	Error err = Error::NONE;
-	XmlElement el;
-
-	err = el_.getChildElementOptional(str, el);
-	if(err || !el)
-	{
-		return err;
-	}
-
-	err = el.getVec3(out);
-	return err;
+	return el.getAttributeNumberOptional(tag, out, found);
 }
 
-static ANKI_USE_RESULT Error xmlF32(const XmlElement& el_, const CString& str, F32& out)
+template<>
+ANKI_USE_RESULT Error getXmlVal(const XmlElement& el, const CString& tag, Vec3& out, Bool& found)
 {
-	Error err = Error::NONE;
-	XmlElement el;
-
-	err = el_.getChildElementOptional(str, el);
-	if(err || !el)
-	{
-		return err;
-	}
-
-	F64 tmp;
-	err = el.getNumber(tmp);
-	if(!err)
-	{
-		out = tmp;
-	}
-
-	return err;
-}
-
-static ANKI_USE_RESULT Error xmlU32(const XmlElement& el_, const CString& str, U32& out)
-{
-	Error err = Error::NONE;
-	XmlElement el;
-
-	err = el_.getChildElementOptional(str, el);
-	if(err || !el)
-	{
-		return err;
-	}
-
-	I64 tmp;
-	err = el.getNumber(tmp);
-	if(!err)
-	{
-		out = static_cast<U32>(tmp);
-	}
-
-	return err;
+	return el.getAttributeVectorOptional(tag, out, found);
 }
 
 ParticleEmitterProperties& ParticleEmitterProperties::operator=(const ParticleEmitterProperties& b)
@@ -76,16 +31,6 @@ ParticleEmitterProperties& ParticleEmitterProperties::operator=(const ParticleEm
 	return *this;
 }
 
-void ParticleEmitterProperties::updateFlags()
-{
-	m_forceEnabled = !isZero(m_particle.m_forceDirection.getLengthSquared());
-	m_forceEnabled = m_forceEnabled || !isZero(m_particle.m_forceDirectionDeviation.getLengthSquared());
-	m_forceEnabled =
-		m_forceEnabled && (m_particle.m_forceMagnitude != 0.0 || m_particle.m_forceMagnitudeDeviation != 0.0);
-
-	m_wordGravityEnabled = isZero(m_particle.m_gravity.getLengthSquared());
-}
-
 ParticleEmitterResource::ParticleEmitterResource(ResourceManager* manager)
 	: ResourceObject(manager)
 {
@@ -97,102 +42,102 @@ ParticleEmitterResource::~ParticleEmitterResource()
 
 Error ParticleEmitterResource::load(const ResourceFilename& filename, Bool async)
 {
-	U32 tmp;
-
 	XmlDocument doc;
 	ANKI_CHECK(openFileParseXml(filename, doc));
-	XmlElement rel; // Root element
-	ANKI_CHECK(doc.getChildElement("particleEmitter", rel));
-
-	// XML load
-	//
-	ANKI_CHECK(xmlF32(rel, "life", m_particle.m_life));
-	ANKI_CHECK(xmlF32(rel, "lifeDeviation", m_particle.m_lifeDeviation));
-
-	ANKI_CHECK(xmlF32(rel, "mass", m_particle.m_mass));
-	ANKI_CHECK(xmlF32(rel, "massDeviation", m_particle.m_massDeviation));
-
-	ANKI_CHECK(xmlF32(rel, "size", m_particle.m_size));
-	ANKI_CHECK(xmlF32(rel, "sizeDeviation", m_particle.m_sizeDeviation));
-	ANKI_CHECK(xmlF32(rel, "sizeAnimation", m_particle.m_sizeAnimation));
-
-	ANKI_CHECK(xmlF32(rel, "alpha", m_particle.m_alpha));
-	ANKI_CHECK(xmlF32(rel, "alphaDeviation", m_particle.m_alphaDeviation));
-
-	tmp = m_particle.m_alphaAnimation;
-	ANKI_CHECK(xmlU32(rel, "alphaAnimationEnabled", tmp));
-	m_particle.m_alphaAnimation = tmp;
-
-	ANKI_CHECK(xmlVec3(rel, "forceDirection", m_particle.m_forceDirection));
-	ANKI_CHECK(xmlVec3(rel, "forceDirectionDeviation", m_particle.m_forceDirectionDeviation));
-	ANKI_CHECK(xmlF32(rel, "forceMagnitude", m_particle.m_forceMagnitude));
-	ANKI_CHECK(xmlF32(rel, "forceMagnitudeDeviation", m_particle.m_forceMagnitudeDeviation));
+	XmlElement rootEl; // Root element
+	ANKI_CHECK(doc.getChildElement("particleEmitter", rootEl));
+
+#define ANKI_XML(varName, VarName) \
+	ANKI_CHECK( \
+		readVar(rootEl, #varName, m_particle.m_min##VarName, m_particle.m_max##VarName, &m_particle.m_min##VarName))
+
+	ANKI_XML(life, Life);
+	ANKI_XML(mass, Mass);
+	ANKI_XML(initialSize, InitialSize);
+	ANKI_XML(finalSize, FinalSize);
+	ANKI_XML(initialAlpha, InitialAlpha);
+	ANKI_XML(finalAlpha, FinalAlpha);
+	ANKI_XML(forceDirection, ForceDirection);
+	ANKI_XML(forceMagnitude, ForceMagnitude);
+	ANKI_XML(gravity, Gravity);
+	ANKI_XML(startingPosition, StartingPosition);
+
+#undef ANKI_XML
 
-	ANKI_CHECK(xmlVec3(rel, "gravity", m_particle.m_gravity));
-	ANKI_CHECK(xmlVec3(rel, "gravityDeviation", m_particle.m_gravityDeviation));
+	XmlElement el;
+	ANKI_CHECK(rootEl.getChildElement("maxNumberOfParticles", el));
+	ANKI_CHECK(el.getAttributeNumber("value", m_maxNumOfParticles));
 
-	ANKI_CHECK(xmlVec3(rel, "startingPosition", m_particle.m_startingPos));
-	ANKI_CHECK(xmlVec3(rel, "startingPositionDeviation", m_particle.m_startingPosDeviation));
+	ANKI_CHECK(rootEl.getChildElement("emissionPeriod", el));
+	ANKI_CHECK(el.getAttributeNumber("value", m_emissionPeriod));
 
-	ANKI_CHECK(xmlU32(rel, "maxNumberOfParticles", m_maxNumOfParticles));
+	ANKI_CHECK(rootEl.getChildElement("particlesPerEmission", el));
+	ANKI_CHECK(el.getAttributeNumber("value", m_particlesPerEmission));
 
-	ANKI_CHECK(xmlF32(rel, "emissionPeriod", m_emissionPeriod));
-	ANKI_CHECK(xmlU32(rel, "particlesPerEmittion", m_particlesPerEmittion));
-	tmp = m_usePhysicsEngine;
-	ANKI_CHECK(xmlU32(rel, "usePhysicsEngine", tmp));
-	m_usePhysicsEngine = tmp;
+	ANKI_CHECK(rootEl.getChildElementOptional("usePhysicsEngine", el));
+	if(el)
+	{
+		ANKI_CHECK(el.getAttributeNumber("value", m_usePhysicsEngine));
+	}
 
-	XmlElement el;
 	CString cstr;
-	ANKI_CHECK(rel.getChildElement("material", el));
-	ANKI_CHECK(el.getText(cstr));
+	ANKI_CHECK(rootEl.getChildElement("material", el));
+	ANKI_CHECK(el.getAttributeText("value", cstr));
 	ANKI_CHECK(getManager().loadResource(cstr, m_material, async));
 
-	// sanity checks
-	//
+	return Error::NONE;
+}
 
-	static const char* ERROR = "Particle emmiter: Incorrect or missing value %s";
+template<typename T>
+Error ParticleEmitterResource::readVar(
+	const XmlElement& rootEl, CString varName, T& minVal, T& maxVal, const T* defaultVal)
+{
+	XmlElement el;
 
-	if(m_particle.m_life <= 0.0)
+	// <varName>
+	ANKI_CHECK(rootEl.getChildElementOptional(varName, el));
+	if(!el && !defaultVal)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "life");
+		ANKI_RESOURCE_LOGE("<%s> is missing", varName.cstr());
 		return Error::USER_DATA;
 	}
 
-	if(m_particle.m_life - m_particle.m_lifeDeviation <= 0.0)
+	if(!el)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "lifeDeviation");
-		return Error::USER_DATA;
+		maxVal = minVal = *defaultVal;
+		return Error::NONE;
 	}
 
-	if(m_particle.m_size <= 0.0)
+	// value tag
+	Bool found;
+	ANKI_CHECK(getXmlVal(el, "value", minVal, found));
+	if(found)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "size");
-		return Error::USER_DATA;
+		maxVal = minVal;
+		return Error::NONE;
 	}
 
-	if(m_maxNumOfParticles < 1)
+	// min & max value tags
+	ANKI_CHECK(getXmlVal(el, "min", minVal, found));
+	if(!found)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "maxNumOfParticles");
+		ANKI_RESOURCE_LOGE("tag min is missing for <%s>", varName.cstr());
 		return Error::USER_DATA;
 	}
 
-	if(m_emissionPeriod <= 0.0)
+	ANKI_CHECK(getXmlVal(el, "max", maxVal, found));
+	if(!found)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "emissionPeriod");
+		ANKI_RESOURCE_LOGE("tag max is missing for <%s>", varName.cstr());
 		return Error::USER_DATA;
 	}
 
-	if(m_particlesPerEmittion < 1)
+	if(minVal > maxVal)
 	{
-		ANKI_RESOURCE_LOGE(ERROR, "particlesPerEmission");
+		ANKI_RESOURCE_LOGE("min tag should have less value than max for <%s>", varName.cstr());
 		return Error::USER_DATA;
 	}
 
-	// Calc some stuff
-	//
-	updateFlags();
-
 	return Error::NONE;
 }
 

+ 41 - 40
src/anki/resource/ParticleEmitterResource.h

@@ -38,59 +38,57 @@ public:
 	class
 	{
 	public:
-		/// Particle life
-		F32 m_life = 10.0;
-		F32 m_lifeDeviation = 0.0;
-
-		/// Particle mass
-		F32 m_mass = 1.0;
-		F32 m_massDeviation = 0.0;
-
-		/// Particle size. It is the size of the collision shape
-		F32 m_size = 1.0;
-		F32 m_sizeDeviation = 0.0;
-		F32 m_sizeAnimation = 1.0;
-
-		/// Alpha factor. If the material supports alpha then multiply with
-		/// this
-		F32 m_alpha = 1.0;
-		F32 m_alphaDeviation = 0.0;
-		Bool8 m_alphaAnimation = false;
-
-		/// Initial force. If not set only the gravity applies
-		Vec3 m_forceDirection = Vec3(0.0, 1.0, 0.0);
-		Vec3 m_forceDirectionDeviation = Vec3(0.0);
-		F32 m_forceMagnitude = 0.0; ///< Default 0.0
-		F32 m_forceMagnitudeDeviation = 0.0;
+		Second m_minLife = 10.0;
+		Second m_maxLife = 10.0;
+
+		F32 m_minMass = 1.0f;
+		F32 m_maxMass = 1.0f;
+
+		F32 m_minInitialSize = 1.0f;
+		F32 m_maxInitialSize = 1.0f;
+		F32 m_minFinalSize = 1.0f;
+		F32 m_maxFinalSize = 1.0f;
+
+		F32 m_minInitialAlpha = 1.0f;
+		F32 m_maxInitialAlpha = 1.0f;
+		F32 m_minFinalAlpha = 1.0f;
+		F32 m_maxFinalAlpha = 1.0f;
+
+		Vec3 m_minForceDirection = Vec3(0.0f, 1.0f, 0.0f);
+		Vec3 m_maxForceDirection = Vec3(0.0f, 1.0f, 0.0f);
+		F32 m_minForceMagnitude = 0.0f;
+		F32 m_maxForceMagnitude = 0.0f;
 
 		/// If not set then it uses the world's default
-		Vec3 m_gravity = Vec3(0.0);
-		Vec3 m_gravityDeviation = Vec3(0.0);
+		Vec3 m_minGravity = Vec3(MAX_F32);
+		Vec3 m_maxGravity = Vec3(MAX_F32);
 
 		/// This position is relevant to the particle emitter pos
-		Vec3 m_startingPos = Vec3(0.0);
-		Vec3 m_startingPosDeviation = Vec3(0.0);
+		Vec3 m_minStartingPosition = Vec3(0.0);
+		Vec3 m_maxStartingPosition = Vec3(0.0);
 	} m_particle;
 	/// @}
 
 	/// @name Emitter specific properties
 	/// @{
+	U32 m_maxNumOfParticles = 16; ///< The size of the particles vector. Required
+
+	F32 m_emissionPeriod = 1.0; ///< How often the emitter emits new particles. In secs. Required
+
+	U32 m_particlesPerEmission = 1; ///< How many particles are emitted every emission. Required
 
-	/// The size of the particles vector. Required
-	U32 m_maxNumOfParticles = 16;
-	/// How often the emitter emits new particles. In secs. Required
-	F32 m_emissionPeriod = 1.0;
-	/// How many particles are emitted every emission. Required
-	U32 m_particlesPerEmittion = 1;
-	/// Use bullet for the simulation
-	Bool m_usePhysicsEngine = true;
+	Bool8 m_usePhysicsEngine = false; ///< Use bullet for the simulation
 	/// @}
 
-	// Optimization flags
-	Bool m_forceEnabled;
-	Bool m_wordGravityEnabled;
+	Bool forceEnabled() const
+	{
+		return m_particle.m_maxForceMagnitude > 0.0f;
+	}
 
-	void updateFlags();
+	Bool wordGravityEnabled() const
+	{
+		return m_particle.m_maxGravity.x() < MAX_F32;
+	}
 };
 
 /// This is the properties of the particle emitter resource
@@ -122,6 +120,9 @@ private:
 	U8 m_lodCount = 1; ///< Cache the value from the material
 
 	void loadInternal(const XmlElement& el);
+
+	template<typename T>
+	ANKI_USE_RESULT Error readVar(const XmlElement& rootEl, CString varName, T& minVal, T& maxVal, const T* defaultVal);
 };
 /// @}
 

+ 1 - 1
src/anki/resource/ShaderProgramResource.cpp

@@ -553,7 +553,7 @@ void ShaderProgramResource::initVariant(ConstWeakArray<ShaderProgramResourceMuta
 
 	// Create the program name
 	StringAuto progName(getTempAllocator());
-	getFilepathFilename(getFilename(), getTempAllocator(), progName);
+	getFilepathFilename(getFilename(), progName);
 	char* cprogName = const_cast<char*>(progName.cstr());
 	if(progName.getLength() > MAX_GR_OBJECT_NAME_LENGTH)
 	{

+ 77 - 98
src/anki/scene/ParticleEmitterNode.cpp

@@ -19,26 +19,19 @@
 namespace anki
 {
 
-static F32 getRandom(F32 initial, F32 deviation)
+static F32 getRandom(F32 min, F32 max)
 {
-	return (deviation == 0.0) ? initial : initial + randFloat(deviation) * 2.0 - deviation;
+	F32 factor = randFloat(1.0f);
+	return mix(min, max, factor);
 }
 
-static Vec3 getRandom(const Vec3& initial, const Vec3& deviation)
+static Vec3 getRandom(const Vec3& min, const Vec3& max)
 {
-	if(deviation == Vec3(0.0))
-	{
-		return initial;
-	}
-	else
-	{
-		Vec3 out;
-		for(U i = 0; i < 3; i++)
-		{
-			out[i] = getRandom(initial[i], deviation[i]);
-		}
-		return out;
-	}
+	Vec3 out;
+	out.x() = mix(min.x(), max.x(), randFloat(1.0f));
+	out.y() = mix(min.y(), max.y(), randFloat(1.0f));
+	out.z() = mix(min.z(), max.z(), randFloat(1.0f));
+	return out;
 }
 
 /// Particle base
@@ -47,8 +40,16 @@ class ParticleEmitterNode::ParticleBase
 public:
 	Second m_timeOfBirth; ///< Keep the time of birth for nice effects
 	Second m_timeOfDeath = -1.0; ///< Time of death. If < 0.0 then dead
-	Second m_size = 1.0;
-	Second m_alpha = 1.0;
+
+	F32 m_initialSize;
+	F32 m_finalSize;
+	F32 m_crntSize;
+
+	F32 m_initialAlpha;
+	F32 m_finalAlpha;
+	F32 m_crntAlpha;
+
+	Vec4 m_crntPosition;
 
 	virtual ~ParticleBase()
 	{
@@ -67,25 +68,32 @@ public:
 	}
 
 	/// Revive the particle
-	virtual void revive(const ParticleEmitterNode& pe, const Transform& trf, Second prevUpdateTime, Second crntTime)
+	virtual void revive(
+		const ParticleEmitterProperties& props, const Transform& trf, Second prevUpdateTime, Second crntTime)
 	{
 		ANKI_ASSERT(isDead());
-		const ParticleEmitterProperties& props = pe;
 
 		// life
-		m_timeOfDeath = getRandom(crntTime + props.m_particle.m_life, props.m_particle.m_lifeDeviation);
+		m_timeOfDeath = crntTime + getRandom(props.m_particle.m_minLife, props.m_particle.m_maxLife);
 		m_timeOfBirth = crntTime;
+
+		// Size
+		m_initialSize = getRandom(props.m_particle.m_minInitialSize, props.m_particle.m_maxInitialSize);
+		m_finalSize = getRandom(props.m_particle.m_minFinalSize, props.m_particle.m_maxFinalSize);
+
+		// Alpha
+		m_initialAlpha = getRandom(props.m_particle.m_minInitialAlpha, props.m_particle.m_maxInitialAlpha);
+		m_finalAlpha = getRandom(props.m_particle.m_minFinalAlpha, props.m_particle.m_maxFinalAlpha);
 	}
 
-	/// Only relevant for non-bullet simulations
-	virtual void simulate(const ParticleEmitterNode& pe, Second prevUpdateTime, Second crntTime)
+	/// Common sumulation code
+	virtual void simulate(Second prevUpdateTime, Second crntTime)
 	{
-		(void)pe;
-		(void)prevUpdateTime;
-		(void)crntTime;
-	}
+		const F32 lifeFactor = (crntTime - m_timeOfBirth) / (m_timeOfDeath - m_timeOfBirth);
 
-	virtual const Vec4& getPosition() const = 0;
+		m_crntSize = mix(m_initialSize, m_finalSize, lifeFactor);
+		m_crntAlpha = mix(m_initialAlpha, m_finalAlpha, lifeFactor);
+	}
 };
 
 /// Simple particle for simple simulation
@@ -94,39 +102,35 @@ class ParticleEmitterNode::ParticleSimple : public ParticleEmitterNode::Particle
 public:
 	Vec4 m_velocity = Vec4(0.0);
 	Vec4 m_acceleration = Vec4(0.0);
-	Vec4 m_position;
 
-	void revive(const ParticleEmitterNode& pe, const Transform& trf, Second prevUpdateTime, Second crntTime) override
+	void revive(
+		const ParticleEmitterProperties& props, const Transform& trf, Second prevUpdateTime, Second crntTime) override
 	{
-		ParticleBase::revive(pe, trf, prevUpdateTime, crntTime);
+		ParticleBase::revive(props, trf, prevUpdateTime, crntTime);
 		m_velocity = Vec4(0.0);
 
-		const ParticleEmitterProperties& props = pe;
-
-		m_acceleration = getRandom(props.m_particle.m_gravity, props.m_particle.m_gravityDeviation).xyz0();
+		m_acceleration = getRandom(props.m_particle.m_minGravity, props.m_particle.m_maxGravity).xyz0();
 
 		// Set the initial position
-		m_position = getRandom(props.m_particle.m_startingPos, props.m_particle.m_startingPosDeviation).xyz0();
+		m_crntPosition =
+			getRandom(props.m_particle.m_minStartingPosition, props.m_particle.m_maxStartingPosition).xyz0();
 
-		m_position += trf.getOrigin();
+		m_crntPosition += trf.getOrigin();
 	}
 
-	void simulate(const ParticleEmitterNode& pe, Second prevUpdateTime, Second crntTime) override
+	void simulate(Second prevUpdateTime, Second crntTime) override
 	{
+		ParticleBase::simulate(prevUpdateTime, crntTime);
+
 		Second dt = crntTime - prevUpdateTime;
 
-		Vec4 xp = m_position;
+		Vec4 xp = m_crntPosition;
 		Vec4 xc = m_acceleration * (dt * dt) + m_velocity * dt + xp;
 
-		m_position = xc;
+		m_crntPosition = xc;
 
 		m_velocity += m_acceleration * dt;
 	}
-
-	const Vec4& getPosition() const override
-	{
-		return m_position;
-	}
 };
 
 /// Particle for bullet simulations
@@ -151,15 +155,14 @@ public:
 		m_body->activate(false);
 	}
 
-	void revive(const ParticleEmitterNode& pe, const Transform& trf, Second prevUpdateTime, Second crntTime) override
+	void revive(
+		const ParticleEmitterProperties& props, const Transform& trf, Second prevUpdateTime, Second crntTime) override
 	{
-		ParticleBase::revive(pe, trf, prevUpdateTime, crntTime);
-
-		const ParticleEmitterProperties& props = pe;
+		ParticleBase::revive(props, trf, prevUpdateTime, crntTime);
 
 		// pre calculate
-		const Bool forceFlag = props.m_forceEnabled;
-		const Bool worldGravFlag = props.m_wordGravityEnabled;
+		const Bool forceFlag = props.forceEnabled();
+		const Bool worldGravFlag = props.wordGravityEnabled();
 
 		// Activate it
 		m_body->activate(true);
@@ -170,44 +173,34 @@ public:
 		// force
 		if(forceFlag)
 		{
-			Vec3 forceDir = getRandom(props.m_particle.m_forceDirection, props.m_particle.m_forceDirectionDeviation);
+			Vec3 forceDir = getRandom(props.m_particle.m_minForceDirection, props.m_particle.m_maxForceDirection);
 			forceDir.normalize();
 
-			if(!pe.m_identityRotation)
-			{
-				// the forceDir depends on the particle emitter rotation
-				forceDir = trf.getRotation().getRotationPart() * forceDir;
-			}
+			// the forceDir depends on the particle emitter rotation
+			forceDir = trf.getRotation().getRotationPart() * forceDir;
 
-			const F32 forceMag =
-				getRandom(props.m_particle.m_forceMagnitude, props.m_particle.m_forceMagnitudeDeviation);
+			const F32 forceMag = getRandom(props.m_particle.m_minForceMagnitude, props.m_particle.m_maxForceMagnitude);
 			m_body->applyForce(forceDir * forceMag, Vec3(0.0f));
 		}
 
 		// gravity
 		if(!worldGravFlag)
 		{
-			m_body->setGravity(getRandom(props.m_particle.m_gravity, props.m_particle.m_gravityDeviation));
+			m_body->setGravity(getRandom(props.m_particle.m_minGravity, props.m_particle.m_maxGravity));
 		}
 
 		// Starting pos. In local space
-		Vec3 pos = getRandom(props.m_particle.m_startingPos, props.m_particle.m_startingPosDeviation);
-
-		if(pe.m_identityRotation)
-		{
-			pos += trf.getOrigin().xyz();
-		}
-		else
-		{
-			pos = trf.transform(pos);
-		}
+		Vec3 pos = getRandom(props.m_particle.m_minStartingPosition, props.m_particle.m_maxStartingPosition);
+		pos = trf.transform(pos);
 
 		m_body->setTransform(Transform(pos.xyz0(), trf.getRotation(), 1.0f));
+		m_crntPosition = pos.xyz0();
 	}
 
-	const Vec4& getPosition() const override
+	void simulate(Second prevUpdateTime, Second crntTime) override
 	{
-		return m_body->getTransform().getOrigin();
+		ParticleBase::simulate(prevUpdateTime, crntTime);
+		m_crntPosition = m_body->getTransform().getOrigin();
 	}
 };
 
@@ -380,7 +373,7 @@ void ParticleEmitterNode::onMoveComponentUpdate(MoveComponent& move)
 void ParticleEmitterNode::createParticlesPhysicsSimulation(SceneGraph* scene)
 {
 	PhysicsCollisionShapePtr collisionShape =
-		getSceneGraph().getPhysicsWorld().newInstance<PhysicsSphere>(m_particle.m_size / 2.0f);
+		getSceneGraph().getPhysicsWorld().newInstance<PhysicsSphere>(m_particle.m_minInitialSize / 2.0f);
 
 	PhysicsBodyInitInfo binit;
 	binit.m_shape = collisionShape;
@@ -389,13 +382,10 @@ void ParticleEmitterNode::createParticlesPhysicsSimulation(SceneGraph* scene)
 
 	for(U i = 0; i < m_maxNumOfParticles; i++)
 	{
-		binit.m_mass = getRandom(m_particle.m_mass, m_particle.m_massDeviation);
+		binit.m_mass = getRandom(m_particle.m_minMass, m_particle.m_maxMass);
 
 		PhysParticle* part = getSceneAllocator().newInstance<PhysParticle>(binit, this);
 
-		part->m_size = getRandom(m_particle.m_size, m_particle.m_sizeDeviation);
-		part->m_alpha = getRandom(m_particle.m_alpha, m_particle.m_alphaDeviation);
-
 		m_particles[i] = part;
 	}
 }
@@ -408,9 +398,6 @@ void ParticleEmitterNode::createParticlesSimpleSimulation()
 	{
 		ParticleSimple* part = getSceneAllocator().newInstance<ParticleSimple>();
 
-		part->m_size = getRandom(m_particle.m_size, m_particle.m_sizeDeviation);
-		part->m_alpha = getRandom(m_particle.m_alpha, m_particle.m_alphaDeviation);
-
 		m_particles[i] = part;
 	}
 }
@@ -431,6 +418,8 @@ Error ParticleEmitterNode::frameUpdate(Second prevUpdateTime, Second crntTime)
 	const F32* verts_base = verts;
 	(void)verts_base;
 
+	F32 maxParticleSize = -1.0f;
+
 	for(ParticleBase* p : m_particles)
 	{
 		if(p->isDead())
@@ -452,32 +441,21 @@ Error ParticleEmitterNode::frameUpdate(Second prevUpdateTime, Second crntTime)
 			ANKI_ASSERT((PtrSize(verts) + VERTEX_SIZE - PtrSize(verts_base)) <= m_vertBuffSize);
 
 			// This will calculate a new world transformation
-			p->simulate(*this, prevUpdateTime, crntTime);
+			p->simulate(prevUpdateTime, crntTime);
 
-			const Vec4& origin = p->getPosition();
+			const Vec4& origin = p->m_crntPosition;
 
 			aabbmin = aabbmin.min(origin);
 			aabbmax = aabbmax.max(origin);
 
-			F32 lifePercent = (crntTime - p->m_timeOfBirth) / (p->m_timeOfDeath - p->m_timeOfBirth);
-
 			verts[0] = origin.x();
 			verts[1] = origin.y();
 			verts[2] = origin.z();
 
-			// XXX set a flag for scale
-			verts[3] = p->m_size + (lifePercent * m_particle.m_sizeAnimation);
+			verts[3] = p->m_crntSize;
+			maxParticleSize = max(maxParticleSize, p->m_crntSize);
 
-			// Set alpha
-			if(m_particle.m_alphaAnimation)
-			{
-				verts[4] = sin(lifePercent * PI) * p->m_alpha;
-			}
-			else
-			{
-				verts[4] = p->m_alpha;
-			}
-			verts[4] = clamp(verts[4], 0.0f, 1.0f);
+			verts[4] = clamp(p->m_crntAlpha, 0.0f, 1.0f);
 
 			++m_aliveParticlesCount;
 			verts += 5;
@@ -486,8 +464,9 @@ Error ParticleEmitterNode::frameUpdate(Second prevUpdateTime, Second crntTime)
 
 	if(m_aliveParticlesCount != 0)
 	{
-		Vec4 min = aabbmin - m_particle.m_size;
-		Vec4 max = aabbmax + m_particle.m_size;
+		ANKI_ASSERT(maxParticleSize > 0.0f);
+		Vec4 min = aabbmin - maxParticleSize;
+		Vec4 max = aabbmax + maxParticleSize;
 		Vec4 center = (min + max) / 2.0;
 
 		m_obb = Obb(center.xyz0(), Mat3x4::getIdentity(), (max - center).xyz0());
@@ -521,7 +500,7 @@ Error ParticleEmitterNode::frameUpdate(Second prevUpdateTime, Second crntTime)
 
 			// do the rest
 			++particlesCount;
-			if(particlesCount >= m_particlesPerEmittion)
+			if(particlesCount >= m_particlesPerEmission)
 			{
 				break;
 			}

+ 2 - 0
src/anki/scene/SceneGraph.cpp

@@ -217,7 +217,9 @@ Error SceneGraph::update(Second prevUpdateTime, Second crntTime)
 	// Update
 	{
 		ANKI_TRACE_SCOPED_EVENT(SCENE_PHYSICS_UPDATE);
+		m_stats.m_physicsUpdate = HighRezTimer::getCurrentTime();
 		m_physics->update(crntTime - prevUpdateTime);
+		m_stats.m_physicsUpdate = HighRezTimer::getCurrentTime() - m_stats.m_physicsUpdate;
 	}
 
 	{

+ 1 - 0
src/anki/scene/SceneGraph.h

@@ -36,6 +36,7 @@ class SceneGraphStats
 public:
 	Second m_updateTime ANKI_DBG_NULLIFY;
 	Second m_visibilityTestsTime ANKI_DBG_NULLIFY;
+	Second m_physicsUpdate ANKI_DBG_NULLIFY;
 };
 
 /// The scene graph that  all the scene entities

+ 2 - 2
src/anki/scene/events/ScriptEvent.cpp

@@ -32,8 +32,8 @@ Error ScriptEvent::init(Second startTime, Second duration, CString script)
 	ANKI_CHECK(getSceneGraph().getScriptManager().newScriptEnvironment(m_env));
 
 	// Do the rest
-	String extension;
-	getFilepathExtension(script, getAllocator(), extension);
+	StringAuto extension(getAllocator());
+	getFilepathExtension(script, extension);
 
 	if(!extension.isEmpty() && extension == "lua")
 	{

+ 4 - 6
src/anki/util/Filesystem.cpp

@@ -8,9 +8,8 @@
 namespace anki
 {
 
-void getFilepathExtension(const CString& filename, GenericMemoryPoolAllocator<U8> alloc, String& out)
+void getFilepathExtension(const CString& filename, StringAuto& out)
 {
-	out.destroy(alloc);
 	const char* pc = std::strrchr(filename.cstr(), '.');
 
 	if(pc == nullptr)
@@ -22,14 +21,13 @@ void getFilepathExtension(const CString& filename, GenericMemoryPoolAllocator<U8
 		++pc;
 		if(*pc != '\0')
 		{
-			out.create(alloc, CString(pc));
+			out.create(CString(pc));
 		}
 	}
 }
 
-void getFilepathFilename(const CString& filename, GenericMemoryPoolAllocator<U8> alloc, String& out)
+void getFilepathFilename(const CString& filename, StringAuto& out)
 {
-	out.destroy(alloc);
 	const char* pc = std::strrchr(filename.cstr(), '/');
 
 	if(pc == nullptr)
@@ -41,7 +39,7 @@ void getFilepathFilename(const CString& filename, GenericMemoryPoolAllocator<U8>
 		++pc;
 		if(*pc != '\0')
 		{
-			out.create(alloc, CString(pc));
+			out.create(CString(pc));
 		}
 	}
 }

+ 2 - 2
src/anki/util/Filesystem.h

@@ -17,10 +17,10 @@ namespace anki
 Bool fileExists(const CString& filename);
 
 /// Get path extension.
-void getFilepathExtension(const CString& filename, GenericMemoryPoolAllocator<U8> alloc, String& out);
+void getFilepathExtension(const CString& filename, StringAuto& out);
 
 /// Get path filename.
-void getFilepathFilename(const CString& filename, GenericMemoryPoolAllocator<U8> alloc, String& out);
+void getFilepathFilename(const CString& filename, StringAuto& out);
 
 /// Return true if directory exists?
 Bool directoryExists(const CString& dir);

+ 10 - 0
src/anki/util/Functions.h

@@ -241,6 +241,16 @@ inline T rcast(Y from)
 	ANKI_ASSERT(from);
 	return reinterpret_cast<T>(from);
 }
+
+#define _ANKI_CONCATENATE(a, b) a##b
+
+/// Concatenate 2 preprocessor tokens.
+#define ANKI_CONCATENATE(a, b) _ANKI_CONCATENATE(a, b)
+
+#define _ANKI_STRINGIZE(a) #a
+
+/// Make a preprocessor token a string.
+#define ANKI_STRINGIZE(a) _ANKI_STRINGIZE(a)
 /// @}
 
 } // end namespace anki

+ 84 - 39
tools/scene/ExporterMesh.cpp

@@ -5,10 +5,13 @@
 
 #include "Exporter.h"
 #include <anki/resource/MeshLoader.h>
+#include <anki/Collision.h>
 #include <anki/Math.h>
 #include <cmath>
 #include <cfloat>
 
+using namespace anki;
+
 void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsigned vertCountPerFace) const
 {
 	std::string name = mesh.mName.C_Str();
@@ -16,7 +19,7 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 
 	const bool hasBoneWeights = mesh.mNumBones > 0;
 
-	anki::MeshBinaryFile::Header header;
+	MeshBinaryFile::Header header;
 	memset(&header, 0, sizeof(header));
 
 	// Checks
@@ -73,7 +76,7 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 
 	float maxPositionDistance = 0.0; // Distance of positions from zero
 	float maxUvDistance = -FLT_MAX, minUvDistance = FLT_MAX;
-	anki::Vec3 aabbMin(anki::MAX_F32), aabbMax(anki::MIN_F32);
+	Vec3 aabbMin(MAX_F32), aabbMax(MIN_F32);
 
 	{
 		const aiMatrix3x3 normalMat = (transform) ? aiMatrix3x3(*transform) : aiMatrix3x3();
@@ -177,43 +180,42 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 		}
 
 		// Bump aabbMax a bit
-		aabbMax += anki::EPSILON * 10.0f;
+		aabbMax += EPSILON * 10.0f;
 	}
 
 	// Chose the formats of the attributes
 	{
 		// Positions
-		auto& posa = header.m_vertexAttributes[anki::VertexAttributeLocation::POSITION];
+		auto& posa = header.m_vertexAttributes[VertexAttributeLocation::POSITION];
 		posa.m_bufferBinding = 0;
-		posa.m_format =
-			(maxPositionDistance < 2.0) ? anki::Format::R16G16B16A16_SFLOAT : anki::Format::R32G32B32_SFLOAT;
+		posa.m_format = (maxPositionDistance < 2.0) ? Format::R16G16B16A16_SFLOAT : Format::R32G32B32_SFLOAT;
 		posa.m_relativeOffset = 0;
 		posa.m_scale = 1.0;
 
 		// Normals
-		auto& na = header.m_vertexAttributes[anki::VertexAttributeLocation::NORMAL];
+		auto& na = header.m_vertexAttributes[VertexAttributeLocation::NORMAL];
 		na.m_bufferBinding = 1;
-		na.m_format = anki::Format::A2B10G10R10_SNORM_PACK32;
+		na.m_format = Format::A2B10G10R10_SNORM_PACK32;
 		na.m_relativeOffset = 0;
 		na.m_scale = 1.0;
 
 		// Tangents
-		auto& ta = header.m_vertexAttributes[anki::VertexAttributeLocation::TANGENT];
+		auto& ta = header.m_vertexAttributes[VertexAttributeLocation::TANGENT];
 		ta.m_bufferBinding = 1;
-		ta.m_format = anki::Format::A2B10G10R10_SNORM_PACK32;
+		ta.m_format = Format::A2B10G10R10_SNORM_PACK32;
 		ta.m_relativeOffset = sizeof(uint32_t);
 		ta.m_scale = 1.0;
 
 		// UVs
-		auto& uva = header.m_vertexAttributes[anki::VertexAttributeLocation::UV];
+		auto& uva = header.m_vertexAttributes[VertexAttributeLocation::UV];
 		uva.m_bufferBinding = 1;
 		if(minUvDistance >= 0.0 && maxUvDistance <= 1.0)
 		{
-			uva.m_format = anki::Format::R16G16_UNORM;
+			uva.m_format = Format::R16G16_UNORM;
 		}
 		else
 		{
-			uva.m_format = anki::Format::R16G16_SFLOAT;
+			uva.m_format = Format::R16G16_SFLOAT;
 		}
 		uva.m_relativeOffset = sizeof(uint32_t) * 2;
 		uva.m_scale = 1.0;
@@ -221,15 +223,15 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 		// Bone weight
 		if(hasBoneWeights)
 		{
-			auto& bidxa = header.m_vertexAttributes[anki::VertexAttributeLocation::BONE_INDICES];
+			auto& bidxa = header.m_vertexAttributes[VertexAttributeLocation::BONE_INDICES];
 			bidxa.m_bufferBinding = 2;
-			bidxa.m_format = anki::Format::R16G16B16A16_UINT;
+			bidxa.m_format = Format::R16G16B16A16_UINT;
 			bidxa.m_relativeOffset = 0;
 			bidxa.m_scale = 1.0;
 
-			auto& wa = header.m_vertexAttributes[anki::VertexAttributeLocation::BONE_WEIGHTS];
+			auto& wa = header.m_vertexAttributes[VertexAttributeLocation::BONE_WEIGHTS];
 			wa.m_bufferBinding = 2;
-			wa.m_format = anki::Format::R8G8B8A8_UNORM;
+			wa.m_format = Format::R8G8B8A8_UNORM;
 			wa.m_relativeOffset = sizeof(uint16_t) * 4;
 			wa.m_scale = 1.0;
 		}
@@ -240,12 +242,12 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 		header.m_vertexBufferCount = 2;
 
 		// First buff has positions
-		const auto& posa = header.m_vertexAttributes[anki::VertexAttributeLocation::POSITION];
-		if(posa.m_format == anki::Format::R32G32B32_SFLOAT)
+		const auto& posa = header.m_vertexAttributes[VertexAttributeLocation::POSITION];
+		if(posa.m_format == Format::R32G32B32_SFLOAT)
 		{
 			header.m_vertexBuffers[0].m_vertexStride = sizeof(float) * 3;
 		}
-		else if(posa.m_format == anki::Format::R16G16B16A16_SFLOAT)
+		else if(posa.m_format == Format::R16G16B16A16_SFLOAT)
 		{
 			header.m_vertexBuffers[0].m_vertexStride = sizeof(uint16_t) * 4;
 		}
@@ -265,11 +267,55 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 		}
 	}
 
+	// Find if it's a convex shape
+	Bool convex = true;
+	for(unsigned i = 0; i < mesh.mNumFaces && vertCountPerFace == 3; i++)
+	{
+		const aiFace& face = mesh.mFaces[i];
+
+		Vec3 triangle[3];
+
+		for(unsigned j = 0; j < 3; j++)
+		{
+			unsigned idx = face.mIndices[j];
+			triangle[j].x() = positions[idx * 3 + 0];
+			triangle[j].y() = positions[idx * 3 + 1];
+			triangle[j].z() = positions[idx * 3 + 2];
+		}
+
+		// Check that all positions are behind the plane
+		Plane plane(triangle[0].xyz0(), triangle[1].xyz0(), triangle[2].xyz0());
+
+		for(unsigned j = 0; j < positions.size(); j += 3)
+		{
+			Vec3 pos;
+			pos.x() = positions[j + 0];
+			pos.y() = positions[j + 1];
+			pos.z() = positions[j + 2];
+
+			F32 test = plane.test(pos.xyz0());
+			if(test > EPSILON)
+			{
+				convex = false;
+				break;
+			}
+		}
+
+		if(convex == false)
+		{
+			break;
+		}
+	}
+
 	// Write some other header stuff
 	{
-		memcpy(&header.m_magic[0], anki::MeshBinaryFile::MAGIC, 8);
-		header.m_flags = (vertCountPerFace == 4) ? anki::MeshBinaryFile::Flag::QUAD : anki::MeshBinaryFile::Flag::NONE;
-		header.m_indexType = anki::IndexType::U16;
+		memcpy(&header.m_magic[0], MeshBinaryFile::MAGIC, 8);
+		header.m_flags = (vertCountPerFace == 4) ? MeshBinaryFile::Flag::QUAD : MeshBinaryFile::Flag::NONE;
+		if(convex)
+		{
+			header.m_flags |= MeshBinaryFile::Flag::CONVEX;
+		}
+		header.m_indexType = IndexType::U16;
 		header.m_totalIndexCount = mesh.mNumFaces * vertCountPerFace;
 		header.m_totalVertexCount = mesh.mNumVertices;
 		header.m_subMeshCount = 1;
@@ -286,7 +332,7 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 
 	// Write sub meshes
 	{
-		anki::MeshBinaryFile::SubMesh smesh;
+		MeshBinaryFile::SubMesh smesh;
 		smesh.m_firstIndex = 0;
 		smesh.m_indexCount = header.m_totalIndexCount;
 		smesh.m_aabbMin = aabbMin;
@@ -322,12 +368,12 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 
 	// Write first vert buffer
 	{
-		const auto& posa = header.m_vertexAttributes[anki::VertexAttributeLocation::POSITION];
-		if(posa.m_format == anki::Format::R32G32B32_SFLOAT)
+		const auto& posa = header.m_vertexAttributes[VertexAttributeLocation::POSITION];
+		if(posa.m_format == Format::R32G32B32_SFLOAT)
 		{
 			file.write(reinterpret_cast<char*>(&positions[0]), positions.size() * sizeof(positions[0]));
 		}
-		else if(posa.m_format == anki::Format::R16G16B16A16_SFLOAT)
+		else if(posa.m_format == Format::R16G16B16A16_SFLOAT)
 		{
 			std::vector<uint16_t> pos16;
 			pos16.resize(mesh.mNumVertices * 4);
@@ -337,10 +383,10 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 			uint16_t* p16 = &pos16[0];
 			while(p32 != p32end)
 			{
-				p16[0] = anki::F16(p32[0]).toU16();
-				p16[1] = anki::F16(p32[1]).toU16();
-				p16[2] = anki::F16(p32[2]).toU16();
-				p16[3] = anki::F16(0.0f).toU16();
+				p16[0] = F16(p32[0]).toU16();
+				p16[1] = F16(p32[1]).toU16();
+				p16[2] = F16(p32[2]).toU16();
+				p16[3] = F16(0.0f).toU16();
 
 				p32 += 3;
 				p16 += 4;
@@ -370,22 +416,21 @@ void Exporter::exportMesh(const aiMesh& mesh, const aiMatrix4x4* transform, unsi
 		{
 			const auto& inVert = ntVerts[i];
 
-			verts[i].m_n = anki::packColorToR10G10B10A2SNorm(inVert.m_n[0], inVert.m_n[1], inVert.m_n[2], 0.0);
-			verts[i].m_t =
-				anki::packColorToR10G10B10A2SNorm(inVert.m_t[0], inVert.m_t[1], inVert.m_t[2], inVert.m_t[3]);
+			verts[i].m_n = packColorToR10G10B10A2SNorm(inVert.m_n[0], inVert.m_n[1], inVert.m_n[2], 0.0);
+			verts[i].m_t = packColorToR10G10B10A2SNorm(inVert.m_t[0], inVert.m_t[1], inVert.m_t[2], inVert.m_t[3]);
 
 			const float uv[2] = {inVert.m_uv[0], inVert.m_uv[1]};
-			const anki::Format uvfmt = header.m_vertexAttributes[anki::VertexAttributeLocation::UV].m_format;
-			if(uvfmt == anki::Format::R16G16_UNORM)
+			const Format uvfmt = header.m_vertexAttributes[VertexAttributeLocation::UV].m_format;
+			if(uvfmt == Format::R16G16_UNORM)
 			{
 				assert(uv[0] <= 1.0 && uv[0] >= 0.0 && uv[1] <= 1.0 && uv[1] >= 0.0);
 				verts[i].m_uv[0] = uv[0] * 0xFFFF;
 				verts[i].m_uv[1] = uv[1] * 0xFFFF;
 			}
-			else if(uvfmt == anki::Format::R16G16_SFLOAT)
+			else if(uvfmt == Format::R16G16_SFLOAT)
 			{
-				verts[i].m_uv[0] = anki::F16(uv[0]).toU16();
-				verts[i].m_uv[1] = anki::F16(uv[1]).toU16();
+				verts[i].m_uv[0] = F16(uv[0]).toU16();
+				verts[i].m_uv[1] = F16(uv[1]).toU16();
 			}
 			else
 			{