From 8fbb48235033729d57366444dc5470285469de6a Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sun, 27 Sep 2026 00:09:16 +0200 Subject: [PATCH] feat(materials): texture randomization for loads * feat(materials): texture randomization for loads --- .../renderer/source/materialparser.cpp | 8 + model/AnimModel.cpp | 4 + model/Model3d.cpp | 117 ++++++++++++ model/Model3d.h | 10 ++ model/material.cpp | 169 ++++++++++++------ model/material.h | 32 +++- rendering/opengl33renderer.cpp | 18 +- rendering/openglrenderer.cpp | 28 ++- utilities/utilities.cpp | 40 ++++- utilities/utilities.h | 8 + vehicle/DynObj.cpp | 4 + vehicle/DynObj.h | 4 + 12 files changed, 379 insertions(+), 63 deletions(-) diff --git a/betterRenderer/renderer/source/materialparser.cpp b/betterRenderer/renderer/source/materialparser.cpp index 20ef4322..0530916b 100644 --- a/betterRenderer/renderer/source/materialparser.cpp +++ b/betterRenderer/renderer/source/materialparser.cpp @@ -187,6 +187,14 @@ bool MaterialAdapterLegacyMatFile::ParseLevel(cParser& parser, int priority) { else if (key.compare(0, 7, "texture") == 0) { key.erase(0, 7); + // "textureN_variants:" declares a pool of textures picked per model instance. this renderer + // doesn't support per instance variants yet, so the pool is reduced to a single entry drawn + // when the material is loaded, which is how such texture used to be picked before variants + if (auto const variantmarker{key.find("_variants")}; + variantmarker != std::string::npos) { + key.erase(variantmarker, std::string("_variants").size()); + } + auto value{deserialize_random_set(parser)}; replace_slashes(value); auto& [tex_priority, texture] = m_texture_mapping[key]; diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index ef4f90a8..bc2d6e84 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -627,6 +627,10 @@ void TAnimModel::RaPrepare() } } TSubModel::iInstance = reinterpret_cast( this ); //żeby nie robić cudzych animacji + // NOTE: scenery models don't pick texture variants of their own, so we clear the seed left by the + // last rendered vehicle. instanced draws share a single material bind anyway, which would make + // per instance variants inconsistent between batched and individually drawn models + TSubModel::iVariantSeed = 0; TSubModel::pasText = &asText; // przekazanie tekstu do wyświetlacza (!!!! do przemyślenia) for (auto entry : m_animlist) { diff --git a/model/Model3d.cpp b/model/Model3d.cpp index 5f59558b..38f39080 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -30,6 +30,7 @@ using namespace Mtable; float TSubModel::fSquareDist = 0.f; std::uintptr_t TSubModel::iInstance; // numer renderowanego egzemplarza obiektu +std::uint32_t TSubModel::iVariantSeed = 0; // ziarno losowania wariantów tekstur renderowanego egzemplarza texture_handle const *TSubModel::ReplacableSkinId = nullptr; int TSubModel::iAlpha = 0x30300030; // maska do testowania flag tekstur wymiennych TModel3d *TSubModel::pRoot; // Ra: tymczasowo wskaźnik na model widoczny z submodelu @@ -2460,6 +2461,120 @@ void TModel3d::LoadFromTextFile(std::string const &FileName, bool dynamic) } } +std::uint32_t TSubModel::VariantId(TSubModel const *Submodel) +{ // the seed is already well scattered, so the group is merely added to it. this way branches of one model + // are guaranteed to land on different variants, as long as the pool holds at least as many entries as + // the model has branches -- two containers carried by the same wagon won't come up with the same number + return iVariantSeed + static_cast(Submodel != nullptr ? Submodel->m_variantgroup : 0); +} + +namespace +{ +// two branches sitting no farther than this from each other are treated as one object. +// generous, a simplified lod mesh doesn't have to sit exactly where the detailed one does +float const variantgrouptolerance{0.25f}; + +// assigns provided variant group id to the submodel and all of its children +void mark_variant_group(TSubModel *Submodel, int const Group, bool const Includesiblings) +{ + while (Submodel != nullptr) + { + Submodel->m_variantgroup = Group; + mark_variant_group(Submodel->ChildGet(), Group, true); + if (false == Includesiblings) + { + break; + } + Submodel = Submodel->NextGet(); + } +} + +// expands provided bounding area to cover geometry carried by the submodel and all of its children +void accumulate_mesh_bounds(TSubModel const *Submodel, glm::vec3 &Minimum, glm::vec3 &Maximum, bool &Isempty, bool const Includesiblings) +{ + while (Submodel != nullptr) + { + if (auto const &vertices{(Submodel->m_geometry.handle != null_handle ? GfxRenderer->Vertices(Submodel->m_geometry.handle) : Submodel->Vertices)}; + false == vertices.empty()) + { + // the transformation chain can still hold a rotation, so the vertices go through it + // rather than get taken at their face value + float4x4 parentmatrix; + Submodel->ParentMatrix(&parentmatrix); + auto const transformationmatrix{glm::make_mat4(parentmatrix.readArray())}; + for (auto const &vertex : vertices) + { + auto const position{glm::vec3{transformationmatrix * glm::vec4{vertex.position, 1}}}; + Minimum = glm::min(Minimum, position); + Maximum = glm::max(Maximum, position); + } + Isempty = false; + } + accumulate_mesh_bounds(Submodel->Child, Minimum, Maximum, Isempty, true); + if (false == Includesiblings) + { + break; + } + Submodel = Submodel->Next; + } +} + +// returns position of provided branch of the model, relative to the model root +glm::vec3 branch_position(TSubModel const *Branch) +{ + // the transformation chain is the cheap way to get there, as it doesn't touch the geometry, + // which keeps this affordable even for models made of thousands of branches... + auto const offset{Branch->offset()}; + if (glm::length2(offset) > variantgrouptolerance * variantgrouptolerance) + { + return offset; + } + // ...but it only tells us anything for branches which actually carry a transform. models loaded from + // text format can have theirs baked into the vertices instead, which leaves the whole model piled up + // at the origin and, without the fallback below, all of its branches in a single variant group. + // NOTE: it's the centre of the bounding area we're after, and not the average vertex position offset() + // would give us -- the latter drifts with vertex density, which puts each lod mesh of one object in + // a group of its own and has the object change its texture variant as the lods get swapped + glm::vec3 minimum{std::numeric_limits::max()}; + glm::vec3 maximum{std::numeric_limits::lowest()}; + bool isempty{true}; + accumulate_mesh_bounds(Branch, minimum, maximum, isempty, false); + return (false == isempty ? (minimum + maximum) * 0.5f : offset); +} +} // namespace + +void TModel3d::assign_variant_groups() +{ // each top level branch of the model gets a group id, which it passes down to all of its children. + // for load models a branch is a single load chunk, and the group id is what makes two chunks of the same + // model pick different texture variants, while keeping all parts of one chunk on the same variant. + // the id comes from the position of the branch, not from its ordinal: lod variants of one object are + // separate branches drawn in disjoint distance ranges, and grouping them by position is what keeps the + // object on the same variant no matter which of them is currently on screen + std::vector grouppositions; // position of each group, indexed by its id + auto const groupid = [&grouppositions](TSubModel const *Branch) { + auto const position{branch_position(Branch)}; + for (std::size_t index = 0; index < grouppositions.size(); ++index) + { + if (glm::length2(grouppositions[index] - position) < variantgrouptolerance * variantgrouptolerance) + { + return static_cast(index); + } + } + grouppositions.emplace_back(position); + return static_cast(grouppositions.size() - 1); + }; + + for (auto *trunk = Root; trunk != nullptr; trunk = trunk->NextGet()) + { + // root level submodels are usually mere containers, but can carry geometry of their own + trunk->m_variantgroup = groupid(trunk); + for (auto *branch = trunk->ChildGet(); branch != nullptr; branch = branch->NextGet()) + { + mark_variant_group(branch, groupid(branch), false); + } + } +} + void TModel3d::Init() { // obrócenie początkowe układu współrzędnych, dla // pojazdów wykonywane po analizie animacji @@ -2492,6 +2607,8 @@ void TModel3d::Init() Root->m_boundingradius = std::max(Root->m_boundingradius, root->m_boundingradius); } + assign_variant_groups(); + if (Global.iConvertModels & 1 && false == asBinary.empty()) { SaveToBinFile(asBinary); diff --git a/model/Model3d.h b/model/Model3d.h index 7ebb1317..0e053609 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -137,6 +137,9 @@ public: TSubModel *Child { nullptr }; public: material_handle m_material { null_handle }; // numer tekstury, -1 wymienna, 0 brak + // index of the top level branch of the model this submodel belongs to. submodels sharing a branch + // also share the texture variant picked for them, so a multi-mesh load chunk stays consistent + int m_variantgroup { 0 }; bool bWire { false }; // nie używane, ale wczytywane float Opacity { 1.0f }; float f_Angle { 0.0f }; @@ -168,6 +171,9 @@ public: public: static size_t iInstance; // identyfikator egzemplarza, który aktualnie renderuje model + // seed of the currently rendered model instance, combined with m_variantgroup it picks + // texture variants for materials which declare them. 0 for instances without own seed + static std::uint32_t iVariantSeed; static material_handle const *ReplacableSkinId; static int iAlpha; // maska bitowa dla danego przebiegu static float fSquareDist; @@ -211,6 +217,8 @@ public: static void ReplacableSet(material_handle const *r, int a) { ReplacableSkinId = r; iAlpha = a; }; + // returns id of texture variant to use for provided submodel of the currently rendered instance + static std::uint32_t VariantId(TSubModel const *Submodel); void Name_Material( std::string const &Name ); void Name( std::string const &Name ); // Ra: funkcje do budowania terenu z E3D @@ -291,6 +299,8 @@ public: void SaveToBinFile(std::string const &FileName); uint32_t Flags() const { return iFlags; }; void Init(); + // assigns each top level branch of the model, and everything below it, its own variant group id + void assign_variant_groups(); std::string NameGet() const { return m_filename; }; nameoffset_sequence const & smoke_sources() const { return m_smokesources; } diff --git a/model/material.cpp b/model/material.cpp index f5e32689..19fa0b37 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -64,55 +64,96 @@ std::map texture_bindings { { "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) { - for (auto it : parse_info->tex_mapping) - { - std::string key = it.first; - std::string value = it.second.name; + // texture bindings which come with a pool of alternatives, gathered while resolving regular bindings + std::vector variantbindings; - if (key.size() > 0 && key[0] != '_') - { - key.erase( key.find_first_not_of( "-1234567890" ) ); - size_t num = std::stoi(key) - 1; - if (num < gl::MAX_TEXTURES) { - textures[num] = GfxRenderer->Fetch_Texture(value, Loadnow); - } - else { - log_error("invalid texture binding: " + std::to_string(num)); - is_good = false; - } - } - else if (key.size() > 2) - { - key.erase(0, 1); - key.pop_back(); - std::map::iterator lookup; - if( shader && shader->texture_conf.find( key ) != shader->texture_conf.end() ) { - textures[ shader->texture_conf[ key ].id ] = GfxRenderer->Fetch_Texture( value, Loadnow ); - } - else if( shader == nullptr - && ( lookup = texture_bindings.find( key ) ) != texture_bindings.end() ) { - textures[ lookup->second ] = GfxRenderer->Fetch_Texture( value, Loadnow ); - } - else { - // 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; - } - } - } - else { - log_error("unrecognized texture binding: " + key); - is_good = false; + 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 @@ -289,6 +330,44 @@ bool is_weather( std::string const &String ) { } // 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 ) { @@ -324,17 +403,7 @@ opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool c else if (key.compare(0, 7, "texture") == 0) { key.erase(0, 7); - - auto value { deserialize_random_set( Input ) }; - replace_slashes( value ); - auto it = parse_info->tex_mapping.find(key); - if (it == parse_info->tex_mapping.end()) - parse_info->tex_mapping.emplace(std::make_pair(key, parse_info_s::tex_def({ value, Priority }))); - else if (Priority > it->second.priority) - { - parse_info->tex_mapping.erase(it); - parse_info->tex_mapping.emplace(std::make_pair(key, parse_info_s::tex_def({ value, Priority }))); - } + deserialize_texture_mapping( Input, key, Priority ); } else if (key.compare(0, 5, "param") == 0) { key.erase(0, 5); diff --git a/model/material.h b/model/material.h index 6d3f2b8d..7e04514c 100644 --- a/model/material.h +++ b/model/material.h @@ -18,7 +18,13 @@ http://mozilla.org/MPL/2.0/. // a collection of parameters for the rendering setup. // for modern opengl this translates to set of attributes for shaders struct opengl_material : public IMaterial { - std::array textures = { null_handle }; + using texture_set = std::array; + + texture_set textures = { null_handle }; + // alternative texture sets, defined with "textureN_variants:" key. + // one of them is picked per rendered model instance, empty when the material declares no variants. + // NOTE: all variants share the remaining material properties, opacity and translucency included + std::vector texture_variants; std::array params; std::vector params_state; @@ -60,6 +66,14 @@ struct opengl_material : public IMaterial { { return textures[slot]; } + // returns texture set to use for model instance identified by provided variant id + texture_set const &GetTextures(std::uint32_t const Variant) const + { + return ( + texture_variants.empty() ? + textures : + texture_variants[Variant % texture_variants.size()]); + } // members static struct path_data { std::unordered_map index_map; @@ -71,10 +85,23 @@ struct opengl_material : public IMaterial { bool update_on_season_change{ false }; private: +// types + // texture binding which comes with a pool of alternatives: slot the binding fills, and the pool itself + using variant_binding = std::pair const *>; + // methods // imports member data pair from the config file bool deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow ); + // imports texture binding, potentially carrying a pool of alternatives, from the config file + void + deserialize_texture_mapping( cParser &Input, std::string const &Key, int const Priority ); + // returns texture slot provided binding key resolves to, or -1 if it doesn't resolve to any + int + texture_slot( std::string const &Key ); + // fills the alternative texture sets from provided pools, one set per entry of the longest pool + void + build_texture_variants( std::vector const &Bindings, bool const Loadnow ); void log_error(const std::string &str); // members @@ -90,6 +117,9 @@ private: { std::string name; int priority; + // optional pool of alternative textures for the same binding, one picked per model instance. + // empty for regular, single texture bindings + std::vector variants; }; struct param_def { diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 576cf404..5419bbcd 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -1286,6 +1286,7 @@ bool opengl33_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squ if( Setup ) { TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset{ Dynamic->vPosition - m_renderpass.pass_camera.position() }; Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu @@ -2262,14 +2263,21 @@ void opengl33_renderer::Bind_Material( material_handle const Material, TSubModel else model_ubs.alpha_mult = 1.0f; + // materials which declare texture variants provide a different texture set per model instance, + // picked from the seed of the rendered instance and the variant group of the submodel + auto const &materialtextures{ + material.texture_variants.empty() ? + material.textures : + material.GetTextures(TSubModel::VariantId(sm))}; + if (GLAD_GL_ARB_multi_bind) { GLuint lastdiff = 0; size_t i; for (i = 0; i < gl::MAX_TEXTURES; i++) - if (material.textures[i] != null_handle) + if (materialtextures[i] != null_handle) { - opengl_texture &tex = m_textures.mark_as_used(material.textures[i]); + opengl_texture &tex = m_textures.mark_as_used(materialtextures[i]); tex.create(); if (opengl_texture::units[i] != tex.id) { @@ -2286,7 +2294,7 @@ void opengl33_renderer::Bind_Material( material_handle const Material, TSubModel else { size_t unit = 0; - for (auto &tex : material.textures) + for (auto &tex : materialtextures) { if (tex == null_handle) break; @@ -3351,6 +3359,7 @@ bool opengl33_renderer::Render(TDynamicObject *Dynamic) // setup TSubModel::iInstance = reinterpret_cast(Dynamic); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; @@ -3518,10 +3527,12 @@ bool opengl33_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li { TSubModel::iInstance = 0; + TSubModel::iVariantSeed = 0; return false; } TSubModel::iInstance = reinterpret_cast(Dynamic); + TSubModel::iVariantSeed = Dynamic->variant_seed(); if ((true == FreeFlyModeFlag) || (false == Dynamic->bDisplayCab) || (Dynamic->mdKabina == Dynamic->mdModel)) { @@ -4445,6 +4456,7 @@ bool opengl33_renderer::Render_Alpha(TDynamicObject *Dynamic) // setup TSubModel::iInstance = (size_t)Dynamic; //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index d159c31a..1da66ba5 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -801,6 +801,7 @@ bool opengl_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squar if( Setup ) { TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu ::glPushMatrix(); @@ -1759,10 +1760,17 @@ opengl_renderer::Bind_Material( material_handle const Material, TSubModel const return; } + // materials which declare texture variants provide a different texture set per model instance, + // picked from the seed of the rendered instance and the variant group of the submodel + auto const &materialtextures { + material.texture_variants.empty() ? + material.textures : + material.GetTextures( TSubModel::VariantId( sm ) ) }; + if( false == Global.BasicRenderer ) { - m_textures.bind( m_normaltextureunit, material.textures[1] ); + m_textures.bind( m_normaltextureunit, materialtextures[1] ); } - m_textures.bind( m_diffusetextureunit, material.textures[0] ); + m_textures.bind( m_diffusetextureunit, materialtextures[0] ); } else { // null material, unbind all textures @@ -2475,6 +2483,7 @@ opengl_renderer::Render( TDynamicObject *Dynamic ) { // setup TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); Dynamic->ABuLittleUpdate( squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu ::glPushMatrix(); @@ -2574,10 +2583,12 @@ opengl_renderer::Render_cab( TDynamicObject const *Dynamic, float const Lightlev if( Dynamic == nullptr ) { TSubModel::iInstance = 0; + TSubModel::iVariantSeed = 0; return false; } TSubModel::iInstance = reinterpret_cast( Dynamic ); + TSubModel::iVariantSeed = Dynamic->variant_seed(); if( ( true == FreeFlyModeFlag ) || ( false == Dynamic->bDisplayCab ) @@ -2866,11 +2877,11 @@ opengl_renderer::Render( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); @@ -2884,11 +2895,11 @@ opengl_renderer::Render( TSubModel *Submodel ) { ::glColor3fv( glm::value_ptr( pick_color( m_pickcontrolsitems.size() ) ) ); // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); @@ -3657,6 +3668,7 @@ opengl_renderer::Render_Alpha( TDynamicObject *Dynamic ) { // setup TSubModel::iInstance = ( size_t )Dynamic; //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; @@ -3909,11 +3921,11 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index c1d0b73d..14e44897 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -527,4 +527,42 @@ std::string deserialize_random_set(cParser &Input, char const *Break) // shouldn't ever get here but, eh return ""; } -} \ No newline at end of file +} + +// returns hash of provided string, combined with provided seed +std::uint32_t hash_string(std::string const &String, std::uint32_t const Seed) +{ + // FNV-1a, 32 bit, with the seed mixed into the standard offset basis + std::uint32_t hash{2166136261u ^ Seed}; + for (auto const character : String) + { + hash ^= static_cast(static_cast(character)); + hash *= 16777619u; + } + return hash; +} + +// extracts a group of tokens from provided data stream, returns all of them +std::vector deserialize_set(cParser &Input, char const *Break) +{ + + std::vector tokens; + + auto token{Input.getToken(true, Break)}; + std::ranges::replace(token, '\\', '/'); + if (token != "[") + { + // simple case, single token + if (false == token.empty()) + { + tokens.emplace_back(token); + } + return tokens; + } + // '[' marks a beginning of a set, retrieve all entries until it's closed + while ((token = deserialize_random_set(Input, Break)) != "" && token != "]") + { + tokens.emplace_back(token); + } + return tokens; +} diff --git a/utilities/utilities.h b/utilities/utilities.h index 70cf1c07..91d74e67 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -377,6 +377,14 @@ glm::dvec3 LoadPoint(class cParser &Input); // extracts a group of tokens from provided data stream std::string deserialize_random_set(cParser &Input, char const *Break = "\n\r\t ;"); +// extracts a group of tokens from provided data stream, returns all of them. +// a single token, not enclosed in "[]", is returned as one element sequence +std::vector deserialize_set(cParser &Input, char const *Break = "\n\r\t ;"); + +// returns hash of provided string, combined with provided seed. FNV-1a, picked for having +// identical result regardless of platform and standard library, unlike std::hash +std::uint32_t hash_string(std::string const &String, std::uint32_t const Seed = 0); + // extracts a group of pairs from provided data stream // NOTE: expects no more than single pair per line template void deserialize_map(MapType_ &Map, cParser &Input) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 184d432a..e84af613 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -1934,6 +1934,10 @@ TDynamicObject::Init(std::string Name, // nazwa pojazdu, np. "EU07-424" iDirection = Reversed ? 0 : 1; // Ra: 0, jeśli ma być wstawiony jako obrócony tyłem asBaseDir = paths::dynamic + BaseDir + "/"; // McZapkie-310302 asName = Name; + // derived from the name and the seed of the simulation rather than drawn at random, so that the vehicle + // keeps the same look for the whole session and looks the same for everyone in a multiplayer game, while + // another run of the same scenario, with another seed, dresses it differently + m_variantseed = hash_string( asName, Global.random_seed ); std::string asAnimName; // zmienna robocza do wyszukiwania osi i wózków // Ra: zmieniamy znaczenie obsady na jednoliterowe, żeby dosadzić kierownika if (DriverType == "headdriver") diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index f8f2ce78..c48e40e1 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -634,6 +634,10 @@ private: std::string asName; const std::string &name() const { return asName; } + // seed for texture variants of materials used by this vehicle and its load + std::uint32_t m_variantseed { 0 }; + std::uint32_t variant_seed() const { + return m_variantseed; } std::string asBaseDir; // std::ofstream PneuLogFile; //zapis parametrow pneumatycznych