From 04c73623d33cb78d30b440aac4acb5ed047ff009 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Tue, 30 Jun 2026 08:41:57 +0200 Subject: [PATCH] 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 --- Assets/Scripts/MeshPainter.Pick.cs | 41 ++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) 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