This puts one of the most advanced 3D production offline renderers at your fingertips in Rust – 3Delight.
The Moana Island Scene,
provided courtesy of Walt Disney Pictures, rendered with 3Delight|ɴsɪ.
This is a huge scene (72GB of data) made of 31 million instances, 78 million polygons defining subdivision surface geometry and 2,300 Ptex textures. The above image was rendered in less than two minutes (wall time) using 3Delight Cloud.
A flexible, modern API for offline 3D renderers
Nsɪ is built around the concept of nodes. Each node has a unique handle to identify it. It also has 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.). The standard ɴsɪ attribute names are exposed as typed constants -- see Typed attribute names below.
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. These are attached to a rendering
Context. -
Nodes 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.
// Create a context to send the scene to.
let ctx = nsi::Context::new(None).expect("Could not create NSI context.");
// Create a dodecahedron.
// 12 regular pentagon faces.
let face_index: [i32; 60] = [
0, 16, 2, 10, 8, 0, 8, 4, 14, 12, 16, 17, 1, 12, 0, 1, 9, 11, 3, 17, 1,
12, 14, 5, 9, 2, 13, 15, 6, 10, 13, 3, 17, 16, 2, 3, 11, 7, 15, 13, 4,
8, 10, 6, 18, 14, 5, 19, 18, 4, 5, 19, 7, 11, 9, 15, 7, 19, 18, 6,
];
// Golden ratio.
let phi: f32 = 0.5 * (1.0 + 5_f32.sqrt());
// Golden ratio conjugate.
let phi_c: f32 = phi - 1.0;
// 20 control points, each a Point3F32 ([f32; 3]).
// Length divisibility by 3 is enforced at the type level.
let positions: [nsi::Point3F32; 20] = [
[1., 1., 1.],
[1., 1., -1.],
[1., -1., 1.],
[1., -1., -1.],
[-1., 1., 1.],
[-1., 1., -1.],
[-1., -1., 1.],
[-1., -1., -1.],
[0., phi_c, phi],
[0., phi_c, -phi],
[0., -phi_c, phi],
[0., -phi_c, -phi],
[phi_c, phi, 0.],
[phi_c, -phi, 0.],
[-phi_c, phi, 0.],
[-phi_c, -phi, 0.],
[phi, 0., phi_c],
[phi, 0., -phi_c],
[-phi, 0., phi_c],
[-phi, 0., -phi_c],
];
// Create a new mesh node and call it 'dodecahedron'.
ctx.create("dodecahedron", nsi::MESH, None);
// Connect the 'dodecahedron' node to the scene's root.
ctx.connect("dodecahedron", None, nsi::ROOT, "objects", None);
// Define the geometry of the 'dodecahedron' node.
ctx.set_attribute(
"dodecahedron",
&[
// Typed name: `nsi::POSITION` is `Attribute<[nsi::Point3F32]>`.
// Wrong-shape data (e.g. a `&[f32]`) is rejected by rustc at
// this call site.
nsi::point_slice!(nsi::POSITION, &positions),
nsi::i32_slice!("P.indices", &face_index),
// 5 vertices per each face.
nsi::i32_slice!("nvertices", &[5; 12]),
// Render this as a subdivison surface.
nsi::string!("subdivision.scheme", "catmull-clark"),
// Crease each of our 30 edges a bit.
nsi::i32_slice!("subdivision.creasevertices", &face_index),
nsi::f32_slice!("subdivision.creasesharpness", &[10.; 30]),
],
);These can be found in the examples
folder.
All the examples in this crate require a (free) 3Delight installation to run!
Demonstrates using the FnStatus callback closure during rendering and a
channel for communicating between main- and rendering thread(s).
Render directly into a Jupyter notebook.
Follow these instructions to get a Rust Jupyter kernel up and running first.
This is a full output example showing color conversion and writing data
out to 8bit/channel PNG and 32bit/channel (float) OpenEXR formats.
Demonstrates rendering an OpenVDB asset. Mostly
through the toolbelt helpers.
The nsi crate is a facade. The work is split across smaller crates so
consumers can pick the layer they need:
nsi-trait-- pure-Rust trait crate. DefinesNsi(selfis the context), theAttributetyped-name machinery,NodeType,Action, and the standard node-type / attribute-name constants. No FFI deps.nsi-ffi-wrap-- FFI wrapper. ProvidesContext, the C-API loader (dynamic viadlopen2or static vialink_lib3delight), the parameter macros (f32!,point_slice!, …), andFfiApiAdapterwhich exposes any pure-RustNsiimpl through the C API (handle-mapping is internal, via a factory closure).nsi-3delight-- helpers for the 3Delight renderer (environment lights, shader graphs, etc.).nsi-toolbelt-- convenience scene-construction helpers (handle generation, append/prepend, transform shortcuts).nsi-jupyter-- render into a Jupyter notebook.nsi-sys-- auto-generated bindings for the NSI C header.nsi-procedural-- scaffolding for writing procedural-node plugins.
The nsi crate re-exports everything from nsi-ffi-wrap (which itself
pulls types and constants from nsi-trait) so a typical user only needs
to depend on nsi.
The standard ɴsɪ attribute names are exposed as typed constants of
Attribute<T>, where T describes the data shape the attribute
accepts. Examples:
Attribute<f32> -- e.g. nsi::FIELD_OF_VIEW
Attribute<i32> -- e.g. nsi::U_COUNT, nsi::U_ORDER
Attribute<[f32]> -- e.g. nsi::U_KNOT, nsi::TRIM_CURVES_KNOT
Attribute<[Point3F32]> -- nsi::POSITION (length always divisible by 3)
Attribute<[Point4F32]> -- nsi::WEIGHTED_POSITION (rational xyzw points)
Attribute<Matrix4F64> -- nsi::MATRIX
The Rust constant identifiers are derived from the new ɴsɪ naming
convention (see the naming-convention.md chapter in the ɴsɪ spec). The
wire-side string literals each constant points to currently still hold
the legacy names ("fov", "nu", "transformationmatrix", …) so the
constants work against today's renderers; switching to the new wire
names is a one-line change per constant when the renderer ships them.
Renderer-specific or experimental attributes are added in their own
crates without touching this one -- Attribute::new("custom_name") is
const, so consumers declare their own typed constants.
Note: the parameter macros (nsi::f32!, nsi::point_slice!, …)
currently accept the wire-side string literal directly; static
verification against Attribute<T> is in progress.
The crate has support for streaming pixels from the renderer, via callbacks
(i.e. closures) during and/or after rendering via the output module.
This module is enabled through the feature of the same name (see below).
It should be straightforward to create an async implementation with this
or use channels to stream pixels back to a main thread (see the
interactive example).
-
output-- Add support for streaming pixels from the renderer to the calling context via closures. -
jupyter-- Add support for rendering to Jupyter notebooks (when using a Rust Jupyter kernel). -
toolbelt-- Add convenience methods that work with aContext. -
delight-- Add some nodes & shaders specific to 3Delight. -
nightly-- Enable some unstable features (suggested if you build with anightlytoolchain) -
ustr_handles-- useustrfor node handles. This will give a you a speed boost if your node names aren't changing while an app using ɴsɪ is running but is not advised otherwise (ustrare never freed).
The 3Delight dynamic library (lib3delight) can either be linked to during
build or loaded at runtime.
By default the lib is loaded at runtime.
-
Load
lib3delighat runtime (default). This has several advantages:-
If you ship your application or library you can ship it without the library. It can still run and will print an informative error if the library cannot be loaded.
-
A user can install an updated version of the renderer and stuff will ‘just work’.
-
-
Dynamically link against
lib3delight.-
lib3delightbecomes a dependency. If it cannot be found your lib/app will not load/start. -
The feature is called
link_lib3delight. -
You should disable default features (they are not needed/used) in this case:
[dependencies] nsi = { version = "0.7", default-features = false, features = ["link_lib3delight"] }
-
-
Download
lib3delightduring build.-
lib3delightis downloaded during build. Note that this may be an outdated version. This feature mainly exists for CI purposes. -
The feature is called
download_lib3delight.
-
Also check out my Diffusion Limited Aggregation play-thingy or Polyhedron Operators for more example code (demonstrates render settings, sending meshes to the renderer, instancing, particle rendering, OSL shaders, environment (lights) and dumping a scene description to disk).
PRs are most welcome!
Before you start it is suggested that you download a 3Delight package for your platform & install it (supported: Linux, macOS, Windows). This will allow you to create pretty pictures with the examples straight away.
It will also install 3Delight Display which you can render to as an alternative to writing images to disk. When working with this crate this is quite handy.
You can skip this step and build with the download_lib3delight feature.
However, this will download an older version of 3Delight so this is
not suggested.
I hang out on the 3Delight Discord (I
have the same user name as on GitHub). Ping me in the #lobby channel
or send me a PM.
There is also a 3Delight Slack which has a dedicated, invitation only channel about ɴsɪ.
If you have more advanced questions or want to add support for the ɴsɪ API/export to ɴsɪ to your renderer/DCC app/whatever ping me and I get you an invite.