Skip to content

10 - BRDF Simulation

This chapter describes how to use LESS to perform bidirectional reflectivity (BRF/BRDF) simulations to analyze the directional reflection characteristics of the earth's surface.

What is BRDF?

Bidirectional Reflection Distribution Function (BRDF) describes the reflection characteristics of the surface for different incident directions and different observation directions. In remote sensing, it is often characterized using the Bidirectional Reflectance Factor (BRF) - it is the ratio of surface reflectance to an ideal Lambertian body under a specific geometric condition.

Changes in BRDF are affected by the three-dimensional vegetation structure:

  • Hot Spot: When the observation direction coincides with the direction of the sun, the reflectivity increases significantly (because no shadow is visible at this time)
  • Bowl Shape: Vegetation canopies are usually more reflective at large viewing zenith angles
  • Forward/Backward Scattering: The scattering properties of the blades cause forward and backward asymmetry

BRFSensor - Results from multiple directions at once

BRFSensor is the sensor of choice for BRF simulation:

  • Transmit a set of directions, and run all the simulations in a single time (forward photon tracking, all directions share the same batch of photons, high efficiency)
  • No images are displayed, only the average BRF value of the scene in each direction is displayed.
  • Does not rely on the scene.size camera layout, and there is no geometric boundary problem for large-angle observations
import less

sensor = less.BRFSensor(
    bands=[650, 850],
    directions=[(0, 0), (30, 180), (60, 0)],   # (zenith°, azimuth°) list
    num_photons=50_000_000,                    # The default is 5e7; if the noise is too loud, add it.
)

What is returned is BRFProduct:

Properties Shape Description
.brf [n_dirs, n_bands] BRF in each direction and each band
.directions list Input (z, a) list
.wavelengths [n_bands] Band center wavelength (nm)

Multi-angle BRF simulation

Construct a list of observation directions (main plane/full hemisphere is acceptable), and you can get the complete BRDF surface in one simulate().

import less
import numpy as np

# ──Constructing the scene───────────────────────────────────────────────
scene = less.Scene()
scene.size = 10.0
scene.repetitive = False  # Limited scene (default); periodic tiling is limited to flat homogeneous terrain

scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())

maize = less.Object("maize", mesh=less.examples.asset_path("maize.obj"))
maize.set_property(less.Prospect(cab=45, car=10, cw=0.012, cm=0.006, N=1.55))

rng = np.random.RandomState(42)
positions = less.place.grid(scene, spacing=0.75, jitter=0.04)
n = len(positions)
scene.add(maize, positions=positions,
          scales=rng.uniform(0.9, 1.1, n),
          rotations=rng.uniform(0, 360, n))
# Sun azimuth angle = 180° (south), so the main plane is north-south
# - Backscatter: Azimuth 180° (same side)
# - Forward scatter: azimuth 0° / 360° (opposite side)
view_zeniths = np.arange(0, 65, 5)
directions = ([(z, 180) for z in view_zeniths]      # backward
            + [(z,   0) for z in view_zeniths[1:]]) # forward (skip z=0 dup)

# ── Get all directions in a single simulation ──────────────────────────────
result = scene.simulate(less.BRFSensor(
    bands=[650, 850],
    directions=directions,
    num_photons=50_000_000,
))

print("Main plane BRF (SZA=30°, SAA=180°)")
print(f"{'VZA':>6s}  {'AZ':>4s}  {'Red(650)':>10s}  {'NIR(850)':>10s}")
for (z, a), (r, n) in zip(result.directions, result.brf):
    signed_vza = z if a == 180 else -z          # Common habits for the main plane: negative = forward
    mark = " ← hot spot" if a == 180 and abs(z - 30) < 3 else ""
    print(f"{signed_vza:>6d}  {a:>4d}  {r:>10.4f}  {n:>10.4f}{mark}")

# Polar coordinates/principal plane curves can be solved in one line
result.show(plane="principal", style="line")    # matplotlib pop-up chart
result.save("plot_brf.csv")                     # Can also export CSV / JSON / NPY

Supporting script: scripts/10_brdf.py

Code interpretation

Two common modes for direction lists

# Principal plane (VZA scan)
directions = [(z, 180) for z in range(0, 65, 5)] \
           + [(z,   0) for z in range(5, 65, 5)]

# Full Hemisphere (VZA × VAA grid) – for polar plots
import itertools
directions = list(itertools.product(
    range(0, 75, 10),     # zeniths
    range(0, 360, 30),    # azimuths
))

Principal Plane

The principal plane is the plane containing the sun direction and the normal direction. In this plane:

  • Backscattering direction: The observer is on the same side of the sun (observation azimuth = solar azimuth)
  • Forward scattering direction: The observer is on the opposite side of the sun (observation azimuth = solar azimuth + 180°)
  • Hotspot: Observation zenith angle = solar zenith angle, and in the backscattering direction

The importance of scene repetition

Periodic BRF datums typically use scene.repetitive=True to represent an infinitely tiled flat homogeneous plot. This tutorial uses a finite scenario, so the results include plot edge effects; the same boundary conditions should be maintained when comparing observations or other models.

BRFSensor vs OpticalImager

What you want Who to use
BRF scalar values ​​in a set of directions (typical BRF / BRDF curve) BRFSensor
High-resolution two-dimensional image in a certain direction (care about spatial texture/shadow) OpticalImager + Orthographic(view_zenith=…)

BRFSensor uses forward photon tracking, all directions share the same batch of photons, and running in N directions ≈ time-consuming in one direction; OpticalImager has to run again in each direction.

RPV BRDF model

In addition to simulating BRF through 3D scenes, LESS also supports the direct use of parametric BRDF models. The RPV model can be used for terrestrial BRDF:

# RPV model parameters
scene.terrain = less.Terrain(
    property=less.RPV(rho0=0.08, k=0.7, theta=-0.15)
)

Parameter meaning:

Parameters Meaning Range
rho0 Reflectivity magnitude 0-1
k Shape parameters: <1 bowl-shaped (brighter at large angles), >1 bell-shaped (brighter vertically) 0-2
theta Scattering asymmetry: <0 backscattering enhancement, >0 forward scattering enhancement -1 ~ 1

Custom visualization

result.show() defaults to the main plane line drawing. To draw a hemispheric polar plot (VZA × VAA grid), you need to organize the data yourself:

import matplotlib.pyplot as plt
import numpy as np

# directions = [(z, a), ...], assuming a regular grid
zeniths  = sorted({z for z, _ in result.directions})
azimuths = sorted({a for _, a in result.directions})
brf_grid = np.zeros((len(azimuths), len(zeniths)))
for (z, a), val in zip(result.directions, result.brf[:, 0]):   # Band 0
    brf_grid[azimuths.index(a), zeniths.index(z)] = val

fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(7, 7))
ax.set_theta_zero_location('N'); ax.set_theta_direction(-1)
theta = np.deg2rad(azimuths)
r = zeniths
ax.contourf(theta, r, brf_grid.T, levels=20, cmap='RdYlGn')
ax.set_title(f"BRF @ {result.wavelengths[0]:.0f} nm")
plt.savefig("brf_polar.png", dpi=150, bbox_inches='tight')

Application scenarios

Application Description
Albedo estimation Integrate BRF to obtain hemispheric albedo
Multi-angle remote sensing Simulate MISR, POLDER and other multi-angle sensors
BRDF Calibration Evaluate BRDF parametric model accuracy
Structural parameter inversion Using BRF anisotropy to invert canopy structure

Next step

  • less.BRFSensorless.Scene.simulate()
  • less.RPVless.Prospectless.Lambertian
  • less.place.grid()less.place.poisson_disk()
  • less.BRFProduct.save()less.BRFProduct.plot_principal_plane()