Browse Source

Add support for (occlusion-)metallic-roughness textures

Existing "selector" mode (which was unused and has been used to
implement ORM textures has been renamed to metallic-roughness, and new
modes have been added for occlusion and occlusion-metallic-roughness.
New GLSL input names have been added.  The .egg format has been
extended.  The Assimp loader has been updated.  The shader generator now
handles occlusion maps, and a unit test for that is added.
rdb 2 days ago
parent
commit
04e5cbe706

+ 25 - 2
panda/src/display/graphicsStateGuardian.cxx

@@ -2496,7 +2496,7 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler,
       return default_normal_height_tex;
     }
 
-  case Shader::STO_stage_selector_i:
+  case Shader::STO_stage_metallic_roughness_i:
     {
       const TextureAttrib *texattrib;
       if (_target_rs->get_attrib(texattrib)) {
@@ -2505,7 +2505,8 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler,
           TextureStage *stage = texattrib->get_on_stage(i);
           TextureStage::Mode mode = stage->get_mode();
 
-          if (mode == TextureStage::M_selector) {
+          if (mode == TextureStage::M_metallic_roughness ||
+              mode == TextureStage::M_occlusion_metallic_roughness) {
             if (si++ == spec._stage) {
               sampler = texattrib->get_on_sampler(stage);
               view += stage->get_tex_view_offset();
@@ -2538,6 +2539,28 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler,
     }
     break;
 
+  case Shader::STO_stage_occlusion_i:
+    {
+      const TextureAttrib *texattrib;
+      if (_target_rs->get_attrib(texattrib)) {
+        int si = 0;
+        for (int i = 0; i < texattrib->get_num_on_stages(); ++i) {
+          TextureStage *stage = texattrib->get_on_stage(i);
+          TextureStage::Mode mode = stage->get_mode();
+
+          if (mode == TextureStage::M_occlusion ||
+              mode == TextureStage::M_occlusion_metallic_roughness) {
+            if (si++ == spec._stage) {
+              sampler = texattrib->get_on_sampler(stage);
+              view += stage->get_tex_view_offset();
+              return texattrib->get_on_texture(stage);
+            }
+          }
+        }
+      }
+    }
+    break;
+
   default:
     nassertr(false, nullptr);
     break;

+ 4 - 1
panda/src/doc/eggSyntax.txt

@@ -339,7 +339,10 @@ appear before they are referenced.
      *GLOW
      *GLOSS
      *HEIGHT
-     *SELECTOR
+     *EMISSION
+     *METALLIC_ROUGHNESS
+     *OCCLUSION
+     *OCCLUSION_METALLIC_ROUGHNESS
 
     The default environment type is MODULATE, which means the texture
     color is multiplied with the base polygon (or vertex) color.  This

+ 20 - 7
panda/src/egg/eggTexture.cxx

@@ -561,11 +561,11 @@ affects_polygon_alpha() const {
   case ET_height:
   case ET_normal_gloss:
   case ET_emission:
+  case ET_occlusion:
+  case ET_metallic_roughness:
+  case ET_occlusion_metallic_roughness:
     return false;
 
-  case ET_selector:
-    return true;
-
   case ET_unspecified:
     break;
   }
@@ -883,8 +883,9 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "height") == 0) {
     return ET_height;
 
-  } else if (cmp_nocase_uh(string, "selector") == 0) {
-    return ET_selector;
+  } else if (cmp_nocase_uh(string, "metallic_roughness") == 0 ||
+             cmp_nocase_uh(string, "selector") == 0) {
+    return ET_metallic_roughness;
 
   } else if (cmp_nocase_uh(string, "normal_gloss") == 0) {
     return ET_normal_gloss;
@@ -892,6 +893,12 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "emission") == 0) {
     return ET_emission;
 
+  } else if (cmp_nocase_uh(string, "occlusion") == 0) {
+    return ET_occlusion;
+
+  } else if (cmp_nocase_uh(string, "occlusion_metallic_roughness") == 0) {
+    return ET_occlusion_metallic_roughness;
+
   } else {
     return ET_unspecified;
   }
@@ -1321,14 +1328,20 @@ ostream &operator << (ostream &out, EggTexture::EnvType type) {
   case EggTexture::ET_height:
     return out << "height";
 
-  case EggTexture::ET_selector:
-    return out << "selector";
+  case EggTexture::ET_metallic_roughness:
+    return out << "metallic_roughness";
 
   case EggTexture::ET_normal_gloss:
     return out << "normal_gloss";
 
   case EggTexture::ET_emission:
     return out << "emission";
+
+  case EggTexture::ET_occlusion:
+    return out << "occlusion";
+
+  case EggTexture::ET_occlusion_metallic_roughness:
+    return out << "occlusion_metallic_roughness";
   }
 
   nassertr(false, out);

+ 4 - 1
panda/src/egg/eggTexture.h

@@ -106,9 +106,12 @@ PUBLISHED:
     ET_glow,
     ET_gloss,
     ET_height,
-    ET_selector,
+    ET_metallic_roughness,
     ET_normal_gloss,
     ET_emission,
+    ET_occlusion,
+    ET_occlusion_metallic_roughness,
+    ET_selector = ET_metallic_roughness,
   };
   enum CombineMode {
     CM_unspecified,

+ 10 - 2
panda/src/egg2pg/eggLoader.cxx

@@ -1579,8 +1579,8 @@ make_texture_stage(const EggTexture *egg_tex) {
     stage->set_mode(TextureStage::M_height);
     break;
 
-  case EggTexture::ET_selector:
-    stage->set_mode(TextureStage::M_selector);
+  case EggTexture::ET_metallic_roughness:
+    stage->set_mode(TextureStage::M_metallic_roughness);
     break;
 
   case EggTexture::ET_normal_gloss:
@@ -1591,6 +1591,14 @@ make_texture_stage(const EggTexture *egg_tex) {
     stage->set_mode(TextureStage::M_emission);
     break;
 
+  case EggTexture::ET_occlusion:
+    stage->set_mode(TextureStage::M_occlusion);
+    break;
+
+  case EggTexture::ET_occlusion_metallic_roughness:
+    stage->set_mode(TextureStage::M_occlusion_metallic_roughness);
+    break;
+
   case EggTexture::ET_unspecified:
     break;
   }

+ 8 - 2
panda/src/egg2pg/eggSaver.cxx

@@ -852,8 +852,8 @@ convert_primitive(const GeomVertexData *vertex_data,
         case TextureStage::M_height:
           egg_tex->set_env_type(EggTexture::ET_height);
           break;
-        case TextureStage::M_selector:
-          egg_tex->set_env_type(EggTexture::ET_selector);
+        case TextureStage::M_metallic_roughness:
+          egg_tex->set_env_type(EggTexture::ET_metallic_roughness);
           break;
         case TextureStage::M_normal_gloss:
           egg_tex->set_env_type(EggTexture::ET_normal_gloss);
@@ -861,6 +861,12 @@ convert_primitive(const GeomVertexData *vertex_data,
         case TextureStage::M_emission:
           egg_tex->set_env_type(EggTexture::ET_emission);
           break;
+        case TextureStage::M_occlusion:
+          egg_tex->set_env_type(EggTexture::ET_occlusion);
+          break;
+        case TextureStage::M_occlusion_metallic_roughness:
+          egg_tex->set_env_type(EggTexture::ET_occlusion_metallic_roughness);
+          break;
         default:
           break;
         }

+ 6 - 2
panda/src/glstuff/glShaderContext_src.cxx

@@ -1043,8 +1043,9 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) {
         else if (noprefix.compare(7, string::npos, "Height") == 0) {
           bind._part = Shader::STO_stage_height_i;
         }
-        else if (noprefix.compare(7, string::npos, "Selector") == 0) {
-          bind._part = Shader::STO_stage_selector_i;
+        else if (noprefix.compare(7, string::npos, "MetallicRoughness") == 0 ||
+                 noprefix.compare(7, string::npos, "Selector") == 0) {
+          bind._part = Shader::STO_stage_metallic_roughness_i;
         }
         else if (noprefix.compare(7, string::npos, "Gloss") == 0) {
           bind._part = Shader::STO_stage_gloss_i;
@@ -1052,6 +1053,9 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) {
         else if (noprefix.compare(7, string::npos, "Emission") == 0) {
           bind._part = Shader::STO_stage_emission_i;
         }
+        else if (noprefix.compare(7, string::npos, "Occlusion") == 0) {
+          bind._part = Shader::STO_stage_occlusion_i;
+        }
         else {
           GLCAT.error()
             << "Unrecognized shader input name: p3d_" << noprefix << "\n";

+ 2 - 1
panda/src/gobj/shader.h

@@ -232,9 +232,10 @@ public:
     STO_stage_add_i,
     STO_stage_normal_i,
     STO_stage_height_i,
-    STO_stage_selector_i,
+    STO_stage_metallic_roughness_i,
     STO_stage_gloss_i,
     STO_stage_emission_i,
+    STO_stage_occlusion_i,
   };
 
   enum ShaderArgClass {

+ 8 - 2
panda/src/gobj/textureStage.cxx

@@ -519,14 +519,20 @@ operator << (ostream &out, TextureStage::Mode mode) {
   case TextureStage::M_height:
     return out << "height";
 
-  case TextureStage::M_selector:
-    return out << "selector";
+  case TextureStage::M_metallic_roughness:
+    return out << "metallic_roughness";
 
   case TextureStage::M_normal_gloss:
     return out << "normal_gloss";
 
   case TextureStage::M_emission:
     return out << "emission";
+
+  case TextureStage::M_occlusion:
+    return out << "occlusion";
+
+  case TextureStage::M_occlusion_metallic_roughness:
+    return out << "occlusion_metallic_roughness";
   }
 
   return out << "**invalid Mode(" << (int)mode << ")**";

+ 5 - 1
panda/src/gobj/textureStage.h

@@ -61,10 +61,14 @@ PUBLISHED:
     M_glow,         // Rarely used: modulate_glow  is more efficient.
     M_gloss,        // Rarely used: modulate_gloss is more efficient.
     M_height,       // Rarely used: normal_height  is more efficient.
-    M_selector,
+    M_metallic_roughness, // metalness in B, roughness in G
     M_normal_gloss,
 
     M_emission,
+    M_occlusion, // In red channel
+    M_occlusion_metallic_roughness,
+
+    M_selector = M_metallic_roughness,
   };
 
   enum CombineMode {

+ 3 - 1
panda/src/grutil/multitexReducer.cxx

@@ -712,9 +712,11 @@ make_texture_layer(const NodePath &render,
   case TextureStage::M_glow:
   case TextureStage::M_gloss:
   case TextureStage::M_height:
-  case TextureStage::M_selector:
+  case TextureStage::M_metallic_roughness:
   case TextureStage::M_normal_gloss:
   case TextureStage::M_emission:
+  case TextureStage::M_occlusion:
+  case TextureStage::M_occlusion_metallic_roughness:
     // Don't know what to do with these funny modes.  We should probably raise
     // an exception or something.  Fall through for now.
 

+ 17 - 2
panda/src/pgraphnodes/shaderGenerator.cxx

@@ -491,6 +491,10 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
     case TextureStage::M_emission:
       info._flags = ShaderKey::TF_map_emission;
       break;
+    case TextureStage::M_occlusion:
+    case TextureStage::M_occlusion_metallic_roughness:
+      info._flags = ShaderKey::TF_map_occlusion;
+      break;
     default:
       break;
     }
@@ -557,9 +561,13 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
 
   // Decide whether to separate ambient and diffuse calculations.
   if (have_ambient) {
-    if (key._material_flags & Material::F_ambient) {
+    if (key._texture_flags & ShaderKey::TF_map_occlusion) {
       key._have_separate_ambient = true;
-    } else {
+    }
+    else if (key._material_flags & Material::F_ambient) {
+      key._have_separate_ambient = true;
+    }
+    else {
       if (key._material_flags & Material::F_diffuse) {
         key._have_separate_ambient = true;
       } else {
@@ -1599,6 +1607,13 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
       text << "\t result *= saturate(2 * (tex" << map_index_glow << ".a - 0.5));\n";
     }
     if (key._have_separate_ambient) {
+      // Apply occlusion textures.
+      for (size_t i = 0; i < key._textures.size(); ++i) {
+        ShaderKey::TextureInfo &tex = key._textures[i];
+        if (tex._flags & ShaderKey::TF_map_occlusion) {
+          text << "\t tot_ambient *= tex" << i << ".r;\n";
+        }
+      }
       if (key._material_flags & Material::F_ambient) {
         text << "\t result += tot_ambient * attr_material[0];\n";
       } else if (key._color_type == ColorAttrib::T_vertex) {

+ 1 - 0
panda/src/pgraphnodes/shaderGenerator.h

@@ -111,6 +111,7 @@ protected:
       TF_map_glow     = 0x080,
       TF_map_gloss    = 0x100,
       TF_map_emission = 0x001000000,
+      TF_map_occlusion = 0x002000000,
       TF_uses_color   = 0x200,
       TF_uses_primary_color = 0x400,
       TF_uses_last_saved_result = 0x800,

+ 33 - 14
pandatool/src/assimp/assimpLoader.cxx

@@ -58,6 +58,10 @@
 #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0
 #endif
 
+#ifndef AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0
+#endif
+
 #ifndef AI_MATKEY_GLTF_ALPHAMODE
 #define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
 #endif
@@ -325,11 +329,13 @@ load_texture(size_t index) {
 
 /**
  * Converts an aiMaterial into a RenderState.
+ * The dummy argument exists so we can pass a MATKEY macro into this function
+ * instead of just a texture type.
  */
 void AssimpLoader::
-load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype,
-                   TextureStage::Mode mode, CPT(TextureAttrib) &tattr,
-                   CPT(TexMatrixAttrib) &tmattr) {
+load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode,
+                   CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr,
+                   const aiTextureType &ttype, unsigned int dummy) {
   aiString path;
   aiTextureMapping mapping;
   unsigned int uvindex;
@@ -596,20 +602,33 @@ load_material(size_t index) {
   // And let's not forget the textures!
   CPT(TextureAttrib) tattr = DCAST(TextureAttrib, TextureAttrib::make());
   CPT(TexMatrixAttrib) tmattr;
-  load_texture_stage(mat, aiTextureType_DIFFUSE, TextureStage::M_modulate, tattr, tmattr);
-
-  // Check for an ORM map, from the glTF/OBJ importer.  glTF also puts it in the
-  // LIGHTMAP slot, despite only having the lightmap in the red channel, so we
-  // have to ignore it.
-  if (mat.GetTextureCount(aiTextureType_UNKNOWN) > 0) {
-    load_texture_stage(mat, aiTextureType_UNKNOWN, TextureStage::M_selector, tattr, tmattr);
+  load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_DIFFUSE);
+
+  // Check for an ORM map, from the glTF/OBJ importer.  glTF also erroneously
+  // puts an occlusion texture in the LIGHTMAP slot.
+  aiString roughness_path, occlusion_path;
+  aiTextureMapping roughness_mapping, occlusion_mapping;
+  unsigned int roughness_uv, occlusion_uv;
+  if (AI_SUCCESS == mat.GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &roughness_path, &roughness_mapping, &roughness_uv)) {
+    if (AI_SUCCESS == mat.GetTexture(aiTextureType_LIGHTMAP, 0, &occlusion_path, &occlusion_mapping, &occlusion_uv)) {
+      if (roughness_path == occlusion_path &&
+          roughness_mapping == occlusion_mapping &&
+          roughness_uv == occlusion_uv) {
+        load_texture_stage(mat, TextureStage::M_occlusion_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+      } else {
+        load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+        load_texture_stage(mat, TextureStage::M_occlusion, tattr, tmattr, aiTextureType_LIGHTMAP);
+      }
+    } else {
+      load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE);
+    }
   } else {
-    load_texture_stage(mat, aiTextureType_LIGHTMAP, TextureStage::M_modulate, tattr, tmattr);
+    load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_LIGHTMAP);
   }
 
-  load_texture_stage(mat, aiTextureType_NORMALS, TextureStage::M_normal, tattr, tmattr);
-  load_texture_stage(mat, aiTextureType_EMISSIVE, TextureStage::M_emission, tattr, tmattr);
-  load_texture_stage(mat, aiTextureType_HEIGHT, TextureStage::M_height, tattr, tmattr);
+  load_texture_stage(mat, TextureStage::M_normal, tattr, tmattr, aiTextureType_NORMALS);
+  load_texture_stage(mat, TextureStage::M_emission, tattr, tmattr, aiTextureType_EMISSIVE);
+  load_texture_stage(mat, TextureStage::M_height, tattr, tmattr, aiTextureType_HEIGHT);
   if (tattr->get_num_on_stages() > 0) {
     state = state->add_attrib(tattr);
   }

+ 3 - 3
pandatool/src/assimp/assimpLoader.h

@@ -78,9 +78,9 @@ private:
   const aiNode *find_node(const aiNode &root, const aiString &name);
 
   void load_texture(size_t index);
-  void load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype,
-                          TextureStage::Mode mode, CPT(TextureAttrib) &tattr,
-                          CPT(TexMatrixAttrib) &tmattr);
+  void load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode,
+                          CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr,
+                          const aiTextureType &ttype, unsigned int dummy=0);
   void load_material(size_t index);
   void create_joint(Character *character, CharacterJointBundle *bundle, PartGroup *parent, const aiNode &node);
   void create_anim_channel(const aiAnimation &anim, AnimBundle *bundle, AnimGroup *parent, const aiNode &node);

+ 28 - 0
tests/display/test_color_buffer.py

@@ -367,3 +367,31 @@ def test_color_transparency_no_light(color_region, shader_attrib):
     )
     result = render_color_pixel(color_region, state)
     assert result.x == pytest.approx(1.0, 0.1)
+
+
+def test_texture_occlusion(color_region):
+    shader_attrib = core.ShaderAttrib.make_default().set_shader_auto(True)
+
+    tex = core.Texture("occlusion")
+    tex.set_clear_color((0.5, 1.0, 1.0, 1.0))
+    stage = core.TextureStage("occlusion")
+    stage.set_mode(core.TextureStage.M_occlusion)
+    texture_attrib = core.TextureAttrib.make().add_on_stage(stage, tex)
+
+    mat = core.Material()
+    mat.diffuse = (0, 1, 0, 1)
+    mat.ambient = (0, 0, 1, 1)
+    material_attrib = core.MaterialAttrib.make(mat)
+
+    alight = core.AmbientLight("ambient")
+    alight.set_color((0, 0, 1, 1))
+    light_attrib = core.LightAttrib.make().add_on_light(core.NodePath(alight))
+
+    state = core.RenderState.make(
+        shader_attrib,
+        material_attrib,
+        texture_attrib,
+        light_attrib
+    )
+    result = render_color_pixel(color_region, state)
+    assert result.z == pytest.approx(0.5, 0.05)