Add brush preview and drive paint opacity from brush color alpha
- 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).
This commit is contained in:
1 parent
c67d519f68
commit
d5fc5ee804
4 files changed
+303
-39
No files matched your search
+189
-36
@@ -17,6 +17,10 @@ using UnityEngine.InputSystem;
|
||||
/// masks is assignable in the Inspector; if it is empty a procedural circle mask is
|
||||
/// used, which reproduces the classic round brush. Attach to the GameObject you want
|
||||
/// to paint.
|
||||
///
|
||||
/// A non-destructive brush preview (size + mask) is shown at the hover point without
|
||||
/// painting (<see cref="previewMode"/>): either a flat decal quad or a real on-surface
|
||||
/// overlay that wraps the mesh exactly like the paint would land.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MeshPainter : MonoBehaviour
|
||||
@@ -30,6 +34,17 @@ public class MeshPainter : MonoBehaviour
|
||||
World
|
||||
}
|
||||
|
||||
/// <summary>How the brush preview at the hover point is rendered.</summary>
|
||||
public enum BrushPreviewMode
|
||||
{
|
||||
/// <summary>No preview.</summary>
|
||||
Off,
|
||||
/// <summary>A flat quad in the tangent plane (cheap, does not follow curvature).</summary>
|
||||
FlatDecal,
|
||||
/// <summary>Real on-surface overlay: wraps the mesh exactly like the paint would land.</summary>
|
||||
OnSurface
|
||||
}
|
||||
|
||||
/// <summary>When false, mouse painting is ignored. Toggled by the GameModeManager.</summary>
|
||||
public bool PaintingEnabled { get; set; } = true;
|
||||
|
||||
@@ -38,7 +53,8 @@ public class MeshPainter : MonoBehaviour
|
||||
public bool useGpu = true;
|
||||
|
||||
[Header("Brush")]
|
||||
[Tooltip("Color used for painting.")]
|
||||
[Tooltip("Color used for painting. The alpha channel is the global opacity multiplier on " +
|
||||
"top of the mask (alpha 1 = mask opacity as-is, lower = fainter).")]
|
||||
public Color brushColor = Color.red;
|
||||
|
||||
[Tooltip("Brush half-extent in texels (= half the square side). Converted into a 3D size per hit.")]
|
||||
@@ -60,9 +76,20 @@ public class MeshPainter : MonoBehaviour
|
||||
[Range(-180f, 180f)]
|
||||
public float brushRotation = 0f;
|
||||
|
||||
[Tooltip("Global opacity multiplier on top of the mask (1 = mask opacity as-is, lower = fainter).")]
|
||||
[Range(0f, 1f)]
|
||||
public float brushOpacity = 1f;
|
||||
[Tooltip("ON = paint once on mouse-down, then only while the mouse moves (a held but " +
|
||||
"stationary button no longer keeps re-stamping the same spot). OFF = continuous painting while held.")]
|
||||
public bool paintOnMoveOnly = false;
|
||||
|
||||
[Header("Preview")]
|
||||
[Tooltip("Brush preview at the hover point (shows size + mask without painting). " +
|
||||
"Flat Decal = a flat quad in the tangent plane (cheap). On Surface = a real " +
|
||||
"overlay that wraps the mesh exactly like the paint would land. Off = no preview.")]
|
||||
public BrushPreviewMode previewMode = BrushPreviewMode.OnSurface;
|
||||
|
||||
[Tooltip("Opacity of the preview overlay (the mask still modulates it per texel). " +
|
||||
"Can exceed 1 to make the preview show up more strongly.")]
|
||||
[Range(0f, 4f)]
|
||||
public float previewOpacity = 0.5f;
|
||||
|
||||
[Header("Canvas")]
|
||||
[Tooltip("Resolution of the paint texture (square).")]
|
||||
@@ -117,6 +144,11 @@ public class MeshPainter : MonoBehaviour
|
||||
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
|
||||
private Material _brushMat;
|
||||
private Material _dilateMat;
|
||||
|
||||
// --- preview overlay ---
|
||||
public Shader previewShader; // optional; else Shader.Find("Hidden/MeshPaintPreview")
|
||||
private Material _previewMat;
|
||||
private Mesh _previewQuad; // unit quad in the tangent plane (flat decal mode)
|
||||
private RenderTexture _displayRT;
|
||||
private RenderTexture _tempRT; // scratch for the dilation blit (ping-pong)
|
||||
private bool _drewThisStroke;
|
||||
@@ -132,6 +164,7 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private const float ScreenStepPixels = 5f; // interpolation step along mouse movement
|
||||
private const int MaxStepsPerFrame = 256; // raycast safeguard for fast movement
|
||||
private const float MoveOnlyMinPixels = 0.75f; // move-only mode: min mouse travel before re-stamping
|
||||
private const float SquareReach = 1.7321f; // sqrt(3): max 3D distance from center to a painted box texel (CPU culling)
|
||||
|
||||
private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap");
|
||||
@@ -145,8 +178,9 @@ public class MeshPainter : MonoBehaviour
|
||||
private static readonly int BrushVId = Shader.PropertyToID("_BrushV");
|
||||
private static readonly int BrushNormalId = Shader.PropertyToID("_BrushNormal");
|
||||
private static readonly int MaskTexId = Shader.PropertyToID("_MaskTex");
|
||||
private static readonly int BrushOpacityId = Shader.PropertyToID("_BrushOpacity");
|
||||
private static readonly int PadId = Shader.PropertyToID("_Pad");
|
||||
private static readonly int PreviewOpacityId = Shader.PropertyToID("_PreviewOpacity");
|
||||
private static readonly int PreviewModeId = Shader.PropertyToID("_PreviewMode");
|
||||
|
||||
private void Start()
|
||||
{
|
||||
@@ -183,6 +217,7 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
EnsureProceduralMask();
|
||||
ResolveActiveMask();
|
||||
SetupPreview();
|
||||
|
||||
_setupValid = true;
|
||||
|
||||
@@ -616,6 +651,94 @@ public class MeshPainter : MonoBehaviour
|
||||
return t.normalized;
|
||||
}
|
||||
|
||||
// --- Brush preview ---
|
||||
|
||||
/// <summary>Creates the preview material and the unit quad used by the flat-decal mode.</summary>
|
||||
private void SetupPreview()
|
||||
{
|
||||
if (!previewShader) previewShader = Shader.Find("Hidden/MeshPaintPreview");
|
||||
if (!previewShader)
|
||||
{
|
||||
Debug.LogWarning("[MeshPainter] Preview shader (Hidden/MeshPaintPreview) not found - brush preview disabled.", this);
|
||||
return;
|
||||
}
|
||||
_previewMat = new Material(previewShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
_previewQuad = BuildPreviewQuad();
|
||||
}
|
||||
|
||||
/// <summary>Unit quad in its XY plane ([-1,1], normal +Z, uv 0..1) - oriented per hit in the flat-decal mode.</summary>
|
||||
private static Mesh BuildPreviewQuad()
|
||||
{
|
||||
var q = new Mesh { name = "MeshPainter_PreviewQuad", hideFlags = HideFlags.HideAndDontSave };
|
||||
q.vertices = new[]
|
||||
{
|
||||
new Vector3(-1f, -1f, 0f), new Vector3(1f, -1f, 0f),
|
||||
new Vector3(1f, 1f, 0f), new Vector3(-1f, 1f, 0f)
|
||||
};
|
||||
q.uv = new[]
|
||||
{
|
||||
new Vector2(0f, 0f), new Vector2(1f, 0f),
|
||||
new Vector2(1f, 1f), new Vector2(0f, 1f)
|
||||
};
|
||||
q.triangles = new[] { 0, 1, 2, 0, 2, 3 };
|
||||
q.RecalculateBounds();
|
||||
return q;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the brush preview at the hover point for this frame (no painting). On-surface
|
||||
/// mode re-draws the baked mesh and clips it to the brush box in the shader; flat-decal
|
||||
/// mode draws the oriented quad. Both sample the active mask (size + shape preview).
|
||||
/// </summary>
|
||||
private void UpdatePreview(Vector2 screenPos)
|
||||
{
|
||||
if (!TryComputeBrushPlacement(screenPos, out Vector3 pLocal, out float radiusLocal,
|
||||
out Vector3 u, out Vector3 v, out Vector3 n))
|
||||
return;
|
||||
|
||||
_previewMat.SetColor(BrushColorId, brushColor);
|
||||
_previewMat.SetTexture(MaskTexId, _resolvedMask);
|
||||
_previewMat.SetFloat(PreviewOpacityId, Mathf.Max(0f, previewOpacity) * Mathf.Clamp01(brushColor.a));
|
||||
|
||||
int layer = gameObject.layer;
|
||||
|
||||
if (previewMode == BrushPreviewMode.OnSurface)
|
||||
{
|
||||
_previewMat.SetFloat(PreviewModeId, 0f);
|
||||
_previewMat.SetVector(BrushCenterId, pLocal);
|
||||
_previewMat.SetFloat(BrushRadiusId, radiusLocal);
|
||||
_previewMat.SetVector(BrushUId, u);
|
||||
_previewMat.SetVector(BrushVId, v);
|
||||
_previewMat.SetVector(BrushNormalId, n);
|
||||
|
||||
// Draw the collider mesh again (object transform): the shader clips it to the
|
||||
// brush box, so only the footprint shows, wrapped onto the real surface. Using
|
||||
// the collider mesh keeps it in the same local space as the raycast hit and
|
||||
// valid for both skinned (baked) and static meshes.
|
||||
Mesh previewMesh = _collider ? _collider.sharedMesh : null;
|
||||
if (!previewMesh) return;
|
||||
Matrix4x4 m = transform.localToWorldMatrix;
|
||||
int sub = previewMesh.subMeshCount;
|
||||
for (int i = 0; i < sub; i++)
|
||||
Graphics.DrawMesh(previewMesh, m, _previewMat, layer, paintCamera, i);
|
||||
}
|
||||
else // FlatDecal
|
||||
{
|
||||
_previewMat.SetFloat(PreviewModeId, 1f);
|
||||
|
||||
// Quad frame in local space: x -> u*r, y -> v*r, lifted slightly along n so
|
||||
// it floats on the surface; uv 0..1 matches the on-surface mask mapping.
|
||||
Vector3 pos = pLocal + n * (radiusLocal * 0.02f);
|
||||
var frame = new Matrix4x4();
|
||||
frame.SetColumn(0, new Vector4(u.x * radiusLocal, u.y * radiusLocal, u.z * radiusLocal, 0f));
|
||||
frame.SetColumn(1, new Vector4(v.x * radiusLocal, v.y * radiusLocal, v.z * radiusLocal, 0f));
|
||||
frame.SetColumn(2, new Vector4(n.x, n.y, n.z, 0f));
|
||||
frame.SetColumn(3, new Vector4(pos.x, pos.y, pos.z, 1f));
|
||||
|
||||
Graphics.DrawMesh(_previewQuad, transform.localToWorldMatrix * frame, _previewMat, layer, paintCamera, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update / input ---
|
||||
|
||||
private void Update()
|
||||
@@ -642,6 +765,11 @@ public class MeshPainter : MonoBehaviour
|
||||
if (!TryGetMouse(out bool pressed, out Vector2 screenPos))
|
||||
return;
|
||||
|
||||
// Brush preview (size + mask) at the hover point - independent of painting,
|
||||
// so you see the brush before pressing the button.
|
||||
if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera)
|
||||
UpdatePreview(screenPos);
|
||||
|
||||
if (!pressed)
|
||||
{
|
||||
_hasLastScreen = false;
|
||||
@@ -650,28 +778,36 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
if (!paintCamera) return;
|
||||
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; }
|
||||
float moveDist = _hasLastScreen ? Vector2.Distance(_lastScreen, screenPos) : 0f;
|
||||
|
||||
if (_hasLastScreen)
|
||||
{
|
||||
// Interpolate along the mouse movement and re-raycast at each step so the
|
||||
// stroke follows the mesh surface.
|
||||
float dist = Vector2.Distance(_lastScreen, screenPos);
|
||||
int steps = Mathf.Clamp(Mathf.CeilToInt(dist / ScreenStepPixels), 1, MaxStepsPerFrame);
|
||||
for (int s = 1; s <= steps; s++)
|
||||
PaintAtScreen(Vector2.Lerp(_lastScreen, screenPos, s / (float)steps));
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintAtScreen(screenPos);
|
||||
}
|
||||
// Move-only mode: stamp once on mouse-down, then only when the mouse actually
|
||||
// moves - a held but stationary button no longer re-stamps the same spot.
|
||||
bool stamp = !_hasLastScreen || !paintOnMoveOnly || moveDist > MoveOnlyMinPixels;
|
||||
|
||||
if (_gpuActive) EndGpuStroke();
|
||||
else
|
||||
if (stamp)
|
||||
{
|
||||
if (paddingTexels > 0) DilatePadding(paddingTexels);
|
||||
FlushDirty();
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; }
|
||||
|
||||
if (_hasLastScreen)
|
||||
{
|
||||
// Interpolate along the mouse movement and re-raycast at each step so the
|
||||
// stroke follows the mesh surface.
|
||||
int steps = Mathf.Clamp(Mathf.CeilToInt(moveDist / ScreenStepPixels), 1, MaxStepsPerFrame);
|
||||
for (int s = 1; s <= steps; s++)
|
||||
PaintAtScreen(Vector2.Lerp(_lastScreen, screenPos, s / (float)steps));
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintAtScreen(screenPos);
|
||||
}
|
||||
|
||||
if (_gpuActive) EndGpuStroke();
|
||||
else
|
||||
{
|
||||
if (paddingTexels > 0) DilatePadding(paddingTexels);
|
||||
FlushDirty();
|
||||
}
|
||||
}
|
||||
|
||||
_lastScreen = screenPos;
|
||||
@@ -681,6 +817,24 @@ public class MeshPainter : MonoBehaviour
|
||||
/// <summary>Raycasts at a screen position and stamps the masked square brush onto the mesh hit point.</summary>
|
||||
private void PaintAtScreen(Vector2 screenPos)
|
||||
{
|
||||
if (!TryComputeBrushPlacement(screenPos, out Vector3 pLocal, out float radiusLocal,
|
||||
out Vector3 u, out Vector3 v, out Vector3 nLocal))
|
||||
return;
|
||||
|
||||
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, nLocal);
|
||||
else PaintStamp3D(pLocal, radiusLocal, u, v, nLocal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// density) and the orthonormal tangent frame (u/v/n). Shared by painting and preview.
|
||||
/// </summary>
|
||||
private bool TryComputeBrushPlacement(Vector2 screenPos, out Vector3 pLocal, out float radiusLocal,
|
||||
out Vector3 u, out Vector3 v, out Vector3 nLocal)
|
||||
{
|
||||
pLocal = default; radiusLocal = 0f; u = default; v = default; nLocal = default;
|
||||
|
||||
Ray ray = paintCamera.ScreenPointToRay(screenPos);
|
||||
|
||||
// Pick the nearest hit that belongs to the paint mesh. A plain raycast would
|
||||
@@ -700,10 +854,10 @@ public class MeshPainter : MonoBehaviour
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) return;
|
||||
if (_verts == null || hit.triangleIndex < 0) return;
|
||||
if (!found) return false;
|
||||
if (_verts == null || hit.triangleIndex < 0) return false;
|
||||
|
||||
Vector3 pLocal = transform.InverseTransformPoint(hit.point);
|
||||
pLocal = transform.InverseTransformPoint(hit.point);
|
||||
|
||||
// Convert the brush radius (texels) into a local 3D radius using the hit
|
||||
// triangle's UV density so the brush keeps a constant visual size.
|
||||
@@ -721,16 +875,14 @@ public class MeshPainter : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
float radiusLocal = (brushSize / (float)textureSize) * scale;
|
||||
if (radiusLocal <= 0f) return;
|
||||
radiusLocal = (brushSize / (float)textureSize) * scale;
|
||||
if (radiusLocal <= 0f) return false;
|
||||
|
||||
// Tangent-plane frame for the square stamp / mask projection.
|
||||
Vector3 nLocal = transform.InverseTransformDirection(hit.normal);
|
||||
ComputeBrushFrame(nLocal, out Vector3 u, out Vector3 v);
|
||||
nLocal = transform.InverseTransformDirection(hit.normal);
|
||||
ComputeBrushFrame(nLocal, out u, out v);
|
||||
nLocal = nLocal.normalized;
|
||||
|
||||
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, nLocal);
|
||||
else PaintStamp3D(pLocal, radiusLocal, u, v, nLocal);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- GPU painting ---
|
||||
@@ -743,7 +895,6 @@ public class MeshPainter : MonoBehaviour
|
||||
Graphics.SetRenderTarget(_displayRT);
|
||||
_brushMat.SetColor(BrushColorId, brushColor);
|
||||
_brushMat.SetTexture(MaskTexId, _resolvedMask);
|
||||
_brushMat.SetFloat(BrushOpacityId, Mathf.Clamp01(brushOpacity));
|
||||
}
|
||||
|
||||
private void DrawBrushGpu(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
||||
@@ -790,7 +941,7 @@ public class MeshPainter : MonoBehaviour
|
||||
private void PaintStamp3D(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
||||
{
|
||||
float br = brushColor.r * 255f, bg = brushColor.g * 255f, bb = brushColor.b * 255f;
|
||||
float globalOpacity = Mathf.Clamp01(brushOpacity);
|
||||
float globalOpacity = Mathf.Clamp01(brushColor.a);
|
||||
float invR = 1f / radiusLocal;
|
||||
int triCount = _tris.Length / 3;
|
||||
float ts = textureSize;
|
||||
@@ -995,6 +1146,8 @@ public class MeshPainter : MonoBehaviour
|
||||
if (_bakedMesh) Destroy(_bakedMesh);
|
||||
if (_materialInstance) Destroy(_materialInstance);
|
||||
if (_proceduralMask) Destroy(_proceduralMask);
|
||||
if (_previewMat) Destroy(_previewMat);
|
||||
if (_previewQuad) Destroy(_previewQuad);
|
||||
TeardownCpu();
|
||||
TeardownGpu();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// 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 * _BrushOpacity
|
||||
// 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.
|
||||
//
|
||||
@@ -44,7 +44,6 @@ Shader "Hidden/MeshPaintBrush"
|
||||
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 _BrushOpacity; // global opacity multiplier (0..1)
|
||||
sampler2D _MaskTex;
|
||||
|
||||
struct appdata
|
||||
@@ -91,7 +90,7 @@ Shader "Hidden/MeshPaintBrush"
|
||||
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 * _BrushOpacity;
|
||||
float opacity = luma * m.a * _BrushColor.a;
|
||||
clip(opacity - 1e-4); // skip fully transparent texels
|
||||
|
||||
return fixed4(_BrushColor.rgb, opacity);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 box + 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 square box 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 - abs(du));
|
||||
clip(_BrushRadius - abs(dv));
|
||||
clip(_BrushRadius - abs(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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8584f72f2fa12c1489898d805b793315
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user