From 24a36c0e61518773ec8bc7a8bacc0d0dad1f78a4 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 24 Jun 2026 20:25:14 +0200 Subject: [PATCH] Add pipette (eyedropper) brush mode and wire it into the HUD 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 --- Assets/Prefabs/PaintHudPanel.prefab | 396 ++++++++++++++++++++++++ Assets/Scripts/MeshPainter.Pick.cs | 71 +++++ Assets/Scripts/MeshPainter.Pick.cs.meta | 2 + Assets/Scripts/MeshPainter.cs | 50 ++- Assets/Scripts/PaintHud.cs | 2 + 5 files changed, 509 insertions(+), 12 deletions(-) create mode 100644 Assets/Scripts/MeshPainter.Pick.cs create mode 100644 Assets/Scripts/MeshPainter.Pick.cs.meta 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);