- Add hover preview (MeshPaintPreview.shader) with a Preview Mode enum (Off / Flat Decal / On Surface) sharing the paint placement path so size, mask, alignment and rotation match the actual stroke. - Preview Opacity now ranges up to 4.0 so faint/grey masks can be shown more strongly (saturates to full opacity before the hardware blend). - Add Paint On Move Only: stamp once on mouse-down, then only while the cursor moves (GPU and CPU paths alike). - Remove brushOpacity; brushColor.a is now the global opacity multiplier over the mask in both backends (the alpha channel was previously unused by the brush shader).
103 lines
4.2 KiB
Plaintext
103 lines
4.2 KiB
Plaintext
// 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" {}
|
|
}
|
|
SubShader
|
|
{
|
|
Tags { "RenderType" = "Opaque" }
|
|
Pass
|
|
{
|
|
ZTest Always
|
|
ZWrite Off
|
|
Cull Off
|
|
// 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
|
|
}
|
|
}
|
|
Fallback Off
|
|
}
|