Make new material bug? (python)

Hi guys, I am making a material with Python but I encounter a bug where Rhino removes the previous made material if I change the “NAME” in the script. It appears to make a new material OK, with a new index and all, but where is the old one?


import rhinoscriptsyntax as rs
import scriptcontext
import random

### --- Make Material
newMatIndex = scriptcontext.doc.Materials.Add()
print "newMatIndex= "+str(newMatIndex)
rs.MaterialName(newMatIndex, str(int(100000*random.random() )))
color =(int(random.random()*250),int(random.random()*250),int(random.random()*250) )
rs.MaterialColor(newMatIndex,  color)
print "MaterialName= "+str(rs.MaterialName(newMatIndex))
scriptcontext.doc.Materials.Add()

This is just a modified and simplified script to illustrate the problem.
My real big problem is that I generate a gradient PNG and assign this in a larger script, and when I run the script the second time the material turns black, I presume some bug is going on internally in Rhino’s material handling and I can not seem to find a workaround. Because IF I make a new material in Rhino’s material editor and name that “NAME” too, then that is fine, but if I do it one more time then I get an error message saying that the name is already in use. (But now I already have two materials named the same… odd)

Also if I delete the generated material in the material library then I can run the script again.
But if I autogeneate names with random then the old materials just disappear… :frowning:

Ok, so I found that if I just add the material, but not to an object, then the material doesn’t appear as a new material. Does Rhino have a list of materials not visible in the material panel? And if so, how can I add them permanently to the file?

Please try this a few times and delete the spheres and see that the materials disappear too:

I modified the example script from https://developer.rhino3d.com/samples/rhinocommon/add-material/ so it adds random names and colors.

import Rhino
import scriptcontext
import System.Drawing
import random

def AddMaterial():
    # materials are stored in the document's material table
    index = scriptcontext.doc.Materials.Add()
    mat = scriptcontext.doc.Materials[index]
    color = System.Drawing.Color.FromArgb(int(random.random()*250),int(random.random()*250),int(random.random()*250) )
    
    mat.DiffuseColor = color
    mat.Name = str(int(random.random()*1000000))
    mat.CommitChanges()

    # set up object attributes to say they use a specific material
    sp = Rhino.Geometry.Sphere(Rhino.Geometry.Plane.WorldXY, 5)
    attr = Rhino.DocObjects.ObjectAttributes()
    attr.MaterialIndex = index
    attr.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
    scriptcontext.doc.Objects.AddSphere(sp, attr)

    scriptcontext.doc.Views.Redraw();

if __name__=="__main__":
    AddMaterial()

Can this be part of the problem?

https://developer.rhino3d.com/5/api/RhinoCommonWin/html/P_Rhino_DocObjects_Material_IsDeleted.htm

“Deleted materials are kept in the runtime material table so that undo will work with materials.”
But I guess it doesn’t explain why materials disappear when objects are deleted.

I also found that I can not change a script generated materials “Type” from custom to say “Metal” or any other Rhino 6 type materials. Not even in the Material Panel after it is generated.

To get the best of materials you should be using RenderMaterials.

Review https://github.com/mcneel/rhino-developer-samples/blob/6/rhinopython/SampleAddRenderMaterials.py

1 Like

Man… I feel stoopid… So I simplified that example and it adds a render material to the document, but I can not figure out how to assign it to an existing object… I just can’t find an index I can use… :frowning:
Is there an easy way to do that? The example you posted only makes rendermaterials, and the material that is assigned to the sphere is just a material and not a rendermaterial. And I have been stumbling away in the dark for hours here, so I have to ask…

I think you have to assign the doc object .RenderMaterial to the RenderMaterial object and commit it. In the example it is assigned to a layer instead.

It doesn’t look like indexes across the rhino and render material tables stay synced. Once a render material is deleted then the referenced rhino material is still there but .IsDeleted becomes True. Also, once the render material is deleted then the rhino material’s .Name is None, but mysteriously there is still a reference to a RenderMaterial. Maybe that is part of the undoiness.

Perhaps it is safest to just use Rhino.RenderMaterial s and Rhino.RenderMaterialTable (which you get to through the .doc). Rhino.RenderMaterialTable seems to WYSIWYG to the materials list in the gui. But I guess the easiest way to make a RenderMaterial is to first make a rhino material like in the example, which seems like it could lead to confusion.

Here is the script I used to observe the behavior. Most of it came from the referenced example. You can create a new red RenderMaterial (via a RhinoMaterial) or not, then assign it to an object or not, then it prints the rhino and render materials to see what is going on.

import rhinoscriptsyntax as rs
import scriptcontext as sc
import Rhino as R
import System


generate_red = rs.GetBoolean('Generate Material?', ('red', 'no', 'yes'), (False))
print(generate_red)

if generate_red[0]:

    # Create basic material
    red_rhino_mat = R.DocObjects.Material()
    red_rhino_mat.DiffuseColor = System.Drawing.Color.Red
    red_rhino_mat.Name = "Red"
    
    # Generate a RenderMaterial from the rhino material
    red_render = R.Render.RenderMaterial.CreateBasicMaterial(red_rhino_mat)
    
    # Add this to the Rhino.Render.RenderMaterialTable, but through the scriptcontex.doc
    sc.doc.RenderMaterials.Add(red_render)
    
    # Get the doc object somehow, here by rs.GetObject
    guid = rs.GetObject('select object')
    
    if guid:
        # Use the guid to get the doc object
        doc_obj = rs.coercerhinoobject(guid)
        
        # Set the material source to object
        doc_obj.Attributes.MaterialSource = R.DocObjects.ObjectMaterialSource.MaterialFromObject
        # Set the doc object render material to the RenderMaterial object from above
        doc_obj.RenderMaterial = red_render
        # Commit the change
        doc_obj.CommitChanges()



# Looks like the basic materials keep a reference to their RenderMaterial
# in the .RenderMaterial property
for rhino_mat in sc.doc.Materials:
    print(rhino_mat, rhino_mat.Name, rhino_mat.RenderMaterial.Id, rhino_mat.IsDeleted)



for mat in sc.doc.RenderMaterials:
    print(mat, mat.Id, mat.Tags, mat.Name)

Don’t know if it will help any but found it interesting.

The RhinoDoc.Materials table is indeed a bit of a pain to use. When you add one material, then add it to five objects you’ll have RhinoDoc.Materials with five entries. The water gets indeed muddied when you start deleting materials as well… As you noticed the material shows up in the Materials editor only once it is actually assigned to some object or layer.

Indeed the RhinoDoc.RenderMaterials is as @nathancoatney found: once you add a RenderMaterial to the document it shows up in the Materials editor. And adding it to 5 objects results in RhinoDoc.RenderMaterials have still only the one instance.

The old-style materials have a RenderMaterial version - so the five objects of the first paragraph all reference just one RenderMaterial, but you see five different material indices into RhinoDoc.Materials.

If you find it easier to create an old-style material you can still do that, just create a RenderMaterial from it before you add it to the document.

The below code is pretty much what @nathancoatney has, and is also pretty much what the sample code does that I linked to.

oldmat = Rhino.DocObjects.Material()
oldmat.DiffuseColor = the_color_you_want
oldmat.Name = "the material"

newstylemat = Rhino.Render.RenderMaterial.CreateBasicMaterial(oldmat)
sc.doc.RenderMaterials.Add(newstylemat)

yourob = GetYourRhinoObjectFromDoc()
yourob.Attributes.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
yourob.RenderMaterial = newstylemat
yourob.CommitChanges()

Oh this is difficult.

I just want to add a material to the document and then assign a texture and add it to an object and suddenly I need a PHD in philosophy to do it… :wink:

I sew together your examples into my document and now it correctly makes and assigns i to the desired existing object, but the moment I try to set the texture to the material I am met with a Message: attribute ‘SetBitmapTexture’ of ‘Material’ object is read-only But I just sat the color value so how can it be read only?
This is just to complicated for a simple python programmer like my self.

EDIT: I worked around it with this after the material is made and assigned.
index= rs.ObjectMaterialIndex(obj)
rs.MaterialTexture(index,FILENAME)

And something is wrong with rs.MaterialTexture since it doesn’t appear in the Material Editor… it appears in the viewport though.

Can you please look at this simple script:

import Rhino
import scriptcontext 
import rhinoscriptsyntax as rs
import random

matNAME = "NewMaterial"
texture = 'D:/temp/Test.JPG'


mat = Rhino.DocObjects.Material()
mat.Name = matNAME
color = rs.CreateColor(int(255*(random.random())),int(255*(random.random())),int(255*(random.random())) )
mat.DiffuseColor = color

mat.SetBitmapTexture = texture

rendmat = Rhino.Render.RenderMaterial.CreateBasicMaterial(mat)
scriptcontext.doc.RenderMaterials.Add(rendmat)

I get this error:
image

What I don’t understand is why “Set BitmapTexture” would be read only on a newly created material.

EDIT: DANG… why does mat.SetBitmapTexture(texture) work???
Why do some parameters use = and others use ()? I am sure there is a logic that I don’t get yet… :crazy_face:

Yes Rhinocommon uses lots of getters and setters where the ‘pythonic’ way would be to use @property where necessary and give direct access to the attribute

1 Like

Because it’s a method (i.e. not a property):

https://developer.rhino3d.com/5/api/RhinoCommonWin/html/Overload_Rhino_DocObjects_Material_SetBitmapTexture.htm

A method (typically) take parameters and is a function of the class (that is run when you call it). A property is, well, a property of the class instance that you can read (i.e get) or assign (i.e. set) using the equals operator. There’s more to it than that, but it should at least clarify things a bit.

Sometimes developers write methods that get/set properties (cough, Java, cough). If I recall, RhinoCommon might occasionally mix these two approaches, but is also generally speaking very well designed and documented in this regard. So definitely keep the RhinoCommon docs handy going forward.

2 Likes

THANKS!
That makes perfect sense in an illogical way I guess… :smiley:
(So I hope I can remember it!)

1 Like