diff --git a/Assets/Prefabs/PaintHudPanel.prefab b/Assets/Prefabs/PaintHudPanel.prefab
index d7f9c9e..d6bca44 100644
--- a/Assets/Prefabs/PaintHudPanel.prefab
+++ b/Assets/Prefabs/PaintHudPanel.prefab
@@ -615,6 +615,81 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
+--- !u!1 &2433608337256672725
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 7263878886564421122}
+ - component: {fileID: 1956171598706770752}
+ - component: {fileID: 8909106410246738347}
+ m_Layer: 0
+ m_Name: Check
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &7263878886564421122
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2433608337256672725}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 378367862261900739}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: -6, y: -6}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &1956171598706770752
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2433608337256672725}
+ m_CullTransparentMesh: 1
+--- !u!114 &8909106410246738347
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2433608337256672725}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.4, g: 0.75, b: 1, a: 1}
+ m_RaycastTarget: 0
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 0}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!1 &2575995739225439787
GameObject:
m_ObjectHideFlags: 0
@@ -798,6 +873,7 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
+ - {fileID: 1189839221186869986}
- {fileID: 5031715123348730660}
m_Father: {fileID: 7387928529902370415}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -1499,6 +1575,113 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
+--- !u!1 &4234799798620546418
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1189839221186869986}
+ - component: {fileID: 2649102957126940421}
+ - component: {fileID: 1564354898808692867}
+ m_Layer: 0
+ m_Name: Toggle_Pipette
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1189839221186869986
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4234799798620546418}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 378367862261900739}
+ - {fileID: 6727538668717060623}
+ m_Father: {fileID: 3665381071208701621}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &2649102957126940421
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4234799798620546418}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
+ m_IgnoreLayout: 0
+ m_MinWidth: -1
+ m_MinHeight: 22
+ m_PreferredWidth: -1
+ m_PreferredHeight: 22
+ m_FlexibleWidth: -1
+ m_FlexibleHeight: -1
+ m_LayoutPriority: 1
+--- !u!114 &1564354898808692867
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4234799798620546418}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Toggle
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 7760795866426062704}
+ toggleTransition: 1
+ graphic: {fileID: 8909106410246738347}
+ m_Group: {fileID: 0}
+ onValueChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_IsOn: 0
--- !u!1 &4533464575680292808
GameObject:
m_ObjectHideFlags: 0
@@ -2026,6 +2209,82 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!1 &5103111695676368396
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 378367862261900739}
+ - component: {fileID: 2394512417631225974}
+ - component: {fileID: 7760795866426062704}
+ m_Layer: 0
+ m_Name: Box
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &378367862261900739
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5103111695676368396}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 7263878886564421122}
+ m_Father: {fileID: 1189839221186869986}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.5}
+ m_AnchorMax: {x: 0, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 18, y: 18}
+ m_Pivot: {x: 0, y: 0.5}
+--- !u!222 &2394512417631225974
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5103111695676368396}
+ m_CullTransparentMesh: 1
+--- !u!114 &7760795866426062704
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5103111695676368396}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.2, g: 0.2, b: 0.24, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 0}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!1 &5257521354364959765
GameObject:
m_ObjectHideFlags: 0
@@ -2684,6 +2943,143 @@ CanvasGroup:
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
+--- !u!1 &6216940102955934110
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6727538668717060623}
+ - component: {fileID: 2246242343579983528}
+ - component: {fileID: 2146181045640304694}
+ m_Layer: 0
+ m_Name: Label
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &6727538668717060623
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6216940102955934110}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1189839221186869986}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 12, y: 0}
+ m_SizeDelta: {x: -24, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &2246242343579983528
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6216940102955934110}
+ m_CullTransparentMesh: 1
+--- !u!114 &2146181045640304694
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6216940102955934110}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 0
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: Pipette
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 964ae5cd392ba08499b88de012ff8213, type: 2}
+ m_sharedMaterial: {fileID: 8234806113954670000, guid: 964ae5cd392ba08499b88de012ff8213, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 14
+ m_fontSizeBase: 14
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 18
+ m_fontSizeMax: 72
+ m_fontStyle: 0
+ m_HorizontalAlignment: 1
+ m_VerticalAlignment: 8192
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 0
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 0
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: 0, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6789245187076848256
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Scripts/MeshPainter.Pick.cs b/Assets/Scripts/MeshPainter.Pick.cs
new file mode 100644
index 0000000..f4465e8
--- /dev/null
+++ b/Assets/Scripts/MeshPainter.Pick.cs
@@ -0,0 +1,71 @@
+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
+
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+}
diff --git a/Assets/Scripts/MeshPainter.Pick.cs.meta b/Assets/Scripts/MeshPainter.Pick.cs.meta
new file mode 100644
index 0000000..7c0bdc3
--- /dev/null
+++ b/Assets/Scripts/MeshPainter.Pick.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 81a936ee72b5dab45b6c0f63237602cc
\ No newline at end of file
diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs
index ddc045a..b640579 100644
--- a/Assets/Scripts/MeshPainter.cs
+++ b/Assets/Scripts/MeshPainter.cs
@@ -606,6 +606,22 @@ public partial class MeshPainter : MonoBehaviour
if (pressed && IsPointerOverUi())
pressed = false;
+ // Pipette (eyedropper): a fresh click samples ALL channel values at the hit point
+ // and adopts them into the brush instead of painting. It takes over the click
+ // entirely (no painting, no brush preview ring while active).
+ if (pipette)
+ {
+ if (pressed)
+ {
+ if (!_pickHeld) PickAtScreen(screenPos);
+ _pickHeld = true;
+ }
+ else _pickHeld = false;
+ _hasLastScreen = false; // keep the paint stroke state clean for when pipette turns off
+ return;
+ }
+ _pickHeld = false;
+
// Brush preview (size + mask) at the hover point - independent of painting,
// so you see the brush before pressing the button.
if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera)
@@ -691,22 +707,19 @@ public partial class MeshPainter : MonoBehaviour
}
///
- /// Raycasts at a screen position against the paint mesh and computes the brush
- /// placement: hit point (local space), local 3D radius (constant visual size via UV
- /// density) and the orthonormal tangent frame (u/v/n). Shared by painting and preview.
+ /// Raycasts at a screen position and returns the nearest hit that belongs to THIS paint
+ /// mesh. A plain raycast would return only the closest collider, so anything in front of
+ /// the mesh (the player capsule, props, the camera-collision proxy) could block it; we
+ /// scan all hits and keep the nearest on our own collider. Shared by painting, preview
+ /// and the pipette.
///
- private bool TryComputeBrushPlacement(Vector2 screenPos, out Vector3 pLocal, out float radiusLocal,
- out Vector3 u, out Vector3 v, out Vector3 nLocal)
+ private bool TryRaycastPaintMesh(Vector2 screenPos, out RaycastHit hit)
{
- pLocal = default; radiusLocal = 0f; u = default; v = default; nLocal = default;
+ hit = default;
+ if (!paintCamera) return false;
Ray ray = paintCamera.ScreenPointToRay(screenPos);
-
- // Pick the nearest hit that belongs to the paint mesh. A plain raycast would
- // return only the closest collider, so anything in front of the mesh (the
- // player capsule, props, the camera-collision proxy) could block painting.
int hitCount = Physics.RaycastNonAlloc(ray, _rayHits, 1000f, ~0, QueryTriggerInteraction.Ignore);
- RaycastHit hit = default;
float nearest = float.MaxValue;
bool found = false;
for (int i = 0; i < hitCount; i++)
@@ -719,7 +732,20 @@ public partial class MeshPainter : MonoBehaviour
found = true;
}
}
- if (!found) return false;
+ return found;
+ }
+
+ ///
+ /// Raycasts at a screen position against the paint mesh and computes the brush
+ /// placement: hit point (local space), local 3D radius (constant visual size via UV
+ /// density) and the orthonormal tangent frame (u/v/n). Shared by painting and preview.
+ ///
+ private bool TryComputeBrushPlacement(Vector2 screenPos, out Vector3 pLocal, out float radiusLocal,
+ out Vector3 u, out Vector3 v, out Vector3 nLocal)
+ {
+ pLocal = default; radiusLocal = 0f; u = default; v = default; nLocal = default;
+
+ if (!TryRaycastPaintMesh(screenPos, out RaycastHit hit)) return false;
if (_verts == null || hit.triangleIndex < 0) return false;
pLocal = transform.InverseTransformPoint(hit.point);
diff --git a/Assets/Scripts/PaintHud.cs b/Assets/Scripts/PaintHud.cs
index dd47731..5ba10ac 100644
--- a/Assets/Scripts/PaintHud.cs
+++ b/Assets/Scripts/PaintHud.cs
@@ -48,6 +48,7 @@ public class PaintHud : MonoBehaviour
private const string ToggleOcclusion = "Toggle_Occlusion";
private const string ToggleEmission = "Toggle_Emission";
private const string ToggleSpray = "Toggle_Spray";
+ private const string TogglePipette = "Toggle_Pipette";
private const string PickerAlbedo = "Picker_Albedo";
private const string PickerEmission = "Picker_Emission";
private const string SliderMetallic = "Slider_Metallic";
@@ -169,6 +170,7 @@ public class PaintHud : MonoBehaviour
WireToggle(panel, ToggleOcclusion, painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; });
WireToggle(panel, ToggleEmission, painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; });
WireToggle(panel, ToggleSpray, painter && painter.spray, v => { if (painter) painter.spray = v; });
+ WireToggle(panel, TogglePipette, painter && painter.pipette, v => { if (painter) painter.pipette = v; });
// Dim a channel panel to 0.5 alpha while its toggle is off, full alpha while on.
WirePanelFade(panel, PanelAlbedo, ToggleAlbedo);