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.
This commit is contained in:
rdb 2025-12-22 11:04:35 +01:00
parent bd1105d545
commit 04e5cbe706
16 changed files with 176 additions and 40 deletions

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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 {

View File

@ -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 << ")**";

View File

@ -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 {

View File

@ -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.

View File

@ -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) {

View File

@ -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,

View File

@ -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);
load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_DIFFUSE);
// 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);
// 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);
}

View File

@ -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);

View File

@ -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)