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

> Explore how LuisaRender's Scene::load_node system parses JSON into SceneNodeDesc descriptors and recursively creates rendering components with memoization for efficient shared reference handling.

- Repository: [LuisaGroup/luisarender](https://github.com/luisagroup/luisarender)
- Tags: internals
- Published: 2026-03-06

---

**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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/src/sdl/scene.h) and [`src/sdl/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/src/sdl/scene.h) and [`src/sdl/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/src/textures/swizzle.cpp):

```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:

```json
{
  "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`](https://github.com/luisagroup/luisarender/blob/main/src/textures/my_texture.cpp))
2. **Register in the factory** by adding a case to the corresponding `Scene::load_*` method:

```cpp
// In src/sdl/scene.cpp (within Scene::load_texture)
if (impl == "my_custom") {
    return new MyCustomTexture(*this, desc);
}

```

3. **Use in JSON**:

```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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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`](https://github.com/luisagroup/luisarender/blob/main/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.