From 283b8523e661e56cfd2924f64f7eb3ddb4b22c68 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Wed, 21 Oct 2026 23:04:53 +0200 Subject: [PATCH] feat(track): normal mapped track materials blended at the joints too --- model/material.cpp | 105 ++++++++++++++------ model/material.h | 9 +- shaders/mat_trackblend_normalmap.frag | 11 ++ shaders/mat_trackblend_normalmap_grain.frag | 12 +++ shaders/trackblend.glsl | 47 ++++++++- 5 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 shaders/mat_trackblend_normalmap.frag create mode 100644 shaders/mat_trackblend_normalmap_grain.frag diff --git a/model/material.cpp b/model/material.cpp index 3b2fe7eb..a1439e8b 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -756,90 +756,129 @@ material_manager::create_tinted( material_handle const Material, glm::vec3 const return tintedhandle; } -bool -material_manager::blendable( material_handle const Material ) { +int +material_manager::blend_lighting( material_handle const Material ) { if( Material == null_handle || Material >= m_materials.size() ) { - return false; + return -1; } if( auto const lookup { m_blendables.find( Material ) }; lookup != m_blendables.end() ) { return lookup->second; } - auto result { false }; + auto result { -1 }; auto const &material { m_materials[ Material ] }; + auto const &conf { material.shader ? material.shader->texture_conf : decltype( material.shader->texture_conf ) {} }; + auto const has = [&]( char const *Name ) { + auto const found { conf.find( Name ) }; + return found != conf.end() && material.textures[ found->second.id ] != null_handle; }; if( material.is_good && material.shader && material.texture_variants.empty() && false == material.update_on_weather_change && false == material.update_on_season_change - && material.shader->texture_conf.size() == 1 - && material.shader->texture_conf.count( "diffuse" ) == 1 - && material.textures[ material.shader->texture_conf.at( "diffuse" ).id ] != null_handle ) { + // a tint takes the parameters the blending shader keeps its lighting in + && false == ( material.params[ 2 ].w < 0.f ) + && has( "diffuse" ) ) { try { - // the parameters of the material go to the blending one as they are, so they have to be in the same places in both - auto const blend { GfxRenderer->Fetch_Shader( "trackblend" ) }; - result = std::all_of( - std::begin( material.shader->param_conf ), std::end( material.shader->param_conf ), - [&]( auto const &Entry ) { - auto const found { blend->param_conf.find( Entry.first ) }; - return found != blend->param_conf.end() - && found->second.location == Entry.second.location - && found->second.offset == Entry.second.offset - && found->second.size == Entry.second.size; } ); + auto lighting { -1 }; + if( conf.size() == 1 ) { + lighting = 0; + } + else if( conf.size() == 2 && has( "normalmap" ) ) { + // the shaders lit as the blending one can be, by the names they're fetched by + char const *const shaders[] = { "normalmap", "sunlessnormalmap", "shadowlessnormalmap" }; + for( int index = 0; index < 3; ++index ) { + if( material.shader == GfxRenderer->Fetch_Shader( shaders[ index ] ) ) { + lighting = index + 1; + } + } + } + if( lighting >= 0 ) { + // the parameters of the material go to the blending one as they are, so they have to be in the same places in both + auto const blend { GfxRenderer->Fetch_Shader( lighting > 0 ? "trackblend_normalmap" : "trackblend" ) }; + auto const fits { std::all_of( + std::begin( material.shader->param_conf ), std::end( material.shader->param_conf ), + [&]( auto const &Entry ) { + auto const found { blend->param_conf.find( Entry.first ) }; + return found != blend->param_conf.end() + && found->second.location == Entry.second.location + && found->second.offset == Entry.second.offset + && found->second.size == Entry.second.size; } ) }; + result = ( fits ? lighting : -1 ); + } } catch( gl::shader_exception const &e ) { ErrorLog( "invalid shader: " + std::string( e.what() ) ); } } - if( false == result ) { + if( result < 0 ) { // once for each material a track would blend, which of the conditions it fails std::string textures; - if( material.shader ) { - for( auto const &entry : material.shader->texture_conf ) { - textures += ( textures.empty() ? "" : ", " ) + entry.first; - } + for( auto const &entry : conf ) { + textures += ( textures.empty() ? "" : ", " ) + entry.first; } WriteLog( "track blend: material \"" + material.name + "\" isn't blended:" + ( material.is_good ? "" : " not good;" ) + ( material.shader ? " a shader with the textures " + textures + ";" : " no shader;" ) + ( material.texture_variants.empty() ? "" : " texture variants;" ) - + ( material.update_on_weather_change || material.update_on_season_change ? " changes with the weather or seasons;" : "" ) ); + + ( material.update_on_weather_change || material.update_on_season_change ? " changes with the weather or seasons;" : "" ) + + ( material.params[ 2 ].w < 0.f ? " tinted;" : "" ) ); } m_blendables.emplace( Material, result ); return result; } +bool +material_manager::blendable( material_handle const Material ) { + + return blend_lighting( Material ) >= 0; +} + material_handle material_manager::create_blend( material_handle const Material, std::array const &Others, bool const Grain ) { - if( false == blendable( Material ) ) { + auto const lighting { blend_lighting( Material ) }; + if( lighting < 0 ) { return Material; } auto const key { std::make_tuple( Material, Others[ 0 ], Others[ 1 ], Others[ 2 ], Grain ) }; if( auto const lookup { m_blends.find( key ) }; lookup != m_blends.end() ) { return lookup->second; } - auto const diffuse_of = [&]( material_handle const Source ) -> texture_handle { + auto const texture_of = [&]( material_handle const Source, char const *Name ) -> texture_handle { if( false == blendable( Source ) ) { return null_handle; } auto const &source { m_materials[ Source ] }; - return source.textures[ source.shader->texture_conf.at( "diffuse" ).id ]; + auto const found { source.shader->texture_conf.find( Name ) }; + return ( found != source.shader->texture_conf.end() ? source.textures[ found->second.id ] : null_handle ); }; + auto const normals { lighting > 0 }; auto blend { variant_of( m_materials[ Material ], Grain ? ":blendgrain" : ":blend" ) }; auto result { Material }; try { - blend.shader = GfxRenderer->Fetch_Shader( Grain ? "trackblend_grain" : "trackblend" ); + blend.shader = GfxRenderer->Fetch_Shader( std::string { "trackblend" } + ( normals ? "_normalmap" : "" ) + ( Grain ? "_grain" : "" ) ); blend.texture_variants.clear(); blend.textures.fill( null_handle ); - auto const own { diffuse_of( Material ) }; - blend.textures[ 0 ] = own; - for( std::size_t other = 0; other < Others.size(); ++other ) { - // the slots of the ends nothing is blended at take the own texture, the shader samples them only with a weight - auto const texture { diffuse_of( Others[ other ] ) }; - blend.textures[ 1 + other ] = ( texture != null_handle ? texture : own ); + auto const owndiffuse { texture_of( Material, "diffuse" ) }; + auto const ownnormal { texture_of( Material, "normalmap" ) }; + blend.textures[ 0 ] = owndiffuse; + if( normals ) { + blend.textures[ 4 ] = ownnormal; } + for( std::size_t other = 0; other < Others.size(); ++other ) { + // the slots of the ends nothing is blended at take the own textures, the shader samples them only with a weight; a + // material without a normal map blended into one with it takes its normal map + auto const diffuse { texture_of( Others[ other ], "diffuse" ) }; + blend.textures[ 1 + other ] = ( diffuse != null_handle ? diffuse : owndiffuse ); + if( normals ) { + auto const normal { texture_of( Others[ other ], "normalmap" ) }; + blend.textures[ 5 + other ] = ( normal != null_handle ? normal : ownnormal ); + } + } + // lit as the shader of the material: normalmap, sunlessnormalmap or shadowlessnormalmap + blend.params[ 2 ].x = static_cast( lighting ); // the parameters taken from the bindings again, by the shader blend.params_state.clear(); blend.finalize( true ); diff --git a/model/material.h b/model/material.h index 89637eee..ab84f7a4 100644 --- a/model/material.h +++ b/model/material.h @@ -152,7 +152,8 @@ public: // itself where it can't be blended material_handle create_blend( material_handle const Material, std::array const &Others, bool const Grain ); - // whether the material can be blended so: one diffuse texture, lit as mat_default, not changing with the weather and seasons + // whether the material can be blended so: a diffuse texture lit as mat_default, or with a normal map lit as normalmap, + // sunlessnormalmap or shadowlessnormalmap; not changing with the weather and seasons, not tinted bool blendable( material_handle const Material ); opengl_material const & @@ -174,6 +175,10 @@ private: // checks whether specified texture is in the texture bank. returns texture id, or npos. material_handle find_in_databank( std::string const &Materialname ) const; + // how the material is lit, of the materials a track blends: 0 as mat_default, 1 normalmap, 2 sunlessnormalmap, + // 3 shadowlessnormalmap; -1 where it can't be blended + int + blend_lighting( material_handle const Material ); public: // checks whether specified file exists. returns name of the located file, or empty string. static std::pair @@ -186,7 +191,7 @@ private: std::unordered_map m_mattes; std::map, material_handle> m_tints; std::map, material_handle> m_blends; - std::unordered_map m_blendables; + std::unordered_map m_blendables; }; //--------------------------------------------------------------------------- diff --git a/shaders/mat_trackblend_normalmap.frag b/shaders/mat_trackblend_normalmap.frag new file mode 100644 index 00000000..6abb1a2f --- /dev/null +++ b/shaders/mat_trackblend_normalmap.frag @@ -0,0 +1,11 @@ +// the rails of a track of a normal mapped material faded into those of the tracks joined at its ends, see trackblend.glsl +#define TRACKBLEND_NORMALMAP 1 +#texture (normalmap, 4, RGBA) +uniform sampler2D normalmap; +#texture (normalmap1, 5, RGBA) +uniform sampler2D normalmap1; +#texture (normalmap2, 6, RGBA) +uniform sampler2D normalmap2; +#texture (normalmap3, 7, RGBA) +uniform sampler2D normalmap3; +#include diff --git a/shaders/mat_trackblend_normalmap_grain.frag b/shaders/mat_trackblend_normalmap_grain.frag new file mode 100644 index 00000000..57fe56cc --- /dev/null +++ b/shaders/mat_trackblend_normalmap_grain.frag @@ -0,0 +1,12 @@ +// the ballast of a track of a normal mapped material mixed in grains into that of the tracks joined at its ends, see trackblend.glsl +#define TRACKBLEND_GRAIN 1 +#define TRACKBLEND_NORMALMAP 1 +#texture (normalmap, 4, RGBA) +uniform sampler2D normalmap; +#texture (normalmap1, 5, RGBA) +uniform sampler2D normalmap1; +#texture (normalmap2, 6, RGBA) +uniform sampler2D normalmap2; +#texture (normalmap3, 7, RGBA) +uniform sampler2D normalmap3; +#include diff --git a/shaders/trackblend.glsl b/shaders/trackblend.glsl index 950f557c..426df319 100644 --- a/shaders/trackblend.glsl +++ b/shaders/trackblend.glsl @@ -1,9 +1,14 @@ // a track drawn with the textures of the tracks joined at its ends blended in near them (world/TrackBlend.h): the weights of the // tracks joined at the ends p1, p2 and p4 of a switch in UserData.yzw, their textures in diffuse1..3, lit as mat_default. -// TRACKBLEND_GRAIN: the others come in as grains, as the stones of two ballasts would mix, rather than faded in +// TRACKBLEND_GRAIN: the others come in as grains, as the stones of two ballasts would mix, rather than faded in. +// TRACKBLEND_NORMALMAP: with the normal maps of the four in normalmap..normalmap3 (declared by the including shader), lit as +// the shader of the material of the track: param[2].x 1 normalmap, 2 sunlessnormalmap, 3 shadowlessnormalmap in vec3 f_normal; in vec2 f_coord; in vec4 f_pos; +#if TRACKBLEND_NORMALMAP +in mat3 f_tbn; +#endif in vec4 f_clip_pos; in vec4 f_clip_future_pos; @@ -15,6 +20,7 @@ in vec4 f_clip_future_pos; #param (specular, 1, 1, 1, specular) #param (reflection, 1, 2, 1, zero) #param (glossiness, 1, 3, 1, glossiness) +#param (blendlighting, 2, 0, 1, zero) #texture (diffuse, 0, sRGB_A) uniform sampler2D diffuse; @@ -96,6 +102,9 @@ void main() float grained = 1.0 - smoothstep(0.35, 0.9, footprint); #endif vec4 tex_color = textureGrad(diffuse, f_coord, dx, dy); +#if TRACKBLEND_NORMALMAP + vec4 tex_normal = textureGrad(normalmap, f_coord, dx, dy); +#endif if (total > 0.0) { #if TRACKBLEND_GRAIN @@ -108,13 +117,34 @@ void main() total = weights.x + weights.y + weights.z; #endif vec4 blended = tex_color * (1.0 - total); +#if TRACKBLEND_NORMALMAP + vec4 blendednormal = tex_normal * (1.0 - total); +#endif if (weights.x > 0.0) + { blended += textureGrad(diffuse1, f_coord, dx, dy) * weights.x; +#if TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap1, f_coord, dx, dy) * weights.x; +#endif + } if (weights.y > 0.0) + { blended += textureGrad(diffuse2, f_coord, dx, dy) * weights.y; +#if TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap2, f_coord, dx, dy) * weights.y; +#endif + } if (weights.z > 0.0) + { blended += textureGrad(diffuse3, f_coord, dx, dy) * weights.z; +#if TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap3, f_coord, dx, dy) * weights.z; +#endif + } tex_color = blended; +#if TRACKBLEND_NORMALMAP + tex_normal = blendednormal; +#endif } bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); @@ -122,10 +152,25 @@ void main() discard; vec3 fragcolor = ambient; +#if TRACKBLEND_NORMALMAP + vec3 normal; + normal.xy = (tex_normal.rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * tex_normal.a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + int lighting = int(param[2].x + 0.5); + if (lighting == 2) + fragcolor = apply_lights_sunless(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + else + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, (lighting == 3 ? 1.0 : shadow_tone)); +#else vec3 fragnormal = normalize(f_normal); matte = true; fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, 0.0, 0.0, shadow_tone); +#endif vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); #if POSTFX_ENABLED out_color = color;