ofxAssimpModelLoader - color faces according to materials without using lights

I am just trying to draw imported objects (I am using .obj but .stl works as well) according to the model’s material colors. The key is that I actually want to be able to draw them as if I were drawing objects in OF by just calling ofSetColor() and then drawing the objects - no lights, no materials. I want them to be drawn using the flat unshaded colors that using only ofSetColor() will give you.

I have had a look at this forum page and applied what was suggested there and it has worked but this solution relies on lights.

I have also succeeded in accessing the diffuse color of the assimpModel:

ofMaterial material = assimpModel.getMaterialForMesh(0);
ofColor col = material.getDiffuseColor();

maybe this can be used like so:

ofSetColor(col);
// draw mesh(0)?

Can the meshes in an imported Assimp model be drawn and altered separately like this (the model I am working with has a few cylinders and spheres)?
Or perhaps there is an easier way…

Hey @rgthings I’ll see if I can be helpful, though I haven’t used ofxAssimpModelLoader.

It looks to me like a model can have multiple ofMesh objects that can be accessed with ofxAssimpModelLoader::getMesh(), which takes either an int or a string as an argument. So if the model does have multiple ofMesh, the following seems like it should work OK but you’ll have to try, maybe in a loop:

// in ofApp::draw()
for(size_t i{0}; i < assimpModel.getMeshCount(); ++i){
    ofMaterial material = assimpModel.getMaterialForMesh(i);
    ofColor col = material.getDiffuseColor();
    ofSetColor(col);
    assimpModel.getMesh(i).draw();
}

An ofMesh has a std::vector<ofFloatColor> that describes a color for each vertex in the mesh. I think the default color is white, so the mesh will draw in the color set by ofSetColor(). There isn’t a .setColors() method, where you can set the entire mesh to one color. An easy approach is to use ofMesh::addColor() or ofMesh()::setColor() in a loop, or something like this will work too:

    // make an ofFloatColor
    ofFloatColor color{ofRandom(1.0), ofRandom(1.0), ofRandom(1.0)};
    // make a std::vector of the same size as the vertices vector, initialized with the color
    std::vector<ofFloatColor> colors(mesh.getVertices().size(), color);
    // copy the vector into the mesh
    mesh.getColors() = colors;

OK hope this helps but definitely post back if you have questions or issues.

1 Like

Thank you again TimChi. After wrestling with this all day, I have found this out: The first method you suggest (I haven’t tried the second yet) works in terms of rendering the colours accurately. Unfortunately setPosition() and setScale() have no effect. Those methods only work when I use assmipModel.drawFaces().
Also worth noting; all of the meshes that use the same material are grouped together as a single mesh in the mesh array. In Blender, I tried creating separate materials for each mesh so that each mesh of the same colour had a different material (eg. white01, white02…) but they still ended up being grouped together.

Trying to figure this out has really taken me down a rabbit hole. I am now working with the ‘CustomShapesExample’ from the ofxBullet examples. I am trying to swap my custom object (created in Blender and saved as .obj) with the one that is used in the example (‘OFlogo.dae’) but it is not working and I think I know why. OFlogo.dae consists of only one mesh whereas my custom object has five. Below is the significant code from setup() in the example:

logos.resize( 3 );
for (int i = 0; i < logos.size(); i++) {
    logos[i] = new ofxBulletCustomShape();
    startLoc = ofVec3f( ofRandom(-5, 5), ofRandom(0, -hwidth+5), ofRandom(-5, 5) );		
	if(i == 0) {
		for(int j = 0; j < assimpModel.getNumMeshes(); j++) {
			logos[j]->addMesh(assimpModel.getMesh(j), scale, true);
		}
	} else {
		logos[i]->init( (btCompoundShape*)logos[0]->getCollisionShape(), logos[0]->getCentroid() );
	}
	logos[i]->create( world.world, startLoc, startRot, 3. );
	logos[i]->add();
}

I am sure the inner for loop is there to accommodate multi-mesh models, but it does not work as is.

Anyways, I am continuing to work on this.

1 Like

Hey @rgthings , I see that the nested loops in the CustomShapesExample both use an int i. I wonder if logos[i] would work better than logos[j] in the inner loop:

	if(i == 0) {
		for(int j = 0; j < assimpModel.getNumMeshes(); j++) {
			logos[i]->addMesh(assimpModel.getMesh(j), scale, true);
		}
	} else { ... }

Also just a quick note that ofxAssimpModelLoader::getMesh() returns a copy of the ofMesh in the model, not the actual ofMesh. So any changes you make to the copy will not apply to the ofMesh in the model. This might be just fine for what you’re doing. But if you need to modify the actual ofMesh, you can implement something similar to the following forum thread:

Glad you are patiently plugging away at this! These addons don’t seem trivial to me and using them together might require some extra effort.

1 Like

I have spent some time with this and not come up with any answers. I have posted a MCVE below using ofxAssimpModelLoader and ofxBullet. I am unable to upload the .obj and .mtl files. The one I am using has eight meshes consisting of differently coloured spheres and cylinders. assimpModel.getMesh(i) does not have setScale or setPosition methods so I am not sure how to change them. I tried saving it to an ofMesh but again, no setScale or setPosition methods were available there either.
In the example below, assimpModel.drawFaces() works, as does the method you suggested using getMesh() (although I do not know how to scale or reposition it). The code from the ofxBullet CustomShapesExample still does not work. I get write access violations when the compiler hits the line: customModel[i]->init((btCompoundShape*)customModel[0]->getCollisionShape(), customModel[0]->getCentroid());. I am not sure why, because customModel[0]->getNumChildShapes returns an ‘8’.
I am prepared to settle for spheres for now - obviously I’m on the steep part of the learning curve here. Thank you for any help.

ofApp.h:

#pragma once

#include "ofMain.h"
#include "ofxAssimpModelLoader.h"
#include "ofxBullet.h"

class ofApp : public ofBaseApp{

	public:

		(...)

		ofxBulletWorldRigid			world;

		ofEasyCam					camera;

		ofLight						light;

		ofxAssimpModelLoader assimpModel;
		vector<ofxBulletCustomShape*> customModel;
		
};

ofApp.cpp:

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
	ofSetFrameRate(60);
	ofSetVerticalSync(true);

	ofBackground(0);

	camera.setPosition(ofVec3f(0.f, -10.f, -100.f));
	camera.lookAt(ofVec3f(0, 0, 0), ofVec3f(0, -1, 0));

	world.setup();
	world.enableGrabbing();
	world.setCamera(&camera);
	world.setGravity(glm::uvec3(0., 160., 0.));

	ofVec3f scale(0.1, 0.1, 0.1);
	assimpModel.loadModel("model-01-02.obj", true);
	assimpModel.setScale(scale.x, scale.y, scale.z);
	assimpModel.setPosition(0,0,0);


	// create and add custom model to the scene
	ofQuaternion startRot = ofQuaternion(0., 1., 0., PI);
	ofVec3f startLoc = ofVec3f(0., -30., 0.);

	customModel.resize(2);

	/*
	for (int j = 0; j < customModel.size(); j++) {
		customModel[0]->addMesh(assimpModel.getMesh(j), scale, true);
		
	}
	customModel[0]->create(world.world, startLoc, startRot, 3.);
	customModel[0]->add();
	*/
    // code from CustomShapesExample
	for (int i = 0; i < 1; i++) {
		customModel[0] = new ofxBulletCustomShape();
		if (i == 0) {
			for (int j = 0; j < assimpModel.getNumMeshes(); j++) {
				customModel[i]->addMesh(assimpModel.getMesh(j), scale, true);
			}
		}
		else {
			customModel[i]->init((btCompoundShape*)customModel[0]->getCollisionShape(),  
+                customModel[0]->getCentroid());  //program halts here
		}
		customModel[i]->create(world.world, startLoc, startRot, 3.);
		customModel[i]->add();
	}
	cout << customModel[0]->getNumChildShapes() << endl; // returns '8'



}

//--------------------------------------------------------------
void ofApp::update(){
	world.update();
}

//--------------------------------------------------------------
void ofApp::draw(){

	ofEnableDepthTest();
	camera.begin();

    // this works
	ofSetColor(255, 0, 0);
	assimpModel.drawFaces();


	ofEnableLighting();
	light.enable();
	light.setPosition(0,-100,0);
	
	ofSetColor(0, 0, 255);
	customModel[0]->draw(); // does not work
	//customModel[1]->draw();

	light.disable();
	ofDisableLighting();


    // this works
	// drawing a custom model
	for (size_t i{ 0 }; i <= assimpModel.getMeshCount(); ++i) {
		ofMaterial material = assimpModel.getMaterialForMesh(i);
		ofColor col = material.getDiffuseColor();
		ofSetColor(col);
		assimpModel.getMesh(i).drawFaces();
	}

	camera.end();
	ofDisableDepthTest();
}

Yeah an ofMesh does not have these methods. One way is to transform the space (or move to a certain viewpoint) before drawing a mesh, like here in the CustomShapesExample in ofxBullet:

	for(int i = 0; i < logos.size(); i++) {
        logos[i]->transformGL();
		ofScale(scale.x,scale.y,scale.z);
		assimpModel.getMesh(0).drawFaces();
		logos[i]->restoreTransformGL();
	}

In OF this can be done with something like:

    ofPushMatrix();
    ofTranslate(0.f, 0.f);
    ofRotateRad(PI / 2.f, 0.0, 1.0, 0.0);
    ofScale(scale.x, scale.y);
    // draw something
    ofPopMatrix();

An of3dPrimitive derives from ofNode, which does have .setPosition() and .setScale(), and a bunch of other useful functions for “moving stuff around” in 3d space. But again I think a transform is applied, and that the ofMesh in the primitive is not affected. Lots (maybe all) of the 3d classes derive from ofNode (ofEasyCam, ofSpherePrimitive, etc).

And then it’s possible to loop thru the vertices and .set() their positions. But applying a transform is likely more efficient.

Hi @rgthings , I got the CustomShapesExample working with a 4-mesh model (astroBoy_walk.dae from /examples/3d/assimpExample).

I made 2 changes. The first one adds multiple meshes to an ofxBulletCustomShape; this is the same change as noted above with two int i declarations in the nested loop. The code works as is, but changing an i to a j makes it more clear I think.
line 76-80 in ofApp.cpp:

		if(i == 0) {
			for(int j = 0; j < assimpModel.getNumMeshes(); j++) {
				logos[i]->addMesh(assimpModel.getMesh(j), scale, true);
			}
		} else { ... }

Then in ofApp::draw(), you have to loop thru all of the meshes in the model and draw them like this (lines 169-176):

	for(int i = 0; i < logos.size(); i++) {
        logos[i]->transformGL();
		ofScale(scale.x,scale.y,scale.z);
        for(size_t j{0}; j < assimpModel.getNumMeshes(); ++j){
            assimpModel.getMesh(j).drawFaces();
        }
		logos[i]->restoreTransformGL();
	}

Now that I’ve used the addons just a bit, I feel like the CustomShapesExample provides a nice design pattern for how to use these 2 addons together. ofxBullet provides the physics, but the model provides the ofMesh for drawing. If needed, the copy of the mesh returned by ofxAssimpModelLoader::getMesh() can be modified without affecting the original mesh in the model.

OK I hope this allows you to move forward. Definitely post back if you have more questions!

1 Like

So very appreciated TimChi. Got it working exactly as I was trying to and thank you for the info along the way as well. One major thing that eludes me is the mechanism by which the assimpModel and ofxBulletCustomShape are internally connected so that they refer to the same model. I am sure I will find an answer by looking through the Bullet library code. I am working to speed things up now. So satisfying to actually see this working - brilliant!

1 Like

Hey @rgthings , I LOVE the idea of looking thru the code for both of these addons if you have time. I’ve done this a bit, but I sometimes struggle with following code that I didn’t write.

I think the general idea of how these addons interact is that the model contains the mesh(es) plus some other things related to that, and ofxBulletCustomShape has its own representation of the mesh(es) and it computes the transform needed to draw the mesh in a certain way, according to the physics that it simulates. The OF app is in charge of coordinating these two, plus rendering, which can happen with a model method like assimpModel.draw(), or also with an ofMesh method like assimpModel.getMesh().draw().

I’m so glad that you can move forward with a working example that you can play around with and/or adapt as needed! Sometimes a little stubborn tenacity is so helpful when trying to figure these things out. Have fun!

2 Likes