mirror of
https://github.com/id-Software/DOOM-3-BFG.git
synced 2025-03-15 07:00:58 +00:00
PBR metal workflow texture support for IBL
This commit is contained in:
parent
21bdc82d13
commit
60f330874d
9 changed files with 160 additions and 26 deletions
|
@ -60,6 +60,11 @@ half3 Fresnel_Schlick( half3 specularColor, half vdotH )
|
|||
return specularColor + ( 1.0 - specularColor ) * pow( 1.0 - vdotH, 5.0 );
|
||||
}
|
||||
|
||||
half3 Fresnel_Glossy( half3 specularColor, half roughness, half vdotH )
|
||||
{
|
||||
return specularColor + ( max( half3( 1.0 - roughness ), specularColor ) - specularColor ) * pow( 1.0 - vdotH, 5.0 );
|
||||
}
|
||||
|
||||
// Visibility term G( l, v, h )
|
||||
// Very similar to Marmoset Toolbag 2 and gives almost the same results as Smith GGX
|
||||
float Visibility_Schlick( half vdotN, half ldotN, float alpha )
|
||||
|
|
|
@ -28,6 +28,7 @@ If you have questions concerning this license or the applicable additional terms
|
|||
*/
|
||||
|
||||
#include "renderprogs/global.inc"
|
||||
#include "renderprogs/BRDF.inc"
|
||||
|
||||
uniform sampler2D samp0 : register(s0); // texture 1 is the per-surface bump map
|
||||
uniform sampler2D samp1 : register(s1); // texture 2 is the light falloff texture
|
||||
|
@ -59,8 +60,7 @@ void main( PS_IN fragment, out PS_OUT result ) {
|
|||
// half4 lightFalloff = idtex2Dproj( samp1, fragment.texcoord2 );
|
||||
// half4 lightProj = idtex2Dproj( samp2, fragment.texcoord3 );
|
||||
half4 YCoCG = tex2D( samp3, fragment.texcoord1.xy );
|
||||
half4 specMapSRGB = tex2D( samp4, fragment.texcoord2.xy );
|
||||
half4 specMap = sRGBAToLinearRGBA( specMapSRGB );
|
||||
half4 specMap = tex2D( samp4, fragment.texcoord2.xy );
|
||||
|
||||
//half3 lightVector = normalize( fragment.texcoord0.xyz );
|
||||
half3 diffuseMap = sRGBToLinearRGB( ConvertYCoCgToRGB( YCoCG ) );
|
||||
|
@ -99,11 +99,41 @@ void main( PS_IN fragment, out PS_OUT result ) {
|
|||
float3 reflectionVector = globalNormal * dot3( globalEye, globalNormal );
|
||||
reflectionVector = ( reflectionVector * 2.0f ) - globalEye;
|
||||
|
||||
//half rim = 1.0f - saturate( hDotN );
|
||||
//half rimPower = 8.0;
|
||||
//half3 rimColor = sRGBToLinearRGB( half3( 0.125 ) * 1.2 ) * lightColor * pow( rim, rimPower );
|
||||
#if defined(USE_PBR)
|
||||
|
||||
const half metallic = specMap.g;
|
||||
const half roughness = specMap.r;
|
||||
const half glossiness = 1.0 - roughness;
|
||||
|
||||
// the vast majority of real-world materials (anything not metal or gems) have F(0°)
|
||||
// values in a very narrow range (~0.02 - 0.08)
|
||||
|
||||
float3 diffuseLight = sRGBToLinearRGB( texCUBE( samp7, globalNormal ).rgb ) * diffuseMap.rgb * ( rpDiffuseModifier.xyz ) * 1.0f;
|
||||
// approximate non-metals with linear RGB 0.04 which is 0.08 * 0.5 (default in UE4)
|
||||
const half3 dielectricColor = half3( 0.04 );
|
||||
|
||||
// derive diffuse and specular from albedo(m) base color
|
||||
const half3 baseColor = diffuseMap;
|
||||
|
||||
half3 diffuseColor = baseColor * ( 1.0 - metallic );
|
||||
half3 specularColor = lerp( dielectricColor, baseColor, metallic );
|
||||
|
||||
//diffuseColor = half3( 1.0 );
|
||||
float3 diffuseLight = sRGBToLinearRGB( texCUBE( samp7, globalNormal ).rgb ) * diffuseColor * ( rpDiffuseModifier.xyz ) * 1.5f;
|
||||
|
||||
//specularColor = half3( 0.0 );
|
||||
|
||||
float mip = clamp( ( roughness * 7.0 ) + 3.0, 0.0, 10.0 );
|
||||
float3 envColor = sRGBToLinearRGB( texCUBElod( samp8, float4( reflectionVector, mip ) ).rgb ) * ( rpSpecularModifier.xyz ) * 1.0f;
|
||||
|
||||
float3 specularLight = envColor * specularColor;
|
||||
|
||||
#else
|
||||
|
||||
half4 specMapSRGB = specMap;
|
||||
specMap = sRGBAToLinearRGBA( specMap );
|
||||
|
||||
//float3 diffuseLight = sRGBToLinearRGB( texCUBE( samp7, globalNormal ).rgb ) * diffuseMap.rgb * ( rpDiffuseModifier.xyz ) * 1.5f;
|
||||
float3 diffuseLight = diffuseMap.rgb * ( rpDiffuseModifier.xyz ) * 1.5f;
|
||||
|
||||
// HACK calculate roughness from D3 gloss maps
|
||||
float Y = dot( LUMINANCE_SRGB.rgb, specMapSRGB.rgb );
|
||||
|
@ -113,11 +143,32 @@ void main( PS_IN fragment, out PS_OUT result ) {
|
|||
|
||||
const float roughness = 1.0 - glossiness;
|
||||
|
||||
float mip = roughness * 7.0;
|
||||
float3 specularLight = sRGBToLinearRGB( texCUBEbias( samp8, float4( reflectionVector, mip ) ).rgb ) * specMap.rgb * ( rpSpecularModifier.xyz ) * 1.0f;
|
||||
float mip = clamp( ( roughness * 7.0 ) + 0.0, 0.0, 10.0 );
|
||||
float3 envColor = sRGBToLinearRGB( texCUBElod( samp8, float4( reflectionVector, mip ) ).rgb ) * ( rpSpecularModifier.xyz ) * 1.0f;
|
||||
|
||||
float3 specularLight = envColor * specMap.rgb;
|
||||
|
||||
#endif
|
||||
|
||||
// add glossy fresnel
|
||||
half hDotN = saturate( dot3( globalEye, globalNormal ) );
|
||||
|
||||
half3 specularColor2 = half3( 0.0 );
|
||||
float3 glossyFresnel = Fresnel_Glossy( specularColor2, roughness, hDotN );
|
||||
|
||||
// horizon fade
|
||||
const half horizonFade = 1.3;
|
||||
half horiz = saturate( 1.0 + horizonFade * saturate( dot3( reflectionVector, globalNormal ) ) );
|
||||
horiz *= horiz;
|
||||
//horiz = clamp( horiz, 0.0, 1.0 );
|
||||
|
||||
//specularLight = glossyFresnel * envColor;
|
||||
specularLight += glossyFresnel * envColor * ( rpSpecularModifier.xyz ) * 0.9 * horiz;
|
||||
|
||||
half3 lightColor = sRGBToLinearRGB( rpAmbientColor.rgb );
|
||||
|
||||
//result.color.rgb = diffuseLight;
|
||||
//result.color.rgb = diffuseLight * lightColor;
|
||||
//result.color.rgb = specularLight;
|
||||
result.color.rgb = ( diffuseLight + specularLight ) * lightColor * fragment.color.rgb;
|
||||
//result.color.rgb = localNormal.xyz * 0.5 + 0.5;
|
||||
|
|
|
@ -216,6 +216,8 @@ typedef enum
|
|||
TD_COVERAGE, // coverage map for fill depth pass when YCoCG is used
|
||||
TD_DEPTH, // depth buffer copy for motion blur
|
||||
// RB begin
|
||||
TD_SPECULAR_PBR_RMAO, // may be compressed, and always zeros the alpha channel, linear RGB R = roughness, G = metal, B = ambient occlusion
|
||||
TD_SPECULAR_PBR_RMAOD, // may be compressed, alpha channel contains displacement map
|
||||
TD_HIGHQUALITY_CUBE, // motorsep - Uncompressed cubemap texture (RGB colorspace)
|
||||
TD_LOWQUALITY_CUBE, // motorsep - Compressed cubemap texture (RGB colorspace DXT5)
|
||||
TD_SHADOW_ARRAY, // 2D depth buffer array for shadow mapping
|
||||
|
@ -367,6 +369,11 @@ public:
|
|||
return ( opts.format == FMT_DXT1 || opts.format == FMT_DXT5 );
|
||||
}
|
||||
|
||||
textureUsage_t GetUsage() const
|
||||
{
|
||||
return usage;
|
||||
}
|
||||
|
||||
bool IsLoaded() const;
|
||||
|
||||
static void GetGeneratedName( idStr& _name, const textureUsage_t& _usage, const cubeFiles_t& _cube );
|
||||
|
|
|
@ -137,6 +137,19 @@ ID_INLINE void idImage::DeriveOpts()
|
|||
opts.format = FMT_DXT1;
|
||||
opts.colorFormat = CFM_DEFAULT;
|
||||
break;
|
||||
|
||||
case TD_SPECULAR_PBR_RMAO:
|
||||
opts.gammaMips = false;
|
||||
opts.format = FMT_DXT1;
|
||||
opts.colorFormat = CFM_DEFAULT;
|
||||
break;
|
||||
|
||||
case TD_SPECULAR_PBR_RMAOD:
|
||||
opts.gammaMips = false;
|
||||
opts.format = FMT_DXT5;
|
||||
opts.colorFormat = CFM_DEFAULT;
|
||||
break;
|
||||
|
||||
case TD_DEFAULT:
|
||||
opts.gammaMips = true;
|
||||
opts.format = FMT_DXT5;
|
||||
|
|
|
@ -1047,17 +1047,17 @@ void idMaterial::ParseBlend( idLexer& src, shaderStage_t* stage )
|
|||
stage->drawStateBits = GLS_SRCBLEND_ZERO | GLS_DSTBLEND_ONE;
|
||||
return;
|
||||
}
|
||||
if( !token.Icmp( "bumpmap" ) )
|
||||
if( !token.Icmp( "bumpmap" ) || !token.Icmp( "normalmap" ) )
|
||||
{
|
||||
stage->lighting = SL_BUMP;
|
||||
return;
|
||||
}
|
||||
if( !token.Icmp( "diffusemap" ) )
|
||||
if( !token.Icmp( "diffusemap" ) || !token.Icmp( "basecolormap" ) )
|
||||
{
|
||||
stage->lighting = SL_DIFFUSE;
|
||||
return;
|
||||
}
|
||||
if( !token.Icmp( "specularmap" ) )
|
||||
if( !token.Icmp( "specularmap" ) || !token.Icmp( "rmaomap" ) )
|
||||
{
|
||||
stage->lighting = SL_SPECULAR;
|
||||
return;
|
||||
|
@ -1907,7 +1907,18 @@ void idMaterial::ParseStage( idLexer& src, const textureRepeat_t trpDefault )
|
|||
td = TD_DIFFUSE;
|
||||
break;
|
||||
case SL_SPECULAR:
|
||||
td = TD_SPECULAR;
|
||||
if( idStr::FindText( imageName, "_rmaod", false ) != -1 )
|
||||
{
|
||||
td = TD_SPECULAR_PBR_RMAOD;
|
||||
}
|
||||
else if( idStr::FindText( imageName, "_rmao", false ) != -1 )
|
||||
{
|
||||
td = TD_SPECULAR_PBR_RMAO;
|
||||
}
|
||||
else
|
||||
{
|
||||
td = TD_SPECULAR;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1920,10 +1931,13 @@ void idMaterial::ParseStage( idLexer& src, const textureRepeat_t trpDefault )
|
|||
// create new coverage stage
|
||||
shaderStage_t* newCoverageStage = &pd->parseStages[numStages];
|
||||
numStages++;
|
||||
|
||||
// copy it
|
||||
*newCoverageStage = *ss;
|
||||
|
||||
// toggle alphatest off for the current stage so it doesn't get called during the depth fill pass
|
||||
ss->hasAlphaTest = false;
|
||||
|
||||
// toggle alpha test on for the coverage stage
|
||||
newCoverageStage->hasAlphaTest = true;
|
||||
newCoverageStage->lighting = SL_COVERAGE;
|
||||
|
@ -2475,7 +2489,7 @@ void idMaterial::ParseMaterial( idLexer& src )
|
|||
continue;
|
||||
}
|
||||
// diffusemap for stage shortcut
|
||||
else if( !token.Icmp( "diffusemap" ) )
|
||||
else if( !token.Icmp( "diffusemap" ) || !token.Icmp( "basecolormap" ) )
|
||||
{
|
||||
str = R_ParsePastImageProgram( src );
|
||||
idStr::snPrintf( buffer, sizeof( buffer ), "blend diffusemap\nmap %s\n}\n", str );
|
||||
|
@ -2497,7 +2511,7 @@ void idMaterial::ParseMaterial( idLexer& src )
|
|||
continue;
|
||||
}
|
||||
// normalmap for stage shortcut
|
||||
else if( !token.Icmp( "bumpmap" ) )
|
||||
else if( !token.Icmp( "bumpmap" ) || !token.Icmp( "normalmap" ) )
|
||||
{
|
||||
str = R_ParsePastImageProgram( src );
|
||||
idStr::snPrintf( buffer, sizeof( buffer ), "blend bumpmap\nmap %s\n}\n", str );
|
||||
|
|
|
@ -1265,7 +1265,7 @@ void idRenderBackend::SetupInteractionStage( const shaderStage_t* surfaceStage,
|
|||
idRenderBackend::DrawSingleInteraction
|
||||
=================
|
||||
*/
|
||||
void idRenderBackend::DrawSingleInteraction( drawInteraction_t* din )
|
||||
void idRenderBackend::DrawSingleInteraction( drawInteraction_t* din, bool useIBL )
|
||||
{
|
||||
if( din->bumpImage == NULL )
|
||||
{
|
||||
|
@ -1298,6 +1298,35 @@ void idRenderBackend::DrawSingleInteraction( drawInteraction_t* din )
|
|||
return;
|
||||
}
|
||||
|
||||
if( useIBL )
|
||||
{
|
||||
const textureUsage_t specUsage = din->specularImage->GetUsage();
|
||||
|
||||
if( specUsage == TD_SPECULAR_PBR_RMAO || specUsage == TD_SPECULAR_PBR_RMAOD )
|
||||
{
|
||||
// PBR path with roughness, metal and AO
|
||||
if( din->surf->jointCache )
|
||||
{
|
||||
renderProgManager.BindShader_ImageBasedLightingSkinned_PBR();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderProgManager.BindShader_ImageBasedLighting_PBR();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( din->surf->jointCache )
|
||||
{
|
||||
renderProgManager.BindShader_ImageBasedLightingSkinned();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderProgManager.BindShader_ImageBasedLighting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bump matrix
|
||||
SetVertexParm( RENDERPARM_BUMPMATRIX_S, din->bumpMatrix[0].ToFloatPtr() );
|
||||
SetVertexParm( RENDERPARM_BUMPMATRIX_T, din->bumpMatrix[1].ToFloatPtr() );
|
||||
|
@ -1836,7 +1865,7 @@ void idRenderBackend::RenderInteractions( const drawSurf_t* surfList, const view
|
|||
// draw any previous interaction
|
||||
if( inter.bumpImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, false );
|
||||
}
|
||||
inter.bumpImage = surfaceStage->texture.image;
|
||||
inter.diffuseImage = NULL;
|
||||
|
@ -1854,7 +1883,7 @@ void idRenderBackend::RenderInteractions( const drawSurf_t* surfList, const view
|
|||
// draw any previous interaction
|
||||
if( inter.diffuseImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, false );
|
||||
}
|
||||
inter.diffuseImage = surfaceStage->texture.image;
|
||||
inter.vertexColor = surfaceStage->vertexColor;
|
||||
|
@ -1872,7 +1901,7 @@ void idRenderBackend::RenderInteractions( const drawSurf_t* surfList, const view
|
|||
// draw any previous interaction
|
||||
if( inter.specularImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, false );
|
||||
}
|
||||
inter.specularImage = surfaceStage->texture.image;
|
||||
inter.vertexColor = surfaceStage->vertexColor;
|
||||
|
@ -1884,7 +1913,7 @@ void idRenderBackend::RenderInteractions( const drawSurf_t* surfList, const view
|
|||
}
|
||||
|
||||
// draw the final interaction
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, false );
|
||||
|
||||
renderLog.CloseBlock();
|
||||
}
|
||||
|
@ -2243,7 +2272,7 @@ void idRenderBackend::AmbientPass( const drawSurf_t* const* drawSurfs, int numDr
|
|||
// draw any previous interaction
|
||||
if( inter.bumpImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, !fillGbuffer );
|
||||
}
|
||||
inter.bumpImage = surfaceStage->texture.image;
|
||||
inter.diffuseImage = NULL;
|
||||
|
@ -2264,7 +2293,7 @@ void idRenderBackend::AmbientPass( const drawSurf_t* const* drawSurfs, int numDr
|
|||
// draw any previous interaction
|
||||
if( inter.diffuseImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, !fillGbuffer );
|
||||
}
|
||||
|
||||
inter.diffuseImage = surfaceStage->texture.image;
|
||||
|
@ -2284,7 +2313,7 @@ void idRenderBackend::AmbientPass( const drawSurf_t* const* drawSurfs, int numDr
|
|||
// draw any previous interaction
|
||||
if( inter.specularImage != NULL )
|
||||
{
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter, !fillGbuffer );
|
||||
}
|
||||
inter.specularImage = surfaceStage->texture.image;
|
||||
inter.vertexColor = surfaceStage->vertexColor;
|
||||
|
@ -2296,7 +2325,7 @@ void idRenderBackend::AmbientPass( const drawSurf_t* const* drawSurfs, int numDr
|
|||
}
|
||||
|
||||
// draw the final interaction
|
||||
DrawSingleInteraction( &inter );
|
||||
DrawSingleInteraction( &inter,!fillGbuffer );
|
||||
|
||||
renderLog.CloseBlock();
|
||||
}
|
||||
|
|
|
@ -109,8 +109,8 @@ void idRenderProgManager::Init()
|
|||
{ BUILTIN_VERTEX_COLOR, "vertex_color.vfp", "", 0, false, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_AMBIENT_LIGHTING, "ambient_lighting", "", 0, false, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_AMBIENT_LIGHTING_SKINNED, "ambient_lighting", "_skinned", BIT( USE_GPU_SKINNING ), true, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_AMBIENT_LIGHTING_IBL, "ambient_lighting_IBL", "", 0, false false, SHADER_STAGE_DEFAULT, LAYOUT_DRA },
|
||||
{ BUILTIN_AMBIENT_LIGHTING_IBL_SKINNED, "ambient_lighting_IBL", "_skinned", BIT( USE_GPU_SKINNING ), true false, SHADER_STAGE_DEFAULT, LAYOUT_DRA },
|
||||
{ BUILTIN_AMBIENT_LIGHTING_IBL, "ambient_lighting_IBL", "", 0, false false, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_AMBIENT_LIGHTING_IBL_SKINNED, "ambient_lighting_IBL", "_skinned", BIT( USE_GPU_SKINNING ), true false, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_SMALL_GEOMETRY_BUFFER, "gbuffer", "", 0, false, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
{ BUILTIN_SMALL_GEOMETRY_BUFFER_SKINNED, "gbuffer", "_skinned", BIT( USE_GPU_SKINNING ), true, SHADER_STAGE_DEFAULT, LAYOUT_DRAW_VERT },
|
||||
// RB end
|
||||
|
@ -245,6 +245,7 @@ void idRenderProgManager::Init()
|
|||
// RB begin
|
||||
renderProgs[builtinShaders[BUILTIN_AMBIENT_LIGHTING_SKINNED]].usesJoints = true;
|
||||
renderProgs[builtinShaders[BUILTIN_AMBIENT_LIGHTING_IBL_SKINNED]].usesJoints = true;
|
||||
renderProgs[builtinShaders[BUILTIN_AMBIENT_LIGHTING_IBL_PBR_SKINNED]].usesJoints = true;
|
||||
renderProgs[builtinShaders[BUILTIN_SMALL_GEOMETRY_BUFFER_SKINNED]].usesJoints = true;
|
||||
renderProgs[builtinShaders[BUILTIN_INTERACTION_SHADOW_MAPPING_SPOT_SKINNED]].usesJoints = true;
|
||||
renderProgs[builtinShaders[BUILTIN_INTERACTION_SHADOW_MAPPING_POINT_SKINNED]].usesJoints = true;
|
||||
|
|
|
@ -300,6 +300,16 @@ public:
|
|||
BindShader_Builtin( BUILTIN_AMBIENT_LIGHTING_IBL_SKINNED );
|
||||
}
|
||||
|
||||
void BindShader_ImageBasedLighting_PBR()
|
||||
{
|
||||
BindShader_Builtin( BUILTIN_AMBIENT_LIGHTING_IBL_PBR );
|
||||
}
|
||||
|
||||
void BindShader_ImageBasedLightingSkinned_PBR()
|
||||
{
|
||||
BindShader_Builtin( BUILTIN_AMBIENT_LIGHTING_IBL_PBR_SKINNED );
|
||||
}
|
||||
|
||||
void BindShader_SmallGeometryBuffer()
|
||||
{
|
||||
BindShader_Builtin( BUILTIN_SMALL_GEOMETRY_BUFFER );
|
||||
|
@ -659,6 +669,8 @@ private:
|
|||
BUILTIN_AMBIENT_LIGHTING_SKINNED,
|
||||
BUILTIN_AMBIENT_LIGHTING_IBL,
|
||||
BUILTIN_AMBIENT_LIGHTING_IBL_SKINNED,
|
||||
BUILTIN_AMBIENT_LIGHTING_IBL_PBR,
|
||||
BUILTIN_AMBIENT_LIGHTING_IBL_PBR_SKINNED,
|
||||
BUILTIN_SMALL_GEOMETRY_BUFFER,
|
||||
BUILTIN_SMALL_GEOMETRY_BUFFER_SKINNED,
|
||||
// RB end
|
||||
|
@ -744,6 +756,7 @@ private:
|
|||
BRIGHTPASS,
|
||||
HDR_DEBUG,
|
||||
USE_SRGB,
|
||||
USE_PBR,
|
||||
|
||||
MAX_SHADER_MACRO_NAMES,
|
||||
};
|
||||
|
|
|
@ -314,7 +314,8 @@ const char* idRenderProgManager::GLSLMacroNames[MAX_SHADER_MACRO_NAMES] =
|
|||
"LIGHT_PARALLEL",
|
||||
"BRIGHTPASS",
|
||||
"HDR_DEBUG",
|
||||
"USE_SRGB"
|
||||
"USE_SRGB",
|
||||
"USE_PBR"
|
||||
};
|
||||
// RB end
|
||||
|
||||
|
|
Loading…
Reference in a new issue