# How to Create Custom BSDF Surfaces for New Material Types in LuisaRender

> Learn to create custom BSDF surfaces for new material types in LuisaRender. Implement Scene Graph Surface, Instance, and Closure classes for advanced material creation.

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

---

**To create custom BSDF surfaces in LuisaRender, you must implement a Surface class for scene graph integration, an Instance class for interaction handling, and a Closure class containing the actual BSDF evaluation logic using LuisaRender's DSL.**

LuisaRender models every material as a *surface* comprising two distinct components: a **Surface** class that lives in the scene graph and a **Closure** class that performs BSDF evaluation during shading. This architecture separates material properties from shading computations, enabling efficient GPU execution through LuisaRender's domain-specific language (DSL). The following guide walks through the complete implementation process using the actual source structure from the `luisagroup/luisarender` repository.

## Understanding the Surface-Closure Architecture

Before writing code, it is essential to understand how LuisaRender separates material representation from shading logic.

The **Surface** class (defined in [`src/base/surface.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/surface.h)) acts as a factory and metadata container. It describes the material's capabilities—whether it is reflective, transmissive, or thin—and constructs the runtime instances used during rendering.

The **Closure** class contains the actual BSDF implementation. It executes on the GPU using LuisaRender's DSL (with types like `Expr<float3>` and functions like `dot()`, `normalize()`, and `cosine_sample_hemisphere()`). The Closure implements `_evaluate()` for BSDF value and PDF computation, and `_sample()` for importance sampling directions.

## Step-by-Step Implementation Guide

### Step 1: Create a New Surface Class

Create a class that inherits from `luisa::render::Surface`. This class defines the material's properties and constructs its runtime instance.

Key methods to implement:
- `properties()`: Return a bitmask of capabilities (e.g., `property_reflective`, `property_transmissive`).
- `_build()`: Construct and return the Surface's Instance object.

```cpp
class MyLambertSurface final : public Surface {
public:
    explicit MyLambertSurface(Scene *scene, const SceneNodeDesc *desc) noexcept
        : Surface{scene, desc} {}

    [[nodiscard]] uint properties() const noexcept override {
        return property_reflective;  // This material reflects light
    }

protected:
    [[nodiscard]] luisa::unique_ptr<Instance> _build(
        Pipeline &pipeline,
        CommandBuffer &command_buffer) const noexcept override;
};

```

### Step 2: Define the Surface Instance

The Instance class (derived from `Surface::Instance`) bridges the Surface and Closure. It prepares the interaction data required for shading.

Critical method:
- `populate_closure()`: Copy geometry and shading frame information into the closure before evaluation.

```cpp
class MyLambertSurface::Instance final : public Surface::Instance {
public:
    using Surface::Instance::Instance;  // Inherit constructor

    void populate_closure(
        Surface::Closure *closure,
        const Interaction &it,
        Expr<float3> wo,
        Expr<float> eta_i) const noexcept final {
        // Forward interaction data to the closure base
        closure->populate_closure_base(it, wo, eta_i);
    }
};

```

### Step 3: Implement the BSDF Closure

The Closure class contains the actual shading logic. It must implement `_evaluate()` and `_sample()` using LuisaRender's DSL.

```cpp
class MyLambertClosure final : public Surface::Closure {
public:
    MyLambertClosure(const Pipeline &pipeline,
                     const SampledWavelengths &swl,
                     Expr<float> time) noexcept
        : Surface::Closure{pipeline, swl, time} {}

private:
    // Evaluate BSDF value and PDF
    [[nodiscard]] Evaluation _evaluate(
        Expr<float3> wo, Expr<float3> wi,
        TransportMode mode) const noexcept final {
        auto eval = Evaluation::zero(swl().dimension());
        // Lambertian BRDF: albedo / π
        eval.f = SampledSpectrum{swl().dimension(), 1.0_f} / pi<float>;
        eval.pdf = fabs(dot(it().shading().n(), wi)) / pi<float>;
        return eval;
    }

    // Importance sampling
    [[nodiscard]] Sample _sample(
        Expr<float3> wo,
        Expr<float> u_lobe, Expr<float2> u,
        TransportMode mode) const noexcept final {
        Sample sample = Sample::zero(swl().dimension());
        // Cosine-weighted hemisphere sampling
        sample.wi = cosine_sample_hemisphere(u);
        sample.wi = it().shading().local_to_world(sample.wi);
        sample.eval = _evaluate(wo, sample.wi, mode);
        sample.event = Surface::event_reflect;
        return sample;
    }
};

```

### Step 4: Register the Surface Type

To make the material available in scene files, register it in the scene node factory. In [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp), locate the `SceneNodeFactory` switch and add:

```cpp
case hash("my_lambert"):
    return luisa::make_unique<MyLambertSurface>(scene, desc);

```

This maps the string `"my_lambert"` used in scene descriptions to your C++ class.

### Step 5: Add Build System Configuration

Finally, add your new source files to [`src/surfaces/CMakeLists.txt`](https://github.com/luisagroup/luisarender/blob/main/src/surfaces/CMakeLists.txt):

```cmake
target_sources(luisa_render PRIVATE
    surfaces/my_lambert.cpp
    # ... other surface sources ...

)

```

## Key Files and Utilities for BSDF Development

When implementing custom BSDF surfaces, reference these core files in the `luisagroup/luisarender` repository:

| File | Purpose |
|------|---------|
| [`src/base/surface.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/surface.h) | Defines the `Surface`, `Surface::Instance`, and `Surface::Closure` base classes. |
| [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp) | Contains the `SceneNodeFactory` where you register new surface types. |
| [`src/util/scattering.h`](https://github.com/luisagroup/luisarender/blob/main/src/util/scattering.h) | Provides sampling utilities like `cosine_sample_hemisphere` and `uniform_sample_hemisphere`. |
| [`src/util/spec.h`](https://github.com/luisagroup/luisarender/blob/main/src/util/spec.h) | Defines `SampledSpectrum` and spectral operations for BSDF evaluation. |
| [`src/surfaces/plastic.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/surfaces/plastic.cpp) | Reference implementation showing a complete BSDF with roughness and specular components. |
| [`src/surfaces/matte.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/surfaces/matte.cpp) | Simple diffuse reference implementation similar to the Lambertian example above. |

## Summary

Creating custom BSDF surfaces for new material types in LuisaRender requires implementing three interconnected classes:

- **Surface**: Defines material properties and constructs the runtime instance via `_build()`.
- **Instance**: Prepares shading data through `populate_closure()` and handles opacity queries.
- **Closure**: Executes the BSDF algorithm on the GPU using `_evaluate()` and `_sample()` with LuisaRender's DSL.

After implementing these classes, register the new material in [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp) and update [`src/surfaces/CMakeLists.txt`](https://github.com/luisagroup/luisarender/blob/main/src/surfaces/CMakeLists.txt) to include the new source files.

## Frequently Asked Questions

### What is the difference between Surface and Closure in LuisaRender?

The **Surface** class acts as a host-side factory and metadata container that lives in the scene graph, while the **Closure** class contains the actual BSDF implementation that executes on the GPU during shading. The Surface creates an Instance that bridges to the Closure, which performs the actual light transport calculations using LuisaRender's DSL.

### How do I register a new material type so it can be used in scene files?

Register your material by adding a case to the `SceneNodeFactory` switch in [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp). Map a string identifier (e.g., `"my_material"`) to your Surface class's static `create` method or constructor using `luisa::make_unique`. This allows scene files to instantiate your material by that name.

### Can I reuse existing surface wrappers like opacity or normal mapping with my custom BSDF?

Yes. LuisaRender's architecture allows you to reuse existing surface wrappers such as opacity masks, normal maps, and two-sided shading with your custom BSDF. These wrappers typically operate on the Surface or Instance level, modifying the interaction data before it reaches your Closure's `_evaluate()` and `_sample()` methods.

### What DSL functions are available for implementing BSDF evaluation?

LuisaRender provides a rich DSL for BSDF implementation including vector math functions (`dot()`, `normalize()`, `cross()`, `make_float3()`), sampling utilities (`cosine_sample_hemisphere()`, `uniform_sample_hemisphere()` from [`src/util/scattering.h`](https://github.com/luisagroup/luisarender/blob/main/src/util/scattering.h)), and spectral types (`SampledSpectrum`, `Expr<float>`). The DSL operates on GPU expressions (`Expr<T>`) rather than immediate values, allowing the compiler to generate efficient GPU kernels.