// GPU brush: paints a square mask stamp seamlessly into the paint texture. // // In the vertex shader the mesh is "unwrapped" into the texture layout using its // UVs (uv -> clip space), while the actual local 3D position of the vertex is // passed through to the fragment shader. There, per texel, the offset to the brush // center (_BrushCenter, in local mesh space) is projected onto the stamp's tangent // frame: _BrushU (right), _BrushV (up) and _BrushNormal. The texel is painted only // if it lies inside the box (|du|,|dv|,|dn| <= _BrushRadius); the brush mask // (_MaskTex, sampled along du/dv) then provides the per-texel OPACITY: // opacity = luma(mask.rgb) * mask.a * _BrushColor.a // and the color is alpha-blended over the existing paint (source-over). So a mask // gray value of 0.5 paints at 50%, white = full, black/transparent = nothing. // // This bridges UV seams automatically: triangles on both sides of a seam share the // same 3D edge and are captured by the same brush stamp, no matter where they lie in // the UV layout (identical behavior to the CPU path). Shader "Hidden/MeshPaintBrush" { Properties { _BrushColor ("Brush Color", Color) = (1,0,0,1) _MaskTex ("Brush Mask", 2D) = "white" {} _ColorWriteMask ("Color Write Mask", Float) = 15 } SubShader { Tags { "RenderType" = "Opaque" } Pass { ZTest Always ZWrite Off Cull Off // Per-channel write mask: lets the shared mask map (R=metallic, G=smoothness, // B=occlusion) be painted by metallic/smoothness and occlusion independently. // ColorWriteMask bits: R=8, G=4, B=2, A=1 (15 = all). Alpha (coverage) is always // written so the dilation pass still sees the freshly painted texels. ColorMask [_ColorWriteMask] // Source-over for color; alpha accumulates coverage toward 1 (painted marker // for the dilation pass). Soft edges keep partial alpha. Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _BrushColor; float3 _BrushCenter; // brush center in local mesh space 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 sampler2D _MaskTex; struct appdata { float4 vertex : POSITION; // local 3D position float2 uv : TEXCOORD0; // UV (target in the texture layout) }; struct v2f { float4 pos : SV_POSITION; float3 local : TEXCOORD0; // interpolated local 3D position }; v2f vert (appdata v) { v2f o; // UV (0..1) -> clip space (-1..1). This makes the GPU rasterize // each triangle exactly at its place in the texture. float2 p = v.uv * 2.0 - 1.0; #if UNITY_UV_STARTS_AT_TOP p.y = -p.y; // compensate the RenderTexture Y-flip (D3D etc.) #endif o.pos = float4(p, 0.0, 1.0); o.local = v.vertex.xyz; return o; } fixed4 frag (v2f i) : SV_Target { // Offset from the brush center, projected onto the stamp frame. float3 d = i.local - _BrushCenter; float du = dot(d, _BrushU); float dv = dot(d, _BrushV); float dn = dot(d, _BrushNormal); // Square stamp in the tangent plane + depth bound (keeps the seam- // bridging 3D behavior while preventing bleed to far surfaces). clip(_BrushRadius - abs(du)); clip(_BrushRadius - abs(dv)); clip(_BrushRadius - abs(dn)); // Mask -> opacity. du/dv in [-r,r] -> uv in [0,1]. float2 muv = float2(du, dv) / _BrushRadius * 0.5 + 0.5; fixed4 m = tex2D(_MaskTex, muv); float luma = dot(m.rgb, fixed3(0.299, 0.587, 0.114)); float opacity = luma * m.a * _BrushColor.a; clip(opacity - 1e-4); // skip fully transparent texels return fixed4(_BrushColor.rgb, opacity); } ENDCG } // Pass 1 - coverage: rasterizes the mesh in UV space (identical vertex transform to // the brush) and writes alpha = 1 on exactly the texels the brush can paint. Used to // seed the dilation guard (display RT alpha) so it matches the GPU rasterizer instead // of a CPU point-in-triangle approximation. Texels the brush can never reach (sub-pixel // island borders) stay alpha = 0 -> the gutter, so dilation can still overpaint them. Pass { ZTest Always ZWrite Off Cull Off Blend Off ColorMask A CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert (appdata v) { v2f o; float2 p = v.uv * 2.0 - 1.0; #if UNITY_UV_STARTS_AT_TOP p.y = -p.y; #endif o.pos = float4(p, 0.0, 1.0); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(0, 0, 0, 1); } ENDCG } } Fallback Off }