05 - Planetary BRDFs¶
Huira’s BSDFs for planetary surfaces side by side, at a 90 degree phase angle and near opposition.
Planetary BRDFs at 90 degree Phase |
|---|
|
Planetary BRDFs as Opposition |
|
Lambertian, Oren-Nayar, Lommel-Seeliger, McEwen, and Hapke BRDFs at various phase angles.
Data¶
None: everything in it is built in code.
Run¶
From this folder, in Python:
python planetary_brdfs.py
or in C++, built as in Running the examples:
../../build/05_planetary_brdfs
Each writes its images to output/, in the folder it runs from.
Source¶
The complete example, in both languages. View this example on GitHub.
"""
05 - Planetary BRDFs
Demonstrates:
- The BSDFs made for planetary surfaces, side by side on spheres of the same albedo:
Lambertian, Oren-Nayar (with a roughness), Lommel-Seeliger, McEwen and Hapke
- Hapke's parameters: opposition surge width and amplitude, and the phase function's
asymmetry and forward/backward fraction
- How the phase angle (Sun-target-camera) changes each one: rendered near opposition, where
regolith brightens sharply, and at 90 degrees, where limb darkening differs most
- Moving a light between renders of the same scene
Builds on: 02 Scene graph
Data: none
Run:
python planetary_brdfs.py
Writes:
output/05_planetary_brdfs.png at a 90 degree phase angle
output/05_planetary_brdfs_opposition.png at a 5 degree phase angle
"""
import math
from pathlib import Path
import huira
from huira.rgb import Scene, SceneView, Renderer, SpectralBins
from huira.units import Meter as m
from huira.units import Millimeter as mm
from huira.units import AstronomicalUnit as au
from huira.units import Microsecond as us
def main():
scene = Scene()
sphere = scene.add_ellipsoid(m(1), m(1), m(1))
# The planetary BSDFs, left to right. Each takes the albedo from its material:
# - Lambertian: ideal diffuse; brightness depends only on the incidence angle
# - Oren-Nayar: rough diffuse, with the roughness from the material (0 is Lambertian)
# - Lommel-Seeliger: single scattering in a dark, dusty medium; no limb darkening
# - McEwen: blends Lommel-Seeliger and Lambert with phase angle, as fits the Moon
# - Hapke: the five-parameter regolith model. h and B0 are the width and amplitude of the
# opposition surge; b and c the asymmetry and forward/backward fraction of its
# double Henyey-Greenstein phase function. (These values are illustrative, of the order
# reported for lunar regolith.) Its albedo is the single scattering albedo.
bsdfs = [
("lambertian", scene.new_bsdf_lambertian()),
("oren_nayar", scene.new_bsdf_oren_nayar()),
("lommel_seeliger", scene.new_bsdf_lommel_seeliger()),
("mcewen", scene.new_bsdf_mcewen()),
("hapke", scene.new_bsdf_hapke(h=0.06, B0=1.0, b=0.25, c=0.6)),
]
spacing = 2.5
for i, (name, bsdf) in enumerate(bsdfs):
material = scene.new_material(bsdf, name)
material.set_albedo_factor(SpectralBins(0.3))
if name == "oren_nayar":
material.set_roughness_factor(0.5)
primitive = scene.add_primitive(sphere, material, name)
instance = scene.root.new_instance(primitive)
instance.set_position(m((i - 2) * spacing), m(0), m(0))
# A long lens from far away, so every sphere is seen from nearly the same direction:
camera_model = scene.new_camera_model()
camera_model.set_focal_length(mm(100))
camera_model.configure_sensor_from_size((1600, 400), mm(36))
camera_model.set_fstop(16)
camera = scene.root.new_instance(camera_model)
camera.set_position(m(0), m(0), m(-40))
sun = scene.root.new_instance(scene.new_sun_light())
time = huira.Time("2026-01-01T00:00:00")
exposure = huira.Interval.from_start(time, us(400))
renderer = Renderer()
renderer.set_samples_per_pixel(64)
output_dir = Path("output")
output_dir.mkdir(exist_ok=True)
# The camera looks along +z, with +y down the image. The Sun, 1 AU away in the y-z plane and
# phase_angle away from straight behind the camera, lights the spheres from above at that
# phase angle (from the side, along the row, each sphere would shadow the next). A scene can
# be rendered again after changes: each SceneView takes the scene as it is when it is made.
for phase_angle_deg, filename in [(90, "05_planetary_brdfs.png"),
(5, "05_planetary_brdfs_opposition.png")]:
phase = math.radians(phase_angle_deg)
sun.set_position(m(0), au(-math.sin(phase)), au(-math.cos(phase)))
scene_view = SceneView(scene, exposure, camera)
frame_buffer = camera_model.make_frame_buffer()
frame_buffer.enable_sensor_response()
renderer.render(scene_view, frame_buffer)
huira.write_png(output_dir / filename, huira.linear_to_srgb(frame_buffer.sensor_response))
if __name__ == "__main__":
main()
// 05 - Planetary BRDFs
//
// Demonstrates:
// - The BSDFs made for planetary surfaces, side by side on spheres of the same albedo:
// Lambertian, Oren-Nayar (with a roughness), Lommel-Seeliger, McEwen and Hapke
// - Hapke's parameters: opposition surge width and amplitude, and the phase function's
// asymmetry and forward/backward fraction
// - How the phase angle (Sun-target-camera) changes each one: rendered near opposition, where
// regolith brightens sharply, and at 90 degrees, where limb darkening differs most
// - Moving a light between renders of the same scene
//
// Builds on: 02 Scene graph
// Data: none
//
// Run:
// ./05_planetary_brdfs
//
// Writes:
// output/05_planetary_brdfs.png at a 90 degree phase angle
// output/05_planetary_brdfs_opposition.png at a 5 degree phase angle
#include <cmath>
#include <filesystem>
#include <string>
#include <utility>
#include <vector>
#include "huira/huira.hpp"
namespace fs = std::filesystem;
using namespace huira::units::literals;
using TSpectral = huira::RGB;
int main()
{
huira::Scene<TSpectral> scene;
auto sphere = scene.add_ellipsoid(1_m, 1_m, 1_m);
// The planetary BSDFs, left to right. Each takes the albedo from its material:
// - Lambertian: ideal diffuse; brightness depends only on the incidence angle
// - Oren-Nayar: rough diffuse, with the roughness from the material (0 is Lambertian)
// - Lommel-Seeliger: single scattering in a dark, dusty medium; no limb darkening
// - McEwen: blends Lommel-Seeliger and Lambert with phase angle, as fits the Moon
// - Hapke: the five-parameter regolith model. h and B0 are the width and amplitude of the
// opposition surge; b and c the asymmetry and forward/backward fraction of its
// double Henyey-Greenstein phase function. (These values are illustrative, of the order
// reported for lunar regolith.) Its albedo is the single scattering albedo.
const float h = 0.06f;
const float B0 = 1.0f;
const float b = 0.25f;
const float c = 0.6f;
std::vector<std::pair<std::string, huira::BSDFHandle<TSpectral>>> bsdfs = {
{"lambertian", scene.new_bsdf_lambertian()},
{"oren_nayar", scene.new_bsdf_oren_nayar()},
{"lommel_seeliger", scene.new_bsdf_lommel_seeliger()},
{"mcewen", scene.new_bsdf_mcewen()},
{"hapke", scene.new_bsdf_hapke(h, B0, b, c)},
};
const double spacing = 2.5;
for (std::size_t i = 0; i < bsdfs.size(); ++i) {
const auto& [name, bsdf] = bsdfs[i];
auto material = scene.new_material(bsdf, name);
material.set_albedo_factor(TSpectral{0.3f});
if (name == "oren_nayar") {
material.set_roughness_factor(0.5f);
}
auto primitive = scene.add_primitive(sphere, material, name);
auto instance = scene.root.new_instance(primitive);
const double x = (static_cast<double>(i) - 2.0) * spacing;
instance.set_position(huira::units::Meter(x), 0_m, 0_m);
}
// A long lens from far away, so every sphere is seen from nearly the same direction:
auto camera_model = scene.new_camera_model();
camera_model.set_focal_length(100_mm);
camera_model.configure_sensor_from_size({1600, 400}, 36_mm);
camera_model.set_fstop(16);
auto camera = scene.root.new_instance(camera_model);
camera.set_position(0_m, 0_m, -40_m);
auto sun = scene.root.new_instance(scene.new_sun_light());
huira::Time time("2026-01-01T00:00:00");
huira::Interval exposure = huira::Interval::from_start(time, 400_us);
huira::Renderer<TSpectral> renderer;
renderer.set_samples_per_pixel(64);
fs::create_directories("output");
// The camera looks along +z, with +y down the image. The Sun, 1 AU away in the y-z plane and
// phase_angle away from straight behind the camera, lights the spheres from above at that
// phase angle (from the side, along the row, each sphere would shadow the next). A scene can
// be rendered again after changes: each SceneView takes the scene as it is when it is made.
const std::vector<std::pair<double, std::string>> renders = {
{90.0, "output/05_planetary_brdfs.png"},
{5.0, "output/05_planetary_brdfs_opposition.png"},
};
for (const auto& [phase_angle_deg, filename] : renders) {
const double phase = phase_angle_deg * huira::PI<double>() / 180.0;
sun.set_position(0_m,
huira::units::AstronomicalUnit(-std::sin(phase)),
huira::units::AstronomicalUnit(-std::cos(phase)));
huira::SceneView<TSpectral> scene_view(scene, exposure, camera);
auto frame_buffer = camera_model.make_frame_buffer();
frame_buffer.enable_sensor_response();
renderer.render(scene_view, frame_buffer);
huira::write_image_png(filename, huira::linear_to_srgb(frame_buffer.sensor_response()));
}
}

