forked from teamnwah/openmw-tes3coop
Add terrain parallax mapping
This commit is contained in:
parent
e381957105
commit
157c11398d
14 changed files with 78 additions and 40 deletions
|
@ -174,6 +174,7 @@ namespace MWRender
|
||||||
resourceSystem->getSceneManager()->setForcePerPixelLighting(Settings::Manager::getBool("force per pixel lighting", "Shaders"));
|
resourceSystem->getSceneManager()->setForcePerPixelLighting(Settings::Manager::getBool("force per pixel lighting", "Shaders"));
|
||||||
resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::Manager::getBool("auto use object normal maps", "Shaders"));
|
resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::Manager::getBool("auto use object normal maps", "Shaders"));
|
||||||
resourceSystem->getSceneManager()->setNormalMapPattern(Settings::Manager::getString("normal map pattern", "Shaders"));
|
resourceSystem->getSceneManager()->setNormalMapPattern(Settings::Manager::getString("normal map pattern", "Shaders"));
|
||||||
|
resourceSystem->getSceneManager()->setNormalHeightMapPattern(Settings::Manager::getString("normal height map pattern", "Shaders"));
|
||||||
resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders"));
|
resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders"));
|
||||||
resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders"));
|
resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders"));
|
||||||
|
|
||||||
|
@ -197,7 +198,8 @@ namespace MWRender
|
||||||
mWater.reset(new Water(mRootNode, sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), fallback, resourcePath));
|
mWater.reset(new Water(mRootNode, sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), fallback, resourcePath));
|
||||||
|
|
||||||
mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(),
|
mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(),
|
||||||
new TerrainStorage(mResourceSystem->getVFS(), Settings::Manager::getString("normal map pattern", "Shaders"), Settings::Manager::getBool("auto use terrain normal maps", "Shaders"),
|
new TerrainStorage(mResourceSystem->getVFS(), Settings::Manager::getString("normal map pattern", "Shaders"), Settings::Manager::getString("normal height map pattern", "Shaders"),
|
||||||
|
Settings::Manager::getBool("auto use terrain normal maps", "Shaders"),
|
||||||
Settings::Manager::getString("terrain specular map pattern", "Shaders"), Settings::Manager::getBool("auto use terrain specular maps", "Shaders")),
|
Settings::Manager::getString("terrain specular map pattern", "Shaders"), Settings::Manager::getBool("auto use terrain specular maps", "Shaders")),
|
||||||
Mask_Terrain, &mResourceSystem->getSceneManager()->getShaderManager(), mUnrefQueue.get()));
|
Mask_Terrain, &mResourceSystem->getSceneManager()->getShaderManager(), mUnrefQueue.get()));
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
namespace MWRender
|
namespace MWRender
|
||||||
{
|
{
|
||||||
|
|
||||||
TerrainStorage::TerrainStorage(const VFS::Manager* vfs, const std::string& normalMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps)
|
TerrainStorage::TerrainStorage(const VFS::Manager* vfs, const std::string& normalMapPattern, const std::string& normalHeightMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps)
|
||||||
: ESMTerrain::Storage(vfs, normalMapPattern, autoUseNormalMaps, specularMapPattern, autoUseSpecularMaps)
|
: ESMTerrain::Storage(vfs, normalMapPattern, normalHeightMapPattern, autoUseNormalMaps, specularMapPattern, autoUseSpecularMaps)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace MWRender
|
||||||
virtual const ESM::LandTexture* getLandTexture(int index, short plugin);
|
virtual const ESM::LandTexture* getLandTexture(int index, short plugin);
|
||||||
public:
|
public:
|
||||||
|
|
||||||
TerrainStorage(const VFS::Manager* vfs, const std::string& normalMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false);
|
TerrainStorage(const VFS::Manager* vfs, const std::string& normalMapPattern = "", const std::string& normalHeightMapPatteern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false);
|
||||||
|
|
||||||
/// Get bounds of the whole terrain in cell units
|
/// Get bounds of the whole terrain in cell units
|
||||||
virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY);
|
virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY);
|
||||||
|
|
|
@ -18,9 +18,10 @@ namespace ESMTerrain
|
||||||
|
|
||||||
const float defaultHeight = ESM::Land::DEFAULT_HEIGHT;
|
const float defaultHeight = ESM::Land::DEFAULT_HEIGHT;
|
||||||
|
|
||||||
Storage::Storage(const VFS::Manager *vfs, const std::string& normalMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps)
|
Storage::Storage(const VFS::Manager *vfs, const std::string& normalMapPattern, const std::string& normalHeightMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps)
|
||||||
: mVFS(vfs)
|
: mVFS(vfs)
|
||||||
, mNormalMapPattern(normalMapPattern)
|
, mNormalMapPattern(normalMapPattern)
|
||||||
|
, mNormalHeightMapPattern(normalHeightMapPattern)
|
||||||
, mAutoUseNormalMaps(autoUseNormalMaps)
|
, mAutoUseNormalMaps(autoUseNormalMaps)
|
||||||
, mSpecularMapPattern(specularMapPattern)
|
, mSpecularMapPattern(specularMapPattern)
|
||||||
, mAutoUseSpecularMaps(autoUseSpecularMaps)
|
, mAutoUseSpecularMaps(autoUseSpecularMaps)
|
||||||
|
@ -512,26 +513,26 @@ namespace ESMTerrain
|
||||||
return found->second;
|
return found->second;
|
||||||
|
|
||||||
Terrain::LayerInfo info;
|
Terrain::LayerInfo info;
|
||||||
//info.mParallax = false;
|
info.mParallax = false;
|
||||||
info.mSpecular = false;
|
info.mSpecular = false;
|
||||||
info.mDiffuseMap = texture;
|
info.mDiffuseMap = texture;
|
||||||
|
|
||||||
/*
|
|
||||||
std::string texture_ = texture;
|
|
||||||
boost::replace_last(texture_, ".", "_nh.");
|
|
||||||
|
|
||||||
if (mVFS->exists(texture_))
|
|
||||||
{
|
|
||||||
info.mNormalMap = texture_;
|
|
||||||
info.mParallax = true;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (mAutoUseNormalMaps)
|
if (mAutoUseNormalMaps)
|
||||||
{
|
{
|
||||||
std::string texture_ = texture;
|
std::string texture_ = texture;
|
||||||
boost::replace_last(texture_, ".", mNormalMapPattern + ".");
|
boost::replace_last(texture_, ".", mNormalHeightMapPattern + ".");
|
||||||
if (mVFS->exists(texture_))
|
if (mVFS->exists(texture_))
|
||||||
|
{
|
||||||
info.mNormalMap = texture_;
|
info.mNormalMap = texture_;
|
||||||
|
info.mParallax = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
texture_ = texture;
|
||||||
|
boost::replace_last(texture_, ".", mNormalMapPattern + ".");
|
||||||
|
if (mVFS->exists(texture_))
|
||||||
|
info.mNormalMap = texture_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mAutoUseSpecularMaps)
|
if (mAutoUseSpecularMaps)
|
||||||
|
@ -554,7 +555,7 @@ namespace ESMTerrain
|
||||||
{
|
{
|
||||||
Terrain::LayerInfo info;
|
Terrain::LayerInfo info;
|
||||||
info.mDiffuseMap = "textures\\_land_default.dds";
|
info.mDiffuseMap = "textures\\_land_default.dds";
|
||||||
//info.mParallax = false;
|
info.mParallax = false;
|
||||||
info.mSpecular = false;
|
info.mSpecular = false;
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ namespace ESMTerrain
|
||||||
virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0;
|
virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Storage(const VFS::Manager* vfs, const std::string& normalMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false);
|
Storage(const VFS::Manager* vfs, const std::string& normalMapPattern = "", const std::string& normalHeightMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false);
|
||||||
|
|
||||||
/// Data is loaded first, if necessary. Will return a 0-pointer if there is no data for
|
/// Data is loaded first, if necessary. Will return a 0-pointer if there is no data for
|
||||||
/// any of the data types specified via \a flags. Will also return a 0-pointer if there
|
/// any of the data types specified via \a flags. Will also return a 0-pointer if there
|
||||||
|
@ -110,6 +110,7 @@ namespace ESMTerrain
|
||||||
OpenThreads::Mutex mLayerInfoMutex;
|
OpenThreads::Mutex mLayerInfoMutex;
|
||||||
|
|
||||||
std::string mNormalMapPattern;
|
std::string mNormalMapPattern;
|
||||||
|
std::string mNormalHeightMapPattern;
|
||||||
bool mAutoUseNormalMaps;
|
bool mAutoUseNormalMaps;
|
||||||
|
|
||||||
std::string mSpecularMapPattern;
|
std::string mSpecularMapPattern;
|
||||||
|
|
|
@ -241,6 +241,11 @@ namespace Resource
|
||||||
mNormalMapPattern = pattern;
|
mNormalMapPattern = pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SceneManager::setNormalHeightMapPattern(const std::string &pattern)
|
||||||
|
{
|
||||||
|
mNormalHeightMapPattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
void SceneManager::setAutoUseSpecularMaps(bool use)
|
void SceneManager::setAutoUseSpecularMaps(bool use)
|
||||||
{
|
{
|
||||||
mAutoUseSpecularMaps = use;
|
mAutoUseSpecularMaps = use;
|
||||||
|
|
|
@ -63,6 +63,9 @@ namespace Resource
|
||||||
/// @see ShaderVisitor::setNormalMapPattern
|
/// @see ShaderVisitor::setNormalMapPattern
|
||||||
void setNormalMapPattern(const std::string& pattern);
|
void setNormalMapPattern(const std::string& pattern);
|
||||||
|
|
||||||
|
/// @see ShaderVisitor::setNormalHeightMapPattern
|
||||||
|
void setNormalHeightMapPattern(const std::string& pattern);
|
||||||
|
|
||||||
void setAutoUseSpecularMaps(bool use);
|
void setAutoUseSpecularMaps(bool use);
|
||||||
|
|
||||||
void setSpecularMapPattern(const std::string& pattern);
|
void setSpecularMapPattern(const std::string& pattern);
|
||||||
|
@ -138,6 +141,7 @@ namespace Resource
|
||||||
bool mForcePerPixelLighting;
|
bool mForcePerPixelLighting;
|
||||||
bool mAutoUseNormalMaps;
|
bool mAutoUseNormalMaps;
|
||||||
std::string mNormalMapPattern;
|
std::string mNormalMapPattern;
|
||||||
|
std::string mNormalHeightMapPattern;
|
||||||
bool mAutoUseSpecularMaps;
|
bool mAutoUseSpecularMaps;
|
||||||
std::string mSpecularMapPattern;
|
std::string mSpecularMapPattern;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace Terrain
|
||||||
{
|
{
|
||||||
std::string mDiffuseMap;
|
std::string mDiffuseMap;
|
||||||
std::string mNormalMap;
|
std::string mNormalMap;
|
||||||
//bool mParallax; // Height info in normal map alpha channel?
|
bool mParallax; // Height info in normal map alpha channel?
|
||||||
bool mSpecular; // Specular info in diffuse map alpha channel?
|
bool mSpecular; // Specular info in diffuse map alpha channel?
|
||||||
|
|
||||||
bool requiresShaders() const { return !mNormalMap.empty() || mSpecular; }
|
bool requiresShaders() const { return !mNormalMap.empty() || mSpecular; }
|
||||||
|
|
|
@ -157,6 +157,7 @@ namespace Terrain
|
||||||
defineMap["blendMap"] = !firstLayer ? "1" : "0";
|
defineMap["blendMap"] = !firstLayer ? "1" : "0";
|
||||||
defineMap["colorMode"] = "2";
|
defineMap["colorMode"] = "2";
|
||||||
defineMap["specularMap"] = it->mSpecular ? "1" : "0";
|
defineMap["specularMap"] = it->mSpecular ? "1" : "0";
|
||||||
|
defineMap["parallax"] = (it->mNormalMap && it->mParallax) ? "1" : "0";
|
||||||
|
|
||||||
osg::ref_ptr<osg::Shader> vertexShader = shaderManager.getShader("terrain_vertex.glsl", defineMap, osg::Shader::VERTEX);
|
osg::ref_ptr<osg::Shader> vertexShader = shaderManager.getShader("terrain_vertex.glsl", defineMap, osg::Shader::VERTEX);
|
||||||
osg::ref_ptr<osg::Shader> fragmentShader = shaderManager.getShader("terrain_fragment.glsl", defineMap, osg::Shader::FRAGMENT);
|
osg::ref_ptr<osg::Shader> fragmentShader = shaderManager.getShader("terrain_fragment.glsl", defineMap, osg::Shader::FRAGMENT);
|
||||||
|
|
|
@ -23,6 +23,7 @@ namespace Terrain
|
||||||
{
|
{
|
||||||
osg::ref_ptr<osg::Texture2D> mDiffuseMap;
|
osg::ref_ptr<osg::Texture2D> mDiffuseMap;
|
||||||
osg::ref_ptr<osg::Texture2D> mNormalMap; // optional
|
osg::ref_ptr<osg::Texture2D> mNormalMap; // optional
|
||||||
|
bool mParallax;
|
||||||
bool mSpecular;
|
bool mSpecular;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -160,6 +160,7 @@ osg::ref_ptr<osg::Node> TerrainGrid::buildTerrain (osg::Group* parent, float chu
|
||||||
for (std::vector<LayerInfo>::const_iterator it = layerList.begin(); it != layerList.end(); ++it)
|
for (std::vector<LayerInfo>::const_iterator it = layerList.begin(); it != layerList.end(); ++it)
|
||||||
{
|
{
|
||||||
TextureLayer textureLayer;
|
TextureLayer textureLayer;
|
||||||
|
textureLayer.mParallax = it->mParallax;
|
||||||
textureLayer.mSpecular = it->mSpecular;
|
textureLayer.mSpecular = it->mSpecular;
|
||||||
osg::ref_ptr<osg::Texture2D> texture = mTextureCache[it->mDiffuseMap];
|
osg::ref_ptr<osg::Texture2D> texture = mTextureCache[it->mDiffuseMap];
|
||||||
if (!texture)
|
if (!texture)
|
||||||
|
|
|
@ -200,6 +200,10 @@ auto use terrain specular maps = false
|
||||||
# The filename pattern to probe for when detecting normal maps (see 'auto use object normal maps', 'auto use terrain normal maps')
|
# The filename pattern to probe for when detecting normal maps (see 'auto use object normal maps', 'auto use terrain normal maps')
|
||||||
normal map pattern = _n
|
normal map pattern = _n
|
||||||
|
|
||||||
|
# Alternative filename pattern to probe for when detecting normal maps. Files with this pattern are expected to include 'height' in the alpha channel.
|
||||||
|
# This height is used for parallax effects. Works for both terrain and objects.
|
||||||
|
normal height map pattern = _nh
|
||||||
|
|
||||||
# The filename pattern to probe for when detecting object specular maps (see 'auto use object specular maps')
|
# The filename pattern to probe for when detecting object specular maps (see 'auto use object specular maps')
|
||||||
specular map pattern = _spec
|
specular map pattern = _spec
|
||||||
|
|
||||||
|
|
|
@ -22,15 +22,47 @@ varying vec4 lighting;
|
||||||
varying vec4 passColor;
|
varying vec4 passColor;
|
||||||
#endif
|
#endif
|
||||||
varying vec3 passViewPos;
|
varying vec3 passViewPos;
|
||||||
varying vec3 passViewNormal;
|
varying vec3 passNormal;
|
||||||
|
|
||||||
|
#if @parallax
|
||||||
|
#define PARALLAX_SCALE 0.04
|
||||||
|
#define PARALLAX_BIAS -0.02
|
||||||
|
uniform mat4 osg_ViewMatrixInverse;
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "lighting.glsl"
|
#include "lighting.glsl"
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec2 diffuseMapUV = (gl_TextureMatrix[0] * vec4(uv, 0.0, 1.0)).xy;
|
vec2 adjustedUV = (gl_TextureMatrix[0] * vec4(uv, 0.0, 1.0)).xy;
|
||||||
|
|
||||||
vec4 diffuseTex = texture2D(diffuseMap, diffuseMapUV);
|
#if @normalMap
|
||||||
|
vec4 normalTex = texture2D(normalMap, adjustedUV);
|
||||||
|
|
||||||
|
vec3 normalizedNormal = normalize(passNormal);
|
||||||
|
vec3 tangent = vec3(1.0, 0.0, 0.0);
|
||||||
|
vec3 binormal = normalize(cross(tangent, normalizedNormal));
|
||||||
|
tangent = normalize(cross(normalizedNormal, binormal)); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal
|
||||||
|
mat3 tbn = mat3(tangent, binormal, normalizedNormal);
|
||||||
|
|
||||||
|
vec3 viewNormal = normalize(gl_NormalMatrix * (tbn * (normalTex.xyz * 2.0 - 1.0)));
|
||||||
|
#else
|
||||||
|
vec3 viewNormal = normalize(gl_NormalMatrix * passNormal);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if @parallax
|
||||||
|
vec3 cameraPos = osg_ViewMatrixInverse[3].xyz;
|
||||||
|
vec3 eyeDir = normalize(cameraPos - (osg_ViewMatrixInverse * vec4(passViewPos, 1)).xyz);
|
||||||
|
vec3 TSeyeDir = normalize((vec4(normalize(tbn * eyeDir),0)).xyz);
|
||||||
|
|
||||||
|
adjustedUV += TSeyeDir.xy * ( normalTex.a * PARALLAX_SCALE + PARALLAX_BIAS );
|
||||||
|
|
||||||
|
// update normal using new coordinates
|
||||||
|
normalTex = texture2D(normalMap, adjustedUV);
|
||||||
|
viewNormal = normalize(gl_NormalMatrix * (tbn * (normalTex.xyz * 2.0 - 1.0)));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
vec4 diffuseTex = texture2D(diffuseMap, adjustedUV);
|
||||||
gl_FragData[0] = vec4(diffuseTex.xyz, 1.0);
|
gl_FragData[0] = vec4(diffuseTex.xyz, 1.0);
|
||||||
|
|
||||||
#if @blendMap
|
#if @blendMap
|
||||||
|
@ -38,20 +70,6 @@ void main()
|
||||||
gl_FragData[0].a *= texture2D(blendMap, blendMapUV).a;
|
gl_FragData[0].a *= texture2D(blendMap, blendMapUV).a;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
vec3 viewNormal = passViewNormal;
|
|
||||||
|
|
||||||
#if @normalMap
|
|
||||||
vec3 normalTex = texture2D(normalMap, diffuseMapUV).xyz;
|
|
||||||
|
|
||||||
vec3 viewTangent = (gl_ModelViewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;
|
|
||||||
vec3 viewBinormal = normalize(cross(viewTangent, viewNormal));
|
|
||||||
viewTangent = normalize(cross(viewNormal, viewBinormal)); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal
|
|
||||||
mat3 tbn = mat3(viewTangent, viewBinormal, viewNormal);
|
|
||||||
|
|
||||||
viewNormal = normalize(tbn * (normalTex * 2.0 - 1.0));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
#if !PER_PIXEL_LIGHTING
|
#if !PER_PIXEL_LIGHTING
|
||||||
gl_FragData[0] *= lighting;
|
gl_FragData[0] *= lighting;
|
||||||
#else
|
#else
|
||||||
|
|
|
@ -11,7 +11,7 @@ varying vec4 lighting;
|
||||||
varying vec4 passColor;
|
varying vec4 passColor;
|
||||||
#endif
|
#endif
|
||||||
varying vec3 passViewPos;
|
varying vec3 passViewPos;
|
||||||
varying vec3 passViewNormal;
|
varying vec3 passNormal;
|
||||||
|
|
||||||
#include "lighting.glsl"
|
#include "lighting.glsl"
|
||||||
|
|
||||||
|
@ -22,14 +22,14 @@ void main(void)
|
||||||
|
|
||||||
vec4 viewPos = (gl_ModelViewMatrix * gl_Vertex);
|
vec4 viewPos = (gl_ModelViewMatrix * gl_Vertex);
|
||||||
gl_ClipVertex = viewPos;
|
gl_ClipVertex = viewPos;
|
||||||
vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz);
|
|
||||||
|
|
||||||
#if !PER_PIXEL_LIGHTING
|
#if !PER_PIXEL_LIGHTING
|
||||||
|
vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz);
|
||||||
lighting = doLighting(viewPos.xyz, viewNormal, gl_Color);
|
lighting = doLighting(viewPos.xyz, viewNormal, gl_Color);
|
||||||
#else
|
#else
|
||||||
passColor = gl_Color;
|
passColor = gl_Color;
|
||||||
#endif
|
#endif
|
||||||
passViewNormal = viewNormal;
|
passNormal = gl_Normal.xyz;
|
||||||
passViewPos = viewPos.xyz;
|
passViewPos = viewPos.xyz;
|
||||||
|
|
||||||
uv = gl_MultiTexCoord0.xy;
|
uv = gl_MultiTexCoord0.xy;
|
||||||
|
|
Loading…
Reference in a new issue