# How Volumetric Rendering Works with Media and Phase Functions in LuisaRender

> Discover how volumetric rendering works with media and phase functions in LuisaRender. Learn about optical properties and nested media handling for advanced path tracing.

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

---

**LuisaRender implements volumetric rendering through a Medium abstraction that manages optical properties and phase functions, using a MediumTracker to handle nested media during path tracing.**

Volumetric rendering in LuisaRender treats participating media such as fog, smoke, and liquids as first-class objects sampled during rendering exactly like surfaces. The system combines optical property evaluation, angular scattering distributions, and nested medium tracking to produce physically accurate light transport through participating media according to the luisagroup/luisarender source code.

## Core Concepts: Medium, PhaseFunction, and MediumTracker

### The Medium Base Class and Closure Interface

The `Medium` abstract base class in [`src/base/medium.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.h) defines the interface for volumetric interactions. It creates a per-ray **Closure** that encapsulates optical properties including absorption coefficient (`σₐ`), scattering coefficient (`σₛ`), and emission (`Lₑ`). The closure also holds a reference to the **PhaseFunction** governing angular scattering distributions.

Key implementation details from [`src/base/medium.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.h) (lines 25-55) show the `Medium` class providing the `build` method that constructs medium instances with texture handles for spatially varying properties.

### Phase Functions and Angular Scattering

Phase functions describe the probability distribution of scattering directions. In [`src/phasefunctions/henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/phasefunctions/henyey_greenstein.cpp), the Henyey-Greenstein phase function implements the standard anisotropic scattering model used by default in LuisaRender.

The `PhaseFunction` interface requires three methods:
- `p(wo, wi)` – evaluates the phase function value
- `sample_p(wo, u)` – importance-samples a scattering direction
- `pdf(wo, wi)` – returns the probability density

The implementation (lines 28-48 in [`henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/henyey_greenstein.cpp)) computes the phase function value and samples a direction based on the asymmetry parameter `g`, where `g = 0` represents isotropic scattering, `g > 0` forward scattering, and `g < 0` backward scattering.

### Tracking Nested Media with MediumTracker

The `MediumTracker` class in [`src/util/medium_tracker.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/medium_tracker.cpp) manages nested participating media using a priority-ordered stack. When rays traverse geometry boundaries, the tracker records enter and exit events to determine the current active medium.

Key operations include:
- `enter(priority, info)` – pushes a medium onto the stack (line 51 in [`mega_volume_path.cpp`](https://github.com/luisagroup/luisarender/blob/main/mega_volume_path.cpp))
- `exit(priority, info)` – removes a medium from the stack (lines 55-58)
- `current()` – returns the active medium tag for closure evaluation

The tracker ensures correct handling of overlapping media by maintaining the highest-priority medium at the top of the stack. The `vacuum()` method indicates whether the ray is in empty space.

## Sampling Media in the Rendering Pipeline

### Homogeneous Medium Implementation

Concrete medium implementations specialize the abstract interface. The `HomogeneousMedium` in [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp) represents uniform participating media with constant optical properties throughout the volume.

The `_build` method constructs texture handles for absorption (`sigma_a`), scattering (`sigma_s`), and emission (`Le`), then creates a `HomogeneousMediumInstance` that provides the `closure` method packaging per-ray data:

```cpp
auto sigma_a = pipeline.build_texture(command_buffer, _sigma_a);
auto sigma_s = pipeline.build_texture(command_buffer, _sigma_s);
auto Le       = pipeline.build_texture(command_buffer, _le);
auto phase_fn = pipeline.build_phasefunction(command_buffer, _phase_function);
return luisa::make_unique<HomogeneousMediumInstance>(
        pipeline, this, sigma_a, sigma_s, Le, phase_fn);

```

The instance's `closure` method (lines 45-55) packages the evaluated textures, wavelength set, and phase function pointer into a `HomogeneousMediumClosure`.

### Distance Sampling and Event Types

The `Medium::Closure::sample` method implements delta tracking to sample distances and event types. For homogeneous media, the algorithm follows these steps as implemented in `HomogeneousMediumClosure::sample`:

1. **Channel selection**: Choose a spectral channel proportional to extinction `σₜ = σₐ + σₛ`
2. **Distance sampling**: Draw `t = -log(1-u) / σₜ[channel]` where `u` is a uniform random number
3. **Surface hit**: If `t > t_max`, return transmittance `Tr = exp(-σₜ·t)`
4. **Medium interaction**: Otherwise decide between absorption and scattering using the channel-wise albedo ratio
5. **Scattering direction**: Call the attached `PhaseFunction` to obtain a new direction

Implementation references:
- Channel selection and distance sampling: lines 58-71 of [`homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/homogeneous.cpp)
- Scattering/absorption decision: lines 84-99
- Phase function invocation: line 103 where `phase_function()->sample_p(...)` is invoked

### Phase Function Sampling

When scattering occurs, the medium closure delegates direction sampling to its attached phase function. The `HenyeyGreensteinInstance::sample_p` method implements importance sampling for the Henyey-Greenstein phase function:

```cpp
virtual Float p(Expr<float3> wo, Expr<float3> wi) const = 0;
virtual PhaseFunctionSample sample_p(Expr<float3> wo, Expr<float2> u) const = 0;
virtual Float pdf(Expr<float3> wo, Expr<float3> wi) const = 0;

```

The implementation (lines 28-48 in [`henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/henyey_greenstein.cpp)) computes the phase function value and samples a direction based on the asymmetry parameter `g`, where `g = 0` represents isotropic scattering, `g > 0` forward scattering, and `g < 0` backward scattering.

## Integrating Volumetric Effects in Path Tracing

### The Megakernel Volumetric Path Tracer

The `MegakernelVolumetricPathTracing` integrator in [`src/integrators/mega_volume_path.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/integrators/mega_volume_path.cpp) orchestrates the complete volumetric rendering pipeline. This integrator handles both surface and medium interactions within a unified path tracing loop.

The algorithm initializes the `MediumTracker` with the environment medium, then iterates through geometry intersections while maintaining the medium stack. When the tracker indicates an active medium (`!medium_tracker.vacuum()`), the integrator samples the medium using `sampleT_maj` (majorant-based ratio tracking).

Key code locations:
- Environment medium initialization: lines 13-16
- Geometry intersection and tracker updates: lines 34-66
- Majorant sampling entry point: lines 6-27 of the second main loop (starting at line 86)

### Handling Medium Events

Within the volumetric sampling loop, the integrator processes four possible medium events:

1. **Surface hit** (`event_surface`): The ray reaches a geometry boundary; proceed with surface BSDF sampling
2. **Absorption** (`event_absorb`): The photon is absorbed; terminate the path or update weights
3. **Scattering** (`event_scatter`): The photon scatters; sample a new direction using the phase function and update throughput weights (`beta`, `r_u`, `r_l`)
4. **Null collision** (`event_null`): No interaction occurs; continue tracking

The integrator updates path throughput and Russian roulette weights based on the event type. For scattering events, it calls `phase_function()->sample_p` to determine the new ray direction, as shown in lines 60-98 of the volumetric sampling block.

Surface handling follows the standard path tracing logic with BSDF evaluation and direct lighting, implemented in lines 30-80 of the second main loop.

## Code Examples

### Declaring a Homogeneous Medium in JSON

Scene files define participating media using the `HomogeneousMedium` plugin with constant or textured optical properties:

```json
{
    "type": "medium",
    "name": "haze",
    "plugin": "HomogeneousMedium",
    "properties": {
        "eta": 1.0,
        "sigma_a": { "type": "const", "value": [0.01, 0.01, 0.01] },
        "sigma_s": { "type": "const", "value": [0.2, 0.2, 0.2] },
        "Le":      null,
        "phasefunction": { "type": "phasefunction", "plugin": "HenyeyGreenstein", "g": 0.0 }
    }
}

```

This configuration creates an isotropic scattering medium (`g = 0`) with constant absorption and scattering coefficients. LuisaRender loads this via `HomogeneousMedium` as implemented in [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp).

### Manual Medium Sampling in C++

Custom integrators can manually sample media by dispatching to the medium instance and invoking the closure interface:

```cpp
// Assume we have a valid `Pipeline &pipeline`, `Scene *scene`, and a `Ray ray`.
auto medium_tag = pipeline.environment_medium_tag();
pipeline.media().dispatch(medium_tag, [&](auto *medium) noexcept {
    auto closure = medium->closure(ray, swl, time);      // swl = sampled wavelengths
    // Sample a distance and a scattering event
    auto sample = closure->sample(t_max, rng);
    if (sample.medium_event == Medium::event_scatter) {
        // New direction given by the phase function
        auto ps = closure->phase_function()->sample_p(-ray->direction(),
                                                     make_float2(rng.uniform_float(),
                                                                 rng.uniform_float()));
        // Continue tracing with the new direction...
    }
});

```

Key calls include `pipeline.media().dispatch` to fetch the medium instance, `closure->sample` to return a `Medium::Sample` (see `HomogeneousMediumClosure::sample`), and `closure->phase_function()->sample_p` to draw a new direction based on the phase function (see `HenyeyGreensteinInstance::sample_p`).

### Using MediumTracker for Nested Media

The `MediumTracker` manages nested media in shader-style code:

```cpp
MediumTracker tracker;
auto env_tag = pipeline.environment_medium_tag();
pipeline.media().dispatch(env_tag, [&](auto *m) {
    tracker.enter(m->priority(),
                 make_medium_info(m->priority(), env_tag));
});

// ... intersect geometry, get a shape with a medium
if (shape.has_medium()) {
    auto tag = shape.medium_tag();
    pipeline.media().dispatch(tag, [&](auto *m) {
        // Enter the inner medium (higher priority overrides the outer one)
        tracker.enter(m->priority(),
                     make_medium_info(m->priority(), tag));
    });
}

// Later, to query the current medium:
auto cur = tracker.current();        // cur.medium_tag holds the active medium

```

The tracker automatically keeps the highest-priority medium on top, and `vacuum()` indicates whether the ray is in empty space.

## Key Source Files

| File | What it Provides | Link |
|---|---|---|
| [`src/base/medium.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.h) | Abstract medium class, `Closure` interface, sampling helpers (`sampleT_maj`) | [medium.h](https://github.com/luisagroup/luisarender/blob/next/src/base/medium.h) |
| [`src/base/medium.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.cpp) | Minimal implementation of `Medium` constructors and `build` wrapper | [medium.cpp](https://github.com/luisagroup/luisarender/blob/next/src/base/medium.cpp) |
| [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp) | Concrete homogeneous medium, texture handling, closure creation | [homogeneous.cpp](https://github.com/luisagroup/luisarender/blob/next/src/media/homogeneous.cpp) |
| [`src/phasefunctions/henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/phasefunctions/henyey_greenstein.cpp) | Henyey-Greenstein phase function implementation | [henyey_greenstein.cpp](https://github.com/luisagroup/luisarender/blob/next/src/phasefunctions/henyey_greenstein.cpp) |
| [`src/util/medium_tracker.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/medium_tracker.cpp) | Stack-like data structure for nested media, enter/exit logic | [medium_tracker.cpp](https://github.com/luisagroup/luisarender/blob/next/src/util/medium_tracker.cpp) |
| [`src/integrators/mega_volume_path.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/integrators/mega_volume_path.cpp) | Full volumetric path-tracer that drives medium sampling, tracker updates, and phase-function usage | [mega_volume_path.cpp](https://github.com/luisagroup/luisarender/blob/next/src/integrators/mega_volume_path.cpp) |

These files together constitute the volumetric rendering pipeline in LuisaRender: **media definitions → per-ray closures → phase-function sampling → medium stack handling → integrator's sampling loop**.

## Summary

- **Medium Abstraction**: The `Medium` base class in [`src/base/medium.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.h) defines the closure interface for optical properties (`σₐ`, `σₛ`, `Lₑ`) and phase function attachment.
- **Phase Functions**: The `PhaseFunction` interface in [`src/phasefunctions/henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/phasefunctions/henyey_greenstein.cpp) provides angular scattering distributions, with Henyey-Greenstein as the default implementation.
- **Nested Media**: `MediumTracker` in [`src/util/medium_tracker.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/medium_tracker.cpp) maintains a priority-ordered stack of media for correct handling of overlapping volumes.
- **Sampling Algorithm**: `HomogeneousMedium` in [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp) implements delta tracking to sample distances and events (absorption, scattering, or surface hit).
- **Integration**: The `MegakernelVolumetricPathTracing` integrator in [`src/integrators/mega_volume_path.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/integrators/mega_volume_path.cpp) orchestrates the full pipeline, handling medium events and phase function sampling within the path tracing loop.

## Frequently Asked Questions

### How does LuisaRender handle multiple overlapping media?

LuisaRender uses the `MediumTracker` class to manage nested media through a priority-ordered stack. When a ray enters a medium, `MediumTracker::enter` pushes the medium onto the stack; when exiting, `MediumTracker::exit` removes it. The `current()` method always returns the highest-priority active medium, ensuring correct light transport calculations when media overlap or contain embedded surfaces.

### What phase function does LuisaRender use by default?

The default phase function implementation is the Henyey-Greenstein model defined in [`src/phasefunctions/henyey_greenstein.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/phasefunctions/henyey_greenstein.cpp). This phase function provides anisotropic scattering controlled by the asymmetry parameter `g`, where `g = 0` produces isotropic scattering, positive values favor forward scattering, and negative values favor backward scattering. The implementation provides analytical evaluation and importance sampling via the `sample_p` method.

### How does the volumetric path tracer decide between surface and medium interactions?

The `MegakernelVolumetricPathTracing` integrator in [`src/integrators/mega_volume_path.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/integrators/mega_volume_path.cpp) uses delta tracking via `Medium::Closure::sample`. The algorithm samples a tentative distance `t` based on the extinction coefficient `σₜ = σₐ + σₛ`. If `t` exceeds the distance to the next surface (`t_max`), the integrator processes a surface hit; otherwise, it processes a medium event (absorption, scattering, or null collision) at distance `t`. This decision occurs in the sampling routine implemented in [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp) (lines 58-99).

### Can custom media be implemented in LuisaRender?

Yes, developers can implement custom media by inheriting from the `Medium` base class in [`src/base/medium.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/medium.h) and implementing the `build` method to create a specialized `Medium::Instance`. The instance must provide a `closure` method that returns a custom `Medium::Closure` implementation defining `sample`, `transmittance`, and `phase_function` accessors. The homogeneous medium in [`src/media/homogeneous.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/media/homogeneous.cpp) serves as the reference implementation for uniform media properties.