Example: opengl_ssao_demo

Interactive demo of Screen-Space Ambient Occlusion (SSAO) and multiple light sources.

opengl_ssao_demo screenshot

Modules: mrpt_gui, mrpt_math, mrpt_viz | Requires: gui

C++ example source code:

/*                    _
                     | |    Mobile Robot Programming Toolkit (MRPT)
 _ __ ___  _ __ _ __ | |_
| '_ ` _ \| '__| '_ \| __|          https://www.mrpt.org/
| | | | | | |  | |_) | |_
|_| |_| |_|_|  | .__/ \__|     https://github.com/MRPT/mrpt/
               | |
               |_|

 Copyright (c) 2005-2026, Individual contributors, see AUTHORS file
 See: https://www.mrpt.org/Authors - All rights reserved.
 SPDX-License-Identifier: BSD-3-Clause
*/

#include <mrpt/gui/CDisplayWindow3D.h>
#include <mrpt/math/TPose3D.h>
#include <mrpt/viz/CBox.h>
#include <mrpt/viz/CCylinder.h>
#include <mrpt/viz/CGridPlaneXY.h>
#include <mrpt/viz/CSphere.h>
#include <mrpt/viz/CText.h>
#include <mrpt/viz/Viewport.h>

#include <chrono>
#include <cstdio>
#include <thread>

namespace
{

// Index of the point light inside TLightParameters::lights (light 0 is the
// directional one; light 1 is our point light).
constexpr int POINT_LIGHT_INDEX = 1;

// -----------------------------------------------------------------------
// Build the demo 3D scene
// -----------------------------------------------------------------------
void buildScene(mrpt::viz::Scene& scene)
{
  // -- Ground plane --
  {
    auto obj = mrpt::viz::CGridPlaneXY::Create(-10, 10, -10, 10, 0, 0.5f);
    obj->setColor(0.55f, 0.55f, 0.55f);
    scene.insert(obj);
  }

  // -- A flat base plate so SSAO has something to occlude against the floor --
  {
    auto box = mrpt::viz::CBox::Create(
        mrpt::math::TPoint3D(-5.5, -5.5, -0.02), mrpt::math::TPoint3D(5.5, 5.5, 0.0));
    box->setColor(0.75f, 0.75f, 0.75f);
    box->enableLight(true);
    scene.insert(box);
  }

  // -- Row of boxes of increasing height --
  for (int i = 0; i < 5; i++)
  {
    const float h = 0.5f + 0.4f * static_cast<float>(i);
    auto box = mrpt::viz::CBox::Create(
        mrpt::math::TPoint3D(-0.35, -0.35, 0.0), mrpt::math::TPoint3D(0.35, 0.35, h));
    box->setColor(0.8f, 0.4f + 0.1f * static_cast<float>(i), 0.3f);
    box->enableLight(true);
    mrpt::math::TPose3D p;
    p.x = -4.0 + 2.0 * i;
    p.y = -2.5;
    box->setPose(p);
    scene.insert(box);
  }

  // -- Row of cylinders --
  for (int i = 0; i < 4; i++)
  {
    const float r = 0.25f + 0.1f * static_cast<float>(i);
    auto cyl = mrpt::viz::CCylinder::Create(r, r, 1.2f, 20);
    cyl->setColor(0.3f, 0.5f, 0.8f + 0.05f * static_cast<float>(i));
    cyl->enableLight(true);
    mrpt::math::TPose3D p;
    p.x = -3.0 + 2.0 * i;
    p.y = 1.5;
    cyl->setPose(p);
    scene.insert(cyl);
  }

  // -- A cluster of spheres of various sizes --
  const float spos[][3] = {
      { 0.0f, 4.5f, 0.45f},
      { 1.1f, 4.5f, 0.30f},
      {-1.0f, 4.5f, 0.30f},
      { 0.5f, 5.4f, 0.25f},
      {-0.5f, 3.6f, 0.25f},
  };
  const float srad[] = {0.45f, 0.30f, 0.30f, 0.25f, 0.25f};
  for (int i = 0; i < 5; i++)
  {
    auto sph = mrpt::viz::CSphere::Create(srad[i], 18);
    sph->setColor(0.9f, 0.85f, 0.3f);
    sph->enableLight(true);
    mrpt::math::TPose3D p;
    p.x = spos[i][0];
    p.y = spos[i][1];
    p.z = spos[i][2];
    sph->setPose(p);
    scene.insert(sph);
  }

  // -- A "room corner" (two tall boxes placed at right angles) --
  {
    auto wallA = mrpt::viz::CBox::Create(
        mrpt::math::TPoint3D(-0.15, -0.15, 0.0), mrpt::math::TPoint3D(0.15, 4.0, 2.0));
    wallA->setColor(0.85f, 0.82f, 0.78f);
    wallA->enableLight(true);
    mrpt::math::TPose3D p;
    p.x = -4.8;
    p.y = 2.0;
    wallA->setPose(p);
    scene.insert(wallA);
  }
  {
    auto wallB = mrpt::viz::CBox::Create(
        mrpt::math::TPoint3D(-0.15, -0.15, 0.0), mrpt::math::TPoint3D(4.0, 0.15, 2.0));
    wallB->setColor(0.78f, 0.82f, 0.85f);
    wallB->enableLight(true);
    mrpt::math::TPose3D p;
    p.x = -4.8;
    p.y = 6.0;
    wallB->setPose(p);
    scene.insert(wallB);
  }

  // -- Small box cluster (tight packing to show contact shadows) --
  const int NB = 3;
  for (int ix = 0; ix < NB; ix++)
  {
    for (int iy = 0; iy < NB; iy++)
    {
      auto box = mrpt::viz::CBox::Create(
          mrpt::math::TPoint3D(0.0, 0.0, 0.0), mrpt::math::TPoint3D(0.45, 0.45, 0.45));
      box->setColor(
          0.65f + 0.1f * static_cast<float>(ix), 0.65f + 0.1f * static_cast<float>(iy), 0.65f);
      box->enableLight(true);
      mrpt::math::TPose3D p;
      p.x = 2.5 + 0.48 * ix;
      p.y = 3.5 + 0.48 * iy;
      box->setPose(p);
      scene.insert(box);
    }
  }
}

// -----------------------------------------------------------------------
// Configure lighting (called once; call again to re-apply after toggle)
// -----------------------------------------------------------------------
void setupLighting(mrpt::viz::Viewport& vp, bool pointLightOn)
{
  auto& lp = vp.lightParameters();

  // Hemisphere ambient: neutral sky/ground
  lp.ambientSkyColor = mrpt::img::TColorf(0.5f, 0.55f, 0.65f);
  lp.ambientGroundColor = mrpt::img::TColorf(0.3f, 0.28f, 0.25f);
  lp.ambient = 0.25f;

  // Clear existing lights; add directional + optional point light
  lp.lights.clear();

  // Directional sun-like light
  mrpt::viz::TLight sun;
  sun.type = mrpt::viz::TLightType::Directional;
  sun.direction = {-0.6f, -0.4f, -0.7f};
  sun.color = mrpt::img::TColorf(1.0f, 0.95f, 0.85f);
  sun.diffuse = 0.85f;
  sun.specular = 0.4f;
  lp.lights.push_back(sun);

  // Colored point light above the scene (toggle-able)
  if (pointLightOn)
  {
    lp.lights.push_back(mrpt::viz::TLight::PointLight(
        {0.0f, 0.0f, 4.0f},                    // above the scene center
        mrpt::img::TColorf(0.4f, 0.6f, 1.0f),  // cool blue-white
        1.0f,                                  // diffuse
        0.7f,                                  // specular
        1.0f,                                  // att_constant
        0.05f,                                 // att_linear
        0.008f                                 // att_quadratic
        ));
  }
}

// -----------------------------------------------------------------------
// Update the on-screen status text
// -----------------------------------------------------------------------
void updateStatusText(mrpt::gui::CDisplayWindow3D& win, bool ssaoOn, bool pointOn, bool shadowsOn)
{
  mrpt::viz::TFontParams fp;
  fp.color = mrpt::img::TColorf(1.0f, 1.0f, 1.0f);
  fp.vfont_scale = 14;
  win.addTextMessage(0.02, 0.97, "SSAO Demo  |  S=SSAO  P=Point light  H=Shadows  ESC=Quit", 0, fp);

  mrpt::viz::TFontParams fp2;
  fp2.vfont_scale = 13;
  fp2.color = ssaoOn ? mrpt::img::TColorf(0.3f, 1.0f, 0.3f) : mrpt::img::TColorf(1.0f, 0.4f, 0.4f);
  win.addTextMessage(0.02, 0.04, std::string("SSAO: ") + (ssaoOn ? "ON" : "OFF"), 1, fp2);

  mrpt::viz::TFontParams fp3;
  fp3.vfont_scale = 13;
  fp3.color = pointOn ? mrpt::img::TColorf(0.3f, 1.0f, 0.3f) : mrpt::img::TColorf(1.0f, 0.4f, 0.4f);
  win.addTextMessage(0.02, 0.07, std::string("Point light: ") + (pointOn ? "ON" : "OFF"), 2, fp3);

  mrpt::viz::TFontParams fp4;
  fp4.vfont_scale = 13;
  fp4.color =
      shadowsOn ? mrpt::img::TColorf(0.3f, 1.0f, 0.3f) : mrpt::img::TColorf(1.0f, 0.4f, 0.4f);
  win.addTextMessage(0.02, 0.10, std::string("Shadows: ") + (shadowsOn ? "ON" : "OFF"), 3, fp4);
}

// -----------------------------------------------------------------------
void RunDemo()
{
  mrpt::gui::CDisplayWindow3D win("SSAO Demo - MRPT", 1280, 800);

  bool ssaoOn = false;
  bool pointLightOn = true;
  bool shadowsOn = false;

  // ----- Build scene -----
  {
    mrpt::viz::Scene::Ptr& scene = win.get3DSceneAndLock();
    buildScene(*scene);
    auto vp = scene->getViewport("main");

    setupLighting(*vp, pointLightOn);
    vp->lightParameters().ssao_enabled = ssaoOn;
    vp->enableShadowCasting(shadowsOn);

    win.unlockAccess3DScene();
  }

  // ----- Camera -----
  win.setCameraAzimuthDeg(40.0f);
  win.setCameraElevationDeg(30.0f);
  win.setCameraZoom(20.0f);
  win.setFOV(55.0f);

  updateStatusText(win, ssaoOn, pointLightOn, shadowsOn);
  win.forceRepaint();

  std::printf(
      "\nSSAO Demo\n"
      "  S - Toggle SSAO (Screen-Space Ambient Occlusion)\n"
      "  P - Toggle point light\n"
      "  H - Toggle shadow casting\n"
      "  ESC / Q - Quit\n\n");

  // ----- Event loop -----
  while (win.isOpen())
  {
    std::this_thread::sleep_for(std::chrono::milliseconds(30));

    if (win.keyHit())
    {
      mrpt::gui::mrptKeyModifier kmods{};
      const int key = win.getPushedKey(&kmods);

      bool changed = false;

      if (key == mrpt::gui::MRPTK_ESCAPE || key == 'q' || key == 'Q')
      {
        break;
      }
      else if (key == 's' || key == 'S')
      {
        ssaoOn = !ssaoOn;
        changed = true;
      }
      else if (key == 'p' || key == 'P')
      {
        pointLightOn = !pointLightOn;
        changed = true;
      }
      else if (key == 'h' || key == 'H')
      {
        shadowsOn = !shadowsOn;
        changed = true;
      }

      if (changed)
      {
        {
          mrpt::viz::Scene::Ptr& scene = win.get3DSceneAndLock();
          auto vp = scene->getViewport("main");
          setupLighting(*vp, pointLightOn);
          vp->lightParameters().ssao_enabled = ssaoOn;
          vp->enableShadowCasting(shadowsOn);
          win.unlockAccess3DScene();
        }
        updateStatusText(win, ssaoOn, pointLightOn, shadowsOn);
        win.forceRepaint();
      }
    }
  }
}

}  // namespace

// ---------------------------------------------------------------------------
int main()
{
  try
  {
    RunDemo();
    return 0;
  }
  catch (const std::exception& e)
  {
    std::fprintf(stderr, "MRPT error: %s\n", e.what());
    return -1;
  }
}