PBR Textures

Problem

Panda3D materials already support assigning constant values for Physically-Based Rendering (PBR) parameters, and these parameters get passed to shaders. However, this system does not take textures into account.

Questions

How are albedo/specular/roughness/metallic maps specified, ie. which texture map corresponds to a given PBR input?
  • Store the textures on Material, instead of TextureAttrib?
  • Make it possible to specially indicate these types of maps on TextureAttrib/TextureStage?
  • Store information about which TextureStage to use on the Material? (Does this add anything?)
How will a shader know if/when to do a texture lookup for a PBR input?
  • Have booleans for each texture and do branching in shaders?
  • Convert constant properties to small textures and always do texture lookups?
How do we handle textures that contain multiple inputs (e.g., a behavior texture)?
  • Specify a sampler and channel for each attribute?
  • Always assume a specific layout? (glTF does this with a metallicRoughnessTexture)
Do we support other flows than the UE4 model of merging diffuse/specular with metalness selector?
  • The metalness flow uses a binary "metalness" selector map to change the interpretation of the color texture: if 1, the color texture contains the specular color, if 0, it indicates the albedo.  It is popular and we should support it, but in multi-material models it suffers from bleeding artifacts due to specular colours bleeding into albedo colours at the transition between metal/dielectric.
  • CryEngine uses separate diffuse/specular textures and no metalness map.  This uses texture space less optimally, but does not suffer from the above issue.  It also gives the freedom to use non-constant specular colors for dielectrics.
What do we do if a constant and a texture is specified for an input?
  • Use the constant value as a factor? (glTF does this)
  • Ignore the constant value?
Do we want to add support for other maps such as normal, occlusion, and emissive?

Solutions

Specifying PBR maps

Via TextureAttrib
One way to assign PBR texture maps to a model would be to use “singleton” texture stages, similar to TextureStage.getDefault(), which would be ignored by the regular texturing pipeline and can be accessed by named shader inputs such as p3d_MetallicRoughnessTexture.  This could be implemented without significantly changing the way textures are stored in Panda.
model.set_texture(TextureStage.get_roughness(), my_texture)
 
More elaborate example:
  color_tex = loader.loadTexture("model_basecol.png", color_space=CS_srgb)
  pbr_tex = loader.loadTexture("model_omr.png", color_space=CS_linear)

  path.set_texture(TextureStage.default, color_tex)
  path.set_texture(TextureStage.metallic_roughness, pbr_tex, texcoord="0")
  path.set_texture(TextureStage.occlusion, pbr_tex, texcoord="1"

The reason for using a single TextureStage (rather than multiple that can have a M_metallic_roughness mode, etc) is that a shader is typically written to use only one of these maps, and this way a sub-node can easily override the texture in this slot that is specified on a parent node.  It’s also easier than requiring the developer to have to create and manage TextureStages to apply simple texture maps.

The default texture stage is used for the base color, so that traditional models specifying a simple texture will still show up when used with PBR shaders.

The metallic_roughness texture takes its values from the blue and green channels, respectively.  The occlusion texture takes its values from the red channel, so that a single texture could be applied to both slots.  This matches glTF and UE4.

(To make it easier to load separate maps into the same texture, we could introduce a syntax to easily combine these maps, eg.)
  pbr_tex = loader.loadTexture(redPath="occlusion.png", greenPath="roughness.png")

The p3d_MetallicRoughnessTexture, p3d_NormalTexture, p3d_OcclusionTexture, and p3d_EmissiveTexture shader inputs are mapped to these fixed texture stages.  Geometry that didn’t specify these maps will instead receive a 1x1 texture mapped to a white color (or #7f7fff in the case of a normal map).

The existing material shader inputs are not intended to be used as factors, as glTF expects; querying metallic or emissive on the default material produces 0, not 1, which will cancel out the effect of any texture that is applied.  We could use different inputs to allow supporting both, which will instead default to 1 when the respective input is absent and the corresponding texture is provided.

The following additional shader inputs would be added to that effect:
Value if =>
Neither specified
Material specified
Texture specified
Both specified