mirror of
https://github.com/vrm-c/UniVRM.git
synced 2026-09-09 04:19:42 -05:00
add PreviewSceneManager.m_materialMap
This commit is contained in:
@@ -15,12 +15,10 @@ namespace VRM
|
||||
[Serializable]
|
||||
public struct MaterialValueBinding
|
||||
{
|
||||
public String RelativePath;
|
||||
public int Index;
|
||||
public string ValueName;
|
||||
public String MaterialName;
|
||||
public String ValueName;
|
||||
public Vector4 TargetValue;
|
||||
|
||||
public Vector4 BaseValue; // <- Get From RelativePath/Materials[Index][ValueName]
|
||||
public Vector4 BaseValue;
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "VRM/BlendShapeClip")]
|
||||
@@ -56,6 +54,7 @@ namespace VRM
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (MaterialValues != null)
|
||||
{
|
||||
foreach (var x in MaterialValues)
|
||||
@@ -73,6 +72,7 @@ namespace VRM
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public static BlendShapeClip Create(BlendShapePreset preset)
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace VRM
|
||||
|
||||
m_currentClip = value;
|
||||
//ClearBlendShape();
|
||||
Bake(m_currentClip.Values, m_currentClip.MaterialValues);
|
||||
Bake(m_currentClip.Values, m_currentClip.MaterialValues, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace VRM
|
||||
[CustomEditor(typeof(BlendShapeClip))]
|
||||
public class BlendShapeClipEditor : PreviewEditor
|
||||
{
|
||||
float m_previewSlider;
|
||||
|
||||
#region for Editor
|
||||
SerializedProperty m_BlendShapeNameProp;
|
||||
SerializedProperty m_PresetProp;
|
||||
@@ -25,8 +27,10 @@ namespace VRM
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_previewSlider = 1.0f;
|
||||
|
||||
m_target = (BlendShapeClip)target;
|
||||
Bake(m_target.Values, m_target.MaterialValues);
|
||||
Bake(m_target.Values, m_target.MaterialValues, m_previewSlider);
|
||||
|
||||
m_BlendShapeNameProp = serializedObject.FindProperty("BlendShapeName");
|
||||
m_PresetProp = serializedObject.FindProperty("Preset");
|
||||
@@ -63,9 +67,17 @@ namespace VRM
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
|
||||
m_changed = false;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
var previewSlider= EditorGUILayout.Slider("Preview Weight", m_previewSlider, 0, 1.0f);
|
||||
if (previewSlider != m_previewSlider) {
|
||||
m_previewSlider = previewSlider;
|
||||
m_changed = true;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.PropertyField(m_BlendShapeNameProp, true);
|
||||
@@ -81,7 +93,7 @@ namespace VRM
|
||||
|
||||
if (m_changed && PreviewSceneManager!=null)
|
||||
{
|
||||
PreviewSceneManager.Bake(m_target.Values, m_target.MaterialValues);
|
||||
PreviewSceneManager.Bake(m_target.Values, m_target.MaterialValues, m_previewSlider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,49 +148,46 @@ namespace VRM
|
||||
|
||||
var y = position.y;
|
||||
var rect = new Rect(position.x, y, position.width, height);
|
||||
int pathIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("RelativePath"), scene.RendererPathList, out pathIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
int materialIndex;
|
||||
if (IntPopup(rect, property.FindPropertyRelative("Index"), scene.GetMaterialNames(pathIndex), out materialIndex))
|
||||
if (StringPopup(rect, property.FindPropertyRelative("MaterialName"), scene.MaterialNames, out materialIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
int propIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("ValueName"), scene.GetMaterialPropNames(pathIndex, materialIndex), out propIndex))
|
||||
if (materialIndex >= 0)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (propIndex >= 0)
|
||||
{
|
||||
var propTypes = scene.GetMaterialPropTypes(pathIndex, materialIndex);
|
||||
if (propTypes != null && propIndex < propTypes.Length)
|
||||
var materialItem = scene.GetMaterialItem(scene.MaterialNames[materialIndex]);
|
||||
if (materialItem != null)
|
||||
{
|
||||
switch (propTypes[propIndex])
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
int propIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("ValueName"), materialItem.PropNames, out propIndex))
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
{
|
||||
var baseValues= scene.GetMaterialPropBaseValues(pathIndex, materialIndex);
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = baseValues[propIndex];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (ColorProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
if (propIndex >= 0)
|
||||
{
|
||||
var propItem = materialItem.PropMap[materialItem.PropNames[propIndex]];
|
||||
{
|
||||
switch (propItem.PropertyType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
{
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = propItem.DefaultValues;
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (ColorProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ namespace VRM
|
||||
}
|
||||
}
|
||||
|
||||
protected void Bake(BlendShapeBinding[] values, MaterialValueBinding[] materialValues)
|
||||
protected void Bake(BlendShapeBinding[] values, MaterialValueBinding[] materialValues, float weight)
|
||||
{
|
||||
if (m_scene != null)
|
||||
{
|
||||
m_scene.Bake(values, materialValues);
|
||||
m_scene.Bake(values, materialValues, weight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,59 @@ using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
[Serializable]
|
||||
public struct PropItem
|
||||
{
|
||||
public ShaderUtil.ShaderPropertyType PropertyType;
|
||||
public Vector4 DefaultValues;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class MaterialItem
|
||||
{
|
||||
public Material Material { get; private set; }
|
||||
#if UNITY_EDITOR
|
||||
public Dictionary<string, PropItem> PropMap = new Dictionary<string, PropItem>();
|
||||
|
||||
public string[] PropNames
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static MaterialItem Create(Material material)
|
||||
{
|
||||
var item = new MaterialItem
|
||||
{
|
||||
Material = material
|
||||
};
|
||||
#if UNITY_EDITOR
|
||||
|
||||
var propNames = new List<string>();
|
||||
for (int i = 0; i < ShaderUtil.GetPropertyCount(material.shader); ++i)
|
||||
{
|
||||
var propType = ShaderUtil.GetPropertyType(material.shader, i);
|
||||
if (propType == ShaderUtil.ShaderPropertyType.Color)
|
||||
{
|
||||
var name = ShaderUtil.GetPropertyName(material.shader, i);
|
||||
item.PropMap.Add(name, new PropItem
|
||||
{
|
||||
PropertyType = propType,
|
||||
DefaultValues = material.GetColor(name),
|
||||
});
|
||||
propNames.Add(name);
|
||||
}
|
||||
}
|
||||
item.PropNames = propNames.ToArray();
|
||||
#endif
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class MeshPreviewItem
|
||||
{
|
||||
@@ -35,77 +86,16 @@ namespace VRM
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public int BlendShapeCount
|
||||
{
|
||||
get { return BlendShapeNames.Length; }
|
||||
}
|
||||
|
||||
Material[] m_materials;
|
||||
public Material[] Materials
|
||||
{
|
||||
get { return m_materials; }
|
||||
private set
|
||||
{
|
||||
m_materials = value;
|
||||
m_materialNames = value.Select(x => x.name).ToArray();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
int i = 0;
|
||||
foreach (var x in value)
|
||||
{
|
||||
var propNames = Enumerable.Range(0, ShaderUtil.GetPropertyCount(x.shader))
|
||||
.Where(y => ShaderUtil.GetPropertyType(x.shader, y) == ShaderUtil.ShaderPropertyType.Color)
|
||||
.Select(y => ShaderUtil.GetPropertyName(x.shader, y))
|
||||
.ToArray();
|
||||
m_materialPropNamesList.Add(propNames);
|
||||
|
||||
var propTypes = Enumerable.Range(0, ShaderUtil.GetPropertyCount(x.shader))
|
||||
.Select(y => ShaderUtil.GetPropertyType(x.shader, y))
|
||||
.Where(y => y == ShaderUtil.ShaderPropertyType.Color)
|
||||
.ToArray();
|
||||
m_materialPropTypesList.Add(propTypes);
|
||||
|
||||
var propValues = propNames.Select(y => (Vector4)x.GetColor(y)).ToArray();
|
||||
m_materialPropDefaultValueList.Add(propValues);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
string[] m_materialNames;
|
||||
public string[] MaterialsNames
|
||||
{
|
||||
get { return m_materialNames; }
|
||||
}
|
||||
|
||||
List<string[]> m_materialPropNamesList = new List<string[]>();
|
||||
public string[] GetMaterialPropNames(int materialIndex)
|
||||
{
|
||||
if(m_materialPropNamesList==null || materialIndex<0 || materialIndex >= m_materialPropNamesList.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return m_materialPropNamesList[materialIndex];
|
||||
}
|
||||
|
||||
List<ShaderUtil.ShaderPropertyType[]> m_materialPropTypesList = new List<ShaderUtil.ShaderPropertyType[]>();
|
||||
public ShaderUtil.ShaderPropertyType[] GetMaterialPropTypes(int materialIndex)
|
||||
{
|
||||
if (m_materialPropTypesList == null || materialIndex < 0 || materialIndex >= m_materialPropTypesList.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return m_materialPropTypesList[materialIndex];
|
||||
}
|
||||
|
||||
List<Vector4[]> m_materialPropDefaultValueList = new List<Vector4[]>();
|
||||
public Vector4[] GetMaterialPropBaseValues(int materialIndex)
|
||||
{
|
||||
if (m_materialPropDefaultValueList == null || materialIndex < 0 || materialIndex >= m_materialPropDefaultValueList.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return m_materialPropDefaultValueList[materialIndex];
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
Transform m_transform;
|
||||
@@ -118,17 +108,17 @@ namespace VRM
|
||||
get { return m_transform.rotation; }
|
||||
}
|
||||
|
||||
MeshPreviewItem(string path, Transform transform)
|
||||
MeshPreviewItem(string path, Transform transform, Material[] materials)
|
||||
{
|
||||
Path = path;
|
||||
m_transform = transform;
|
||||
Materials = materials;
|
||||
}
|
||||
|
||||
public void Bake(BlendShapeBinding[] values, MaterialValueBinding[] materialValues)
|
||||
public void Bake(BlendShapeBinding[] values, float weight)
|
||||
{
|
||||
if (SkinnedMeshRenderer == null) return;
|
||||
|
||||
if (values != null)
|
||||
// Update baked mesh
|
||||
if (SkinnedMeshRenderer != null && values != null)
|
||||
{
|
||||
// clear
|
||||
for (int i = 0; i < BlendShapeCount; ++i)
|
||||
@@ -140,47 +130,19 @@ namespace VRM
|
||||
{
|
||||
if (x.RelativePath == Path)
|
||||
{
|
||||
SkinnedMeshRenderer.SetBlendShapeWeight(x.Index, x.Weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (materialValues != null && m_materialPropNamesList!=null)
|
||||
{
|
||||
// clear
|
||||
for (int i = 0; i < Materials.Length; ++i)
|
||||
{
|
||||
var names = m_materialPropNamesList[i];
|
||||
var defaultValues = m_materialPropDefaultValueList[i];
|
||||
for (int j = 0; j < names.Length; ++j)
|
||||
{
|
||||
Materials[i].SetColor(names[j], defaultValues[j]);
|
||||
SkinnedMeshRenderer.SetBlendShapeWeight(x.Index, x.Weight * weight);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var x in materialValues)
|
||||
{
|
||||
if(x.RelativePath == Path)
|
||||
{
|
||||
var material = Materials[x.Index];
|
||||
material.SetColor(x.ValueName, x.TargetValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SkinnedMeshRenderer.BakeMesh(Mesh);
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
foreach (var x in Materials)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(x);
|
||||
SkinnedMeshRenderer.BakeMesh(Mesh);
|
||||
}
|
||||
}
|
||||
|
||||
public static MeshPreviewItem Create(Transform t, Transform root, Func<Material, Material> getOrCreateMaterial)
|
||||
public static MeshPreviewItem Create(Transform t, Transform root,
|
||||
Func<Material, Material> getOrCreateMaterial)
|
||||
{
|
||||
//Debug.Log("create");
|
||||
|
||||
var meshFilter = t.GetComponent<MeshFilter>();
|
||||
var meshRenderer = t.GetComponent<MeshRenderer>();
|
||||
var skinnedMeshRenderer = t.GetComponent<SkinnedMeshRenderer>();
|
||||
@@ -188,10 +150,9 @@ namespace VRM
|
||||
{
|
||||
// copy
|
||||
meshRenderer.sharedMaterials = meshRenderer.sharedMaterials.Select(x => getOrCreateMaterial(x)).ToArray();
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t)
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t, meshRenderer.sharedMaterials)
|
||||
{
|
||||
Mesh = meshFilter.sharedMesh,
|
||||
Materials = meshRenderer.sharedMaterials
|
||||
Mesh = meshFilter.sharedMesh
|
||||
};
|
||||
}
|
||||
else if (skinnedMeshRenderer != null)
|
||||
@@ -202,20 +163,18 @@ namespace VRM
|
||||
{
|
||||
// bake required
|
||||
var sharedMesh = skinnedMeshRenderer.sharedMesh;
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t)
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t, skinnedMeshRenderer.sharedMaterials)
|
||||
{
|
||||
SkinnedMeshRenderer = skinnedMeshRenderer,
|
||||
Mesh = new Mesh(), // for bake
|
||||
BlendShapeNames = Enumerable.Range(0, sharedMesh.blendShapeCount).Select(x => sharedMesh.GetBlendShapeName(x)).ToArray(),
|
||||
Materials = skinnedMeshRenderer.sharedMaterials
|
||||
BlendShapeNames = Enumerable.Range(0, sharedMesh.blendShapeCount).Select(x => sharedMesh.GetBlendShapeName(x)).ToArray()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t)
|
||||
return new MeshPreviewItem(t.RelativePathFrom(root), t, skinnedMeshRenderer.sharedMaterials)
|
||||
{
|
||||
Mesh = skinnedMeshRenderer.sharedMesh,
|
||||
Materials = skinnedMeshRenderer.sharedMaterials
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ namespace VRM
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
foreach(var x in m_meshes)
|
||||
foreach (var kv in m_materialMap)
|
||||
{
|
||||
x.Clean();
|
||||
UnityEngine.Object.DestroyImmediate(kv.Value.Material);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,15 +89,21 @@ namespace VRM
|
||||
}
|
||||
*/
|
||||
|
||||
var materialNames = new List<string>();
|
||||
var map = new Dictionary<Material, Material>();
|
||||
Func<Material, Material> getOrCreateMaterial = src =>
|
||||
{
|
||||
if (src == null) return null;
|
||||
if (string.IsNullOrEmpty(src.name)) return null; // !
|
||||
|
||||
Material dst;
|
||||
if(!map.TryGetValue(src, out dst))
|
||||
{
|
||||
dst = new Material(src);
|
||||
map.Add(src, dst);
|
||||
|
||||
materialNames.Add(src.name);
|
||||
m_materialMap.Add(src.name, MaterialItem.Create(dst));
|
||||
}
|
||||
return dst;
|
||||
};
|
||||
@@ -107,6 +113,8 @@ namespace VRM
|
||||
.Where(x => x != null)
|
||||
.ToArray()
|
||||
;
|
||||
MaterialNames = materialNames.ToArray();
|
||||
|
||||
m_blendShapeMeshes = m_meshes
|
||||
.Where(x => x.SkinnedMeshRenderer != null
|
||||
&& x.SkinnedMeshRenderer.sharedMesh.blendShapeCount>0)
|
||||
@@ -137,6 +145,14 @@ namespace VRM
|
||||
}
|
||||
}
|
||||
|
||||
public string[] MaterialNames
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
Dictionary<string, MaterialItem> m_materialMap = new Dictionary<string, MaterialItem>();
|
||||
|
||||
string[] m_rendererPathList;
|
||||
public string[] RendererPathList
|
||||
{
|
||||
@@ -160,62 +176,18 @@ namespace VRM
|
||||
return null;
|
||||
}
|
||||
|
||||
public string[] GetMaterialNames(int rendererIndex)
|
||||
public MaterialItem GetMaterialItem(string materialName)
|
||||
{
|
||||
if (rendererIndex >= 0 && rendererIndex < m_meshes.Length)
|
||||
{
|
||||
return m_meshes[rendererIndex].MaterialsNames;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public string[] GetMaterialPropNames(int rendererIndex, int materialIndex)
|
||||
{
|
||||
if(rendererIndex<0 || rendererIndex >= m_meshes.Length)
|
||||
{
|
||||
MaterialItem item;
|
||||
if(!m_materialMap.TryGetValue(materialName, out item)){
|
||||
return null;
|
||||
}
|
||||
|
||||
if(materialIndex<0 || materialIndex >= m_meshes[rendererIndex].MaterialsNames.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return m_meshes[rendererIndex].GetMaterialPropNames(materialIndex);
|
||||
}
|
||||
|
||||
public ShaderUtil.ShaderPropertyType[] GetMaterialPropTypes(int rendererIndex, int materialIndex)
|
||||
{
|
||||
if (rendererIndex < 0 || rendererIndex >= m_meshes.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (materialIndex < 0 || materialIndex >= m_meshes[rendererIndex].MaterialsNames.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return m_meshes[rendererIndex].GetMaterialPropTypes(materialIndex);
|
||||
}
|
||||
|
||||
public Vector4[] GetMaterialPropBaseValues(int rendererIndex, int materialIndex)
|
||||
{
|
||||
if (rendererIndex < 0 || rendererIndex >= m_meshes.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (materialIndex < 0 || materialIndex >= m_meshes[rendererIndex].MaterialsNames.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return m_meshes[rendererIndex].GetMaterialPropBaseValues(materialIndex);
|
||||
return item;
|
||||
}
|
||||
|
||||
Bounds m_bounds;
|
||||
public void Bake(BlendShapeBinding[] values=null, MaterialValueBinding[] materialValues=null)
|
||||
public void Bake(BlendShapeBinding[] values=null, MaterialValueBinding[] materialValues=null, float weight=1.0f)
|
||||
{
|
||||
//Debug.LogFormat("Bake");
|
||||
m_bounds = default(Bounds);
|
||||
@@ -223,10 +195,40 @@ namespace VRM
|
||||
{
|
||||
foreach (var x in m_meshes)
|
||||
{
|
||||
x.Bake(values, materialValues);
|
||||
x.Bake(values, weight);
|
||||
m_bounds.Expand(x.Mesh.bounds.size);
|
||||
}
|
||||
}
|
||||
|
||||
// Udpate Material
|
||||
if (materialValues != null && m_materialMap != null)
|
||||
{
|
||||
// clear
|
||||
Debug.LogFormat("clear material");
|
||||
foreach (var kv in m_materialMap)
|
||||
{
|
||||
foreach (var _kv in kv.Value.PropMap)
|
||||
{
|
||||
kv.Value.Material.SetColor(_kv.Key, _kv.Value.DefaultValues);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var x in materialValues)
|
||||
{
|
||||
MaterialItem item;
|
||||
if (m_materialMap.TryGetValue(x.MaterialName, out item))
|
||||
{
|
||||
Debug.Log("set material");
|
||||
PropItem prop;
|
||||
if (item.PropMap.TryGetValue(x.ValueName, out prop))
|
||||
{
|
||||
var value = x.BaseValue + (x.TargetValue - x.BaseValue) * weight;
|
||||
item.Material.SetColor(x.ValueName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user