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;
// 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;
}
}