The Enhanced 3ds Max .NET SDK
 
 
 

How .NET assemblies are loaded by 3ds Max

There are three mechanisms to load utilities written using .NET into 3ds Max: you can use either CUIAction, .NET Assembly Loader and MAXScript. We will talk about the first two mechanisms in this topic.

CUIAction loader

An action item is simply a command that you can assign to a keyboard shortcut, toolbar, quad menu, or menu. You can define action items as a custom user interface (CUI) in 3ds Max. In order to create action items in a .NET assembly you need to create a public class that implements the interface ICuiActionCommand (found in MaxCustomControls.dll) and placing the DLL containing the implemented interface in the bin\assemblies folder. You can also extend from CuiActionCommandAdapter, an adapter class for ICuiActionCommand that makes implementation easier. 3ds Max will then dynamically load an instance of this class as an action in its CUI interface. When the action is executed by 3ds Max the Execute() function will be called on the loaded class. Following example shows how to write a custom action that adds a prompt to the screen.

public class MaxDotNetCUIDemo : CuiActionCommandAdapter {
    public override string ActionText {
        get { return InternalActionText; }
    }

    public override string Category {
        get { return InternalCategory; }
    }

    public override void Execute(object parameter) {
        Autodesk.Max.Glboal.Instance.COREInstance13.AddPrompt("Yeeehaaaaaaaaaa!");
    }

    public override string InternalActionText {
        get { return "Prompt me!"; }
    }

    public override string InternalCategory {
        get { return "3ds Max .NET Demo"; }
    }
}

.NET assembly loader

With the release of the Subscription Advantage Pack for 3ds Max 2012 there have been significant enhancements to the exposure of 3ds Max SDK to .NET. Previously only a small subset of the 3ds Max SDK was exposed to .NET via the ManagedServices.dll. Now nearly all of the global functions and classes in the SDK can be accessed directly from .NET using the new Autodesk.Max.dll.

The Autodesk.Max.dll assembly is automatically generated from the 3ds Max SDK header files, so most of the functions and classes in the 3ds Max SDK are mapped to similarly named constructs in the DLL.

One of the easiest ways to become familiar with the contents of the DLL is to load it in the Visual Studio object browser. In the rest of this document we will review some key features of this assembly.

Accessing the services exposed by 3ds Max - the IGlobal Interface and Interface13

Before we can do anything interesting with the Autodesk.Max.dll we need a handle to an instance of the "IGlobal" interface. This interface provides access to all of the global functions and variables in the 3ds Max SDK. You might use this from a function as follows:

IGlobal Global = Autodesk.Max.GlobalInterface.Instance;
IInterface13 Interface = _Global.COREInterface13;

Note the presence of a property "COREInterface13" instead of the expected function "GetCOREInterface13()". See the topic "Functions to Properties" for more information.

Interfaces and Classes

The 3ds Max SDK from C++ uses classes exclusively. What are called "interfaces" in the C++ SDK, are actually "abstract classes". That means they are instances of classes with one or more pure virtual functions. In Autodesk.Max.dll almost every class has a corresponding interface that is used where the class might be expected as an argument type or return type. For example we used IAnimatatable instead of Animatable. The DemoTeapot() function in the next example demonstrates this issue.

Functions to Properties

The Autodesk.Max.dll maps certain methods in the 3dsMax C++ SDK to .NET properties according to the following rules:

  • · The function must have no arguments
  • · The function name must start with 'Get'

The following code shows how the COREInterface property is used instead of the GetCOREInterface() function.

void DemoTeapot(IGlobal global) 
{
    IGlobal global = Autodesk.Max.GlobalInterface.Instance;  //note that global will be an instance of an abstract class.
    var intfc = global.COREInterface13;
    IClass_ID cid = global.Class_ID.Create((uint)BuiltInClassIDA.TEAPOT_CLASS_ID, (uint)BuiltInClassIDB.TEAPOT_CLASS_ID);
    object obj = intfc.CreateInstance(SClass_ID.Geomobject, cid as IClass_ID);
    if (obj == null) throw new Exception("Failed to create a sphere!");
    IINode n = global.COREInterface.CreateObjectNode((IObject)obj);
    IObject iobj = (IObject)obj;
    IIParamArray ps = iobj.ParamBlock;
    ps.SetValue(0, global.COREInterface.Time, 20.0f);
    n.Move(global.COREInterface.Time, global.Matrix3.Create(), global.Point3.Create(20, 20, 0), true, true, 0, true);
}

Instantiating types

In order to instantiate a specific type from the Autodesk.Max.dll we need to use properties of the IGlobal interface. For example in order to instantiate an instance of a Point3 you would write:

IPoint3 pt =
Autodesk.Max.GlobalInterface.Instance.Point3.Create(20, 20, 0)

Note that IGlobal.Point3 is a property that returns an instance of an interface of type IGlobal.IGlobalPoint3. The IGlobalPoint3 type is a member type of the IGlobal type and has a Create() function that will return a Point3 instance.

Default arguments

In Autodesk.Max.dll default arguments are not supported. As a result, some functions require many more arguments than the 3ds Max SDK requires. You can look up the values that are used as default arguments in the 3ds Max SDK documentation.

Macros

Many macros (especially function macros) are not ported from 3ds Max SDK to .NET. Instead, you will have to figure out what the macro expands to and then use it directly. For example in C++ the following line:

EPoly13* epoly = GetEPoly13Interface( myObject );
expands to:
EPoly13* epoly = (EPoly*)( myObject )->GetInterface( EPOLY13_INTERFACE );
So using Autodesk.Max.dll:
IEPoly epoly = (IEPoly)myObject.GetInterface( EPOLY13_INTERFACE );

To do this easily, it is recommended to use the Visual Studio "Find in Files" feature to search the \maxsdk\include folder for the correct macro expansion. Alternatively you can use the C++ SDK documentation.

Creating .NET Plug-ins

Using Autodesk.Max.dll it is also possible to create plug-ins by overriding classes in the Autodesk.Max.Plugins namespace.

The steps are:

  1. 1) Write a custom class derived from a plug-in base class in the Autodesk.Max.Plugins namespace (e.g. Autodesk.Max.Plugins.Texmap)
  2. 2) Write a custom class descriptor derived from Autodesk.Max.Plugins.ClassDesc2.
  3. 3) Register the new class descriptor using the function IInterface.AddClass() from public static void AssemblyMain() of your assembly.

Import and export plug-ins cannot be written in this manner. You can refer to the lesson on .NET plug-ins for detailed instructions of creating a .NET plug-in using Autodesk.Max.dll.

.NET Texture Map Example

namespace Samples.Texmap
{
    public class Texmap : Autodesk.Max.Plugins.Texmap
    {
        public class Descriptor : Autodesk.Max.Plugins.ClassDesc2
        {
            IParamBlockDesc2 paramBlockDesc;
            public IParamBlockDesc2 ParamBlockDesc
            {
                get { return this.paramBlockDesc; }
            }

            IGlobal global;

            public IGlobal Global {
                get { return this.global; }
            }

            public Descriptor( IGlobal global ) {
                this.global = global;
                this.paramBlockDesc = this.global.ParamBlockDesc2.Create( 0, "Parameters", IntPtr.Zero, this,
                    (ParamBlock2Flags)( (int)ParamBlock2Flags.Version + (int)ParamBlock2Flags.AutoConstruct + (int)ParamBlock2Flags.AutoUi ),
                    new object[] { 1, 0 } );
                // Add parameter and specify name, type, flags, control id, default, minimum, and maximum values
                this.paramBlockDesc.AddParam( 0, new object[] { "color", ParamType2.Float, ParamBlock2Flags.Animatable, 0, 0.5f, 0.0f, 1.0f } );
            }

            public override string Category {
                get { return "Samples"; }
            }

            public override IClass_ID ClassID {
                get { return this.Global.Class_ID.Create( 0x8217fd92, 0xef980abc ); }
            }

            public override string ClassName {
                get { return "Sample Texture"; }
            }

            public override object Create( bool loading ) {
                return new Texmap( this );
            }

            public override bool IsPublic {
                get { return true; }
            }

            public override SClass_ID SuperClassID    {
                get { return SClass_ID.Texmap; }
            }
        }

        Descriptor descriptor;
        IInterval validity;

        public Texmap( Descriptor descriptor ) {
            this.descriptor = descriptor;
            this.validity = this.descriptor.Global.Interval.Create();
            this.validity.SetInfinite();
        }

        public override IAColor EvalColor( IShadeContext sc ) {
            return this.descriptor.Global.AColor.Create( 0, 0, 0, 0 );
        }

        public override IPoint3 EvalNormalPerturb( IShadeContext sc ) {
            return this.descriptor.Global.Point3.Create( 0, 0, 0 );
        }

        public override IParamDlg CreateParamDlg( IntPtr hwMtlEdit, IIMtlParams imp ) {
            IIAutoMParamDlg masterDlg = this.descriptor.CreateParamDlgs( hwMtlEdit, imp, this );
            return masterDlg;
        }

        public override void Reset() {            
        }

        public override void Update( int t, IInterval valid ) {            
        }

        public override IInterval Validity( int t ) {
            return this.validity;
        }

        public override IReferenceTarget RefTarget {
            get { return this; }
        }

        public override RefResult NotifyRefChanged( IInterval changeInt, IReferenceTarget hTarget, ref UIntPtr partID, RefMessage message ) {
            return RefResult.Succeed;
        }

        public override void RefAdded( IReferenceMaker rm ) {
            // If rm is null we get an unmanaged 'access' exception
            if( rm != null ) 
                base.RefAdded( rm );
        }
    }
}

FAQ