Pipette samples all painted channel values (albedo, metallic, smoothness, occlusion, emission) at the clicked texel and adopts them into the brush with every opacity forced to 1, instead of painting. The screen raycast is extracted into a shared TryRaycastPaintMesh helper used by painting, preview and the pipette. A pipette toggle is added to the HUD panel prefab and wired in PaintHud. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
3.4 KiB
C#
72 lines
3.4 KiB
C#
using UnityEngine;
|
|
|
|
public partial class MeshPainter
|
|
{
|
|
// --- Pipette (eyedropper) ---
|
|
//
|
|
// A sampling MODE rather than a paint channel: while active, a left-click raycasts onto
|
|
// the mesh, reads ALL painted channel values at the hit texel (albedo, metallic,
|
|
// smoothness, occlusion, emission) and adopts them into the brush properties - with every
|
|
// opacity/alpha forced to 1 so re-painting reproduces the picked look at full strength.
|
|
// The values are read through ReadCanonical(), which already converts both backends (GPU
|
|
// RenderTextures / CPU Texture2D mirrors) into the same canonical material layout, so the
|
|
// pick matches what is stored without any new colour-space handling.
|
|
|
|
[Header("Brush - pipette (eyedropper)")]
|
|
[Tooltip("Pipette mode: a click samples ALL channel values at the hit point (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;
|
|
|
|
private bool _pickHeld; // edge tracking so a held button picks once, not every frame
|
|
|
|
/// <summary>
|
|
/// Raycasts at a screen position, finds the hit texel UV and copies every painted channel
|
|
/// value there into the brush (albedo color, metallic, smoothness, occlusion, emission
|
|
/// color), forcing all opacities/alphas to 1. Reads regardless of which channels are
|
|
/// currently toggled for painting.
|
|
/// </summary>
|
|
private void PickAtScreen(Vector2 screenPos)
|
|
{
|
|
if (!TryRaycastPaintMesh(screenPos, out RaycastHit hit)) return;
|
|
if (_verts == null || _uvs == null || _tris == null || hit.triangleIndex < 0) return;
|
|
|
|
int t = hit.triangleIndex;
|
|
if (t * 3 + 2 >= _tris.Length) return;
|
|
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;
|
|
|
|
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);
|
|
|
|
// "Set all alpha values to 1": full-strength adoption across every opacity control.
|
|
metallicSmoothnessOpacity = 1f;
|
|
occlusionOpacity = 1f;
|
|
}
|
|
}
|