forked from blender/blender
Support instancing for other objects: curves and volumes #74
@ -38,7 +38,7 @@ pxr::HdMeshTopology BlenderSceneDelegate::GetMeshTopology(pxr::SdfPath const &id
|
||||
{
|
||||
CLOG_INFO(LOG_RENDER_HYDRA_SCENE, 3, "%s", id.GetText());
|
||||
MeshData *m_data = mesh_data(id);
|
||||
return m_data->mesh_topology(id);
|
||||
return m_data->topology(id);
|
||||
}
|
||||
|
||||
pxr::HdBasisCurvesTopology BlenderSceneDelegate::GetBasisCurvesTopology(pxr::SdfPath const &id)
|
||||
@ -53,7 +53,7 @@ pxr::GfMatrix4d BlenderSceneDelegate::GetTransform(pxr::SdfPath const &id)
|
||||
CLOG_INFO(LOG_RENDER_HYDRA_SCENE, 3, "%s", id.GetText());
|
||||
InstancerData *i_data = instancer_data(id, true);
|
||||
if (i_data) {
|
||||
return i_data->get_transform(id);
|
||||
return i_data->transform(id);
|
||||
}
|
||||
ObjectData *obj_data = object_data(id);
|
||||
if (obj_data) {
|
||||
@ -162,7 +162,7 @@ pxr::SdfPath BlenderSceneDelegate::GetInstancerId(pxr::SdfPath const &prim_id)
|
||||
{
|
||||
CLOG_INFO(LOG_RENDER_HYDRA_SCENE, 3, "%s", prim_id.GetText());
|
||||
InstancerData *i_data = instancer_data(prim_id, true);
|
||||
if (i_data) {
|
||||
if (i_data && mesh_data(prim_id)) {
|
||||
return i_data->prim_id;
|
||||
}
|
||||
return pxr::SdfPath();
|
||||
@ -187,7 +187,7 @@ pxr::GfMatrix4d BlenderSceneDelegate::GetInstancerTransform(pxr::SdfPath const &
|
||||
{
|
||||
CLOG_INFO(LOG_RENDER_HYDRA_SCENE, 3, "%s", instancer_id.GetText());
|
||||
InstancerData *i_data = instancer_data(instancer_id);
|
||||
return i_data->get_transform(instancer_id);
|
||||
return i_data->transform(instancer_id);
|
||||
}
|
||||
|
||||
pxr::HdVolumeFieldDescriptorVector BlenderSceneDelegate::GetVolumeFieldDescriptors(
|
||||
@ -449,17 +449,21 @@ void BlenderSceneDelegate::update_collection()
|
||||
object)
|
||||
{
|
||||
if (data.dupli_object_current) {
|
||||
instancer_data_->update_instance(data.dupli_parent, data.dupli_object_current);
|
||||
DupliObject *dupli = data.dupli_object_current;
|
||||
if (!ObjectData::is_supported(dupli->ob) ||
|
||||
!ObjectData::is_visible(this, data.dupli_parent, OB_VISIBLE_INSTANCES) ||
|
||||
(!shading_settings.use_scene_lights && object->type == OB_LAMP))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ObjectData::is_supported(object)) {
|
||||
instancer_data_->update_instance(data.dupli_parent, dupli);
|
||||
continue;
|
||||
}
|
||||
if (!ObjectData::is_visible(this, object)) {
|
||||
continue;
|
||||
}
|
||||
if (!shading_settings.use_scene_lights && object->type == OB_LAMP) {
|
||||
|
||||
if (!ObjectData::is_supported(object) || !ObjectData::is_visible(this, object) ||
|
||||
(!shading_settings.use_scene_lights && object->type == OB_LAMP))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -16,23 +16,6 @@ InstancerData::InstancerData(BlenderSceneDelegate *scene_delegate, pxr::SdfPath
|
||||
{
|
||||
}
|
||||
|
||||
bool InstancerData::is_instance_supported(Object *object)
|
||||
{
|
||||
switch (object->type) {
|
||||
case OB_MESH:
|
||||
case OB_SURF:
|
||||
case OB_FONT:
|
||||
case OB_CURVES_LEGACY:
|
||||
case OB_MBALL:
|
||||
case OB_LAMP:
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InstancerData::init() {}
|
||||
|
||||
void InstancerData::insert() {}
|
||||
@ -48,11 +31,11 @@ void InstancerData::remove()
|
||||
}
|
||||
mesh_instances_.clear();
|
||||
|
||||
for (auto &l_inst : light_instances_.values()) {
|
||||
for (auto &l_inst : nonmesh_instances_.values()) {
|
||||
l_inst.transforms.clear();
|
||||
update_light_instance(l_inst);
|
||||
update_nonmesh_instance(l_inst);
|
||||
}
|
||||
light_instances_.clear();
|
||||
nonmesh_instances_.clear();
|
||||
}
|
||||
|
||||
void InstancerData::update() {}
|
||||
@ -66,11 +49,11 @@ pxr::VtValue InstancerData::get_data(pxr::TfToken const &key) const
|
||||
return pxr::VtValue();
|
||||
}
|
||||
|
||||
pxr::GfMatrix4d InstancerData::get_transform(pxr::SdfPath const &id) const
|
||||
pxr::GfMatrix4d InstancerData::transform(pxr::SdfPath const &id) const
|
||||
{
|
||||
LightInstance *l_inst = light_instance(id);
|
||||
if (l_inst) {
|
||||
return l_inst->transforms[light_prim_id_index(id)];
|
||||
NonmeshInstance *nm_inst = nonmesh_instance(id);
|
||||
if (nm_inst) {
|
||||
return nm_inst->transforms[nonmesh_prim_id_index(id)];
|
||||
}
|
||||
|
||||
/* Mesh instance transform must be identity */
|
||||
@ -99,9 +82,9 @@ ObjectData *InstancerData::object_data(pxr::SdfPath const &id) const
|
||||
if (m_inst) {
|
||||
return m_inst->data.get();
|
||||
}
|
||||
LightInstance *l_inst = light_instance(id);
|
||||
if (l_inst) {
|
||||
return l_inst->data.get();
|
||||
NonmeshInstance *nm_inst = nonmesh_instance(id);
|
||||
if (nm_inst) {
|
||||
return nm_inst->data.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@ -120,7 +103,10 @@ pxr::SdfPathVector InstancerData::prototypes() const
|
||||
void InstancerData::available_materials(Set<pxr::SdfPath> &paths) const
|
||||
{
|
||||
for (auto &m_inst : mesh_instances_.values()) {
|
||||
((MeshData *)m_inst.data.get())->available_materials(paths);
|
||||
m_inst.data->available_materials(paths);
|
||||
}
|
||||
for (auto &l_inst : nonmesh_instances_.values()) {
|
||||
l_inst.data->available_materials(paths);
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,50 +123,39 @@ void InstancerData::pre_update()
|
||||
for (auto &m_inst : mesh_instances_.values()) {
|
||||
m_inst.indices.clear();
|
||||
}
|
||||
for (auto &l_inst : light_instances_.values()) {
|
||||
for (auto &l_inst : nonmesh_instances_.values()) {
|
||||
l_inst.transforms.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void InstancerData::update_instance(Object *parent_ob, DupliObject *dupli)
|
||||
{
|
||||
if (!ObjectData::is_visible(scene_delegate_, parent_ob, OB_VISIBLE_INSTANCES)) {
|
||||
return;
|
||||
}
|
||||
Object *ob = dupli->ob;
|
||||
if (!is_instance_supported(ob)) {
|
||||
return;
|
||||
}
|
||||
if (!scene_delegate_->shading_settings.use_scene_lights && ob->type == OB_LAMP) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::SdfPath p_id = object_prim_id(ob);
|
||||
if (ob->type == OB_LAMP) {
|
||||
LightInstance *inst = light_instance(p_id);
|
||||
if (!inst) {
|
||||
inst = &light_instances_.lookup_or_add_default(p_id);
|
||||
inst->data = std::make_unique<LightData>(scene_delegate_, ob, p_id);
|
||||
inst->data->init();
|
||||
}
|
||||
ID_LOG(2, "Light %s %d", inst->data->id->name, (int)inst->transforms.size());
|
||||
inst->transforms.push_back(gf_matrix_from_transform(dupli->mat));
|
||||
Object *object = dupli->ob;
|
||||
pxr::SdfPath p_id = object_prim_id(object);
|
||||
if (ObjectData::is_mesh(object)) {
|
||||
MeshInstance *m_inst = mesh_instance(p_id);
|
||||
if (!m_inst) {
|
||||
m_inst = &mesh_instances_.lookup_or_add_default(p_id);
|
||||
m_inst->data = std::make_unique<MeshData>(scene_delegate_, object, p_id);
|
||||
m_inst->data->init();
|
||||
m_inst->data->insert();
|
||||
}
|
||||
else {
|
||||
MeshInstance *inst = mesh_instance(p_id);
|
||||
if (!inst) {
|
||||
inst = &mesh_instances_.lookup_or_add_default(p_id);
|
||||
inst->data = std::make_unique<MeshData>(scene_delegate_, ob, p_id);
|
||||
inst->data->init();
|
||||
inst->data->insert();
|
||||
m_inst->data->update();
|
||||
}
|
||||
else {
|
||||
inst->data->update();
|
||||
}
|
||||
ID_LOG(2, "Mesh %s %d", inst->data->id->name, (int)mesh_transforms_.size());
|
||||
inst->indices.push_back(mesh_transforms_.size());
|
||||
ID_LOG(2, "Mesh %s %d", m_inst->data->id->name, (int)mesh_transforms_.size());
|
||||
m_inst->indices.push_back(mesh_transforms_.size());
|
||||
mesh_transforms_.push_back(gf_matrix_from_transform(dupli->mat));
|
||||
}
|
||||
else {
|
||||
NonmeshInstance *nm_inst = nonmesh_instance(p_id);
|
||||
if (!nm_inst) {
|
||||
nm_inst = &nonmesh_instances_.lookup_or_add_default(p_id);
|
||||
nm_inst->data = ObjectData::create(scene_delegate_, object, p_id);
|
||||
}
|
||||
ID_LOG(2, "Light %s %d", nm_inst->data->id->name, (int)nm_inst->transforms.size());
|
||||
nm_inst->transforms.push_back(gf_matrix_from_transform(dupli->mat));
|
||||
}
|
||||
}
|
||||
|
||||
void InstancerData::post_update()
|
||||
@ -195,16 +170,16 @@ void InstancerData::post_update()
|
||||
});
|
||||
|
||||
/* Update light intances and remove instances without transforms */
|
||||
for (auto &l_inst : light_instances_.values()) {
|
||||
update_light_instance(l_inst);
|
||||
for (auto &l_inst : nonmesh_instances_.values()) {
|
||||
update_nonmesh_instance(l_inst);
|
||||
}
|
||||
light_instances_.remove_if([&](auto item) { return item.value.transforms.empty(); });
|
||||
nonmesh_instances_.remove_if([&](auto item) { return item.value.transforms.empty(); });
|
||||
|
||||
/* Insert/remove/update instancer in RenderIndex */
|
||||
pxr::HdRenderIndex &index = scene_delegate_->GetRenderIndex();
|
||||
if (mesh_instances_.is_empty()) {
|
||||
/* Important: removing instancer when light_instances_ are empty too */
|
||||
if (index.HasInstancer(prim_id) && light_instances_.is_empty()) {
|
||||
/* Important: removing instancer when nonmesh_instances_ are empty too */
|
||||
if (index.HasInstancer(prim_id) && nonmesh_instances_.is_empty()) {
|
||||
index.RemoveInstancer(prim_id);
|
||||
ID_LOG(1, "Remove instancer");
|
||||
}
|
||||
@ -229,73 +204,62 @@ pxr::SdfPath InstancerData::object_prim_id(Object *object) const
|
||||
return prim_id.AppendElementString(name);
|
||||
}
|
||||
|
||||
pxr::SdfPath InstancerData::light_prim_id(LightInstance const &inst, int index) const
|
||||
pxr::SdfPath InstancerData::nonmesh_prim_id(pxr::SdfPath const &prim_id, int index) const
|
||||
{
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "L_%08x", index);
|
||||
return inst.data->prim_id.AppendElementString(name);
|
||||
snprintf(name, sizeof(name), "NM_%08x", index);
|
||||
return prim_id.AppendElementString(name);
|
||||
}
|
||||
|
||||
int InstancerData::light_prim_id_index(pxr::SdfPath const &id) const
|
||||
int InstancerData::nonmesh_prim_id_index(pxr::SdfPath const &id) const
|
||||
{
|
||||
int index;
|
||||
sscanf(id.GetName().c_str(), "L_%x", &index);
|
||||
sscanf(id.GetName().c_str(), "NM_%x", &index);
|
||||
return index;
|
||||
}
|
||||
|
||||
void InstancerData::update_light_instance(LightInstance &inst)
|
||||
void InstancerData::update_nonmesh_instance(NonmeshInstance &nm_inst)
|
||||
{
|
||||
auto &render_index = scene_delegate_->GetRenderIndex();
|
||||
LightData &l_data = *inst.data;
|
||||
|
||||
ObjectData *obj_data = nm_inst.data.get();
|
||||
pxr::SdfPath prev_id = nm_inst.data->prim_id;
|
||||
int i;
|
||||
pxr::SdfPath p;
|
||||
|
||||
/* Remove old light instances */
|
||||
while (inst.count > inst.transforms.size()) {
|
||||
--inst.count;
|
||||
p = light_prim_id(inst, inst.count);
|
||||
render_index.RemoveSprim(l_data.prim_type_, p);
|
||||
ID_LOG(2, "Remove %s", p.GetText());
|
||||
while (nm_inst.count > nm_inst.transforms.size()) {
|
||||
--nm_inst.count;
|
||||
obj_data->prim_id = nonmesh_prim_id(prev_id, nm_inst.count);
|
||||
obj_data->remove();
|
||||
}
|
||||
|
||||
/* Update current light instances */
|
||||
if (inst.data->prim_type((Light *)((Object *)l_data.id)->data) != l_data.prim_type_) {
|
||||
/* Recreate instances when prim_type was changed */
|
||||
for (i = 0; i < inst.count; ++i) {
|
||||
p = light_prim_id(inst, i);
|
||||
render_index.RemoveSprim(l_data.prim_type_, p);
|
||||
ID_LOG(2, "Remove %s", p.GetText());
|
||||
LightData *l_data = dynamic_cast<LightData *>(obj_data);
|
||||
if (l_data && l_data->prim_type((Light *)((Object *)l_data->id)->data) != l_data->prim_type_) {
|
||||
/* Special case: recreate instances when prim_type was changed */
|
||||
for (i = 0; i < nm_inst.count; ++i) {
|
||||
obj_data->prim_id = nonmesh_prim_id(prev_id, i);
|
||||
obj_data->remove();
|
||||
}
|
||||
inst.data->init();
|
||||
for (i = 0; i < inst.count; ++i) {
|
||||
p = light_prim_id(inst, i);
|
||||
render_index.InsertSprim(l_data.prim_type_, scene_delegate_, p);
|
||||
ID_LOG(2, "Insert %s (%s)", p.GetText(), l_data.id->name);
|
||||
l_data->init();
|
||||
for (i = 0; i < nm_inst.count; ++i) {
|
||||
obj_data->prim_id = nonmesh_prim_id(prev_id, i);
|
||||
obj_data->insert();
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Update light instances*/
|
||||
pxr::HdDirtyBits bits = pxr::HdLight::DirtyTransform;
|
||||
Object *obj = (Object *)inst.data->id;
|
||||
if (obj->id.recalc & ID_RECALC_GEOMETRY || ((ID *)obj->data)->recalc & ID_RECALC_GEOMETRY) {
|
||||
l_data.init();
|
||||
bits = pxr::HdLight::AllDirty;
|
||||
}
|
||||
for (i = 0; i < inst.count; ++i) {
|
||||
p = light_prim_id(inst, i);
|
||||
render_index.GetChangeTracker().MarkSprimDirty(p, bits);
|
||||
ID_LOG(2, "Update %s (%s)", p.GetText(), l_data.id->name);
|
||||
for (i = 0; i < nm_inst.count; ++i) {
|
||||
obj_data->prim_id = nonmesh_prim_id(prev_id, i);
|
||||
obj_data->update();
|
||||
}
|
||||
}
|
||||
|
||||
/* Add new light instances */
|
||||
while (inst.count < inst.transforms.size()) {
|
||||
p = light_prim_id(inst, inst.count);
|
||||
render_index.InsertSprim(l_data.prim_type_, scene_delegate_, p);
|
||||
ID_LOG(2, "Insert %s (%s)", p.GetText(), l_data.id->name);
|
||||
++inst.count;
|
||||
while (nm_inst.count < nm_inst.transforms.size()) {
|
||||
obj_data->prim_id = nonmesh_prim_id(prev_id, nm_inst.count);
|
||||
obj_data->insert();
|
||||
++nm_inst.count;
|
||||
}
|
||||
|
||||
obj_data->prim_id = prev_id;
|
||||
}
|
||||
|
||||
InstancerData::MeshInstance *InstancerData::mesh_instance(pxr::SdfPath const &id) const
|
||||
@ -308,14 +272,14 @@ InstancerData::MeshInstance *InstancerData::mesh_instance(pxr::SdfPath const &id
|
||||
return const_cast<MeshInstance *>(m_inst);
|
||||
}
|
||||
|
||||
InstancerData::LightInstance *InstancerData::light_instance(pxr::SdfPath const &id) const
|
||||
InstancerData::NonmeshInstance *InstancerData::nonmesh_instance(pxr::SdfPath const &id) const
|
||||
{
|
||||
auto l_inst = light_instances_.lookup_ptr(id.GetPathElementCount() == 4 ? id.GetParentPath() :
|
||||
auto nm_inst = nonmesh_instances_.lookup_ptr(id.GetPathElementCount() == 4 ? id.GetParentPath() :
|
||||
id);
|
||||
if (!l_inst) {
|
||||
if (!nm_inst) {
|
||||
return nullptr;
|
||||
}
|
||||
return const_cast<LightInstance *>(l_inst);
|
||||
return const_cast<NonmeshInstance *>(nm_inst);
|
||||
}
|
||||
|
||||
} // namespace blender::render::hydra
|
||||
|
@ -7,7 +7,6 @@
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "light.h"
|
||||
#include "mesh.h"
|
||||
|
||||
namespace blender::render::hydra {
|
||||
@ -18,15 +17,14 @@ class InstancerData : public IdData {
|
||||
pxr::VtIntArray indices;
|
||||
};
|
||||
|
||||
struct LightInstance {
|
||||
std::unique_ptr<LightData> data;
|
||||
struct NonmeshInstance {
|
||||
std::unique_ptr<ObjectData> data;
|
||||
pxr::VtMatrix4dArray transforms;
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
public:
|
||||
InstancerData(BlenderSceneDelegate *scene_delegate, pxr::SdfPath const &prim_id);
|
||||
static bool is_instance_supported(Object *object);
|
||||
|
||||
void init() override;
|
||||
void insert() override;
|
||||
@ -34,7 +32,7 @@ class InstancerData : public IdData {
|
||||
void update() override;
|
||||
|
||||
pxr::VtValue get_data(pxr::TfToken const &key) const override;
|
||||
pxr::GfMatrix4d get_transform(pxr::SdfPath const &id) const;
|
||||
pxr::GfMatrix4d transform(pxr::SdfPath const &id) const;
|
||||
pxr::HdPrimvarDescriptorVector primvar_descriptors(pxr::HdInterpolation interpolation) const;
|
||||
pxr::VtIntArray indices(pxr::SdfPath const &id) const;
|
||||
ObjectData *object_data(pxr::SdfPath const &id) const;
|
||||
@ -54,14 +52,14 @@ class InstancerData : public IdData {
|
||||
|
||||
private:
|
||||
pxr::SdfPath object_prim_id(Object *object) const;
|
||||
pxr::SdfPath light_prim_id(LightInstance const &inst, int index) const;
|
||||
int light_prim_id_index(pxr::SdfPath const &id) const;
|
||||
void update_light_instance(LightInstance &inst);
|
||||
pxr::SdfPath nonmesh_prim_id(pxr::SdfPath const &prim_id, int index) const;
|
||||
int nonmesh_prim_id_index(pxr::SdfPath const &id) const;
|
||||
void update_nonmesh_instance(NonmeshInstance &inst);
|
||||
MeshInstance *mesh_instance(pxr::SdfPath const &id) const;
|
||||
LightInstance *light_instance(pxr::SdfPath const &id) const;
|
||||
NonmeshInstance *nonmesh_instance(pxr::SdfPath const &id) const;
|
||||
|
||||
Map<pxr::SdfPath, MeshInstance> mesh_instances_;
|
||||
Map<pxr::SdfPath, LightInstance> light_instances_;
|
||||
Map<pxr::SdfPath, NonmeshInstance> nonmesh_instances_;
|
||||
pxr::VtMatrix4dArray mesh_transforms_;
|
||||
};
|
||||
|
||||
|
@ -119,7 +119,7 @@ void MeshData::available_materials(Set<pxr::SdfPath> &paths) const
|
||||
}
|
||||
}
|
||||
|
||||
pxr::HdMeshTopology MeshData::mesh_topology(pxr::SdfPath const &id) const
|
||||
pxr::HdMeshTopology MeshData::topology(pxr::SdfPath const &id) const
|
||||
{
|
||||
const SubMesh &sm = submesh(id);
|
||||
return pxr::HdMeshTopology(pxr::PxOsdOpenSubdivTokens->none,
|
||||
|
@ -37,7 +37,7 @@ class MeshData : public ObjectData {
|
||||
pxr::SdfPath material_id(pxr::SdfPath const &id) const override;
|
||||
void available_materials(Set<pxr::SdfPath> &paths) const override;
|
||||
|
||||
pxr::HdMeshTopology mesh_topology(pxr::SdfPath const &id) const;
|
||||
pxr::HdMeshTopology topology(pxr::SdfPath const &id) const;
|
||||
pxr::HdPrimvarDescriptorVector primvar_descriptors(pxr::HdInterpolation interpolation) const;
|
||||
pxr::HdCullStyle cull_style(pxr::SdfPath const &id) const;
|
||||
bool double_sided(pxr::SdfPath const &id) const;
|
||||
|
@ -24,7 +24,6 @@ std::unique_ptr<ObjectData> ObjectData::create(BlenderSceneDelegate *scene_deleg
|
||||
pxr::SdfPath const &prim_id)
|
||||
{
|
||||
std::unique_ptr<ObjectData> obj_data;
|
||||
|
||||
switch (object->type) {
|
||||
case OB_MESH:
|
||||
case OB_SURF:
|
||||
@ -73,6 +72,24 @@ bool ObjectData::is_supported(Object *object)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ObjectData::is_mesh(Object *object)
|
||||
{
|
||||
switch (object->type) {
|
||||
case OB_MESH:
|
||||
case OB_SURF:
|
||||
case OB_FONT:
|
||||
case OB_CURVES_LEGACY:
|
||||
case OB_MBALL:
|
||||
if (VolumeModifierData::is_volume_modifier(object)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ObjectData::is_visible(BlenderSceneDelegate *scene_delegate, Object *object, int mode)
|
||||
{
|
||||
eEvaluationMode deg_mode = DEG_get_mode(scene_delegate->depsgraph);
|
||||
|
@ -25,6 +25,7 @@ class ObjectData : public IdData {
|
||||
Object *object,
|
||||
pxr::SdfPath const &prim_id);
|
||||
static bool is_supported(Object *object);
|
||||
static bool is_mesh(Object *object);
|
||||
static bool is_visible(BlenderSceneDelegate *scene_delegate,
|
||||
Object *object,
|
||||
int mode = OB_VISIBLE_SELF);
|
||||
|
Loading…
Reference in New Issue
Block a user