using System; using UnityEngine; public partial class MeshPainter { // --- Pipette (eyedropper) --- // // A sampling MODE rather than a paint channel: while active, a left-click raycasts onto // ANY surface (not just the paint mesh) and reads every channel value at the hit point // (albedo, metallic, smoothness, occlusion, emission) into the brush properties - with // every opacity/alpha forced to 1 so re-painting reproduces the picked look at full // strength. // // Two read paths, chosen by what was hit: // - Our own paint mesh: read texel-exact from the painted maps via ReadCanonical(), // which converts both backends (GPU RenderTextures / CPU Texture2D mirrors) into the // same canonical layout, so the pick matches what is stored bit-for-bit. // - Any foreign object: read texel-exact from its material maps at the hit UV // (BaseMap/MetallicGloss/Occlusion/Emission) combined with the material scalars, per // the URP/Lit conventions, falling back to scalar properties where a map is absent or // the collider carries no UV (non-MeshCollider). [Header("Brush - pipette (eyedropper)")] [Tooltip("Pipette mode: a click samples ALL channel values at the hit point on ANY surface " + "(albedo, metallic, smoothness, occlusion, emission) and adopts them into the brush " + "(all alphas set to 1) instead of painting. Held button picks only once.")] public bool pipette = false; /// /// Raised after a successful pick, once the brush has adopted the sampled values and all four /// paint-channel flags have been enabled. The in-game HUD listens to re-sync its widgets. /// public event Action Picked; private bool _pickHeld; // edge tracking so a held button picks once, not every frame /// /// Raycasts at a screen position against ANY collider, samples every channel value at the hit /// (texel-exact where possible) and copies it into the brush, forcing all opacities/alphas to /// 1 and enabling every paint channel. Reads regardless of which channels are currently toggled. /// private void PickAtScreen(Vector2 screenPos) { if (!TryRaycastAny(screenPos, out RaycastHit hit)) return; bool ok = (hit.collider == _collider) ? PickFromPaintTextures(hit) : PickFromForeignMaterial(hit); if (!ok) return; // "Set all alpha values to 1": full-strength adoption across every opacity control. metallicSmoothnessOpacity = 1f; occlusionOpacity = 1f; // Picking a whole material implies repainting all of it, so enable every channel. paintAlbedo = true; paintMetallicSmoothness = true; paintOcclusion = true; paintEmission = true; // A pick is a one-shot action: leave pipette mode automatically so the next click paints. pipette = false; // Let listeners (the in-game HUD) refresh their widgets to the adopted values + flags. Picked?.Invoke(); } /// /// Picks from our own painted maps at the hit texel (bit-exact, backend-agnostic). /// private bool PickFromPaintTextures(RaycastHit hit) { if (_verts == null || _uvs == null || _tris == null || hit.triangleIndex < 0) return false; int t = hit.triangleIndex; if (t * 3 + 2 >= _tris.Length) return false; int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; // UV at the hit via barycentric interpolation of the cached UVs - same UV space the // brush paints into, so the sampled texel is exactly the one that would be painted. Vector3 bc = hit.barycentricCoordinate; Vector2 uv = _uvs[i0] * bc.x + _uvs[i1] * bc.y + _uvs[i2] * bc.z; int px = Mathf.Clamp(Mathf.FloorToInt(uv.x * textureSize), 0, textureSize - 1); int py = Mathf.Clamp(Mathf.FloorToInt(uv.y * textureSize), 0, textureSize - 1); int idx = py * textureSize + px; // ReadCanonical gives material layout for either backend: // albedo -> sRGB RGB // MSO -> R = metallic, G = occlusion, A = smoothness (linear data) // emission -> sRGB RGB Color32[] alb = ReadCanonical(MAP_ALBEDO); Color32[] mso = ReadCanonical(MAP_MSO); Color32[] em = ReadCanonical(MAP_EM); if (idx < 0 || idx >= alb.Length || idx >= mso.Length || idx >= em.Length) return false; Color32 a = alb[idx]; Color32 m = mso[idx]; Color32 e = em[idx]; // sRGB bytes map straight back into the gamma-space brush colours (round-trips exactly). brushColor = new Color(a.r / 255f, a.g / 255f, a.b / 255f, 1f); metallic = m.r / 255f; occlusion = m.g / 255f; smoothness = m.a / 255f; emissionColor = new Color(e.r / 255f, e.g / 255f, e.b / 255f, 1f); return true; } /// /// Picks from a foreign object's material at the hit UV: texel-exact texture samples /// (where the surface has a MeshCollider and the map exists) combined with the material /// scalars per the URP/Lit conventions, with scalar fallbacks everywhere else. /// private bool PickFromForeignMaterial(RaycastHit hit) { Collider col = hit.collider; if (!col) return false; Renderer rend = col.GetComponent(); if (!rend) rend = col.GetComponentInParent(); if (!rend) return false; Material mat = rend.sharedMaterial; if (!mat) return false; // textureCoord is only meaningful for a MeshCollider; other collider shapes carry no UV, // so we fall back to the material's uniform scalar values for those. bool hasUv = col is MeshCollider; Vector2 uv = hasUv ? hit.textureCoord : Vector2.zero; // --- Albedo: BaseMap (sRGB) * BaseColor --- Color baseCol = GetColorProp(mat, "_BaseColor", "_Color", Color.white); Texture baseTex = GetTexProp(mat, "_BaseMap", "_MainTex"); Color albedo = baseCol; if (hasUv && baseTex) albedo = SampleTexel(baseTex, ApplyST(mat, "_BaseMap", "_MainTex", uv), true) * baseCol; brushColor = new Color(Mathf.Clamp01(albedo.r), Mathf.Clamp01(albedo.g), Mathf.Clamp01(albedo.b), 1f); // --- Metallic / Smoothness: MetallicGloss map (linear) --- // URP/Lit: metallic = map.r ; smoothness = map.a * _Smoothness (or _Metallic/_Glossiness scalars). float metallicVal = GetFloatProp(mat, "_Metallic", null, 0f); float smoothVal = GetFloatProp(mat, "_Smoothness", "_Glossiness", 0.5f); Texture mgTex = GetTexProp(mat, "_MetallicGlossMap", null); if (hasUv && mgTex) { Color mg = SampleTexel(mgTex, ApplyST(mat, "_MetallicGlossMap", null, uv), false); metallicVal = mg.r; smoothVal = mg.a * smoothVal; } metallic = Mathf.Clamp01(metallicVal); smoothness = Mathf.Clamp01(smoothVal); // --- Occlusion: OcclusionMap.g (linear), lerp(1, g, strength) --- float occStrength = GetFloatProp(mat, "_OcclusionStrength", null, 1f); float occ = 1f; Texture occTex = GetTexProp(mat, "_OcclusionMap", null); if (hasUv && occTex) occ = Mathf.Lerp(1f, SampleTexel(occTex, ApplyST(mat, "_OcclusionMap", null, uv), false).g, occStrength); occlusion = Mathf.Clamp01(occ); // --- Emission: EmissionMap (sRGB) * EmissionColor --- Color emColor = GetColorProp(mat, "_EmissionColor", null, Color.black); Texture emTex = GetTexProp(mat, "_EmissionMap", null); Color emCol = emColor; if (hasUv && emTex) emCol = SampleTexel(emTex, ApplyST(mat, "_EmissionMap", null, uv), true) * emColor; emissionColor = new Color(Mathf.Clamp01(emCol.r), Mathf.Clamp01(emCol.g), Mathf.Clamp01(emCol.b), 1f); return true; } // --- Material read helpers (name-tolerant: URP and legacy Standard) --- private static Color GetColorProp(Material m, string nameA, string nameB, Color fallback) { if (m.HasProperty(nameA)) return m.GetColor(nameA); if (nameB != null && m.HasProperty(nameB)) return m.GetColor(nameB); return fallback; } private static float GetFloatProp(Material m, string nameA, string nameB, float fallback) { if (m.HasProperty(nameA)) return m.GetFloat(nameA); if (nameB != null && m.HasProperty(nameB)) return m.GetFloat(nameB); return fallback; } private static Texture GetTexProp(Material m, string nameA, string nameB) { if (m.HasProperty(nameA)) { Texture t = m.GetTexture(nameA); if (t) return t; } if (nameB != null && m.HasProperty(nameB)) { Texture t = m.GetTexture(nameB); if (t) return t; } return null; } private static Vector2 ApplyST(Material m, string nameA, string nameB, Vector2 uv) { string prop = m.HasProperty(nameA) ? nameA : (nameB != null && m.HasProperty(nameB) ? nameB : null); if (prop == null) return uv; Vector2 s = m.GetTextureScale(prop); Vector2 o = m.GetTextureOffset(prop); return new Vector2(uv.x * s.x + o.x, uv.y * s.y + o.y); } /// /// Samples a single texel from a texture at a UV (texel-exact). Uses GetPixelBilinear when the /// texture is CPU-readable; otherwise blits it to a temporary RenderTexture and reads back the /// one texel, so it works regardless of the texture's "Read/Write" import flag. selects the colour space so the returned value matches the readable path /// (gamma for colour maps, linear for data maps). /// private static Color SampleTexel(Texture tex, Vector2 uv, bool srgb) { Texture2D t2d = tex as Texture2D; if (t2d != null && t2d.isReadable) return t2d.GetPixelBilinear(uv.x, uv.y); int w = Mathf.Max(1, tex.width), h = Mathf.Max(1, tex.height); var rw = srgb ? RenderTextureReadWrite.sRGB : RenderTextureReadWrite.Linear; RenderTexture rt = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, rw); RenderTexture prev = RenderTexture.active; Graphics.Blit(tex, rt); RenderTexture.active = rt; var tmp = new Texture2D(1, 1, TextureFormat.RGBA32, false, !srgb); int px = Mathf.Clamp(Mathf.FloorToInt(Mathf.Repeat(uv.x, 1f) * w), 0, w - 1); int py = Mathf.Clamp(Mathf.FloorToInt(Mathf.Repeat(uv.y, 1f) * h), 0, h - 1); tmp.ReadPixels(new Rect(px, py, 1, 1), 0, 0, false); tmp.Apply(false); Color c = tmp.GetPixel(0, 0); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt); if (Application.isPlaying) Destroy(tmp); else DestroyImmediate(tmp); return c; } }