Introduce normal mapping
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ build
|
||||
res/models
|
||||
res/textures
|
||||
.kdev4
|
||||
.cache
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if(UNIX)
|
||||
set(CMAKE_GENERATOR "Ninja" CACHE INTERNAL "" FORCE)
|
||||
set(CMAKE_GENERATOR "Unix Makefiles" CACHE INTERNAL "" FORCE)
|
||||
endif(UNIX)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
{
|
||||
"unique_name": "cube",
|
||||
"path": "data/res/models/cube.ffo"
|
||||
},
|
||||
{
|
||||
"unique_name": "container",
|
||||
"path": "data/res/models/container.ffo"
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
@@ -26,6 +30,13 @@
|
||||
"rotation": [0.0, 0.0, 0.0],
|
||||
"scale": 0.6
|
||||
},
|
||||
{
|
||||
"unique_name": "container",
|
||||
"model": "container",
|
||||
"shaderProgram": "defaultProgram",
|
||||
"position": [10.0, 1.0, 0.0],
|
||||
"rotation": [45.0, 45.0, 45.0]
|
||||
},
|
||||
{
|
||||
"unique_name": "ground",
|
||||
"model": "ground",
|
||||
@@ -37,6 +48,12 @@
|
||||
"shaderProgram": "lightProgram"
|
||||
}
|
||||
],
|
||||
"textures" : [
|
||||
{
|
||||
"unique_name": "fallback_normal",
|
||||
"path": "data/res/models/tex/fallback_normal.png"
|
||||
}
|
||||
],
|
||||
"skybox": {
|
||||
"texturePath": "data/res/textures/skybox/"
|
||||
}
|
||||
|
||||
2
data/res
2
data/res
Submodule data/res updated: 598d92bdd6...1cf275e513
@@ -5,42 +5,39 @@ layout(location = 0) out vec4 f_color;
|
||||
in vec3 v_normal;
|
||||
in vec2 v_texCoord;
|
||||
in vec3 v_fragmentPosition;
|
||||
in vec3 v_fragmentPositionTangent;
|
||||
in vec4 v_fragmentPositionDirectionalLightSpace;
|
||||
|
||||
in vec3 v_lightDirectionTangent;
|
||||
in vec3 v_lightPositionTangent0;
|
||||
//in vec3 v_lightPositionTangent1;
|
||||
//in vec3 v_lightPositionTangent2;
|
||||
//in vec3 v_lightPositionTangent3;
|
||||
|
||||
in vec3 v_viewPositionTangent;
|
||||
|
||||
struct Material {
|
||||
sampler2D texture_diffuse0;
|
||||
sampler2D texture_diffuse1;
|
||||
sampler2D texture_specular0;
|
||||
sampler2D texture_specular1;
|
||||
sampler2D texture_normal0;
|
||||
sampler2D texture_normal1;
|
||||
sampler2D texture_height0;
|
||||
sampler2D texture_height1;
|
||||
sampler2D texture_gloss0;
|
||||
sampler2D texture_gloss1;
|
||||
float shininess;
|
||||
};
|
||||
uniform Material u_material;
|
||||
|
||||
struct DirectionalLight {
|
||||
bool isActive;
|
||||
vec3 direction;
|
||||
|
||||
vec3 ambient;
|
||||
vec3 diffuse;
|
||||
vec3 specular;
|
||||
bool isActive;
|
||||
vec3 color;
|
||||
};
|
||||
uniform DirectionalLight u_directionalLight;
|
||||
|
||||
struct PointLight {
|
||||
bool isActive;
|
||||
vec3 position;
|
||||
|
||||
float K_q;
|
||||
|
||||
vec3 ambient;
|
||||
vec3 diffuse;
|
||||
vec3 specular;
|
||||
bool isActive;
|
||||
vec3 color;
|
||||
};
|
||||
#define NUM_POINT_LIGHTS 1
|
||||
uniform PointLight u_pointLight[NUM_POINT_LIGHTS];
|
||||
@@ -62,7 +59,6 @@ uniform PointLight u_pointLight[NUM_POINT_LIGHTS];
|
||||
uniform SpotLight u_spotLight;*/
|
||||
|
||||
uniform mat3 u_normalMatrix;
|
||||
uniform vec3 u_viewPosition;
|
||||
|
||||
uniform sampler2D u_texture_directionalShadowMap;
|
||||
uniform samplerCube u_texture_pointShadowMap0;
|
||||
@@ -101,17 +97,18 @@ void main() {
|
||||
|
||||
vec3 fragmentColor = vec3(0.0f);
|
||||
|
||||
vec3 normal = normalize(u_normalMatrix * v_normal);
|
||||
vec3 viewDir = normalize(u_viewPosition - v_fragmentPosition);
|
||||
vec3 normal = texture(u_material.texture_normal0, v_texCoord).rgb;
|
||||
normal = normalize(normal * 2.0 - 1.0);
|
||||
vec3 viewDir = normalize(v_viewPositionTangent - v_fragmentPositionTangent);
|
||||
|
||||
fragmentColor += directionalLightContribution(u_directionalLight, normal, viewDir);
|
||||
|
||||
for(int i = 0; i < NUM_POINT_LIGHTS; i++) {
|
||||
fragmentColor += pointLightContribution(u_pointLight[i], normal, v_fragmentPosition, viewDir);
|
||||
fragmentColor += pointLightContribution(u_pointLight[i], normal, v_fragmentPositionTangent, viewDir);
|
||||
}
|
||||
|
||||
// There are currently no spotlights
|
||||
//fragmentColor += spotLightContribution(u_spotLight, normal, v_fragmentPosition, viewDir);
|
||||
//fragmentColor += spotLightContribution(u_spotLight, normal, v_fragmentPositionTangent, viewDir);
|
||||
|
||||
f_color = vec4(fragmentColor, 1.0f);
|
||||
|
||||
@@ -123,10 +120,15 @@ vec3 directionalLightContribution(DirectionalLight light, vec3 normal, vec3 view
|
||||
if(!light.isActive)
|
||||
return vec3(0.0f);
|
||||
|
||||
vec3 lightDir = normalize(-light.direction);
|
||||
//vec3 lightDir = normalize(-light.direction);
|
||||
vec3 lightDir = normalize(-v_lightDirectionTangent);
|
||||
|
||||
vec3 diffuseColor = light.color;
|
||||
vec3 specularColor = light.color;
|
||||
vec3 ambientColor = light.color * 0.002f;
|
||||
|
||||
vec3 ambient, diffuse, specular;
|
||||
computeShading(light.ambient, light.diffuse, light.specular, lightDir, viewDir, normal, ambient, diffuse, specular);
|
||||
computeShading(ambientColor, diffuseColor, specularColor, lightDir, viewDir, normal, ambient, diffuse, specular);
|
||||
|
||||
float shadows = 0.0f;
|
||||
if(b_drawShadows)
|
||||
@@ -141,19 +143,23 @@ vec3 pointLightContribution(PointLight light, vec3 normal, vec3 fragPos, vec3 vi
|
||||
if(!light.isActive)
|
||||
return vec3(0.0f);
|
||||
|
||||
vec3 lightDir = normalize(light.position - fragPos);
|
||||
vec3 lightDir = normalize(v_lightPositionTangent0 - fragPos);
|
||||
|
||||
vec3 diffuseColor = light.color;
|
||||
vec3 specularColor = light.color;
|
||||
vec3 ambientColor = light.color * 0.002f;
|
||||
|
||||
vec3 ambient, diffuse, specular;
|
||||
computeShading(light.ambient, light.diffuse, light.specular, lightDir, viewDir, normal, ambient, diffuse, specular);
|
||||
computeShading(ambientColor, diffuseColor, specularColor, lightDir, viewDir, normal, ambient, diffuse, specular);
|
||||
|
||||
float attenuation = computeAttenuation(light.position, fragPos, light.K_q);
|
||||
ambient *= attenuation;
|
||||
diffuse *= attenuation;
|
||||
specular *= attenuation;
|
||||
float attenuation = computeAttenuation(v_lightPositionTangent0, fragPos, 0.032f);
|
||||
//ambient *= attenuation;
|
||||
//diffuse *= attenuation;
|
||||
//specular *= attenuation;
|
||||
|
||||
float shadows = 0.0f;
|
||||
if(b_drawShadows)
|
||||
shadows = computePointShadows(fragPos, light.position);
|
||||
shadows = computePointShadows(v_fragmentPosition, light.position);
|
||||
|
||||
return (ambient + (1.0f - shadows) * (diffuse + specular));
|
||||
}
|
||||
@@ -240,7 +246,6 @@ float computeDirectionalShadows(vec4 fragPosLightSpace, vec3 normal, vec3 lightD
|
||||
}
|
||||
shadow /= 9.0f;
|
||||
|
||||
|
||||
return shadow;
|
||||
}
|
||||
|
||||
@@ -253,9 +258,9 @@ float computePointShadows(vec3 fragPos, vec3 lightPos) {
|
||||
float currentDepth = length(fragToLight);
|
||||
|
||||
float shadow = 0.0;
|
||||
float bias = 0.05;
|
||||
int samples = 20;
|
||||
float viewDistance = length(u_viewPosition - fragPos);
|
||||
float bias = 0.05;
|
||||
int samples = 20;
|
||||
float viewDistance = length(v_viewPositionTangent - fragPos);
|
||||
float diskRadius = 0.05;
|
||||
|
||||
for(int i = 0; i < samples; ++i) {
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
#version 330 core
|
||||
|
||||
layout(location = 0) in vec3 a_position;
|
||||
layout(location = 1) in vec3 a_normal;
|
||||
layout(location = 2) in vec2 a_texCoord;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
layout(location = 2) in vec3 a_normal;
|
||||
layout(location = 3) in vec3 a_tangent;
|
||||
layout(location = 4) in vec3 a_bitangent;
|
||||
|
||||
out vec3 v_normal;
|
||||
out vec2 v_texCoord;
|
||||
|
||||
out vec3 v_fragmentPosition;
|
||||
out vec3 v_fragmentPositionTangent;
|
||||
out vec4 v_fragmentPositionDirectionalLightSpace;
|
||||
|
||||
out vec3 v_viewPositionTangent;
|
||||
|
||||
struct DirectionalLight {
|
||||
vec3 direction;
|
||||
bool isActive;
|
||||
vec3 color;
|
||||
};
|
||||
uniform DirectionalLight u_directionalLight;
|
||||
out vec3 v_lightDirectionTangent;
|
||||
|
||||
struct PointLight {
|
||||
vec3 position;
|
||||
bool isActive;
|
||||
vec3 color;
|
||||
};
|
||||
#define NUM_POINT_LIGHTS 1
|
||||
uniform PointLight u_pointLight[NUM_POINT_LIGHTS];
|
||||
out vec3 v_lightPositionTangent0;
|
||||
//out vec3 v_lightPositionTangent1;
|
||||
//out vec3 v_lightPositionTangent2;
|
||||
//out vec3 v_lightPositionTangent3;
|
||||
|
||||
uniform vec3 u_viewPosition;
|
||||
|
||||
uniform mat4 u_modelViewProjMatrix;
|
||||
uniform mat4 u_modelMatrix;
|
||||
|
||||
@@ -18,9 +45,24 @@ uniform mat4 u_directionalLightViewProjMatrix;
|
||||
void main() {
|
||||
gl_Position = u_modelViewProjMatrix * vec4(a_position, 1.0f);
|
||||
|
||||
vec3 T = normalize(vec3(u_modelMatrix * vec4(a_tangent, 0.0f)));
|
||||
vec3 B = normalize(vec3(u_modelMatrix * vec4(a_bitangent, 0.0f)));
|
||||
vec3 N = normalize(vec3(u_modelMatrix * vec4(a_normal, 0.0f)));
|
||||
mat3 TBN_transposed = transpose(mat3(T, B, N));
|
||||
|
||||
v_lightDirectionTangent = TBN_transposed * u_directionalLight.direction;
|
||||
v_lightPositionTangent0 = TBN_transposed * u_pointLight[0].position;
|
||||
//v_lightPositionTangent1 = vec3(0.0f);
|
||||
//v_lightPositionTangent2 = vec3(0.0f);
|
||||
//v_lightPositionTangent3 = vec3(0.0f);
|
||||
|
||||
v_fragmentPositionTangent = TBN_transposed * vec3(u_modelMatrix * vec4(a_position, 1.0f));
|
||||
|
||||
v_fragmentPosition = vec3(u_modelMatrix * vec4(a_position, 1.0f));
|
||||
v_fragmentPositionDirectionalLightSpace = u_directionalLightViewProjMatrix * vec4(v_fragmentPosition, 1.0);
|
||||
|
||||
v_fragmentPositionDirectionalLightSpace = u_directionalLightViewProjMatrix * vec4(v_fragmentPosition, 1.0f);
|
||||
|
||||
v_viewPositionTangent = TBN_transposed * u_viewPosition;
|
||||
|
||||
v_normal = a_normal;
|
||||
v_texCoord = a_texCoord;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#version 330 core
|
||||
|
||||
layout(location = 0) in vec3 a_position;
|
||||
layout(location = 2) in vec2 a_texCoord;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
|
||||
out vec2 v_texCoord;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Size=894,195
|
||||
Collapsed=0
|
||||
|
||||
[Window][Debug Utils]
|
||||
Pos=14,9
|
||||
Pos=58,19
|
||||
Size=791,379
|
||||
Collapsed=0
|
||||
|
||||
|
||||
@@ -266,15 +266,12 @@ void Controller::renderImGui(World *world, PointLight *pointLight, glm::vec3 *li
|
||||
|
||||
// color picker
|
||||
ImGui::Text("\nLight Source");
|
||||
static float K_q = 1.0f;
|
||||
ImGui::SliderFloat("Attenuation Parameter", &K_q, 0, 1.5f);
|
||||
|
||||
updateExposure(postProcessingProgram);
|
||||
pointLight->setParameters(K_q);
|
||||
|
||||
static float color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
ImGui::SliderFloat("Intensity", intensity, 0, 50.f);
|
||||
ImGui::SliderFloat("Intensity", intensity, 0, 250.f);
|
||||
|
||||
ImGui::ColorEdit3("Color", color);
|
||||
lightColor->x = color[0];
|
||||
|
||||
@@ -169,7 +169,7 @@ std::vector<Light*> JsonParser::getLights(ShaderProgram* shaderProgram)
|
||||
const Json::Value pointLightsJson = root["pointLights"];
|
||||
|
||||
int index = 0;
|
||||
for (; index < pointLightsJson.size(); index++) {
|
||||
for (; index < (int)pointLightsJson.size(); index++) {
|
||||
PointLight *current_pointLight;
|
||||
|
||||
const Json::Value positionJson = pointLightsJson[index]["position"];
|
||||
|
||||
@@ -12,9 +12,6 @@ Light::Light(glm::vec3 color, float intensity, ShaderProgram* shaderProgram) :
|
||||
{
|
||||
id = id_counter++;
|
||||
lightColor = color * intensity;
|
||||
diffuseColor = lightColor * glm::vec3(1.0f);
|
||||
ambientColor = diffuseColor * glm::vec3(0.002f);
|
||||
specularColor = lightColor * glm::vec3(1.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +29,6 @@ void Light::setShaderProgram(ShaderProgram* shaderProgram)
|
||||
void Light::setColor(glm::vec3 color)
|
||||
{
|
||||
lightColor = color * intensity;
|
||||
diffuseColor = lightColor * glm::vec3(1.0f);
|
||||
ambientColor = diffuseColor * glm::vec3(0.002f);
|
||||
specularColor = lightColor * glm::vec3(1.0f);
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -65,10 +59,7 @@ void PointLight::update()
|
||||
|
||||
shaderProgram->setUniform((getStructMemberName() + "isActive").c_str(), isActive);
|
||||
shaderProgram->setUniform((getStructMemberName() + "position").c_str(), position);
|
||||
shaderProgram->setUniform((getStructMemberName() + "ambient").c_str(), ambientColor);
|
||||
shaderProgram->setUniform((getStructMemberName() + "diffuse").c_str(), diffuseColor);
|
||||
shaderProgram->setUniform((getStructMemberName() + "specular").c_str(), specularColor);
|
||||
shaderProgram->setUniform((getStructMemberName() + "K_q").c_str(), K_q);
|
||||
shaderProgram->setUniform((getStructMemberName() + "color").c_str(), lightColor);
|
||||
|
||||
shaderProgram->unbind();
|
||||
}
|
||||
@@ -91,11 +82,6 @@ void PointLight::setPosition(glm::vec3 position)
|
||||
update();
|
||||
}
|
||||
|
||||
void PointLight::setParameters(float K_q)
|
||||
{
|
||||
this->K_q = K_q;
|
||||
}
|
||||
|
||||
// DirectionalLight
|
||||
|
||||
DirectionalLight::DirectionalLight(glm::vec3 direction, glm::vec3 color, float intensity, ShaderProgram *shaderProgram) :
|
||||
@@ -111,9 +97,7 @@ void DirectionalLight::update()
|
||||
|
||||
shaderProgram->setUniform("u_directionalLight.isActive", isActive);
|
||||
shaderProgram->setUniform("u_directionalLight.direction", direction);
|
||||
shaderProgram->setUniform("u_directionalLight.ambient", ambientColor);
|
||||
shaderProgram->setUniform("u_directionalLight.diffuse", diffuseColor);
|
||||
shaderProgram->setUniform("u_directionalLight.specular", specularColor);
|
||||
shaderProgram->setUniform("u_directionalLight.color", lightColor);
|
||||
|
||||
shaderProgram->unbind();
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ protected:
|
||||
|
||||
// Color
|
||||
glm::vec3 lightColor;
|
||||
glm::vec3 diffuseColor;
|
||||
glm::vec3 ambientColor;
|
||||
glm::vec3 specularColor;
|
||||
};
|
||||
|
||||
class PointLight : public Light
|
||||
@@ -48,7 +45,6 @@ public:
|
||||
~PointLight() = default;
|
||||
|
||||
void setPosition(glm::vec3 position);
|
||||
void setParameters(float K_q);
|
||||
|
||||
glm::vec3 getPosition();
|
||||
|
||||
@@ -58,8 +54,6 @@ private:
|
||||
|
||||
private:
|
||||
glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f);
|
||||
|
||||
float K_q = 0.032f;
|
||||
};
|
||||
|
||||
class DirectionalLight : public Light
|
||||
|
||||
@@ -77,6 +77,18 @@ void Model::loadModel(std::string &pathToModel)
|
||||
loadedTextures.push_back(newTex);
|
||||
}
|
||||
|
||||
// When there is no normal map bound, please use fallback texture
|
||||
bool hasNormalMap = false;
|
||||
for (auto it = textureTypes.begin(); it != textureTypes.end(); it++) {
|
||||
if (*it == textureType::texture_normal)
|
||||
hasNormalMap = true;
|
||||
}
|
||||
|
||||
if (!hasNormalMap) {
|
||||
Texture *newTex = new Texture("data/res/models/tex/fallback_normal.png", textureType::texture_normal);
|
||||
loadedTextures.push_back(newTex);
|
||||
}
|
||||
|
||||
// Here starts the first mesh
|
||||
uint32_t numMeshes;
|
||||
input.read((char *) &numMeshes, sizeof(uint32_t));
|
||||
@@ -109,6 +121,11 @@ void Model::loadModel(std::string &pathToModel)
|
||||
meshTextures.push_back(loadedTextures[currentTextureId]);
|
||||
}
|
||||
|
||||
if (!hasNormalMap) {
|
||||
// This will be the last texture
|
||||
meshTextures.push_back(loadedTextures[numTextures]);
|
||||
}
|
||||
|
||||
Mesh *currentMesh = new Mesh(meshVertices, meshIndices, meshTextures);
|
||||
meshes.push_back(currentMesh);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,21 @@ VertexArray::VertexArray(void *vertexData, void *indexData, uint32_t numVertices
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, position));
|
||||
|
||||
// Normal vectors
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, normalVec));
|
||||
|
||||
// UV Texture Mapping
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, textureCoords));
|
||||
|
||||
// Normal vectors
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, textureCoords));
|
||||
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, normalVec));
|
||||
|
||||
// Tangent vectors
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, tangentVec));
|
||||
|
||||
// Bitangent vectors
|
||||
glEnableVertexAttribArray(4);
|
||||
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *) offsetof(struct Vertex, bitangentVec));
|
||||
|
||||
// This will also unbind the vertex buffer and index buffer
|
||||
glBindVertexArray(0);
|
||||
|
||||
@@ -159,6 +159,18 @@ Mesh processMesh(aiMesh *mesh, const aiScene *scene, Model *model) {
|
||||
vector.y = mesh->mNormals[i].y;
|
||||
vector.z = mesh->mNormals[i].z;
|
||||
vertex.normalVec = vector;
|
||||
|
||||
// Tangents
|
||||
vector.x = mesh->mTangents[i].x;
|
||||
vector.y = mesh->mTangents[i].y;
|
||||
vector.z = mesh->mTangents[i].z;
|
||||
vertex.tangentVec = vector;
|
||||
|
||||
// Bitangents
|
||||
vector.x = mesh->mBitangents[i].x;
|
||||
vector.y = mesh->mBitangents[i].y;
|
||||
vector.z = mesh->mBitangents[i].z;
|
||||
vertex.bitangentVec = vector;
|
||||
|
||||
// Texture UV mapping
|
||||
if(mesh->mTextureCoords[0]) {
|
||||
@@ -191,9 +203,10 @@ Mesh processMesh(aiMesh *mesh, const aiScene *scene, Model *model) {
|
||||
std::vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, texture_specular, ¤tMesh, model);
|
||||
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
|
||||
|
||||
std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_NORMALS, texture_normal, ¤tMesh, model);
|
||||
std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, texture_normal, ¤tMesh, model);
|
||||
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
|
||||
|
||||
// Not entirely sure if aiTextureType_HEIGHT is correct
|
||||
std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, texture_height, ¤tMesh, model);
|
||||
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user