Drawing large amounts of billboarded triangles in the viewport



  • Hello,
    Thank you for the taking the time to look at this thread, i'm a little new to Cinema4d development, please excuse any first time mistakes : )
    I'm using R23, c++ and I have an ObjectData plugin which represents a volume and I'd like to draw it in the viewport in different ways, one of which is colored triangles at the center of each voxel of my volume, oriented to look at the camera. Like so:
    tristocam.PNG
    I have found a few ways to achieve this in C4D
    First is triangle by triangle with BaseDraw::DrawPolygon() (the above screenshot). This provides me per vertex colors and i can modify the triangle vertices to be oriented towards the camera before drawing. So it works but is slow when the triangles become 100k or more.

    The second method I found is by caching a PolygonObject* in GetVirtualObjects() with the same polygon data from the previous method. This is quite fast and seems to support per vertex color. However the Tlookatcamera tag seems to treat all the polygons as a single object and orients them in a weird way like so:
    badori.PNG

    So my first question is whether it's possible to orient each polygon in the PolygonObject* separately towards the camera, without generating geometry over and over again like in the first method?

    I also found your VolumeBuilder, which I fed with the PolygonObject* from the previous method, and this provided me with a cool representation
    My second question is for the volume builder and whether it's possible to pass a color to each of the planes that the volume is made up of ?

    Regards,
    Georgi.



  • @mastergog said in Drawing large amounts of billboarded triangles in the viewport:

    So my first question is whether it's possible to orient each polygon in the PolygonObject* separately towards the camera, without generating geometry over and over again like in the first method?

    No, it is not, because to do that you would have to separate your triangles into singular objects within the cache (which would be a huge strain on performance in the first place) and then attach an orientation tag to each of them. But expressions, i.e. tags, are not being evaluated in caches, because otherwise it would not be a cache anymore ;)

    I also found your VolumeBuilder, which I fed with the PolygonObject* from the previous method, and this provided me with a cool representation
    My second question is for the volume builder and whether it's possible to pass a color to each of the planes that the volume is made up of ?

    What do you mean by planes? Your triangles, i.e. your input objects? I do not think that this is possible out of the box, but I might very well be wrong; I do not know much about the depths of volumes in Cinema. You could of course tessellate that volume and than colour each vertex of that tessellated mesh over voronoi diagram where the seed points are your triangles. But that would be ridiculously inefficient.

    But you might want to take a look into BaseDraw::DrawPoints() which is much more performant than drawing each triangle on its own. This of course only applies when you are willing to give up triangles as a representation for your voxels. And finally, and this will apply to all methods, you might want to consider some view culling. No matter how efficient you make your draw calls in the first place, potentially sending out tens of thousands of draw calls for things that are not visible, is obviously a bad idea.

    I hope this helps a little bit.

    Cheers,
    zipit



  • Hey @zipit. Thanks for the reply.

    By planes I meant the squares which make the native volume with VolumeBuilder, which are made up of my PolygonObject* data. I added the per vertex color tag to the PolygonObject* and i was hoping it would transfer over to the volume builder but no luck.

    Regarding the culling, what do you mean exactly.
    Looking at the BaseDraw() culling options i'm not sure i'm understanding how to use them. There's a BaseDraw::BackfaceCulling() method, but this requires me to transform the face normal and center in screen space to perform this check, before i call the DrawPolygon() method.

    Regards,
    Georgi.



  • Hi,

    I meant more your own culling logic. And BaseDraw::BackfaceCulling is a method to evaluate if you want to draw a face or not based on the angle of incident. This obviously won't work when you have a grid of camera aligned triangles, because they will all pass, because they are camera aligned ;)

    What you would have to do is to calculate the hull of your voxel lattice. Should be pretty straight forward, just march through the lattice from two principal planes and follow the even-odd-rule, throwing away everything that is "volume" and keeping everything that is "surface". With that hull you could then probably also compute normals for your voxels with which you then could cull the voxels that are "facing away" from the camera. But the first step would obviously yield the far greater performance increase (at least when your voxel data is not super weird).

    To my knowledge Cinema has neither types to view cull voxel/point cloud data, nor does the viewport something like that automatically.

    But as already stated, my first point of attack would be moving away from polygons/triangles towards points, unless there is a very good reason to stick with them.

    Cheers,
    zipit



  • @zipit Ahhh you are correct, shows im still a noob : D

    Anyway about the points. I didn't find BaseDraw::DrawPoints() but there is the BaseDraw::DrawPointArray() which does the trick. Also there is a way to control the size but say if I wanted to keep a fixed size in screen space no matter where the camera is. Is that possible?

    Im also wondering about the PointObject from which the PolygonObject is derived. Is there a similar routine like with the PolygonObject where i can cache the points in GetVirtualObjects(), since PointObject is abstract?

    Thanks again for the guidance.

    Regards,
    Georgi.



  • Hi @mastergog, thanks for reaching out to us.

    With regard to your request there are a few considerations:

    • the public API currently shipped with Cinema is not providing you with the best options to effectively tackle this problem. Since R22, with the new Viewport system being included in Cinema 4D, there's a not-yet public API that could be used to get this sort of things being natively executed on GPU. Considering the fluid state of such API, please write us directly via email to further specify your final intent and we'll be eager to evaluate the request to get you accessing it.
    • using the BaseDraw class which is the sole option, if accessing the new API is not possible, I have a few considerations:
      • adopt caching mechanism using two checks: one for checking that the point cloud doesn't changes and the second to check that the camera has rotated. The first check will ensure that the volume you're representing has not changed, the second will ensure that you'll redraw your triangles only if the camera has rotated.
      • separate the voxel data acquisition which should happen likely in the ObjectData::GetVirtualObjects(), from the triangles representation which should take place in the ObjectData::Draw(): this will let you use Cinema deformers with your ObjectData and at the same time be sure that triangles will be correctly facing if a volume is deformed.
      • use the BaseDraw::DrawPolygonObject()which should perform better than drawing single triangles one after the other.

    The basic idea is this:

    1. in ObjectData::GetVirtualObjects() you will process your data and return an zero-polygon PolygonObject which contains all the points representing the centroids of your voxels. Only when your data changes the PolygonObject cache will be updated
    2. in ObjectData::Draw() you will take the data generated in the previous step and use it to generate a PolygonObject with camera-pointing-normal triangles. This shall be stored somewhere to be used as a cached geometry, that will be updated only if the camera rotation changes. The in this overridden method you will call the DrawPolygonObject passing to it the just-created PolygonObject.
      Finally, about back-face culling, back-faces are automatically culled unless you specify DRAWOBJECT::NOBACKCULL.

    Cheers, R



  • @r_gigante Hey thanks for the great suggestions.
    Not sure why I didn't think of adding a camera check... Now in combination with the BaseDraw::DrawPolygonObject() the performance is quite ok.
    If I could bother you guys for 1 more question : ), would really appreciate it. It is regarding per vertex colors. I followed the guide here - https://developers.maxon.net/docs/Cinema4DCPPSDK/html/page_manual_vertexcolortag.html
    Created the VertexColorTag , inserted in the PolygonObject, got the write address and filled it up, but i'm crashing at VertexColorTag::Get() during the write process.
    Is there anything specific when using the per vertex color tag in a PolygonObject that is drawn with BaseDraw::DrawPolygonObject(). Basically should that guide work out of the box?

    Regards,
    Georgi.



  • Hi @mastergog the manual just illustrates the standard case and, as much as for any other snippet, it might be adapted to your scope to deliver the desired function.

    Give the limited information, I can only assume that the VertexColorHandle passed as first parameter to VertexColorTag::Get() might be invalid. Have you checked if vcTag->GetDataAddressW(); returns a valid pointer? Can you share a code to reproduce the issue? What's the instance scope in the code of the PolygonObject you've inserted to Tag into?

    Cheers, r



  • @maxon Hey sorry for the weird phrasing, had some other tasks and I was just wondering if there is some known issue with this functionality before i got back to it.

    Anyway about the code. As suggested, created a zero polygon PolygonObject* with my base vertices in GetVirtualObjects() and inserted it under the cache root.
    In Draw() I find that PolygonObject and use its point data to allocate a PolygonObject with actual polygons that is drawn with DrawPolygonObject(). This just created PolygonObject pointer is kept as a member in my class.

    So the vertex color tag code is the following. And the VertexColorHandle seems to be valid. Tried this code in both my Draw() and GetVirtualObjects() implementation to the same effect.

                    BaseTag* tag = polygonObjectToDraw->GetTag(Tvertexcolor);
    		if (tag == nullptr) {
    			tag = VertexColorTag::Alloc(polygonObjectToDraw->GetPointCount());
    			VertexColorTag* vcTag = static_cast<VertexColorTag*>(tag);
    			vcTag->SetPerPointMode(false);
    		}
    
    		VertexColorTag* vcTag = static_cast<VertexColorTag*>(tag);
    		VertexColorHandle handle = vcTag->GetDataAddressW();
    		for (Int32 i = 0; i < Int32(polygonObjectToDraw->GetPointCount()); i++)
    		{
    			VertexColorStruct vcs;
    			VertexColorTag::Get(handle, i, vcs);
    
    			for (int p = 0; p < 4; p
    				++)
    			{
    				vcs[p] = maxon::ColorA32(1.0f, 1.0f, 0.0f, 1.0f);
    			}
    			VertexColorTag::Set(handle, i, vcs);
    		}
    
    		polygonObjectToDraw->InsertTag(tag);
    
    		baseDraw->DrawPolygonObject(baseDrawHelp, polygonObjectToDraw, DRAWOBJECT::NONE);
    
    


  • @maxon Hey so the crashing was because I was looping out of bounds it seems. Corrected my code like so:

                   BaseTag* tag = polygonObjectToDraw->GetTag(Tvertexcolor);
    		if (tag == nullptr) {
    			tag = VertexColorTag::Alloc(polygonObjectToDraw->GetPolygonCount() * 4);
    			VertexColorTag* vcTag = static_cast<VertexColorTag*>(tag);
    			vcTag->SetPerPointMode(true);
    		}
    
    		VertexColorTag* vcTag = static_cast<VertexColorTag*>(tag);
    		VertexColorHandle handle = vcTag->GetDataAddressW();
    		for (Int32 i = 0; i < Int32(polygonObjectToDraw->GetPolygonCount()); i++)
    		{
    			VertexColorStruct vcs;
    			VertexColorTag::Get(handle, i, vcs);
    
    			for (int p = 0; p < 4; p++)
    			{
    				vcs[p] = maxon::ColorA32(1.0f, 1.0f, 0.0f, 1.0f);
    			}
    			VertexColorTag::Set(handle, i, vcs);
    		}
    
    		polygonObjectToDraw->InsertTag(tag);
    
    		baseDraw->DrawPolygonObject(baseDrawHelp, polygonObjectToDraw, DRAWOBJECT::NONE);
    

    Still this doesn't use the color i've specified, but rather the default grey.



  • @maxon Hey guys, one more update.
    Edited my code so that I allocate the full PolygonObject in GetVirtualObjects(), place it under the cache, and just modify it in Draw(), without using DrawPolygonObject()
    Managed to get the per vertex colors to show when I select my object, click "c" so i can get the edit mode and see the cache. Then I select the PolygonObject in the cache with the per vertex color tag and double click the tag as if I'm editing the colors. That's when my colors show up.

    Any ideas how to get them without going into edit mode and double clicking the tag?

    Regards,
    Georgi.



  • @maxon @r_gigante

    Hey guys, it was suggested to me that the vertex color tag is designed to pass data to the renderer not the viewport. And the reason my colors were showing up using the paint tool was because it overrides the viewport. Is that correct?

    Also one of your tutorials showed how to create a Material with a vertex map using a vertex color tag, so I tried that. And applied the Material to my object and the colors showed up. So perhaps this is the "correct" way to use the vertex color tag for representation in the viewport? Achieved this through the UI but I assume there's a way to do it programmatically. correct?

    Thanks for taking the time!

    -Georgi.



  • Ok resolved this by making the following connection TextureTag->Material->Shader->VertexColorTag and inserting the TextureTag in my object.

    Going to close this. Thanks for the support.



  • Hi @mastergog,

    we are sorry that it took us so long. We actually were aware of your problem, and had it in the workings, but were a bit busy with all the new Apple stuff and Cinema supporting it from the get go. Thank your for sharing your solution and thank you for your understanding.

    Cheers
    Ferdinand



  • @zipit Oh no worries you provided great support to get me started. As someone had suggested will also write you guys a more detailed email about the next representations I plan on implementing.

    Regards,
    Georgi.



  • As already told by @zipit, I'm terribly sorry for coming so late here.
    I've prepared a full example showing how to fill a VertexColorTag in the ObjectData::GetVirtualObjects() and use this values to color the triangles shown up in the ObjectData::Draw() method. My code actually uses the VertexColorTag::SetColor()/GetColor().
    5b4213d8-e4b4-434a-ad71-5ef0d69be6ea-image.png

    The VertexColorTag in this case is not specify to each vertex of the triangle but specifies the color for the whole triangle: this is because the tag is created in the ObjectData::GetVirtualObjects() and store color for each point of the cloud, but then it's used in the ObjectData::Draw() method to specify the ObjectColorProperty used to color the drawn PolygonObject. As far as I know the BaseDraw::DrawPolygonObject() ignores any VertexColorTag assigned to the PolygonObject to be drawn.

    BaseObject* PC_12974::GetVirtualObjects(BaseObject* op, HierarchyHelp* hh)
    {
    	// check cache before continue
    	BaseObject* cache = op->GetCache();
    	if (cache)
    		return cache;
    	
    	// init the random gen and get a arbitrary number of tris
    	_rndGen.Init(GeGetTimer());
    	_randomTrisCnt = (SAFEINT32(_rndGen.Get01() * (1 + _max - _min)) + _min);
    	
    	
    	// allocate a PolygonObject and fill it with points randomly
    	PolygonObject* po = PolygonObject::Alloc(_randomTrisCnt, 0);
    	if (!po)
    		return BaseObject::Alloc(Onull);
    	
    	// fill the points array with random positions
    	Vector* points = po->GetPointW();
    	for (Int32 i = 0; i < _randomTrisCnt; i++)
    		points[i] = Vector (100.0 * _rndGen.Get11(), 100.0 * _rndGen.Get11(), 100.0 * _rndGen.Get11());
    
    	// look for VertexColor tag
    	BaseTag* tag = po->GetTag(Tvertexcolor);
    
    	// if not existing just create a new one
    	VertexColorTag* vcTag = nullptr;
    	if (!tag)
    	{
    		tag = VertexColorTag::Alloc(_randomTrisCnt);
    		po->InsertTag(tag);
    	}
    
    	vcTag = static_cast<VertexColorTag*>(tag);
    	vcTag->SetPerPointMode(true);
    	VertexColorHandle handle = vcTag->GetDataAddressW();
    	
    	// fill the vertexcolor tag with random values
    	for (Int32 i = 0; handle && i < _randomTrisCnt; i++)
    		VertexColorTag::SetColor(handle, nullptr, nullptr, i, maxon::Color32(_rndGen.Get01(), _rndGen.Get01(), _rndGen.Get01()));
    
    	// update host object
    	po->Message(MSG_UPDATE);
    	
    	return po;
    	
    }
    
    Bool PC_12974::TriangleOnPoint(const Vector &pos, PolygonObject& po)
    {
    	// given a point create a triangle centered over there.
    	Vector* points = po.GetPointW();
    	const Matrix poMG = po.GetMg();
    	const Vector posMG = poMG * pos;
    	if (!points)
    		return false;
    	
    	// set vertexes positions for the triangle.
    	points[0] = Vector(0,  0, _triangleRad) + posMG;
    	points[1] = Vector(_triangleRad * Cos(-PI / 6), 0, _triangleRad * Sin(-PI / 6)) + posMG;
    	points[2] = Vector(_triangleRad * Cos(-PI * 5 / 6), 0, _triangleRad * Sin(-PI * 5 / 6)) + posMG;
    	
    	// create the polygon
    	CPolygon* polys = po.GetPolygonW();
    	if (!polys)
    		return false;
    	
    	polys[0] = CPolygon(0, 1, 2);
    	
    	return true;
    }
    
    DRAWRESULT 	PC_12974::Draw (BaseObject *op, DRAWPASS drawpass, BaseDraw *bd, BaseDrawHelp *bh)
    {
    	// skip anything but OBJECT drawpass
    	if (drawpass != DRAWPASS::OBJECT)
    		return DRAWRESULT::SKIP;
    
    	// check for the object and its cache
    	if (op && op->GetCache())
    	{
    		// get the PolygonObject from the cache
    		PolygonObject* opPO = static_cast<PolygonObject*>(op->GetCache());
    		
    		// look for the attached VertexColor Tag
    		BaseTag* const tag = opPO->GetTag(Tvertexcolor);
    		if (!tag)
    			return DRAWRESULT::SKIP;
    		VertexColorTag* const vcTag = static_cast<VertexColorTag*>(tag);
    		
    		// retrieve the read-only handle
    		ConstVertexColorHandle vcTagHandleR = vcTag->GetDataAddressR();
    		
    		// get the points and iterate over the VertexColor to set the color
    		// of each triangle drawn
    		maxon::Int pointCnt = opPO->GetPointCount();
    		Vector const *points = opPO->GetPointR();
    		for (maxon::Int i = 0; i < pointCnt; i++)
    		{
    			
    			maxon::Color32 vtxCol(0.0);
    			
    			// get the color
    			if (vcTagHandleR && vcTag->IsPerPointColor())
    			  vtxCol = VertexColorTag::GetColor(vcTagHandleR, nullptr, nullptr, (Int32)i);
    
    			// set the color in the ObjectColorProperties
    			ObjectColorProperties colProp;
    			colProp.usecolor = ID_BASEOBJECT_USECOLOR_ALWAYS;
    			colProp.color.x = vtxCol.r;
    			colProp.color.y = vtxCol.g;
    			colProp.color.z = vtxCol.b;
    			colProp.xray = false;
    			
    			// allocate the temp triangle
    			PolygonObject* tri = PolygonObject::Alloc(3, 1);
    			
    			// set the color prop, create the tri and draw!
    			tri->SetColorProperties(&colProp);
    			if (TriangleOnPoint(op->GetMg() * points[i], *tri))
    				bd->DrawPolygonObject(bh, tri, DRAWOBJECT::NONE, nullptr);
    
    			// free the tri
    			PolygonObject::Free(tri);
    			
    		}
    
    	}
    	
    	return DRAWRESULT::OK;
    
    }
    
    

    Feel free to comment and again sorry for the silence.

    Riccardo