The basic element that Nitrous uses to display anything on the screen is RenderItem
. The object plug-in must generate a list of RenderItem
s according to the scene and viewport context. The object plug-ins usually do not need to hold references to these RenderItem
s. Nitrous internally organizes the RenderItem
s 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 RenderItem
s under the RenderNode
.
RenderItem
TypesAll RenderItem
s are exposed in the Nitrous SDK as SmartHandle
s. The base class of all RenderItem
s is RenderItemHandle
. The following diagram shows the relationship between the RenderItemHandle
classes:
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()
.
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:
IMeshDisplay2
interface from the Mesh
or MNMesh
instance.IMeshDisplay2::PrepareDisplay()
, which is usually called in the plug-in's PrepareDisplay()
function.IMeshDisplay2::GetRenderItems()
in the plug-in's UpdatePerNodeItems()
or UpdatePerViewItems()
function.Elements in Mesh
and MNMesh
include the following:
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 ownGenerateMeshRenderItemContext
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);
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:
GeometryRenderItemHandle
. However, if this value is specified, the GeometryRenderItemHandle
uses the specified world matrix regardless of the owner node's matrix.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.
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()
orICustomRenderItem::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;
}