RenderMaterial with child materials - is it OK?

Hello,

I would like to create a material which is quite complex.
(It means it can have variable number of sub-components, and some of the sub-components can have sub-sub-components.)

My idea was to create classes for these sub-components (based on RenderContent) and connect these via child slots to my material. But it is not possible to make a class from RenderContent only from RenderMaterial.

So OK, what about to create these custom sub-components from RenderMaterial? There is an option to make my custom RenderContent (RenderMaterial) private
[CustomRenderContent(IsBuiltIn = true, IsPrivate = true)]
which means it is not visible for users, right?

My question is, is it a feasible solution? Is it OK to define many RenderMaterial to use them only as sub-components for my “real” material? Or does it cause some unexpected results I am not prepared for:)?

Thanks,
Márton

Nope. Sounds like a great way to do it.

Great:)!!! Thanks!

Márton

FWIW child materials are used in Rhino WIP in the Double-sided material.

Thanks for the info Nathan! Is there a sample code for it maybe?

Márton

Marton

Any sample code that deals with textures will be fine - materials attach to materials in exactly the same way.

Literally all you need to do is

var material2 = new MyDiffuseLobe_GGX();
material2.Initialize();
material.SetChild(material2, “diffuse-lobe”);

  • Andy

In the API comment I can see
“Do not call this method to add children in your constructor. If you want
to add default children, you should override Initialize() and add them there.”

But RenderContent Initialize is not a virtual function. What is the correct way?

Edit:
And another question.
In case of double sided materials, the user can click on the 2 material slots and go to the child material to edit it.
How can I do it programmatically? So user clicks a button and one of the child becomes active in editor. Should I just select it?

Edit2:

  • I add the sub-material now in the constructor and it behaves correctly it seems… (?)
  • and I use mySubMaterial.OpenInEditor() to step into the submaterial for the users, and it seems good to me in Modeless mode. In case I do the same under Modal dialog it doesn’t step into the sub-material. But what I can do in this case is to call mySubMaterial.Edit() and it opens the sub-material in an upcoming modal dialog.

Márton

Marton

I believe it will work in the ctor in .net.

@johnc Can you help Martin about how a child is opened “inline”?

Andy

How can I add the sub-component combined with undo?

I do it like this

undo = Rhino.RhinoDoc.ActiveDoc.BeginUndoRecord("Add Layer");
contentMaterial.BeginChange(Rhino.Render.RenderContent.ChangeContexts.UI);

contentMaterial.SetChild(contentLayer,"layer");

contentMaterial.EndChange();
Rhino.RhinoDoc.ActiveDoc.EndUndoRecord(undo);

And it adds the layer as child but it doesn’t add the change to the Undo stack.

Márton

Hi Marton,

The method you are using to open a material in the editor is deprecated in the C++ SDK but the C# interface is still active. We have to rethink how this works and to that end I have filed a bug: https://mcneel.myjetbrains.com/youtrack/issue/RH-56652. I don’t think this will solve your problem because at the moment the deprecated function still works.

The issue you are having about the Modal Editor is by design. Once a Modal Editor opens, all child editing is done in new Modal Editors.

Regards,

John

Hi Márton,

When you want undo to work with render contents, it is not enough to start and end an undo record; you also have to create a content undo for the type of change. I’m afraid I don’t know the syntax for C# but In C++ you would write something like this (pseudocode):

	CRhRdkContentUndo cu(rdkDocument);
	cu.AddContent(contentLayer, contentMaterial);
	contentMaterial.SetChild(contentLayer,"layer");

If you need help with the C# I can get a C# developer to help you.

Regards,

John

@maxsoder would be the best candidate for helping on the C# front re undo.

The above code should work. There might be a bug? Maybe the undo does not work with SetChild or then it is possible that I do not understand completely why a content undo is needed? I will also test this on Windows.

My undo code that I tried (on the Mac WIP) is similar and it worked. I have make more tests.

          uint undo = Rhino.RhinoDoc.ActiveDoc.BeginUndoRecord("Custom value changed");

          content.BeginChange(Rhino.Render.RenderContent.ChangeContexts.UI);
          content.Fields.Set("My custom bool value", (bool)m_cb.Checked);
          content.EndChange();

          Rhino.RhinoDoc.ActiveDoc.EndUndoRecord(undo);

My entire Test material with it’s UI is implemented below.

  {
    private CheckBox m_cb;

    public Rhino.Render.RenderContentCollection RenderContentCollectionSelectionReader
    {
      get
      {
        return ViewModel.GetData(Rhino.UI.Controls.DataSource.ProviderIds.ContentSelection, false, false) as Rhino.Render.RenderContentCollection;
      }
    }

    protected Rhino.Render.RenderContentCollection ContentSelectionForSetParamsWriter
    {
      get
      {
        return ViewModel.GetData(Rhino.UI.Controls.DataSource.ProviderIds.ContentSelectionForSetParams, true, true) as Rhino.Render.RenderContentCollection;
      }
    }

    public TestMaterialUiSection()
    {
      DataChanged += OnDataChanged;

      m_cb = new CheckBox();
      m_cb.Text = "My custom bool value";
      m_cb.ThreeState = false;
      m_cb.CheckedChanged += OnCheckedChanged;

      Label label = new Label();
      label.Text = "Hello there";

      TableLayout table = new TableLayout()
      {
        Spacing = new Eto.Drawing.Size(5, 5),
        Rows =
        {
          new TableRow(label),
          new TableRow(m_cb),
          null
        }
      };

      Content = table;
    }

    public override Guid ViewModelId
    {
      get
      {
        return Guid.Empty;
      }
    }

    public override Rhino.UI.LocalizeStringPair Caption
    {
      get
      {
        return new LocalizeStringPair("TestMaterial UI", "TestMaterial UI");
      }
    }

    public override int SectionHeight
    {
      get
      {
        return 250;
      }
    }

    private void OnDataChanged(object sender, Rhino.UI.Controls.DataSource.EventArgs args)
    {
      if (args.DataType == Rhino.UI.Controls.DataSource.ProviderIds.ContentParam ||
          args.DataType == Rhino.UI.Controls.DataSource.ProviderIds.ContentSelection)
        DisplayData();
    }

    private void DisplayData()
    {
      Rhino.Render.RenderContentCollection collection = RenderContentCollectionSelectionReader;

      if (collection != null)
      {
        ContentCollectionIterator iterator = collection.Iterator();

        Rhino.Render.RenderContent content = iterator.First();

        if (content != null)
        {
          bool test_key = false;
          content.Fields.TryGetValue("My custom bool value", out test_key);

          m_cb.CheckedChanged -= OnCheckedChanged;
          m_cb.Checked = test_key;
          m_cb.CheckedChanged += OnCheckedChanged;
        }

        iterator.DeleteThis();
      }
    }

    private void OnCheckedChanged(object sender, EventArgs e)
    {
      Rhino.Render.RenderContentCollection collection = RenderContentCollectionSelectionReader;

      if (collection != null)
      {
        ContentCollectionIterator iterator = collection.Iterator();

        Rhino.Render.RenderContent content = iterator.First();

        if (content != null)
        {
          uint undo = Rhino.RhinoDoc.ActiveDoc.BeginUndoRecord("Custom value changed");

          content.BeginChange(Rhino.Render.RenderContent.ChangeContexts.UI);
          content.Fields.Set("My custom bool value", (bool)m_cb.Checked);
          content.EndChange();

          Rhino.RhinoDoc.ActiveDoc.EndUndoRecord(undo);
        }

        iterator.DeleteThis();
      }
    }
  }

  [Guid ("37D8ABBD-14BD-488C-B717-1FAA1F14EF62")]
  public class TestMaterial : RenderMaterial
  {
    public override string TypeName { get { return "Test Material (DEV)"; } }
    public override string TypeDescription { get { return "Test Material (DEV)"; } }

    public float Gamma { get; set; }

    public TestMaterial ()
    {
      Fields.Add ("My custom bool value", false);
      Fields.Add ("diffuse_color", Rhino.Display.Color4f.White, "Diffuse Color");
    }

    protected override void OnAddUserInterfaceSections ()
    {
      AddAutomaticUserInterfaceSection ("Parameters", 0);

      AddUserInterfaceSection (new TestMaterialUiSection ());
    }

    public override void SimulateMaterial (ref Rhino.DocObjects.Material simulatedMaterial, bool forDataOnly)
    {
      base.SimulateMaterial (ref simulatedMaterial, forDataOnly);

      Rhino.Display.Color4f color;
      if (Fields.TryGetValue ("diffuse_color", out color)) {
        simulatedMaterial.DiffuseColor = color.AsSystemColor ();
      }
    }
  }
´´´

Another solution to the problem might be the following. The UI section has a ViewModel (IRdkViewModel) and that view model has the following undo datasources (Rhino.Render.RdkUndoRecord and Rhino.Render.RdkUndo). However to make the usage of them easier I have a helper method and helper class.

This helper method can be added to the UI section (EtoCollapsibleSection)

public NewUndoRecord NewUndoHelper(string description)
    {
      return new NewUndoRecord(description, ViewModel);
    }

This helper class wraps the undo

    //This class is intended to be used in a using construct - so that undo
    //records are created neatly around property setting areas.
    public class NewUndoRecord : IDisposable
    {
      bool _disposed;
      Rhino.Render.RdkUndoRecord _undo_record;
      Rhino.Render.RdkUndo _undo;
      IRdkViewModel _view_model;

      public NewUndoRecord(string description, IRdkViewModel viewModel)
      {
        _view_model = viewModel;
        _undo = viewModel.GetData(Rhino.UI.Controls.DataSource.ProviderIds.Undo, false, true) as Rhino.Render.RdkUndo;

        if(_undo != null)
          _undo_record = _undo.NewUndoRecord();

        if(_undo_record != null)
          _undo_record.SetDescription(description);
      }

      public void Dispose()
      {
        Dispose(true);
        GC.SuppressFinalize(this);
      }

      ~NewUndoRecord()
      {
        Dispose(false);
      }

      protected virtual void Dispose(bool disposing)
      {
        if (_disposed)
          return;

        if (_undo_record != null)
        {
          _undo_record.Dispose();
        }
        _disposed = true;
      }
    }

(Pseudocode)Then one can use it with a simple using statement like below. The using statement creates and destroys the undo record in a nice way.

      Rhino.Render.RenderContentCollection collection = ContentSelectionForSetParamsWriter;
      Rhino.Render.ContentCollectionIterator iterator = collection.Iterator();

      using (var u = NewUndoHelper("Change some value"))
      {
        Rhino.Render.RenderContent content = iterator.First();

        while (content != null)
        {
          content.SetParameter("some-value", (bool)some_value);
          content = iterator.Next();
        }

        Commit(ProviderIds.ContentSelectionForSetParams);

        iterator.DeleteThis();
      }

The helper class and method should be made available through the sdk. I will check where it should be placed, but for now you could just copy these and see if they work for you.

The selection solution should also work. So if you select the child and commit the selection datasource, then the same editor should open it for editing.

Ok, so forget what I wrote about the undo datasources.

This is the correct way to go. I have translated John C. Cpp pseudocode. Below is the same code in C#.

          using (var cu = new Rhino.Render.ContentUndoHelper(Rhino.RhinoDoc.ActiveDoc, "new layer"))
          {
            cu.AddContent(contentLayer, contentMaterial);
            contentMaterial.AddChild(contentLayer,"layer")
          }
1 Like

@maxsoder In the underlying C++, SetField() includes content undo. SetChild() is a lower level function and it does not include undo which is why clients must add it to their code. Perhaps it would be possible to include the undo inside the C# SetChild() – but maybe that would break existing code. Anyway, for now, the client must type it :slight_smile:

John

Thanks, I had implemented now the ContentUndoHelper method and it seems it works fine! At least for adding child content (I haven’t tried if inverse works too)

Márton

RH-56652 is fixed in the latest WIP

Thanks!!!