Pick the material of the hit submesh on multi-material renderers

PickFromForeignMaterial read rend.sharedMaterial, which is always slot 0, so any renderer with several materials (one per submesh) always sampled the first material's values. Resolve the submesh from the hit triangle (collider triangleIndex, or the recovered render-mesh triangle) and read sharedMaterials[submesh] (clamped) instead. Single-material renderers stay on slot 0, byte-identical to before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-30 08:41:57 +02:00
1 parent 06f5f33e63
commit 04c73623d3
1 file changed
+36 -5
+36 -5
View File
@@ -181,26 +181,35 @@ public partial class MeshPainter
Renderer rend = col.GetComponent<Renderer>();
if (!rend) rend = col.GetComponentInParent<Renderer>();
if (!rend) return false;
Material mat = rend.sharedMaterial;
if (!mat) return false;
// A UV at the hit is required to sample any material map texel-exact. hit.textureCoord is
// only populated for a NON-convex MeshCollider; convex MeshColliders and primitive colliders
// (Box/Sphere/Capsule/Terrain) return Vector2.zero. In that case recover the UV from the
// renderer's own mesh (ray vs. triangles), which works for any solid collider whose mesh is
// readable.
// readable. Alongside the UV we resolve which SUBMESH the hit triangle belongs to, so a
// multi-material renderer reads the sub-material covering the hit instead of always slot 0.
Vector2 uv;
bool hasUv;
int subMesh;
if (col is MeshCollider mc && !mc.convex && hit.triangleIndex >= 0)
{
uv = hit.textureCoord;
hasUv = true;
subMesh = SubMeshFromIndex(mc.sharedMesh, hit.triangleIndex * 3);
}
else
{
hasUv = TryRecoverMeshUv(rend, hit, out uv);
hasUv = TryRecoverMeshUv(rend, hit, out uv, out subMesh);
}
// One material per submesh: pick the slot the hit submesh maps to (clamped, so a renderer with
// fewer materials than submeshes still reads). Reads the shared material - never instances it.
Material[] mats = rend.sharedMaterials;
Material mat = (mats != null && mats.Length > 0)
? mats[Mathf.Clamp(subMesh, 0, mats.Length - 1)]
: null;
if (!mat) return false;
// A base map exists but no UV could be resolved (e.g. a non-readable mesh behind a convex
// collider): a texel-exact read is impossible, so defer to the rendered on-screen pixel,
// which at least reflects the texture - rather than silently picking the flat _BaseColor.
@@ -473,9 +482,10 @@ public partial class MeshPainter
/// a MeshFilter whose mesh is readable and UV-mapped; returns false otherwise so the caller can
/// fall back. A one-shot click cost, so the O(triangles) sweep is acceptable.
/// </summary>
private bool TryRecoverMeshUv(Renderer rend, RaycastHit hit, out Vector2 uv)
private bool TryRecoverMeshUv(Renderer rend, RaycastHit hit, out Vector2 uv, out int subMesh)
{
uv = default;
subMesh = 0;
if (!rend) return false;
MeshFilter mf = rend.GetComponent<MeshFilter>();
Mesh mesh = mf ? mf.sharedMesh : null;
@@ -519,9 +529,30 @@ public partial class MeshPainter
if (bestTri < 0) return false;
uv = uvs[tris[bestTri]] * bestBary.x + uvs[tris[bestTri + 1]] * bestBary.y + uvs[tris[bestTri + 2]] * bestBary.z;
subMesh = SubMeshFromIndex(mesh, bestTri); // bestTri is the offset into the flattened index buffer
return true;
}
/// <summary>
/// Resolves which submesh a triangle belongs to, given its offset into the mesh's flattened index
/// buffer (triangleNumber * 3, matching the <see cref="Mesh.triangles"/> layout). Submesh index then
/// maps 1:1 onto the renderer's material slots. Returns 0 for a single-material mesh (the common
/// case, no descriptor lookup) or when the mesh is missing.
/// </summary>
private static int SubMeshFromIndex(Mesh mesh, int indexOffset)
{
if (!mesh) return 0;
int subCount = mesh.subMeshCount;
if (subCount <= 1) return 0;
for (int s = 0; s < subCount; s++)
{
UnityEngine.Rendering.SubMeshDescriptor d = mesh.GetSubMesh(s);
if (indexOffset >= d.indexStart && indexOffset < d.indexStart + d.indexCount)
return s;
}
return 0;
}
/// <summary>
/// Moller-Trumbore ray/triangle intersection. On a hit, <paramref name="t"/> is the ray distance
/// and (<paramref name="u"/>, <paramref name="v"/>) are the barycentric weights of vertices b and