mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-08 23:07:38 +01:00
The copies don't update themselves, their names aren't files to read again: after a change of the weather or the season the material got its new textures and its copies kept the old ones. They are made again from it then, under their handles. primitive() of an empty handle or one past the banks read out of the bank list; it gives no_primitive there, -1 from the renderer.
874 lines
31 KiB
C++
874 lines
31 KiB
C++
/*
|
|
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<float>::quiet_NaN());
|
|
}
|
|
|
|
bool
|
|
opengl_material::deserialize( cParser &Input, bool const Loadnow ) {
|
|
parse_info = std::make_unique<parse_info_s>();
|
|
|
|
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<std::string, int> 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<int>( 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<int>( 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<variant_binding> 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<variant_binding> 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<std::size_t>( 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<float>::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<std::string> seasons = {
|
|
"winter:", "spring:", "summer:", "autumn:"
|
|
};
|
|
|
|
bool is_season( std::string const &String ) {
|
|
|
|
return seasons.find(String) != seasons.end();
|
|
}
|
|
|
|
std::unordered_set<std::string> 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<std::string> 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<std::string>( 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<std::string>( 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<std::string>( 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<std::string>( 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<std::string>( 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<std::string>( 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<material_handle>( 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;
|
|
}
|
|
|
|
namespace {
|
|
|
|
opengl_material
|
|
variant_of( opengl_material const &Source, std::string const &Suffix ) {
|
|
|
|
opengl_material variant;
|
|
variant.textures = Source.textures;
|
|
variant.texture_variants = Source.texture_variants;
|
|
variant.params = Source.params;
|
|
variant.params_state = Source.params_state;
|
|
variant.shader = Source.shader;
|
|
variant.opacity = Source.opacity;
|
|
variant.selfillum = Source.selfillum;
|
|
variant.shadow_rank = Source.shadow_rank;
|
|
variant.glossiness = Source.glossiness;
|
|
variant.name = Source.name + Suffix;
|
|
variant.size = Source.size;
|
|
variant.is_good = Source.is_good;
|
|
variant.path = Source.path;
|
|
return variant;
|
|
}
|
|
|
|
// whether another param of the shader lies on a component of the named one, which a value set for that one would change too
|
|
bool
|
|
shares_slot( std::unordered_map<std::string, gl::shader::param_entry> const &Conf, std::string const &Name ) {
|
|
|
|
auto const own { Conf.find( Name ) };
|
|
if( own == Conf.end() ) {
|
|
return false;
|
|
}
|
|
return std::any_of(
|
|
std::begin( Conf ), std::end( Conf ),
|
|
[&]( auto const &Entry ) {
|
|
return Entry.first != Name
|
|
&& Entry.second.location == own->second.location
|
|
&& Entry.second.offset < own->second.offset + own->second.size
|
|
&& own->second.offset < Entry.second.offset + Entry.second.size; } );
|
|
}
|
|
|
|
// the highlights of a copy of a material damped and its weathering on
|
|
void
|
|
make_matte( opengl_material &Variant ) {
|
|
|
|
Variant.glossiness = 0.1f;
|
|
if( !Variant.shader ) {
|
|
return;
|
|
}
|
|
for( auto const &[ key, value ] : std::vector<std::pair<std::string, glm::vec4>>{ { "weathering", glm::vec4{ 1.f } } } ) {
|
|
auto const entry { Variant.shader->param_conf.find( key ) };
|
|
if( entry == Variant.shader->param_conf.end()
|
|
|| shares_slot( Variant.shader->param_conf, key ) ) {
|
|
continue;
|
|
}
|
|
for( std::size_t i = 0; i < entry->second.size; ++i ) {
|
|
Variant.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast<glm::length_t>( i ) ];
|
|
}
|
|
Variant.params_state.erase(
|
|
std::remove_if(
|
|
std::begin( Variant.params_state ), std::end( Variant.params_state ),
|
|
[&]( gl::shader::param_entry const &State ) {
|
|
return State.location == entry->second.location && State.offset == entry->second.offset; } ),
|
|
std::end( Variant.params_state ) );
|
|
}
|
|
}
|
|
|
|
// the colour in the rgb of the slot of the weathering of a copy of a material, which tells the shader to tint by its sign. returns:
|
|
// whether the shader can tint it, the weathering and the tint alone in that slot
|
|
bool
|
|
make_tinted( opengl_material &Variant, glm::vec3 const &Colour ) {
|
|
|
|
if( !Variant.shader ) {
|
|
return false;
|
|
}
|
|
auto const &conf { Variant.shader->param_conf };
|
|
auto const weathering { conf.find( "weathering" ) };
|
|
auto const tint { conf.find( "tint" ) };
|
|
if( weathering == conf.end()
|
|
|| tint == conf.end()
|
|
|| weathering->second.offset != 3
|
|
|| tint->second.location != weathering->second.location
|
|
|| tint->second.offset != 0
|
|
|| std::any_of(
|
|
std::begin( conf ), std::end( conf ),
|
|
[&]( auto const &Entry ) {
|
|
return Entry.first != weathering->first && Entry.first != tint->first && Entry.second.location == weathering->second.location; } ) ) {
|
|
return false;
|
|
}
|
|
auto const location { weathering->second.location };
|
|
Variant.params[ location ] = glm::vec4{ Colour, -1.f };
|
|
Variant.params_state.erase(
|
|
std::remove_if(
|
|
std::begin( Variant.params_state ), std::end( Variant.params_state ),
|
|
[&]( gl::shader::param_entry const &State ) {
|
|
return State.location == location; } ),
|
|
std::end( Variant.params_state ) );
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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 matte { variant_of( m_materials[ Material ], ":matte" ) };
|
|
make_matte( matte );
|
|
auto const mattehandle { static_cast<material_handle>( m_materials.size() ) };
|
|
m_materials.emplace_back( std::move( matte ) );
|
|
m_mattes.emplace( Material, mattehandle );
|
|
return mattehandle;
|
|
}
|
|
|
|
material_handle
|
|
material_manager::create_tinted( material_handle const Material, glm::vec3 const &Colour ) {
|
|
|
|
if( Material == null_handle || Material >= m_materials.size() || !m_materials[ Material ].shader ) {
|
|
return Material;
|
|
}
|
|
auto const key { std::make_tuple( Material, Colour.r, Colour.g, Colour.b ) };
|
|
if( auto const lookup { m_tints.find( key ) }; lookup != m_tints.end() ) {
|
|
return lookup->second;
|
|
}
|
|
auto tinted { variant_of( m_materials[ Material ], ":tinted" ) };
|
|
if( false == make_tinted( tinted, Colour ) ) {
|
|
return Material;
|
|
}
|
|
auto const tintedhandle { static_cast<material_handle>( m_materials.size() ) };
|
|
m_materials.emplace_back( std::move( tinted ) );
|
|
m_tints.emplace( key, tintedhandle );
|
|
return tintedhandle;
|
|
}
|
|
|
|
void
|
|
material_manager::update_variants( bool const Weather ) {
|
|
|
|
// the copies don't update themselves, their names aren't files to read again
|
|
auto const changed = [&]( material_handle const Source ) {
|
|
auto const &source { m_materials[ Source ] };
|
|
return Weather ? source.update_on_weather_change : source.update_on_season_change; };
|
|
for( auto const &[ source, matte ] : m_mattes ) {
|
|
if( changed( source ) ) {
|
|
auto variant { variant_of( m_materials[ source ], ":matte" ) };
|
|
make_matte( variant );
|
|
m_materials[ matte ] = std::move( variant );
|
|
}
|
|
}
|
|
for( auto const &[ key, tinted ] : m_tints ) {
|
|
auto const source { std::get<0>( key ) };
|
|
if( changed( source ) ) {
|
|
auto variant { variant_of( m_materials[ source ], ":tinted" ) };
|
|
make_tinted( variant, glm::vec3 { std::get<1>( key ), std::get<2>( key ), std::get<3>( key ) } );
|
|
m_materials[ tinted ] = std::move( variant );
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
material_manager::on_weather_change() {
|
|
|
|
for( auto &material : m_materials ) {
|
|
if( material.update_on_weather_change ) { material.update(); }
|
|
}
|
|
update_variants( true );
|
|
}
|
|
|
|
void
|
|
material_manager::on_season_change() {
|
|
|
|
for( auto &material : m_materials ) {
|
|
if( material.update_on_season_change ) { material.update(); }
|
|
}
|
|
update_variants( false );
|
|
}
|
|
|
|
// 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<std::string> 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<std::string, std::string>
|
|
material_manager::find_on_disk( std::string const &Materialname ) {
|
|
|
|
auto const materialname { ToLower( Materialname ) };
|
|
return FileExists({Global.asCurrentTexturePath + materialname, materialname, paths::textures + materialname}, {".mat"});
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|