mirror of
https://github.com/bailwillharr/engine.git
synced 2024-09-21 04:51:18 +00:00
Add better model loading, add test models
This commit is contained in:
parent
6c50c37825
commit
d91def268a
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,5 @@ MinSizeRel/
|
||||
CMakeSettings.json
|
||||
compile_commands.json
|
||||
runme
|
||||
res
|
||||
|
||||
*.log
|
||||
|
@ -30,6 +30,8 @@ set(SRC_FILES
|
||||
"src/resources/texture.cpp"
|
||||
"src/resources/font.cpp"
|
||||
|
||||
"src/util/model_loader.cpp"
|
||||
|
||||
"src/resource_manager.cpp"
|
||||
|
||||
"src/gfx_device_vulkan.cpp"
|
||||
@ -70,6 +72,8 @@ set(INCLUDE_FILES
|
||||
"include/resources/texture.hpp"
|
||||
"include/resources/font.hpp"
|
||||
|
||||
"include/util/model_loader.hpp"
|
||||
|
||||
"include/resource_manager.hpp"
|
||||
|
||||
"include/gfx.hpp"
|
||||
@ -214,6 +218,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE dependencies/stb)
|
||||
# assimp
|
||||
set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "" FORCE)
|
||||
set(ASSIMP_BUILD_TESTS OFF CACHE INTERNAL "" FORCE)
|
||||
set(ASSIMP_NO_EXPORT OFF CACHE INTERNAL "" FORCE)
|
||||
set(ASSIMP_INSTALL OFF CACHE INTERNAL "" FORCE)
|
||||
add_subdirectory(dependencies/assimp)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE dependencies/assimp/include)
|
||||
|
@ -29,9 +29,6 @@ public:
|
||||
std::shared_ptr<resources::Mesh> m_mesh = nullptr;
|
||||
std::shared_ptr<resources::Texture> m_texture;
|
||||
|
||||
glm::vec3 m_color = { 1.0f, 1.0f, 1.0f };
|
||||
glm::vec3 m_emission = { 0.0f, 0.0f, 0.0f };
|
||||
|
||||
private:
|
||||
|
||||
std::shared_ptr<resources::Shader> m_shader;
|
||||
|
@ -23,7 +23,6 @@ public:
|
||||
|
||||
struct UniformBuffer {
|
||||
glm::mat4 p;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
gfx::Pipeline* getPipeline()
|
||||
|
11
include/util/model_loader.hpp
Normal file
11
include/util/model_loader.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace engine::util {
|
||||
|
||||
Object* loadAssimpMeshFromFile(Object* parent, const std::string& path);
|
||||
|
||||
}
|
@ -17,7 +17,7 @@ namespace engine::components {
|
||||
Renderer::Renderer(Object* parent) : Component(parent, TypeEnum::RENDERER)
|
||||
{
|
||||
m_shader = this->parent.res.get<resources::Shader>("shader.glsl");
|
||||
m_texture = this->parent.res.get<resources::Texture>("textures/missing.png");
|
||||
m_texture = this->parent.res.get<resources::Texture>("textures/white.png");
|
||||
}
|
||||
|
||||
Renderer::~Renderer()
|
||||
@ -28,8 +28,6 @@ Renderer::~Renderer()
|
||||
void Renderer::render(glm::mat4 transform, glm::mat4 view)
|
||||
{
|
||||
resources::Shader::UniformBuffer uniformData{};
|
||||
uniformData.color = glm::vec4{ m_color.r, m_color.g, m_color.b, 1.0 };
|
||||
gfxdev->updateUniformBuffer(m_shader->getPipeline(), &uniformData.color, sizeof(uniformData.color), offsetof(resources::Shader::UniformBuffer, color));
|
||||
|
||||
glm::mat4 pushConsts[] = { transform, view };
|
||||
gfxdev->draw(m_shader->getPipeline(), m_mesh->vb, m_mesh->ib, m_mesh->m_indices.size(), pushConsts, sizeof(glm::mat4) * 2, m_texture->getHandle());
|
||||
|
@ -12,12 +12,9 @@ struct MeshFileHeader {
|
||||
int32_t material;
|
||||
};
|
||||
|
||||
static void loadMeshFromFile(const std::filesystem::path& path, std::vector<Vertex>* vertices, std::vector<uint32_t>* indices)
|
||||
static void loadCustomMeshFromFile(const std::filesystem::path& path, std::vector<Vertex>* vertices, std::vector<uint32_t>* indices)
|
||||
{
|
||||
|
||||
// TODO
|
||||
// Replace this aberation with something that's readable and doesn't use FILE*
|
||||
|
||||
struct MeshFileHeader header{};
|
||||
|
||||
FILE* fp = fopen(path.string().c_str(), "rb");
|
||||
@ -62,7 +59,13 @@ Mesh::Mesh(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& ind
|
||||
// To be used with the resource manager
|
||||
Mesh::Mesh(const std::filesystem::path& resPath) : Resource(resPath, "mesh")
|
||||
{
|
||||
loadMeshFromFile(resPath, &m_vertices, &m_indices);
|
||||
if (resPath.extension() == ".mesh") {
|
||||
loadCustomMeshFromFile(resPath, &m_vertices, &m_indices);
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("Mesh load error, unknown file type");
|
||||
}
|
||||
|
||||
initMesh();
|
||||
}
|
||||
|
||||
|
213
src/util/model_loader.cpp
Normal file
213
src/util/model_loader.cpp
Normal file
@ -0,0 +1,213 @@
|
||||
#include "util/model_loader.hpp"
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
#include "resources/texture.hpp"
|
||||
#include "resources/mesh.hpp"
|
||||
|
||||
#include "components/mesh_renderer.hpp"
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/LogStream.hpp>
|
||||
#include <assimp/Logger.hpp>
|
||||
#include <assimp/DefaultLogger.hpp>
|
||||
#include <assimp/postprocess.h>
|
||||
#include <assimp/mesh.h>
|
||||
#include <assimp/scene.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace engine::util {
|
||||
|
||||
static void buildGraph(
|
||||
const std::map<int, std::shared_ptr<resources::Texture>>& textures,
|
||||
const std::vector<std::shared_ptr<resources::Mesh>>& meshes,
|
||||
const std::vector<unsigned int>& meshTextureIndices,
|
||||
aiNode* parentNode, Object* parentObj)
|
||||
{
|
||||
|
||||
// convert to glm column major
|
||||
glm::mat4 transform;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
transform[i][j] = parentNode->mTransformation[j][i];
|
||||
}
|
||||
}
|
||||
|
||||
// get position
|
||||
glm::vec3 position{ transform[3][0], transform[3][1], transform[3][2] };
|
||||
|
||||
// remove position from matrix
|
||||
transform[3][0] = 0.0f;
|
||||
transform[3][1] = 0.0f;
|
||||
transform[3][2] = 0.0f;
|
||||
|
||||
// get scale
|
||||
glm::vec3 scale{};
|
||||
scale.x = sqrt(transform[0][0] * transform[0][0] + transform[0][1] * transform[0][1] + transform[0][2] * transform[0][2]);
|
||||
scale.y = sqrt(transform[1][0] * transform[1][0] + transform[1][1] * transform[1][1] + transform[1][2] * transform[1][2]);
|
||||
scale.z = sqrt(transform[2][0] * transform[2][0] + transform[2][1] * transform[2][1] + transform[2][2] * transform[2][2]);
|
||||
|
||||
// remove scaling from matrix
|
||||
for (int row = 0; row < 3; row++) {
|
||||
transform[0][row] /= scale.x;
|
||||
transform[1][row] /= scale.y;
|
||||
transform[2][row] /= scale.z;
|
||||
}
|
||||
|
||||
// get rotation
|
||||
glm::quat rotation = glm::quat_cast(transform);
|
||||
|
||||
// update position, scale, rotation
|
||||
parentObj->transform.position = position;
|
||||
parentObj->transform.scale = scale;
|
||||
parentObj->transform.rotation = rotation;
|
||||
|
||||
for (int i = 0; i < parentNode->mNumMeshes; i++) {
|
||||
// create child node for each mesh
|
||||
auto child = parentObj->createChild("_mesh" + std::to_string(i));
|
||||
auto childRenderer = child->createComponent<components::Renderer>();
|
||||
childRenderer->m_mesh = meshes[parentNode->mMeshes[i]];
|
||||
if (textures.contains(meshTextureIndices[parentNode->mMeshes[i]])) {
|
||||
childRenderer->m_texture = textures.at(meshTextureIndices[parentNode->mMeshes[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < parentNode->mNumChildren; i++) {
|
||||
buildGraph(textures, meshes, meshTextureIndices, parentNode->mChildren[i], parentObj->createChild("child" + std::to_string(i)));
|
||||
}
|
||||
}
|
||||
|
||||
Object* loadAssimpMeshFromFile(Object* parent, const std::string& path)
|
||||
{
|
||||
Assimp::Importer importer;
|
||||
|
||||
class myStream : public Assimp::LogStream {
|
||||
public:
|
||||
void write(const char* message) override {
|
||||
DEBUG("ASSIMP: {}", message);
|
||||
}
|
||||
};
|
||||
|
||||
const unsigned int severity = Assimp::Logger::Debugging | Assimp::Logger::Info | Assimp::Logger::Err | Assimp::Logger::Warn;
|
||||
Assimp::DefaultLogger::get()->attachStream(new myStream, severity);
|
||||
|
||||
// remove everything but texcoords, normals, meshes, materials
|
||||
importer.SetPropertyInteger(AI_CONFIG_PP_RVC_FLAGS,
|
||||
aiComponent_ANIMATIONS |
|
||||
aiComponent_BONEWEIGHTS |
|
||||
aiComponent_CAMERAS |
|
||||
aiComponent_COLORS |
|
||||
aiComponent_LIGHTS |
|
||||
aiComponent_TANGENTS_AND_BITANGENTS |
|
||||
aiComponent_TEXTURES |
|
||||
0
|
||||
);
|
||||
importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE,
|
||||
aiPrimitiveType_POINT |
|
||||
aiPrimitiveType_LINE |
|
||||
aiPrimitiveType_POLYGON
|
||||
);
|
||||
|
||||
const aiScene* scene = importer.ReadFile(path,
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_SortByPType |
|
||||
aiProcess_RemoveComponent |
|
||||
aiProcess_SplitLargeMeshes | // leave at default maximum
|
||||
aiProcess_ValidateDataStructure | // make sure to log the output
|
||||
aiProcess_ImproveCacheLocality |
|
||||
aiProcess_RemoveRedundantMaterials |
|
||||
aiProcess_FindInvalidData |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_GenUVCoords |
|
||||
aiProcess_TransformUVCoords |
|
||||
aiProcess_FlipUVs | // Maybe?
|
||||
0
|
||||
);
|
||||
|
||||
const char* errString = importer.GetErrorString();
|
||||
if (errString[0] != '\0' || scene == nullptr) {
|
||||
throw std::runtime_error(errString);
|
||||
}
|
||||
|
||||
if (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) {
|
||||
throw std::runtime_error(errString);
|
||||
}
|
||||
|
||||
assert(scene->HasAnimations() == false);
|
||||
assert(scene->HasCameras() == false);
|
||||
assert(scene->HasLights() == false);
|
||||
assert(scene->hasSkeletons() == false);
|
||||
|
||||
INFO("material count: {}, mesh count: {}", scene->mNumMaterials, scene->mNumMeshes);
|
||||
|
||||
std::map<int, std::shared_ptr<resources::Texture>> textures{};
|
||||
|
||||
for (int i = 0; i < scene->mNumMaterials; i++) {
|
||||
const aiMaterial* m = scene->mMaterials[i];
|
||||
INFO("Material {}:", i);
|
||||
INFO(" Name: {}", m->GetName().C_Str());
|
||||
for (int j = 0; j < m->mNumProperties; j++) {
|
||||
const aiMaterialProperty* p = m->mProperties[j];
|
||||
INFO(" prop {}, key: {}", j, p->mKey.C_Str());
|
||||
}
|
||||
|
||||
if (aiGetMaterialTextureCount(m, aiTextureType_DIFFUSE) >= 1) {
|
||||
aiString texPath{};
|
||||
aiGetMaterialTexture(m, aiTextureType_DIFFUSE, 0, &texPath);
|
||||
INFO(" Diffuse tex: {}", texPath.C_Str());
|
||||
std::filesystem::path absPath = path;
|
||||
absPath = absPath.parent_path();
|
||||
absPath /= texPath.C_Str();
|
||||
textures[i] = std::make_shared<resources::Texture>(absPath);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<resources::Mesh>> meshes{};
|
||||
std::vector<unsigned int> meshMaterialIndices{};
|
||||
for (int i = 0; i < scene->mNumMeshes; i++) {
|
||||
const aiMesh* m = scene->mMeshes[i];
|
||||
meshMaterialIndices.push_back(m->mMaterialIndex);
|
||||
std::vector<Vertex> vertices(m->mNumVertices);
|
||||
std::vector<uint32_t> indices(m->mNumFaces * 3);
|
||||
INFO("Mesh {}: vertex count {}", i, vertices.size());
|
||||
INFO("Mesh {}: index count {}", i, indices.size());
|
||||
|
||||
for (int j = 0; j < vertices.size(); j++) {
|
||||
Vertex v{};
|
||||
v.pos.x = m->mVertices[j].x;
|
||||
v.pos.y = m->mVertices[j].y;
|
||||
v.pos.z = m->mVertices[j].z;
|
||||
v.norm.x = m->mNormals[j].x;
|
||||
v.norm.y = m->mNormals[j].y;
|
||||
v.norm.z = m->mNormals[j].z;
|
||||
vertices[j] = v;
|
||||
}
|
||||
if (m->mNumUVComponents[0] >= 2) {
|
||||
for (int j = 0; j < vertices.size(); j++) {
|
||||
vertices[j].uv.x = m->mTextureCoords[0][j].x;
|
||||
vertices[j].uv.y = m->mTextureCoords[0][j].y;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < indices.size() / 3; j++) {
|
||||
indices[j * 3 + 0] = m->mFaces[j].mIndices[0];
|
||||
indices[j * 3 + 1] = m->mFaces[j].mIndices[1];
|
||||
indices[j * 3 + 2] = m->mFaces[j].mIndices[2];
|
||||
}
|
||||
meshes.push_back(std::make_shared<resources::Mesh>(vertices, indices));
|
||||
}
|
||||
|
||||
Object* obj = parent->createChild(scene->GetShortFilename(path.c_str()));
|
||||
|
||||
buildGraph(textures, meshes, meshMaterialIndices, scene->mRootNode, obj);
|
||||
|
||||
Assimp::DefaultLogger::kill();
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
BIN
test/res/meshes/pyramid.mesh
Normal file
BIN
test/res/meshes/pyramid.mesh
Normal file
Binary file not shown.
289
test/res/models/astronaut/astronaut.dae
Normal file
289
test/res/models/astronaut/astronaut.dae
Normal file
File diff suppressed because one or more lines are too long
BIN
test/res/models/astronaut/piece1_basecolor.jpg
Normal file
BIN
test/res/models/astronaut/piece1_basecolor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
test/res/models/astronaut/piece2_basecolor.jpg
Normal file
BIN
test/res/models/astronaut/piece2_basecolor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
BIN
test/res/models/astronaut/piece3_basecolor.jpg
Normal file
BIN
test/res/models/astronaut/piece3_basecolor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
249
test/res/models/plane/plane.dae
Normal file
249
test/res/models/plane/plane.dae
Normal file
File diff suppressed because one or more lines are too long
BIN
test/res/models/pyramid/Pyramid Bricks.png
Normal file
BIN
test/res/models/pyramid/Pyramid Bricks.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 293 KiB |
285
test/res/models/pyramid/pyramid.dae
Normal file
285
test/res/models/pyramid/pyramid.dae
Normal file
File diff suppressed because one or more lines are too long
1853
test/res/models/room/room.dae
Normal file
1853
test/res/models/room/room.dae
Normal file
File diff suppressed because one or more lines are too long
@ -4,7 +4,6 @@ layout(location = 0) in vec3 fragPos;
|
||||
layout(location = 1) in vec3 fragNorm;
|
||||
layout(location = 2) in vec2 fragUV;
|
||||
layout(location = 3) in vec3 fragLightPos;
|
||||
layout(location = 4) in vec3 fragColor;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
layout(binding = 0) uniform UBO {
|
||||
mat4 proj;
|
||||
vec4 color;
|
||||
} ubo;
|
||||
|
||||
layout( push_constant ) uniform Constants {
|
||||
@ -18,7 +17,6 @@ layout(location = 0) out vec3 fragPos;
|
||||
layout(location = 1) out vec3 fragNorm;
|
||||
layout(location = 2) out vec2 fragUV;
|
||||
layout(location = 3) out vec3 fragLightPos;
|
||||
layout(location = 4) out vec3 fragColor;
|
||||
|
||||
void main() {
|
||||
gl_Position = ubo.proj * constants.view * constants.model * vec4(inPosition, 1.0);
|
||||
@ -27,6 +25,5 @@ void main() {
|
||||
fragUV = inUV;
|
||||
vec3 lightPos = vec3(-5.0, 20.0, 5.0);
|
||||
fragLightPos = vec3(constants.view * vec4(lightPos, 1.0));
|
||||
fragColor = ubo.color.rgb;
|
||||
}
|
||||
|
||||
|
BIN
test/res/textures/pyramid.png
Normal file
BIN
test/res/textures/pyramid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 293 KiB |
@ -12,6 +12,8 @@
|
||||
#include "resource_manager.hpp"
|
||||
#include "resources/texture.hpp"
|
||||
|
||||
#include "util/model_loader.hpp"
|
||||
|
||||
#include "camera_controller.hpp"
|
||||
#include "meshgen.hpp"
|
||||
|
||||
@ -68,7 +70,6 @@ void playGame()
|
||||
auto gunRenderer = gun->createComponent<engine::components::Renderer>();
|
||||
gunRenderer->setMesh("meshes/gun.mesh");
|
||||
gunRenderer->setTexture("textures/gun.png");
|
||||
gunRenderer->m_color = { 0.2f, 0.3f, 0.2f };
|
||||
|
||||
// FLOOR
|
||||
|
||||
@ -86,12 +87,10 @@ void playGame()
|
||||
{ { -16.0f, 0.0f, 16.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, GRASS_DENSITY } }
|
||||
});
|
||||
floor->transform.scale = { 100.0f, 1.0f, 100.0f };
|
||||
floorRenderer->m_color = { 0.1f, 0.9f, 0.1f };
|
||||
|
||||
auto cube = app.scene()->createChild("cube");
|
||||
auto cubeRen = cube->createComponent<engine::components::Renderer>();
|
||||
cubeRen->setMesh("meshes/cube.mesh");
|
||||
cubeRen->m_color = { 0.8f, 0.2f, 0.05f };
|
||||
|
||||
cube->transform.position = glm::vec3{ -5.0f, 1.0f, 0.0f };
|
||||
class Spin : public engine::components::CustomComponent {
|
||||
@ -112,6 +111,14 @@ void playGame()
|
||||
yawQuat.z = 0.0f;
|
||||
yawQuat.w = glm::cos(halfYaw);
|
||||
parent.transform.rotation = yawQuat;
|
||||
|
||||
constexpr float halfPitch = -glm::half_pi<float>() / 2.0f;
|
||||
glm::quat pitchQuat{};
|
||||
pitchQuat.x = glm::sin(halfPitch);
|
||||
pitchQuat.y = 0.0f;
|
||||
pitchQuat.z = 0.0f;
|
||||
pitchQuat.w = glm::cos(halfPitch);
|
||||
parent.transform.rotation *= pitchQuat;
|
||||
}
|
||||
|
||||
private:
|
||||
@ -128,35 +135,26 @@ void playGame()
|
||||
sphereRenderer->m_mesh = genSphereMesh(5.0f, 100, false);
|
||||
sphereRenderer->setTexture("textures/cobble_stone.png");
|
||||
|
||||
/* castle */
|
||||
auto castle = app.scene()->createChild("castle");
|
||||
castle->transform.scale = { 0.01f, 0.01f, 0.01f };
|
||||
std::vector<engine::Object*> castleParts(6);
|
||||
for (int i = 0; i < castleParts.size(); i++) {
|
||||
if (i == 2) continue;
|
||||
castleParts[i] = castle->createChild(std::to_string(i));
|
||||
auto ren = castleParts[i]->createComponent<engine::components::Renderer>();
|
||||
ren->setMesh("meshes/castle_" + std::to_string(i) + ".mesh");
|
||||
ren->setTexture("textures/rock.jpg");
|
||||
|
||||
if (i == 5) {
|
||||
ren->setTexture("textures/metal.jpg");
|
||||
}
|
||||
if (i == 4) {
|
||||
ren->setTexture("textures/door.jpg");
|
||||
}
|
||||
}
|
||||
|
||||
// boundary
|
||||
auto bounds = app.scene()->createChild("bounds");
|
||||
auto boundsRen = bounds->createComponent<engine::components::Renderer>();
|
||||
boundsRen->m_mesh = genSphereMesh(100.0f, 100, true);
|
||||
boundsRen->setTexture("textures/metal.jpg");
|
||||
|
||||
auto pyramid = app.scene()->createChild("pyramid");
|
||||
auto pyramidRen = pyramid->createComponent<engine::components::Renderer>();
|
||||
pyramidRen->setMesh("meshes/pyramid.mesh");
|
||||
pyramidRen->setTexture("textures/pyramid.png");
|
||||
auto myModel = engine::util::loadAssimpMeshFromFile(app.scene(), app.resources()->getFilePath("models/pyramid/pyramid.dae").string());
|
||||
|
||||
auto myRoom = engine::util::loadAssimpMeshFromFile(app.scene(), app.resources()->getFilePath("models/room/room.dae").string());
|
||||
|
||||
auto astronaut = engine::util::loadAssimpMeshFromFile(app.scene(), app.resources()->getFilePath("models/astronaut/astronaut.dae").string());
|
||||
astronaut->transform.position.z += 5.0f;
|
||||
astronaut->createComponent<Spin>();
|
||||
|
||||
auto plane = engine::util::loadAssimpMeshFromFile(app.scene(), app.resources()->getFilePath("models/plane/plane.dae").string());
|
||||
plane->transform.position = { -30.0f, 2.0f, 10.0f };
|
||||
|
||||
// END TESTING
|
||||
|
||||
app.scene()->printTree();
|
||||
|
||||
app.gameLoop();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user