// Brush preview overlay: shows the brush footprint (size + mask) on the mesh // WITHOUT painting. Two modes, selected via _PreviewMode: // // 0 = On-surface: the baked mesh is drawn again (object transform) and each // fragment runs the SAME sphere + mask test as the paint shader, in local mesh // space (_BrushCenter/_BrushU/_BrushV/_BrushNormal/_BrushRadius). So the // preview wraps over curvature and across UV seams exactly like the paint // would land. Depth test (LEqual) + a slight polygon offset keep it glued to // the surface and correctly occluded by closer geometry. // // 1 = Flat decal quad: a single quad oriented in the tangent plane; the mask is // sampled straight from the quad UVs (0..1 already spans the stamp, so it // lines up 1:1 with the on-surface mapping). Flat - does not follow curvature. // // Unlit, transparent (SRPDefaultUnlit) so URP renders it via Graphics.DrawMesh. The // mask drives the alpha (luma * alpha * _PreviewOpacity), tinted with _BrushColor. Shader "Hidden/MeshPaintPreview" { Properties { _BrushColor ("Brush Color", Color) = (1,0,0,1) _MaskTex ("Brush Mask", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" } Pass { ZWrite Off ZTest LEqual Cull Off Offset -1, -1 // pull the overlay toward the camera so it wins over the painted surface Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _BrushColor; float3 _BrushCenter; // brush center in local mesh space (on-surface mode) float3 _BrushU; // stamp right axis (unit, local space) float3 _BrushV; // stamp up axis (unit, local space) float3 _BrushNormal; // surface normal at the hit (unit, local space) float _BrushRadius; // stamp half-extent in local mesh space float _PreviewOpacity; // overall preview alpha multiplier (>1 saturates to fully opaque) float _PreviewMode; // 0 = on-surface (mesh), 1 = flat decal quad sampler2D _MaskTex; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 local : TEXCOORD0; // object-space position (on-surface box test) float2 uv : TEXCOORD1; // quad uv (decal sampling) }; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.local = v.vertex.xyz; o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { float2 muv; if (_PreviewMode < 0.5) { // On-surface: same brush-sphere test as the paint shader, in local space. float3 d = i.local - _BrushCenter; float du = dot(d, _BrushU); float dv = dot(d, _BrushV); float dn = dot(d, _BrushNormal); clip(_BrushRadius * _BrushRadius - (du * du + dv * dv + dn * dn)); muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5; } else { // Flat decal quad: uv already spans the stamp. muv = i.uv; } fixed4 m = tex2D(_MaskTex, muv); float luma = dot(m.rgb, fixed3(0.299, 0.587, 0.114)); float opacity = luma * m.a * _PreviewOpacity; clip(opacity - 1e-4); // skip fully transparent texels return fixed4(_BrushColor.rgb, opacity); } ENDCG } } Fallback Off }