// GPU brush: paints a 3D sphere 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 distance to the // brush center (_BrushCenter, in local mesh space) is tested against _BrushRadius. // // This bridges UV seams automatically: triangles on both sides of a seam share // the same 3D edge and are captured by the same brush stroke, 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) } SubShader { Tags { "RenderType" = "Opaque" } Pass { ZTest Always ZWrite Off Cull Off Blend Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _BrushColor; float3 _BrushCenter; // brush center in local mesh space float _BrushRadius; // brush radius in local mesh space 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 { // Only color texels inside the 3D brush sphere. float d = distance(i.local, _BrushCenter); clip(_BrushRadius - d); // Alpha = 1 marks "painted by the brush" (source for the edge padding). return fixed4(_BrushColor.rgb, 1.0); } ENDCG } } Fallback Off }