MeshPainter: Rückseiten-Abweisung über Flächen-Normale bei großem Brush
Zusätzlich zum Kugel-Test wird die Oberflächen-Normale jedes Texels gegen die Brush-Normale geprüft (CPU/GPU/Preview). Neues Inspector-Feld "Brush Max Surface Angle" (90-180°, Default 120°). Bei fehlenden Mesh-Normalen Degradation zur reinen Kugel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d17baee85
commit
cdd9fa9532
7 files changed
+68
-8
No files matched your search
@@ -510,6 +510,7 @@ MonoBehaviour:
|
||||
furLength: 0.5
|
||||
brushSize: 25
|
||||
brushSizeScrollFactor: 0.15
|
||||
brushMaxSurfaceAngle: 135
|
||||
brushMasks:
|
||||
- {fileID: 2800000, guid: d07c01c40cc7be54da0ff3ae52484cd2, type: 3}
|
||||
- {fileID: 2800000, guid: ccc205272b051834eb39593f647c7bf1, type: 3}
|
||||
@@ -520,9 +521,6 @@ MonoBehaviour:
|
||||
brushRotation: 0
|
||||
paintOnMoveOnly: 1
|
||||
paintInterval: 0
|
||||
cornerGuard: 1
|
||||
cornerGuardAngle: 70
|
||||
cornerGuardMaxFraction: 0.7
|
||||
previewMode: 2
|
||||
previewOpacity: 1
|
||||
paintParticlesPrefab: {fileID: 723043959019929999, guid: 129e63016225faa4cb3c3ec86a7f66e5, type: 3}
|
||||
|
||||
@@ -167,6 +167,11 @@ public partial class MeshPainter
|
||||
|
||||
float invR = 1f / radiusLocal;
|
||||
float r2 = radiusLocal * radiusLocal;
|
||||
// Back-face rejection: skip texels whose surface normal faces away from the clicked
|
||||
// surface (the opposite side of thin geometry faces ~180deg away). cos(maxAngle); at
|
||||
// 180deg it is -1 -> test disabled. n is unit, so dot(normalize(texelN), n) >= cutoff.
|
||||
float nCutoff = BrushNormalCutoff;
|
||||
bool doNormalTest = _normals != null && nCutoff > -0.9999f;
|
||||
int triCount = _tris.Length / 3;
|
||||
float ts = textureSize;
|
||||
|
||||
@@ -226,6 +231,18 @@ public partial class MeshPainter
|
||||
// bleed across a seam / around a corner. Also guarantees |du|,|dv| <= r -> mu,mv in [0,1].
|
||||
if (du * du + dv * dv + dn * dn > r2) continue;
|
||||
|
||||
// Reject the opposite side of thin geometry (interpolated texel normal vs. brush normal).
|
||||
if (doNormalTest)
|
||||
{
|
||||
Vector3 tn0 = _normals[i0], tn1 = _normals[i1], tn2 = _normals[i2];
|
||||
float nx = a * tn0.x + b * tn1.x + c * tn2.x;
|
||||
float ny = a * tn0.y + b * tn1.y + c * tn2.y;
|
||||
float nz = a * tn0.z + b * tn1.z + c * tn2.z;
|
||||
float nl = nx * n.x + ny * n.y + nz * n.z;
|
||||
float nmag = Mathf.Sqrt(nx * nx + ny * ny + nz * nz);
|
||||
if (nmag > 1e-6f && nl < nCutoff * nmag) continue;
|
||||
}
|
||||
|
||||
float mu = du * invR * 0.5f + 0.5f;
|
||||
float mv = dv * invR * 0.5f + 0.5f;
|
||||
float maskOp = SampleMaskOpacity(mu, mv);
|
||||
|
||||
@@ -342,6 +342,7 @@ public partial class MeshPainter
|
||||
_brushMat.SetVector(BrushUId, u);
|
||||
_brushMat.SetVector(BrushVId, v);
|
||||
_brushMat.SetVector(BrushNormalId, n);
|
||||
_brushMat.SetFloat(BrushNormalCutoffId, BrushNormalCutoff);
|
||||
|
||||
if (mi == MAP_MSO)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ public partial class MeshPainter
|
||||
// BuildMeshCaches scratch: refilled per bake without allocating (with updateColliderEveryFrame this
|
||||
// runs per frame while painting; mesh.vertices/.triangles/.uv return fresh arrays per call).
|
||||
private readonly System.Collections.Generic.List<Vector3> _vertScratch = new System.Collections.Generic.List<Vector3>();
|
||||
private readonly System.Collections.Generic.List<Vector3> _normalScratch = new System.Collections.Generic.List<Vector3>();
|
||||
private int _meshCacheId; // mesh instance the topology/UV caches were built for (0 = none)
|
||||
|
||||
/// <summary>Reads vertices/triangles/UVs of the collider mesh and computes culling data.
|
||||
@@ -56,6 +57,16 @@ public partial class MeshPainter
|
||||
if (_verts == null || _verts.Length != vCount) _verts = new Vector3[vCount];
|
||||
_vertScratch.CopyTo(_verts);
|
||||
|
||||
// Per-vertex normals for the brush's back-face rejection (opposite side of thin geometry).
|
||||
// Re-baked each frame for skinned meshes; falls back to null (test disabled) if absent.
|
||||
m.GetNormals(_normalScratch);
|
||||
if (_normalScratch.Count == vCount)
|
||||
{
|
||||
if (_normals == null || _normals.Length != vCount) _normals = new Vector3[vCount];
|
||||
_normalScratch.CopyTo(_normals);
|
||||
}
|
||||
else _normals = null;
|
||||
|
||||
if (_meshCacheId != m.GetInstanceID())
|
||||
{
|
||||
_meshCacheId = m.GetInstanceID();
|
||||
|
||||
@@ -266,6 +266,21 @@ public partial class MeshPainter : MonoBehaviour
|
||||
[Range(0.01f, 1f)]
|
||||
public float brushSizeScrollFactor = 0.15f;
|
||||
|
||||
[Tooltip("Max angle (deg) between a texel's surface normal and the clicked surface for the brush to " +
|
||||
"still paint it. Prevents a large brush from bleeding onto the opposite side of thin geometry " +
|
||||
"(that side faces ~180deg away). Lower = stricter separation; 180 = disabled (paint everything " +
|
||||
"inside the sphere, old behavior). ~120 keeps corners/rounded edges while cutting the back face.")]
|
||||
[Range(90f, 180f)]
|
||||
public float brushMaxSurfaceAngle = 120f;
|
||||
|
||||
/// <summary>
|
||||
/// cos(brushMaxSurfaceAngle): a texel is painted only if dot(texelNormal, brushNormal) >= this.
|
||||
/// At 180deg the cutoff is -1 (always true) -> the normal test is effectively disabled. Returns -1
|
||||
/// when the mesh has no normals (test not possible) so the brush degrades to the plain sphere.
|
||||
/// </summary>
|
||||
private float BrushNormalCutoff =>
|
||||
_normals != null ? Mathf.Cos(brushMaxSurfaceAngle * Mathf.Deg2Rad) : -1f;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after Shift+wheel changes <see cref="brushSize"/> so the in-game HUD can re-sync its
|
||||
/// brush-size slider (the slider then re-clamps the value to its own configured range).
|
||||
@@ -372,6 +387,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
// Mesh caches (in local/collider space).
|
||||
private Vector3[] _verts;
|
||||
private Vector3[] _normals; // per-vertex normals (brush back-face rejection); null if unavailable
|
||||
private int[] _tris;
|
||||
private Vector2[] _uvs;
|
||||
private Vector3[] _triCenter; // centroid per triangle (CPU culling)
|
||||
@@ -464,6 +480,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int BrushUId = Shader.PropertyToID("_BrushU");
|
||||
private static readonly int BrushVId = Shader.PropertyToID("_BrushV");
|
||||
private static readonly int BrushNormalId = Shader.PropertyToID("_BrushNormal");
|
||||
private static readonly int BrushNormalCutoffId = Shader.PropertyToID("_BrushNormalCutoff");
|
||||
private static readonly int MaskTexId = Shader.PropertyToID("_MaskTex");
|
||||
private static readonly int PadId = Shader.PropertyToID("_Pad");
|
||||
private static readonly int PreviewOpacityId = Shader.PropertyToID("_PreviewOpacity");
|
||||
@@ -773,6 +790,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
_previewMat.SetVector(BrushUId, u);
|
||||
_previewMat.SetVector(BrushVId, v);
|
||||
_previewMat.SetVector(BrushNormalId, n);
|
||||
_previewMat.SetFloat(BrushNormalCutoffId, BrushNormalCutoff);
|
||||
|
||||
// Re-draw the collider mesh; the shader clips it to the brush box. The collider mesh is in
|
||||
// the same local space as the raycast hit and valid for skinned (baked) and static meshes.
|
||||
|
||||
@@ -51,18 +51,21 @@ Shader "Hidden/MeshPaintBrush"
|
||||
float3 _BrushV; // stamp up axis (unit, local space)
|
||||
float3 _BrushNormal; // surface normal at the hit (unit, local space)
|
||||
float _BrushRadius; // stamp half-extent in local mesh space
|
||||
float _BrushNormalCutoff; // cos(maxAngle): paint only if dot(texelN, brushN) >= this (-1 = off)
|
||||
sampler2D _MaskTex;
|
||||
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION; // local 3D position
|
||||
float3 normal : NORMAL; // local surface normal
|
||||
float2 uv : TEXCOORD0; // UV (target in the texture layout)
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float3 local : TEXCOORD0; // interpolated local 3D position
|
||||
float4 pos : SV_POSITION;
|
||||
float3 local : TEXCOORD0; // interpolated local 3D position
|
||||
float3 normal : TEXCOORD1; // interpolated local surface normal
|
||||
};
|
||||
|
||||
v2f vert (appdata v)
|
||||
@@ -76,6 +79,7 @@ Shader "Hidden/MeshPaintBrush"
|
||||
#endif
|
||||
o.pos = float4(p, 0.0, 1.0);
|
||||
o.local = v.vertex.xyz;
|
||||
o.normal = v.normal;
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -91,6 +95,11 @@ Shader "Hidden/MeshPaintBrush"
|
||||
// (both sides of a 3D edge) yet never bleeds onto far/adjacent surfaces past r.
|
||||
clip(_BrushRadius * _BrushRadius - (du * du + dv * dv + dn * dn));
|
||||
|
||||
// Back-face rejection: the opposite side of thin geometry faces ~180deg away,
|
||||
// so drop texels whose normal turns away from the clicked surface past the cutoff.
|
||||
float nl = dot(normalize(i.normal), _BrushNormal);
|
||||
clip(nl - _BrushNormalCutoff);
|
||||
|
||||
// Mask -> opacity. The sphere guarantees |du|,|dv| <= r -> uv in [0,1].
|
||||
float2 muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5;
|
||||
fixed4 m = tex2D(_MaskTex, muv);
|
||||
|
||||
@@ -43,6 +43,7 @@ Shader "Hidden/MeshPaintPreview"
|
||||
float3 _BrushV; // stamp up axis (unit, local space)
|
||||
float3 _BrushNormal; // surface normal at the hit (unit, local space)
|
||||
float _BrushRadius; // stamp half-extent in local mesh space
|
||||
float _BrushNormalCutoff; // cos(maxAngle): show only if dot(texelN, brushN) >= this (-1 = off)
|
||||
float _PreviewOpacity; // overall preview alpha multiplier (>1 saturates to fully opaque)
|
||||
float _PreviewMode; // 0 = on-surface (mesh), 1 = flat decal quad
|
||||
sampler2D _MaskTex;
|
||||
@@ -50,14 +51,16 @@ Shader "Hidden/MeshPaintPreview"
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float3 normal : NORMAL;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float3 local : TEXCOORD0; // object-space position (on-surface box test)
|
||||
float2 uv : TEXCOORD1; // quad uv (decal sampling)
|
||||
float4 pos : SV_POSITION;
|
||||
float3 local : TEXCOORD0; // object-space position (on-surface box test)
|
||||
float2 uv : TEXCOORD1; // quad uv (decal sampling)
|
||||
float3 normal : TEXCOORD2; // object-space normal (on-surface back-face test)
|
||||
};
|
||||
|
||||
v2f vert (appdata v)
|
||||
@@ -66,6 +69,7 @@ Shader "Hidden/MeshPaintPreview"
|
||||
o.pos = UnityObjectToClipPos(v.vertex);
|
||||
o.local = v.vertex.xyz;
|
||||
o.uv = v.uv;
|
||||
o.normal = v.normal;
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -80,6 +84,8 @@ Shader "Hidden/MeshPaintPreview"
|
||||
float dv = dot(d, _BrushV);
|
||||
float dn = dot(d, _BrushNormal);
|
||||
clip(_BrushRadius * _BrushRadius - (du * du + dv * dv + dn * dn));
|
||||
// Back-face rejection: hide the preview on the opposite side of thin geometry.
|
||||
clip(dot(normalize(i.normal), _BrushNormal) - _BrushNormalCutoff);
|
||||
muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5;
|
||||
}
|
||||
else
|
||||
|
||||
Reference in new issue
Block a user