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(out var renderer)) return; _renderMaterial = renderer.material; _renderMaterial.SetTexture(MaskMap, _maskRT); } /// /// Нейросеть написала и этот метод, слишком много математики мне он не нравится. Он заставляет игру лагать. Нужно сделать проще. /// Хотябы комментарии оставлены человеком. /// Я профнепригоден. /// /// /// /// 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 data = request.GetData(); 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(); 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(); 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; /// /// Этот большой и сложный метод написан нейросетью Claude Sonnet 5 Medium. Я профнепригоден /// /// 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(); 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(); var triI0 = new List(); var triI1 = new List(); var triI2 = new List(); var triCanonA = new List(); var triCanonB = new List(); var triCanonC = new List(); var triNormal = new List(); 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 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(); 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(); var newNormals = new List(); var newUvMask = new List(); var newUvTile = new List(); var subTriangleLists = new List[subMeshCount]; for (int s = 0; s < subMeshCount; s++) subTriangleLists[s] = new List(); 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 } } }