Просмотр исходного кода

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 1 неделя назад
Родитель
Сommit
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;
       return default_normal_height_tex;
     }
     }
 
 
-  case Shader::STO_stage_selector_i:
+  case Shader::STO_stage_metallic_roughness_i:
     {
     {
       const TextureAttrib *texattrib;
       const TextureAttrib *texattrib;
       if (_target_rs->get_attrib(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 *stage = texattrib->get_on_stage(i);
           TextureStage::Mode mode = stage->get_mode();
           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) {
             if (si++ == spec._stage) {
               sampler = texattrib->get_on_sampler(stage);
               sampler = texattrib->get_on_sampler(stage);
               view += stage->get_tex_view_offset();
               view += stage->get_tex_view_offset();
@@ -2538,6 +2539,28 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler,
     }
     }
     break;
     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:
   default:
     nassertr(false, nullptr);
     nassertr(false, nullptr);
     break;
     break;

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

@@ -339,7 +339,10 @@ appear before they are referenced.
      *GLOW
      *GLOW
      *GLOSS
      *GLOSS
      *HEIGHT
      *HEIGHT
-     *SELECTOR
+     *EMISSION
+     *METALLIC_ROUGHNESS
+     *OCCLUSION
+     *OCCLUSION_METALLIC_ROUGHNESS
 
 
     The default environment type is MODULATE, which means the texture
     The default environment type is MODULATE, which means the texture
     color is multiplied with the base polygon (or vertex) color.  This
     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_height:
   case ET_normal_gloss:
   case ET_normal_gloss:
   case ET_emission:
   case ET_emission:
+  case ET_occlusion:
+  case ET_metallic_roughness:
+  case ET_occlusion_metallic_roughness:
     return false;
     return false;
 
 
-  case ET_selector:
-    return true;
-
   case ET_unspecified:
   case ET_unspecified:
     break;
     break;
   }
   }
@@ -883,8 +883,9 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "height") == 0) {
   } else if (cmp_nocase_uh(string, "height") == 0) {
     return ET_height;
     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) {
   } else if (cmp_nocase_uh(string, "normal_gloss") == 0) {
     return ET_normal_gloss;
     return ET_normal_gloss;
@@ -892,6 +893,12 @@ string_env_type(const string &string) {
   } else if (cmp_nocase_uh(string, "emission") == 0) {
   } else if (cmp_nocase_uh(string, "emission") == 0) {
     return ET_emission;
     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 {
   } else {
     return ET_unspecified;
     return ET_unspecified;
   }
   }
@@ -1321,14 +1328,20 @@ ostream &operator << (ostream &out, EggTexture::EnvType type) {
   case EggTexture::ET_height:
   case EggTexture::ET_height:
     return out << "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:
   case EggTexture::ET_normal_gloss:
     return out << "normal_gloss";
     return out << "normal_gloss";
 
 
   case EggTexture::ET_emission:
   case EggTexture::ET_emission:
     return out << "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);
   nassertr(false, out);

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

@@ -106,9 +106,12 @@ PUBLISHED:
     ET_glow,
     ET_glow,
     ET_gloss,
     ET_gloss,
     ET_height,
     ET_height,
-    ET_selector,
+    ET_metallic_roughness,
     ET_normal_gloss,
     ET_normal_gloss,
     ET_emission,
     ET_emission,
+    ET_occlusion,
+    ET_occlusion_metallic_roughness,
+    ET_selector = ET_metallic_roughness,
   };
   };
   enum CombineMode {
   enum CombineMode {
     CM_unspecified,
     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);
     stage->set_mode(TextureStage::M_height);
     break;
     break;
 
 
-  case EggTexture::ET_selector:
-    stage->set_mode(TextureStage::M_selector);
+  case EggTexture::ET_metallic_roughness:
+    stage->set_mode(TextureStage::M_metallic_roughness);
     break;
     break;
 
 
   case EggTexture::ET_normal_gloss:
   case EggTexture::ET_normal_gloss:
@@ -1591,6 +1591,14 @@ make_texture_stage(const EggTexture *egg_tex) {
     stage->set_mode(TextureStage::M_emission);
     stage->set_mode(TextureStage::M_emission);
     break;
     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:
   case EggTexture::ET_unspecified:
     break;
     break;
   }
   }

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

@@ -852,8 +852,8 @@ convert_primitive(const GeomVertexData *vertex_data,
         case TextureStage::M_height:
         case TextureStage::M_height:
           egg_tex->set_env_type(EggTexture::ET_height);
           egg_tex->set_env_type(EggTexture::ET_height);
           break;
           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;
           break;
         case TextureStage::M_normal_gloss:
         case TextureStage::M_normal_gloss:
           egg_tex->set_env_type(EggTexture::ET_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:
         case TextureStage::M_emission:
           egg_tex->set_env_type(EggTexture::ET_emission);
           egg_tex->set_env_type(EggTexture::ET_emission);
           break;
           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:
         default:
           break;
           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) {
         else if (noprefix.compare(7, string::npos, "Height") == 0) {
           bind._part = Shader::STO_stage_height_i;
           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) {
         else if (noprefix.compare(7, string::npos, "Gloss") == 0) {
           bind._part = Shader::STO_stage_gloss_i;
           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) {
         else if (noprefix.compare(7, string::npos, "Emission") == 0) {
           bind._part = Shader::STO_stage_emission_i;
           bind._part = Shader::STO_stage_emission_i;
         }
         }
+        else if (noprefix.compare(7, string::npos, "Occlusion") == 0) {
+          bind._part = Shader::STO_stage_occlusion_i;
+        }
         else {
         else {
           GLCAT.error()
           GLCAT.error()
             << "Unrecognized shader input name: p3d_" << noprefix << "\n";
             << "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_add_i,
     STO_stage_normal_i,
     STO_stage_normal_i,
     STO_stage_height_i,
     STO_stage_height_i,
-    STO_stage_selector_i,
+    STO_stage_metallic_roughness_i,
     STO_stage_gloss_i,
     STO_stage_gloss_i,
     STO_stage_emission_i,
     STO_stage_emission_i,
+    STO_stage_occlusion_i,
   };
   };
 
 
   enum ShaderArgClass {
   enum ShaderArgClass {

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

@@ -519,14 +519,20 @@ operator << (ostream &out, TextureStage::Mode mode) {
   case TextureStage::M_height:
   case TextureStage::M_height:
     return out << "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:
   case TextureStage::M_normal_gloss:
     return out << "normal_gloss";
     return out << "normal_gloss";
 
 
   case TextureStage::M_emission:
   case TextureStage::M_emission:
     return out << "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 << ")**";
   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_glow,         // Rarely used: modulate_glow  is more efficient.
     M_gloss,        // Rarely used: modulate_gloss is more efficient.
     M_gloss,        // Rarely used: modulate_gloss is more efficient.
     M_height,       // Rarely used: normal_height  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_normal_gloss,
 
 
     M_emission,
     M_emission,
+    M_occlusion, // In red channel
+    M_occlusion_metallic_roughness,
+
+    M_selector = M_metallic_roughness,
   };
   };
 
 
   enum CombineMode {
   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_glow:
   case TextureStage::M_gloss:
   case TextureStage::M_gloss:
   case TextureStage::M_height:
   case TextureStage::M_height:
-  case TextureStage::M_selector:
+  case TextureStage::M_metallic_roughness:
   case TextureStage::M_normal_gloss:
   case TextureStage::M_normal_gloss:
   case TextureStage::M_emission:
   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
     // Don't know what to do with these funny modes.  We should probably raise
     // an exception or something.  Fall through for now.
     // 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:
     case TextureStage::M_emission:
       info._flags = ShaderKey::TF_map_emission;
       info._flags = ShaderKey::TF_map_emission;
       break;
       break;
+    case TextureStage::M_occlusion:
+    case TextureStage::M_occlusion_metallic_roughness:
+      info._flags = ShaderKey::TF_map_occlusion;
+      break;
     default:
     default:
       break;
       break;
     }
     }
@@ -557,9 +561,13 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
 
 
   // Decide whether to separate ambient and diffuse calculations.
   // Decide whether to separate ambient and diffuse calculations.
   if (have_ambient) {
   if (have_ambient) {
-    if (key._material_flags & Material::F_ambient) {
+    if (key._texture_flags & ShaderKey::TF_map_occlusion) {
       key._have_separate_ambient = true;
       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) {
       if (key._material_flags & Material::F_diffuse) {
         key._have_separate_ambient = true;
         key._have_separate_ambient = true;
       } else {
       } 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";
       text << "\t result *= saturate(2 * (tex" << map_index_glow << ".a - 0.5));\n";
     }
     }
     if (key._have_separate_ambient) {
     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) {
       if (key._material_flags & Material::F_ambient) {
         text << "\t result += tot_ambient * attr_material[0];\n";
         text << "\t result += tot_ambient * attr_material[0];\n";
       } else if (key._color_type == ColorAttrib::T_vertex) {
       } 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_glow     = 0x080,
       TF_map_gloss    = 0x100,
       TF_map_gloss    = 0x100,
       TF_map_emission = 0x001000000,
       TF_map_emission = 0x001000000,
+      TF_map_occlusion = 0x002000000,
       TF_uses_color   = 0x200,
       TF_uses_color   = 0x200,
       TF_uses_primary_color = 0x400,
       TF_uses_primary_color = 0x400,
       TF_uses_last_saved_result = 0x800,
       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
 #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0
 #endif
 #endif
 
 
+#ifndef AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0
+#endif
+
 #ifndef AI_MATKEY_GLTF_ALPHAMODE
 #ifndef AI_MATKEY_GLTF_ALPHAMODE
 #define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
 #define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
 #endif
 #endif
@@ -325,11 +329,13 @@ load_texture(size_t index) {
 
 
 /**
 /**
  * Converts an aiMaterial into a RenderState.
  * 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::
 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;
   aiString path;
   aiTextureMapping mapping;
   aiTextureMapping mapping;
   unsigned int uvindex;
   unsigned int uvindex;
@@ -596,20 +602,33 @@ load_material(size_t index) {
   // And let's not forget the textures!
   // And let's not forget the textures!
   CPT(TextureAttrib) tattr = DCAST(TextureAttrib, TextureAttrib::make());
   CPT(TextureAttrib) tattr = DCAST(TextureAttrib, TextureAttrib::make());
   CPT(TexMatrixAttrib) tmattr;
   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 {
   } 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) {
   if (tattr->get_num_on_stages() > 0) {
     state = state->add_attrib(tattr);
     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);
   const aiNode *find_node(const aiNode &root, const aiString &name);
 
 
   void load_texture(size_t index);
   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 load_material(size_t index);
   void create_joint(Character *character, CharacterJointBundle *bundle, PartGroup *parent, const aiNode &node);
   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);
   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)
     result = render_color_pixel(color_region, state)
     assert result.x == pytest.approx(1.0, 0.1)
     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)