From 523bfd3b0e5f83356a61c90ef9904c7e29ad1183 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 1 Jul 2026 20:35:03 +0200 Subject: [PATCH] Fix brush size varying with UV density; decouple preview opacity Brush radius now derives from the mesh-wide average UV density instead of the hit triangle's own density. Per-triangle density varied up to ~7.6x across the mesh (spiking at UV seams and stretched islands), so the same brushSize painted a visibly larger patch on seam triangles than on evenly-mapped ones. A single global scale keeps the brush the same physical size everywhere; the stamp remains a 3D box, so seams are still bridged. Also decouple the hover preview opacity from the brush albedo alpha: the preview now stays fully opaque and is scaled only by the Preview Opacity property, since the ring is a size/shape indicator, not a paint-strength preview. Co-Authored-By: Claude Opus 4.8 --- Assets/Scripts/MeshPainter.cs | 36 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs index a05fd81..dd539d0 100644 --- a/Assets/Scripts/MeshPainter.cs +++ b/Assets/Scripts/MeshPainter.cs @@ -669,7 +669,10 @@ public partial class MeshPainter : MonoBehaviour // Spray paints scattered dots, but the preview shows the round footprint (the area // the dots land in), so the hover ring stays a stable, readable size indicator. _previewMat.SetTexture(MaskTexId, spray ? _proceduralMask : _resolvedMask); - _previewMat.SetFloat(PreviewOpacityId, Mathf.Max(0f, previewOpacity) * Mathf.Clamp01(brushColor.a)); + // Preview stays fully opaque regardless of the brush's albedo alpha; only the + // Preview Opacity property scales it (the hover ring is a size/shape indicator, + // not a paint-strength preview). + _previewMat.SetFloat(PreviewOpacityId, Mathf.Max(0f, previewOpacity)); int layer = gameObject.layer; @@ -999,8 +1002,9 @@ public partial class MeshPainter : MonoBehaviour /// /// Raycasts at a screen position against the paint mesh and computes the brush - /// placement: hit point (local space), local 3D radius (constant visual size via UV - /// density) and the orthonormal tangent frame (u/v/n). Shared by painting and preview. + /// placement: hit point (local space), local 3D radius (constant physical size from the + /// mesh-wide average UV density) and the orthonormal tangent frame (u/v/n). Shared by + /// painting and preview. /// private bool TryComputeBrushPlacement(Vector2 screenPos, out Vector3 pLocal, out float radiusLocal, out Vector3 u, out Vector3 v, out Vector3 nLocal, out int hitTri) @@ -1011,25 +1015,15 @@ public partial class MeshPainter : MonoBehaviour if (_verts == null || hit.triangleIndex < 0) return false; pLocal = transform.InverseTransformPoint(hit.point); + hitTri = hit.triangleIndex; - // Convert the brush radius (texels) into a local 3D radius using the hit - // triangle's UV density so the brush keeps a constant visual size. - float scale = _avgUvToLocal; - int t = hit.triangleIndex; - hitTri = t; - if (t * 3 + 2 < _tris.Length) - { - int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; - float area3D = 0.5f * Vector3.Cross(_verts[i1] - _verts[i0], _verts[i2] - _verts[i0]).magnitude; - float areaUv = 0.5f * Mathf.Abs(Cross2D(_uvs[i1] - _uvs[i0], _uvs[i2] - _uvs[i0])); - if (areaUv > 1e-9f) - { - float s = Mathf.Sqrt(area3D / areaUv); - scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f); // guard degenerate UVs - } - } - - radiusLocal = (brushSize / (float)textureSize) * scale; + // Brush radius (texels) -> constant local 3D radius via the mesh-wide average UV + // density. Deliberately NOT the hit triangle's own density: that varied up to ~7.6x + // across the mesh (spiking at UV seams / stretched islands), so the same brushSize + // painted a visibly larger patch on seam triangles than on evenly-mapped ones. A single + // global scale keeps the brush the same physical size everywhere; the stamp is still a + // 3D box, so seams are bridged as before. + radiusLocal = (brushSize / (float)textureSize) * _avgUvToLocal; if (radiusLocal <= 0f) return false; // Tangent-plane frame for the square stamp / mask projection.