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);