Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Background

The Nodal Scene Interface (ɴsɪ) was developed to replace existing APIs in the 3Delight renderer which were showing their age. Particualry the RenderMan Interface and the RenderMan Shading Language.

Having been designed in the 80s and extended several times since, they include features which are no longer relevant and design decisions which do not reflect modern needs.

This makes some features more complex to use than they should be and prevents or greatly increases the complexity of implementing other features.

The design of the ɴsɪ was shaped by multiple goals:

  • Simplicity — The interface itself should be simple to understand and use, even if complex things can be done with it. This simplicity is carried into everything which derives from the interface.
  • Interactive Rendering and Scene Edits — Scene edit operations should not be a special case. There should be no difference between scene description and scene edits. In other words, a scene description is a series of edits and vice versa.
  • Tight Integration with Open Shading Languageᴏsʟ integration is not superficial and affects scene definition. For example, there are no explicit light sources in ɴsɪ: light sources are created by connecting shaders with an emission() closure to a geometry.
  • Scripting — The interface should be accessible from a platform independent, efficient and easily accessible scripting language. Scripts can be used to add render time intelligence to a given scene description.
  • Performance and Multi-Threading — All API design decisions are made with performance in mind and this includes the possibility to run all API calls in a concurrent, multi-threaded environment. Nearly all software today which deals with large data sets needs to use multiple threads at some point. It is important for the interface to support this directly so it does not become a single thread communication bottleneck. This is why commands are self-contained and do not rely on a current state. Everything which is needed to perform an action is passed in on every call.
  • Support for Serialization — The interface calls should be serializable. This implies a mostly unidirectional dataflow from the client application to the renderer and allows greater implementation flexibility.
  • Extensibility — The interface should have as few assumptions as possible built-in about which features the renderer supports. It should also be abstract enough that new features can be added without looking out of place.

The Interface

The Interface Abstraction

The Nodal Scene Interface is built around the concept of nodes. Each node has a unique handle to identify it and a type which describes its intended function in the scene. Nodes are abstract containers for data. The interpretation depends on the node type. Nodes can also be connected to each other to express relationships.

Data is stored on nodes as attributes. Each attribute has a name which is unique on the node and a type which describes the kind of data it holds (strings, integer numbers, floating point numbers, etc).

Relationships and data flow between nodes are represented as connections. Connections have a source and a destination. Both can be either a node or a specific attribute of a node. There are no type restrictions for connections in the interface itself. It is acceptable to connect attributes of different types or even attributes to nodes. The validity of such connections depends on the types of the nodes involved.

What we refer to as the ɴsɪ has two major components:

  • Methods to create nodes, attributes and their connections.
  • Node types understood by the renderer.

Much of the complexity and expressiveness of the interface comes from the supported nodes. The first part was kept deliberately simple to make it easy to support multiple ways of creating nodes. We will list a few of those in the following sections but this list is not meant to be final. New languages and file formats will undoubtedly be supported in the future.

APIs

The C API

This section describes the C implementation of the ɴsɪ, as provided in the nsi.h file. This will also be a reference for the interface in other languages as all concepts are the same.

#define NSI_VERSION 1

The NSI_VERSION macro exists in case there is a need at some point to break source compatibility of the C interface.

#define NSI_SCENE_ROOT ".root"

The NSI_SCENE_ROOT macro defines the handle of the root node.

#define NSI_ALL_NODES ".all"

The NSI_ALL_NODES macro defines a special handle to refer to all nodes in some contexts, such as removing connections.

#define NSI_ALL_ATTRIBUTES ".all"

The NSI_ALL_ATTRIBUTES macro defines a special handle to refer to all attributes in some contexts, such as removing connections.

Context Handling

NSIContext_t NSIBegin(
   int n_params,
   const NSIParam_t *args
)
void NSIEnd(
   NSIContext_t ctx
)

These two functions control creation and destruction of a ɴsɪ context, identified by a handle of type NSIContext_t.

A context must be given explicitly when calling all other functions of the interface. Contexts may be used in multiple threads at once. The NSIContext_t is a convenience typedef and is defined as:

typedef int NSIContext_t;

If NSIBegin fails for some reason, it returns NSI_BAD_CONTEXT which is defined in nsi.h:

#define NSI_BAD_CONTEXT ((NSIContext_t)0)

Optional arguments may be given to NSIBegin() to control the creation of the context:

NameTypeDescription/Values
typestringSets the type of context to create. The possible types are:
render — Execute the calls directly in the renderer. This is the default.
apistream — To write the interface calls to a stream, for later execution. The target for writing the stream must be specified in another argument.
streamfilenamestringThe file to which the stream is to be output, if the context type is apistream. Specify stdout to write to standard output and stderr to write to standard error.
streamformatstringThe format of the command stream to write. Possible formats are:
nsi — Produces an ɴsɪ stream.
binarynsi — Produces a binary encoded ɴsɪ stream.
autonsi — Automatically selects the best format.
stream.compressionstringThe type of compression to apply to the written command stream.
streampathreplacementintUse 0 to disable replacement of path prefixes by references to environment variables which begin with NSI_PATH_ in an ɴsɪ stream. This should generally be left enabled to ease creation of files which can be moved between systems.
separateprocessintWhen set to 1, the render will be executed in a separate process.
errorhandlerpointerA function which is to be called by the renderer to report errors. The default handler will print messages to the console.
errorhandler.datapointerThe userdata argument of the error reporting function.
executeproceduralsstringA list of procedural types that should be executed immediately when a call to NSIEvaluate() or a procedural node is encountered and NSIBegin()’s output type is apistream. This will replace any matching call to NSIEvaluate() with the results of the procedural’s execution.

Arguments vs. Attributes

Arguments are what a user specifies when calling a function of the API. Each function takes extra, optional arguments.

Attributes are properties of nodes and are only set through the aforementioned optional arguments using the NSISetAttribute() and NSISetAttributeAtTime() functions.

Optional Arguments

Any API call can take extra arguments. These are always optional. What this means the call can do work without the user specifying these arguments.

Nodes are special as they have mandatory extra attributes that are set after the node is created inside the API but which must be set before the geometry or concept the node represents can actually be created in the scene.

These attributes are passed as extra arguments to the NSISetAttribute() and NSISetAttributeAtTime() functions.

[!NOTE] Nodes can also take extra arguments when they are created. These optional arguments are only meant to add information needed to create the node that a particular implementation may need.

As of this writing there is no implementation that has any such optional arguments on the NSICreate() function. The possibility to specify them is solely there to make the API future proof.

[!CAUTION] Nodes do not have optional arguments for now. An optional argument on a node is not the same as an attribute on a node.

Attributes — Describe the Node’s Specifics

Attributes are only for nodes. They must be set using the NSISetAttribute() or NSISetAttributeAtTime() functions.

They can not be set on the node when it is created with the NSICreate() function.

[!CAUTION] Only nodes have attributes. They are sent to the API via optional arguments on the API’s attribute functions.

Passing Optional Arguments

struct NSIParam_t
{
    const char *name;
    const void *data;
    int type;
    int arraylength;
    size_t count;
    int flags;
};

This structure is used to pass variable argument lists through the C interface. Most functions accept an array of the structure in a args argument along with its length in a n_params argument.

The meaning of these two arguments will not be documented for every function. Instead, each function will document the arguments which can be given in the array.

name : A C string which gives the argument’s name.

type : Identifies the argument’s type, using one of the following constants:

ConstantDescription
NSITypeFloatSingle 32-bit floating point value.
NSITypeDoubleSingle 64-bit floating point value.
NSITypeIntegerSingle 32-bit integer value.
NSITypeStringString value, given as a pointer to a C string.
NSITypeColorColor, given as three 32-bit floating point values.
NSITypePointPoint, given as three 32-bit floating point values.
NSITypeVectorVector, given as three 32-bit floating point values.
NSITypeNormalNormal vector, given as three 32-bit floating point values.
NSITypeMatrixTransformation matrix, in row-major order, given as 16 32-bit floating point values.
NSITypeDoubleMatrixTransformation matrix, in row-major order, given as 16 64-bit floating point values.
NSITypePointerC pointer.

Tuple types are specified by setting the bit defined by the NSIArgIsArray constant in the flags member and the length of the tuple in the arraylength member.

[!TIP] It helps to view arraylength as a part of the data type. The data type is a tuple with this length when NSIArgIsArray is set.

[!NOTE] If NSIArgIsArray is not set, arraylength is ignored.

The NSIArgIsArray flag is necessary to distinguish between arguments that happen to be of length 1 (set in the count member) and tuples that have a length of 1 (set in the arraylength member) for the resp. argument.

"foo" "int[1]" 1 [42]  # The answer to the ultimate question – in a (single) tuple
"bar" "int" 1 13       # My favorite Friday

The count member gives the number of data items given as the value of the argument.

The data member is a pointer to the data for the argument. This is a pointer to a single value or a number of values. Depending on type, count and arraylength settings.

[!NOTE] When data is an array, the actual number of elements in the array is count × arraylength × n. Where n is specified implicitly through the type member in the table above.

For example, if the type is NSITypeColor (3 values), NSIArgIsArray is set, arraylength is 2 and count is 4, data is expected to contain 24 32-bit floating point values (3×2×4).

The flags member is a bit field with a number of constants used to communicate more information about the argument:

FlagDescription
NSIArgIsArrayTo specify that the argument is an array type, as explained above.
NSIArgPerFaceTo specify that the argument has different values for every face of a geometric primitive, where this might be ambiguous.
NSIArgPerVertexSpecify that the argument has different values for every vertex of a geometric primitive, where this might be ambiguous.
NSIArgInterpolateLinearSpecify that the argument is to be interpolated linearly instead of using some other, default method.

[!NOTE] NSIArgPerFace or NSIArgPerVertex are only strictly needed in rare circumstances when a geometric primitive’s number of vertices matches the number of faces. The most simple case is a tetrahedral mesh which has exactly four vertices and also four faces.

Indirect lookup of arguments is achieved by giving an integer argument of the same name, with the .indices suffix added. This is read to know which values of the other argument to use.

Create "subdiv" "mesh"
SetAttribute "subdiv"
  "nvertices" "int" 4 [ 4 4 4 4 ]
  "P" "point" 9 [
    0 0 0  1 0 0  2 0 0
    0 1 0  1 1 0  2 1 0
    0 2 0  1 2 0  2 2 2 ]
  "P.indices" "int" 16 [
    0 1 4 3  2 3 5 4  3 4 7 6  4 5 8 7 ]
  "subdivision.scheme" "string" 1 "catmull-clark"

Node Creation

void NSICreate(
    NSIContext_t context,
    NSIHandle_t handle,
    const char *type,
    int n_params,
    const NSIParam_t *args
)

This function is used to create a new node. Its arguments are:

context : The context returned by NSIBegin(). See context handling.

handle : A node handle. This string will uniquely identify the node in the scene.

If the supplied handle matches an existing node, the function does nothing if all other arguments match the call which created that node. Otherwise, it emits an error. Note that handles need only be unique within a given interface context. It is acceptable to reuse the same handle inside different contexts. The NSIHandle_t typedef is defined in nsi.h:

typedef const char* NSIHandle_t;

type : The type of node to create.

n_params, args : This pair describes a list of optional arguments. The NSIParam_t type is described in this section.

[!CAUTION] There are no optional arguments defined as of now.


void NSIDelete(
    NSIContext_t ctx,
    NSIHandle_t handle,
    int n_params,
    const NSIParam_t *args
)

This function deletes a node from the scene. All connections to and from the node are also deleted. Note that it is not possible to delete the root or the global node. Its arguments are:

context : The context returned by NSIBegin(). See context handling.

handle : A node handle. It identifies the node to be deleted.

It accepts the following optional arguments:

NameTypeDescription/Values
recursiveintSpecifies whether deletion is recursive. By default, only the specified node is deleted. If a value of 1 is given, then nodes which connect to the specified node are recursively removed unless they also have connections which do not eventually lead to the specified node, or their connection to the deleted node was created with a strength greater than 0. This allows, for example, deletion of an entire shader network in a single call.

Setting Attributes

void NSISetAttribute(
    NSIContext_t ctx,
    NSIHandle_t object,
    int n_params,
    const NSIParam_t *args
)

This function sets attributes on a previously created node. All optional arguments of the function become attributes of the node.

On a shader node, this function is used to set the implicitly defined shader arguments.

Setting an attribute using this function replaces any value previously set by NSISetAttribute() or NSISetAttributeAtTime(). To reset an attribute to its default value, use NSIDeleteAttribute().


void NSISetAttributeAtTime(
    NSIContext_t ctx,
    NSIHandle_t object,
    double time,
    int n_params,
    const NSIParam_t *args
)

This function sets time-varying attributes (i.e. motion blurred). The time argument specifies at which time the attribute is being defined.

It is not required to set time-varying attributes in any particular order. In most uses, attributes that are motion blurred must have the same specification throughout the time range.

A notable exception is the P attribute on particles which can be of different size for each time step because of appearing or disappearing particles. Setting an attribute using this function replaces any value previously set by NSISetAttribute().


void NSIDeleteAttribute(
    NSIContext_t ctx,
    NSIHandle_t object,
    const char *name
)

This function deletes any attribute with a name which matches the name argument on the specified object. There is no way to delete an attribute only for a specific time value.

Deleting an attribute resets it to its default value.

For example, after deleting the transformationmatrix attribute on a transform node, the transform will be an identity. Deleting a previously set attribute on a shader node will default to whatever is declared inside the shader.

Making Connections

void NSIConnect(
    NSIContext_t ctx,
    NSIHandle_t from,
    const char *from_attr,
    NSIHandle_t to,
    const char *to_attr,
    int n_params,
    const NSIParam_t *args
)
void NSIDisconnect(
    NSIContext_t ctx,
    NSIHandle_t from,
    const char *from_attr,
    NSIHandle_t to,
    const char *to_attr
)

These two functions respectively create or remove a connection between two elements. It is not an error to create a connection which already exists or to remove a connection which does not exist but the nodes on which the connection is performed must exist. The arguments are:

from : The handle of the node from which the connection is made.

from_attr : The name of the attribute from which the connection is made. If this is an empty string then the connection is made from the node instead of from a specific attribute of the node.

to : The handle of the node to which the connection is made.

to_attr : The name of the attribute to which the connection is made. If this is an empty string then the connection is made to the node instead of to a specific attribute of the node.

NSIConnect() accepts additional optional arguments.

NameTypeDescription/Values
valueThis can be used to change the value of a node’s attribute in some contexts. Refer to guidelines on inter-object visibility for more information about the utility of this parameter.
priorityWhen connecting attribute nodes, indicates in which order the nodes should be considered when evaluating the value of an attribute.
strengthint (0)A connection with a strength greater than 0 will block the progression of a recursive NSIDelete.

Severing Connections

With NSIDisconnect(), the handle for either node may be the special value .all. This will remove all connections which match the other three arguments. For example, to disconnect everything from the scene’s root:

NSIDisconnect( NSI_ALL_NODES, "", NSI_SCENE_ROOT, "objects" );

Evaluating Procedurals

void NSIEvaluate(
    NSIContext_t ctx,
    int n_params,
    const NSIParam_t *args
)

This function includes a block of interface calls from an external source into the current scene. It blends together the concepts of a straight file include, commonly known as an archive, with that of procedural include which is traditionally a compiled executable. Both are really the same idea expressed in a different language (note that for delayed procedural evaluation one should use the procedural node).

The ɴsɪ adds a third option which sits in-between — Lua scripts. They are much more powerful than a simple included file yet they are also much easier to generate as they do not require compilation. It is, for example, very realistic to export a whole new script for every frame of an animation. It could also be done for every character in a frame. This gives great flexibility in how components of a scene are put together.

The ability to load ɴsɪ commands straight from memory is also provided.

The optional arguments accepted by this function are:

NameTypeDescription/Values
typestringThe type of file which will generate the interface calls. This can be one of:
apistream — Read in an ɴsɪ stream. This requires either filename or buffer/size arguments to be specified too.
lua — Execute a Lua script, either from file or inline. See also how to evaluate a Lua script.
dynamiclibrary — Execute native compiled code in a loadable library. See dynamic library procedurals for an implementation example.
filenamestringThe file from which to read the interface stream.
scriptstringA valid Lua script to execute when type is set to lua.
buffer / sizepointer / intThese two arguments define a memory block that contains ɴsɪ commands to execute.
backgroundloadintIf this is nonzero, the object may be loaded in a separate thread, at some later time. This requires that further interface calls not directly reference objects defined in the included file. The only guarantee is that the file will be loaded before rendering begins.

Error Reporting

enum NSIErrorLevel
{
    NSIErrMessage = 0,
    NSIErrInfo = 1,
    NSIErrWarning = 2,
    NSIErrError = 3
}
typedef void (*NSIErrorHandler_t)(
    void *userdata, int level, int code, const char *message
)

This defines the type of the error handler callback given to the NSIBegin() function. When it is called, the level argument is one of the values defined by the NSIErrorLevel enum. The code argument is a numeric identifier for the error message, or 0 when irrelevant. The message argument is the text of the message.

The text of the message will not contain the numeric identifier nor any reference to the error level. It is usually desirable for the error handler to present these values together with the message. The identifier exists to provide easy filtering of messages.

The intended meaning of the error levels is as follows:

LevelDescription
NSIErrMessageFor general messages, such as may be produced by printf() in shaders. The default error handler will print this type of messages without an eol terminator as it’s the duty of the caller to format the message.
NSIErrInfoFor messages which give specific information. These might simply inform about the state of the renderer, files being read, settings being used and so on.
NSIErrWarningFor messages warning about potential problems. These will generally not prevent producing images and may not require any corrective action. They can be seen as suggestions of what to look into if the output is broken but no actual error is produced.
NSIErrErrorFor error messages. These are for problems which will usually break the output and need to be fixed.

Rendering

void NSIRenderControl(
    NSIContext_t ctx,
    int n_params,
    const NSIParam_t *args
)

This function is the only control function of the API. It is responsible for starting, suspending and stopping the render. It also allows for synchronizing the render with interactive calls that might have been issued. The function accepts:

NameTypeDescription/Values
actionstringSpecifies the operation to be performed, which should be one of the following:
start — This starts rendering the scene in the provided context. The render starts in parallel and the control flow is not blocked.
wait — Wait for a render to finish.
synchronize — For an interactive render, apply all the buffered calls to scene’s state.
suspend — Suspends render in the provided context.
resume — Resumes a previously suspended render.
stop — Stops rendering in the provided context without destroying the scene.

NSIRenderControl() accepts the following optional arguments:

NameTypeDescription/Values
progressiveintegerIf set to 1, render the image in a progressive fashion.
interactiveintegerIf set to 1, the renderer will accept commands to edit scene’s state while rendering. The difference with a normal render is that the render task will not exit even if rendering is finished. Interactive renders are by definition progressive.
frameSpecifies the frame number of this render.
stoppedcallbackpointerA pointer to a user function that should be called on rendering status changes. The function signature is:
void StoppedCallback(
    void* stoppedcallbackdata,
    NSIContext_t ctx,
    int status
)

The status argument can take the following values:

  • NSIRenderCompleted indicates that rendering has completed normally.
  • NSIRenderAborted indicates that rendering was interrupted before completion.
  • NSIRenderSynchronized indicates that an interactive render has produced an image which reflects all changes to the scene.
  • NSIRenderRestarted indicates that an interactive render has received new changes to the scene and no longer has an up to date image.
NameTypeDescription/Values
stoppedcallbackdatapointerA pointer that will be passed back to the stoppedcallback function.

The C++ API

The nsi.hpp file provides C++ wrappers which are less tedious to use than the low level C interface. All the functionality is inline so no additional libraries are needed and there are no abi issues to consider.

Creating a Context

The core of these wrappers is the NSI::Context class. Its default construction will require linking with the renderer.

#include "nsi.hpp"

NSI::Context nsi;

The nsi_dynamic.hpp file provides an alternate api source which will load the renderer at runtime and thus requires no direct linking.

#include "nsi.hpp"
#include "nsi_dynamic.hpp"

NSI::DynamicAPI nsi_api;
NSI::Context nsi(nsi_api);

In both cases, a new nsi context can then be created with the Begin() method.

nsi.Begin();

This will be bound to the NSI::Context object and released when the object is deleted. It is also possible to bind the object to a handle from the C API, in which case it will not be released unless the End() method is explicitly called.

Argument Passing

The NSI::Context class has methods for all the other ɴsɪ calls. The optional arguments of those can be set by several accessory classes and given in many ways. The most basic is a single argument.

nsi.SetAttribute("handle", NSI::FloatArg("fov", 45.0f));

It is also possible to provide static lists:

nsi.SetAttribute(
    "handle",(
        NSI::FloatArg("fov", 45.0f),
        NSI::DoubleArg("depthoffield.fstop", 4.0)
    )
);

And finally a class supports dynamically building a list.

NSI::ArgumentList args;
args.Add(new NSI::FloatArg("fov", 45.0f));
args.Add(new NSI::DoubleArg("depthoffield.fstop", 4.0));
nsi.SetAttribute("handle", args);

The NSI::ArgumentList object will delete all the objects added to it when it is deleted.

Argument Classes

To be continued …

The Rust API

The nsi crate provides Rust wrappers for the ɴsɪ API. These are based on the low-level wrapper crate nsi-sys that contains autogenerated bindings on top of nsi.h.

Creating a Context

The core of these wrappers is the Context struct. Its construction triggers dynamic linking with the renderer.

#![allow(unused)]
fn main() {
let ctx = nsi::Context::new(None)?
}

The Lua API

The scripted interface is slightly different than its counterpart since it has been adapted to take advantage of the niceties of Lua. The main differences with the C   API are:

  • No need to pass a ɴsɪ context to function calls since it’s already embodied in the ɴsɪ Lua table (which is used as a class).
  • The type argument can be omitted if the argument is an integer, real or string (as with the Kd and filename in the example below).
  • ɴsɪ arguments can either be passed as a variable number of arguments or as a single argument representing an array of arguments (as in the "ggx" shader below)
  • There is no need to call NSIBegin() and NSIEnd() equivalents since the Lua script is run in a valid context.

Below is an example shader creation logic in Lua.

nsi.Create( "lambert", "shader" );
nsi.SetAttribute(
    "lambert", {
       { name = "filename", data = "lambert_material.oso" },
       { name = "Kd", data = 0.55 },
       { name = "albedo", data = { 1, 0.5, 0.3 }, type = nsi.TypeColor }
    }
);

nsi.Create( "ggx", "shader" );
nsi.SetAttribute(
    "ggx", {
        {name = "filename", data = "ggx_material.oso" },
        {name = "anisotropy_direction", data = {0.13, 0 ,1}, type = nsi.TypeVector }
    }
);

API Calls

All (in a scripting context) useful ɴsɪ functions are provided and are listed below. There is also a nsi.utilities class which, for now, only contains a method to print errors.

Lua FunctionC equivalent
nsi.SetAttribute()NSISetAttribute()
nsi.SetAttributeAtTime()NSISetAttributeAtTime()
nsi.Create()NSICreate()
nsi.Delete()NSIDelete()
nsi.DeleteAttribute()NSIDeleteAttribute()
nsi.Connect()NSIConnect()
nsi.Disconnect()NSIDisconnect()
Evaluate()NSIEvaluate()

ɴsɪ functions

Optional Function Arguments Format

Each single argument is passed as a Lua table containing the following key values:

  • name – the name of the argument.

  • data – the argument data. Either a value (integer, float or string) or an array.

  • type – the type of the argument. Possible values are:

    Lua TypeC equivalent
    nsi.TypeFloatNSITypeFloat
    nsi.TypeIntegerNSITypeInteger
    nsi.TypeStringNSITypeString
    nsi.TypeNormalNSITypeNormal
    nsi.TypeVectorNSITypeVector
    nsi.TypePointNSITypePoint
    nsi.TypeMatrixNSITypeMatrix

    Lua ɴsɪ argument types

  • arraylength – length of the array for each element.

Here are some example of well formed arguments:

--[[ strings, floats and integers do not need a 'type' specifier ]] --
p1 = {
    name = "shaderfilename",
    data = "emitter"
};
p2 = {
    name = "power",
    data = 10.13
};
p3 = {
    name = "toggle",
    data = 1
};

--[[ All other types, including colors and points, need a
     type specified for disambiguation. ]]--
p4 = {
    name = "Cs",
    data = { 1, 0.9, 0.7 },
    type=nsi.TypeColor
};

--[[ An array of 2 colors ]] --
p5 = {
    name = "vertex_color",
    arraylength = 2,
    data= { 1, 1, 1, 0, 0, 0 },
    type= nsi.TypeColor
};

--[[ Create a simple mesh and connect it root ]] --
nsi.Create( "floor", "mesh" )
nsi.SetAttribute(
    "floor", {
        name = "nvertices",
        data = 4
    }, {
        name = "P",
        type = nsi.TypePoint,
        data = { -2, -1, -1, 2, -1, -1, 2, 0, -3, -2, 0, -3 }
    }
)
nsi.Connect( "floor", "", ".root", "objects" )

Evaluating a Lua Script

Script evaluation is done through C, an ɴsɪ stream or even another Lua script. Here is an example using an ɴsɪ stream:

Evaluate
    "filename" "string" 1 ["test.nsi.lua"]
    "type" "string" 1 ["lua"]

It is also possible to evaluate a Lua script inline using the script argument. For example:

Evaluate
    "script" "string" 1 ["nsi.Create(\"light\", \"shader\");"]
    "type" "string" 1 ["lua"]

Both filename and script can be specified to NSIEvaluate() in one go, in which case the inline script will be evaluated before the file and both scripts will share the same ɴsɪ and Lua contexts.

Any error during script parsing or evaluation will be sent to ɴsɪ’s error handler.

Some utilities, such as error reporting, are available through the nsi.utilities class.

[!NOTE] All Lua scripts are run in a sandbox in which all Lua system libraries are disabled.

Passing Arguments to a Lua Script

All arguments passed to NSIEvaluate() will appear in the nsi.scriptarguments table. For example, the following call:

Evaluate
    "filename" "string" 1 ["test.lua"]
    "type" "string" 1 ["lua"]
    "userdata" "color[2]" 1 [1 0 1 2 3 4]

Will register a userdata entry in the nsi.scriptarguments table. So executing the following line in the test.lua script that the above snippete references:

print( nsi.scriptarguments.userdata.data[5] );

Will print:

3.0

Reporting Errors from a Lua Script

Use nsi.utilities.ReportError() to send error messages to the error handler defined in the current nsi context. For example:

nsi.utilities.ReportError( nsi.ErrWarning, "Watch out!" );

The and are shown in .

Lua Error CodesC equivalent
nsi.ErrMessageNSIErrMessage
nsi.ErrWarningNSIErrMessage
nsi.ErrInfoNSIErrInfo
nsi.ErrErrorNSIErrError

Lua ɴsɪ error codes

The Python API

The nsi.py file provides a python wrapper to the C interface. It is compatible with both Python 2.7 and Python 3.

An example of how to us it is provided in python/examples/live_edit/live_edit.py.

The Interface Stream

It is important for a scene description API to be streamable. This allows saving scene description into files, communicating scene state between processes and provide extra flexibility when sending commands to the renderer ​1.

Instead of re-inventing the wheel, the authors have decided to use exactly the same format as is used by the RenderMan Interface Bytestream (RIB). This has several advantages:

  • Well defined ASCII and binary formats.
  • The ASCII format is human readable and easy to understand.
  • Easy to integrate into existing renderers (writers and readers already available).

Note that since Lua is part of the API, one can use Lua files for API streaming ​2.


Footnotes


  1. The streamable nature of the RenderMan API, through RIB, is an undeniable advantage. RenderMan is a registered trademark of Pixar.

  2. Preliminary tests show that the Lua parser is as fast as an optimized ASCII RIB parser.

Dynamic Library Procedurals

NSIEvaluate and procedural nodes can execute code loaded from a dynamically loaded library that defines a procedural. Executing the procedural is expected to result in a series of ɴsɪ API calls that contribute to the description of the scene. For example, a procedural could read a part of the scene stored in a different file format and translate it directly into ɴsɪ calls.

This section describes how to use the definitions from the nsi_procedural.h header to write such a library in C or C++. However, the process of compiling and linking it is specific to each operating system and out of the scope of this manual.

Entry Point

The renderer expects a dynamic library procedural to contain a NSIProceduralLoad() symbol, which is an entry point for the library’s main function:

struct NSIProcedural_t* NSIProceduralLoad(
    NSIContext_t ctx,
    NSIReport_t report,
    const char* nsi_library_path,
    const char* renderer_version);

It will be called only once per render and has the responsibility of initializing the library and returning a description of the functions implemented by the procedural. However, it is not meant to generate ɴsɪ calls.

It returns a pointer to a descriptor struct of type NSIProcedural_t (see below).

NSIProceduralLoad() receives the following parameters:

NameTypeDescription
ctxNSIContext_tThe ɴsɪ context into which the procedural is being loaded.
reportNSIReport_tA function that can be used to display informational, warning or error messages through the renderer.
nsi_library_pathconst char*The path to the ɴsɪ implementation that is loading the procedural. This allows the procedural to explicitly make its ɴsɪ API calls through the same implementation (for example, by using NSI::DynamicAPI defined in nsi_dynamic.hpp). It’s usually not required if only one implementation of ɴsɪ is installed on the system.
renderer_versionconst char*A character string describing the current version of the renderer.

Procedural Description

typedef void (*NSIProceduralUnload_t)(
    NSIContext_t ctx,
    NSIReport_t report,
    struct NSIProcedural_t* proc);

typedef void (*NSIProceduralExecute_t)(
    NSIContext_t ctx,
    NSIReport_t report,
    struct NSIProcedural_t* proc,
    int nparams,
    const struct NSIParam_t* params);

struct NSIProcedural_t
{
    unsigned nsi_version;
    NSIProceduralUnload_t unload;
    NSIProceduralExecute_t execute;
};

The structure returned by NSIProceduralLoad() contains information needed by the renderer to use the procedural.

[!NOTE] The allocation of this structure is managed entirely from within the procedural and it will never be copied or modified by the renderer.

[!TIP] This means that it is possible for a procedural to extend the structure (by over-allocating memory or subclassing, for example) in order to store any extra information that it might need later.

The nsi_version member must be set to NSI_VERSION (defined in nsi.h), so the renderer is able to determine which version of ɴsɪ was used when compiling the procedural.

The function pointer types used in the definition are:

  • NSIProceduralUnload_t is a function that cleans-up after the last execution of the procedural. This is the dual of NSIProceduralLoad(). In addition to arguments ctx and report, also received by NSIProceduralLoad(), it receives the description of the procedural returned by NSIProceduralLoad().
  • NSIProceduralExecute_t is a function that contributes to the description of the scene by generating ɴsɪ API calls. Since NSIProceduralExecute_t might be called multiple times in the same render, it’s important that it uses the context ctx it receives as a parameter to make its ɴsɪ calls, and not the context previously received by NSIProceduralLoad(). It also receives any extra parameters sent to NSIEvaluate, or any extra attributes set on a procedural node. They are stored in the params array (of length nparams). NSIParam_t is described in passing optional arguments.

Error Reporting

All functions of the procedural called by ɴsɪ receive a parameter of type NSIReport_t. This is a pointer to a function which should be used by the procedural to report errors or display any informational message.

typedef void (*NSIReport_t)(
    NSIContext_t ctx, int level, const char* message);

It receives the current context, the error level (as described in error reporting) and the message to be displayed. This information will be forwarded to any error handler attached to the current context, along with other regular renderer messages. Using this, instead of a custom error reporting mechanism, will benefit the user by ensuring that all messages are displayed in a consistent manner.

Preprocessor Macros

Some convenient C preprocessor macros are also defined in nsi_procedural.h:

NSI_PROCEDURAL_UNLOAD(name)

and

NSI_PROCEDURAL_EXECUTE(name)

declare functions of the specified name that match NSIProceduralUnload_t and NSIProceduralExecute_t, respectively.

NSI_PROCEDURAL_LOAD

declares a NSIProceduralLoad function.

NSI_PROCEDURAL_INIT(proc, unload_fct, execute_fct)

initializes a NSIProcedural_t (passed as proc) using the addresses of the procedural’s main functions. It also initializes proc.nsi_version.

So, a skeletal dynamic library procedural (that does nothing) could be implemented as follows.

Please note, however, that the proc static variable in this example contains only constant values, which allows it to be allocated as a static variable. In a more complex implementation, it could have been over-allocated (or subclassed, in C++) to hold additional, variable data1. In that case, it would have been better to allocate the descriptor dynamically — and release it in NSI_PROCEDURAL_UNLOAD — so the procedural could be loaded independently from multiple parallel renders, each using its own instance of the NSIProcedural_t descriptor.

#include "nsi_procedural.h"

NSI_PROCEDURAL_UNLOAD(min_unload)
{
}

NSI_PROCEDURAL_EXECUTE(min_execute)
{
}

NSI_PROCEDURAL_LOAD
{
    static struct NSIProcedural_t proc;
    NSI_PROCEDURAL_INIT(proc, min_unload, min_execute);
    return &proc;
}


  1. A good example of this is available in the 3Delight installation, in file gear.cpp.

Nodes

The following sections describe available nodes in technical terms. Refer to the rendering guidelines for usage details.

NodeFunction
rootScene’s root
globalGlobal settings node
setTo express relationships of groups of nodes
shaderᴏsʟ shader or layer in a shader group
attributesContainer for generic attributes (e.g. visibility)
transformTransformation to place objects in the scene
meshPolygonal mesh or subdivision surface
planeInfinite plane
facesetAssign attributes to part of a mesh
curvesLinear, B-spline and Catmull-Rom curves
particlesCollection of particles
proceduralGeometry to be loaded in delayed fashion
environmentGeometry type to define environment lighting
vdbparticlesParticles defined by OpenVDB data
volumeVolumetric object defined by OpenVDB data
outputdriverLocation where to output rendered pixels
outputlayerDescribes one render layer to be connected to an outputdriver node
screenDescribes how the view from a camera will be rasterized into an outputlayer node
*cameraSet of nodes to create viewing cameras

Common Attributes

NameTypeDefault
nicenamestring

This is an optional identifier which may be used by the renderer instead of the node handle for various identification purposes.

root

The root node is much like a transform node with the particularity that it is the end connection for all renderable scene elements (see basic scene anatomy). A node can exist in an ɴsɪ context without being connected to the root note but in that case it won’t affect the render in any way. The root node has the reserved handle name .root and doesn’t need to be created using NSICreate. The root node has two defined attributes: objects and geometryattributes. Both are explained in the transform node.

global

This node contains various global settings for a particular ɴsɪ context. Note that these attributes are for the most case implementation specific. This node has the reserved handle name .global and doesn’t need to be created using NSICreate. The following attributes are recognized by 3Delight:

NameTypeDefault
numberofthreadsint0

Specifies the total number of threads to use for a particular render:

  • A value of zero lets the render engine choose an optimal thread value. This is the default behaviour.
  • Any positive value directly sets the total number of render threads.
  • A negative value will start as many threads as optimal plus the specified value. This allows for an easy way to decrease the total number of render threads.
NameTypeDefault
texturememoryint

Specifies the approximate maximum memory size, in megabytes, the renderer will allocate to accelerate texture access.

NameTypeDefault
networkcache.sizeint0

Specifies the maximum network cache size, in gigabytes, the renderer will use to cache textures on a local drive to accelerate data access.

NameTypeDefault
networkcache.directorystring

Specifies the directory in which textures will be cached. A good default value is /var/tmp/3DelightCache on Linux systems.

NameTypeDefault
networkcache.mipmapint1

Enables caching of texture mipmaps separately. This makes more efficient use of available cache space.

NameTypeDefault
networkcache.writestring0

Enables caching for image write operations. This alleviates pressure on networks by first rendering images to a local temporary location and copying them to their final destination at the end of the render. This replaces many small network writes by more efficient larger operations.

NameTypeDefault
license.serverstring

Specifies the name or address of the license server to be used.

NameTypeDefault
license.waitint1

When no license is available for rendering, the behaviour depends on this attribute. Set to 1, the renderer waits until a license becomes available. Set to 0, it stops immediately — useful when managing a renderfarm so other work can be scheduled instead.

NameTypeDefault
license.holdint0

By default, the renderer will get new licenses for every render and release them once it’s done. This can be undesirable if several frames are rendered in sequence from the same process. If this option is set to 1, the licenses obtained for the first frame are held until the last frame is finished.

NameTypeDefault
renderatlowpriorityint0

If set to 1, start the render with a lower process priority. This can be useful if there are other applications that must run during rendering.

NameTypeDefault
bucketorderstringhorizontal

Specifies in what order the buckets are rendered. The available values are:

  • horizontal — row by row, left to right and top to bottom.
  • vertical — column by column, top to bottom and left to right.
  • zigzag — row by row, left to right on even rows and right to left on odd rows.
  • spiral — in a clockwise spiral from the centre of the image.
  • circle — in concentric circles from the centre of the image.
NameTypeDefault
framedouble0

Provides a frame number to be used as a seed for the sampling pattern. See the screen node.

NameTypeDefault
hidemessagesint

This specifies error and warning messages which will not be displayed. The attribute values are the message numbers to ignore.

NameTypeDefault
maximumraydepth.diffuseint1

Specifies the maximum bounce depth a diffuse ray can reach. A depth of 1 specifies one additional bounce compared to purely local illumination.

NameTypeDefault
maximumraydepth.hairint4

Specifies the maximum bounce depth a hair ray can reach. Note that hair are akin to volumetric primitives and might need elevated ray depth to properly capture the illumination.

NameTypeDefault
maximumraydepth.reflectionint1

Specifies the maximum bounce depth a reflection ray can reach. Setting the reflection depth to 0 will only compute local illumination meaning that only emissive surfaces will appear in the reflections.

NameTypeDefault
maximumraydepth.refractionint4

Specifies the maximum bounce depth a refraction ray can reach. A value of 4 allows light to shine through a properly modeled object such as a glass.

NameTypeDefault
maximumraydepth.volumeint0

Specifies the maximum bounce depth a volume ray can reach.

NameTypeDefault
maximumraylength.diffusedouble-1

Limits the distance a ray emitted from a diffuse material can travel. A relatively low value can improve performance with minimal impact on the look, since it restrains the extent of global illumination. A negative value disables the limit.

NameTypeDefault
maximumraylength.hairdouble-1

Limits the distance a ray emitted from a hair closure can travel. Setting it to a negative value disables the limitation.

NameTypeDefault
maximumraylength.reflectiondouble-1

Limits the distance a ray emitted from a reflective material can travel. Setting it to a negative value disables the limitation.

NameTypeDefault
maximumraylength.refractiondouble-1

Limits the distance a ray emitted from a refractive material can travel. Setting it to a negative value disables the limitation.

NameTypeDefault
maximumraylength.speculardouble-1

Limits the distance a ray emitted from a specular (glossy) material can travel. Setting it to a negative value disables the limitation.

NameTypeDefault
maximumraylength.volumedouble-1

Limits the distance a ray emitted from a volume can travel. Setting it to a negative value disables the limitation.

NameTypeDefault
quality.denoiseint1

Enables denoising of output. Currently only supported for interactive renders.

NameTypeDefault
quality.iprglobalupdateint1

Enables a different method of updating the image for interactive renders.

NameTypeDefault
quality.iprinterpolateint1

Enables interpolation of low resolution interactive output, when denoised.

NameTypeDefault
quality.iprspeedmultiplierdouble1

Adjusts targeted render speed when processing multiple scene edits. A higher value will produce faster but lower quality results.

NameTypeDefault
quality.shadingsamplesint1

Controls the quality of ʙsᴅꜰ sampling. Larger values give less visible noise.

NameTypeDefault
quality.volumesamplesint1

Controls the quality of volume sampling. Larger values give less visible noise.

NameTypeDefault
referencetimedouble

Specifies a reference time for the frame, where deformation data is most valid. This is the default when the same attribute is not set on a geometry node. It is also the default for the velocityreferencetime attribute of the vdbparticles node and the volume node. If not set, the center of the camera shutter is used.

NameTypeDefault
quality.samplevolumeemissionint1

Enables or disables the higher quality sampling of emission of ᴠᴅʙ volumes. The emission is visible either way, this only affects quality and render time.

NameTypeDefault
show.displacementint1

When set to 1, enables displacement shading. Otherwise, it must be set to 0, which forces the renderer to ignore any displacement shader in the scene.

NameTypeDefault
show.atmosphereint1

When set to 1, enables atmosphere shader(s). Otherwise, it must be set to 0, which forces the renderer to ignore any atmosphere shader in the scene.

NameTypeDefault
show.multiplescatteringdouble1.0

This is a multiplier on the multiple scattering of ᴠᴅʙ nodes. This parameter is useful to obtain faster draft renders by lowering the value below 1. The range is 0 to 1.

NameTypeDefault
show.osl.subsurfaceint1

When set to 1, enables the subsurface() ᴏsʟ closure. Otherwise, it must be set to 0, which will ignore this closure in ᴏsʟ shaders.

NameTypeDefault
statistics.progressint0

When set to 1, prints rendering progress as a percentage of completed pixels.

NameTypeDefault
statistics.filenamestringnull

Full path of the file where rendering statistics will be written. An empty string will write statistics to standard output. The name null will not output statistics.

NameTypeDefault
texture.missingcolorfloat[4]

If specified, this is used as a default value for the missingcolor parameter of the ᴏsʟ texture() function. The fourth value is used as missingalpha.

NameTypeDefault
texture.missingcolorerrorsint0

If nonzero, errors are reported even when the missingcolor of the ᴏsʟ texture() function is used. This goes against the documented behavior.

NameTypeDefault
exclusiveshading<connection>

When geometry nodes are connected here, all others in the scene will be rendered as black to the camera. This is meant to be used to speed up rendering when adjusting parameters of specific objects during an interactive render. Connected shader nodes will behave in a similar way: objects not using them will be rendered as black. If the connected shader nodes are not the root of their shading network, evaluation of the network ends at them. “Not the root” means they are not connected to an attributes node, and their output is used as another shader node’s input. This allows fine-tune parts of a shading network in isolation.

NameTypeDefault
verboseint0

When set to 1, enables additional informative messages before, during and after rendering.

NameTypeDefault
messages.timestampint0

When set to 1, messages output by the renderer will include the local time.

set

This node can be used to express relationships between objects. An example is to connect many lights to such a node to create a light set and then to connect this node to outputlayer.lightset (outputlayer and light layers). It has the following attributes:

NameTypeDefault
members<connection>

This connection accepts all nodes that are members of the set.

plane

This node represents an infinite plane, centered at the origin and pointing towards Z+. It has no required attributes. The UV coordinates are defined as the X and Y coordinates of the plane.

mesh

This node represents a polygon mesh. It has the following required attributes:

NameTypeDefault
Ppoint

The positions of the object’s vertices. Typically, this attribute will be addressed indirectly through a P.indices attribute.

NameTypeDefault
nverticesint

The number of vertices for each face of the mesh. The number of values for this attribute specifies total face number (unless nholes is defined).

It also has optional attributes:

NameTypeDefault
nholesint

The number of holes in the polygons. When this attribute is defined, the total number of faces in the mesh is defined by the number of values for nholes rather than for nvertices. For each face, there should be (nholes+1) values in nvertices: the respective first value specifies the number of vertices on the outside perimeter of the face, while additional values describe the number of vertices on perimeters of holes in the face.

NameTypeDefault
clockwisewindingint0

A value of 1 specifies that polygons with a clockwise winding order are front facing. The default is 0, making counterclockwise polygons front facing.

NameTypeDefault
subdivision.schemestring

A value of "catmull-clark" will cause the mesh to render as a Catmull-Clark subdivision surface.

NameTypeDefault
subdivision.cornerverticesint

This attribute is a list of vertices which are sharp corners. The values are indices into the P attribute, like P.indices.

NameTypeDefault
subdivision.cornersharpnessfloat

This attribute is the sharpness of each specified sharp corner. It must have a value for each value given in subdivision.cornervertices.

NameTypeDefault
subdivision.creaseverticesint

This attribute is a list of crease edges. Each edge is specified as a pair of indices into the P attribute, like P.indices.

NameTypeDefault
subdivision.creasesharpnessfloat

This attribute is the sharpness of each specified crease. It must have a value for each pair of values given in subdivision.creasevertices.

NameTypeDefault
subdivision.smoothcreasecornersint1

This attribute controls whether or not the surface uses enhanced subdivision rules on vertices where more than two creased edges meet. With a value of 0, the vertex becomes a sharp corner. With a value of 1, the vertex is subdivided using an extended crease vertex subdivision rule which yields a smooth crease.

NameTypeDefault
referencetimedouble

Specifies a reference time where deformation data is most valid. This is mainly relevant for velocity blur, in which case it should be set to the time at which position data was originally available. If not set, see the same attribute on the global node.

NameTypeDefault
quadraticmotionint0

A value of 1 will enable curved deformation blur if three equally spaced time samples are provided for the P attribute. Linear deformation is used otherwise.

NameTypeDefault
outlinecreasethresholdfloat10

Controls how sharp a crease must be to be considered for the creation of outlines.

nurbs

This node represents a NURBS surface patch — a tensor-product spline defined by a grid of control points, two knot vectors, and an order in each parametric direction. It has the following required attributes:

NameTypeDefault
nuint

Control-point count along u. Total control-point count is nu * nv. Should be at least uorder; if smaller, the surface is rendered with order equal to nu.

NameTypeDefault
nvint

Control-point count along v. Same constraint as nu relative to vorder.

NameTypeDefault
uorderint

Order along u: degree + 1, so 2 is linear, 3 quadratic, 4 cubic. Must be at least 2. May differ from vorder.

NameTypeDefault
vorderint

Order along v. See uorder.

NameTypeDefault
uknotfloat

Knot vector along u. Length must equal nu + uorder. Values must be non-decreasing.

NameTypeDefault
vknotfloat

Knot vector along v. Length must equal nv + vorder. Values must be non-decreasing.

The surface’s active parameter range can be restricted with the optional umin/umax/vmin/vmax attributes. Unlike other geometric primitives, NURBS surfaces do not assume [0, 1] parameter ranges — by default, the active range is the full extent of the corresponding knot vector.

NameTypeDefault
uminfloat

Lower bound of the active range along u. Must be less than umax and at least the (uorder − 1)-th value of uknot.

NameTypeDefault
umaxfloat

Upper bound of the active range along u. Must be greater than umin and at most the nu-th value of uknot.

NameTypeDefault
vminfloat

Lower bound of the active range along v. Must be less than vmax and at least the (vorder − 1)-th value of vknot.

NameTypeDefault
vmaxfloat

Upper bound of the active range along v. Must be greater than vmin and at most the nv-th value of vknot.

One of P or Pw must be supplied to provide the control points. P defines a polynomial surface; Pw defines a rational one.

NameTypeDefault
Ppoint

The nu * nv control points (xyz), stored row-major: P[i*nu + j] is the point at row i, column j.

NameTypeDefault
Pwfloat[4]

Rational alternative to P: each control point is four floats (x, y, z, w), enabling rational NURBS. Pass as a single flat array of 4 * nu * nv floats — do not declare it with array_len(4).

Trim Curves

Trim curves carve a region out of the surface’s parameter domain. They are NURBS curves in homogeneous (u, v, w) parameter space — the actual (u, v) of a control point is (u/w, v/w). Curves are organised into loops: within a loop they connect head-to-tail. Each loop must be explicitly closed — the last point of the last curve must coincide with the first point of the first curve.

The trimcurves.* attributes are all-or-nothing: supply the full set or omit it entirely.

NameTypeDefault
trimcurves.nloopsint

The number of trim loops.

NameTypeDefault
trimcurves.ncurvesint

The number of curves in each loop. One value per loop.

NameTypeDefault
trimcurves.nint

The control-point count of each curve. One value per curve.

NameTypeDefault
trimcurves.orderint

The order of each curve. One value per curve.

NameTypeDefault
trimcurves.knotfloat

The concatenated knot vectors for all curves. The total length is the sum over curves of n[i] + order[i].

NameTypeDefault
trimcurves.minfloat

The parametric start of each curve. One value per curve.

NameTypeDefault
trimcurves.maxfloat

The parametric end of each curve. One value per curve.

NameTypeDefault
trimcurves.ufloat

Concatenated u coordinates of all trim-curve control points. The total length is the sum over curves of n[i].

NameTypeDefault
trimcurves.vfloat

Concatenated v coordinates of all trim-curve control points. The total length is the sum over curves of n[i].

NameTypeDefault
trimcurves.wfloat

Concatenated weights of all trim-curve control points. The total length is the sum over curves of n[i]. Use 1.0 for non-rational curves.

NameTypeDefault
trimcurves.senseint

The sense of each loop. One value per loop. A value of 0 keeps the surface inside the loop; a value of 1 keeps the surface outside the loop (i.e. the loop describes a hole).

faceset

This node is used to provide a way to attach attributes to some faces of another geometric primitive, such as the mesh node. It has the following attributes:

NameTypeDefault
facesint

This attribute is a list of indices of faces. It identifies which faces of the original geometry will be part of this face set.

curves

This node represents a group of curves. It has the following required attributes:

NameTypeDefault
nverticesint

The number of vertices for each curve. This must be at least 4 for cubic curves and 2 for linear curves. There can be either a single value or one value per curve.

NameTypeDefault
Ppoint

The positions of the curve vertices. The number of values provided, divided by nvertices, gives the number of curves which will be rendered.

NameTypeDefault
widthfloat

The width of the curves.

NameTypeDefault
basisstringcatmull-rom

The basis functions used for curve interpolation. Possible choices are:

  • b-spline — B-spline interpolation.
  • catmull-rom — Catmull-Rom interpolation.
  • linear — Linear interpolation.
  • hobby — Hobby interpolation.
NameTypeDefault
extrapolateint0

By default, cubic curves will not be drawn to their end vertices as the basis functions require an extra vertex to define the curve. If this attribute is set to 1, an extra vertex is automatically extrapolated so the curves reach their end vertices, as with linear interpolation.

Attributes may also have a single value, one value per curve, one value per vertex or one value per vertex of a single curve, reused for all curves. Attributes which fall in that last category must always specify NSIParamPerVertex. Note that a single curve is considered a face as far as use of NSIParamPerFace is concerned.

particles

This geometry node represents a collection of tiny particles. Particles are represented by either a disk or a sphere. This primitive is not suitable to render large particles as these should be represented by other means (e.g. instancing).

NameTypeDefault
Ppoint

A mandatory attribute that specifies the center of each particle.

NameTypeDefault
widthfloat

A mandatory attribute that specifies the width of each particle. It can be specified for the entire particles node (only one value provided) or per-particle.

NameTypeDefault
Nnormal

The presence of a normal indicates that each particle is to be rendered as an oriented disk. The orientation of each disk is defined by the provided normal which can be constant or a per-particle attribute. Each particle is assumed to be a sphere if a normal is not provided.

NameTypeDefault
reverseorientationint0

Setting this to 1 will reverse the orientation of spherical particles. Specifically, their u parametric direction is reversed, which also reverses their normal so it points inwards. It has no effect on particles for which N is provided.

NameTypeDefault
idint

This attribute, of the same size as P, assigns a unique identifier to each particle which must be constant throughout the entire shutter range. Its presence is necessary in the case where particles are motion blurred and some of them could appear or disappear during the motion interval. Having such identifiers allows the renderer to properly render such transient particles. This implies that the number of ids might vary for each time step of a motion-blurred particle cloud so the use of NSISetAttributeAtTime is mandatory by definition.

NameTypeDefault
quadraticmotionint0

A value of 1 will enable curved deformation blur if three equally spaced time samples are provided for the P attribute. Linear deformation is used otherwise.

procedural

This node acts as a proxy for geometry that could be defined at a later time than the node’s definition, using a procedural supported by NSIEvaluate. Since the procedural is evaluated in complete isolation from the rest of the scene, it can be done either lazily (depending on its boundingbox attribute) or in parallel with other procedural nodes.

The procedural node supports, as its attributes, all the parameters of the NSIEvaluate API call, meaning that procedural types accepted by that API call (NSI archives, dynamic libraries, LUA scripts) are also supported by this node. Those attributes are used to call a procedural that is expected to define a sub-scene, which has to be independent from the other nodes in the scene. The procedural node will act as the sub-scene’s local root and, as such, also supports all the attributes of a regular transform node. In order to connect the nodes it creates to the sub-scene’s root, the procedural simply has to connect them to the regular root node .root.

In the context of an interactive render, the procedural will be executed again after the node’s attributes have been edited. All nodes previously connected by the procedural to the sub-scene’s root will be deleted automatically before the procedural’s re-execution.

Additionally, this node has the following optional attribute:

NameTypeDefault
boundingboxpoint[2]

Specifies a bounding box for the geometry where boundingbox[0] and boundingbox[1] correspond, respectively, to the “minimum” and the “maximum” corners of the box.

environment

This geometry node defines a sphere of infinite radius. Its only purpose is to render environment lights, solar lights and directional lights; lights which cannot be efficiently modeled using area lights. In practical terms, this node is no different than a geometry node with the exception of shader execution semantics: there is no surface position P, only a direction I (refer to lighting guidelines for more practical details). The following node attribute is recognized:

NameTypeDefault
angledouble360

Specifies the cone angle representing the region of the sphere to be sampled. The angle is measured around the Z+ axis. If the angle is set to 0, the environment describes a directional light. Refer to lighting guidelines for more about how to specify light sources.

shader

This node represents an ᴏsʟ shader, also called layer when part of a shader group. It has the following attributes:

NameTypeDefault
shaderfilenamestring

This is the name of the file which contains the shader’s compiled code.

NameTypeDefault
shaderobjectstring

This contains the complete compiled shader code. It allows providing custom shaders without going through files.

NameTypeDefault
materialxnodedefstring

The name of the MaterialX node definition to use.

NameTypeDefault
materialxversionstring

The MaterialX library version to use to find the node. If unspecified, the most up to date version is used.

Either shaderfilename, shaderobject or materialxnodedef must be provided. All other attributes on this node are considered parameters of the shader. They may either be given values or connected to attributes of other shader nodes to build shader networks. ᴏsʟ shader networks must form acyclic graphs or they will be rejected. Refer to creating ᴏsʟ networks for instructions on ᴏsʟ network creation and usage.

attributes

This node is a generic container for attributes. Its exact purpose depends on where it is connected in the scene. There are currently two uses.

Geometry Attributes

This node can provide various geometry related rendering attributes that are not intrinsic to a particular node (for example, one can’t set the topology of a polygonal mesh using this attributes node). For this use, instances of this node must be connected to the geometryattributes attribute of either geometric primitives or transform nodes (to build attributes hierarchies). Attribute values are gathered along the path starting from the geometric primitive, through all the transform nodes it is connected to, until the scene root is reached.

When an attribute is defined multiple times along this path, the definition with the highest priority is selected. In case of conflicting priorities, the definition that is closest to the geometric primitive (i.e. the furthest from the root) is selected. Connections (for shaders, essentially) can also be assigned priorities, which are used in the same way as for regular attributes. Multiple attributes nodes can be connected to the same geometry or transform nodes (e.g. one attributes node can set object visibility and another can set the surface shader) and will all be considered.

In this case, the node has the following attributes:

NameTypeDefault
surfaceshader<connection>

The shader node which will be used to shade the surface is connected to this attribute. A priority (useful for overriding a shader from higher in the scene graph) can be specified by setting the priority attribute of the connection itself.

NameTypeDefault
displacementshader<connection>

The shader node which will be used to displace the surface is connected to this attribute. A priority (useful for overriding a shader from higher in the scene graph) can be specified by setting the priority attribute of the connection itself.

NameTypeDefault
volumeshader<connection>

The shader node which will be used to shade the volume inside the primitive is connected to this attribute.

NameTypeDefault
ATTR.priorityint0

Sets the priority of attribute ATTR when gathering attributes in the scene hierarchy.

NameTypeDefault
visibility.cameraint1
visibility.diffuseint1
visibility.hairint1
visibility.reflectionint1
visibility.refractionint1
visibility.shadowint1
visibility.specularint1
visibility.volumeint1

These attributes set visibility for each ray type specified in ᴏsʟ. The same effect could be achieved using shader code (using the raytype() function) but it is much faster to filter intersections at trace time. A value of 1 makes the object visible to the corresponding ray type, while 0 makes it invisible.

NameTypeDefault
visibilityint1

This attribute sets the default visibility for all ray types. When visibility is set both per ray type and with this default visibility, the attribute with the highest priority is used. If their priority is the same, the more specific attribute (i.e. per ray type) is used.

NameTypeDefault
visibility.set.subsurface<connection>

If a set node is connected to this attribute, subsurface rays will only see objects with a connection to that same set node.

NameTypeDefault
matteint0

If this attribute is set to 1, the object becomes a matte for camera rays. Its transparency is used to control the matte opacity and all other shading components are ignored.

NameTypeDefault
regularemissionint1

If this is set to 1, closures not used with quantize() will use emission from the objects affected by the attribute. If set to 0, they will not.

NameTypeDefault
quantizedemissionint1

If this is set to 1, quantized closures will use emission from the objects affected by the attribute. If set to 0, they will not.

NameTypeDefault
bounds<connection>

When a geometry node (usually a mesh node) is connected to this attribute, it restricts the effect of the attributes node. The node then applies only inside the volume defined by the connected geometry. A transform node may be connected here instead, which is equivalent to connecting every geometry node reachable through that transform.

NameTypeDefault
displacementresolutionfloat1

A multiplier on the level of detail in displacement. Larger values provide more detail; smaller values reduce memory use. It is usually not necessary to set this attribute.

Shader Attributes

This node can be a container for attributes available to shaders. For this purpose, instances of this node must be connected to the shaderattributes attribute of geometric primitives, transform nodes or set nodes. Attribute values are gathered along the path starting from the geometric primitive, through all the transform nodes it is connected to, until the scene root is reached.

Priority is given to nodes attached closest to the geometric primitive, with the highest priority given to attributes set directly on the geometric primitive. Attributes set on this node may only have a single value.

transform

This node represents a geometric transformation. Transform nodes can be chained together to express transform concatenation, hierarchies and instances. Transform nodes also accept attributes to implement hierarchical attribute assignment and overrides. It has the following attributes:

NameTypeDefault
transformationmatrixdoublematrix

This is a 4x4 matrix which describes the node’s transformation. Matrices in ɴsɪ post-multiply column vectors so are of the form:

| w11  w12  w13  0 |
| w21  w22  w23  0 |
| w31  w32  w33  0 |
| Tx   Ty   Tz   1 |
NameTypeDefault
objects<connection>

This is where the transformed objects are connected to. This includes geometry nodes, other transform nodes and camera nodes.

NameTypeDefault
geometryattributes<connection>

This is where attributes nodes may be connected to affect any geometry transformed by this node. Refer to attributes and instancing for explanation on how this connection is used.

NameTypeDefault
shaderattributes<connection>

This is where attributes nodes may be connected to provide shader attributes for any geometry transformed by this node.

instances

This node is an efficient way to specify a large number of instances. It has the following attributes:

NameTypeDefault
sourcemodels<connection>

The instanced models should connect to this attribute. Connections must have an integer index attribute if there are several, so the models effectively form an ordered list.

NameTypeDefault
transformationmatricesdoublematrix

A transformation matrix for each instance.

NameTypeDefault
modelindicesint0

An optional model selector for each instance. The value used is matched to the index attribute of the model connection. A negative value will cause an instance to not be rendered.

NameTypeDefault
disabledinstancesint

An optional list of indices of instances which are not to be rendered.

outputdriver

An output driver defines how an image is transferred to an output destination. The destination could be a file (e.g. “exr” output driver), frame buffer or a memory address. It can be connected to the outputdrivers attribute of an output layer node. It has the following attributes:

NameTypeDefault
drivernamestring

This is the name of the driver to use. The API of the driver is implementation specific and is not covered by this documentation.

NameTypeDefault
imagefilenamestring

Full path to a file for a file-based output driver or some meaningful identifier depending on the output driver.

NameTypeDefault
embedstatisticsint1

A value of 1 specifies that statistics will be embedded into the image file.

Any extra attributes are also forwarded to the output driver which may interpret them however it wishes.

outputlayer

This node describes one specific layer of render output data. It can be connected to the outputlayers attribute of a screen node. It has the following attributes:

NameTypeDefault
variablenamestring

This is the name of a variable to output.

NameTypeDefault
variablesourcestringshader

Indicates where the variable to be output is read from. Possible values are:

  • shader — computed by a shader and output through an ᴏsʟ closure (such as outputvariable() or debug()) or the Ci global variable.
  • attribute — retrieved directly from an attribute with a matching name attached to a geometric primitive.
  • builtin — generated automatically by the renderer (e.g. "z", "alpha", "N.camera", "P.world").
NameTypeDefault
layernamestring

This will be name of the layer as written by the output driver. For example, if the output driver writes to an EXR file then this will be the name of the layer inside that file.

NameTypeDefault
scalarformatstringuint8

Specifies the format in which data will be encoded (quantized) prior to passing it to the output driver. Possible values are:

  • int8 — signed 8-bit integer
  • uint8 — unsigned 8-bit integer
  • int16 — signed 16-bit integer
  • uint16 — unsigned 16-bit integer
  • int32 — signed 32-bit integer
  • uint32 — unsigned 32-bit integer
  • half — IEEE 754 half-precision binary floating point (binary16)
  • float — IEEE 754 single-precision binary floating point (binary32)
NameTypeDefault
layertypestringcolor

Specifies the type of data that will be written to the layer. Possible values are:

  • scalar — A single quantity. Useful for opacity (“alpha”) or depth (“Z”) information.
  • color — A 3-component color.
  • vector — A 3D point or vector. This will help differentiate the data from a color in further processing.
  • quad — A sequence of 4 values, where the fourth value is not an alpha channel.

Each component of those types is stored according to the scalarformat attribute set on the same outputlayer node.

NameTypeDefault
colorprofilestring

The name of an OCIO color profile to apply to rendered image data prior to quantization.

NameTypeDefault
ditheringinteger0

If set to 1, dithering is applied to integer scalars. Otherwise, it must be set to 0.

NameTypeDefault
withalphainteger0

If set to 1, an alpha channel is included in the output layer. Otherwise, it must be set to 0.

NameTypeDefault
sortkeyinteger

This attribute is used as a sorting key when ordering multiple output layer nodes connected to the same output driver node. Layers with the lowest sortkey attribute appear first.

NameTypeDefault
lightset<connection>

This connection accepts either light sources or set nodes to which lights are connected. In this case only listed lights will affect the render of the output layer. If nothing is connected to this attribute then all lights are rendered.

If an environment node is connected here, a component string attribute can be specified on the connection with a value of either sun or background. If this is used, only the corresponding part of the environment will contribute to the output layer.

NameTypeDefault
lightsetnamestring

This can be provided as friendly name for the connected light set. Otherwise, a default name is built from the connected node.

NameTypeDefault
outputdrivers<connection>

This connection accepts output driver nodes to which the layer’s image will be sent.

NameTypeDefault
filterstringblackman-harris

The type of filter to use when reconstructing the final image from sub-pixel samples. Possible values are: "box", "triangle", "catmull-rom", "bessel", "gaussian", "sinc", "mitchell", "blackman-harris", "zmin" and "zmax".

NameTypeDefault
filterwidthdouble3.0

Diameter in pixels of the reconstruction filter. It is not applied when filter is "box" or "zmin".

NameTypeDefault
backgroundvaluefloat0

The value given to pixels where nothing is rendered.

NameTypeDefault
backgroundlayer<connection>

This connection accepts a single output layer node which is meant to be displayed as a background. Not all output drivers support this behavior, so it might be ignored.

NameTypeDefault
lightdepthstringauto

Allows filtering light contributions according to the number of bounces light has made from a light source to the objects in front of the camera. This is only meaningful when the layer’s variablesource is set to shader (otherwise, it’s ignored). Possible values are:

  • direct — Only light coming directly from light sources to visible objects (ie: with no bounce) is included.
  • indirect — Only light coming from light sources to visible objects through at least one bounce is shown.
  • both — All light is included.
  • auto — Selects the appropriate value for lightdepth according to the value of the variablename attribute. If it ends with either .direct or .indirect, the corresponding light depth will be used, and the suffix will be removed from the effective variable name. Otherwise, it will default to both.
NameTypeDefault
cryptomatte.enableint0

Setting this attribute to 1 enables Cryptomatte encoding of the layer’s data. cryptomatte.level should also be set properly.

NameTypeDefault
cryptomatte.levelint0

If this value is negative, the layer will contain a human-readable “Cryptomatte header” image. Otherwise, the value indicates the index of the first Cryptomatte level that will be output. Since Cryptomatte levels are output by pairs, a Cryptomatte file with 4 levels would contain output layers with cryptomatte.level set to -1, 0 and 2. This has no effect unless Cryptomatte encoding is enabled using cryptomatte.enable.

Any extra attributes are also forwarded to the output driver which may interpret them however it wishes.

screen

This node describes how the view from a camera node will be rasterized into an output layer node. It can be connected to the screens attribute of a camera node.

NameTypeDefault
outputlayers<connection>

This connection accepts output layer nodes which will receive a rendered image of the scene as seen by the camera.

NameTypeDefault
resolutioninteger[2]

Horizontal and vertical resolution of the rendered image, in pixels.

NameTypeDefault
oversamplinginteger

The total number of samples (i.e. camera rays) to be computed for each pixel in the image.

NameTypeDefault
crop2 × float[2]

The region of the image to be rendered. It’s defined by a list of exactly 2 pairs of floating-point number. Each pair represents a point in ɴᴅᴄ space:

  • Top-left corner of the crop region
  • Bottom-right corner of the crop region
NameTypeDefault
prioritywindow2 × int[2]

For progressive renders, this is the region of the image to be rendered first. It is two pairs of integers. Each represents pixel coordinates:

  • Top-left corner of the high priority region
  • Bottom-right corner of the high priority region
NameTypeDefault
screenwindow2 × double[2]

Specifies the screen space region to be rendered. Each pair represents a 2D point in screen space:

  • Bottom-left corner of the region
  • Top-right corner of the region

Note that the default screen window is set implicitly by the frame aspect ratio: screenwindow = [-f, -1], [f, 1] for f = xres/yres

NameTypeDefault
overscan2 × int[2]

Specifies how many extra pixels to render around the image. The four values represent the amount of overscan on the left, top, right and bottom of the image.

NameTypeDefault
pixelaspectratiofloat

Ratio of the physical width to the height of a single pixel. A value of 1.0 corresponds to square pixels.

NameTypeDefault
staticsamplingpatternint0

This controls whether or not the sampling pattern used to produce the image change for every frame. A nonzero value will cause the same pattern to be used for all frames. A value of zero will cause the pattern to change with the frame attribute of the global node.

NameTypeDefault
importancesamplefilterint0

This enables a rendering mode where the pixel filter is importance sampled. Quality will be reduced and the same filter will be used for all output layers of the screen. Filters with negative lobes (eg. sinc) are unsupported.

vdbparticles

This node represents particles defined by OpenVDB data. It has the following attributes:

NameTypeDefault
vdbfilenamestring

The path to an OpenVDB file with the particle data.

NameTypeDefault
pointsgridstring

The name of the OpenVDB grid to use for particle data. It must be of type PointDataGrid.

NameTypeDefault
velocityreferencetimedouble

The reference time at which the grid data is used directly, without being moved by the velocity attribute. Defaults to the global node’s referencetime attribute.

NameTypeDefault
velocityscaledouble1

A scaling factor applied to the velocity data.

NameTypeDefault
enablepscaleint1

Enables use of the pscale attribute in the grid to specify particle radius.

NameTypeDefault
widthdouble1

The width of particles, if there is no pscale attribute in the file to specify their radius or it is disabled by setting enablepscale to 0.

NameTypeDefault
widthscaledouble1

A scaling factor applied to particle width.

The P, v and pscale grid attributes are used to respectively define particle position, velocity and radius. Other grid attributes may be read by shaders.

volume

This node represents a volumetric object defined by OpenVDB data. It has the following attributes:

NameTypeDefault
vdbfilenamestring

The path to an OpenVDB file with the volumetric data.

NameTypeDefault
densitygridstring

The name of the OpenVDB grid to use as volume density for the volume shader.

NameTypeDefault
colorgridstring

The name of the OpenVDB grid to use as a scattering color multiplier for the volume shader.

NameTypeDefault
emissiongridstring

The name of the OpenVDB grid to use directly as emission for the volume shader.

NameTypeDefault
emissionintensitygridstring

The name of the OpenVDB grid to use as emission intensity for the volume shader.

NameTypeDefault
temperaturegridstring

The name of the OpenVDB grid to use as temperature for the volume shader.

NameTypeDefault
velocitygridstring

The name of the OpenVDB grid to use as motion vectors. This can also name the first of three scalar grids (ie. “velocityX”).

NameTypeDefault
velocityreferencetimedouble

The reference time at which the grid data is used directly, without being moved by the velocity grid. Defaults to the global node’s referencetime attribute.

NameTypeDefault
velocityscaledouble1

A scaling factor applied to the motion vectors.

Camera Nodes

All camera nodes share a set of common attributes. These are listed below.

NameTypeDefault
screens<connection>

This connection accepts screen nodes which will rasterize an image of the scene as seen by the camera. Refer to defining output drivers and layers for more information.

NameTypeDefault
shutterrangedouble

Time interval during which the camera shutter is at least partially open. It’s defined by a list of exactly two values:

  • Time at which the shutter starts opening.
  • Time at which the shutter finishes closing.
NameTypeDefault
shutteropeningdouble

A normalized time interval indicating the time at which the shutter is fully open (a) and the time at which the shutter starts to close (b). These two values define the top part of a trapezoid filter. The end goal of this feature it to simulate a mechanical shutter on which open and close movements are not instantaneous.

An example shutter opening configuration

NameTypeDefault
clippingrangedouble

Distance of the near and far clipping planes from the camera. It’s defined by a list of exactly two values:

  • Distance to the near clipping plane, in front of which scene objects are clipped.
  • Distance to the far clipping plane, behind which scene objects are clipped.

orthographiccamera

This node defines an orthographic camera with a view direction towards the Z- axis. This camera has no specific attributes.

perspectivecamera

This node defines a perspective camera. The canonical camera is viewing in the direction of the Z- axis. The node is usually connected into a transform node for camera placement. It has the following attributes:

NameTypeDefault
fovfloat

The field of view angle, in degrees.

NameTypeDefault
depthoffield.enableinteger0

Enables depth of field effect for this camera.

NameTypeDefault
depthoffield.fstopdouble

Relative aperture of the camera.

NameTypeDefault
depthoffield.focallengthdouble

Vertical focal length, in scene units, of the camera lens.

NameTypeDefault
depthoffield.focallengthratiodouble1

Ratio of vertical focal length to horizontal focal length. This is the squeeze ratio of an anamorphic lens.

NameTypeDefault
depthoffield.focaldistancedouble

Distance, in scene units, in front of the camera at which objects will be in focus.

NameTypeDefault
depthoffield.aperture.enableinteger0

By default, the renderer simulates a circular aperture for depth of field. Enable this feature to simulate aperture “blades” as on a real camera. This feature affects the look in out-of-focus regions of the image.

NameTypeDefault
depthoffield.aperture.sidesinteger5

Number of sides of the camera’s aperture. The minimum number of sides is 3.

NameTypeDefault
depthoffield.aperture.angledouble0

A rotation angle (in degrees) to be applied to the camera’s aperture, in the image plane.

NameTypeDefault
unitlengthmillimetersdouble

Physical length, in millimeters, of one scene unit. Since NSI only uses virtual scene units, this has no effect on the rendered images. However, this value can be useful if the renderer has to communicate with other software or file formats using physical units. For example, the focal length of the camera, expressed in millimeters, would be the product depthoffield.focallength * unitlengthmillimeters.

fisheyecamera

Fish eye cameras are useful for a multitude of applications (e.g. virtual reality). This node accepts these attributes:

NameTypeDefault
fovfloat

Specifies the field of view for this camera node, in degrees.

NameTypeDefault
mappingstringequidistant

Defines one of the supported fisheye mapping functions:

  • equidistant — Maintains angular distances.
  • equisolidangle — Every pixel in the image covers the same solid angle.
  • orthographic — Maintains planar illuminance. This mapping is limited to a 180 field of view.
  • stereographic — Maintains angles throughout the image. Note that stereographic mapping fails to work with field of views close to 360 degrees.

cylindricalcamera

This node specifies a cylindrical projection camera and has the following attributes:

NameTypeDefault
fovfloat90

Specifies the vertical field of view, in degrees.

NameTypeDefault
horizontalfovfloat360

Specifies the horizontal field of view, in degrees.

NameTypeDefault
eyeoffsetfloat

This offset allows to render stereoscopic cylindrical images by specifying an eye offset.

sphericalcamera

This node defines a spherical projection camera. This camera has no specific attributes.

Lens Shaders

A lens shader is an ᴏsʟ network connected to a camera through the lensshader connection. Such shaders receive the position and the direction of each tracer ray and can either change or completely discard the traced ray. This allows to implement distortion maps and cut maps. The following shader variables are provided:

  • P — Contains ray’s origin.
  • I — Contains ray’s direction. Setting this variable to zero instructs the renderer not to trace the corresponding ray sample.
  • time — The time at which the ray is sampled.
  • (u, v) — Coordinates, in screen space, of the ray being traced.

Script Objects

It is a design goal to provide an easy to use and flexible scripting language for ɴsɪ.

The Lua language has been selected for such a task because of its performance, lightness and features1. A flexible scripting interface greatly reduces the need to have API extensions.

For example, what is known as ‘conditional evaluation’ and ‘Ri filters’ in the RenderMan API are superseded by the scripting features of ɴsɪ.

[!NOTE] Although they go hand in hand, scripting objects are not to be confused with the Lua binding.

The binding allows for calling ɴsɪ functions in Lua while scripting objects allow for scene inspection and decision making in Lua. Script objects can make Lua binding calls to make modifications to the scene.


To be continued …

Footnotes


  1. Lua is also portable and streamable.

Rendering Guidelines

Basic Scene Anatomy

The fundamental building blocks of an ɴsɪ scene

A minimal (and useful) ɴsɪ scene graph contains the three following components:

  1. Geometry linked to the .root node, usually through a transform chain.
  2. ᴏsʟ materials linked to scene geometry through an attributes node.
  3. At least one outputdriver ​ → ​ outputlayer ​ → ​ screen ​ → ​ camera ​ → ​ .root chain to describe a view and an output device.

The scene graph in shows a renderable scene with all the necessary elements. Note how the connections always lead to the .root node.

In this view, a node with no output connections is not relevant by definition and will be ignored.

[!CAUTION] For the scene to be visible, at least one of the materials has to be emissive.

A Word – or Two – About Attributes

Those familiar with the RenderMan standard will remember the various ways to attach information to elements of the scene (standard attributes, user attributes, primitive variables, construction parameters). E.g parameters passed to RenderMan Interface calls to build certain objects. For example, knot vectors passed to RiNuPatch().

Attribute inheritance and override

In ɴsɪ things are simpler and all attributes are set through the NSISetAttribute() mechanism. The only distinction is that some attributes are required (intrinsic attributes) and some are optional: a mesh node needs to have P and nvertices defined — otherwise the geometry is invalid.

[!NOTE] In this documentation, all intrinsic attributes are documented at the beginning of each section describing a particular node.

In ᴏsʟ shaders, attributes are accessed using the getattribute() function and this is the only way to access attributes in ɴsɪ. Having one way to set and to access attributes makes things simpler (a design goal) and allows for extra flexibility (another design goal). shows two features of attribute assignment in ɴsɪ:

Attribute inheritance

: Attributes attached at some parent (in this case, a metal material) affect geometry downstream.

Attribute override

: It is possible to override attributes for a specific geometry by attaching them to a transform node directly upstream (the plastic material overrides metal upstream).

Note that any non-intrinsic attribute can be inherited and overridden, including vertex attributes such as texture coordinates.

Instancing

Instancing in ɴsɪ is naturally performed by connecting a geometry to more than one transform (connecting a geometry node into a transform.objects attribute).

Instancing in ɴsɪ with attribute inheritance and per-instance attribute override

The above figure shows a simple scene with a geometry instanced three times. The scene also demonstrates how to override an attribute for one particular geometry instance, an operation very similar to what we have seen in the attributes section. Note that transforms can also be instanced and this allows for instances of instances using the same semantics.

Creating ᴏsʟ Networks

A simple ᴏsʟ network connected to an attributes node

The semantics used to create ᴏsʟ networks are the same as for scene creation. Each shader node in the network corresponds to a shader node which must be created using NSICreate. Each shader node has implicit attributes corresponding to shader’s parameters and connection between said arguments is done using NSIConnect. Above diagram depicts a simple ᴏsʟ network connected to an attributes node.

Some observations:

  • Both the source and destination attributes (passed to NSIConnect) must be present and map to valid and compatible shader parameters (Lines 21–23).

[!NOTE] There is an exception to this: any non-shader node can be connected to a string attribute of a shader node. This will result in the non-shader node’s handle being used as the string’s value.

This behavior is useful when the shader needs to refer to another node, in a ᴏsʟ call to transform() or getattribute(), for example.

  • There is no symbolic linking between shader arguments and geometry attributes (a.k.a. primvars). One has to explicitly use the getattribute() ᴏsʟ function to read attributes attached to geometry. In this is done in the read_attribute node (Lines 11–14). Also see the section on attributes.
Create "ggx_metal" "shader"
SetAttribute "ggx"
    "shaderfilename" "string" 1  ["ggx.oso"]

Create "noise" "shader"
SetAttribute "noise"
    "shaderfilename" "string" 1 ["simplenoise.oso"]
    "frequency" "float" 1 [1.0]
    "lacunarity" "float" 1 [2.0]

Create "read_attribute" "shader"
SetAttribute "read_attribute"
    "shaderfilename" "string" 1 ["read_attributes.oso"]
    "attributename" "string" 1 ["st"]

Create "read_texture" "shader"
SetAttribute "read_texture"
    "shaderfilename" "string" 1 ["read_texture.oso"]
    "texturename" "string" 1 ["dirt.exr"]

Connect "read_attribute" "output" "read_texture" "uv"
Connect "read_texture" "output" "ggx_metal" "dirtlayer"
Connect "noise" "output" "ggx_metal" "roughness"

# Connect the OSL network to an attribute node
Connect "ggx_metal" "Ci" "attr" "surfaceshader"

Lighting in the Nodal Scene Interface

Creating lights in nsi

There are no special light source nodes in ɴsɪ (although the environment node, which defines a sphere of infinite radius, could be considered a light in practice).

Any scene geometry can become a light source if its surface shader produces an emission() closure. Some operations on light sources, such as light linking, are done using more general approaches.

Following is a quick summary on how to create different kinds of light in ɴsɪ.

Area Lights

Area lights are created by attaching an emissive surface material to geometry. Below is a simple ᴏsʟ shader for such lights (standard ᴏsʟ emitter).

// Copyright (c) 2009-2010 Sony Pictures Imageworks Inc., et al.  All Rights Reserved.
surface emitter     [[ string help = "Lambertian emitter material" ]]
(
    float power = 1 [[ string help = "Total power of the light" ]],
    color Cs = 1    [[ string help = "Base color" ]])
{
    // Because emission() expects a weight in radiance, we must convert by dividing
    // the power (in Watts) by the surface area and the factor of PI implied by
    // uniform emission over the hemisphere. N.B.: The total power is BEFORE Cs
    // filters the color!
    Ci = (power / (M_PI * surfacearea())) * Cs * emission();
}

Spot and Point Lights

Such lights are created using an epsilon sized geometry (a small disk, a particle, etc.) and optionally using extra arguments to the emission() closure.

surface spotlight(
    color i_color = color(1),
    float intenstity = 1,
    float coneAngle = 40,
    float dropoff = 0,
    float penumbraAngle = 0
) {
    color result = i_color * intenstity * M_PI;

    // Cone and penumbra
    float cosangle = dot(-normalize(I), normalize(N));
    float coneangle = radians(coneAngle);
    float penumbraangle = radians(penumbraAngle);

    float coslimit = cos(coneangle / 2);
    float cospen = cos((coneangle / 2) + penumbraangle);
    float low = min(cospen, coslimit);
    float high = max(cospen, coslimit);

    result *= smoothstep(low, high, cosangle);

    if (dropoff > 0) {
        result *= clamp(pow(cosangle, 1 + dropoff),0,1);
    }
    Ci = result / surfacearea() * emission();
}

Directional and HDR Lights

Directional lights are created by using the environment node and setting the angle attribute to 0. HDR lights are also created using the environment node, albeit with a 2π cone angle, and reading a high dynamic range texture in the attached surface shader. Other directional constructs, such as solar lights, can also be obtained using the environment node.

Since the environment node defines a sphere of infinite radius any connected ᴏsʟ shader must only rely on the I variable and disregard P, as is shown below.

shader hdrlight(
    string texturename = ""
) {
    vector wi = transform("world", I);

    float longitude = atan2(wi[0], wi[2]);
    float latitude = asin(wi[1]);

    float s = (longitude + M_PI) / M_2PI;
    float t = (latitude + M_PI_2) / M_PI;

    Ci = emission() * texture(texturename, s, t);
}

[!NOTE] Environment geometry is visible to camera rays by default so it will appear as a background in renders. To disable this simply switch off camera visibility on the associated node.

Defining Output Drivers and Layers

ɴsɪ graph showing the image output chain

ɴsɪ allows for a very flexible image output model. All the following operations are possible:

  • Defining many outputs in the same render (e.g. many EXR outputs)
  • Defining many output layers per output (e.g. multi-layer EXRs)
  • Rendering different scene views per output layer (e.g. one pass stereo render)
  • Rendering images of different resolutions from the same camera (e.g. two viewports using the same camera, in an animation software)

depicts a ɴsɪ scene to create one file with three layers. In this case, all layers are saved to the same file and the render is using one view. A more complex example is shown in : a left and right cameras are used to drive two file outputs, each having two layers (Ci and Diffuse colors).

ɴsɪ graph for a stereo image output

Light Layers

Light layers

The ability to render a certain set of lights per output layer has a formal workflow in ɴsɪ. One can use three methods to define the lights used by a given output layer:

  1. Connect the geometry defining lights directly to the outputlayer.lightset attribute
  2. Create a set of lights using the set node and connect it into outputlayer.lightset
  3. A combination of both 1 and 2

Above diagram a scene using method to create an output layer containing only illumination from two lights of the scene. Note that if there are no lights or light sets connected to the lightset attribute then all lights are rendered. The final output pixels contain the illumination from the considered lights on the specific surface variable specified in outputlayer.variablename ().

Inter-Object Visibility

Some common rendering features are difficult to achieve using attributes and hierarchical tree structures. One such example is inter-object visibility in a 3D scene. A special case of this feature is light linking which allows the artist to select which objects a particular light illuminates, or not. Another classical example is a scene in which a ghost character is invisible to camera rays but visible in a mirror.

In ɴsɪ such visibility relationships are implemented using cross-hierarchy connection between one object and another. In the case of the mirror scene, one would first tag the character invisible using the attribute and then connect the attribute node of the receiving object (mirror) to the visibility attribute of the source object (ghost) to override its visibility status. Essentially, this “injects” a new value for the ghost visibility for rays coming from the mirror.

Visibility override, both hierarchically and inter-object

Above figure shows a scenario where both hierarchy attribute overrides and inter-object visibility are applied:

  • The ghost transform has a visibility attribute set to 0 which makes the ghost invisible to all ray types

  • The hat of the ghost has its own attribute with a visibility set to 1 which makes it visible to all ray types

  • The mirror object has its own attributes node that is used to override the visibility of the ghost as seen from the mirror. The nsi stream code to achieve that would look like this:

    Connect "mirror_attribute" "" "ghost_attributes" "visibility"
        "value" "int" 1 [1]
        "priority" "int" 1 [2]
    

    Here, a priority of 2 has been set on the connection for documenting purposes, but it could have been omitted since connections always override regular attributes of equivalent priority.

Footnotes

Cookbook

The Nodal Scene Interface (NSI) is a simple yet expressive API to describe a scene to a renderer. From geometry declaration, to instancing, to attribute inheritance and shader assignments, everything fits in 12 API calls. The following recipes demonstrate how to achieve most common manipulations.

Geometry Creation

Creating geometry nodes is simple. The content of each node is filled using the NSISetAttribute call.

## Polygonal meshes can be created minimally by specifying "P".
## NSI's C++ API provides an easy interface to pass parameters to all NSI
## API calls through the Args class.

Create "simple polygon" "mesh"
SetAttribute "simple polygon"
    "P" "point" 1 [ -1  1  0   1  1  0   1 -1  0   -1 -1  0 ]

Geometry Creation in C++

/*
    Polygonal meshes can be created minimally by specifying "P".
    NSI's C++ API provides an easy interface to pass parameters
    to all NSI API calls through the Args class.
*/
const char *k_poly_handle = "simple polygon"; /* avoids typos */

nsi.Create( k_poly_handle, "mesh" );

NSI::ArgumentList mesh_args;
float points[3*4] = { -1, 1, 0,  1, 1, 0, 1, -1, 0, -1, -1, 0 };
mesh_args.Add(
    NSI::Argument::New( "P" )
        ->SetType( NSITypePoint )
        ->SetCount( 4 )
        ->SetValuePointer( points ) );
nsi.SetAttribute( k_poly_handle, mesh_args );

Specifying normals and other texture coordinates follows the same logic. Constant attributes can be declared in a concise form too:

SetAttribute "simple polygon"
    "subdivision.scheme" "string" 1 ["catmull-clark"]

Adding constant attributes in C++

/** Turn our mesh into a subdivision surface */
nsi.SetAttribute( k_poly_handle,
    NSI::CStringPArg("subdivision.scheme", "catmull-clark") );

Transforming Geometry

In NSI, a geometry is rendered only if connected to the scene’s root (which has the special handle “.root”). It is possible to directly connect a geometry node (such as the simple polygon above) to scene’s root but it wouldn’t be very useful. To place/instance a geometry anywhere in the 3D world a transform node is used as in the code snippet below.

Create "my translation" "transform"
Connect "translation"  "" ".root" "objects"
Connect "simple polygon" "" "translation" "objects" );

# Transalte 1 unit in Y
SetAttribute "my translation"
    "transformationmatrix" "matrix" 1 [
    1 0 0 0
    0 1 0 0
    0 0 1 0
    0 1 0 1]

Adding constant attributes in C++

const char *k_instance1 = "my translation";

nsi.Create( k_instance1, "transform" );
nsi.Connect( k_instance1, "", NSI_SCENE_ROOT, "objects" );
nsi.Connect( k_poly_handle, "", k_instance1, "objects" );

/*
    Matrices in NSI are in double format to allow for greater
    range and precision.
*/
double trs[16] =
{
    1., 0., 0., 0.,
    0., 1., 0., 0.,
    0., 0., 1., 0.,
    0., 1., 0., 1. /* transalte 1 unit in Y */
};

nsi.SetAttribute( k_instance1,
    NSI::DoubleMatrixArg("transformationmatrix", trs) );

Instancing is as simple as connecting a geometry to different attributes. Instances of instances do work as expected too.

const char *k_instance2 = "another translation";
trs[13] += 1.0; /* translate in Y+ */

nsi.Create( k_instance2, "transform" );
nsi.Connect( k_poly_handle, "", k_instance2, "objects" );
nsi.Connect( k_instance2, "", NSI_SCENE_ROOT, "objects" );

/* We know have two instances of the same polygon in the scene */

OSL Extensions in 3Delight

Subsurface Scattering Inside Intersecting Volumes

Subsurface scattering (SSS) is accessible in 3Delight through the ᴏsʟ subsurface() closure. It simulates the scattering of light inside a volume bounded by a closed surface. A complication that often arises when using this feature is the problem of intersecting SSS volumes. This results in a third volume, also bounded by a composite closed surface.

3Delight handles overlapping SSS volumes that don’t have the same properties by mixing them together, thus creating a hybrid material inside the intersection. For this to work properly, the ᴏsʟ subsurface() closure must be used on both entry and exit of the volume. As a consequence, it shouldn’t depend on the orientation of the bounding surface normal.

Cross-section of overlapping red and blue SSS volumes — a hybrid material is used inside the intersection.

Intersection Priorities

However, this behavior can be changed by assigning priorities to SSS shaders through the optional intersectionpriority parameter of the subsurface() closure. Inside the intersection, the SSS shader with the highest intersection priorities will be used exclusively.

Cross-section of overlapping SSS volumes — highest priority assigned to the red object (left) or the blue object (right).

This tends to be useful when the intersection is not accidental, but rather the result of a decision made when defining the scene geometry. For example, in a model of a mouth a set of teeth can be designed to penetrate the geometry of the gums. This avoids modelling a small “pocket” on the gums around each tooth. In that case, the teeth should be assigned a higher priority than the gums in order for their roots to use only the tooth shader.

The intersectionpriority parameter is an integer between -60 and 60. Its default value is 0.

Merge Sets

Even when overlapping SSS objects use the same shader, 3Delight still treats the intersection as a separate volume with its own surface. This is often not the desired effect. The geometry of the intersecting objects is still intact, so it hinders the propagation of light inside the volumes. The result is darker or brighter areas on the surface along the boundary.

Cross-section of overlapping SSS volumes — internal surfaces are still present on the left, removed on the right using a Merge Set.

This can be fixed by assigning a Merge Set name to each SSS material through the optional mergeset parameter of the subsurface() closure. SSS volumes within the same Merge Set will be considered as a single volume, without internal divisions. This tends to be useful when a complex object is made up of multiple pieces of geometry that overlap in order to appear as a single object.

Acknowledgements

Many thanks to John Haddon, Daniel Dresser, David Minor, Moritz Mœller and Gregory Ducatel for initiating the first discussions and encouraging us to design a new scene description API. Bo Zhou and Paolo Berto helped immensely with plug-in design which ultimately led to improvements in ɴsɪ (e.g. adoption of the screen node). Jordan Thistlewood opened the way for the first integration of ɴsɪ into a commercial plug-in. Stefan Habel did a thorough proofreading of the entire document and gave many suggestions.

The ɴsɪ logo was designed by Paolo Berto.

nsi.h

#ifndef __nsi_h
#define __nsi_h

#include <stddef.h>

#ifdef _WIN32
    #define DL_INTERFACE __declspec(dllimport)
#else
    #define DL_INTERFACE
#endif

#ifdef  __cplusplus
extern "C" {
#endif

typedef int NSIContext_t;
typedef const char* NSIHandle_t;

#define NSI_BAD_CONTEXT ((NSIContext_t)0)
#define NSI_SCENE_ROOT ".root"
#define NSI_SCENE_GLOBAL ".global"
#define NSI_ALL_NODES ".all"
#define NSI_ALL_ATTRIBUTES ".all"
#define NSI_VERSION 2

/* Type values for NSIParam_t.type */
enum NSIType_t
{
	NSITypeInvalid = 0,
	NSITypeFloat = 1,
	NSITypeDouble = NSITypeFloat | 0x10,
	NSITypeInteger = 2,
	NSITypeString = 3,
	NSITypeColor = 4,
	NSITypePoint = 5,
	NSITypeVector = 6,
	NSITypeNormal = 7,
	NSITypeMatrix = 8,
	NSITypeDoubleMatrix = NSITypeMatrix | 0x10,
	NSITypePointer = 9
};

static inline
size_t NSITypeSizeOf(unsigned t)
{
	static const unsigned char sizes[] =
	{
		0, sizeof(float), sizeof(int), sizeof(char*),
		3*sizeof(float), 3*sizeof(float), 3*sizeof(float), 3*sizeof(float),
		16*sizeof(float), sizeof(void*), 0, 0,
		0, 0, 0, 0,
		0, sizeof(double), 0, 0,
		0, 0, 0, 0,
		16*sizeof(double)
	};
	return t <= 24 ? sizes[t] : 0;
}

/* Flag values for NSIParam_t.flags */
enum
{
	NSIParamIsArray = 1,
	NSIParamPerFace = 2,
	NSIParamPerVertex = 4,
	NSIParamInterpolateLinear = 8
};

/* Structure for optional parameters. */
struct NSIParam_t
{
	const char *name;
	const void *data;
	int type;
	int arraylength;
	size_t count;
	int flags;
};

/* Values for second parameter of NSIRenderStopped_t */
enum NSIStoppingStatus
{
	NSIRenderCompleted = 0,
	NSIRenderAborted = 1,
	NSIRenderSynchronized = 2,
	NSIRenderRestarted = 3
};

/* Error levels for the error callback. */
enum NSIErrorLevel
{
	NSIErrMessage = 0,
	NSIErrInfo = 1,
	NSIErrWarning = 2,
	NSIErrError = 3
};

/* Error handler callback type. */
typedef void (*NSIErrorHandler_t)(
	void *userdata, int level, int code, const char *message );

/* Stopped callback type. */
typedef void (*NSIRenderStopped_t)(
	void *userdata, NSIContext_t ctx, int status );

DL_INTERFACE NSIContext_t NSIBegin(
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSIEnd( NSIContext_t ctx );

DL_INTERFACE void NSICreate(
	NSIContext_t ctx,
	NSIHandle_t handle,
	const char *type,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSIDelete(
	NSIContext_t ctx,
	NSIHandle_t handle,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSISetAttribute(
	NSIContext_t ctx,
	NSIHandle_t object,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSISetAttributeAtTime(
	NSIContext_t ctx,
	NSIHandle_t object,
	double time,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSIDeleteAttribute(
	NSIContext_t ctx,
	NSIHandle_t object,
	const char *name );

DL_INTERFACE void NSIConnect(
	NSIContext_t ctx,
	NSIHandle_t from,
	const char *from_attr,
	NSIHandle_t to,
	const char *to_attr,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSIDisconnect(
	NSIContext_t ctx,
	NSIHandle_t from,
	const char *from_attr,
	NSIHandle_t to,
	const char *to_attr );

DL_INTERFACE void NSIEvaluate(
	NSIContext_t ctx,
	int nparams,
	const struct NSIParam_t *params );

DL_INTERFACE void NSIRenderControl(
	NSIContext_t ctx,
	int nparams,
	const struct NSIParam_t *params );

#ifdef __cplusplus
}
#endif

#endif

nsi.hpp

#ifndef __nsi_hpp
#define __nsi_hpp

#include "nsi.h"

#include <cstdlib>
#include <cstring>
#include <vector>
#include <string>

namespace NSI
{

/* Interface which provides the C API. */
class CAPI
{
public:
	virtual ~CAPI() {}

	virtual NSIContext_t NSIBegin(
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSIEnd(
		NSIContext_t ctx ) const = 0;

	virtual void NSICreate(
		NSIContext_t ctx,
		NSIHandle_t handle,
		const char *type,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSIDelete(
		NSIContext_t ctx,
		NSIHandle_t handle,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSISetAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSISetAttributeAtTime(
		NSIContext_t ctx,
		NSIHandle_t object,
		double time,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSIDeleteAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		const char *name ) const = 0;

	virtual void NSIConnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSIDisconnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr ) const = 0;

	virtual void NSIEvaluate(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const = 0;

	virtual void NSIRenderControl(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const = 0;
};

/* Default API provider, used when linking directly with the renderer. */
class LinkedAPI : public CAPI
{
public:
	static CAPI& Instance() { static LinkedAPI api; return api; }

	virtual NSIContext_t NSIBegin(
		int nparams,
		const NSIParam_t *params ) const
	{
		return ::NSIBegin( nparams, params );
	}

	virtual void NSIEnd(
		NSIContext_t ctx ) const
	{
		::NSIEnd( ctx );
	}

	virtual void NSICreate(
		NSIContext_t ctx,
		NSIHandle_t handle,
		const char *type,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSICreate( ctx, handle, type, nparams, params );
	}

	virtual void NSIDelete(
		NSIContext_t ctx,
		NSIHandle_t handle,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSIDelete( ctx, handle, nparams, params );
	}

	virtual void NSISetAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSISetAttribute( ctx, object, nparams, params );
	}

	virtual void NSISetAttributeAtTime(
		NSIContext_t ctx,
		NSIHandle_t object,
		double time,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSISetAttributeAtTime( ctx, object, time, nparams, params );
	}

	virtual void NSIDeleteAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		const char *name ) const
	{
		::NSIDeleteAttribute( ctx, object, name );
	}

	virtual void NSIConnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSIConnect( ctx, from, from_attr, to, to_attr, nparams, params );
	}

	virtual void NSIDisconnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr ) const
	{
		::NSIDisconnect( ctx, from, from_attr, to, to_attr );
	}

	virtual void NSIEvaluate(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSIEvaluate( ctx, nparams, params );
	}

	virtual void NSIRenderControl(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const
	{
		::NSIRenderControl( ctx, nparams, params );
	}
};

class ArgBase;

/*
	StaticArgumentListProxy is a linked list of ArgBase references which is
	eventually flattened into an array of NSIParam_t.

	What it allows is convenient syntax to build a list of static length
	inline:

	ctx.SetAttribute( "handle",
		(
			NSI::IntegerArg( "arg1", 1 ),
			NSI::StringArg( "arg2", "2" )
		) );
*/
template<unsigned N>
class StaticArgumentListProxy
{
public:
	StaticArgumentListProxy(
		const ArgBase &a,
		const StaticArgumentListProxy<N-1> &prev )
	:
		m_arg( a ), m_prev( prev )
	{
	}

	/* Write the list to a contiguous NSIParam_t array, from the end. */
	inline void FlattenList( NSIParam_t *p ) const;

	/* This appends an argument to an existing list. */
	StaticArgumentListProxy<N+1> operator,( const ArgBase &a ) const
	{
		return StaticArgumentListProxy<N+1>( a, *this );
	}

private:
	const ArgBase &m_arg;
	const StaticArgumentListProxy<N-1> &m_prev;
};


/*
	This template specialization exists so the object can be built directly
	from ArgBase's operator, without using any temporaries.

	It also ends the recursive list. The the reason above is why it does not
	end with a <0> specialization which would have been much simpler.
*/
template<>
class StaticArgumentListProxy<2>
{
public:
	StaticArgumentListProxy(
		const ArgBase &a0,
		const ArgBase &a1 )
	:
		m_arg0( a0 ), m_arg1( a1 )
	{
	}

	/* Write the list to a contiguous NSIParam_t array, from the end. */
	inline void FlattenList( NSIParam_t *p ) const;

	/* This appends an argument to an existing list. */
	StaticArgumentListProxy<3> operator,( const ArgBase &a ) const
	{
		return StaticArgumentListProxy<3>( a, *this );
	}

private:
	const ArgBase &m_arg0;
	const ArgBase &m_arg1;
};


class ArgBase
{
	/* Arguments are not meant to be copied around. */
	ArgBase( const ArgBase& );
	void operator=( const ArgBase& );

public:
	/*
		It is a convention that argument names provided as C strings are not
		copied (caller must keep the string valid) while the names provided as
		C++ string are copied. This should fit with the general behavior of C
		vs C++ strings.
	*/
	ArgBase( const char *name )
	:
		m_name( name ), m_name_buf( 0 )
	{
	}

	ArgBase( const std::string &name )
	:
		m_name_buf( new char[name.size() + 1u] )
	{
		m_name = m_name_buf;
		m_name_buf[ name.copy( m_name_buf, std::string::npos ) ] = 0;
	}

	virtual ~ArgBase()
	{
		if( m_name_buf )
			delete[] m_name_buf;
	}

	/* This starts building a list from (arg, arg). */
	StaticArgumentListProxy<2> operator,( const ArgBase &arg )
	{
		return StaticArgumentListProxy<2>( *this, arg );
	}

	virtual void FillNSIParam( NSIParam_t &p ) const = 0;

protected:
	const char *m_name;
	/* When we own the name string, this points to it. Otherwise 0. */
	char *m_name_buf;
};


template<unsigned N>
void StaticArgumentListProxy<N>::FlattenList( NSIParam_t *p ) const
{
	m_arg.FillNSIParam( *p );
	m_prev.FlattenList( p - 1 );
}

void StaticArgumentListProxy<2>::FlattenList( NSIParam_t *p ) const
{
	m_arg1.FillNSIParam( p[0] );
	m_arg0.FillNSIParam( p[-1] );
}


/*
	Generic argument class to handle what is not easily done with the more
	specific classes.
*/
class Argument : public ArgBase
{
public:
	Argument( const char *name )
	:
		ArgBase( name ),
		m_data_buffer( 0 )
	{
		m_param.name = m_name;
		m_param.data = 0;
		m_param.type = NSITypeInvalid;
		m_param.arraylength = 0;
		m_param.count = 1;
		m_param.flags = 0;
	}

	Argument( const std::string &name )
	:
		ArgBase( name ),
		m_data_buffer( 0 )
	{
		m_param.name = m_name;
		m_param.data = 0;
		m_param.type = NSITypeInvalid;
		m_param.arraylength = 0;
		m_param.count = 1;
		m_param.flags = 0;
	}

	virtual ~Argument()
	{
		if( m_data_buffer )
			std::free( m_data_buffer );
	}

	/*
		Those two, along with the SetXX returning this, are to support adding
		arguments to an ArgumentList without using a variable to hold the
		pointer. eg.

		argument_list.Add(
			Argument::New( "attributename" )
			->SetType( NSITypeInteger )
			->SetCount( 4 )
			->CopyValue( att_value, 4*sizeof(int) ) );
	*/
	static Argument* New( const char *name )
		{ return new Argument( name ); }
	static Argument* New( const std::string &name )
		{ return new Argument( name ); }

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p = m_param;
	}

	Argument* SetType( NSIType_t type )
	{
		m_param.type = type;
		m_param.flags &= ~int(NSIParamIsArray);
		return this;
	}

	Argument* SetArrayType( NSIType_t type, size_t arraylength )
	{
		m_param.type = type;
		m_param.arraylength = int(arraylength);
		m_param.flags |= NSIParamIsArray;
		return this;
	}

	Argument* SetCount( size_t count ) { m_param.count = count; return this; }

	void* AllocValue( size_t value_size )
	{
		if( m_data_buffer )
			std::free( m_data_buffer );
		m_data_buffer = std::malloc( value_size );
		m_param.data = m_data_buffer;
		return m_data_buffer;
	}

	Argument* CopyValue( const void *value, size_t value_size )
	{
		std::memcpy( AllocValue( value_size ), value, value_size );
		return this;
	}

	Argument* SetValuePointer( const void *value )
	{
		m_param.data = value;
		return this;
	}

	Argument* SetFlags( int flags )
	{
		m_param.flags |= flags;
		return this;
	}

	Argument* ResetFlags( int flags )
	{
		m_param.flags &= ~flags;
		return this;
	}

private:
	NSIParam_t m_param;
	void *m_data_buffer;
};

class IntegerArg : public ArgBase
{
public:
	IntegerArg( const char *name, int v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	IntegerArg( const std::string &name, int v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypeInteger;
		p.count = 1;
		p.flags = 0;
	}

private:
	int m_v;
};

class FloatArg : public ArgBase
{
public:
	FloatArg( const char *name, float v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	FloatArg( const std::string &name, float v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypeFloat;
		p.count = 1;
		p.flags = 0;
	}

private:
	float m_v;
};

class DoubleArg : public ArgBase
{
public:
	DoubleArg( const char *name, double v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	DoubleArg( const std::string &name, double v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypeDouble;
		p.count = 1;
		p.flags = 0;
	}

private:
	double m_v;
};

template<int TYPE>
class F3Arg : public ArgBase
{
public:
	F3Arg( const char *name, const float *v )
	:
		ArgBase( name )
	{
		m_v[0] = v[0];
		m_v[1] = v[1];
		m_v[2] = v[2];
	}

	F3Arg( const std::string &name, const float *v )
	:
		ArgBase( name )
	{
		m_v[0] = v[0];
		m_v[1] = v[1];
		m_v[2] = v[2];
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v[0];
		p.type = TYPE;
		p.count = 1;
		p.flags = 0;
	}

private:
	float m_v[3];
};

typedef F3Arg<NSITypeColor> ColorArg;
typedef F3Arg<NSITypePoint> PointArg;
typedef F3Arg<NSITypeVector> VectorArg;
typedef F3Arg<NSITypeNormal> NormalArg;

class DoubleMatrixArg : public ArgBase
{
public:
	DoubleMatrixArg( const char *name, const double *v )
	:
		ArgBase( name )
	{
		for( int i=0; i<16; i++ )
			m_v[i] = v[i];
	}

	DoubleMatrixArg( const std::string &name, const double *v )
	:
		ArgBase( name )
	{
		for( int i=0; i<16; i++ )
			m_v[i] = v[i];
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v[0];
		p.type = NSITypeDoubleMatrix;
		p.count = 1;
		p.flags = 0;
	}

private:
	double m_v[16];
};

/*
	This does not make a copy of the given string. Use StringArg if that string
	is shorter lived than the argument list.
*/
class CStringPArg : public ArgBase
{
public:
	CStringPArg( const char *name, const char *v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	CStringPArg( const std::string &name, const char *v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypeString;
		p.count = 1;
		p.flags = 0;
	}

private:
	const char *m_v;
};

class StringArg : public ArgBase
{
public:
	StringArg( const char *name, const char *v )
	:
		ArgBase( name ), m_s( v )
	{
	}

	StringArg( const std::string &name, const char *v )
	:
		ArgBase( name ), m_s( v )
	{
	}

	StringArg( const char *name, const std::string &v )
	:
		ArgBase( name ), m_s( v )
	{
	}

	StringArg( const std::string &name, const std::string &v )
	:
		ArgBase( name ), m_s( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		m_v = m_s.c_str();
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypeString;
		p.count = 1;
		p.flags = 0;
	}

private:
	std::string m_s;
	mutable const char *m_v;
};


class PointerArg : public ArgBase
{
public:
	PointerArg( const char *name, const void *v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	PointerArg( const std::string &name, const void *v )
	:
		ArgBase( name ), m_v( v )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = &m_v;
		p.type = NSITypePointer;
		p.count = 1;
		p.flags = 0;
	}

private:
	const void *m_v;
};


/* This does not make a copy of the given integers array */
class IntegersArg : public ArgBase
{
public:
	IntegersArg( const char *name, const int *v, size_t count )
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	IntegersArg( const std::string &name, const int *v, size_t count)
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = m_v;
		p.type = NSITypeInteger;
		p.count = m_count;
		p.flags = 0;
	}

private:
	const int *m_v;
	size_t m_count;
};


/* This does not make a copy of the given points array */
class PointsArg : public ArgBase
{
public:
	PointsArg( const char *name, const float *v, size_t count )
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	PointsArg( const std::string &name, const float *v, size_t count)
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = m_v;
		p.type = NSITypePoint;
		p.count = m_count;
		p.flags = 0;
	}

private:
	const float *m_v;
	size_t m_count;
};


/* This does not make a copy of the given normals array */
class NormalsArg : public ArgBase
{
public:
	NormalsArg( const char *name, const float *v, size_t count )
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	NormalsArg( const std::string &name, const float *v, size_t count)
	:
		ArgBase( name ), m_v( v ), m_count( count )
	{
	}

	virtual void FillNSIParam( NSIParam_t &p ) const
	{
		p.name = m_name;
		p.data = m_v;
		p.type = NSITypeNormal;
		p.count = m_count;
		p.flags = 0;
	}

private:
	const float *m_v;
	size_t m_count;
};


class ArgumentList
{
	ArgumentList( const ArgumentList& );
	void operator=( const ArgumentList& );
public:
	ArgumentList() {}

	~ArgumentList() { clear(); }

	void clear()
	{
		while( !m_args.empty() )
		{
			delete m_args.back();
			m_args.pop_back();
		}
	}

	bool empty() const { return m_args.empty(); }

	size_t size() const { return m_args.size(); }
	const ArgBase* operator[]( size_t i ) const { return m_args[i]; }

	void Add( ArgBase *arg )
	{
		m_args.push_back( arg );
	}

	void push( ArgBase *arg )
	{
		m_args.push_back( arg );
	}

	void push_back( ArgBase *arg )
	{
		m_args.push_back( arg );
	}


private:
	std::vector<ArgBase*> m_args;
};
typedef ArgumentList DynamicArgumentList; /* backward compatibility */




class Context
{
	/*
		Don't allow copying because ownership semantics get really messy. If
		you really know what you're doing, use the Handle() method and build
		another context from it.
	*/
	Context( const Context& );
	void operator=( const Context& );

private:
	class FlatArgumentList
	{
		void operator=( const FlatArgumentList& );
#if __cplusplus < 201103L
	/* Stupid rule with old C++ requires this to be visible. */
	public:
#endif
		FlatArgumentList( const FlatArgumentList& );

	public:
		/* Empty list. */
		FlatArgumentList()
		:
			m_nsi_params( 0 ),
			m_size_nsi_params( 0 )
		{
		}

		/* From dynamically built argument list. */
		FlatArgumentList( const ArgumentList &arglist )
		{
			m_size_nsi_params = arglist.size();
			m_nsi_params = new NSIParam_t[ m_size_nsi_params ];
			for( unsigned i = 0; i < m_size_nsi_params; ++i )
			{
				arglist[i]->FillNSIParam( m_nsi_params[i] );
			}
		}

		/* From a static argument list. */
		template<unsigned N>
		FlatArgumentList( const StaticArgumentListProxy<N> &arglist )
		{
			m_size_nsi_params = N;
			m_nsi_params = new NSIParam_t[ m_size_nsi_params ];
			arglist.FlattenList( m_nsi_params + N - 1u );
		}

		/* From a single argument. */
		FlatArgumentList( const ArgBase &arg )
		{
			m_size_nsi_params = 1;
			m_nsi_params = new NSIParam_t[ m_size_nsi_params ];
			arg.FillNSIParam( m_nsi_params[0] );
		}

		~FlatArgumentList()
		{
			delete[] m_nsi_params;
		}

		int size() const { return int(m_size_nsi_params); }
		const NSIParam_t* list() const { return m_nsi_params; }

	private:
		mutable NSIParam_t *m_nsi_params;
		mutable size_t m_size_nsi_params;
	};

public:
	/* Deprecated. */
	explicit Context( NSIContext_t ctx )
	:
		m_ctx( ctx ),
		m_owns_context( false ),
		m_api( LinkedAPI::Instance() )
	{
	}

	Context( const CAPI &api = LinkedAPI::Instance() )
	:
		m_ctx( NSI_BAD_CONTEXT ),
		m_owns_context( false ),
		m_api( api )
	{
	}

	/* Destroys the context, if owned by this object. */
	~Context()
	{
		if( m_owns_context )
			End();
	}

	/*
		Use an existing C API handle. The context will not be destroyed with
		this object but End() may be called explicitly to destroy it.
	*/
	void SetHandle( NSIContext_t ctx )
	{
		if( m_owns_context && m_ctx != NSI_BAD_CONTEXT )
			End();

		m_ctx = ctx;
		m_owns_context = false;
	}

	/* Retrieve the C API handle. */
	NSIContext_t Handle() const { return m_ctx; }

	/*
		Make this object no longer own the C API handle. Meaning the destructor
		will not End() it.
	*/
	void Detach() { m_owns_context = false; }

	/* Create a new context. */
	void Begin( const FlatArgumentList &params = FlatArgumentList() )
	{
		if( m_owns_context && m_ctx != NSI_BAD_CONTEXT )
			End();

		m_ctx = m_api.NSIBegin( params.size(), params.list() );
		m_owns_context = true;
	}

	/* Destroy the context. */
	void End()
	{
		m_api.NSIEnd( m_ctx );
		m_ctx = NSI_BAD_CONTEXT;
		m_owns_context = false;
	}

	void Create(
		const std::string &handle,
		const std::string &type,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSICreate(
			m_ctx,
			handle.c_str(),
			type.c_str(),
			params.size(), params.list() );
	}

	void Delete(
		const std::string &handle,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSIDelete(
			m_ctx,
			handle.c_str(),
			params.size(), params.list() );
	}

	void SetAttribute(
		const std::string &object,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSISetAttribute(
			m_ctx,
			object.c_str(),
			params.size(), params.list() );
	}

	void SetAttributeAtTime(
		const std::string &object,
		double time,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSISetAttributeAtTime(
			m_ctx,
			object.c_str(),
			time,
			params.size(), params.list() );
	}

	void DeleteAttribute(
		const std::string &object,
		const std::string &name )
	{
		m_api.NSIDeleteAttribute(
			m_ctx,
			object.c_str(),
			name.c_str() );
	}

	void Connect(
		const std::string &from,
		const std::string &from_attr,
		const std::string &to,
		const std::string &to_attr,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSIConnect(
			m_ctx,
			from.c_str(),
			from_attr.c_str(),
			to.c_str(),
			to_attr.c_str(),
			params.size(), params.list() );
	}

	void Disconnect(
		const std::string &from,
		const std::string &from_attr,
		const std::string &to,
		const std::string &to_attr,
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSIDisconnect(
			m_ctx,
			from.c_str(),
			from_attr.c_str(),
			to.c_str(),
			to_attr.c_str() );
	}

	void Evaluate(
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSIEvaluate(
			m_ctx,
			params.size(), params.list() );
	}

	void RenderControl(
		const FlatArgumentList &params = FlatArgumentList() )
	{
		m_api.NSIRenderControl(
			m_ctx,
			params.size(), params.list() );
	}

private:
	NSIContext_t m_ctx;
	bool m_owns_context;
	const CAPI &m_api;
};

};

#endif

nsi_dynamic.hpp

/*
	This file contains the code required to load NSI at runtime instead of
	linking with it at build time. To use, simply give an instance to the
	context constructor:

	NSI::DynamicAPI api;
	NSI::Context ctx( api );

	ctx.Begin();
	ctx.Create( "myhandle", "mesh" );
	...

	The DynamicAPI class loads and unloads the library so at least one instance
	must be kept active while the renderer is in use.
*/

#ifndef __nsi_dynamic_hpp
#define __nsi_dynamic_hpp

#include "nsi.hpp"

#if defined(__linux__) || defined(__APPLE__)
#	include <dlfcn.h>
#elif defined(_WIN32)
#	include <windows.h>
#endif

#include <algorithm>
#include <cstdlib>
#include <string>

namespace NSI
{

/* API provider which dynamically loads the renderer. */
class DynamicAPI : public CAPI
{
#if defined(__linux__) || defined(__APPLE__)
	void *m_lib;

public:
	template<typename T>
	void LoadFunction( T &function, const char *name )
	{
		if( m_lib )
			function = (T) dlsym( m_lib, name );
		else
			function = (T)0;
	}
#elif defined(_WIN32)
	HMODULE m_lib;

public:
	template<typename T>
	void LoadFunction( T &function, const char *name )
	{
		if( m_lib )
			function = (T) GetProcAddress( m_lib, name );
		else
			function = (T)0;
	}
#else
public:
	template<typename T>
	void LoadFunction( T &function, const char *name )
	{
		function = (T)0;
	}
#endif

public:
	DynamicAPI(const char* path = 0)
	{
#if defined(__linux__)
		m_lib = dlopen( path ? path : "lib3delight.so", RTLD_NOW );
#elif defined(__APPLE__)
		if( path )
		{
			m_lib = dlopen( path, RTLD_NOW );
		}
		else
		{
			m_lib = dlopen( "/Applications/3Delight/lib/lib3delight.dylib", RTLD_NOW );
			if( !m_lib )
			{
				m_lib = dlopen( "lib3delight.dylib", RTLD_NOW );
			}
			if( !m_lib )
			{
				/* Last resort, try DELIGHT. */
				const char *delight = getenv("DELIGHT");
				if( delight && delight[0] )
				{
					std::string dl = delight;
					if( dl.back() != '/' )
						dl.push_back('/');
					dl.append("lib/lib3delight.dylib");
					m_lib = dlopen(dl.c_str(), RTLD_NOW);
				}
			}
		}
#elif defined(_WIN32)
		m_lib = LoadLibraryA( path ? path : "3Delight.dll" );
		if( m_lib == NULL && !path )
		{
			/* PATH search might have been disabled by
			   SetDefaultDllDirectories(). Try with DELIGHT. */
			const char *delight = getenv("DELIGHT");
			if( delight && delight[0] )
			{
				std::string dl = delight;
				std::replace(dl.begin(), dl.end(), '/', '\\');
				if( dl.back() != '\\' )
					dl.push_back('\\');
				dl.append("bin\\3Delight.dll");
				m_lib = LoadLibraryA(dl.c_str());
			}
		}
#endif
		LoadFunction( Begin, "NSIBegin" );
		LoadFunction( End, "NSIEnd" );
		LoadFunction( Create, "NSICreate" );
		LoadFunction( Delete, "NSIDelete" );
		LoadFunction( SetAttribute, "NSISetAttribute" );
		LoadFunction( SetAttributeAtTime, "NSISetAttributeAtTime" );
		LoadFunction( DeleteAttribute, "NSIDeleteAttribute" );
		LoadFunction( Connect, "NSIConnect" );
		LoadFunction( Disconnect, "NSIDisconnect" );
		LoadFunction( Evaluate, "NSIEvaluate" );
		LoadFunction( RenderControl, "NSIRenderControl" );
	}

	virtual ~DynamicAPI()
	{
#if defined(__linux__) || defined(__APPLE__)
		if( m_lib != 0 )
			dlclose( m_lib );
#elif defined(_WIN32)
		if( m_lib != 0 )
			FreeLibrary( m_lib );
#endif
	}

	virtual NSIContext_t NSIBegin(
		int nparams,
		const NSIParam_t *params ) const
	{
		if( Begin )
			return Begin( nparams, params );
		else
			return NSI_BAD_CONTEXT;
	}

	virtual void NSIEnd(
		NSIContext_t ctx ) const
	{
		if( End )
			End( ctx );
	}

	virtual void NSICreate(
		NSIContext_t ctx,
		NSIHandle_t handle,
		const char *type,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( Create )
			Create( ctx, handle, type, nparams, params );
	}

	virtual void NSIDelete(
		NSIContext_t ctx,
		NSIHandle_t handle,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( Delete )
			Delete( ctx, handle, nparams, params );
	}

	virtual void NSISetAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( SetAttribute )
			SetAttribute( ctx, object, nparams, params );
	}

	virtual void NSISetAttributeAtTime(
		NSIContext_t ctx,
		NSIHandle_t object,
		double time,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( SetAttributeAtTime )
			SetAttributeAtTime( ctx, object, time, nparams, params );
	}

	virtual void NSIDeleteAttribute(
		NSIContext_t ctx,
		NSIHandle_t object,
		const char *name ) const
	{
		if( DeleteAttribute )
			DeleteAttribute( ctx, object, name );
	}

	virtual void NSIConnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( Connect )
			Connect( ctx, from, from_attr, to, to_attr, nparams, params );
	}

	virtual void NSIDisconnect(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr ) const
	{
		if( Disconnect )
			Disconnect( ctx, from, from_attr, to, to_attr );
	}

	virtual void NSIEvaluate(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( Evaluate )
			Evaluate( ctx, nparams, params );
	}

	virtual void NSIRenderControl(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params ) const
	{
		if( RenderControl )
			RenderControl( ctx, nparams, params );
	}

private:
	/* API function pointers. */
	NSIContext_t (*Begin)(
		int nparams,
		const NSIParam_t *params );

	void (*End)(
		NSIContext_t ctx );

	void (*Create)(
		NSIContext_t ctx,
		NSIHandle_t handle,
		const char *type,
		int nparams,
		const NSIParam_t *params );

	void (*Delete)(
		NSIContext_t ctx,
		NSIHandle_t handle,
		int nparams,
		const NSIParam_t *params );

	void (*SetAttribute)(
		NSIContext_t ctx,
		NSIHandle_t object,
		int nparams,
		const NSIParam_t *params );

	void (*SetAttributeAtTime)(
		NSIContext_t ctx,
		NSIHandle_t object,
		double time,
		int nparams,
		const NSIParam_t *params );

	void (*DeleteAttribute)(
		NSIContext_t ctx,
		NSIHandle_t object,
		const char *name );

	void (*Connect)(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr,
		int nparams,
		const NSIParam_t *params );

	void (*Disconnect)(
		NSIContext_t ctx,
		NSIHandle_t from,
		const char *from_attr,
		NSIHandle_t to,
		const char *to_attr );

	void (*Evaluate)(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params );

	void (*RenderControl)(
		NSIContext_t ctx,
		int nparams,
		const NSIParam_t *params );
};

}

#endif

nsi.py

"""
Python binding to 3Delight's Nodal Scene Interface
"""

BAD_CONTEXT = 0

SCENE_ROOT = '.root'
SCENE_GLOBAL = '.global'
ALL_NODES = '.all'

import ctypes
import os
import platform

# Load 3Delight
if platform.system() == "Windows":
    _lib3delight = ctypes.cdll.LoadLibrary('3Delight')
elif platform.system() == "Darwin":
    __delight = os.getenv('DELIGHT')
    if __delight is None:
        __delight = '/Applications/3Delight'
    _lib3delight = ctypes.cdll.LoadLibrary(__delight + '/lib/lib3delight.dylib')
else:
    _lib3delight = ctypes.cdll.LoadLibrary('lib3delight.so')


class _NSIParam_t(ctypes.Structure):
    """
    Python version of the NSIParam_t struct to interface with the C API.
    """
    _fields_ = [
        ("name", ctypes.c_char_p),
        ("data", ctypes.c_void_p),
        ("type", ctypes.c_int),
        ("arraylength", ctypes.c_int),
        ("count", ctypes.c_size_t),
        ("flags", ctypes.c_int)
        ]

class Type:
    """
    Python version of the C NSIType_t enum
    """
    Invalid = 0
    Float = 1
    Double = Float | 0x10
    Integer = 2
    String = 3
    Color = 4
    Point = 5
    Vector = 6
    Normal = 7
    Matrix = 8
    DoubleMatrix = Matrix | 0x10
    Pointer = 9

_nsi_type_num_elements = {
    Type.Float : 1,
    Type.Double : 1,
    Type.Integer : 1,
    Type.String : 1,
    Type.Color : 3,
    Type.Point : 3,
    Type.Vector : 3,
    Type.Normal : 3,
    Type.Matrix : 16,
    Type.DoubleMatrix : 16,
    Type.Pointer : 1 }

class Flags:
    """
    Python version of the NSIParam_t flags values
    """
    IsArray = 1
    PerFace = 2
    PerVertex = 4
    InterpolateLinear = 8

def _GetArgNSIType(value):
    if isinstance(value, Arg):
        if value.type is not None:
            return value.type
        else:
            return _GetArgNSIType(value.value)
    if isinstance(value, (tuple, list)):
        return _GetArgNSIType(value[0])
    if isinstance(value, (int, bool)):
        return Type.Integer
    if isinstance(value, float):
        return Type.Double
    if isinstance(value, str):
        return Type.String
    return Type.Invalid

def _GetArgCType(value):
    nsitype = _GetArgNSIType(value)
    typemap = {
        Type.Float : ctypes.c_float,
        Type.Double : ctypes.c_double,
        Type.Integer : ctypes.c_int,
        Type.String : ctypes.c_char_p,
        Type.Color : ctypes.c_float,
        Type.Point : ctypes.c_float,
        Type.Vector : ctypes.c_float,
        Type.Normal : ctypes.c_float,
        Type.Matrix : ctypes.c_float,
        Type.DoubleMatrix : ctypes.c_double,
        Type.Pointer : ctypes.c_void_p
    }
    return typemap.get(nsitype)

def _BuildOneCArg(nsiparam, value):
    """
    Fill one _NSIParam_t object from an argument value.
    """
    # TODO: Support numpy.matrix as DoubleMatrix argument.
    datatype = _GetArgCType(value)
    arraylength = None
    countoverride = None
    flags = 0
    v = value
    if isinstance(value, Arg):
        v = value.value
        arraylength = value.arraylength
        countoverride = value.count
        flags = value.flags

    if v is None:
        nsiparam.type = Type.Invalid
        return

    if isinstance(v, ctypes.c_void_p):
        # Raw data given with ctypes. Must use nsi.Arg. No safety here.
        datacount = 0 # Will be overriden by countoverride.
        nsiparam.data = v
    elif isinstance(v, (tuple, list)):
        # Data is multiple values (eg. a list of floats).
        datacount = len(v)
        arraytype = datatype * datacount;
        if v and isinstance(v[0], str):
            # Encode all the strings to utf-8
            fixedv = [x.encode('utf-8') for x in v]
            nsiparam.data = ctypes.cast(ctypes.pointer(
                arraytype(*fixedv)), ctypes.c_void_p)
        else:
            nsiparam.data = ctypes.cast(ctypes.pointer(
                arraytype(*v)), ctypes.c_void_p)
    else:
        # Data is a single object (string, float, int).
        datacount = 1
        if isinstance(v, str):
            nsiparam.data = ctypes.cast(ctypes.pointer(
                datatype(v.encode('utf-8'))), ctypes.c_void_p)
        else:
            nsiparam.data = ctypes.cast(ctypes.pointer(
                datatype(v)), ctypes.c_void_p)

    valuecount = datacount

    nsitype = _GetArgNSIType(value)
    numelements = _nsi_type_num_elements.get(nsitype, 0)
    if numelements == 0:
        valuecount = 0
    else:
        valuecount = int(valuecount / numelements)

    if arraylength is not None:
        nsiparam.arraylength = arraylength
        flags |= Flags.IsArray
        if arraylength == 0:
            valuecount = 0
        else:
            valuecount = int(valuecount / arraylength)

    if countoverride is not None:
        if countoverride < valuecount or isinstance(v, ctypes.c_void_p):
            valuecount = countoverride

    nsiparam.type = nsitype
    nsiparam.count = valuecount
    nsiparam.flags = flags

def _BuildCArgumentList(args):
    cargs_type = _NSIParam_t * len(args)
    cargs = cargs_type()
    for i, arg in enumerate(args.items()):
        cargs[i].name = arg[0].encode('utf-8')
        _BuildOneCArg(cargs[i], arg[1])
    return cargs

class Context:
    """
    A NSI context.

    All NSI operations are done in a specific context. Multiple contexts may
    cohexist.

    Most methods of the Context accept a named argument list. The argument
    values can be native python integer, string or float (which is given to NSI
    as a double). More complex types should use one of the Arg classes in this
    module.
    """

    def __init__(self, handle=None):
        """
        If an integer handle argument is provided, this object will be bound to
        an existing NSI context with that handle.

        It is not required that the context have been created by the python
        binding.
        """
        self._handle = BAD_CONTEXT

    def Begin(self, **arglist):
        """
        Create a new NSI context and bind this object to it.
        """
        a = _BuildCArgumentList(arglist)
        self._handle = _lib3delight.NSIBegin(len(a), a)

    def End(self):
        """
        Release the context.

        If this object was bound to an external handle, that handle will on
        longer be valid after this call.
        """
        # TODO: Support with statement
        if self._handle != BAD_CONTEXT:
            _lib3delight.NSIEnd( self._handle )

    def Create(self, handle, type, **arglist):
        """
        Create a new node.

        Parameters
        handle : The handle of the node to create.
        type : The type of node to create.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSICreate(
            self._handle,
            handle.encode('utf-8'),
            type.encode('utf-8'),
            len(a), a)

    def Delete(self, handle, **arglist):
        """
        Delete a node.

        Parameters
        handle : The handle of the node to delete.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSIDelete(
            self._handle,
            handle.encode('utf-8'),
            len(a), a)

    def SetAttribute(self, handle, **arglist):
        """
        Set attributes of a node.

        Parameters
        handle : The handle of the node on which to set attributes.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSISetAttribute(
            self._handle,
            handle.encode('utf-8'),
            len(a), a)

    def SetAttributeAtTime(self, handle, time, **arglist):
        """
        Set attributes of a node for a specific time.

        Parameters
        handle : The handle of the node on which to set attributes.
        time : The time for which the attributes are set.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSISetAttributeAtTime(
            self._handle,
            handle.encode('utf-8'),
            ctypes.c_double(time),
            len(a), a)

    def DeleteAttribute(self, handle, name):
        """
        Delete an attribute of a node.

        Parameters
        handle : The handle of the node on which to delete an attribute.
        name : The name of the attribute to delete.
        """
        _lib3delight.NSIDeleteAttribute(
            self._handle,
            handle.encode('utf-8'),
            name.encode('utf-8'))

    def Connect(self, from_handle, from_attr, to_handle, to_attr, **arglist):
        """
        Connect nodes or specific attributes of nodes.

        Parameters
        from_handle : The handle of the node to connect from.
        from_attr : Optional attribute to connect from.
        to_handle : The handle of the node to connect to.
        to_attr : Optional attribute to connect to.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSIConnect(
            self._handle,
            from_handle.encode('utf-8'),
            (from_attr if from_attr else '').encode('utf-8'),
            to_handle.encode('utf-8'),
            (to_attr if to_attr else '').encode('utf-8'),
            len(a), a)

    def Disconnect(self, from_handle, from_attr, to_handle, to_attr, **arglist):
        """
        Disconnect nodes or specific attributes of nodes.

        Parameters
        from_handle : The handle of the node to disconnect from.
        from_attr : Optional attribute to disconnect from.
        to_handle : The handle of the node to disconnect to.
        to_attr : Optional attribute to disconnect to.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSIDisconnect(
            self._handle,
            from_handle.encode('utf-8'),
            (from_attr if from_attr else '').encode('utf-8'),
            to_handle.encode('utf-8'),
            (to_attr if to_attr else '').encode('utf-8'))

    def Evaluate(self, **arglist):
        """
        Evaluate NSI commands from some other source.

        This can read other files, run scripts, etc.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSIEvaluate(
            self._handle,
            len(a), a)

    def RenderControl(self, **arglist):
        """
        Control rendering.

        This is used to start and stop renders, wait for them to complete, etc.
        """
        a = _BuildCArgumentList(arglist)
        _lib3delight.NSIRenderControl(
            self._handle,
            len(a), a)

class Arg:
    """
    Wrapper for NSI parameter list values.

    NSI functions which accept a parameter list may be given values wrapped in
    an Arg object to specify details about the argument. For example, 3 float
    values are normally output as 3 NSITypeDouble values. To set a color
    instead, give nsi.Arg((0.4, 0.2, 0.5), type=nsi.Type.Color)

    The most common types have specific wrappers which are easier to use. The
    above example could instead be nsi.ColorArg(0.4, 0.2, 0.5)
    """
    def __init__(self, v, type=None, arraylength=None, flags=None, count=None):
        """
        Parameters
        v : The value.
        type : An optional value from nsi.Type
        arraylength : An optional integer to specify the array length of the
        base type. For example, 2 for texture coordinates.
        flags : Optional flags from nsi.Flags
        count : Number of values of the base type.
        """
        self.type = None
        self.arraylength = None
        self.flags = 0
        self.count = None

        if isinstance(v, Arg):
            # Fold its attributes into this object. This allows chaining Arg
            # objects.
            self.value = v.value
            self.type = v.type
            self.flags = v.flags
            self.count = v.count
        else:
            self.value = v

        if arraylength is not None:
            self.arraylength = arraylength
        if type is not None:
            self.type = type
        if flags is not None:
            self.flags |= flags
        if count is not None:
            self.count = count

class IntegerArg(Arg):
    """
    Wrapper for NSI parameter list integer value.

    Use as nsi.IntegerArg(2). This is generally not needed as it is the default
    behavior when an int is given. Using this class will enforce the type.
    """
    def __init__(self, v):
        Arg.__init__(self, int(v), type=Type.Integer)

class FloatArg(Arg):
    """
    Wrapper for NSI parameter list float value.

    Use as nsi.FloatArg(0.5).
    """
    def __init__(self, v):
        Arg.__init__(self, v, type=Type.Float)

class DoubleArg(Arg):
    """
    Wrapper for NSI parameter list double value.

    Use as nsi.DoubleArg(0.5).
    """
    def __init__(self, v):
        Arg.__init__(self, v, type=Type.Double)

class ColorArg(Arg):
    """
    Wrapper for NSI parameter list color value.

    Use as nsi.ColorArg(0.2, 0.3, 0.4) or nsi.ColorArg(0.5)
    """
    def __init__(self, r, g=None, b=None):
        if b is None:
            Arg.__init__(self, (r,r,r), type=Type.Color)
        else:
            Arg.__init__(self, (r,g,b), type=Type.Color)

# vim: set softtabstop=4 expandtab shiftwidth=4:

nsi_procedural.h

#ifndef __nsi_procedural_h
#define __nsi_procedural_h

#include "nsi.h"

#ifdef  __cplusplus
extern "C" {
#endif

struct NSIProcedural_t;

/* A function that reports messages through the renderer */
typedef void (*NSIReport_t)(NSIContext_t ctx, int level, const char* message);

/* A function that cleans-up after the last execution of the procedural */
#define NSI_PROCEDURAL_UNLOAD(name) \
	void name( \
		NSIContext_t ctx, \
		NSIReport_t report, \
		struct NSIProcedural_t* proc)
typedef NSI_PROCEDURAL_UNLOAD((*NSIProceduralUnload_t));

/* A function that translates the procedural into NSI calls */
#define NSI_PROCEDURAL_EXECUTE(name) \
	void name( \
		NSIContext_t ctx, \
		NSIReport_t report, \
		struct NSIProcedural_t* proc, \
		int nparams, \
		const struct NSIParam_t* params)
typedef NSI_PROCEDURAL_EXECUTE((*NSIProceduralExecute_t));

/* Descriptor of procedural */
struct NSIProcedural_t
{
	/* Expected version of NSI */
	unsigned nsi_version;
	/* Pointers to procedural's functions */
	NSIProceduralUnload_t unload;
	NSIProceduralExecute_t execute;
};

/* Convenient macro for procedural descriptor initialization */
#define NSI_PROCEDURAL_INIT(proc, unload_fct, execute_fct) \
	{ \
		(proc).nsi_version = NSI_VERSION; \
		(proc).unload = unload_fct; \
		(proc).execute = execute_fct; \
	}

/* The entry-point of the procedural. Returns a descriptor. */
#define NSI_PROCEDURAL_LOAD_SYMBOL NSIProceduralLoad
#define NSI_PROCEDURAL_LOAD_PARAMS \
	NSIContext_t ctx, \
	NSIReport_t report, \
	const char* nsi_library_path, \
	const char* renderer_version
typedef struct NSIProcedural_t* (*NSIProceduralLoad_t)(
	NSI_PROCEDURAL_LOAD_PARAMS);

/* Convenient macro for declaration of NSIProceduralLoad */
#define NSI_PROCEDURAL_LOAD \
	_3DL_EXTERN_C _3DL_EXPORT \
		struct NSIProcedural_t* NSI_PROCEDURAL_LOAD_SYMBOL( \
			NSI_PROCEDURAL_LOAD_PARAMS)

#ifdef __cplusplus
}
#endif

#endif

gear.cpp

#include "nsi_procedural.h"

#include "nsi_dynamic.hpp"
#include "nsi_util.h"

#include <math.h>


// Extends NSIProcedural_t to store private data
struct GearProcedural : public NSIProcedural_t
{
	explicit GearProcedural(const char* nsi_library_path)
		:	api(nsi_library_path)
	{
	}

	/*
		This loads symbols from the calling NSI library directly.
		It avoids having to link the procedural against an NSI library and
		letting the operating system locate it later.
		If linking the procedural against NSI is not a problem, then this can
		be omitted and the regular NSI API can be used directly.
	*/
	NSI::DynamicAPI api;
};


static NSI_PROCEDURAL_UNLOAD(gear_unload)
{
	/*
		The "proc" parameter is actually a pointer to the GearProcedural object
		allocated in NSIProceduralLoad.
	*/
	GearProcedural* gproc = (GearProcedural*)proc;
	delete gproc;
}


static NSI_PROCEDURAL_EXECUTE(gear_execute)
{
	/*
		The "proc" parameter is actually a pointer to the GearProcedural object
		allocated in NSIProceduralLoad.
	*/
	GearProcedural* gproc = (GearProcedural*)proc;

	// Retrieve parameters using utility functions from nsi_util.h
	const char* parent_node =
		NSI::FindStringParameter("parentnode", nparams, params);
	const char* node =
		NSI::FindStringParameter("node", nparams, params);
	const int* nb_teeth =
		NSI::FindIntegerParameter("nb_teeth", nparams, params);
	const float* inner_radius =
		NSI::FindFloatParameter("inner_radius", nparams, params);
	const float* outer_radius =
		NSI::FindFloatParameter("outer_radius", nparams, params);
	const float* teeth_slope =
		NSI::FindFloatParameter("teeth_slope", nparams, params);

	// Validate parameters and report errors

	if(!parent_node)
	{
		/*
			This is normal when the procedural is executed through a procedural
			node instead of a call to NSIEvaluate.
		*/
		parent_node = NSI_SCENE_ROOT;
	}

	if(!node)
	{
		/*
			This is normal when the procedural is executed through a procedural
			node instead of a call to NSIEvaluate.
			In that case, since the procedural is evaluated inside its own NSI
			context, in isolation from the main scene, there is no need to
			specify the new node's handle from outside the procedural.
		*/
		node = "gear";
	}

	if(!nb_teeth || *nb_teeth < 6)
	{
		report(ctx, NSIErrError, "gear : invalid number of teeth");
		return;
	}

	if(!inner_radius || *inner_radius <= 0.0f)
	{
		report(ctx, NSIErrError, "gear : invalid inner radius");
		return;
	}

	if(!outer_radius || *outer_radius <= *inner_radius)
	{
		report(ctx, NSIErrError, "gear : invalid outer radius");
		return;
	}

	float slope = teeth_slope ? *teeth_slope : 0.75f;
	if(slope <= 0.0f || slope > 1.0f)
	{
		report(ctx, NSIErrError, "gear : invalid teeth slope");
		return;
	}

	// Build the positions vector
	std::vector<float> P;
	float pitch = 2.0f * float(M_PI) / float(*nb_teeth);
	float inner_offset = pitch * (1.0f / (1.0f+slope)) / 2.0f;
	float outer_offset =
		pitch * (slope / (1.0f+slope)) / 2.0f * *inner_radius / *outer_radius;
	for(int v = 0; v < *nb_teeth; v++)
	{
		float tooth_axis = v*pitch;

		P.push_back(-sinf(tooth_axis-inner_offset) * *inner_radius);
		P.push_back(cosf(tooth_axis-inner_offset) * *inner_radius);
		P.push_back(0.0f);

		P.push_back(-sinf(tooth_axis-outer_offset) * *outer_radius);
		P.push_back(cosf(tooth_axis-outer_offset) * *outer_radius);
		P.push_back(0.0f);

		P.push_back(-sinf(tooth_axis+outer_offset) * *outer_radius);
		P.push_back(cosf(tooth_axis+outer_offset) * *outer_radius);
		P.push_back(0.0f);

		P.push_back(-sinf(tooth_axis+inner_offset) * *inner_radius);
		P.push_back(cosf(tooth_axis+inner_offset) * *inner_radius);
		P.push_back(0.0f);
	}

	/*
		Initialize a NSI::Context wrapper object from the DynamicAPI and the
		actual context handle (see comment in NSIProceduralLoad).
		If the procedural was to be linked directly against an NSI
		implementation, we could have omitted NSI::Context's constructor
		parameter.
	*/
	NSI::Context nsi(gproc->api);
	nsi.SetHandle(ctx);

	// Create the mesh and set its attributes
	nsi.Create(node, "mesh");
	nsi.SetAttribute(node,
		(
			NSI::IntegerArg("nvertices", P.size()/3),
			NSI::PointsArg("P", &P[0], P.size()/3)
		) );

	// Connect it to its parent transform
	nsi.Connect(node, "", parent_node, "objects");
}


// Main procedural entry point
NSI_PROCEDURAL_LOAD
{
	GearProcedural* proc = new GearProcedural(nsi_library_path);
	NSI_PROCEDURAL_INIT(*proc, gear_unload, gear_execute);

	return proc;
}

Naming Convention Redesign

Status: Draft

Rationale

The current ɴsɪ attribute names grew organically and suffer from several inconsistencies that make the API harder to learn and use than it needs to be.

No word separation. Most multi-word names are run together, forcing users to mentally parse where words begin and end — and the conventions are unpredictable:

NameIntended meaning
numberofthreadsnumber of threads
texturememorytexture memory
renderatlowpriorityrender at low priority
clockwisewindingclockwise winding
importancesamplefilterimportance sample filter
unitlengthmillimetersunit length millimeters

Inconsistent grouping. Some attributes use dot-separated groups, others don’t — even for closely related settings:

GroupedNot grouped
subdivision.cornerverticesclockwisewinding
subdivision.creasesharpnessoutlinecreasethreshold
visibility.camerasurfaceshader
quality.shadingsamplestexturememory
show.displacementrenderatlowpriority

Cryptic abbreviations alongside verbose names. Single-letter names coexist with long compound words:

TerseVerbose
Ptransformationmatrices
Nemissionintensitygrid
fovquality.samplevolumeemission
idstoppedcallbackdata

Node type names baked into attributes. Some attributes redundantly include the node type, others don’t:

RedundantClean
shaderfilename (on shader node)filter (on outputlayer node)
shaderobject (on shader node)angle (on environment node)
vdbfilename (on volume node)width (on particles node)
imagefilename (on outputdriver node)basis (on curves node)

This document proposes a systematic naming convention that resolves these inconsistencies. The complete mapping from current to proposed names is in the attribute mapping below.

Why Separate Words at All?

Concatenated names like maximumraylength or importancesamplefilter are hard to read — especially for non-native English speakers, who may not immediately see where one word ends and the next begins. Word separators make attribute names accessible to a wider audience without any downside: attribute name strings are interned by the renderer, so separators have zero runtime cost. They add a few bytes to the source but nothing to render time.

A common objection is that separators mean more typing. In practice this matters less than it used to: code is increasingly written with AI assistance and autocompletion, so keystroke count is a non-issue. What matters is how easily a human can read and review the code — and depth-of-field.focal-length is unambiguously clearer than depthoffield.focallength.

Redesign Proposal

Hyphenate Multi-Word Attributes

Attribute names use hyphens (-) as word separators, not underscores (_). This is a deliberate choice: almost no programming language allows hyphens in identifiers (variable-name is invalid in C, C++, Python, Rust, Lua, etc.), so attribute name strings are instantly distinguishable from code identifiers in any language. When you see reflection.ray-depth-max in source code, it can only be an attribute name — never a variable, function, or type.

Core Convention

  • . (dots) separate hierarchy levels — these correspond to what would be groups/rollouts/sections in UI/attribute editor.
  • - (hyphens) separate words within a single label.
  • Singular nouns when used as modifiers in compound names (English compound noun rule).
    • Example: point-grid not points-grid — “point” modifies “grid”.
  • Plain English over jargon (governing principle — R9).
    • Example field-of-view not fov.
  • Compound node type names also use hyphens: vdb-particles, output-driver, output-layer, face-set, perspective-camera, fisheye-camera, etc.
  • Example: subdivision.corner-sharpness — group “Subdivision”, label “Corner Sharpness”.

Rulings

R1: Type-first grouping for ray settings

Ray depth/length settings on the global node are grouped by ray type, since each type has multiple related attributes (depth + length):

reflection.ray-depth-max
reflection.ray-length-max
diffuse.ray-depth-max
diffuse.ray-length-max

Only use dot-separated hierarchy when there are 2 or more related attributes that form a logical group. Single standalone attributes use flat hyphenated naming:

Groups (dot-separated):

subdivision.scheme
subdivision.corner-index
subdivision.corner-sharpness
visibility.camera
visibility.diffuse
visibility.reflection

Flat (hyphen-separated):

vertex-count
hole-count
clockwise
reference-time
quadratic-motion

R3: The grouping level is context-dependent, not concept-fixed

The same concept (e.g., “reflection”) can be a group in one context and a leaf in another. The rule is: whichever level has 2+ siblings becomes the group.

On the global node — each ray type has 2+ settings (depth + length), so the ray type is the group:

reflection.ray-depth-max
reflection.ray-length-max

On the attributes node — each ray type has only ONE visibility flag, but “visibility” has 8+ flags, so visibility is the group:

visibility.reflection
visibility.diffuse
visibility.camera

The alternative (type-first everywhere: reflection.visibility, diffuse.visibility, etc.) would create 8 singleton groups, violating R2. The 2+ rule takes precedence over concept consistency.

R4: Node type is an implicit top-level group

The node type itself provides context, so attribute names should not redundantly include the node type. On a curves node, the attribute is basis, not curve-basis. On a particles node, the attribute is id, not particle-id.

R5: Rename everything, including legacy single-letter names

No grandfather clause for industry-standard abbreviations:

  • Pposition
  • Nnormal
  • nverticesvertex-count
  • nholeshole-count

Single-word names that are already clear stay as-is: width, basis, id, matte, clockwise.

R6: Hyphen-separate compound domain terms

No concatenated words. All multi-word terms get hyphens:

  • depthoffielddepth-of-field
  • fstopfocal-stop
  • focallengthfocal-length
  • focaldistancefocal-distance

This applies within both group names and leaf labels:

depth-of-field.enable
depth-of-field.focal-stop
depth-of-field.focal-length
depth-of-field.aperture.enable    ← sub-group (3 attrs: enable, sides, angle)
depth-of-field.aperture.sides
depth-of-field.aperture.angle

R7: Connection attribute plurality matches cardinality

If a connection attribute accepts multiple connections, use plural. If it accepts only one, use singular.

Plural (multi-connection):

objects              (root, transform — multiple geometry nodes)
attributes           (root, transform — multiple attribute nodes)
members              (set — multiple objects)
screens              (camera — multiple screen nodes)
output-layers        (screen — multiple layer nodes)
output-drivers       (output-layer — multiple driver nodes)

Singular (single-connection):

shader.surface       (attributes — one surface shader)
shader.displacement  (attributes — one displacement shader)
shader.volume        (attributes — one volume shader)
background-layer     (output-layer — one background layer)

R8: Group by concern, not by concept

Attributes are grouped by what kind of setting they are, not by what rendering concept they relate to. This keeps groups semantically coherent:

  • quality.* = how much effort the renderer spends (sampling counts, performance)
  • shading.* = which shading features are enabled/disabled (feature toggles)
  • {type}.* = per-ray-type limits (depth, length)
quality.shading-samples           ← sampling effort
quality.volume-samples            ← sampling effort
quality.denoise                   ← quality toggle
quality.volume-emission-sampling  ← quality toggle
quality.preview.global-update     ← preview/IPR quality
quality.preview.interpolate       ← preview/IPR quality
quality.preview.speed-multiplier  ← preview/IPR quality

shading.displacement              ← feature toggle
shading.atmosphere                ← feature toggle
shading.multiple-scattering       ← feature toggle
shading.osl-subsurface            ← feature toggle

volume.ray-depth-max              ← ray limit
volume.ray-length-max             ← ray limit

This avoids scattering quality-related attrs across concept groups (which would make it hard to find “all the knobs that affect render speed”).

R9: Plain English over jargon (governing principle)

This is a governing principle that applies across all naming decisions. When choosing between a technical abbreviation/jargon term and a plain English equivalent, always prefer plain English. Names should be understandable without domain-specific knowledge.

  • iprpreview (Interactive Progressive Rendering → just “preview”)
  • fovfield-of-view
  • fstopfocal-stop (kept because it’s the actual name of the unit, not jargon)

The group quality.ipr.* becomes quality.preview.*:

quality.preview.global-update
quality.preview.interpolate
quality.preview.speed-multiplier

R10: Use .enable suffix for booleans in mixed groups

When a boolean on/off attribute belongs to a group that also has non-boolean attrs, append .enable to distinguish the toggle from the group:

depth-of-field.enable         ← mixed group (has focal-stop, focal-length, etc.)
depth-of-field.focal-stop
depth-of-field.focal-length

cryptomatte.enable            ← mixed group (has level)
cryptomatte.level

When the group is all-toggles or the boolean is standalone, no .enable needed:

shading.displacement          ← all-toggle group, obviously a toggle
shading.atmosphere

quality.denoise               ← obviously a toggle from context

R11: Unify callbacks under callback.* group

All callback/handler function pointers across the API follow a consistent pattern: callback.{purpose} for the function pointer and callback.{purpose}.data for the associated userdata.

This applies across different API calls — the callback group is a cross-cutting convention:

# NSIBegin
callback.error               ← error handler function (was: errorhandler)
callback.error.data          ← error handler userdata (was: errorhandler.data)

# NSIRenderControl
callback.stop                ← stopped callback function (was: stoppedcallback)
callback.stop.data           ← stopped callback userdata (was: stoppedcallbackdata)

R12: Singular nouns as modifiers in compound names

When a noun serves as a modifier (adjective) in a compound name, use its singular form. This follows standard English compound noun formation: “dog house” not “dogs house”, “vertex count” not “vertices count”.

vertex-count         ✓  (not vertices-count)
hole-count           ✓  (not holes-count)
face-index           ✓  (not faces-index)
point-grid           ✓  (not points-grid)
object-index         ✓  (not objects-index)

Plurals are reserved for R7 (connection cardinality), where the attribute name IS the thing, not a modifier:

objects              ✓  (the attribute is multiple objects, not a modifier)
members              ✓
output-layers        ✓

Open Question: Node Granularity

The convention above renames attributes consistently, but it doesn’t resolve a deeper inconsistency in how ɴsɪ models primitives. Three different patterns are in play across the existing node types:

PrimitivePatternExamples
MeshesOne node, type attributemesh (polygons and Catmull-Clark via subdivision.scheme)
VolumesOne node, but only one backendvolume (renders OpenVDB exclusively)
ParticlesSplit by backendparticles, vdbparticles
CamerasSplit by projectionperspectivecamera, fisheyecamera, cylindricalcamera, sphericalcamera, orthographiccamera

mesh collapses polygons and subdivision surfaces behind a subdivision.scheme attribute. Cameras do the opposite — five separate node types that differ only in their projection function. Volumes name themselves generically while only one backend is implemented. Particles are split by the data format their control points hold, not by what the renderer sees.

Three coherent resolutions, from least to most invasive:

Option 1 — Honest names, same shape

Keep one node per concern and rename for honesty. The mapping later in this document already adopts these names:

  • volumevdb-volume (it only renders OpenVDB).
  • vdbparticlesvdb-particles (hyphenated).
  • Cameras keep their five hyphenated names (perspective-camera, …).
  • mesh stays as the one merged exception.

The inconsistency with mesh remains. This is a pure rename — no API change.

Option 2 — Collapse to one canonical primitive

Bring volumes, particles, and cameras in line with mesh’s “one node, type attribute” pattern:

  • vdb-volume and vdb-particles collapse into a single vdb node with kind = "volume" | "particles" (or distinguished by which data attribute is supplied).
  • The five camera nodes collapse into one camera node with projection = "perspective" | "fisheye" | "cylindrical" | "spherical" | "orthographic". Projection-specific attributes live behind the projection’s prefix (e.g. fisheye.mapping).

Every scene-graph entity ends up with a single canonical primitive. Migration is “rename node type, add a kind / projection attribute”. The renderer’s dispatch table has to flatten, but no user-side attribute is lost.

Option 3 — Split mesh to match the rest

Adopt the honest renames from Option 1 — volumevdb-volume, vdbparticlesvdb-particles — and additionally replace mesh with polygon-mesh and subdivision-mesh. Each mesh node carries only the attributes meaningful for its surface kind; subdivision.scheme disappears entirely.

This is the most invasive option: every existing scene using subdivision surfaces has to re-target the new node, and the subdivision.* attributes migrate from prefix-grouped on mesh to top-level on subdivision-mesh.

Trade-offs

  • Option 1 is cheapest to deliver; it preserves the inconsistency under prettier names.
  • Option 2 matches the mesh pattern. Requires backend dispatch work but no user-facing data loss.
  • Option 3 is the cleanest in isolation but invalidates the largest existing-asset footprint.

No recommendation in this draft. The decision belongs in the API roadmap, not in a renaming pass.

Open Question: ᴏsʟ Built-In Variable Alignment

ɴsɪ is designed to feed ᴏsʟ shaders. When the renderer puts the control-point position into an attribute named P on a mesh node, an ᴏsʟ shader reads it as the built-in global variable P without any plumbing in between. That 1:1 mapping is part of what makes ᴏsʟ shaders portable across renderers, and it directly conflicts with R5.

R5 currently renames the legacy single-letter attribute names:

CurrentR5 proposalᴏsʟ built-in
Ppositionpoint P
Nnormalnormal N

Adopting R5 as written means the attribute name and the ᴏsʟ global diverge. The renderer either has to translate positionP at the ᴏsʟ binding step — hidden machinery that surprises anyone debugging by attribute name — or the cross-renderer ᴏsʟ contract has to break for ɴsɪ specifically.

The ᴏsʟ globals that overlap with current ɴsɪ attribute names are: P, N, Ng, u, v, dPdu, dPdv, I.

Option A — Carve out an exception in R5

Legacy single-letter names that match an ᴏsʟ global stay as-is, even where the surrounding convention would rename them. This preserves the ɴsɪ ↔ ᴏsʟ alignment.

Affected rows revert in the rename mapping:

  • mesh: P stays.
  • nurbs: P stays. Pw stays (semantics flow into the same ᴏsʟ binding).
  • curves: P stays.
  • particles: P stays, N stays.

nvertices, nholes, clockwisewinding etc. still rename — they aren’t ᴏsʟ globals.

Option B — Rename and translate

Adopt R5 as written. The renderer translates positionP (and normalN, …) when binding attributes to ᴏsʟ shader globals.

This keeps ɴsɪ-level naming uniform but introduces hidden translation. Shader authors writing portable code now read about P in the ᴏsʟ docs and position in the ɴsɪ docs and have to internalise the mapping. Debugging tools that show attribute names won’t match what the shader sees.

Trade-off

The decision turns on which contract matters more:

  • ᴏsʟ portability (Option A) — the convention that “the attribute named P is what the shader reads as P” is sacred.
  • ɴsɪ-internal consistency (Option B) — single-letter names are jargon and R5’s reasoning applies uniformly; the ᴏsʟ binding layer can absorb the cost.

No recommendation in this draft.

Complete Attribute Mapping

Every attribute across all node types, with current → new name and the ruling(s) that apply. Attributes where current = new are omitted.

global Node

CurrentNewRules
numberofthreadsthread-countR2 (1 attr, flat), R5, R6
texturememorytexture-memoryR2 (1 attr, flat), R6
networkcache.sizenetwork-cache.sizeR6
networkcache.directorynetwork-cache.directoryR6
networkcache.mipmapnetwork-cache.mipmapR6
networkcache.writenetwork-cache.writeR6
renderatlowprioritylow-priorityR6, R9
bucketorderbucket-orderR6
hidemessagesmessages.hideR2 (2 message attrs: hide + timestamp)
maximumraydepth.diffusediffuse.ray-depth-maxR1
maximumraydepth.hairhair.ray-depth-maxR1
maximumraydepth.reflectionreflection.ray-depth-maxR1
maximumraydepth.refractionrefraction.ray-depth-maxR1
maximumraydepth.volumevolume.ray-depth-maxR1
maximumraylength.diffusediffuse.ray-length-maxR1
maximumraylength.hairhair.ray-length-maxR1
maximumraylength.reflectionreflection.ray-length-maxR1
maximumraylength.refractionrefraction.ray-length-maxR1
maximumraylength.specularspecular.ray-length-maxR1 (pattern consistency)
maximumraylength.volumevolume.ray-length-maxR1
quality.denoisequality.denoiseR8
quality.iprglobalupdatequality.preview.global-updateR8, R9
quality.iprinterpolatequality.preview.interpolateR8, R9
quality.iprspeedmultiplierquality.preview.speed-multiplierR8, R9
quality.shadingsamplesquality.shading-samplesR8, R6
quality.volumesamplesquality.volume-samplesR8, R6
quality.samplevolumeemissionquality.volume-emission-samplingR8, R6
referencetimereference-timeR6
show.displacementshading.displacementR8
show.atmosphereshading.atmosphereR8
show.multiplescatteringshading.multiple-scatteringR8, R6
show.osl.subsurfaceshading.osl-subsurfaceR8, R6
exclusiveshadingexclusive-shadingR6
messages.timestampmessages.timestamp

Unchanged: license.server, license.wait, license.hold, frame, statistics.progress, statistics.filename, verbose

root Node

CurrentNewRules
geometryattributesattributesR6, R7 (multi-conn → plural)

Unchanged: objects

set Node

Unchanged: members

mesh Node

CurrentNewRules
PpositionR5
nverticesvertex-countR5, R6
nholeshole-countR5, R6
clockwisewindingclockwiseR6 (simplification)
subdivision.cornerverticessubdivision.corner.indexR2 (3 corner attrs → sub-group)
subdivision.cornersharpnesssubdivision.corner.sharpnessR2
subdivision.smoothcreasecornerssubdivision.corner.automaticR2
subdivision.creaseverticessubdivision.crease.indexR2 (2 crease attrs → sub-group)
subdivision.creasesharpnesssubdivision.crease.sharpnessR2
referencetimereference-timeR6
quadraticmotionquadratic-motionR6
outlinecreasethresholdoutline-crease-thresholdR6

Unchanged: subdivision.scheme

nurbs Node

The u and v axes each have five related attributes (count, order, knot, min, max), so per R3 each axis becomes a group.

CurrentNewRules
nuu.countR3 (axis group), R5, R6
nvv.countR3 (axis group), R5, R6
uorderu.orderR3, R6
vorderv.orderR3, R6
uknotu.knotR3, R6
vknotv.knotR3, R6
uminu.minR3, R6
umaxu.maxR3, R6
vminv.minR3, R6
vmaxv.maxR3, R6
PpositionR5
Pwweighted-positionR5, R6 (alternative to position)
trimcurves.nloopstrim-curves.loop-countR5, R6
trimcurves.ncurvestrim-curves.curve-countR5, R6
trimcurves.ntrim-curves.point-countR5 (n and cv are jargon), R9
trimcurves.ordertrim-curves.orderR6 (group prefix only)
trimcurves.knottrim-curves.knotR6
trimcurves.mintrim-curves.minR6
trimcurves.maxtrim-curves.maxR6
trimcurves.u, .vtrim-curves.positionAPI change (consolidation); mirrors surface
trimcurves.u, .v, .wtrim-curves.weighted-positionAPI change (consolidation); mirrors surface
trimcurves.sensetrim-curves.senseR6

Consolidation note (API change, not pure rename). The current API stores trim-curve control points as three parallel arrays (trimcurves.u, trimcurves.v, trimcurves.w) — a structure-of-arrays layout. The surface stores its control points as one interleaved array (P or Pw) — an array-of-structures layout. The redesign aligns the two:

  • trim-curves.positionfloat[2], non-rational (u, v) pairs. Replaces trimcurves.u and trimcurves.v.
  • trim-curves.weighted-positionfloat[3], rational (u, v, w) triples. Replaces trimcurves.u, trimcurves.v, and trimcurves.w.

Supply one of the two; never both. This is the same supply-one-of pattern the surface uses for position / weighted-position.

face-set Node

CurrentNewRules
facesface-indexR9 (descriptive)

curves Node

CurrentNewRules
nverticesvertex-countR5, R6
PpositionR5

Unchanged: width, basis, extrapolate

particles Node

CurrentNewRules
PpositionR5
NnormalR5
reverseorientationreverse-orientationR6
quadraticmotionquadratic-motionR6

Unchanged: width, id

procedural Node

CurrentNewRules
boundingboxbounding-boxR6

environment Node

Unchanged: angle

shader Node

CurrentNewRules
shaderfilenamefilenameR4 (node type provides context)
shaderobjectobjectR4

attributes (geometry) Node

CurrentNewRules
surfaceshadershader.surfaceR2, R7 (single-conn → singular)
displacementshadershader.displacementR2, R7
volumeshadershader.volumeR2, R7
visibility.set.subsurfacevisibility.subsurface-setR6
regularemissionemission.regularR2 (2 emission attrs → group)
quantizedemissionemission.quantizedR2

Unchanged: ATTR.priority, visibility.camera, visibility.diffuse, visibility.hair, visibility.reflection, visibility.refraction, visibility.shadow, visibility.specular, visibility.volume, visibility, matte, bounds

transform Node

CurrentNewRules
transformationmatrixmatrixR4
geometryattributesattributesR6, R7
shaderattributesshader-attributesR6

Unchanged: objects

instances Node

CurrentNewRules
sourcemodelsobjectsR7 (multi-conn), R9
transformationmatricesmatricesR4, R6
modelindicesobject-indexR6, R9
disabledinstancesdisabled-indexR6

output-driver Node

CurrentNewRules
drivernamedriver-nameR6
imagefilenamefilenameR4
embedstatisticsembed-statisticsR6

output-layer Node

CurrentNewRules
variablenamevariable-nameR6
variablesourcevariable-sourceR6
layernamelayer-nameR6
scalarformatscalar-formatR6
layertypelayer-typeR6
colorprofilecolor-profileR6
withalphawith-alphaR6
sortkeysort-keyR6
lightsetlight-setR6
lightsetnamelight-set-nameR6
outputdriversoutput-driversR6, R7
filterwidthfilter.widthR2 (2 filter attrs → group)
filterfilter.nameR2
backgroundvaluebackground.valueR2 (2 background attrs → group)
backgroundlayerbackground.layerR2, R7 (single-conn)
lightdepthlight-depthR6

Unchanged: dithering, cryptomatte.enable, cryptomatte.level

screen Node

CurrentNewRules
outputlayersoutput-layersR6, R7
prioritywindowpriority-windowR6
screenwindowscreen-windowR6
pixelaspectratiopixel-aspect-ratioR6
staticsamplingpatternstatic-sampling-patternR6
importancesamplefilterimportance-sample-filterR6

Unchanged: resolution, oversampling, crop, overscan

vdb-particles Node

CurrentNewRules
vdbfilenamefilenameR4
pointsgridpoint-gridR6, R12
velocityreferencetimevelocity.reference-timeR2 (2 velocity attrs → group)
velocityscalevelocity.scaleR2
enablepscaleuse-point-scaleR9 (plain English)
widthscalewidth-scaleR6

Unchanged: width

volume Node

CurrentNewRules
vdbfilenamefilenameR4
densitygridgrid.densityR2 (6 grid attrs → group)
colorgridgrid.colorR2
emissiongridgrid.emissionR2
emissionintensitygridgrid.emission-intensityR2, R6
temperaturegridgrid.temperatureR2
velocitygridgrid.velocityR2
velocityreferencetimevelocity.reference-timeR2
velocityscalevelocity.scaleR2

Camera Nodes (perspective-camera, fisheye-camera, cylindrical-camera)

Common (all cameras):

CurrentNewRules
screensscreensR7
shutterrangeshutter.rangeR2 (2 shutter attrs → group)
shutteropeningshutter.openingR2
clippingrangeclipping-rangeR6

perspective-camera:

CurrentNewRules
fovfield-of-viewR9
depthoffield.enabledepth-of-field.enableR6, R10
depthoffield.fstopdepth-of-field.focal-stopR6
depthoffield.focallengthdepth-of-field.focal-lengthR6
depthoffield.focallengthratiodepth-of-field.focal-length-ratioR6
depthoffield.focaldistancedepth-of-field.focal-distanceR6
depthoffield.aperture.enabledepth-of-field.aperture.enableR6, R10
depthoffield.aperture.sidesdepth-of-field.aperture.sidesR6
depthoffield.aperture.angledepth-of-field.aperture.angleR6
unitlengthmillimetersunit-length-millimetersR6

fisheye-camera:

CurrentNewRules
fovfield-of-viewR9

Unchanged: mapping

cylindrical-camera:

CurrentNewRules
fovfield-of-view.verticalR2 (2 fov attrs → group), R9
horizontalfovfield-of-view.horizontalR2, R6, R9
eyeoffseteye-offsetR6

API Parameter Mapping

NSIBegin

CurrentNewRules
streamfilenamestream.filenameR2 (4 stream attrs → group)
streamformatstream.formatR2
streamcompressionstream.compressionR2
streampathreplacementstream.path-replacementR2, R6
separateprocessseparate-processR6
errorhandlercallback.errorR11 (unified callback group)
errorhandler.datacallback.error.dataR11
executeproceduralsevaluate-replaceR9

Unchanged: type

NSIDelete

Unchanged: recursive

NSIConnect

Unchanged: value, priority, strength

NSIEvaluate

CurrentNewRules
backgroundloadbackground-loadR6

Unchanged: type, filename, script, buffer, size

NSIRenderControl

CurrentNewRules
stoppedcallbackcallback.stopR11 (unified callback group)
stoppedcallbackdatacallback.stop.dataR11

Unchanged: action, progressive, interactive, frame

Open question — action as a positional argument: Every meaningful NSIRenderControl call needs an action value — one of start, wait, synchronize, suspend, resume, stop. The call is a no-op without it. Yet the current signature carries action inside the optional-parameter bag, peer to the genuinely-optional progressive / interactive / callback parameters.

Promoting action to a required positional argument would make intent visible at the call site:

typedef enum {
    NSIRenderStart,
    NSIRenderWait,
    NSIRenderSynchronize,
    NSIRenderSuspend,
    NSIRenderResume,
    NSIRenderStop,
} NSIRenderAction;

void NSIRenderControl(
    NSIContext_t        ctx,
    NSIRenderAction     action,
    int                 nparams,
    const NSIParam_t   *params);

A string overload can be kept for the Lua and Python bindings, where "start" / "wait" / "stop" read idiomatically. The enum form catches typos at compile time and surfaces the closed set of allowed values to IDE completion.

If adopted, action leaves this rename table entirely — there is nothing left to rename.