diff --git a/Assets/Scripts/MeshPainter.Pick.cs b/Assets/Scripts/MeshPainter.Pick.cs index b0ae17e..f3a0630 100644 --- a/Assets/Scripts/MeshPainter.Pick.cs +++ b/Assets/Scripts/MeshPainter.Pick.cs @@ -181,26 +181,35 @@ public partial class MeshPainter Renderer rend = col.GetComponent(); if (!rend) rend = col.GetComponentInParent(); 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. /// - 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(); 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; } + /// + /// Resolves which submesh a triangle belongs to, given its offset into the mesh's flattened index + /// buffer (triangleNumber * 3, matching the 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. + /// + 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; + } + /// /// Moller-Trumbore ray/triangle intersection. On a hit, is the ray distance /// and (, ) are the barycentric weights of vertices b and