03 - Load a model

NASA’s model of the Gateway, loaded from a glTF file and path traced.

03 - Load a model

Data

File

What it is

From

gateway.glb

NASA’s model of the Gateway

media.huira.space

Download it into this folder:

curl -fLO https://media.huira.space/data/models/nasa/gateway.glb

Licenses, and how to credit the data: Gateway model.

Run

From this folder, in Python:

python load_a_model.py gateway.glb

or in C++, built as in Running the examples:

../../build/03_load_a_model gateway.glb

Each writes its images to output/, in the folder it runs from.

Source

The complete example, in both languages. View this example on GitHub.

"""
03 - Load a model

Demonstrates:
    - Loading a 3D model from a file (glTF/GLB here; any format Assimp reads), with its
      meshes, materials and textures
    - Instancing a model, and printing its internal graph
    - The camera's intrinsic matrix
    - A background radiance, for the sky behind the model
    - Path tracing settings: samples per pixel, bounces, and clamping indirect light
    - Dynamic sampling: stopping early on pixels that have converged

Builds on: 02 Scene graph
Data: gateway.glb, NASA's model of the Gateway (see README.md)

Run:
    python load_a_model.py gateway.glb

Writes:
    output/03_load_a_model.png
"""

import sys
from pathlib import Path

import huira
from huira.rgb import Scene, SceneView, Renderer

from huira.units import Meter as m
from huira.units import Millimeter as mm
from huira.units import Degree as deg
from huira.units import AstronomicalUnit as au
from huira.units import Millisecond as ms


def main():
    if len(sys.argv) != 2:
        sys.exit("Usage: python load_a_model.py <gateway.glb>\nREADME.md says where to get it.")
    model_path = Path(sys.argv[1])

    huira.enable_console_info()

    scene = Scene()

    # load_model reads a model file into the scene as one asset: its meshes, materials and
    # textures, and the graph of nodes that places them. Huira reads models with Assimp, so
    # besides glTF it reads OBJ, FBX, PLY, STL and many other formats.
    gateway_model = scene.load_model(model_path, "gateway")
    gateway_model.print_graph()

    # Like any asset, a model is placed with an instance. Here, at the origin:
    gateway = scene.root.new_instance(gateway_model)

    sun = scene.root.new_instance(scene.new_sun_light())
    sun.set_position(m(0), au(-1), m(0))

    # Rays that hit nothing see the background radiance, given here as one value for every
    # spectral bin: a faint glow, rather than black. It can also be an image (an environment).
    scene.set_background_radiance(1e-7)

    camera_model = scene.new_camera_model()
    camera_model.use_blender_convention()
    camera_model.set_focal_length(mm(50))
    camera_model.configure_sensor_from_size((1920, 1080), mm(36))
    camera_model.set_fstop(16)

    # The camera matrix, in pixels, as set_intrinsic_matrix() would take it:
    print("Camera matrix (pixels):")
    print(camera_model.intrinsic_matrix())

    camera = scene.root.new_instance(camera_model)
    camera.set_position(m(40), m(-40), m(0))
    camera.set_euler_angles(deg(95), deg(0), deg(45))

    scene.print_contents()

    time = huira.Time("2026-01-01T00:00:00")
    exposure = huira.Interval.from_start(time, ms(1))
    scene_view = SceneView(scene, exposure, camera)

    frame_buffer = camera_model.make_frame_buffer()
    frame_buffer.enable_sensor_response()

    # Path tracing follows each camera ray as it bounces from surface to surface:
    #   - samples per pixel: rays traced through each pixel (more is less noisy)
    #   - max bounces: how many surfaces a path may hit (1 is direct light only)
    #   - indirect clamp: a cap on the light any one bounce adds, which trades a little bias
    #     for much less "firefly" noise
    renderer = Renderer()
    renderer.set_samples_per_pixel(256)
    renderer.set_max_bounces(3)
    renderer.set_indirect_clamp(10.0)

    # Dynamic sampling stops tracing a pixel once its noise (the uncertainty in its brightness,
    # relative to it) and its neighbours' is below a threshold (0.01, about 1%, by default),
    # after a minimum number of samples: flat and empty parts of the image finish early, and
    # the samples go where the noise is.
    renderer.set_dynamic_sampling(True)
    renderer.set_min_samples(16)
    renderer.set_noise_threshold(0.01)

    # The camera's optics are precomputed before the first render. Doing it here keeps that time
    # out of the render (and is when any problem with the settings is reported):
    camera_model.precompute()

    renderer.render(scene_view, frame_buffer)

    output_dir = Path("output")
    output_dir.mkdir(exist_ok=True)
    huira.write_png(
        output_dir / "03_load_a_model.png", huira.linear_to_srgb(frame_buffer.sensor_response)
    )


if __name__ == "__main__":
    main()
// 03 - Load a model
//
// Demonstrates:
//   - Loading a 3D model from a file (glTF/GLB here; any format Assimp reads), with its
//     meshes, materials and textures
//   - Instancing a model, and printing its internal graph
//   - The camera's intrinsic matrix
//   - A background radiance, for the sky behind the model
//   - Path tracing settings: samples per pixel, bounces, and clamping indirect light
//   - Dynamic sampling: stopping early on pixels that have converged
//
// Builds on: 02 Scene graph
// Data: gateway.glb, NASA's model of the Gateway (see README.md)
//
// Run:
//   ./03_load_a_model gateway.glb
//
// Writes:
//   output/03_load_a_model.png

#include <filesystem>
#include <iostream>

#include "huira/huira.hpp"

namespace fs = std::filesystem;

using namespace huira::units::literals;

using TSpectral = huira::RGB;

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "Usage: 03_load_a_model <gateway.glb>\nREADME.md says where to get it.\n";
        return 1;
    }
    const fs::path model_path = argv[1];

    huira::enable_console_info();

    huira::Scene<TSpectral> scene;

    // load_model reads a model file into the scene as one asset: its meshes, materials and
    // textures, and the graph of nodes that places them. Huira reads models with Assimp, so
    // besides glTF it reads OBJ, FBX, PLY, STL and many other formats.
    auto gateway_model = scene.load_model(model_path, "gateway");
    gateway_model.print_graph();

    // Like any asset, a model is placed with an instance. Here, at the origin:
    auto gateway = scene.root.new_instance(gateway_model);

    auto sun = scene.root.new_instance(scene.new_sun_light());
    sun.set_position(0_m, -1_au, 0_m);

    // Rays that hit nothing see the background radiance, given here as one value for every
    // spectral bin: a faint glow, rather than black. It can also be an image (an environment).
    scene.set_background_radiance(1e-7f);

    auto camera_model = scene.new_camera_model();
    camera_model.use_blender_convention();
    camera_model.set_focal_length(50_mm);
    camera_model.configure_sensor_from_size({1920, 1080}, 36_mm);
    camera_model.set_fstop(16);

    // The camera matrix, in pixels, as set_intrinsic_matrix() would take it (Mat3 is indexed
    // [column][row]):
    const huira::Mat3<float> K = camera_model.intrinsic_matrix();
    std::cout << "fx = " << K[0][0] << ", fy = " << K[1][1] << ", cx = " << K[2][0]
              << ", cy = " << K[2][1] << " (pixels)\n";

    auto camera = scene.root.new_instance(camera_model);
    camera.set_position(40_m, -40_m, 0_m);
    camera.set_euler_angles(95_deg, 0_deg, 45_deg);

    scene.print_contents();

    huira::Time time("2026-01-01T00:00:00");
    huira::Interval exposure = huira::Interval::from_start(time, 1_ms);
    huira::SceneView<TSpectral> scene_view(scene, exposure, camera);

    auto frame_buffer = camera_model.make_frame_buffer();
    frame_buffer.enable_sensor_response();

    // Path tracing follows each camera ray as it bounces from surface to surface:
    //   - samples per pixel: rays traced through each pixel (more is less noisy)
    //   - max bounces: how many surfaces a path may hit (1 is direct light only)
    //   - indirect clamp: a cap on the light any one bounce adds, which trades a little bias
    //     for much less "firefly" noise
    huira::Renderer<TSpectral> renderer;
    renderer.set_samples_per_pixel(256);
    renderer.set_max_bounces(3);
    renderer.set_indirect_clamp(10.0f);

    // Dynamic sampling stops tracing a pixel once its noise (the uncertainty in its brightness,
    // relative to it) and its neighbours' is below a threshold (0.01, about 1%, by default),
    // after a minimum number of samples: flat and empty parts of the image finish early, and
    // the samples go where the noise is.
    renderer.set_dynamic_sampling(true);
    renderer.set_min_samples(16);
    renderer.set_noise_threshold(0.01f);

    // The camera's optics are precomputed before the first render. Doing it here keeps that time
    // out of the render (and is when any problem with the settings is reported):
    camera_model.precompute();

    renderer.render(scene_view, frame_buffer);

    fs::create_directories("output");
    huira::write_image_png("output/03_load_a_model.png",
                           huira::linear_to_srgb(frame_buffer.sensor_response()));
}