Example: opengl_normal_maps

Normal maps on textured planes: two brick walls side by side, with and without a normal map, lit by a grazing point light.

opengl_normal_maps screenshot

Modules: mrpt_core, mrpt_gui, mrpt_img, mrpt_system, 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/core/format.h>
#include <mrpt/gui/CDisplayWindow3D.h>
#include <mrpt/img/CImage.h>
#include <mrpt/img/TColor.h>
#include <mrpt/system/filesystem.h>
#include <mrpt/viz/CSkyBox.h>
#include <mrpt/viz/CText.h>
#include <mrpt/viz/CTexturedPlane.h>
#include <mrpt/viz/Viewport.h>

#include <iostream>
#include <string>
#include <thread>

namespace
{

// ---------------------------------------------------------------------------
// Texture URLs
// ---------------------------------------------------------------------------
const char* URL_ASPHALT_COLOR =
    "https://mrpt.github.io/mvsim-models/textures-cgbookcase/"
    "Asphalt01_1K_BaseColor.png";
const char* URL_ASPHALT_NORMAL =
    "https://mrpt.github.io/mvsim-models/textures-cgbookcase/"
    "Asphalt01_1K_Normal.png";
const char* URL_BRICK_COLOR =
    "https://mrpt.github.io/mvsim-models/textures-cgbookcase/"
    "BrickWall01_1K_BaseColor.png";
const char* URL_BRICK_NORMAL =
    "https://mrpt.github.io/mvsim-models/textures-cgbookcase/"
    "BrickWall01_1K_Normal.png";

// Skybox
const char* URL_SKYBOX_ZIP = "https://mrpt.github.io/mvsim-models/skyboxes/SunSet.zip";
const char* SKYBOX_DIR = "./SunSet/";
const char* SKYBOX_FILE_PATTERN = "./SunSet/SunSet%s.jpg";

// ---------------------------------------------------------------------------
// Helper: download a single file if not already present
// ---------------------------------------------------------------------------
void downloadFile(const std::string& url, const std::string& destPath)
{
  if (mrpt::system::fileExists(destPath))
  {
    return;
  }

  std::cout << "Downloading: " << url << " -> " << destPath << "\n";

  // Try wget first, fall back to curl
  int ret = ::system(mrpt::format(R"(wget  "%s" -O "%s")", url.c_str(), destPath.c_str()).c_str());
  if (ret != 0)
  {
    ret = ::system(mrpt::format(R"(wget  "%s" -o "%s")", url.c_str(), destPath.c_str()).c_str());
  }
  if (ret != 0)
  {
    THROW_EXCEPTION_FMT("Could not download file: %s", url.c_str());
  }
}

// ---------------------------------------------------------------------------
// Download skybox ZIP and extract if needed
// ---------------------------------------------------------------------------
void downloadSkybox()
{
  if (mrpt::system::directoryExists(SKYBOX_DIR))
  {
    return;
  }

  std::cout << "Downloading skybox...\n";
  int ret = ::system(mrpt::format("wget -q \"%s\" -O skybox.zip", URL_SKYBOX_ZIP).c_str());
  if (ret != 0)
  {
    ret = ::system(mrpt::format("wget -q \"%s\" -o skybox.zip", URL_SKYBOX_ZIP).c_str());
  }
  if (ret != 0)
  {
    THROW_EXCEPTION("Could not download skybox zip.");
  }

  ret = ::system("unzip -q skybox.zip");
  if (ret != 0)
  {
    THROW_EXCEPTION("Could not unzip skybox.");
  }
}

// ---------------------------------------------------------------------------
// Main demo
// ---------------------------------------------------------------------------
void TestNormalMaps()
{
  // Download textures
  downloadFile(URL_ASPHALT_COLOR, "asphalt_color.png");
  downloadFile(URL_ASPHALT_NORMAL, "asphalt_normal.png");
  downloadFile(URL_BRICK_COLOR, "brick_color.png");
  downloadFile(URL_BRICK_NORMAL, "brick_normal.png");
  downloadSkybox();

  // -----------------------------------------------------------------------
  // Create the window
  // -----------------------------------------------------------------------
  mrpt::gui::CDisplayWindow3D win("Normal Maps Demo - MRPT", 1024, 768);

  mrpt::viz::Scene::Ptr& theScene = win.get3DSceneAndLock();

  // -----------------------------------------------------------------------
  // Skybox
  // -----------------------------------------------------------------------
  {
    using mrpt::viz::CUBE_TEXTURE_FACE;
    auto sb = mrpt::viz::CSkyBox::Create();

    const std::vector<std::pair<CUBE_TEXTURE_FACE, const char*>> faceImages = {
        { CUBE_TEXTURE_FACE::FRONT, "Front"},
        {  CUBE_TEXTURE_FACE::BACK,  "Back"},
        {CUBE_TEXTURE_FACE::BOTTOM,  "Down"},
        {   CUBE_TEXTURE_FACE::TOP,    "Up"},
        {  CUBE_TEXTURE_FACE::LEFT,  "Left"},
        { CUBE_TEXTURE_FACE::RIGHT, "Right"},
    };
    for (const auto& p : faceImages)
    {
      const std::string fil = mrpt::format(SKYBOX_FILE_PATTERN, p.second);
      sb->assignImage(p.first, mrpt::img::CImage::LoadFromFile(fil));
    }
    theScene->insert(sb);
  }

  // -----------------------------------------------------------------------
  // Hemispheric ambient lighting (blueish sky, brownish ground)
  // -----------------------------------------------------------------------
  {
    auto& lp = theScene->getViewport("main")->lightParameters();
    lp.ambientSkyColor = mrpt::img::TColorf(0.6f, 0.7f, 1.0f);
    lp.ambientGroundColor = mrpt::img::TColorf(0.4f, 0.35f, 0.25f);

    // One directional sun-like light
    lp.lights.clear();
    mrpt::viz::TLight sunLight;
    sunLight.type = mrpt::viz::TLightType::Directional;
    sunLight.direction = {-0.8f, 0.0f, -0.4f};
    sunLight.color = mrpt::img::TColorf(1.0f, 0.95f, 0.85f);
    sunLight.diffuse = 0.9f;
    sunLight.specular = 0.4f;
    lp.lights.push_back(sunLight);

    // A warm point light close to the walls to emphasize normal map detail
    lp.lights.push_back(mrpt::viz::TLight::PointLight(
        {0.0f, -7.0f, 2.5f},                   // position: grazing the walls
        mrpt::img::TColorf(1.0f, 0.8f, 0.5f),  // warm color
        1.0f,                                  // diffuse
        0.8f,                                  // specular
        1.0f,                                  // att_constant
        0.07f,                                 // att_linear
        0.017f                                 // att_quadratic
        ));
  }

  // -----------------------------------------------------------------------
  // Ground: asphalt WITH normal map
  // -----------------------------------------------------------------------
  {
    const float halfSize = 8.0f;
    auto ground = mrpt::viz::CTexturedPlane::Create(-halfSize, halfSize, -halfSize, halfSize);
    ground->enableLighting(true);

    ground->assignImage(mrpt::img::CImage::LoadFromFile("asphalt_color.png"));
    ground->assignNormalMap(mrpt::img::CImage::LoadFromFile("asphalt_normal.png"));
    ground->setTextureRepeat(4.0f, 4.0f);

    // Ground lies in XY plane at Z=0 by default — no pose change needed
    theScene->insert(ground);
  }

  // -----------------------------------------------------------------------
  // Two brick walls side by side, in the same vertical plane (y=-8) and both
  // facing +Y, so they can be compared under the same grazing light:
  //  - Left (as seen from the camera): WITHOUT normal map
  //  - Right: WITH normal map
  // -----------------------------------------------------------------------
  for (const bool withNormalMap : {false, true})
  {
    const float wallW = 4.0f;  // half-width in X
    const float wallH = 3.0f;  // height
    auto wall = mrpt::viz::CTexturedPlane::Create(-wallW, wallW, 0.0f, wallH);
    wall->enableLighting(true);

    wall->assignImage(mrpt::img::CImage::LoadFromFile("brick_color.png"));
    if (withNormalMap)
    {
      wall->assignNormalMap(mrpt::img::CImage::LoadFromFile("brick_normal.png"));
    }
    wall->setTextureRepeat(4.0f, 1.5f);

    // Rotate 90° around X (stands vertically, facing -Y), then 180° around Z
    // so it faces +Y, towards the scene center
    mrpt::math::TPose3D pose;
    pose.x = withNormalMap ? -wallW : wallW;
    pose.y = -8.0;
    pose.z = 0;
    pose.yaw = mrpt::DEG2RAD(180.0);
    pose.roll = mrpt::DEG2RAD(90.0);
    pose.pitch = 0;
    wall->setPose(pose);

    theScene->insert(wall);
  }

  // IMPORTANT: unlock before adding 2D text messages
  win.unlockAccess3DScene();

  // -----------------------------------------------------------------------
  // 2D overlay labels
  // -----------------------------------------------------------------------
  mrpt::viz::TFontParams fp;
  fp.color = mrpt::img::TColorf(1, 1, 1);
  fp.vfont_scale = 15;
  win.addTextMessage(0.02, 0.95, "Normal Maps Demo (press ESC to exit)", 0, fp);

  mrpt::viz::TFontParams fp2;
  fp2.color = mrpt::img::TColorf(1, 1, 0.5f);
  fp2.vfont_scale = 13;
  win.addTextMessage(0.02, 0.04, "Left wall:  NO normal map", 1, fp2);
  win.addTextMessage(0.02, 0.08, "Right wall: WITH normal map", 2, fp2);

  // -----------------------------------------------------------------------
  // Camera
  // -----------------------------------------------------------------------
  win.setCameraAzimuthDeg(90.0f);
  win.setCameraElevationDeg(10.0f);
  win.setCameraZoom(11.0f);
  win.setCameraPointingToPoint(0.0f, -8.0f, 1.5f);
  win.setFOV(50.0f);

  win.forceRepaint();

  std::cout << "\n";
  std::cout << "Normal Maps Demo\n";
  std::cout << "  Left wall : brick texture WITHOUT normal map\n";
  std::cout << "  Right wall: brick texture WITH normal map\n";
  std::cout << "  Ground           : asphalt WITH normal map\n";
  std::cout << "  Lights           : directional sun + warm point light\n";
  std::cout << "\nPress ESC or any key to exit.\n";

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

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

}  // namespace

// ---------------------------------------------------------------------------
int main()
{
  try
  {
    TestNormalMaps();
    return 0;
  }
  catch (const std::exception& e)
  {
    std::cerr << "MRPT error: " << mrpt::exception_to_str(e) << "\n";
    return -1;
  }
}