using UnityEngine; public partial class MeshPainter { // --- Spray can brush --- // // Instead of one solid stamp, the spray scatters many small dots (random position, // size and alpha) within the brush footprint. It is implemented by baking those dots // into a fresh procedural MASK each stamp and then painting that mask with ONE normal // brush stamp. This reuses the entire seam-bridging / curvature-following / per-channel // pipeline as-is and costs a single mesh draw per stamp (not one per dot). Because the // per-dot alpha lives in the mask value, it flows into every channel (albedo, mask map, // emission) automatically - on both the GPU and CPU backends. [Header("Brush - spray can")] [Tooltip("Spray-can mode: scatter random dots (position, size, alpha) within the brush " + "radius instead of a single solid stamp. The dots are baked into the brush mask, " + "so this works on all channels and both backends.")] public bool spray = false; [Tooltip("Number of dots scattered per stamp.")] [Range(1, 128)] public int sprayDots = 24; [Tooltip("Dot diameter as a fraction of the brush diameter (randomized per dot between min and max).")] [Range(0.01f, 1f)] public float sprayDotSizeMin = 0.05f; [Tooltip("Dot diameter as a fraction of the brush diameter (randomized per dot between min and max).")] [Range(0.01f, 1f)] public float sprayDotSizeMax = 0.18f; [Tooltip("Per-dot opacity (randomized per dot between min and max). Multiplied on top of the channel color's alpha.")] [Range(0f, 1f)] public float sprayDotAlphaMin = 0.25f; [Tooltip("Per-dot opacity (randomized per dot between min and max). Multiplied on top of the channel color's alpha.")] [Range(0f, 1f)] public float sprayDotAlphaMax = 1f; // Resolution of the per-stamp spray mask (square). Small + reused so regenerating it // every stamp stays cheap. private const int SprayRes = 128; private const float SprayDotSoftEdge = 0.4f; // fraction of the dot radius that fades to 0 private Texture2D _sprayTex; // the baked spray mask uploaded to the brush private Color32[] _sprayPixels; // reused upload buffer private float[] _sprayOpacity; // reused per-texel opacity (CPU sampling) // --- Active stamp mask (indirection shared by both backends) --- // The standard mask cache (_resolvedMask / _maskOpacity / _maskW / _maskH) stays the // canonical resolved mask used by the preview. _stamp* is what the CURRENT stamp paints // with - the resolved mask for normal painting, or a fresh spray mask in spray mode. private Texture2D _stampMask; private float[] _stampOpacity; private int _stampW, _stampH; /// Points the active stamp mask at the resolved standard mask (normal painting). private void UseResolvedStampMask() { _stampMask = _resolvedMask; _stampOpacity = _maskOpacity; _stampW = _maskW; _stampH = _maskH; } /// /// Bakes a fresh random spray pattern into the spray mask and makes it the active stamp /// mask. Each dot gets a random center inside the brush disc, a random diameter and a /// random opacity; dots composite by max so overlaps never exceed full opacity. /// private void BuildSprayStampMask() { EnsureSprayBuffers(); System.Array.Clear(_sprayOpacity, 0, _sprayOpacity.Length); int dots = Mathf.Max(1, sprayDots); float sizeLo = Mathf.Clamp(Mathf.Min(sprayDotSizeMin, sprayDotSizeMax), 0.01f, 1f); float sizeHi = Mathf.Clamp(Mathf.Max(sprayDotSizeMin, sprayDotSizeMax), 0.01f, 1f); float alphaLo = Mathf.Clamp01(Mathf.Min(sprayDotAlphaMin, sprayDotAlphaMax)); float alphaHi = Mathf.Clamp01(Mathf.Max(sprayDotAlphaMin, sprayDotAlphaMax)); for (int k = 0; k < dots; k++) { // Diameter is a fraction of the brush diameter -> uv radius = fraction * 0.5. float dotR = Mathf.Max(0.5f / SprayRes, Random.Range(sizeLo, sizeHi) * 0.5f); float alpha = Random.Range(alphaLo, alphaHi); // Keep the WHOLE dot inside the brush disc (uv radius 0.5 = the brush radius) so the // spray footprint matches the normal brush instead of bleeding past it by dotR. float centerR = Mathf.Max(0f, 0.5f - dotR); Vector2 c = Random.insideUnitCircle * centerR + new Vector2(0.5f, 0.5f); SplatDot(c.x, c.y, dotR, alpha); } for (int i = 0; i < _sprayOpacity.Length; i++) { byte g = (byte)(Mathf.Clamp01(_sprayOpacity[i]) * 255f + 0.5f); _sprayPixels[i] = new Color32(g, g, g, 255); } _sprayTex.SetPixels32(_sprayPixels); _sprayTex.Apply(false); _stampMask = _sprayTex; _stampOpacity = _sprayOpacity; _stampW = SprayRes; _stampH = SprayRes; } /// Rasterizes one soft dot (center/radius in uv, value = alpha) into the opacity buffer, compositing by max. private void SplatDot(float cx, float cy, float r, float alpha) { if (alpha <= 0f) return; float inner = 1f - SprayDotSoftEdge; // solid core up to this normalized distance, then fade int minX = Mathf.Max(0, Mathf.FloorToInt((cx - r) * SprayRes)); int maxX = Mathf.Min(SprayRes - 1, Mathf.CeilToInt((cx + r) * SprayRes)); int minY = Mathf.Max(0, Mathf.FloorToInt((cy - r) * SprayRes)); int maxY = Mathf.Min(SprayRes - 1, Mathf.CeilToInt((cy + r) * SprayRes)); float invR = 1f / r; for (int y = minY; y <= maxY; y++) { float vY = (y + 0.5f) / SprayRes; int row = y * SprayRes; for (int x = minX; x <= maxX; x++) { float vX = (x + 0.5f) / SprayRes; float dx = vX - cx, dy = vY - cy; float dist = Mathf.Sqrt(dx * dx + dy * dy) * invR; // 0 = center, 1 = edge if (dist >= 1f) continue; float falloff = dist <= inner ? 1f : 1f - Mathf.SmoothStep(inner, 1f, dist); float val = alpha * falloff; int idx = row + x; if (val > _sprayOpacity[idx]) _sprayOpacity[idx] = val; } } } private void EnsureSprayBuffers() { if (_sprayTex) return; _sprayTex = NewMaskTexture(SprayRes, "MeshPainter_SprayMask"); _sprayPixels = new Color32[SprayRes * SprayRes]; _sprayOpacity = new float[SprayRes * SprayRes]; } private void TeardownSpray() { if (_sprayTex) { Destroy(_sprayTex); _sprayTex = null; } _sprayPixels = null; _sprayOpacity = null; } }