From 9df8c6076d1a81a6530397516e89c2890ebaefaf Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 24 Jun 2026 21:05:35 +0200 Subject: [PATCH] Raycast any object for pipette and read foreign materials texel-accurately The pipette now raycasts the nearest collider of any object, not just the paint mesh. On a foreign hit it reads all channels texel-accurately from the object's material maps (BaseMap/MetallicGloss/Occlusion/Emission) combined with the material scalars per URP/Lit conventions, falling back to scalar properties where a map is missing or the collider yields no UV. Includes scene test props and materials for verifying foreign-object picking. Co-Authored-By: Claude Opus 4.8 --- Assets/Materials.meta | 8 + Assets/Materials/Blue Mirror.mat | 137 ++++++++++ Assets/Materials/Blue Mirror.mat.meta | 8 + Assets/Materials/Red Metal.mat | 137 ++++++++++ Assets/Materials/Red Metal.mat.meta | 8 + Assets/Materials/Yellow Glow.mat | 138 ++++++++++ Assets/Materials/Yellow Glow.mat.meta | 8 + Assets/Scenes/SampleScene.unity | 350 +++++++++++++++++++++++++- Assets/Scripts/MeshPainter.Pick.cs | 197 +++++++++++++-- Assets/Scripts/MeshPainter.cs | 12 + 10 files changed, 976 insertions(+), 27 deletions(-) create mode 100644 Assets/Materials.meta create mode 100644 Assets/Materials/Blue Mirror.mat create mode 100644 Assets/Materials/Blue Mirror.mat.meta create mode 100644 Assets/Materials/Red Metal.mat create mode 100644 Assets/Materials/Red Metal.mat.meta create mode 100644 Assets/Materials/Yellow Glow.mat create mode 100644 Assets/Materials/Yellow Glow.mat.meta diff --git a/Assets/Materials.meta b/Assets/Materials.meta new file mode 100644 index 0000000..789acc1 --- /dev/null +++ b/Assets/Materials.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9977e3f116d56d546ac6d24e57d3684a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/Blue Mirror.mat b/Assets/Materials/Blue Mirror.mat new file mode 100644 index 0000000..218cbf8 --- /dev/null +++ b/Assets/Materials/Blue Mirror.mat @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Blue Mirror + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 1 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 1 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.2591703, g: 0.22327036, b: 1, a: 1} + - _Color: {r: 0.25917026, g: 0.22327033, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &5288681647234694520 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion + version: 10 diff --git a/Assets/Materials/Blue Mirror.mat.meta b/Assets/Materials/Blue Mirror.mat.meta new file mode 100644 index 0000000..547062f --- /dev/null +++ b/Assets/Materials/Blue Mirror.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 27e7e012d216e0143b94df13179d62dc +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/Red Metal.mat b/Assets/Materials/Red Metal.mat new file mode 100644 index 0000000..1ade886 --- /dev/null +++ b/Assets/Materials/Red Metal.mat @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Red Metal + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 1 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.75 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 0, b: 0, a: 1} + - _Color: {r: 1, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &5288681647234694520 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion + version: 10 diff --git a/Assets/Materials/Red Metal.mat.meta b/Assets/Materials/Red Metal.mat.meta new file mode 100644 index 0000000..d571e3b --- /dev/null +++ b/Assets/Materials/Red Metal.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8d826f81581c9314fb1c2cd886cd78c1 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/Yellow Glow.mat b/Assets/Materials/Yellow Glow.mat new file mode 100644 index 0000000..a27cea7 --- /dev/null +++ b/Assets/Materials/Yellow Glow.mat @@ -0,0 +1,138 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Yellow Glow + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _EMISSION + m_InvalidKeywords: [] + m_LightmapFlags: 2 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: _BaseColor _BaseMap + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 0.85953015, b: 0.4433961, a: 1} + - _Color: {r: 1, g: 0.85953015, b: 0.44339606, a: 1} + - _EmissionColor: {r: 89.45407, g: 89.45407, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &5288681647234694520 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion + version: 10 diff --git a/Assets/Materials/Yellow Glow.mat.meta b/Assets/Materials/Yellow Glow.mat.meta new file mode 100644 index 0000000..99afacc --- /dev/null +++ b/Assets/Materials/Yellow Glow.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3bc5b774ad58c8740b5705e96c54ade6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 5bcbbf3..a9a7342 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -446,6 +446,7 @@ MonoBehaviour: brushShader: {fileID: 0} dilateShader: {fileID: 0} packMsShader: {fileID: 0} + pipette: 0 spray: 0 sprayDots: 24 sprayDotSizeMin: 0.05 @@ -460,7 +461,9 @@ MonoBehaviour: brushColor: {r: 0.6, g: 0.6, b: 1, a: 1} metallic: 1 smoothness: 1 + metallicSmoothnessOpacity: 1 occlusion: 1 + occlusionOpacity: 1 emissionColor: {r: 1, g: 0.6, b: 0, a: 1} emissionIntensity: 200 brushSize: 100 @@ -1352,6 +1355,118 @@ Transform: m_CorrespondingSourceObject: {fileID: 6779641804511181039, guid: 930b7a19d446f4b4c9c810e37e84e625, type: 3} m_PrefabInstance: {fileID: 788444} m_PrefabAsset: {fileID: 0} +--- !u!1 &468175225 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 468175226} + - component: {fileID: 468175229} + - component: {fileID: 468175228} + - component: {fileID: 468175227} + m_Layer: 0 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &468175226 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 468175225} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -1, y: 0.5, z: 1} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 686187287} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!135 &468175227 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 468175225} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &468175228 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 468175225} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 8d826f81581c9314fb1c2cd886cd78c1, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &468175229 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 468175225} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} --- !u!4 &523455893 stripped Transform: m_CorrespondingSourceObject: {fileID: 4597260953850914690, guid: 930b7a19d446f4b4c9c810e37e84e625, type: 3} @@ -1419,6 +1534,7 @@ MonoBehaviour: brushShader: {fileID: 0} dilateShader: {fileID: 0} packMsShader: {fileID: 0} + pipette: 0 spray: 0 sprayDots: 24 sprayDotSizeMin: 0.05 @@ -1433,7 +1549,9 @@ MonoBehaviour: brushColor: {r: 0.6, g: 0.6, b: 1, a: 1} metallic: 1 smoothness: 1 + metallicSmoothnessOpacity: 1 occlusion: 1 + occlusionOpacity: 1 emissionColor: {r: 1, g: 0.6, b: 0, a: 1} emissionIntensity: 20 brushSize: 50 @@ -1492,6 +1610,9 @@ Transform: - {fileID: 1478475841} - {fileID: 1746495279} - {fileID: 200502122} + - {fileID: 468175226} + - {fileID: 1504466455} + - {fileID: 829281441} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!4 &691617530 stripped @@ -1762,6 +1883,118 @@ PrefabInstance: insertIndex: -1 addedObject: {fileID: 682237521} m_SourcePrefab: {fileID: 100100000, guid: 554c0bdf86263754d862d7effd04045b, type: 3} +--- !u!1 &829281440 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 829281441} + - component: {fileID: 829281444} + - component: {fileID: 829281443} + - component: {fileID: 829281442} + m_Layer: 0 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &829281441 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 829281440} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -2, y: 0.5, z: 1} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 686187287} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!135 &829281442 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 829281440} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &829281443 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 829281440} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 27e7e012d216e0143b94df13179d62dc, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &829281444 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 829281440} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &832575517 GameObject: m_ObjectHideFlags: 0 @@ -2635,6 +2868,118 @@ Transform: m_Children: [] m_Father: {fileID: 686187287} m_LocalEulerAnglesHint: {x: 60, y: 0, z: 0} +--- !u!1 &1504466454 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1504466455} + - component: {fileID: 1504466458} + - component: {fileID: 1504466457} + - component: {fileID: 1504466456} + m_Layer: 0 + m_Name: Sphere (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1504466455 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1504466454} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1.5, y: 0.5, z: 1} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 686187287} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!135 &1504466456 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1504466454} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1504466457 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1504466454} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 3bc5b774ad58c8740b5705e96c54ade6, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1504466458 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1504466454} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &1516578026 GameObject: m_ObjectHideFlags: 0 @@ -2770,6 +3115,7 @@ MonoBehaviour: brushShader: {fileID: 0} dilateShader: {fileID: 0} packMsShader: {fileID: 0} + pipette: 0 spray: 0 sprayDots: 24 sprayDotSizeMin: 0.05 @@ -2784,7 +3130,9 @@ MonoBehaviour: brushColor: {r: 0.6, g: 0.6, b: 1, a: 1} metallic: 1 smoothness: 1 + metallicSmoothnessOpacity: 1 occlusion: 1 + occlusionOpacity: 1 emissionColor: {r: 1, g: 0.6, b: 0, a: 1} emissionIntensity: 200 brushSize: 100 @@ -3685,8 +4033,8 @@ SceneRoots: - {fileID: 172071436} - {fileID: 832575519} - {fileID: 410087041} + - {fileID: 2025317892} - {fileID: 1141039639} - {fileID: 1129539841} - {fileID: 1006134333} - {fileID: 686187287} - - {fileID: 2025317892} diff --git a/Assets/Scripts/MeshPainter.Pick.cs b/Assets/Scripts/MeshPainter.Pick.cs index ca54ce9..a0f5499 100644 --- a/Assets/Scripts/MeshPainter.Pick.cs +++ b/Assets/Scripts/MeshPainter.Pick.cs @@ -6,16 +6,23 @@ 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. + // 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 (albedo, " + - "metallic, smoothness, occlusion, emission) and adopts them into the brush " + + [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; @@ -28,18 +35,42 @@ public partial class MeshPainter 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. + /// 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. /// private void PickAtScreen(Vector2 screenPos) { - if (!TryRaycastPaintMesh(screenPos, out RaycastHit hit)) return; - if (_verts == null || _uvs == null || _tris == null || hit.triangleIndex < 0) return; + 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; + + // Let listeners (the in-game HUD) refresh their widgets to the adopted values + flags. + Picked?.Invoke(); + } + + /// + /// Picks from our own painted maps at the hit texel (bit-exact, backend-agnostic). + /// + 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; + 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 @@ -58,7 +89,7 @@ public partial class MeshPainter 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; + if (idx < 0 || idx >= alb.Length || idx >= mso.Length || idx >= em.Length) return false; Color32 a = alb[idx]; Color32 m = mso[idx]; @@ -70,18 +101,132 @@ public partial class MeshPainter occlusion = m.g / 255f; smoothness = m.a / 255f; emissionColor = new Color(e.r / 255f, e.g / 255f, e.b / 255f, 1f); + return true; + } - // "Set all alpha values to 1": full-strength adoption across every opacity control. - metallicSmoothnessOpacity = 1f; - occlusionOpacity = 1f; + /// + /// 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. + /// + private bool PickFromForeignMaterial(RaycastHit hit) + { + Collider col = hit.collider; + if (!col) return false; + Renderer rend = col.GetComponent(); + if (!rend) rend = col.GetComponentInParent(); + if (!rend) return false; + Material mat = rend.sharedMaterial; + if (!mat) return false; - // Picking a whole material implies repainting all of it, so enable every channel. - paintAlbedo = true; - paintMetallicSmoothness = true; - paintOcclusion = true; - paintEmission = true; + // 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; - // Let listeners (the in-game HUD) refresh their widgets to the adopted values + flags. - Picked?.Invoke(); + // --- 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); + } + + /// + /// 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. selects the colour space so the returned value matches the readable path + /// (gamma for colour maps, linear for data maps). + /// + 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; } } diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs index b640579..2be4cb8 100644 --- a/Assets/Scripts/MeshPainter.cs +++ b/Assets/Scripts/MeshPainter.cs @@ -735,6 +735,18 @@ public partial class MeshPainter : MonoBehaviour return found; } + /// + /// Raycasts at a screen position and returns the nearest hit on ANY collider (not just the + /// paint mesh). Used by the pipette, which can sample foreign objects' materials too. + /// + private bool TryRaycastAny(Vector2 screenPos, out RaycastHit hit) + { + hit = default; + if (!paintCamera) return false; + Ray ray = paintCamera.ScreenPointToRay(screenPos); + return Physics.Raycast(ray, out hit, 1000f, ~0, QueryTriggerInteraction.Ignore); + } + /// /// 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