Files
Hasebe/Assets/Code/D/Cleaning/CleanableSurface.cs
T
2026-09-20 15:26:03 +03:00

490 lines
18 KiB
C#

using System.Collections.Generic;
using Code.D.Cleaning.CleanTools;
using Cysharp.Threading.Tasks;
using TinyGoose.Tremble;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Code.D.Cleaning
{
[BrushEntity("CleanableSurface", layer: "Cleanable")]
public class CleanableSurface : Cleanable, IOnImportFromMapEntity
{
private static readonly int MaskMap = Shader.PropertyToID("_Mask");
private static readonly int DrawPosition = Shader.PropertyToID("_DrawPosition");
private static readonly int DrawRadius = Shader.PropertyToID("_DrawRadius");
private static readonly int ToolDirtiness = Shader.PropertyToID("_ToolDirtiness");
private static readonly int BrushTex = Shader.PropertyToID("_BrushTex");
private static readonly int DrawAngleX = Shader.PropertyToID("_DrawAngleX");
private static readonly int DrawAngleY = Shader.PropertyToID("_DrawAngleY");
private const int CalcResolution = 64;
[SerializeField] private float cleanedThreshold = 0.7f;
private RenderTexture _maskRT;
private RenderTexture _calcRT;
private Material _brushMaterial;
private Material _renderMaterial;
private bool _onProcess;
private UniTask _calculationOperation;
private void Awake()
{
gameObject.tag = "CleanableSurface";
_maskRT = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGB32);
_maskRT.Create();
Graphics.Blit(Texture2D.whiteTexture, _maskRT);
_calcRT = new RenderTexture(CalcResolution, CalcResolution, 0, RenderTextureFormat.ARGB32);
_calcRT.Create();
_brushMaterial = new Material(Shader.Find("Unlit/BrushPainter"));
if (!TryGetComponent<MeshRenderer>(out var renderer)) return;
_renderMaterial = renderer.material;
_renderMaterial.SetTexture(MaskMap, _maskRT);
}
/// <summary>
/// Нейросеть написала и этот метод, слишком много математики мне он не нравится. Он заставляет игру лагать. Нужно сделать проще.
/// Хотябы комментарии оставлены человеком.
/// Я профнепригоден.
/// </summary>
/// <param name="hit"></param>
/// <param name="tryapkaUsageInfo"></param>
/// <param name="playerCamera"></param>
public void Clean(RaycastHit hit, TryapkaUsageInfo tryapkaUsageInfo, Transform playerCamera)
{
if (Cleaned.CurrentValue) return;
Vector2 uv = hit.textureCoord;
// Логика вычисления направления кисти
Vector3 dirToCamera = playerCamera.position - hit.point;
Vector3 projDir = Vector3.ProjectOnPlane(dirToCamera, hit.normal);
if (projDir.sqrMagnitude < 0.0001f)
{
projDir = Vector3.ProjectOnPlane(playerCamera.up, hit.normal);
}
projDir.Normalize();
// Логика определения U V или W
Vector3 localDir = hit.transform.InverseTransformDirection(projDir);
Vector3 localNormal = hit.transform.InverseTransformDirection(hit.normal).normalized;
ComputeTangentBasis(localNormal, out var axisU, out var axisV);
// Определяем синус и косинус поворота
float u = Vector3.Dot(localDir, axisU);
float v = Vector3.Dot(localDir, axisV);
_brushMaterial.SetVector(DrawPosition, uv);
_brushMaterial.SetFloat(DrawRadius, tryapkaUsageInfo.Radius);
_brushMaterial.SetFloat(DrawAngleX, v);
_brushMaterial.SetFloat(DrawAngleY, u);
_brushMaterial.SetFloat(ToolDirtiness, tryapkaUsageInfo.Dirtiness);
_brushMaterial.SetTexture(BrushTex, tryapkaUsageInfo.BrushTexture);
Graphics.Blit(Texture2D.blackTexture, _maskRT, _brushMaterial, 0);
if (_calculationOperation.Status != UniTaskStatus.Pending)
{
_calculationOperation = CalculateCleanPercentage();
}
}
private static void ComputeTangentBasis(Vector3 normal, out Vector3 axisU, out Vector3 axisV)
{
if (Mathf.Abs(normal.y) > FlatNormalThreshold)
{
axisU = Vector3.right;
axisV = Vector3.forward;
}
else
{
axisU = Vector3.Cross(Vector3.up, normal).normalized;
axisV = Vector3.up;
}
}
private async UniTask CalculateCleanPercentage()
{
if (!_maskRT || _onProcess) return;
_onProcess = true;
Graphics.Blit(_maskRT, _calcRT);
var destroyToken = this.GetCancellationTokenOnDestroy();
var request = AsyncGPUReadback.Request(_calcRT, 0, TextureFormat.RGBA32);
await request.ToUniTask(cancellationToken: destroyToken);
if (request.hasError)
{
_onProcess = false;
return;
}
NativeArray<byte> data = request.GetData<byte>();
int totalBytes = data.Length;
long totalBrightness = 0;
for (var i = 0; i < totalBytes; i += 4)
{
totalBrightness += data[i];
}
int pixelCount = totalBytes / 4;
float maxPossibleBrightness = pixelCount * 255f;
float progress = 1.0f - (totalBrightness / maxPossibleBrightness);
if (Mathf.Abs(progress - CleanProgress.CurrentValue) > 0.01f)
{
CleanProgress.Value = progress;
}
if (CleanProgress.CurrentValue >= cleanedThreshold && !Cleaned.CurrentValue)
{
AssumedAsCleaned();
}
_onProcess = false;
}
private void AssumedAsCleaned()
{
Cleaned.Value = true;
FinishClean();
}
private void FinishClean()
{
Graphics.Blit(Texture2D.blackTexture, _maskRT);
}
private void OnDestroy()
{
if (_maskRT) _maskRT.Release();
if (_calcRT) _calcRT.Release();
if (_brushMaterial) Destroy(_brushMaterial);
if (_renderMaterial) Destroy(_renderMaterial);
}
public void OnImportFromMapEntity(MapBsp mapBsp, BspEntity entity)
{
Debug.Log("Found a brush!");
var meshFilter = gameObject.GetComponent<MeshFilter>();
if (meshFilter == null)
{
Debug.LogError($"[CleanableSurface] MeshFilter отсутствует на {gameObject.name}");
return;
}
var sourceMesh = meshFilter.sharedMesh;
if (sourceMesh == null)
{
Debug.LogError($"[CleanableSurface] sharedMesh == null на {gameObject.name}");
return;
}
if (!sourceMesh.isReadable)
{
Debug.LogError($"[CleanableSurface] Меш '{sourceMesh.name}' не readable, не могу построить UV");
return;
}
if (sourceMesh.vertexCount == 0 || sourceMesh.triangles.Length == 0)
{
Debug.LogError($"[CleanableSurface] Меш '{sourceMesh.name}' пустой");
return;
}
try
{
BuildLayeredUVInPlace(sourceMesh);
}
catch (System.Exception ex)
{
Debug.LogError($"[CleanableSurface] Исключение при построении UV для {gameObject.name}: {ex}");
return;
}
var meshCollider = gameObject.GetComponent<MeshCollider>();
if (meshCollider != null)
meshCollider.sharedMesh = sourceMesh;
#if UNITY_EDITOR
var existingFlags = GameObjectUtility.GetStaticEditorFlags(gameObject);
GameObjectUtility.SetStaticEditorFlags(gameObject, existingFlags | StaticEditorFlags.ContributeGI);
#endif
Debug.Log(
$"[CleanableSurface] UV построен для {gameObject.name}: verts={sourceMesh.vertexCount}, tris={sourceMesh.triangles.Length / 3}");
}
private const float MaskUvPadding = 0.08f;
private const float WeldEpsilon = 0.0005f;
private const float CoplanarDotThreshold = 0.999f;
private const float FlatNormalThreshold = 0.999f;
/// <summary>
/// Этот большой и сложный метод написан нейросетью Claude Sonnet 5 Medium. Я профнепригоден
/// </summary>
/// <param name="mesh"></param>
private static void BuildLayeredUVInPlace(Mesh mesh)
{
var srcVerts = mesh.vertices;
var srcNormals = mesh.normals;
var srcUvTile = mesh.uv;
bool hasNormals = srcNormals.Length == srcVerts.Length;
bool hasUvTile = srcUvTile.Length == srcVerts.Length;
int subMeshCount = mesh.subMeshCount;
var posToId = new Dictionary<Vector3Int, int>();
int CanonicalId(Vector3 p)
{
var key = new Vector3Int(
Mathf.RoundToInt(p.x / WeldEpsilon),
Mathf.RoundToInt(p.y / WeldEpsilon),
Mathf.RoundToInt(p.z / WeldEpsilon));
if (posToId.TryGetValue(key, out var id)) return id;
id = posToId.Count;
posToId[key] = id;
return id;
}
var triSubMesh = new List<int>();
var triI0 = new List<int>();
var triI1 = new List<int>();
var triI2 = new List<int>();
var triCanonA = new List<int>();
var triCanonB = new List<int>();
var triCanonC = new List<int>();
var triNormal = new List<Vector3>();
for (int s = 0; s < subMeshCount; s++)
{
var tris = mesh.GetTriangles(s);
for (int t = 0; t < tris.Length; t += 3)
{
int i0 = tris[t], i1 = tris[t + 1], i2 = tris[t + 2];
Vector3 p0 = srcVerts[i0], p1 = srcVerts[i1], p2 = srcVerts[i2];
Vector3 n = Vector3.Cross(p1 - p0, p2 - p0);
n = n.sqrMagnitude > 1e-10f ? n.normalized : Vector3.up;
triSubMesh.Add(s);
triI0.Add(i0);
triI1.Add(i1);
triI2.Add(i2);
triCanonA.Add(CanonicalId(p0));
triCanonB.Add(CanonicalId(p1));
triCanonC.Add(CanonicalId(p2));
triNormal.Add(n);
}
}
int triCount = triSubMesh.Count;
var parent = new int[triCount];
for (int i = 0; i < triCount; i++) parent[i] = i;
int Find(int x)
{
while (parent[x] != x)
{
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void Union(int a, int b)
{
a = Find(a);
b = Find(b);
if (a != b) parent[a] = b;
}
var edgeMap = new Dictionary<long, int>();
long PackEdge(int a, int b)
{
if (a > b) (a, b) = (b, a);
return ((long)a << 32) | (uint)b;
}
void ProcessEdge(long e, int t)
{
if (edgeMap.TryGetValue(e, out var other))
{
if (Vector3.Dot(triNormal[t], triNormal[other]) > CoplanarDotThreshold)
Union(t, other);
}
else
{
edgeMap[e] = t;
}
}
for (int t = 0; t < triCount; t++)
{
int a = triCanonA[t], b = triCanonB[t], c = triCanonC[t];
ProcessEdge(PackEdge(a, b), t);
ProcessEdge(PackEdge(b, c), t);
ProcessEdge(PackEdge(c, a), t);
}
var islandOf = new int[triCount];
var islandRootToIndex = new Dictionary<int, int>();
for (int t = 0; t < triCount; t++)
{
int root = Find(t);
if (!islandRootToIndex.TryGetValue(root, out var idx))
{
idx = islandRootToIndex.Count;
islandRootToIndex[root] = idx;
}
islandOf[t] = idx;
}
int islandCount = islandRootToIndex.Count;
var accumNormal = new Vector3[islandCount];
for (int t = 0; t < triCount; t++)
accumNormal[islandOf[t]] += triNormal[t];
var islandAxisU = new Vector3[islandCount];
var islandAxisV = new Vector3[islandCount];
for (int i = 0; i < islandCount; i++)
{
var n = accumNormal[i].normalized;
ComputeTangentBasis(n, out islandAxisU[i], out islandAxisV[i]);
}
var islandMinU = new float[islandCount];
var islandMinV = new float[islandCount];
var islandMaxU = new float[islandCount];
var islandMaxV = new float[islandCount];
for (int i = 0; i < islandCount; i++)
{
islandMinU[i] = islandMinV[i] = float.PositiveInfinity;
islandMaxU[i] = islandMaxV[i] = float.NegativeInfinity;
}
Vector2 ProjectUV(Vector3 local, Vector3 axisU, Vector3 axisV) =>
new Vector2(Vector3.Dot(local, axisU), Vector3.Dot(local, axisV));
for (int t = 0; t < triCount; t++)
{
int island = islandOf[t];
Vector2 a = ProjectUV(srcVerts[triI0[t]], islandAxisU[island], islandAxisV[island]);
Vector2 b = ProjectUV(srcVerts[triI1[t]], islandAxisU[island], islandAxisV[island]);
Vector2 c = ProjectUV(srcVerts[triI2[t]], islandAxisU[island], islandAxisV[island]);
islandMinU[island] = Mathf.Min(islandMinU[island], a.x, b.x, c.x);
islandMinV[island] = Mathf.Min(islandMinV[island], a.y, b.y, c.y);
islandMaxU[island] = Mathf.Max(islandMaxU[island], a.x, b.x, c.x);
islandMaxV[island] = Mathf.Max(islandMaxV[island], a.y, b.y, c.y);
}
int gridSize = Mathf.Max(1, Mathf.CeilToInt(Mathf.Sqrt(islandCount)));
float cellSize = 1f / gridSize;
float cellPad = cellSize * MaskUvPadding * 0.5f;
var islandCellOrigin = new Vector2[islandCount];
var islandScale = new float[islandCount];
for (int i = 0; i < islandCount; i++)
{
int col = i % gridSize;
int row = i / gridSize;
islandCellOrigin[i] = new Vector2(col * cellSize, row * cellSize);
float span = Mathf.Max(Mathf.Max(islandMaxU[i] - islandMinU[i], islandMaxV[i] - islandMinV[i]), 1e-5f);
float usableCell = cellSize - cellPad * 2f;
islandScale[i] = usableCell / span;
}
var newVerts = new List<Vector3>();
var newNormals = new List<Vector3>();
var newUvMask = new List<Vector2>();
var newUvTile = new List<Vector2>();
var subTriangleLists = new List<int>[subMeshCount];
for (int s = 0; s < subMeshCount; s++) subTriangleLists[s] = new List<int>();
for (int t = 0; t < triCount; t++)
{
int island = islandOf[t];
Vector2 MapAtlas(Vector3 local)
{
Vector2 uv = ProjectUV(local, islandAxisU[island], islandAxisV[island]);
Vector2 norm = new Vector2(uv.x - islandMinU[island], uv.y - islandMinV[island]) *
islandScale[island];
return islandCellOrigin[island] + new Vector2(cellPad, cellPad) + norm;
}
int i0 = triI0[t], i1 = triI1[t], i2 = triI2[t];
Vector3 p0 = srcVerts[i0], p1 = srcVerts[i1], p2 = srcVerts[i2];
int baseIndex = newVerts.Count;
newVerts.Add(p0);
newVerts.Add(p1);
newVerts.Add(p2);
newNormals.Add(hasNormals ? srcNormals[i0] : triNormal[t]);
newNormals.Add(hasNormals ? srcNormals[i1] : triNormal[t]);
newNormals.Add(hasNormals ? srcNormals[i2] : triNormal[t]);
newUvMask.Add(MapAtlas(p0));
newUvMask.Add(MapAtlas(p1));
newUvMask.Add(MapAtlas(p2));
newUvTile.Add(hasUvTile ? srcUvTile[i0] : Vector2.zero);
newUvTile.Add(hasUvTile ? srcUvTile[i1] : Vector2.zero);
newUvTile.Add(hasUvTile ? srcUvTile[i2] : Vector2.zero);
int subMesh = triSubMesh[t];
subTriangleLists[subMesh].Add(baseIndex);
subTriangleLists[subMesh].Add(baseIndex + 1);
subTriangleLists[subMesh].Add(baseIndex + 2);
}
mesh.Clear();
mesh.indexFormat = newVerts.Count > 65000
? UnityEngine.Rendering.IndexFormat.UInt32
: UnityEngine.Rendering.IndexFormat.UInt16;
mesh.SetVertices(newVerts);
mesh.SetNormals(newNormals);
mesh.SetUVs(0, newUvMask);
mesh.SetUVs(2, newUvTile);
mesh.subMeshCount = subMeshCount;
for (int s = 0; s < subMeshCount; s++)
mesh.SetTriangles(subTriangleLists[s], s);
mesh.RecalculateBounds();
if (!hasNormals) mesh.RecalculateNormals();
mesh.RecalculateTangents();
#if UNITY_EDITOR
Unwrapping.GenerateSecondaryUVSet(mesh);
#endif
}
}
}