Add spray brush, paint interval, and per-channel paint opacity
Spray brush mode: bakes a procedural scattered-dot mask per stamp (random position, size, alpha within the brush disc) and paints it through the existing single-stamp pipeline; dot centers clamped to (0.5 - dotR) so the spray footprint matches the solid brush radius. Paint interval: float on MeshPainter; 0 keeps the continuous interpolated stroke, >0 emits a single timed dab every x.xx seconds. Color picker alpha: horizontal alpha bar on both HSV pickers, runtime-built in Rebind (no prefab re-bake), passed through to brushColor.a / emissionColor.a. Metallic/smoothness + occlusion paint opacity: new opacity fields applied in both CPU and GPU backends (GPU MSO draw split into MS and occlusion sub-stamps for independent opacity), with cloned alpha sliders wired into the HUD. Also includes the prefab layout tweaks and scene adjustments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
af3aa34d66
commit
b07d9a22d2
9 files changed
+1227
-86
No files matched your search
@@ -85,12 +85,24 @@ public partial class MeshPainter : MonoBehaviour
|
||||
[Range(0f, 1f)]
|
||||
public float smoothness = 0.5f;
|
||||
|
||||
[Tooltip("Paint opacity for metallic + smoothness on top of the mask (1 = mask opacity " +
|
||||
"as-is, lower = blends the metallic/smoothness target in more faintly, 0 = no-op). " +
|
||||
"The albedo equivalent of brushColor.a, but for the metallic/smoothness channels.")]
|
||||
[Range(0f, 1f)]
|
||||
public float metallicSmoothnessOpacity = 1f;
|
||||
|
||||
[Tooltip("Ambient occlusion painted into the mask map (1 = fully lit / no darkening, " +
|
||||
"0 = fully occluded / dark). NOTE: URP only darkens INDIRECT light (ambient + " +
|
||||
"reflections), so the effect is subtle under strong direct lighting. 1 = no-op.")]
|
||||
[Range(0f, 1f)]
|
||||
public float occlusion = 0.5f;
|
||||
|
||||
[Tooltip("Paint opacity for occlusion on top of the mask (1 = mask opacity as-is, lower = " +
|
||||
"blends the occlusion target in more faintly, 0 = no-op). The albedo equivalent of " +
|
||||
"brushColor.a, but for the occlusion channel.")]
|
||||
[Range(0f, 1f)]
|
||||
public float occlusionOpacity = 1f;
|
||||
|
||||
[Header("Brush - emission")]
|
||||
[Tooltip("Emission color painted into the map. Alpha = global opacity multiplier on top of the mask.")]
|
||||
public Color emissionColor = Color.black;
|
||||
@@ -123,6 +135,13 @@ public partial class MeshPainter : MonoBehaviour
|
||||
"stationary button no longer keeps re-stamping the same spot). OFF = continuous painting while held.")]
|
||||
public bool paintOnMoveOnly = false;
|
||||
|
||||
[Tooltip("Seconds between paint stamps while the button is held. 0 = paint every frame as " +
|
||||
"an interpolated, continuous stroke (as before). Above 0 = deposit one stamp at the " +
|
||||
"cursor every this many seconds (timed dabs, no movement interpolation - pairs well " +
|
||||
"with the spray can).")]
|
||||
[Min(0f)]
|
||||
public float paintInterval = 0f;
|
||||
|
||||
[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 " +
|
||||
@@ -188,6 +207,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
private bool _hasLastScreen;
|
||||
private Vector2 _lastScreen;
|
||||
private float _intervalTimer; // accumulates while held; gates timed-dab stamping (paintInterval > 0)
|
||||
private bool _gpuActive;
|
||||
private bool _setupValid;
|
||||
|
||||
@@ -385,24 +405,24 @@ public partial class MeshPainter : MonoBehaviour
|
||||
return px;
|
||||
}
|
||||
|
||||
/// <summary>Bilinear sample of the resolved mask opacity at uv in [0,1]. Returns 1 (full) if no mask.</summary>
|
||||
/// <summary>Bilinear sample of the active stamp mask opacity at uv in [0,1]. Returns 1 (full) if no mask.</summary>
|
||||
private float SampleMaskOpacity(float u, float v)
|
||||
{
|
||||
if (_maskOpacity == null || _maskW <= 0 || _maskH <= 0) return 1f;
|
||||
if (_stampOpacity == null || _stampW <= 0 || _stampH <= 0) return 1f;
|
||||
|
||||
float fx = Mathf.Clamp01(u) * (_maskW - 1);
|
||||
float fy = Mathf.Clamp01(v) * (_maskH - 1);
|
||||
float fx = Mathf.Clamp01(u) * (_stampW - 1);
|
||||
float fy = Mathf.Clamp01(v) * (_stampH - 1);
|
||||
int x0 = Mathf.FloorToInt(fx);
|
||||
int y0 = Mathf.FloorToInt(fy);
|
||||
int x1 = Mathf.Min(x0 + 1, _maskW - 1);
|
||||
int y1 = Mathf.Min(y0 + 1, _maskH - 1);
|
||||
int x1 = Mathf.Min(x0 + 1, _stampW - 1);
|
||||
int y1 = Mathf.Min(y0 + 1, _stampH - 1);
|
||||
float tx = fx - x0;
|
||||
float ty = fy - y0;
|
||||
|
||||
float l00 = _maskOpacity[y0 * _maskW + x0];
|
||||
float l10 = _maskOpacity[y0 * _maskW + x1];
|
||||
float l01 = _maskOpacity[y1 * _maskW + x0];
|
||||
float l11 = _maskOpacity[y1 * _maskW + x1];
|
||||
float l00 = _stampOpacity[y0 * _stampW + x0];
|
||||
float l10 = _stampOpacity[y0 * _stampW + x1];
|
||||
float l01 = _stampOpacity[y1 * _stampW + x0];
|
||||
float l11 = _stampOpacity[y1 * _stampW + x1];
|
||||
|
||||
float a = Mathf.Lerp(l00, l10, tx);
|
||||
float b = Mathf.Lerp(l01, l11, tx);
|
||||
@@ -509,7 +529,9 @@ public partial class MeshPainter : MonoBehaviour
|
||||
return;
|
||||
|
||||
_previewMat.SetColor(BrushColorId, brushColor);
|
||||
_previewMat.SetTexture(MaskTexId, _resolvedMask);
|
||||
// Spray paints scattered dots, but the preview shows the round footprint (the area
|
||||
// the dots land in), so the hover ring stays a stable, readable size indicator.
|
||||
_previewMat.SetTexture(MaskTexId, spray ? _proceduralMask : _resolvedMask);
|
||||
_previewMat.SetFloat(PreviewOpacityId, Mathf.Max(0f, previewOpacity) * Mathf.Clamp01(brushColor.a));
|
||||
|
||||
int layer = gameObject.layer;
|
||||
@@ -598,18 +620,39 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (!paintCamera) return;
|
||||
|
||||
bool anyChannel = AnyChannelActive(); // includes occlusion (matches IsChannelActive / PaintStamp3D)
|
||||
float moveDist = _hasLastScreen ? Vector2.Distance(_lastScreen, screenPos) : 0f;
|
||||
bool firstPress = !_hasLastScreen;
|
||||
float moveDist = firstPress ? 0f : Vector2.Distance(_lastScreen, 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 = anyChannel && (!_hasLastScreen || !paintOnMoveOnly || moveDist > MoveOnlyMinPixels);
|
||||
bool stamp;
|
||||
if (paintInterval > 0f)
|
||||
{
|
||||
// Timed-dab mode: one stamp on press, then one at the cursor every paintInterval
|
||||
// seconds while held (discrete dabs, no movement interpolation).
|
||||
if (firstPress)
|
||||
{
|
||||
_intervalTimer = 0f;
|
||||
stamp = anyChannel;
|
||||
}
|
||||
else
|
||||
{
|
||||
_intervalTimer += Time.deltaTime;
|
||||
stamp = anyChannel && _intervalTimer >= paintInterval;
|
||||
if (stamp) _intervalTimer = 0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Continuous mode: stamp every frame as an interpolated stroke. Move-only optionally
|
||||
// gates it - a held but stationary button no longer re-stamps the same spot.
|
||||
stamp = anyChannel && (firstPress || !paintOnMoveOnly || moveDist > MoveOnlyMinPixels);
|
||||
}
|
||||
|
||||
if (stamp)
|
||||
{
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else ResetCpuDirty();
|
||||
|
||||
if (_hasLastScreen)
|
||||
if (!firstPress && paintInterval <= 0f)
|
||||
{
|
||||
// Interpolate along the mouse movement and re-raycast at each step so the
|
||||
// stroke follows the mesh surface.
|
||||
@@ -619,6 +662,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// First stamp of a stroke, or a timed dab: a single discrete stamp at the cursor.
|
||||
PaintAtScreen(screenPos);
|
||||
}
|
||||
|
||||
@@ -637,6 +681,11 @@ public partial class MeshPainter : MonoBehaviour
|
||||
out Vector3 u, out Vector3 v, out Vector3 nLocal))
|
||||
return;
|
||||
|
||||
// Pick what this stamp paints with: a fresh random spray pattern, or the resolved
|
||||
// standard mask. Both backends read it through the _stamp* fields.
|
||||
if (spray) BuildSprayStampMask();
|
||||
else UseResolvedStampMask();
|
||||
|
||||
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, nLocal);
|
||||
else PaintStamp3D(pLocal, radiusLocal, u, v, nLocal);
|
||||
}
|
||||
@@ -761,6 +810,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (_proceduralMask) Destroy(_proceduralMask);
|
||||
if (_previewMat) Destroy(_previewMat);
|
||||
if (_previewQuad) Destroy(_previewQuad);
|
||||
TeardownSpray();
|
||||
TeardownCpu();
|
||||
TeardownGpu();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user