How the Scene::load_node System Parses and Creates Rendering Components in LuisaRender

The Scene::load_node system in LuisaRender uses a two-stage pipeline where SceneParserJSON first converts JSON scene files into a directed acyclic graph of SceneNodeDesc descriptors, then Scene factory methods recursively instantiate concrete rendering components with memoization to handle shared references.

LuisaRender is a next-generation open-source rendering framework that uses JSON-encoded scene descriptions to define complex rendering pipelines. The Scene::load_node system serves as the critical bridge between these human-readable descriptors and the high-performance C++ objects—textures, shapes, surfaces, and lights—used during ray tracing.

The Two-Step Pipeline from JSON to Rendering Objects

The load-node architecture separates scene construction into distinct parsing and instantiation phases. This separation allows the renderer to resolve references, handle imports, and optimize object reuse before any heavy rendering resources are allocated.

Step 1: Parsing with SceneParserJSON

The SceneParserJSON class in src/sdl/scene_parser_json.cpp handles the initial ingestion of scene files. At line 27, it uses json::parse to convert raw JSON text into a nlohmann::json object【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L22-L30】.

The parser then processes the JSON structure through several specialized methods:

  • define_root (lines 58-64): Creates the top-level render node with the ROOT tag【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L58-L64】
  • define (lines 65-80): Processes global nodes that require "type" and "impl" fields, with optional "base" inheritance【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L65-L80】
  • _parse_import (lines 52-57): Dispatches external scene file imports to a thread pool for concurrent parsing【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L52-L57】

Step 2: Instantiation with Scene Loaders

Once the SceneNodeDesc graph is constructed, the Scene class in src/sdl/scene.h and src/sdl/scene.cpp executes the second phase. Through its load_* helper methods—load_texture, load_shape, load_surface, load_sampler, and load_light—the scene recursively converts descriptors into concrete rendering objects.

Each loader follows a consistent pattern: check an internal memoization map (_objects) for existing instances, dispatch to a factory based on the node's implementation string, and recursively load child components.

Parsing Scene Descriptors with SceneParserJSON

The parsing phase transforms JSON key-value pairs into a directed acyclic graph of SceneNodeDesc objects defined in src/sdl/scene_node_desc.h. Each node maintains an identifier, a type tag (TEXTURE, SHAPE, SURFACE, etc.), an implementation descriptor, and a property list.

Node Definition and Property Parsing

The _parse_node method (lines 34-124 in scene_parser_json.cpp) processes individual JSON objects recursively. It handles three property categories:

  • Scalars: Strings, numbers, and booleans added via add_property
  • Arrays: Converted to reference lists, internal node arrays, or scalar vectors depending on content
  • Inline objects: Processed through parse_internal to create sub-nodes with their own "impl" and optional "prop" fields

References and Internal Nodes

The parser resolves node relationships through two mechanisms:

References (@name syntax): When a property value starts with @, the _reference method (lines 47-50) resolves it through SceneDesc::reference, creating links to previously defined nodes【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L47-L50】.

Internal Nodes: The define_internal method (lines 48-52) creates derived nodes that inherit from base nodes, allowing composition patterns where a node extends another's properties【/cache/repos/github.com/luisagroup/luisarender/next/src/sdl/scene_parser_json.cpp#L48-L52】.

Instantiating Components via Scene::load_* Methods

The Scene class serves as the object factory, transforming the descriptor graph into executable rendering components. Located in src/sdl/scene.h and src/sdl/scene.cpp, these loaders implement a memoized factory pattern.

Factory Dispatch and Memoization

Each load_* method follows a three-step workflow:

  1. Lookup: Check the _objects map to see if the descriptor has already been instantiated
  2. Factory dispatch: Switch on parse_scene_node_tag(desc->type()) or the implementation string to select the concrete class
  3. Construction: Instantiate the object, which may recursively call other load_* methods to resolve dependencies

This memoization ensures that shared sub-graphs—such as textures referenced by multiple materials—result in a single runtime object, conserving memory and maintaining consistency.

Recursive Loading Pattern

Concrete component classes receive a Scene& and SceneNodeDesc const* in their constructors, immediately invoking load routines to fetch dependencies. For example, in src/textures/swizzle.cpp:

class SwizzleTexture final : public Texture {
private:
    const Texture *_base;
public:
    SwizzleTexture(Scene &scene, const SceneNodeDesc *desc) noexcept
        : Texture{scene, desc},
          _base{scene->load_texture(desc->property_node("base"))} {}
    // …
};

Here, desc->property_node("base") extracts the child descriptor, and scene->load_texture() either retrieves an existing instance or creates a new one, propagating the recursive loading chain.

Complete Example: From JSON to C++ Objects

Consider a typical scene descriptor that defines a textured mesh:

{
  "my_render": {
    "type": "render",
    "impl": "path_tracer",
    "prop": {
      "camera": {
        "type": "camera",
        "impl": "pinhole",
        "prop": {
          "film": { "type": "film", "impl": "rgb", "prop": { "resolution": [800,600] } }
        }
      },
      "world": {
        "type": "shape",
        "impl": "mesh",
        "prop": {
          "file": "models/teapot.obj",
          "material": {
            "type": "surface",
            "impl": "plastic",
            "prop": {
              "Kd": { "type": "texture", "impl": "image", "prop": { "filename": "textures/white.png" } }
            }
          }
        }
      }
    }
  }
}

The parsing phase creates a SceneNodeDesc graph where my_render is the root, containing child nodes for the camera, film, mesh, plastic surface, and image texture. When Scene instantiation begins, the following C++ objects are created:

JSON Node C++ Class Instantiated Loader Method
my_render RenderNode (root) Scene constructor
camera PinholeCamera Scene::load_camera
film RGBFilm Scene::load_film
world (mesh) Mesh Scene::load_shape
material (plastic) PlasticSurface Scene::load_surface
Kd (image) ImageTexture Scene::load_texture

The ImageTexture is instantiated once, even if referenced multiple times, thanks to the memoization in Scene::load_texture.

Extending the System with Custom Components

Developers can add new rendering components by implementing the two-phase contract:

  1. Create the C++ class inheriting from the appropriate base (e.g., Texture in src/textures/my_texture.cpp)
  2. Register in the factory by adding a case to the corresponding Scene::load_* method:
// In src/sdl/scene.cpp (within Scene::load_texture)
if (impl == "my_custom") {
    return new MyCustomTexture(*this, desc);
}
  1. Use in JSON:
{
  "my_tex": { 
    "type": "texture", 
    "impl": "my_custom",
    "prop": { "custom_param": 1.0 } 
  }
}

The parser automatically handles the "impl" dispatch, and the loader constructs your class with the descriptor node.

Summary

  • Two-phase architecture: SceneParserJSON converts JSON into a SceneNodeDesc graph, then Scene loaders instantiate concrete objects.
  • Parser details: Located in src/sdl/scene_parser_json.cpp, handles imports, references (@name), internal nodes, and property recursion.
  • Factory pattern: Scene::load_texture, Scene::load_shape, and similar methods in src/sdl/scene.cpp provide memoized instantiation.
  • Recursive construction: Component constructors receive Scene& and SceneNodeDesc*, calling loaders to resolve dependencies.
  • Extensibility: New component types require C++ implementation and registration in the appropriate Scene::load_* factory switch.

Frequently Asked Questions

What is the difference between SceneNodeDesc and the actual rendering component?

SceneNodeDesc is an intermediate descriptor class defined in src/sdl/scene_node_desc.h that stores the raw parsed data from JSON—essentially a node in a graph containing type tags, implementation strings, and property values. The actual rendering component (like ImageTexture or Mesh) is a concrete C++ class instantiated by Scene::load_* methods that performs the actual rendering computations. The descriptor is the blueprint; the component is the constructed object.

How does the Scene::load_node system handle shared components or circular references?

The system handles shared components through memoization implemented in the Scene class's _objects map. When Scene::load_texture or any other loader is called, it first checks if that SceneNodeDesc has already been instantiated. If so, it returns the existing pointer, ensuring that multiple references to the same node (using the @name syntax) result in a single shared object. The parser constructs a directed acyclic graph (DAG) of descriptors, which prevents circular references at the descriptor level before instantiation begins.

Can I use scene description formats other than JSON with LuisaRender?

The current implementation in the next branch provides SceneParserJSON specifically for JSON-encoded scene files located in src/sdl/scene_parser_json.cpp. However, the architecture is designed to support additional formats. You could implement a new parser class (e.g., SceneParserXML or SceneParserYAML) that produces the same SceneNodeDesc graph structure. As long as your parser populates the descriptor graph correctly, the existing Scene::load_* machinery will instantiate components without requiring changes to the loading logic.

What happens if a required property is missing from a node descriptor?

During the instantiation phase, concrete component constructors call methods like desc->property_node("base") or desc->property("filename") to retrieve required values. If a property is missing, these methods typically throw exceptions or return null pointers depending on the specific implementation in src/sdl/scene_node_desc.h. The Scene loader catches these errors during construction, preventing the creation of invalid rendering objects. At parse time, the JSON schema validation in SceneParserJSON ensures that nodes contain the required "type" and "impl" fields, catching structural errors before instantiation begins.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →