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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-01 20:35:03 +02:00
1 parent 410f9808b5
commit 523bfd3b0e
1 file changed
+15 -21
+15 -21
View File
@@ -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
/// <summary>
/// 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.
/// </summary>
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.