PBR Clear Coat
Repository source: 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_Parameters.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_Parameters.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.
Question
If you have a question about this example, please use the VTK Discourse Forum
Code¶
PBR_Clear_Coat.py
#!/usr/bin/env python3
import json
from dataclasses import dataclass
from pathlib import Path
from vtkmodules.util.execution_model import select_ports
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkFiltersCore import (
vtkPolyDataTangents,
vtkTriangleFilter
)
from vtkmodules.vtkFiltersSources import vtkCubeSource
from vtkmodules.vtkIOImage import (
vtkHDRReader,
vtkImageReader2Factory,
vtkPNGReader
)
from vtkmodules.vtkImagingCore import vtkImageFlip
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkLight,
vtkPolyDataMapper,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkSkybox,
vtkTexture
)
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLRenderer
def get_program_parameters():
import argparse
description = 'Render a cube with custom texture mapping and a coat normal texture.'
epilogue = '''
'''
parser = argparse.ArgumentParser(description=description, epilog=epilogue,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('file_name',
help='The path to the JSON file e.g. PBR_Parameters.json.')
parser.add_argument('-e', '--use_equirectangular', action='store_true',
help='Use the equirectangular entry in the json file.')
args = parser.parse_args()
return args.file_name, args.use_equirectangular
def main():
fn, use_cubemap = get_program_parameters()
use_cubemap = not use_cubemap
fn_path = Path(fn)
if not fn_path.suffix:
fn_path = fn_path.with_suffix(".json")
if not fn_path.is_file():
print('Unable to find: ', fn_path)
paths_ok, parameters = get_parameters(fn_path)
if not paths_ok:
return
res = display_parameters(parameters)
print('\n'.join(res))
print()
colors = vtkNamedColors()
light = vtkLight()
light.SetPosition(2.0, 0.0, 2.0)
light.SetFocalPoint(0.0, 0.0, 0.0)
ren = vtkOpenGLRenderer(background=colors.GetColor3d('Black'), automatic_light_creation=False)
ren.AddLight(light)
ren_win = vtkRenderWindow(size=(600, 600), window_name='PBR_Anisotropy')
ren_win.AddRenderer(ren)
iren = vtkRenderWindowInteractor()
iren.render_window = ren_win
style = vtkInteractorStyleTrackballCamera()
iren.interactor_style = style
skybox = vtkSkybox()
irradiance = ren.GetEnvMapIrradiance()
irradiance.SetIrradianceStep(0.3)
# Choose how to generate the skybox.
is_hdr = False
has_skybox = False
gamma_correct = False
if use_cubemap and 'cubemap' in parameters.keys():
print('Using the cubemap files to generate the environment texture.')
env_texture = read_cubemap(parameters['cubemap'])
if parameters['skybox']:
skybox.texture = env_texture
has_skybox = True
elif 'equirectangular' in parameters.keys():
print('Using the equirectangular file to generate the environment texture.')
env_texture = read_equirectangular_file(parameters['equirectangular'])
if parameters['equirectangular'].suffix.lower() in '.hdr .pic':
gamma_correct = True
is_hdr = True
if parameters['skybox']:
# Generate a skybox.
skybox.floor_right = (0, 0, 1)
skybox.projection = vtkSkybox.Sphere
skybox.texture = env_texture
has_skybox = True
else:
print('An environment texture is required,\n'
'please add the necessary equirectangular'
' or cubemap file paths to the json file.')
return
# Turn off the default lighting and use image based lighting.
# ren.automatic_light_creation = False
ren.use_image_based_lighting = True
if is_hdr:
ren.use_spherical_harmonics = True
ren.SetEnvironmentTexture(env_texture, False)
else:
ren.use_spherical_harmonics = False
ren.SetEnvironmentTexture(env_texture, True)
cube = vtkCubeSource()
triangulation = vtkTriangleFilter()
tangents = vtkPolyDataTangents()
mapper = vtkPolyDataMapper()
cube >> triangulation >> tangents >> mapper
material_reader = vtkPNGReader(file_name=parameters['material'])
material = vtkTexture(interpolate=True)
material_reader >> material
albedo_reader = vtkPNGReader(file_name=parameters['albedo'])
albedo = vtkTexture(use_srgb_color_space=True, interpolate=True)
albedo_reader >> albedo
normal_reader = vtkPNGReader(file_name=parameters['normal'])
normal = vtkTexture(interpolate=True)
normal_reader >> normal
coat_normal = vtkTexture(interpolate=True)
normal_reader >> coat_normal
# Uncomment this if you want a similar image to the VTK test image.
# flip = vtkImageFlip(filtered_axes=0)
# normal_reader >> flip >> coat_normal
actor = vtkActor(mapper=mapper, orientation=(0.0, 25.0, 0.0))
actor.property.interpolation = Property.Interpolation.VTK_PBR
# Set metallic, roughness and coat strength to 1.0 as they act as multipliers
# with texture value.
actor.property.metallic = 1.0
actor.property.roughness = 1.0
actor.property.coat_strength = 1.0
actor.property.color = colors.GetColor3d('Red')
actor.property.base_color_texture = albedo
actor.property.orm_texture = material
actor.property.normal_texture = normal
actor.property.coat_normal_texture = coat_normal
ren.AddActor(actor)
if has_skybox:
if gamma_correct:
skybox.gamma_correct = True
else:
skybox.gamma_correct = False
ren.AddActor(skybox)
ren_win.Render()
ren.active_camera.Zoom(1.5)
# ren_win.Render()
iren.Start()
def get_parameters(fn_path):
"""
Read the parameters from a JSON file and check that the file paths exist.
:param fn_path: The path to the JSON file.
:return: True if the paths correspond to files and the parameters.
"""
with open(fn_path) as data_file:
json_data = json.load(data_file)
parameters = dict()
# Extract the values.
keys_no_paths = {'title', 'object', 'objcolor', 'bkgcolor', 'skybox'}
keys_with_paths = {'cubemap', 'equirectangular', 'albedo', 'normal', 'material', 'coat', 'anisotropy', 'emissive'}
paths_ok = True
for k, v in json_data.items():
if k in keys_no_paths:
parameters[k] = v
continue
if k in keys_with_paths:
if k == 'cubemap':
if ('root' in v) and ('files' in v):
root = fn_path.parent / Path(v['root'])
if not root.exists():
print(f'Bad cubemap path: {root}')
paths_ok = False
elif len(v['files']) != 6:
print(f'Expect six cubemap file names.')
paths_ok = False
else:
cm = list(map(lambda p: root / p, v['files']))
for fn in cm:
if not fn.is_file():
paths_ok = False
print(f'Not a file {fn}')
if paths_ok:
parameters['cubemap'] = cm
else:
paths_ok = False
print('Missing the key "root" and/or the key "fĂles" for the cubemap.')
else:
fn = fn_path.parent / Path(v)
if not fn.exists():
print(f'Bad {k} path: {fn}')
paths_ok = False
else:
parameters[k] = fn
# Set Boy as the default surface.
if ('object' in parameters.keys() and not parameters['object']) or 'object' not in parameters.keys():
parameters['object'] = 'Boy'
return paths_ok, parameters
def display_parameters(parameters):
res = list()
parameter_keys = ['title', 'object', 'objcolor', 'bkgcolor', 'skybox', 'cubemap', 'equirectangular', 'albedo',
'normal', 'material', 'coat', 'anisotropy', 'emissive']
for k in parameter_keys:
if k != 'cubemap':
if k in parameters:
res.append(f'{k:15}: {parameters[k]}')
else:
if k in parameters:
for idx in range(len(parameters[k])):
if idx == 0:
res.append(f'{k:15}: {parameters[k][idx]}')
else:
res.append(f'{" " * 17}{parameters[k][idx]}')
return res
def read_cubemap(cubemap):
"""
Read six images forming a cubemap.
:param cubemap: The paths to the six cubemap files.
:return: The cubemap texture.
"""
cube_map = vtkTexture(mipmap=True, interpolate=True, cube_map=True)
i = 0
for fn in cubemap:
# Read the images.
img_reader = vtkImageReader2Factory().CreateImageReader2(str(fn))
img_reader.file_name = str(fn)
# Each image must be flipped in Y due to canvas
# versus vtk ordering.
flip = vtkImageFlip(filtered_axis=1)
img_reader >> flip
flip >> select_ports(i, cube_map)
i += 1
return cube_map
def read_equirectangular_file(fn_path):
"""
Read an equirectangular environment file and convert to a texture.
:param fn_path: The equirectangular file path.
:return: The texture.
"""
texture = vtkTexture(mipmap=True, interpolate=True)
suffix = fn_path.suffix.lower()
if suffix in ['.jpeg', '.jpg', '.png']:
img_reader = vtkImageReader2Factory().CreateImageReader2(str(fn_path))
img_reader.file_name = str(fn_path)
select_ports(img_reader, 0) >> texture
else:
reader = vtkHDRReader()
extensions = reader.GetFileExtensions()
# Check the image can be read.
if not reader.CanReadFile(str(fn_path)):
print('CanReadFile failed for ', fn_path)
return None
if suffix not in extensions:
print('Unable to read this file extension: ', suffix)
return None
reader.file_name = str(fn_path)
texture.color_mode = Texture.ColorMode.VTK_COLOR_MODE_DIRECT_SCALARS
reader >> texture
return texture
@dataclass(frozen=True)
class Property:
@dataclass(frozen=True)
class Interpolation:
VTK_FLAT: int = 0
VTK_GOURAUD: int = 1
VTK_PHONG: int = 2
VTK_PBR: int = 3
@dataclass(frozen=True)
class Representation:
VTK_POINTS: int = 0
VTK_WIREFRAME: int = 1
VTK_SURFACE: int = 2
@dataclass(frozen=True)
class Texture:
@dataclass(frozen=True)
class Quality:
VTK_TEXTURE_QUALITY_DEFAULT: int = 0
VTK_TEXTURE_QUALITY_16BIT: int = 16
VTK_TEXTURE_QUALITY: int = 32
@dataclass(frozen=True)
class ColorMode:
VTK_COLOR_MODE_DEFAULT: int = 0
VTK_COLOR_MODE_MAP_SCALARS: int = 1
VTK_COLOR_MODE_DIRECT_SCALARS: int = 2
if __name__ == '__main__':
main()