/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "model/material.h" #include "rendering/renderer.h" #include "utilities/parser.h" #include "utilities/utilities.h" #include "utilities/Logs.h" #include "scene/sn_utils.h" #include "utilities/Globals.h" opengl_material::path_data opengl_material::paths; opengl_material::opengl_material() { for (size_t i = 0; i < params.size(); i++) params[i] = glm::vec4(std::numeric_limits::quiet_NaN()); } bool opengl_material::deserialize( cParser &Input, bool const Loadnow ) { parse_info = std::make_unique(); bool result { false }; while( true == deserialize_mapping( Input, 0, Loadnow ) ) { result = true; // once would suffice but, eh } if( path == -1 && ( update_on_weather_change || update_on_season_change ) ) { // record current texture path in the material, potentially needed when material is reloaded on environment change // NOTE: we're storing this only for textures that can actually change, to keep the size of path database modest auto const lookup{ paths.index_map.find( Global.asCurrentTexturePath ) }; if( lookup != paths.index_map.end() ) { path = lookup->second; } else { path = paths.data.size(); paths.data.emplace_back( Global.asCurrentTexturePath ); paths.index_map.emplace( Global.asCurrentTexturePath, path ); } } return result; } void opengl_material::log_error(const std::string &str) { ErrorLog("bad material: " + name + ": " + str, logtype::material); } std::map texture_bindings { { "diffuse", 0 }, { "normals", 1 }, { "normalmap", 1 } }; int opengl_material::texture_slot( std::string const &Key ) { auto key { Key }; if( ( key.size() > 0 ) && ( key[ 0 ] != '_' ) ) { // numbered binding, "textureN:" fills the slot N-1 key.erase( key.find_first_not_of( "-1234567890" ) ); size_t const num = std::stoi( key ) - 1; if( num < gl::MAX_TEXTURES ) { return static_cast( num ); } log_error( "invalid texture binding: " + std::to_string( num ) ); is_good = false; return -1; } if( key.size() > 2 ) { // named binding, the slot it fills is up to the shader key.erase( 0, 1 ); key.pop_back(); if( shader && ( shader->texture_conf.find( key ) != shader->texture_conf.end() ) ) { return static_cast( shader->texture_conf[ key ].id ); } auto const lookup { texture_bindings.find( key ) }; if( ( shader == nullptr ) && ( lookup != texture_bindings.end() ) ) { return lookup->second; } // ignore unrecognized texture bindings in legacy render mode, it's most likely data for more advanced shaders if( Global.GfxRenderer == "default" ) { log_error( "unknown texture binding: " + key ); is_good = false; } return -1; } log_error( "unrecognized texture binding: " + key ); is_good = false; return -1; } void opengl_material::build_texture_variants( std::vector const &Bindings, bool const Loadnow ) { if( true == Bindings.empty() ) { return; } // build a texture set for each entry of the largest of the declared pools. // pools of different size can be combined, shorter ones simply repeat from the start std::size_t variantcount { 0 }; for( auto const &[ slot, pool ] : Bindings ) { variantcount = std::max( variantcount, pool->size() ); } texture_variants.resize( variantcount, textures ); for( auto const &[ slot, pool ] : Bindings ) { for( std::size_t variantindex = 0; variantindex < variantcount; ++variantindex ) { auto const &texturename { ( *pool )[ variantindex % pool->size() ] }; auto const texturehandle { GfxRenderer->Fetch_Texture( texturename, Loadnow ) }; if( texturehandle == null_handle ) { // a single bad entry shouldn't take down the whole material, fall back to the basic texture log_error( "missing texture variant: " + texturename ); continue; } texture_variants[ variantindex ][ slot ] = texturehandle; } } } void opengl_material::finalize(bool Loadnow) { is_good = true; if (parse_info) { // texture bindings which come with a pool of alternatives, gathered while resolving regular bindings std::vector variantbindings; for( auto const &[ bindingkey, binding ] : parse_info->tex_mapping ) { auto const slot { texture_slot( bindingkey ) }; if( slot < 0 ) { continue; } textures[ slot ] = GfxRenderer->Fetch_Texture( binding.name, Loadnow ); if( binding.variants.size() > 1 ) { variantbindings.emplace_back( static_cast( slot ), &( binding.variants ) ); } } build_texture_variants( variantbindings, Loadnow ); if (!shader) { // TODO: add error severity to logging, re-enable these errors as low severity messages if (textures[0] == null_handle) { // log_error("shader not specified, assuming \"default_0\""); shader = GfxRenderer->Fetch_Shader("default_0"); } else if (textures[1] == null_handle) { // log_error("shader not specified, assuming \"default_1\""); shader = GfxRenderer->Fetch_Shader("default_1"); } else if (textures[2] == null_handle) { // log_error("shader not specified, assuming \"default_2\""); shader = GfxRenderer->Fetch_Shader("default_2"); } } // TBD, TODO: move material validation to renderer, to eliminate branching? if( Global.GfxRenderer == "default" ) { if( !shader ) { is_good = false; return; } for (auto it : parse_info->param_mapping) { std::string key = it.first; glm::vec4 value = it.second.data; if (key.size() > 1 && key[0] != '_') { size_t num = std::stoi(key) - 1; if (num < gl::MAX_PARAMS) { params[num] = value; } else { log_error("invalid param binding: " + std::to_string(num)); is_good = false; } } else if (key.size() > 2) { key.erase(0, 1); key.pop_back(); if (shader->param_conf.find(key) != shader->param_conf.end()) { gl::shader::param_entry entry = shader->param_conf[key]; for (size_t i = 0; i < entry.size; i++) params[entry.location][entry.offset + i] = value[i]; } else { log_error("unknown param binding: " + key); is_good = false; } } else { log_error("unrecognized param binding: " + key); is_good = false; } } } parse_info.reset(); } if( Global.GfxRenderer != "default" ) { // basic texture validation for legacy branch for( auto texturehandle : textures ) { if( texturehandle == null_handle ) { break; } if( GfxRenderer->Texture( texturehandle ).get_id() <= 0 ) { is_good = false; } } return; } if( !shader ) { is_good = false; return; } for (auto it : shader->param_conf) { gl::shader::param_entry entry = it.second; if (std::isnan(params[entry.location][entry.offset])) { float value = std::numeric_limits::quiet_NaN(); if (entry.defaultparam == gl::shader::defaultparam_e::one) value = 1.0f; else if (entry.defaultparam == gl::shader::defaultparam_e::zero) value = 0.0f; else if (entry.defaultparam == gl::shader::defaultparam_e::required) log_error("unspecified required param: " + it.first); else if (entry.defaultparam != gl::shader::defaultparam_e::nan) { params_state.push_back(entry); continue; } for (size_t i = 0; i < entry.size; i++) params[entry.location][entry.offset + i] = value; } } for (auto it : shader->texture_conf) { gl::shader::texture_entry &entry = it.second; texture_handle handle = textures[entry.id]; // NOTE: texture validation at this stage relies on forced texture load behaviour during its create() call // TODO: move texture id validation to later stage if/when deferred texture loading is implemented if( handle && GfxRenderer->Texture(handle).get_id() > 0 ) { GfxRenderer->Texture(handle).set_components_hint((GLint)entry.components); } else { log_error("missing texture: " + it.first); is_good = false; } } } bool opengl_material::update() { auto const texturepathbackup { Global.asCurrentTexturePath }; auto const namebackup { name }; auto const pathbackup { path }; cParser materialparser( name + ".mat", cParser::buffer_FILE ); // fairly safe to presume .mat is present for branching materials // temporarily set texture path to state recorded in the material Global.asCurrentTexturePath = paths.data[ path ]; // clean material slate, restore relevant members *this = opengl_material(); name = namebackup; path = pathbackup; auto result { false }; if( true == deserialize( materialparser, true ) ) { try { finalize( true ); result = true; } catch( gl::shader_exception const &e ) { ErrorLog( "invalid shader: " + std::string( e.what() ) ); } } // restore texture path Global.asCurrentTexturePath = texturepathbackup; return result; } std::unordered_set seasons = { "winter:", "spring:", "summer:", "autumn:" }; bool is_season( std::string const &String ) { return seasons.find(String) != seasons.end(); } std::unordered_set weather = { "clear:", "cloudy:", "rain:", "snow:" }; bool is_weather( std::string const &String ) { return weather.find(String) != weather.end(); } // imports member data pair from the config file void opengl_material::deserialize_texture_mapping( cParser &Input, std::string const &Key, int const Priority ) { // a texture binding can be provided as a pool of alternatives, "textureN_variants: [ a b c ]". // unlike a regular random set, which is resolved once when the material is loaded and thus shared // by everything using it, such pool is preserved whole and one entry is picked per model instance auto key { Key }; auto const variantmarker { key.find( "_variants" ) }; auto const isvariantset { variantmarker != std::string::npos }; if( true == isvariantset ) { // reduce the key to its plain form, "N_variants:" becomes "N:" key.erase( variantmarker, std::string( "_variants" ).size() ); } std::vector variants; std::string value; if( false == isvariantset ) { value = deserialize_random_set( Input ); replace_slashes( value ); } else { variants = deserialize_set( Input ); for( auto &variant : variants ) { replace_slashes( variant ); } if( true == variants.empty() ) { log_error( "empty texture variant pool: " + key ); return; } // first entry doubles as the regular, variant-agnostic texture of the material value = variants.front(); } auto const [ it, inserted ] { parse_info->tex_mapping.try_emplace( key, parse_info_s::tex_def{ value, Priority, variants } ) }; if( ( false == inserted ) && ( Priority > it->second.priority ) ) { it->second = parse_info_s::tex_def{ value, Priority, variants }; } } bool opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow ) { // NOTE: comma can be part of legacy file names, so we don't treat it as a separator here auto key { Input.getToken( true, "\n\r\t ;[]" ) }; // key can be an actual key or block end if( true == key.empty() || key == "}" ) { return false; } if( Priority != -1 ) { // regular attribute processing mode // mark potential material change update_on_weather_change |= is_weather( key ); update_on_season_change |= is_season( key ); if( key == Global.Weather ) { // weather textures override generic (pri 0) and seasonal (pri 1) textures // seasonal weather textures (pri 1+2=3) override generic weather (pri 2) textures // skip the opening bracket auto const value { Input.getToken( true, "\n\r\t ;" ) }; while( true == deserialize_mapping( Input, Priority + 2, Loadnow ) ) { ; // all work is done in the header } } else if( key == Global.Season ) { // seasonal textures override generic textures // skip the opening bracket auto const value { Input.getToken( true, "\n\r\t ;" ) }; while( true == deserialize_mapping( Input, Priority + 1, Loadnow ) ) { ; // all work is done in the header } } else if (key.compare(0, 7, "texture") == 0) { key.erase(0, 7); deserialize_texture_mapping( Input, key, Priority ); } else if (key.compare(0, 5, "param") == 0) { key.erase(0, 5); std::string value = Input.getToken( true, "\n\r\t;" ); std::istringstream stream(value); glm::vec4 data; stream >> data.r; stream >> data.g; stream >> data.b; stream >> data.a; auto it = parse_info->param_mapping.find(key); if (it == parse_info->param_mapping.end()) parse_info->param_mapping.emplace(std::make_pair(key, parse_info_s::param_def({ data, Priority }))); else if (Priority > it->second.priority) { parse_info->param_mapping.erase(it); parse_info->param_mapping.emplace(std::make_pair(key, parse_info_s::param_def({ data, Priority }))); } } else if (key == "shader:" && (!shader || Priority > m_shader_priority)) { try { std::string value = deserialize_random_set( Input ); shader = GfxRenderer->Fetch_Shader(value); m_shader_priority = Priority; } catch (gl::shader_exception const &e) { log_error("invalid shader: " + std::string(e.what())); } } else if (key == "opacity:" && Priority > m_opacity_priority) { std::string value = deserialize_random_set( Input ); opacity = std::stof(value); //m7t: handle exception m_opacity_priority = Priority; } else if (key == "selfillum:" && Priority > m_selfillum_priority) { std::string value = deserialize_random_set( Input ); selfillum = std::stof(value); //m7t: handle exception m_selfillum_priority = Priority; } else if (key == "glossiness:" && Priority > m_glossiness_priority) { std::string value = deserialize_random_set( Input ); glossiness = std::stof(value); //m7t: handle exception m_glossiness_priority = Priority; } else if (key == "shadow_rank:") { auto const value { deserialize_random_set( Input ) }; shadow_rank = std::stof(value); //m7t: handle exception } else if( key == "size:" ) { Input.getTokens( 2 ); Input >> size.x >> size.y; } else { auto const value = Input.getToken( true, "\n\r\t ;" ); if( value == "{" ) { // unrecognized or ignored token, but comes with attribute block and potential further nesting // go through it and discard the content while( true == deserialize_mapping( Input, -1, Loadnow ) ) { ; // all work is done in the header } } } } else { // discard mode; ignores all retrieved tokens auto const value { Input.getToken( true, "\n\r\t ;" ) }; if( value == "{" ) { // ignored tokens can come with their own blocks, ignore these recursively // go through it and discard the content while( true == deserialize_mapping( Input, -1, Loadnow ) ) { ; // all work is done in the header } } } return true; // return value marks a key: value pair was extracted, nothing about whether it's recognized } float opengl_material::get_or_guess_opacity() const { if( opacity ) { return opacity.value(); } if (textures[0] != null_handle) { auto const &tex = GfxRenderer->Texture(textures[0]); if (tex.get_has_alpha()) return 0.0f; else return 0.5f; } return 0.5f; } bool opengl_material::is_translucent() const { return textures[0] != null_handle ? GfxRenderer->Texture(textures[0]).get_has_alpha() : false; } // create material object from data stored in specified file. // NOTE: the deferred load parameter is passed to textures defined by material, the material itself is always loaded immediately material_handle material_manager::create( std::string const &Filename, bool const Loadnow ) { auto filename { Filename }; if( contains( filename, '|' ) ) { filename.erase( filename.find( '|' ) ); // po | może być nazwa kolejnej tekstury } // discern references to textures generated by a script // TBD: support file: for file resources? auto const isgenerated { filename.find( "make:" ) == 0 || filename.find( "internal_src:" ) == 0 }; // process supplied resource name if( isgenerated ) { // generated resource // scheme:(user@)path?query // remove scheme indicator filename.erase( 0, filename.find(':') + 1 ); // TBD, TODO: allow shader specification as part of the query? erase_leading_slashes( filename ); } else { // regular file resource // (filepath/)filename.extension erase_extension( filename ); replace_slashes( filename ); erase_leading_slashes( filename ); } auto const databanklookup { find_in_databank( filename ) }; if( databanklookup != null_handle ) { return databanklookup; } opengl_material material; material_handle materialhandle { null_handle }; auto const locator { isgenerated ? std::make_pair( filename, "make:" ) : find_on_disk( filename ) }; if( false == isgenerated && false == locator.first.empty() ) { // try to parse located file resource cParser materialparser( locator.first + locator.second, cParser::buffer_FILE ); if( true == material.deserialize( materialparser, Loadnow ) ) { material.name = locator.first; } } else { // if there's no .mat file, this can be either autogenerated texture, // or legacy method of referring just to diffuse texture directly. // wrap basic material around it in either case auto const texturehandle { GfxRenderer->Fetch_Texture( Filename, Loadnow ) }; if( texturehandle != null_handle ) { // use texture path and name to tell the newly created materials apart material.name = GfxRenderer->Texture( texturehandle ).get_name(); /* // material would attach default shader anyway, but it would spit to error log try { material.shader = GfxRenderer->Fetch_Shader("default_1"); } catch (gl::shader_exception const &e) { ErrorLog("invalid shader: " + std::string(e.what())); } } */ // HACK: create parse info for material finalize() method cParser materialparser( "texture1: \"" + Filename + "\"", cParser::buffer_TEXT ); material.deserialize( materialparser, Loadnow ); } } if( false == material.name.empty() ) { // if we have material name and shader it means resource was processed succesfully. // // IMPORTANT: capture the handle ONLY after emplace_back succeeds. The // previous version pre-assigned `materialhandle = m_materials.size()` // and then ran `finalize(Loadnow)` (which can throw shader_exception // when a shader fails to compile) and `emplace_back` together inside // the try. If finalize() threw, emplace_back never ran, but the // handle was already set to size() -- pointing one past the actual // last element. Subsequent `m_materials[handle]` lookups would then // read garbage past end-of-vector, producing the classic 0x30-offset // access violation seen in TSubModel::BinInit (vtable read on a // bogus IMaterial pointer). Now we leave materialhandle as // null_handle if anything throws, and the caller treats it as a // failed-to-load material. try { material.finalize(Loadnow); materialhandle = m_materials.size(); m_materials.emplace_back( std::move(material) ); m_materialmappings.emplace( m_materials.back().name, materialhandle ); } catch (gl::shader_exception const &e) { ErrorLog("invalid shader: " + std::string(e.what())); // record the failure so subsequent Fetch_Material(filename) calls // short-circuit without re-running the failing compile. m_materialmappings.emplace( material.name, null_handle ); } } else { // otherwise record our failure to process the resource, to speed up subsequent attempts m_materialmappings.emplace( filename, materialhandle ); } return materialhandle; }; material_handle material_manager::create_from_text( std::string const &Name, std::string const &Definition ) { opengl_material material; cParser materialparser( Definition, cParser::buffer_TEXT ); if( false == material.deserialize( materialparser, true ) ) { return null_handle; } material.name = Name; try { material.finalize( true ); } catch( gl::shader_exception const &e ) { ErrorLog( "invalid shader: " + std::string( e.what() ) ); return null_handle; } auto const materialhandle { static_cast( m_materials.size() ) }; m_materials.emplace_back( std::move( material ) ); // not listed under its name: the name isn't a file which could be asked for return materialhandle; } material_handle material_manager::create_matte( material_handle const Material ) { if( Material == null_handle || Material >= m_materials.size() ) { return Material; } auto const lookup { m_mattes.find( Material ) }; if( lookup != m_mattes.end() ) { return lookup->second; } auto const &source { m_materials[ Material ] }; opengl_material matte; matte.textures = source.textures; matte.texture_variants = source.texture_variants; matte.params = source.params; matte.params_state = source.params_state; matte.shader = source.shader; matte.opacity = source.opacity; matte.selfillum = source.selfillum; matte.glossiness = 0.1f; matte.shadow_rank = source.shadow_rank; matte.name = source.name + ":matte"; matte.size = source.size; matte.is_good = source.is_good; matte.path = source.path; if( matte.shader ) { for( auto const &[ key, value ] : std::vector>{ { "weathering", glm::vec4{ 1.f } } } ) { auto const entry { matte.shader->param_conf.find( key ) }; if( entry == matte.shader->param_conf.end() ) { continue; } for( std::size_t i = 0; i < entry->second.size; ++i ) { matte.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast( i ) ]; } matte.params_state.erase( std::remove_if( std::begin( matte.params_state ), std::end( matte.params_state ), [&]( gl::shader::param_entry const &State ) { return State.location == entry->second.location && State.offset == entry->second.offset; } ), std::end( matte.params_state ) ); } } auto const mattehandle { static_cast( m_materials.size() ) }; m_materials.emplace_back( std::move( matte ) ); m_mattes.emplace( Material, mattehandle ); return mattehandle; } void material_manager::on_weather_change() { for( auto &material : m_materials ) { if( material.update_on_weather_change ) { material.update(); } } } void material_manager::on_season_change() { for( auto &material : m_materials ) { if( material.update_on_season_change ) { material.update(); } } } // checks whether specified material is in the material bank. returns handle to the material, or a null handle material_handle material_manager::find_in_databank( std::string const &Materialname ) const { std::vector const filenames { Global.asCurrentTexturePath + Materialname, Materialname, paths::textures + Materialname }; for( auto const &filename : filenames ) { auto const lookup { m_materialmappings.find( filename ) }; if( lookup != m_materialmappings.end() ) { return lookup->second; } } // all lookups failed return null_handle; } // checks whether specified file exists. // NOTE: technically could be static, but we might want to switch from global texture path to instance-specific at some point std::pair material_manager::find_on_disk( std::string const &Materialname ) { auto const materialname { ToLower( Materialname ) }; return FileExists({Global.asCurrentTexturePath + materialname, materialname, paths::textures + materialname}, {".mat"}); } //---------------------------------------------------------------------------