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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-24 20:25:14 +02:00
1 parent cdeebf9243
commit 24a36c0e61
5 files changed
+509 -12

No files matched your search

+396
View File
@@ -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
+71
View File
@@ -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
/// <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;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 81a936ee72b5dab45b6c0f63237602cc
+38 -12
View File
@@ -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
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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);
+2
View File
@@ -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);