C: Gaussian Splats#

This example loads the same USDA scene as Python: Gaussian Splats, renders one frame without preceding warmup frames, and saves LdrColor. Both static-loader and shared-loader executables are built.

The scene defines a ParticleField3DGaussianSplat prim. The following USDA declaration and equivalent Python code show one orange splat. The supplied scene uses the same array layout for all 73 splats. See Gaussian Splats for the attribute meanings.

def ParticleField3DGaussianSplat "Splat" {
    point3f[] positions = [(0, 0, 0)]
    quatf[] orientations = [(1, 0, 0, 0)]
    float3[] scales = [(1, 0.5, 0.25)]
    float[] opacities = [0.9]
    uniform int radiance:sphericalHarmonicsDegree = 0
    # SH0 = (linear RGB - 0.5) / 0.2820947918; this is orange RGB (1, .3, 0).
    float3[] radiance:sphericalHarmonicsCoefficients = [(1.772454, -0.708982, -1.772454)]
}
splat = stage.DefinePrim("/Splat", "ParticleField3DGaussianSplat")
splat.CreateAttribute("positions", Sdf.ValueTypeNames.Point3fArray, custom=False).Set([(0, 0, 0)])
splat.CreateAttribute("orientations", Sdf.ValueTypeNames.QuatfArray, custom=False).Set([Gf.Quatf(1)])
splat.CreateAttribute("scales", Sdf.ValueTypeNames.Float3Array, custom=False).Set([(1, 0.5, 0.25)])
splat.CreateAttribute("opacities", Sdf.ValueTypeNames.FloatArray, custom=False).Set([0.9])
splat.CreateAttribute("radiance:sphericalHarmonicsDegree", Sdf.ValueTypeNames.Int,
                      custom=False, variability=Sdf.VariabilityUniform).Set(0)
splat.CreateAttribute("radiance:sphericalHarmonicsCoefficients", Sdf.ValueTypeNames.Float3Array,
                      custom=False).Set([(1.772454, -0.708982, -1.772454)])

Before running, follow the C API setup and check the GPU and driver requirements. The first render may take longer while shaders compile and cache.

Start in the public ovrtx repository root, rendering/ovrtx/public in a Kit checkout. Choose the commands for your platform to build and run both loaders:

cd examples/c/gaussian-splats
cmake -B build
cmake --build build --config Release
.\build\static\Release\gaussian-splats.exe `
  ../../python/gaussian-splats/gaussian_splats.usda gaussian-static.png
.\build\shared\Release\gaussian-splats-dynamic.exe `
  ../../python/gaussian-splats/gaussian_splats.usda gaussian-shared.png
cd examples/c/gaussian-splats
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/static/gaussian-splats \
  ../../python/gaussian-splats/gaussian_splats.usda gaussian-static.png
./build/shared/gaussian-splats-dynamic \
  ../../python/gaussian-splats/gaussian_splats.usda gaussian-shared.png

Both loaders render the supplied gaussian_splats.usda scene. The commands write gaussian-static.png and gaussian-shared.png in examples/c/gaussian-splats. If you omit the output filename, the example writes out.png in the current directory.

To render PT or Minimal, change omni:rtx:rendermode on /Render/Camera in the supplied scene to PathTracing or MinimalRendering. For Minimal, keep omni:rtx:minimal:mode = 3 and omni:rtx:minimal:dlss:mode = 2, which the scene already supplies. Both loaders use these settings.

The following code requests one RenderProduct and waits for its first frame.

// We render a frame by stepping the renderer.
//
// Any sensors whose exposures end during this step will generate a frame
// that will be available in the step result. Since the camera in the loaded
// USD layer is instantaneous (does not specify motion blur or rolling
// shutter), it will generate a frame every time the render is stepped.
//
// To step the renderer we need to tell ovrtx which RenderProducts we're
// interested in, which in this case is the RenderProduct we defined in the
// loading layer.
ovrtx_render_product_set_t render_products = {};
ovx_string_t render_product_str = {"/Render/Camera", strlen("/Render/Camera")};
render_products.render_products = &render_product_str;
render_products.num_render_products = 1;

std::cerr << "Stepping renderer..." << std::endl;
ovrtx_step_result_handle_t step_result_handle = 0;
ovrtx_enqueue_result_t enqueue_result =
    ovrtx_step_with_stage(renderer, render_products, 1.0 / 60.0, stage_ordinal, &step_result_handle);
if (check_and_print_error(enqueue_result, "step")) {
    return cleanup(1);
}

// Wait for the render to complete. Here we'll just block until it's done.
ovrtx_op_wait_result_t wait_result;
result = ovrtx_wait_op(renderer,
                       enqueue_result.op_index,
                       ovrtx_timeout_infinite,
                       &wait_result);
if (check_and_print_error(result, "wait_op")) {
    ovrtx_destroy_results(renderer, step_result_handle);
    return cleanup(1);
}
std::cerr << "Stepped renderer." << std::endl;

The output is copied to a PNG while its CPU mapping is alive:

// Map rendered output so that it can be accessed on the CPU
ovrtx_map_output_description_t map_desc = {};
map_desc.device_type = OVRTX_MAP_DEVICE_TYPE_CPU;
ovrtx_render_var_output_t rendered_output = {};
result = ovrtx_map_render_var_output(renderer,
                                   ldrcolor_output_handle,
                                   &map_desc,
                                   ovrtx_timeout_infinite,
                                   &rendered_output);
if (check_and_print_error(result, "map_render_var_output")) {
    ovrtx_destroy_results(renderer, step_result_handle);
    return cleanup(1);
}

// LdrColor is a single-tensor render variable; read tensors[0].
// Image outputs follow shape [H, W, C] with dtype.lanes == 1.
if (rendered_output.num_tensors != 1) {
    std::cerr << "Unexpected LdrColor render variable: expected 1 tensor, got " << rendered_output.num_tensors << "." << std::endl;
    ovrtx_cuda_sync_t no_sync = {};
    ovrtx_unmap_render_var_output(renderer, rendered_output.map_handle, no_sync);
    ovrtx_destroy_results(renderer, step_result_handle);
    return cleanup(1);
}
DLTensor const& tensor = *rendered_output.tensors[0].dl;
if (tensor.ndim != 3 || !tensor.shape || tensor.shape[2] != 4 ||
    tensor.dtype.lanes != 1 || tensor.dtype.code != kDLUInt || tensor.dtype.bits != 8 ||
    (tensor.strides && (tensor.strides[2] != 1 || tensor.strides[1] != 4 ||
                        tensor.strides[0] != 4 * tensor.shape[1]))) {
    std::cerr << "Unexpected LdrColor tensor layout. Expected [H, W, 4] and dtype.lanes == 1." << std::endl;
    ovrtx_cuda_sync_t no_sync = {};
    ovrtx_unmap_render_var_output(renderer, rendered_output.map_handle, no_sync);
    ovrtx_destroy_results(renderer, step_result_handle);
    return cleanup(1);
}
int width = static_cast<int>(tensor.shape[1]);
int height = static_cast<int>(tensor.shape[0]);
int saved = stbi_write_png(argc == 3 ? argv[2] : "out.png",
               width,
               height,
               /* components = */ 4,
               static_cast<unsigned char const*>(tensor.data) + tensor.byte_offset,
               /* row stride in bytes = */ 4 * width);