Reset the pipette toggle after a successful pick so each pick is a one-shot action (HUD toggle follows via the Picked sync). Split the wall-climb pose drag so the lean-out offset stays on LMB while body rotation moves to RMB, letting both be held independently. Also pulls in the terrain physics module and the user's scene setup for the terrain test area. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
236 lines
11 KiB
C#
236 lines
11 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public event Action Picked;
|
|
|
|
private bool _pickHeld; // edge tracking so a held button picks once, not every frame
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Picks from our own painted maps at the hit texel (bit-exact, backend-agnostic).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private bool PickFromForeignMaterial(RaycastHit hit)
|
|
{
|
|
Collider col = hit.collider;
|
|
if (!col) return false;
|
|
Renderer rend = col.GetComponent<Renderer>();
|
|
if (!rend) rend = col.GetComponentInParent<Renderer>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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. <paramref
|
|
/// name="srgb"/> selects the colour space so the returned value matches the readable path
|
|
/// (gamma for colour maps, linear for data maps).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|