Tutorial 201: - Advanced Custom Material System

From Newton Wiki
Jump to: navigation, search

Template:Languages
In tutorial 105 we introduced the material Interface to create very basic scene with different materials integration between bodies. The Material system is very powerful and very flexible, however is has one problem.

Material only can only be assigned to Rigid Bodies, This But if can result in very complex set of function call back when a body has a shape that is composed of different parte the application want to act as different Material properties.

The Newton Engine do not provide Native support for treating Sub Material behaviors in collision call back, instead this is delegated to be implemented by the end application. One way to deal with this is by writing switch case in the Material call back, were the call back check for the shape ID and the appropriate case handle the integration. For example say that a solid box colliding with a level mesh with three different types of faces: concrete, bricks, wood.

In the Call back the application will detect find the Level Mesh and read the ID of the colliding Face. If the ID is for a concrete face, the concrete case will handle the behaviors.

This solution work and is is very simple, however if have the problems that because is implemented by writing actual code, it requires a lot of maintenance each time a new material is added to the scene.

Also it can result into a quadric expansion of materials integrations, just consider a dynamics body with a compound shape made of three different solids: concrete, bricks, wood. Now when this object collides with the level mesh, there are 3 x 3 possible pairs of shapes IDs that can come in contact, you can see how this can become very hard to maintain as more and more complexity is added to the application.

What we need is a way to unify the handling of sub material interactions in a data driven maner with a consistent interface.

This is the subject of this tutorial. One thing I want to say is the Material integrations are a quadric problem, if the application want to tread material in physics way. You do not need to implement a complex material system in order to use Newton, you can use the traditional heuristic approach of material functions, or bodies priorities. For example you can say the friction between to colliding shapes is the Average, the median, of the higher, etc. You can play the sound of the body with higher priority, or play the two sounds, etc. I will not going into detail explaining material systems bases of heuristic, but it is very easy to implement that approach.

With the previous motivation let us explain this advanced material system.

Select project Tutorial_201_AbvanceMaterial as Startup project in visual studio and open file CreateScene.cpp in the editor and find function CreateScene. Function CreateScene is a typical physics and graphics scene with a Level Mesh as background, few simple dynamics boxes with a choice of tow material and few complex dynamics bodies made out of tree different collision shapes with a different ID each.

The part this in different between this scene and all previous scenes on these tutorial is that now we plug a Generic a Material Manager, which happen at eth very begging of function CreateScene

void CreateScene (NewtonWorld* world, SceneManager* sceneManager)
{
	Entity* floor;
	NewtonCollision* shape;
	NewtonBody* floorBody;
	void* materialManager;
	SoundManager* sndManager;
	PhysicsMaterialInteration matInterations;
	
	sndManager = sceneManager->GetSoundManager();

	// Create the material for this scene, and attach it to the Newton World
	materialManager = CreateMaterialManager (world, sndManager);

	// add the Material table
	matInterations.m_restitution = 0.6f;
	matInterations.m_staticFriction = 0.6f;
	matInterations.m_kineticFriction = 0.3f;
	matInterations.m_scrapingSound = NULL;

	matInterations.m_impactSound = sndManager->LoadSound ("metalMetal.wav");
	AddMaterilInteraction (materialManager, m_metal, m_metal, &matInterations);

	matInterations.m_impactSound = sndManager->LoadSound ("boxBox.wav");
	AddMaterilInteraction (materialManager, m_wood, m_wood, &matInterations);

	matInterations.m_impactSound = sndManager->LoadSound ("metalBox.wav");
	AddMaterilInteraction (materialManager, m_metal, m_wood, &matInterations);
	
	matInterations.m_impactSound = sndManager->LoadSound ("grass0.wav");
	AddMaterilInteraction (materialManager, m_wood, m_grass, &matInterations);

	matInterations.m_impactSound = sndManager->LoadSound ("boxHit.wav");
	AddMaterilInteraction (materialManager, m_wood, m_bricks, &matInterations);

	matInterations.m_impactSound = sndManager->LoadSound ("grass1.wav");
	AddMaterilInteraction (materialManager, m_metal, m_grass, &matInterations);
	AddMaterilInteraction (materialManager, m_grass, m_bricks, &matInterations);

	matInterations.m_impactSound = sndManager->LoadSound ("metal.wav");
	AddMaterilInteraction (materialManager, m_metal, m_bricks, &matInterations);
	AddMaterilInteraction (materialManager, m_grass, m_grass, &matInterations);


The Class Material Manager is a very basic Material system that for these tutorials, is capable to handling unique: static and kinetic friction, restitution and sounds effect between two different colliding shapes. The definition of this particular implementation looks like this:

struct PhysicsMaterialInteration
{
	dFloat m_restitution;
	dFloat m_staticFriction;
	dFloat m_kineticFriction;

	void* m_impactSound;
	void* m_scrapingSound;
};

class SoundManager;

// Create an advance Material Manager 
void* CreateMaterialManager (NewtonWorld* world, SoundManager* soudnManager);

// Set Default Material
void AddDefaultMaterial (void* manager, PhysicsMaterialInteration* interation);

// This Add and Interaction between two Material to the database 
void AddMaterilInteraction (void* manager, int MaterialId_0, int MaterialId_1, PhysicsMaterialInteration* interation);

Note: the application can use this basic system as a template to implement this own systems with the application specific functionality. For this tutorial I added sounds as an example of how to implement one application specific feature, but this is not the only feature the can be added. Thing like particles effects, events generated by triggers, etc is also possible.

The definition of the MaterialManager is hidden from the public interface and looks like this:

class MaterialManager
{
	public:
	struct HashMap
	{
		unsigned m_id0;
		unsigned m_id1;
		unsigned m_key;
		int m_InterationEntry;
	};

	MaterialManager::MaterialManager(NewtonWorld* world, SoundManager* soudnManager)
	{
		int defaultMaterialId;

		// save sound manager
		m_soundManager = soudnManager;

		// Save the current User Data assigned to the world
		m_userData = NewtonWorldGetUserData(world);

		// set the new user Data;
		NewtonWorldSetUserData (world, this);

		// Save the Current Destructor Call back
		m_destrutorCallBack = NewtonWorldGetDestructorCallBack(world);

		// Set The Material Call Back 
		NewtonWorldSetDestructorCallBack (world, MaterialDestructor);


		// this Manager will use Default Material to handle all material integrations
		defaultMaterialId = NewtonMaterialGetDefaultGroupID (world);
		NewtonMaterialSetCollisionCallback (world, defaultMaterialId, defaultMaterialId, NULL, NULL, GenericContactProcess);

		// clear the default to material,
		memset (&m_defaultInteration, 0, sizeof (PhysicsMaterialInteration));

		// crate space for the the Database
		m_interactionCount = 0;
		m_maxInteractionSize = 8 * 2;
		m_InteractionTable = (PhysicsMaterialInteration*) malloc (m_maxInteractionSize * sizeof (PhysicsMaterialInteration));

		// create the HaspMap table;
		m_hapMapSize = 17;
		m_hapMap = (HashMap*) malloc (m_hapMapSize * sizeof (HashMap));
		memset (m_hapMap, 0, m_hapMapSize * sizeof (HashMap));
	}

	MaterialManager::~MaterialManager(void)
	{
		// destroy all used Memory;
		free (m_hapMap);
		free (m_InteractionTable);
	}

	static void MaterialDestructor (const NewtonWorld* newtonWorld)
	{
		MaterialManager* manager;

		// get the pointer to the Material Class
		manager = (MaterialManager*) NewtonWorldGetUserData(newtonWorld);

		// Installed the other Object so that other object can be destroyed too
		NewtonWorldSetUserData (newtonWorld, manager->m_userData);
		if (manager->m_destrutorCallBack) {
			manager->m_destrutorCallBack (newtonWorld);
		}

		// Destroy the Material Class
		delete manager;
	}

	unsigned GetHashCode (int id0, int id1, int hapMapSize) const
	{
		unsigned crc;
		unsigned val;

		if (id0 < id1) {
			int id;
			id = id0;
			id0 = id1;
			id1 = id;
		}

		crc = 0;
		val = randBits[((crc >> 24) ^ id0) & 0xff];
		crc = (crc << 8) ^ val;

		val = randBits[((crc >> 24) ^ id1) & 0xff];
		crc = (crc << 8) ^ val;

		val = randBits[((crc >> 24) ^ hapMapSize) & 0xff];
		crc = (crc << 8) ^ val;
		
		return crc;
	}

	int ReHachTheTable (int size)
	{
		HashMap *newMap;

		// allocate a new Has space
		newMap = (HashMap*) malloc (size * sizeof (HashMap));
		memset (newMap, 0, size * sizeof (HashMap));

		// copy each entry to the new hash map
		for (int i = 0; i < m_hapMapSize; i ++) {
			if (m_hapMap[i].m_key) {
				unsigned index;
				unsigned hashCode;
				hashCode = GetHashCode (m_hapMap[i].m_id0, m_hapMap[i].m_id1, size);
				index = hashCode % size;
				if (newMap[index].m_key) {
					// if there is a collision return with an error
					free (newMap);
					return 0;
				}
				newMap[index] = m_hapMap[i];
				newMap[index].m_key = hashCode;
			}
		}
		
		// delete old Hash map and set the pointer to the new map
		free (m_hapMap);
		m_hapMap = newMap;
		m_hapMapSize = size;

		return 1;
	}

	void AddInteraction (int materialId_0, int materialId_1, PhysicsMaterialInteration& interation)
	{
//		unsigned index;
		unsigned hashCode;

		//add the Material interaction to the end of the list
		if (m_interactionCount >= m_maxInteractionSize) {
			// the interaction pool is full we need to reallocate a larger Pool 
			PhysicsMaterialInteration* newPool;

			newPool = (PhysicsMaterialInteration*) malloc (2 * m_maxInteractionSize * sizeof (PhysicsMaterialInteration));
			memcpy (newPool, m_InteractionTable, m_interactionCount* sizeof (PhysicsMaterialInteration));
			free (m_InteractionTable);

			m_InteractionTable = newPool;
			m_maxInteractionSize *= 2;
		}

		m_InteractionTable[m_interactionCount] = interation;

		// find the entry into the HahMap table;
		hashCode = GetHashCode (materialId_0, materialId_1, m_hapMapSize);
//		index = hashCode % m_hapMapSize;

		for (int i = 0; i < MAX_HASH_COLLISION; i ++) {
			int entry = (hashCode + i) % m_hapMapSize;
			if (m_hapMap[entry].m_key == hashCode)  { 
				return;
			}
		}

		for (;;) {	
			for (int i = 0; i < MAX_HASH_COLLISION; i ++) {

				int entry = (hashCode + i) % m_hapMapSize;
				if (!m_hapMap[entry].m_key) {
					// save the Key information into the Hash Map
					m_hapMap[entry].m_key = hashCode;
					m_hapMap[entry].m_id0 = materialId_0;
					m_hapMap[entry].m_id1 = materialId_1;
					m_hapMap[entry].m_InterationEntry = m_interactionCount;
					m_interactionCount ++;
					return;
				}
			}
			for (int size = 2 * m_hapMapSize; !ReHachTheTable (size); size *= 2);	
			hashCode = GetHashCode (materialId_0, materialId_1, m_hapMapSize);
		}
	}

	
	const PhysicsMaterialInteration* GetMaterial (int id0, int id1) const
	{
		unsigned code;

		code = GetHashCode (id0, id1, m_hapMapSize);
		for (int i = 0; i < MAX_HASH_COLLISION; i ++) {
			unsigned index;
			index = (code + i) % m_hapMapSize;
			if (m_hapMap[index].m_key == code) {
				return &m_InteractionTable[m_hapMap[index].m_InterationEntry];
			}
		}

		return &m_defaultInteration;
	}
				

	static void GenericContactProcess (const NewtonJoint* contactJoint, dFloat timestep, int threadIndex)
	{
		MaterialManager* manager;
		manager = (MaterialManager*) NewtonWorldGetUserData (NewtonBodyGetWorld (NewtonJointGetBody0(contactJoint)));
		manager->ContactProcess (contactJoint, timestep, threadIndex);
	}


	void ContactProcess (const NewtonJoint* contactJoint, dFloat timestep, int threadIndex)
	{
		dFloat contactBestSpeed;
		dVector contactPosit;
		void* bestSound;
		NewtonBody* body0;
		NewtonBody* body1;

		bestSound = NULL;
		contactBestSpeed = 0.5f;
		body0 = NewtonJointGetBody0(contactJoint);
		body1 = NewtonJointGetBody1(contactJoint);
		for (void* contact = NewtonContactJointGetFirstContact (contactJoint); contact; contact = NewtonContactJointGetNextContact (contactJoint, contact)) {
			int id0;
			int id1;
			dFloat contactNormalSpeed;
			NewtonMaterial* material;
			const PhysicsMaterialInteration* appMaterial;

			// get the material for this contact;
			material = NewtonContactGetMaterial (contact);

			id0 = NewtonMaterialGetBodyCollisionID (material, body0);
			if (id0 == 0) {
				id0	= NewtonMaterialGetContactFaceAttribute (material);
			}
			id1 = NewtonMaterialGetBodyCollisionID (material, body1);
			if (id1 == 0) {
				id1	= NewtonMaterialGetContactFaceAttribute (material);
			}

			// get the material interaction for these two Ids from the hash map 
			appMaterial = GetMaterial (id0, id1);

			// set the physics material properties.
			NewtonMaterialSetContactElasticity (material, appMaterial->m_restitution);
			NewtonMaterialSetContactFrictionCoef (material, appMaterial->m_staticFriction, appMaterial->m_kineticFriction, 0);
			NewtonMaterialSetContactFrictionCoef (material, appMaterial->m_staticFriction, appMaterial->m_kineticFriction, 1);

			// do any other action the application desires.
			// for these toturial I am playing a sound effect.
			contactNormalSpeed = NewtonMaterialGetContactNormalSpeed (material);
			if (contactNormalSpeed > contactBestSpeed){
				// this will collect the strongest contact impact
				contactBestSpeed = contactNormalSpeed;
				dVector normal;
				contactBestSpeed = contactNormalSpeed;
				NewtonMaterialGetContactPositionAndNormal (material, &contactPosit[0], &normal[0]);
				bestSound = appMaterial->m_impactSound;
			}
		}


		
		// if the strongest impact contact had a sound effect now play that effect.
		if (bestSound) {
			dFloat volume;
			dFloat dist2;

			// control sound volume based on camera distance to the contact
			dVector eyePoint (GetCameraEyePoint() - contactPosit);
			dist2 = eyePoint % eyePoint;
			if (dist2 < (MAX_SOUND_DISTANCE * MAX_SOUND_DISTANCE)) {
				volume = 1.0f;
				if (dist2 > (MIN_SOUND_DISTANCE * MIN_SOUND_DISTANCE)) {
					volume = 1.0f - (dSqrt (dist2) - MIN_SOUND_DISTANCE) / (MAX_SOUND_DISTANCE -  MIN_SOUND_DISTANCE);
				}
				// play this sound effect
				m_soundManager->Play (bestSound, volume, 0);
			}
		}
	}

	void* m_userData;						// user data in the work for destructor Chaining
	SoundManager* m_soundManager;
	NewtonDestroyWorld m_destrutorCallBack;

	int m_hapMapSize;
	int m_interactionCount;
	int m_maxInteractionSize;
	HashMap* m_hapMap;
	PhysicsMaterialInteration* m_InteractionTable; 
	PhysicsMaterialInteration m_defaultInteration;
};

The more important functions are AddInteraction and ContactProcess which I am going to explain better.

Function AddInteraction basically takes the two indices of the material making the interaction and calculate the hash code to look up and entry in to a hash table. The implementation of the hash database is very similar the one use internally by the engine to handle rigid bodies materials. When the slot calculated by the hash function is occupied, the code tried to handle the case as a hash collision, and try to find an entry slot in eth N adjacent entries, and if that fail then it expand the database and rehash all of the entries that are already in the table. More information about hash map databases is out of the scope of this tutorial, but you can found on any text book about data structures or in the internet.


Function ContactProcess is the interface to the collision system of the physics engine. When two bodies collide this function is called with the pointer to the contact joint containing all of the information to process all of the contacts in this call. This function is different to function GenericContactProcess in Tutorial 105: - Basic Materials, in that here we can process per contacts materials in a unified way. Basically the code iterates over each contact in the contact joint, getting the collision ID of the two collison shapes that generate that contact. There is one special case when the shape is generated by a face on one of the poliginal collaion shapes. For this the code uses shape ID zero as a flag that indicates that shape id soudl be read form the the Face ID. Once the code have the two IDs of the two colliding shapes, it search for a Material description stored at that location in the Hash map database. From that point the rest is the same as before, the code copies the physical properties for those contacts in to the contact, plus it also do some Application specific functionality. In the case we find for the strongest impact contact, and we save the position and the sound clip to be played. The application can do anything it desires. With this we concluded the Advance Material system tutorial.

This is the material system we will use from now on all tutorials, I encourage the end user to do the same since it is a lot more flexible than using switch cases, and more important is open source and the end application have control over it.

It is always possible to combine the tow systems, for example if you can have a body type that is so special that it requires specialize functionality, but adding that functionality to the generic material system will make more complex than what you desire. In this case you can single out that body with body type and treat it with a special set of callback, while all other bodies are handled by the generic system.