Apply material to individual faces of a polygon
-
Hi,
In Python I have code to build a PolygonObject from an imported mesh file. I want to color code the faces according to a surface distribution that is already computed (i.e. every face has an imported RGB assignment).
I had tried to do this with vertex shading, and found out how that you literally need to write a custom shader !
Instead, my work-around is to cluster the RGB values and generate a collection of materials (say 100) that cover the range of shades. I want to assign each polygon face an appropriate material to achieve an approximate color mapping.
I can generate the materials, and in fact I can apply them interactively by selecting polygon faces, right-clicking on a material and choosing to Apply it. The result is visible not only in the viewport but also when I render.
HOWEVER, I cannot find a way to do this programmatically. The faces in my PolygonObject are CPolygon objects, which do not support inserting a texture tag.
What is the missing link to make this work in Python?
Thanks in advance.
Randy
-
Hello @zauhar,
Thank you for reaching out to us. Please share your existing and executable code code as lined out in our forum guidelines when asking questions. Lately it is increasingly getting out of hand that users do not provide any code context, which then requires us to write and explain everything from scratch. We reserve the right to reject such questions as lined out in our forum guidelines.
About your Question
Without your code, I can only speculate what you are doing, but it seems like you are trying to reference polygons directly in a texture tag.
Ttexture
, a texture tag, takes a string as a reference to the name of aSelectionTag
. That has always been an oddity of the texture tag, that selections are referenced by string and not aBaseLink
, as for example the material is referenced in such texture tag.Selections are represented by the type
BaseSelect
in our API. There are multiple entry points for selection states onc4d.PolygonObject
andc4d.PointObject
. The most relevant one isPolygonObject.GetPolygonS
for the selected polygons in your case. Then there is the typeSelectionTag
which represents selections stored as tags on an object, as used by the material system. You can however not allocate such tags directly in the public API and must instead use the modeling commandMCOMMAND_GENERATESELECTION
.Find below an example script which assign a new material with a random color to the selected polygons of the currently selected polygon object.
Cheers,
FerdinandResult:
Code:
"""Demonstrates how to assign a material to an object that is restricted to a polygon selection. Must be run in the Script Manager and requires a polygon object with at least one selected polygon to be selected. """ import c4d import random op: c4d.BaseObject | None # The selected object, can be #None. def main() -> None: """ """ # Make sure that an editable polygon object is being selected with at least one polygon # being selected. if not isinstance(op, c4d.PolygonObject): raise TypeError("Please select an editable polygon object") selection: c4d.BaseSelect = op.GetPolygonS() if selection.GetCount() < 1: raise RuntimeError("Please select at least one polygon.") # To limit a material to a selection, we need a selection tag. In the public API, we cannot # instantiate them directly, we must use the modelling command #MCOMMAND_GENERATESELECTION # instead. We create here a tag for the current selection state. We could of course modify # that state beforehand with #PolygonObject.GetPolygonS. # We create a selection tag for #op in the mode #MODELINGCOMMANDMODE_POLYGONSELECTION. if not c4d.utils.SendModelingCommand( c4d.MCOMMAND_GENERATESELECTION, [op], c4d.MODELINGCOMMANDMODE_POLYGONSELECTION, c4d.BaseContainer(), op.GetDocument(), c4d.MODELINGCOMMANDFLAGS_NONE): raise RuntimeError("Could not create polygon selection.") # This command unfortunately does not return the generated tag, but only if the operation was # successful. So, we must find the new tag ourselves. It will be the last polygon selection # on our object. selectionTagCollection: list[c4d.BaseTag] = [ t for t in op.GetTags() if t.CheckType(c4d.Tpolygonselection)] if not selectionTagCollection: raise RuntimeError(f"{selectionTagCollection = }") selectionTag: c4d.BaseTag = selectionTagCollection[-1] # Now that we have our tag, we can create a material and a texture tag to link the material in. material: c4d.Material = c4d.BaseMaterial(c4d.Mmaterial) textureTag: c4d.BaseTag = op.MakeTag(c4d.Ttexture) if not material: raise MemoryError(f"{material = }") if not textureTag: raise MemoryError(f"{textureTag = }") # Insert the material into the document of #op, set its color to a random color, and reference # both the material and the selection in the texture tags. Selections references in texture tags # are a bit weird as in that they work over strings and not BaseLinks. So we pass the name of # our selection tag. op.GetDocument().InsertMaterial(material) material[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(random.random(), random.random(), random.random()) textureTag[c4d.TEXTURETAG_MATERIAL] = material textureTag[c4d.TEXTURETAG_RESTRICTION] = selectionTag.GetName() c4d.EventAdd() if __name__ == "__main__": main()
-
Ferdinand, thanks for the reply.
In my case I need to programmatically select particular polygons to apply different materials to, I cannot rely on an interactive selection.
So as to not involve my custom code, which relies on numpy, etc, the example code below tries to set the material for the one half of the polygons in a mesh that is brought in as 'Default' object using File->Merge Project.
I drag the object from the Objects panel into the python console, and assign it to the variable 'op' to match your examples.
The code where I select the first 50% polygons clearly works, I see one half of the mesh selected if I view in 'select polygon' mode. (The code for programmatic selection was suggested by this thread : https://developers.maxon.net/forum/topic/13194/polygon-islands-convenience-method)
The final result - apparently nothing happens. If I leave my programmatic selection active, right-click on the material I inserted, and choose to 'Apply', the action takes place and half of the mesh has the new color.
What am I still missing?
Thanks, Randy
# Ferdinand, the following line is created by dragging the imported object into the python session so I get a reference to # it; I assign it to the variable 'op' to match your code op = Default kwargs = {"command": c4d.MCOMMAND_SELECTALL,"list": [op],"mode": c4d.MODELINGCOMMANDMODE_POLYGONSELECTION,"bc": c4d.BaseContainer(),"doc": doc} c4d.utils.SendModelingCommand(**kwargs) polySelection = op.GetPolygonS() polySelection.DeselectAll() count = op.GetPolygonCount() for i in range(count) : if i < count/2 : polySelection.Select(i) ## the above 'works' in the sense that I see half of the polygons selected if I'm in face selection mode if not c4d.utils.SendModelingCommand(c4d.MCOMMAND_GENERATESELECTION, [op], c4d.MODELINGCOMMANDMODE_POLYGONSELECTION, c4d.BaseContainer(), op.GetDocument(), c4d.MODELINGCOMMANDFLAGS_NONE): raise RuntimeError("Could not create polygon selection.") selectionTagCollection: list[c4d.BaseTag] = [t for t in op.GetTags() if t.CheckType(c4d.Tpolygonselection)] selectionTag: c4d.BaseTag = selectionTagCollection[-1] bs = selectionTag.GetBaseSelect() print('# selected = %d of %d' % (bs.GetCount(),count)) # selected = 24998 of 49996 material: c4d.Material = c4d.BaseMaterial(c4d.Mmaterial) textureTag: c4d.BaseTag = op.MakeTag(c4d.Ttexture) # Insert the material into the document of #op, set its color to a random color, and reference # both the material and the selection in the texture tags. Selections references in texture tags # are a bit weird as in that they work over strings and not BaseLinks. So we pass the name of # our selection tag. op.GetDocument().InsertMaterial(material) # try to make half the mesh blue material[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(0., 0., 1.) textureTag[c4d.TEXTURETAG_MATERIAL] = material textureTag[c4d.TEXTURETAG_RESTRICTION] = selectionTag.GetName() c4d.EventAdd() # nothing happens
-
I think I found the stupid issue.
When I imported the mesh, it included a default material. That tag was 'in front' of the ones I added. All I have to do is import with no material (or delete the default material from the tag list) and I see my changes.
Thanks, I will close this once I have my intended code working.
Randy
-
Got it working !
This relies on a lot of custom stuff, but I am pasting the code if anyone wants to follow the same idea. importOBJ takes a path to a file in wavefront format with RGB values appended to the vertex positions and returns lists of numpy objects (the names are obvious).
Thanks !
Randy
NUM_RGB = 100 class c4dSURFACE_test(object) : def __init__(self, surfpath, transparency=0. ) : vertices,elems,normals,centers,centernorms,areas,rgb = importOBJ(surfpath) self.mesh = c4d.PolygonObject(len(vertices),len(elems)) c4dverts = [ c4d.Vector(v[0],v[1],-v[2]) for v in vertices ] # need to flip face orientations elems = [ list(e) for e in elems ] for elem in elems : t = elem[1] elem[1] = elem[2] elem[2] = t # self.mesh.SetAllPoints(c4dverts) self.polys = [ c4d.CPolygon(elem[0],elem[1],elem[2]) for elem in elems ] # for idx,poly in enumerate(self.polys) : self.mesh.SetPolygon(idx,poly) # doc.InsertObject(self.mesh) self.materials = [] self.ttags = [] if None not in rgb : # make a range of NUM_RGB colors # use kmeans to make clusters based on rgb, take average color in each cluster # NOTE that my export currently assigns colors to vertices, need tp compute face color rgb = numpy.array(rgb) faceRGB = [] for eidx in range(len(elems)) : s = sorted(elems[eidx]) faceRGB.append(numpy.mean(rgb[s],axis=0)) faceRGB = numpy.array(faceRGB) means = numpy.mean(faceRGB,axis=0) stds = numpy.std(faceRGB,axis=0) diff = faceRGB - means[numpy.newaxis,:] rgbZ = diff * numpy.reciprocal(stds)[numpy.newaxis,:] kmeans = KMeans(n_clusters=NUM_RGB).fit(rgbZ) labels = kmeans.labels_ labelToElems = {} # for eidx,label in enumerate(labels) : if label not in labelToElems : labelToElems[label] = [] labelToElems[label].append(eidx) labelToMeanRGB = {} for label in labelToElems : color = numpy.mean(faceRGB[labelToElems[label]],axis=0) labelToMeanRGB[label] = color # # Get a selection of all faces kwargs = {"command": c4d.MCOMMAND_SELECTALL,"list": [self.mesh],"mode": c4d.MODELINGCOMMANDMODE_POLYGONSELECTION,"bc": c4d.BaseContainer(),"doc": self.mesh.GetDocument()} c4d.utils.SendModelingCommand(**kwargs) polySelection = self.mesh.GetPolygonS() # for lidx in range(NUM_RGB) : polySelection.DeselectAll() for eidx in labelToElems[lidx] : polySelection.Select(eidx) c4d.utils.SendModelingCommand(c4d.MCOMMAND_GENERATESELECTION, \ [self.mesh], c4d.MODELINGCOMMANDMODE_POLYGONSELECTION, c4d.BaseContainer(), self.mesh.GetDocument(), c4d.MODELINGCOMMANDFLAGS_NONE) selectionTagCollection: list[c4d.BaseTag] = [t for t in self.mesh.GetTags() if t.CheckType(c4d.Tpolygonselection)] selectionTag: c4d.BaseTag = selectionTagCollection[-1] # material: c4d.Material = c4d.BaseMaterial(c4d.Mmaterial) textureTag: c4d.BaseTag = self.mesh.MakeTag(c4d.Ttexture) self.mesh.GetDocument().InsertMaterial(material) r,g,b = labelToMeanRGB[lidx] material[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(r, g, b) material.SetName('COLOR_' + str(label)) material[c4d.MATERIAL_USE_TRANSPARENCY] = 1 material[c4d.MATERIAL_TRANSPARENCY_BRIGHTNESS] = transparency textureTag[c4d.TEXTURETAG_MATERIAL] = material textureTag[c4d.TEXTURETAG_RESTRICTION] = selectionTag.GetName() self.materials.append(material) self.ttags.append(textureTag) else : mat = c4d.BaseMaterial(c4d.Mmaterial) mat.SetName('surface') mat[c4d.MATERIAL_USE_TRANSPARENCY] = 1 mat[c4d.MATERIAL_TRANSPARENCY_BRIGHTNESS] = transparency mat[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(1., 1., 1.) self.mesh.GetDocument().InsertMaterial(mat) self.materials.append(mat) ttag = c4d.TextureTag() ttag.SetMaterial(mat) self.ttags.append(ttag) self.mesh.InsertTag(ttag) # # self.mesh.SetBit(c4d.BIT_ACTIVE) c4d.EventAdd() return
-
-
Ferdinand, I was indeed the one that asked, and I got a visualization like the one above working via that approach. My memory is that the surface coloring was only visible in the viewport, when I tried to render it did not show up.
That was when you or someone else told me that I needed to write a custom shader, which honestly surprised me. Also, I was warned that a shader in python would be 'slow' . I tried to look for more documentation and/or examples, found little that helped me. So I put the problem on the shelf for a while.
Is my understanding about needing a custom shader incorrect?
Finally, while I have been only looking at the python SDK, I am perfectly comfortable in C/C++. Is that the better route for doing things like this?
Thanks!
Randy
-
Hey @zauhar,
I do not think so that I have told you that, at least I hope so, because that what you say there is not quite correct
A vertex map or a vertex color tag can be rendered. When you use the standard renderer, you will have to use the Vertex Map shader, and when you use Redshift, you will need the Vertex Attribute node (just drag and drop the tag in both cases in the respective reference fields of the shaders). Below is an example for the Redshift case:
Other renderers support Cinema 4D vertex colors too, but you will have to ask their support how that works
Vertex Map Shader
Vertex Attribute NodeCheers,
Ferdinand