Gjk, FPS view.
This commit is contained in:
@@ -34,6 +34,7 @@ add_executable(vk_expe
|
|||||||
src/Vulkan/Swapchain.cpp
|
src/Vulkan/Swapchain.cpp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/utils.cpp
|
src/utils.cpp
|
||||||
|
src/Simplex.cpp
|
||||||
src/Logger.cpp
|
src/Logger.cpp
|
||||||
src/Planet.cpp
|
src/Planet.cpp
|
||||||
src/VkExpe.cpp
|
src/VkExpe.cpp
|
||||||
|
|||||||
141
src/Simplex.cpp
Normal file
141
src/Simplex.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
#include <Simplex.h>
|
||||||
|
|
||||||
|
|
||||||
|
Vector3 polyhedron_support(Vector3AV polyhedron, const Vector3& direction) {
|
||||||
|
assert(polyhedron.size() > 0);
|
||||||
|
|
||||||
|
Index best = 0;
|
||||||
|
Real best_dot = polyhedron[0].dot(direction);
|
||||||
|
|
||||||
|
for (Index index = 1; index < polyhedron.size(); index += 1) {
|
||||||
|
Real dot = polyhedron[index].dot(direction);
|
||||||
|
if (dot > best_dot) {
|
||||||
|
best = index;
|
||||||
|
best_dot = dot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polyhedron[best];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Simplex::Simplex() = default;
|
||||||
|
Simplex::Simplex(const Vector3& point)
|
||||||
|
: m_points{ point, Vector3{}, Vector3{}, Vector3{} }
|
||||||
|
, m_point_count(1)
|
||||||
|
{}
|
||||||
|
|
||||||
|
|
||||||
|
Gjk::Gjk(Vector3AV polyhedron0, Vector3AV polyhedron1, const Vector3& direction)
|
||||||
|
: m_polyhedron0(polyhedron0)
|
||||||
|
, m_polyhedron1(polyhedron1)
|
||||||
|
, m_direction(direction)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Gjk::~Gjk() = default;
|
||||||
|
|
||||||
|
const Vector3& Gjk::direction() const {
|
||||||
|
return m_direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::operator()() {
|
||||||
|
while (true) {
|
||||||
|
m_simplex.push(support(m_direction));
|
||||||
|
|
||||||
|
if (m_simplex.last().dot(m_direction) < Real(0))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (nextSimplex())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::nextSimplex() {
|
||||||
|
switch(m_simplex.point_count()) {
|
||||||
|
case 1: return nextFromPoint();
|
||||||
|
case 2: return nextFromLine();
|
||||||
|
case 3: return nextFromTriangle();
|
||||||
|
case 4: return nextFromTetrahedron();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::nextFromPoint() {
|
||||||
|
m_direction = -m_simplex.last();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::nextFromLine() {
|
||||||
|
Vector3 v01 = m_simplex.point(1) - m_simplex.point(0);
|
||||||
|
Vector3 v1o = -m_simplex.point(1);
|
||||||
|
|
||||||
|
m_direction = v01.cross(v1o).cross(v01);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::nextFromTriangle() {
|
||||||
|
Vector3 v20 = m_simplex.point(0) - m_simplex.point(2);
|
||||||
|
Vector3 v21 = m_simplex.point(1) - m_simplex.point(2);
|
||||||
|
Vector3 v2o = -m_simplex.point(2);
|
||||||
|
|
||||||
|
Vector3 n = v20.cross(v21);
|
||||||
|
Vector3 n0 = n.cross(v21);
|
||||||
|
Vector3 n1 = v20.cross(n);
|
||||||
|
|
||||||
|
if(n0.dot(v2o) > Real(0)) {
|
||||||
|
m_simplex.pop(0);
|
||||||
|
m_direction = n0;
|
||||||
|
}
|
||||||
|
else if(n1.dot(v2o) > Real(0)) {
|
||||||
|
m_simplex.pop(1);
|
||||||
|
m_direction = n1;
|
||||||
|
}
|
||||||
|
else if(n.dot(v2o) > Real(0)) {
|
||||||
|
m_direction = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_simplex.swap(0, 1);
|
||||||
|
m_direction = -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gjk::nextFromTetrahedron() {
|
||||||
|
Vector3 v30 = m_simplex.point(0) - m_simplex.point(3);
|
||||||
|
Vector3 v31 = m_simplex.point(1) - m_simplex.point(3);
|
||||||
|
Vector3 v32 = m_simplex.point(2) - m_simplex.point(3);
|
||||||
|
Vector3 v3o = -m_simplex.point(3);
|
||||||
|
|
||||||
|
Vector3 n[3] = {
|
||||||
|
v31.cross(v32),
|
||||||
|
v32.cross(v30),
|
||||||
|
v30.cross(v31),
|
||||||
|
};
|
||||||
|
|
||||||
|
Real d[3] = {
|
||||||
|
n[0].dot(v3o),
|
||||||
|
n[1].dot(v3o),
|
||||||
|
n[2].dot(v3o),
|
||||||
|
};
|
||||||
|
|
||||||
|
Index best = (d[0] > d[1])? ((d[0] > d[2])? 0: 2):
|
||||||
|
((d[1] > d[2])? 1: 2);
|
||||||
|
|
||||||
|
if(d[best] <= Real(0))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
m_simplex.pop(best);
|
||||||
|
m_direction = n[best];
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 Gjk::support(const Vector3& direction) const
|
||||||
|
{
|
||||||
|
return polyhedron_support(m_polyhedron0, direction)
|
||||||
|
- polyhedron_support(m_polyhedron1, -direction);
|
||||||
|
}
|
||||||
75
src/Simplex.h
Normal file
75
src/Simplex.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <core.h>
|
||||||
|
|
||||||
|
|
||||||
|
class Simplex {
|
||||||
|
public:
|
||||||
|
Simplex();
|
||||||
|
Simplex(const Vector3& point);
|
||||||
|
|
||||||
|
inline Index point_count() const {
|
||||||
|
return m_point_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Vector3& point(Index index) const {
|
||||||
|
assert(index < m_point_count);
|
||||||
|
return m_points[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Vector3& last() const {
|
||||||
|
assert(m_point_count > 0);
|
||||||
|
return point(m_point_count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void push(const Vector3& point) {
|
||||||
|
assert(m_point_count < MaxPointCount);
|
||||||
|
m_points[m_point_count] = point;
|
||||||
|
m_point_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pop(Index index) {
|
||||||
|
assert(index < m_point_count);
|
||||||
|
for(Index i = index; i + 1 < m_point_count; i += 1)
|
||||||
|
m_points[i] = m_points[i + 1];
|
||||||
|
m_point_count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void swap(Index index0, Index index1) {
|
||||||
|
using std::swap;
|
||||||
|
swap(m_points[index0], m_points[index1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr Index MaxPointCount = 4;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector3 m_points[MaxPointCount];
|
||||||
|
Index m_point_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class Gjk {
|
||||||
|
public:
|
||||||
|
Gjk(Vector3AV polyhedron0, Vector3AV polyhedron1, const Vector3& direction=Vector3::UnitX());
|
||||||
|
~Gjk();
|
||||||
|
|
||||||
|
const Vector3& direction() const;
|
||||||
|
|
||||||
|
bool operator()();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool nextSimplex();
|
||||||
|
bool nextFromPoint();
|
||||||
|
bool nextFromLine();
|
||||||
|
bool nextFromTriangle();
|
||||||
|
bool nextFromTetrahedron();
|
||||||
|
|
||||||
|
Vector3 support(const Vector3& direction) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector3AV m_polyhedron0;
|
||||||
|
Vector3AV m_polyhedron1;
|
||||||
|
Simplex m_simplex;
|
||||||
|
Vector3 m_direction;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
|
||||||
void SdlWindowDeleter::operator()(SDL_Window* window) const {
|
void SdlWindowDeleter::operator()(SDL_Window* window) const {
|
||||||
@@ -25,7 +26,7 @@ void VkExpe::initialize() {
|
|||||||
auto const window = SDL_CreateWindow(
|
auto const window = SDL_CreateWindow(
|
||||||
"vk_expe",
|
"vk_expe",
|
||||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
800, 600,
|
1920, 1080,
|
||||||
SDL_WINDOW_VULKAN
|
SDL_WINDOW_VULKAN
|
||||||
| SDL_WINDOW_ALLOW_HIGHDPI
|
| SDL_WINDOW_ALLOW_HIGHDPI
|
||||||
| SDL_WINDOW_SHOWN
|
| SDL_WINDOW_SHOWN
|
||||||
@@ -60,6 +61,8 @@ void VkExpe::run() {
|
|||||||
m_running = false;
|
m_running = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
auto last_time = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while(m_running) {
|
while(m_running) {
|
||||||
while(SDL_PollEvent(&event)) {
|
while(SDL_PollEvent(&event)) {
|
||||||
@@ -72,17 +75,88 @@ void VkExpe::run() {
|
|||||||
m_running = false;
|
m_running = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case SDL_MOUSEMOTION: {
|
||||||
|
m_mouse_offset += Vector2(
|
||||||
|
event.motion.xrel,
|
||||||
|
event.motion.yrel
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case SDL_WINDOWEVENT: {
|
case SDL_WINDOWEVENT: {
|
||||||
switch(event.window.event) {
|
switch(event.window.event) {
|
||||||
case SDL_WINDOWEVENT_RESIZED: {
|
case SDL_WINDOWEVENT_RESIZED: {
|
||||||
m_vulkan.invalidate_swapchain();
|
m_vulkan.invalidate_swapchain();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_WINDOWEVENT_FOCUS_GAINED: {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_WINDOWEVENT_FOCUS_LOST: {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto new_time = std::chrono::high_resolution_clock::now();
|
||||||
|
const auto elapsed = new_time - last_time;
|
||||||
|
last_time = new_time;
|
||||||
|
|
||||||
|
update(std::chrono::duration<double>(elapsed).count());
|
||||||
|
|
||||||
|
m_vulkan.set_camera(m_camera_position, m_camera_z, m_camera_y);
|
||||||
m_vulkan.draw_frame();
|
m_vulkan.draw_frame();
|
||||||
// m_running = false;
|
|
||||||
|
m_mouse_offset = Vector2::Zero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkExpe::update(double elapsed) {
|
||||||
|
int key_count = 0;
|
||||||
|
const auto keys = SDL_GetKeyboardState(&key_count);
|
||||||
|
|
||||||
|
const auto test_key = [key_count, keys](int scan_code) {
|
||||||
|
return scan_code < key_count && keys[scan_code];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!m_mouse_offset.isZero()) {
|
||||||
|
const Real x_sensi = 0.001;
|
||||||
|
const Real y_sensi = -0.001;
|
||||||
|
|
||||||
|
const Vector3 camera_x = m_camera_y.cross(m_camera_z);
|
||||||
|
Vector3 axis =
|
||||||
|
x_sensi * m_mouse_offset[0] * m_camera_y +
|
||||||
|
y_sensi * m_mouse_offset[1] * camera_x;
|
||||||
|
Real rot_norm = axis.norm();
|
||||||
|
AngleAxis rot(rot_norm, axis / rot_norm);
|
||||||
|
m_camera_y = rot * m_camera_y;
|
||||||
|
m_camera_z = rot * m_camera_z;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 walk_direction = Vector3::Zero();
|
||||||
|
if(test_key(SDL_SCANCODE_W))
|
||||||
|
walk_direction += Vector3::UnitZ();
|
||||||
|
if(test_key(SDL_SCANCODE_S))
|
||||||
|
walk_direction -= Vector3::UnitZ();
|
||||||
|
if(test_key(SDL_SCANCODE_A))
|
||||||
|
walk_direction -= Vector3::UnitX();
|
||||||
|
if(test_key(SDL_SCANCODE_D))
|
||||||
|
walk_direction += Vector3::UnitX();
|
||||||
|
if(test_key(SDL_SCANCODE_SPACE))
|
||||||
|
walk_direction -= Vector3::UnitY();
|
||||||
|
if(test_key(SDL_SCANCODE_LCTRL))
|
||||||
|
walk_direction += Vector3::UnitY();
|
||||||
|
|
||||||
|
if(!walk_direction.isZero()) {
|
||||||
|
walk_direction.normalize();
|
||||||
|
const Real base_velocity = 1;
|
||||||
|
|
||||||
|
Matrix3 camera_basis;
|
||||||
|
camera_basis << m_camera_y.cross(m_camera_z), m_camera_y, m_camera_z;
|
||||||
|
m_camera_position += elapsed * base_velocity * (camera_basis * walk_direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/VkExpe.h
10
src/VkExpe.h
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <core.h>
|
||||||
#include <VulkanTutorial.h>
|
#include <VulkanTutorial.h>
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
@@ -26,8 +27,17 @@ public:
|
|||||||
|
|
||||||
void run();
|
void run();
|
||||||
|
|
||||||
|
void update(double elapsed);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VulkanTutorial m_vulkan;
|
VulkanTutorial m_vulkan;
|
||||||
|
|
||||||
|
Vector3 m_camera_position = Vector3(0.0f, 0.0f, -3.0f);
|
||||||
|
Vector3 m_camera_z = Vector3(0.0f, 0.0f, 1.0f);
|
||||||
|
Vector3 m_camera_y = Vector3(0.0f, 1.0f, 0.0f);
|
||||||
|
|
||||||
WindowUP m_window;
|
WindowUP m_window;
|
||||||
bool m_running = false;
|
bool m_running = false;
|
||||||
|
|
||||||
|
Vector2 m_mouse_offset = Vector2::Zero();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ void VulkanTutorial::shutdown() {
|
|||||||
m_context.shutdown();
|
m_context.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VulkanTutorial::set_camera(const Vector3& camera_position, const Vector3& camera_z, const Vector3& camera_y) {
|
||||||
|
m_camera_position = camera_position;
|
||||||
|
m_camera_z = camera_z;
|
||||||
|
m_camera_y = camera_y;
|
||||||
|
}
|
||||||
|
|
||||||
void VulkanTutorial::draw_frame() {
|
void VulkanTutorial::draw_frame() {
|
||||||
m_swapchain.begin_frame();
|
m_swapchain.begin_frame();
|
||||||
auto const image_index = m_swapchain.current_image_index();
|
auto const image_index = m_swapchain.current_image_index();
|
||||||
@@ -206,15 +212,14 @@ void VulkanTutorial::draw_frame() {
|
|||||||
}
|
}
|
||||||
m_last_frame_time = now;
|
m_last_frame_time = now;
|
||||||
|
|
||||||
const float alpha = SecondsD(m_time).count() * (2.0 * M_PI) / 10.0;
|
Matrix3 linear_view;
|
||||||
const float dist = 2.0f;
|
linear_view <<
|
||||||
const Eigen::Matrix4f view = look_at_matrix(
|
m_camera_y.cross(m_camera_z).transpose(),
|
||||||
// Eigen::Vector3f(0.0f, 0.0f, -dist),
|
m_camera_y.transpose(),
|
||||||
Eigen::Vector3f(0.0f, -dist, -dist),
|
m_camera_z.transpose();
|
||||||
// dist * Eigen::Vector3f(std::cos(alpha), std::sin(alpha), -1.0),
|
Matrix4 view;
|
||||||
Eigen::Vector3f::Zero(),
|
view << linear_view, linear_view * -m_camera_position,
|
||||||
-Eigen::Vector3f::UnitY()
|
Vector4::UnitW().transpose();
|
||||||
);
|
|
||||||
|
|
||||||
const float fov = M_PI / 3.0f;
|
const float fov = M_PI / 3.0f;
|
||||||
const float near = 0.1f;
|
const float near = 0.1f;
|
||||||
@@ -227,12 +232,7 @@ void VulkanTutorial::draw_frame() {
|
|||||||
near, far
|
near, far
|
||||||
);
|
);
|
||||||
|
|
||||||
using Transform = Eigen::Transform<float, 3, Eigen::Affine>;
|
|
||||||
Transform model = Transform::Identity();
|
Transform model = Transform::Identity();
|
||||||
model.rotate(Eigen::AngleAxisf(
|
|
||||||
alpha,
|
|
||||||
Eigen::Vector3f::UnitY()
|
|
||||||
));
|
|
||||||
|
|
||||||
const Uniforms uniforms = {
|
const Uniforms uniforms = {
|
||||||
.scene_from_model = model.matrix(),
|
.scene_from_model = model.matrix(),
|
||||||
@@ -241,7 +241,7 @@ void VulkanTutorial::draw_frame() {
|
|||||||
0.5 * m_swapchain.extent().width,
|
0.5 * m_swapchain.extent().width,
|
||||||
0.5 * m_swapchain.extent().height,
|
0.5 * m_swapchain.extent().height,
|
||||||
},
|
},
|
||||||
.lod = std::cos(alpha) * 0.5f + 0.5f,
|
// .lod = std::cos(alpha) * 0.5f + 0.5f,
|
||||||
};
|
};
|
||||||
|
|
||||||
void* uniform_buffer;
|
void* uniform_buffer;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <core.h>
|
||||||
|
|
||||||
#include <Vulkan/Context.h>
|
#include <Vulkan/Context.h>
|
||||||
#include <Vulkan/Swapchain.h>
|
#include <Vulkan/Swapchain.h>
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ public:
|
|||||||
void initialize(SDL_Window* window);
|
void initialize(SDL_Window* window);
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
|
void set_camera(const Vector3& camera_position, const Vector3& camera_z, const Vector3& camera_y);
|
||||||
|
|
||||||
void draw_frame();
|
void draw_frame();
|
||||||
void invalidate_swapchain();
|
void invalidate_swapchain();
|
||||||
|
|
||||||
@@ -78,6 +82,10 @@ private:
|
|||||||
std::vector<VkCommandBuffer> m_command_buffers;
|
std::vector<VkCommandBuffer> m_command_buffers;
|
||||||
std::vector<VkSemaphore> m_render_done;
|
std::vector<VkSemaphore> m_render_done;
|
||||||
|
|
||||||
|
Vector3 m_camera_position = Vector3(0.0f, 0.0f, -3.0f);
|
||||||
|
Vector3 m_camera_z = Vector3(0.0f, 0.0f, 1.0f);
|
||||||
|
Vector3 m_camera_y = Vector3(0.0f, 1.0f, 0.0f);
|
||||||
|
|
||||||
TimePoint m_last_frame_time;
|
TimePoint m_last_frame_time;
|
||||||
Duration m_time = Duration(0);
|
Duration m_time = Duration(0);
|
||||||
};
|
};
|
||||||
|
|||||||
13
src/core.h
13
src/core.h
@@ -8,8 +8,21 @@
|
|||||||
|
|
||||||
using Byte = unsigned char;
|
using Byte = unsigned char;
|
||||||
using Index = uint32_t;
|
using Index = uint32_t;
|
||||||
|
|
||||||
using Real = float;
|
using Real = float;
|
||||||
|
|
||||||
|
using Vector2 = Eigen::Matrix<Real, 2, 1>;
|
||||||
using Vector3 = Eigen::Matrix<Real, 3, 1>;
|
using Vector3 = Eigen::Matrix<Real, 3, 1>;
|
||||||
|
using Vector4 = Eigen::Matrix<Real, 4, 1>;
|
||||||
|
|
||||||
|
using Matrix2 = Eigen::Matrix<Real, 2, 2>;
|
||||||
|
using Matrix3 = Eigen::Matrix<Real, 3, 3>;
|
||||||
|
using Matrix4 = Eigen::Matrix<Real, 4, 4>;
|
||||||
|
|
||||||
|
using Transform = Eigen::Transform<Real, 3, Eigen::Affine>;
|
||||||
|
using AngleAxis = Eigen::AngleAxis<Real>;
|
||||||
|
using Quaternion = Eigen::Quaternion<Real>;
|
||||||
|
|
||||||
using Triangle = Eigen::Array<Index, 3, 1>;
|
using Triangle = Eigen::Array<Index, 3, 1>;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user