PBR Clear Coat
vtk-examples/Cxx/Rendering/PBR_Clear_Coat
Description¶
This example is based on TestPBRClearCoat.cxx and renders a cube with custom texture mapping and a coat normal texture.
Physically based rendering sets metallicity, roughness, occlusion strength and normal scaling of the object. Textures are used to set base color, ORM, anisotropy and normals. Textures for the image based lighting and the skymap are supplied from a cubemap.
Image based lighting uses a cubemap texture to specify the environment. A Skybox is used to create the illusion of distant three-dimensional surroundings. Textures for the image based lighting and the skybox are supplied from a HDR or JPEG equirectangular Environment map or cubemap consisting of six image files.
A good source for Skybox HDRs and Textures is Poly Haven. Start with the 4K HDR versions of Skyboxes.
The parameters used to generate the example image are loaded from a generic JSON file, not all the parameters are used:
<DATA>/PBR_Examples.json
Where <DATA>
is the path to vtk-examples/src/Testing/Data
.
For information about the parameters in the JSON file, please see PBR_JSON_format.
Further Reading¶
- Introducing Physically Based Rendering with VTK
- PBR Journey Part 1: High Dynamic Range Image Based Lighting with VTK
- PBR Journey Part 2 : Anisotropy model with VTK
- PBR Journey Part 3 : Clear Coat Model with VTK
- Object Shading Properties
Note
<DATA>/PBR_Examples.json
assumes that the skyboxes and textures are in the subfoldersSkyboxes
andTextures
relative to this file This allows you to copy this JSON file and the associated subfolders to any other location on your computer.- You can turn off the skybox in the JSON file by setting
"skybox":false
. Image based lighting will still be active.
Note
- The C++ example requires C++17 as
std::filesystem
is used. If your compiler does not support C++17 comment out the filesystem stuff.
Other languages
See (Python)
Question
If you have a question about this example, please use the VTK Discourse Forum
Code¶
PBR_Clear_Coat.cxx
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCubeSource.h>
#include <vtkHDRReader.h>
#include <vtkImageFlip.h>
#include <vtkImageReader2.h>
#include <vtkImageReader2Factory.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkLight.h>
#include <vtkNamedColors.h>
#include <vtkOpenGLRenderer.h>
#include <vtkOpenGLTexture.h>
#include <vtkPBRIrradianceTexture.h>
#include <vtkPNGReader.h>
#include <vtkPolyDataMapper.h>
#include <vtkPolyDataTangents.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkSkybox.h>
#include <vtkTexture.h>
#include <vtkTriangleFilter.h>
#include <vtk_cli11.h>
#include <vtk_jsoncpp.h>
#include <cstdlib>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
namespace {
struct Parameters
{
std::map<std::string, std::string> parameters;
std::vector<std::string> cubemap;
bool skybox{true};
bool parsedOk{false};
};
/**
* Read the parameters from a json file and check that the file paths exist.
*
* @param fnPath: The path to the json file.
* @param parameters: The parameters.
*/
void GetParameters(const fs::path fnPath, Parameters& parameters);
std::string DisplayParameters(Parameters& parameters);
/**
* Read six images forming a cubemap.
*
* @param fileNames: The paths to the six cubemap files.
*
* @return The cubemap texture.
*/
vtkNew<vtkTexture> ReadCubemap(std::vector<std::string> const& fileNames);
/**
* Read an equirectangular environment file and convert it to a texture.
*
* @param fileName: The equirectangular file path.
*
* @return The texture.
*/
vtkNew<vtkTexture> ReadEquirectangularFile(std::string const& fileName);
} // namespace
int main(int argc, char* argv[])
{
CLI::App app{
"Render a cube with custom texture mapping and a coat normal texture."};
// Define options
std::string fileName;
app.add_option("fileName", fileName,
"The path to the JSON file containing the parameters e.g. "
"PBR_Parameters.");
auto useEquirectangular{false};
app.add_flag("-e, --use_equirectangular", useEquirectangular,
"Use the equirectangular entry in the json file.");
CLI11_PARSE(app, argc, argv);
auto fnPath = fs::path(fileName);
if (!fnPath.has_extension())
{
fnPath.replace_extension(".json");
}
if (!fs::is_regular_file(fnPath))
{
std::cerr << "Unable to find: " << fnPath << std::endl;
return EXIT_FAILURE;
}
auto useCubemap = !useEquirectangular;
Parameters parameters;
GetParameters(fnPath, parameters);
if (!parameters.parsedOk)
{
return EXIT_FAILURE;
}
auto res = DisplayParameters(parameters);
std::cout << res << std::endl;
vtkNew<vtkNamedColors> colors;
colors->SetColor("Brass",
std::array<unsigned char, 4>{184, 115, 51, 255}.data());
vtkNew<vtkOpenGLRenderer> ren;
ren->SetBackground(colors->GetColor3d("Black").GetData());
ren->AutomaticLightCreationOff();
vtkNew<vtkLight> light;
light->SetPosition(2.0, 0.0, 2.0);
light->SetFocalPoint(0.0, 0.0, 0.0);
ren->AddLight(light);
vtkNew<vtkRenderWindow> renWin;
renWin->SetSize(600, 600);
renWin->AddRenderer(ren);
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(renWin);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
vtkSmartPointer<vtkPBRIrradianceTexture> irradiance =
ren->GetEnvMapIrradiance();
irradiance->SetIrradianceStep(0.3);
ren->UseSphericalHarmonicsOff();
vtkNew<vtkSkybox> skybox;
// Choose how to generate the skybox.
vtkNew<vtkTexture> cubeMap;
vtkNew<vtkTexture> envTexture;
auto isHDR = false;
auto hasSkybox = false;
auto gammaCorrect = false;
if (useCubemap && !parameters.cubemap.empty())
{
std::cout << "Using the cubemap files to generate the environment texture."
<< std::endl;
envTexture = ReadCubemap(parameters.cubemap);
if (parameters.skybox)
{
skybox->SetTexture(envTexture);
hasSkybox = true;
}
}
else if (parameters.parameters.find("equirectangular") !=
parameters.parameters.end() &&
!parameters.parameters["equirectangular"].empty())
{
std::cout
<< "Using the equirectangular file to generate the environment texture."
<< std::endl;
envTexture =
ReadEquirectangularFile(parameters.parameters["equirectangular"]);
std::string extension = fs::path(parameters.parameters["equirectangular"])
.extension()
.generic_string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](char c) { return std::tolower(c); });
std::string extensions{".hdr .pic"};
if (std::string(extensions).find(extension, 0) != std::string::npos)
{
isHDR = true;
gammaCorrect = true;
}
if (parameters.skybox)
{
skybox->SetTexture(envTexture);
skybox->SetFloorRight(0, 0, 1);
skybox->SetProjection(vtkSkybox::Sphere);
skybox->SetTexture(envTexture);
hasSkybox = true;
}
}
else
{
std::cerr << "An environment texture is required,\n"
<< "please add the necessary equirectangular"
<< " or cubemap file paths to the json file." << std::endl;
return EXIT_FAILURE;
}
ren->UseImageBasedLightingOn();
if (isHDR)
{
ren->UseSphericalHarmonicsOn();
ren->SetEnvironmentTexture(envTexture, false);
}
else
{
ren->UseSphericalHarmonicsOff();
ren->SetEnvironmentTexture(envTexture, true);
}
vtkNew<vtkCubeSource> cube;
vtkNew<vtkTriangleFilter> triangulation;
triangulation->SetInputConnection(cube->GetOutputPort());
vtkNew<vtkPolyDataTangents> tangents;
tangents->SetInputConnection(triangulation->GetOutputPort());
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(tangents->GetOutputPort());
vtkNew<vtkPNGReader> materialReader;
materialReader->SetFileName(parameters.parameters["material"].c_str());
vtkNew<vtkTexture> material;
material->InterpolateOn();
material->SetInputConnection(materialReader->GetOutputPort());
vtkNew<vtkPNGReader> albedoReader;
albedoReader->SetFileName(parameters.parameters["albedo"].c_str());
vtkNew<vtkTexture> albedo;
albedo->UseSRGBColorSpaceOn();
albedo->InterpolateOn();
albedo->SetInputConnection(albedoReader->GetOutputPort());
vtkNew<vtkPNGReader> normalReader;
normalReader->SetFileName(parameters.parameters["normal"].c_str());
// Uncomment this if you want a similar image to the VTK test image.
// vtkNew<vtkImageFlip> flip;
// flip->SetInputConnection(normalReader->GetOutputPort());
// flip->SetFilteredAxes(0);
vtkNew<vtkTexture> normal;
normal->InterpolateOn();
normal->SetInputConnection(normalReader->GetOutputPort());
vtkNew<vtkTexture> coatNormal;
coatNormal->InterpolateOn();
coatNormal->SetInputConnection(normalReader->GetOutputPort());
// Uncomment this if you want a similar image to the VTK test image.
// coatNormal->SetInputConnection(flip->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetOrientation(0.0, 25.0, 0.0);
actor->SetMapper(mapper);
actor->GetProperty()->SetInterpolationToPBR();
// Set metallic, roughness and coat strength to 1.0 as they act as multipliers
// with texture value.
actor->GetProperty()->SetMetallic(1.0);
actor->GetProperty()->SetRoughness(1.0);
actor->GetProperty()->SetCoatStrength(1.0);
actor->GetProperty()->SetCoatColor(colors->GetColor3d("Red").GetData());
actor->GetProperty()->SetBaseColorTexture(albedo);
actor->GetProperty()->SetORMTexture(material);
actor->GetProperty()->SetNormalTexture(normal);
actor->GetProperty()->SetCoatNormalTexture(coatNormal);
ren->AddActor(actor);
if (hasSkybox)
{
if (gammaCorrect)
{
skybox->GammaCorrectOn();
}
else
{
skybox->GammaCorrectOff();
}
ren->AddActor(skybox);
}
renWin->SetWindowName("PBR_Clear_Coat");
renWin->Render();
ren->GetActiveCamera()->Zoom(1.5);
iren->Start();
return EXIT_SUCCESS;
}
namespace {
void GetParameters(const fs::path fnPath, Parameters& parameters)
{
std::ifstream ifs(fnPath);
Json::Value root;
if (ifs)
{
std::string str;
std::string errors;
Json::CharReaderBuilder builder{};
auto reader = std::unique_ptr<Json::CharReader>(builder.newCharReader());
std::ostringstream ss;
ss << ifs.rdbuf(); // Read in the file comtents
str = ss.str();
auto parsingSuccessful =
reader->parse(str.c_str(), str.c_str() + str.size(), &root, &errors);
ifs.close();
if (!parsingSuccessful)
{
std::cout << errors << std::endl;
parameters.parsedOk = false;
return;
}
parameters.parsedOk = true;
}
else
{
std::cerr << "Unable to open: " << fnPath << std::endl;
parameters.parsedOk = false;
}
// Extract the values.
std::set<std::string> keysNoPaths{"title", "object", "objcolor", "bkgcolor",
"skybox"};
std::set<std::string> keysWithPaths{"cubemap", "equirectangular", "albedo",
"normal", "material", "coat",
"anisotropy", "emissive"};
fs::path cubemapPath;
for (Json::Value::const_iterator outer = root.begin(); outer != root.end();
++outer)
{
if (keysNoPaths.find(outer.name()) != keysNoPaths.end())
{
if (outer.name() == "skybox")
{
parameters.skybox = outer->asBool();
}
else
{
parameters.parameters[outer.name()] = outer->asString();
}
continue;
}
if (keysWithPaths.find(outer.name()) != keysWithPaths.end())
{
if (outer.name() == "cubemap")
{
std::string path;
for (Json::Value::const_iterator pth = root["cubemap"].begin();
pth != root["cubemap"].end(); ++pth)
{
if (pth.name() == "root")
{
cubemapPath = fs::path(pth->asString());
std::cout << path << std::endl;
}
if (pth.name() == "files")
{
for (Json::Value::const_iterator fls =
root["cubemap"]["files"].begin();
fls != root["cubemap"]["files"].end(); ++fls)
{
parameters.cubemap.push_back(fls->asString());
}
}
}
}
else
{
parameters.parameters[outer.name()] = outer->asString();
}
}
}
// Build and check the paths.
if (!parameters.cubemap.empty())
{
if (parameters.cubemap.size() != 6)
{
std::cerr << "There must be six filenames for the cubemap.";
parameters.parsedOk = false;
}
else
{
for (size_t i = 0; i < parameters.cubemap.size(); i++)
{
auto pth = fnPath.parent_path() / cubemapPath /
fs::path(parameters.cubemap[i]);
if (parameters.cubemap[i].empty())
{
std::cerr << "A missing path in the cubemap." << std::endl;
parameters.parsedOk = false;
}
else
{
parameters.cubemap[i] = pth.make_preferred().string();
if (!(fs::is_regular_file(pth) && fs::exists(pth)))
{
std::cerr << "Not a file or path does not exist: "
<< parameters.cubemap[i] << std::endl;
parameters.parsedOk = false;
}
}
}
}
}
// Check the remaining paths, we don't need cubemap now.
keysWithPaths.erase("cubemap");
for (auto& p : parameters.parameters)
{
if (keysWithPaths.find(p.first) != keysWithPaths.end())
{
if (p.second.empty())
{
std::cerr << "No path for the key " << p.first << std::endl;
parameters.parsedOk = false;
}
else
{
auto pth = fnPath.parent_path() / fs::path(p.second);
p.second = pth.make_preferred().string();
if (!(fs::is_regular_file(pth) && fs::exists(pth)))
{
std::cerr << "Not a file or path does not exist: " << p.second
<< std::endl;
parameters.parsedOk = false;
}
}
}
}
return;
}
std::string DisplayParameters(Parameters& parameters)
{
std::stringstream res;
std::vector<std::string> parameterKeys{
"title", "object", "objcolor", "bkgcolor", "skybox",
"cubemap", "equirectangular", "albedo", "normal", "material",
"coat", "anisotropy", "emissive"};
for (auto const& e : parameterKeys)
{
if (e == "cubemap")
{
for (std::vector<std::string>::const_iterator it =
parameters.cubemap.begin();
it != parameters.cubemap.end(); ++it)
{
if (it == parameters.cubemap.cbegin())
{
res << std::setw(15) << std::left << "cubemap"
<< ": " << *it << "\n";
}
else
{
res << std::setw(17) << " " << *it << "\n";
}
}
}
else if (e == "skybox")
{
res << std::setw(15) << std::left << e << ": " << std::boolalpha
<< parameters.skybox << "\n";
}
else
{
if (parameters.parameters.find(e) != parameters.parameters.end())
{
res << std::setw(15) << std::left << e << ": "
<< parameters.parameters[e] << "\n";
}
}
}
return res.str();
}
vtkNew<vtkTexture> ReadCubemap(std::vector<std::string> const& fileNames)
{
vtkNew<vtkTexture> cubeMap;
cubeMap->CubeMapOn();
auto i = 0;
for (auto const& fn : fileNames)
{
// Read the images
vtkNew<vtkImageReader2Factory> readerFactory;
vtkSmartPointer<vtkImageReader2> imgReader;
imgReader.TakeReference(readerFactory->CreateImageReader2(fn.c_str()));
imgReader->SetFileName(fn.c_str());
// Each image must be flipped in Y due to canvas
// versus vtk ordering.
vtkNew<vtkImageFlip> flip;
flip->SetInputConnection(imgReader->GetOutputPort());
flip->SetFilteredAxis(1); // flip y axis
cubeMap->SetInputConnection(i, flip->GetOutputPort(0));
++i;
}
cubeMap->MipmapOn();
cubeMap->InterpolateOn();
return cubeMap;
}
vtkNew<vtkTexture> ReadEquirectangularFile(std::string const& fileName)
{
vtkNew<vtkTexture> texture;
std::string extension = fs::path(fileName).extension().generic_string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](char c) { return std::tolower(c); });
if (std::string(".jpeg .jpg .png").find(extension, 0) != std::string::npos)
{
vtkNew<vtkImageReader2Factory> readerFactory;
vtkSmartPointer<vtkImageReader2> imgReader;
imgReader.TakeReference(
readerFactory->CreateImageReader2(fileName.c_str()));
imgReader->SetFileName(fileName.c_str());
texture->SetInputConnection(imgReader->GetOutputPort());
}
else
{
vtkNew<vtkHDRReader> reader;
auto extensions = reader->GetFileExtensions();
if (std::string(extensions).find(extension, 0) != std::string::npos)
{
if (reader->CanReadFile(fileName.c_str()))
{
reader->SetFileName(fileName.c_str());
texture->SetInputConnection(reader->GetOutputPort());
texture->SetColorModeToDirectScalars();
}
else
{
std::cerr << "Unable to read the file: " << fileName << std::endl;
return texture;
}
}
}
texture->MipmapOn();
texture->InterpolateOn();
return texture;
}
} // namespace
CMakeLists.txt¶
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(PBR_Clear_Coat)
find_package(VTK COMPONENTS
)
if (NOT VTK_FOUND)
message(FATAL_ERROR "PBR_Clear_Coat: Unable to find the VTK build folder.")
endif()
# Prevent a "command line is too long" failure in Windows.
set(CMAKE_NINJA_FORCE_RESPONSE_FILE "ON" CACHE BOOL "Force Ninja to use response files.")
add_executable(PBR_Clear_Coat MACOSX_BUNDLE PBR_Clear_Coat.cxx )
target_link_libraries(PBR_Clear_Coat PRIVATE ${VTK_LIBRARIES}
)
# vtk_module_autoinit is needed
vtk_module_autoinit(
TARGETS PBR_Clear_Coat
MODULES ${VTK_LIBRARIES}
)
Download and Build PBR_Clear_Coat¶
Click here to download PBR_Clear_Coat and its CMakeLists.txt file. Once the tarball PBR_Clear_Coat.tar has been downloaded and extracted,
cd PBR_Clear_Coat/build
If VTK is installed:
cmake ..
If VTK is not installed but compiled on your system, you will need to specify the path to your VTK build:
cmake -DVTK_DIR:PATH=/home/me/vtk_build ..
Build the project:
make
and run it:
./PBR_Clear_Coat
WINDOWS USERS
Be sure to add the VTK bin directory to your path. This will resolve the VTK dll's at run time.