About RenderItem

The basic element that Nitrous uses to display anything on the screen is RenderItem. The object plug-in must generate a list of RenderItems according to the scene and viewport context. The object plug-ins usually do not need to hold references to these RenderItems. Nitrous internally organizes the RenderItems and manages their lifetime.

Many render nodes can refer to a RenderItem simultaneously. The following figure shows a Nitrous scene graph example:

In Nitrous, the RenderNode has one-one mapping to max INode, while the object/modifier plug-in generates RenderItems under the RenderNode.

RenderItem Types

All RenderItems are exposed in the Nitrous SDK as SmartHandles. The base class of all RenderItems is RenderItemHandle. The following diagram shows the relationship between the RenderItemHandle classes:

Visibility Group of RenderItem

The visibility group of a RenderItem can be one of the following values:

Value Description
RenderItemVisible_Shaded The RenderItem is visible if the visual style is shaded, realistic, hidden line, flat, facet, and others.
RenderItemVisible_Wireframe The RenderItem is visible in wireframe visual style. The edged faces are on or the display selected edged faces is on, and the node is selected.
RenderItemVisible_Gizmo The RenderItem is always visible irrespective of the visual style. This is the default visibility group for all RenderItem. Any RenderItem in this group does not receive lights because this group is for gizmos.

The visibility group provides a basic visibility control. For more flexible visibility control, the plug-in can use IObjectDisplay2::UpdatePerNodeItems() and IObjectDisplay2::UpdatePerViewItems().

Getting RenderItems from Mesh and MNMesh

A lot of plug-ins use Mesh or MNMesh to store the display data. The Nitrous SDK allows plug-ins to quickly get render items from a Mesh or MNMesh and display them.

To get GeometryRenderItemHandle, you need to do the following:

  1. Get the IMeshDisplay2 interface from the Mesh or MNMesh instance.
  2. Call IMeshDisplay2::PrepareDisplay(), which is usually called in the plug-in’s PrepareDisplay() function.
  3. Use IMeshDisplay2::GetRenderItems() in the plug-in’s UpdatePerNodeItems() or UpdatePerViewItems() function.

Mesh Elements

Elements in Mesh and MNMesh include the following:

  • Solid mesh
  • Selected face
  • Vertex ticks
  • Wireframe
  • Selected edge
  • Diagonal
  • Selected Diagonal
  • Wireframe with soft selection color
  • Backface culled wireframe

The typical elements of a Mesh and MNMesh are show in the following figure:

The Nitrous SDK allows plug-ins to get the specified element easily. Each element is represented as a render item.

IMeshDisplay2

The IMeshDisplay2 interface can be used to obtain the render items from a Mesh and MNMesh. It is easy to get the IMeshDisplay2 interface from a Mesh or MNMesh instance as shown in the following example:

IMeshDisplay2* pMeshDisplay = NULL;
pMeshDisplay = static_cast<IMeshDisplay2*>(mesh.GetInterface(IMesh_DISPLAY2_INTERFACE_ID));

The IMeshDisplay2 interface is defined as follows:

class IMeshDisplay2:public BaseInterface
{
    virtual void PrepareDisplay(
        const GenerateMeshRenderItemsContext& renderItemContext) = 0;
    virtual bool GetRenderItems(
        const GenerateMeshRenderItemsContext& renderItemContext,
        UpdateNodeContext& nodeContext,
        IRenderItemContainer& targetRenderItemContainer) = 0;
};

The two functions must be called in strict order. The IMeshDisplay2::PrepareDisplay() is for initializing the internal render item builder with a element list. After the render item builder is initialized, the plug-ins can call IMeshDisplay2::GetRenderItems() multiple times to get the render items that are prepared. Plug-ins must make sure that the renderItemContext passed to GetRenderItems() equals or is a subset of the renderItemContext passed to PrepareDisplay().

Usually,IMeshDisplay2::PrepareDisplay() is called in Object::PrepareDisplay(), and IMeshDisplay2::GetRenderItems() is called in Object::UpdatePerNodeItems() or Object::UpdatePerViewItems(). However, you can call these functions in other places.

GenerateMeshRenderItemsContext

Both functions of IMeshDisplay2 require a renderItemContext input. This parameter defines the required mesh element list and their vertex buffer format. Usually, an object/modifier plug-in can use the default mesh render item context to make the mesh display exactly the same as a standard object (respond to visual style, respond to node properties, and others). The following code shows how to create the default GenerateMeshRenderItemContext:

GenerateMeshRenderItemsContext renderItemContext;
renderItemContext.GenerateDefaultContext(displayContext);

You can create your own GenerateMeshRenderItemContext to get certain render items from the mesh. For example, you might only want to get the backface culled vertex tick of the mesh, and then you can just specify the vertex tick element.

GenerateMeshRenderItemsContext renderItemContext;
MeshElementDescription elementDescription;

elementDescription = GetBuiltInMeshElementDescription(ElementDescriptionVetexTicks);
elementDescription.SetBackfaceCull(true);
renderItemContext.AddMeshElementDescription(elementDescription);

Customize RenderItems

GeometryRenderItemHandle

The GeometryRenderItemHandle is a commonly used RenderItem type. It contains two parts: the display properties and actual geometry. The display properties include the following:

  • World matrix – By default, the owner node’s world matrix is used. Usually, a plug-in does not need to specify the world matrix for a GeometryRenderItemHandle. However, if this value is specified, the GeometryRenderItemHandle uses the specified world matrix regardless of the owner node's matrix.
  • Material – By default, the owner node’s material is used. Usually, a plug-in does not need to specify the material for a GeometryRenderItemHandle. However if this value is specified, the GeometryRenderItemHandle uses the specified material regardless of the node material.

The geometry contains a set of buffers. It controls how these buffers are drawn. For example, if a plug-in needs to display triangles, it can create a PrimitiveListGeometry instance to store the triangle data. The following example code shows the implementation of PrimitiveListGeometry:

class PrimitiveListGeometry:public IRenderGeometry
{
public:
    // From IRenderGeometry
    virtual void Display(
        DrawContext& dc,
        int start,
        int count,
        int lod)
    {
        IVirtualDevice& vd = *dc.GetVirtualDevice();
        vd.SetStreamFormat(mStreamFormat);
        vd.SetVertexStreams(mVertexBufferArray);
        if (mIndexBuffer.IsValid())
        {
            vd.SetIndexBuffer(&mIndexBuffer);
        }
        else
        {
            vd.SetIndexBuffer(nullptr);
        }
        vd.Draw(mPrimType, start, count);
    }

    virtual PrimitiveType GetPrimitiveType()
    {
        return mPrimType;
    }

    virtual size_t GetVertexCount()
    {
        if (mVertexBufferArray.length() > 0)
        {
            return mVertexBufferArray[0].GetNumberOfVertices();
        }
        else
        {
            return 0;
        }
    }
    // Other functions

private:
    VertexBufferHandleArray mVertexBufferArray;
    IndexBufferHandle mIndexBuffer;
    PrimitiveType mPrimType;
    MaterialRequiredStreams mStreamFormat;
    size_t mPrimitiveCount;
};

The plug-in must make sure that the geometry has consistent format with the material. Otherwise, if the geometry has inconsistent vertex buffer format compared to the material stream requirement, the geometry is not displayed in the viewport.

CustomRenderItemHandle

If a plug-in wants to use a low-level device API to better control the display process, the plug-in needs to initialize and provide its own CustomRenderItemHandle. The following code shows the CustomRenderItemHandle declaration:

class CustomRenderItemHandle:public RenderItemHandle
{
public:
    virtual void SetCustomImplementation(ICustomRenderItem* impl) = 0;
    virtual ICustomRenderItem* GetCustomImplementation() = 0;
};

The CustomRenderItemHandle is just a container. The actual display code is provided by ICustomRenderItem. Plug-ins need to create ICustomRenderItem and set the pointer to a valid CustomRenderItemHandle. The following code shows the interface declaration of ICustomRenderItem:

class ICustomRenderItem:public ARefObject, public BaseInterfaceServer
{
public:
    virtual void Realize(DrawContext& dc) = 0;
    virtual void Display(DrawContext& dc) = 0;
};

In ICustomRenderItem::Realize() function, the plug-in must build customized display data. In ICustomRenderItem::Display() function, the plug-in must use the pre-built display data to draw. The ICustomRenderItem::Realize() function is provided because the render item might be realized once, but displayed multiple times (for example, shadow map pass). In the ICustomRenderItem::Realize() function, the plug-ins must cache and try to reuse the display data whenever possible.

NOTE:Nitrous might be running in a separate thread, and it is important for plug-ins to implement their ICustomRenderItem as self-independent. It must not have any dependency on the object plug-in or other outside resources.

The following example shows a typical Display() function.

class SampleItemDX9 : public ICustomRenderItem
{
    ...
};
void SampleItemDX9::Display(DrawContext& dc)
{
    IVirtualDevice& vd = drawContext.GetVirtualDevice();
    BaseMaterialHandle& material = const_cast<BaseMaterialHandle&>(drawContext.GetMaterial()); //Use draw context’s material
    vd.SetStreamFormat(const_cast<MaterialRequiredStreams&>(*material.GetRequiredStreams()));
    vd.SetVertexStreams(mVertexBuffer);
    vd.SetIndexBuffer(&mIndexBuffer);
    
    material.Activate(drawContext);
    unsigned int passCount = material.GetPassCount(drawContext);
    for (int pass = 0; pass < passCount; ++pass) //Loop through all passes
    {
        material.ActivatePass(drawContext, pass);
        vd.Draw(PrimitiveTriangleList, 0, (int)mPrimitiveCount);
    }
    material.Terminate(drawContext);
}

To create and add the custom render item handle, you must first create the instance of the item. Therefore, the PrepareDisplay() function might be as shown in the following example:

bool CustomPlugin::PrepareDisplay(
      const MaxSDK::Graphics::UpdateDisplayContext& displayContext)
{
      if (nullptr == mpSampleItem)
      {
           MaxSDK::Graphics::DeviceCaps caps;
           GetIDisplayManager2()->GetDeviceCaps(caps);
           if (caps.FeatureLevel != Level5_0)
           {
               mpSampleItem = new SampleItemDX9;
           }
           else
           {
               mpSampleItem = new SampleItemDX11;
           }		
      }
}

After creating the custom render item instance, you can use a CustomRenderItemHandle to wrap it and add it to the render pipeline. This is typically done in the UpdatePerNodeItems() function.

bool CustomPlugin::UpdatePerNodeItems(
      const UpdateDisplayContext& updateDisplayContext,
      UpdateNodeContext& nodeContext,
      IRenderItemContainer& targetRenderItemContainer)
{
      CustomRenderItemHandle newHandle;
      newHandle.Initialize();
      //VisibilityGroup can be shaded or gizmo. By putting to shaded group,
      //the render item can take benefit from multipass effects like DOF.
      //Gizmo group is rendered in each viewport.
      newHandle.SetVisibilityGroup(RenderItemVisible_Gizmo);
      newHandle.SetCustomImplementation(mpSampleItem.GetPointer());
      targetRenderItemContainer.AddRenderItem(newHandle);
      return true;
}

DrawContext

When implementing the ICustomRenderItem::Realize() or ICustomRenderItem::Display() function, plug-ins might follow the draw context. The following figure shows what a draw context is:

RenderItemHandleDecorator

At times if you are not satisfied with the render item generated by the mesh or other objects, the RenderItemHandleDecorator can help replace the display properties (matrix, material) of another GeometryRenderItemHandle without modifying the original render item. This is especially useful when you want to customize the display of an object that the plug-in references, but you do not have the source code of the object.

The following figure shows how RenderItemHandleDecorator works:

Following is an example that shows the use of RenderItemHandleDecorator. A BoolObject might refer to two sub-objects and it wants to display both the sub-objects correctly.

class BoolObject : 
      public GeomObject, 
      public IObjectDisplay2
{
      ...
      Mesh mMesh;
      Tab<IObjectDisplay*> mSubObjectDisplays;
      Tab<IObjectDisplay2*> mSubObject2Displays;
};

BaseInterface* BoolObject::GetInterface(Interface_ID id)
{
      // Remember to expose IOBJECT_DISPLAY2_INTERFACE_ID for 
      // the BoolObject that helps Nitrous determine if the plug-in 
      // is ported using IOjbectDisplay2
      if (id == IOBJECT_DISPLAY2_INTERFACE_ID)
      {
          return static_cast<IObjectDisplay2*>(this);
      }
      return IBoolObject::GetInterface(id);
}

unsigned long BoolObject::GetObjectDisplayRequirement() const
{
      using namespace MaxSDK::Graphics;
      unsigned long requirement = ObjectDisplayRequireLegacyDisplayMode;
      if (!GetDisplayResult())
      {
          // Because updating render items on per view basis 
          // is very time consuming, so please append the 
          // ObjectDisplayRequireUpdatePerViewItems requirement
          // when you really need to add per view items.
          requirement |= ObjectDisplayRequireUpdatePerViewItems;
      }

      return requirement;
}

bool BoolObject::PrepareDisplay(
      const UpdateDisplayContext& prepareDisplayContext)
{
      TimeValue t = prepareDisplayContext.GetDisplayTime();
      RenderItemHandleArray tempRenderItemArray;
      IObjectDisplay* pObjDisplay = NULL;

      mRenderItemHandles.ClearAllRenderItems();
      mSubObjectDisplays.SetCount(0);
      mSubObject2Displays.SetCount(0);

      if (GetDisplayResult()) {
          UpdateMesh(t,true);  
          IMeshDisplay2* pMeshDisplay = static_cast<IMeshDisplay2*>(mMesh.GetInterface(IMesh_DISPLAY2_INTERFACE_ID));
          if (NULL == pMeshDisplay)
          {
              return false;
          }
          GenerateMeshRenderItemsContext itemContext;
          itemContext.GenerateDefaultContext(prepareDisplayContext);
          pMeshDisplay->PrepareDisplay(itemContext);
      
          return true;
      } 
      // display the two sub-objects
      // Note : Currently there are still some plug-ins not yet ported 
      // using IObjectDisplay2. So, check if 
      // IObjectDisplay is exposed from the operand of this bool 
      // object and then call IObjectDisplay::UpdateDisplay 
      // accordingly.

      for (int i = 0; i < 2; i++) 
      {
          IObjectDisplay *pObjDisplay = NULL;
          IObjectDisplay2 *pObjDisplay2 = NULL;
          Object *pObject = GetPipeObj(t,i);
          if (NULL != pObject)
          {
              pObjDisplay2 = static_cast<IObjectDisplay2*>(pObject->GetInterface(IOBJECT_DISPLAY2_INTERFACE_ID));
              if (NULL != pObjDisplay2)
              {
                  pObjDisplay2->PrepareDisplay(prepareDisplayContext);
              }
              else
              {
                  pObjDisplay = static_cast<IObjectDisplay*>(pObject->GetInterface(IOBJECT_DISPLAY_INTERFACE_ID));
                  if (NULL != pObjDisplay)
                  {
                      pObjDisplay->UpdateDisplay(prepareDisplayContext);
                  }
              }
          }
          mSubObjectDisplays.Append(1,&pObjDisplay);
          mSubObject2Displays.Append(1,&pObjDisplay2);
      }
      
      return true;
}

void BoolObject::PopulateSubObjectRenderItems(
      const Matrix3& renderItemMatrix,
      const IRenderItemContainer& sourceItems,
      IRenderItemContainer& targetItems)
{
      // Using RenderItemHandleDecorator to decorate the render 
      // items of the sub-object if we need to customize the 
      // behavior of those render items. In this sample, we 
      // need RenderItemHandleDecorator for two reasons:
      // 1. When choosing the 'Operands' option in the 
      //    'Display' panel, we need to customize the position 
      //    of render items of operands.
      // 2. When choosing the 'Billboard Operands' option in 
      //    the 'Display' panel, we need to customize the 
      //    rotation of those render items of operands to 
      //    align with the view camera.
      for (size_t i = 0; i < sourceItems.GetNumberOfRenderItems(); ++i)
      {
          RenderItemHandleDecorator hRenderItemDecorator;
          if (hRenderItemDecorator.Initialize(sourceItems.GetRenderItem(i)))
          {
              Matrix44 offsetMatrix;
              MaxWorldMatrixToMatrix44(offsetMatrix,renderItemMatrix);
              hRenderItemDecorator.SetOffsetMatrix(offsetMatrix);
              targetItems.AddRenderItem(hRenderItemDecorator);
          }
      }
}

void BoolObject::AddSubObjectRenderItems(
      int subObjectindex,
      const Matrix3& subObjectOffsetMatrix,
      const UpdateDisplayContext& updateDisplayContext,
      UpdateNodeContext& nodeContext,
      IRenderItemContainer& targetItems)
{
      using namespace MaxSDK::Graphics;
      if (NULL != mSubObjectDisplays[subObjectindex])
      {
          RenderItemHandleArray renderItems;
          // For IObjectDisplay instance, we still call 
          // IObjectDisplay::AddRenderItems() to add render 
          // items according to the properties of the current node
          mSubObjectDisplays[subObjectindex]->AddRenderItems(nodeContext.GetRenderNode(),renderItems);
          PopulateSubObjectRenderItems(subObjectOffsetMatrix, renderItems, targetItems);
      }
      if (NULL != mSubObject2Displays[subObjectindex])
      {
          RenderItemHandleArray renderItems;
          mSubObject2Displays[subObjectindex]->UpdatePerNodeItems(updateDisplayContext, nodeContext, renderItems);
          PopulateSubObjectRenderItems(subObjectOffsetMatrix, renderItems, targetItems);
      }
}

bool BoolObject::UpdatePerNodeItems(
      const UpdateDisplayContext& updateDisplayContext,
      UpdateNodeContext& nodeContext,
      IRenderItemContainer& targetItems)
{
      using namespace MaxSDK::Graphics;

      RenderNodeHandle& hRenderNode = nodeContext.GetRenderNode();
      if (GetDisplayResult()) 
      {  
          IMeshDisplay2* pMeshDisplay = static_cast<IMeshDisplay2*>(mMesh.GetInterface(IMesh_DISPLAY2_INTERFACE_ID));
          if (NULL == pMeshDisplay)
          {
              return false;
          }
          GenerateMeshRenderItemsContext itemContext;
          itemContext.GenerateDefaultContext(updateDisplayContext);
          // If you are not sure whether any mMesh element is not 
          // required for the current node, feel free to call 
          // GenerateMeshRenderItemsContext::
          //     RemoveInvisibleMeshElementDescriptions()
          // that will help you remove unwanted render items 
          // for hRenderNode.
          itemContext.RemoveInvisibleMeshElementDescriptions(hRenderNode);
          pMeshDisplay->GetRenderItems(itemContext,nodeContext,targetItems);
      }
      else if (TestFlag(BOOL_DISP_OPS))
      {
          TimeValue t = updateDisplayContext.GetDisplayTime();
          for (int i = 0; i < mSubObjectDisplays.Count(); ++i)
          {
          AddSubObjectRenderItems(i, GetOpTM(t,i), updateDisplayContext, nodeContext, targetItems);
          }
      }
      
      return true;
}

bool BoolObject::UpdatePerViewItems(
      const UpdateDisplayContext& updateDisplayContext,
      UpdateNodeContext& nodeContext,
      UpdateViewContext& viewContext,
      IRenderItemContainer& targetItems)
{
      using namespace MaxSDK::Graphics;
      if (GetDisplayResult())
      {
          return false;
      }

      bool bResult = false;
      TimeValue t = updateDisplayContext.GetDisplayTime();
      // To display Billborad operands
      if (TestFlag(BOOL_DISP_BILLBOARD_OPS))
      {
          Matrix3 viewMatrix;
          viewContext.GetView()->GetAffineTM(viewMatrix);
          Matrix3 inverseViewMatrix = Inverse(viewMatrix);
          for (int i = 0; i < mSubObjectDisplays.Count(); ++i)
          {	
              // Assign view-dependent offset matrix to the 
              // render items of operands
              inverseViewMatrix.SetTrans(GetOpTM(t,i).GetTrans());
              AddSubObjectRenderItems(i, inverseViewMatrix, updateDisplayContext, nodeContext, targetItems);
              bResult = true;
          }
      }

      for (int i = 0; i < mSubObject2Displays.Count(); ++i)
      {
          RenderItemHandleArray renderItems;
          IObjectDisplay2* pObjectDisplay2 = mSubObject2Displays[i];
          // For operands exposing IObjectDisplay2, we have to 
          // check if they require updating their render items 
          // on per-view basis and call IObjectDisplay2::
          // UpdatePerViewItems() accordingly.
          if (NULL != pObjectDisplay2 &&
              (pObjectDisplay2->GetObjectDisplayRequirement() & 
              ObjectDisplayRequireUpdatePerViewItems) &&
              pObjectDisplay2->UpdatePerViewItems(updateDisplayContext, nodeContext, viewContext, renderItems))
          {
              bResult = true;
              PopulateSubObjectRenderItems(
              GetOpTM(t,i),
              renderItems,
              targetItems);
          }
      }
      return bResult;
}