Deduplication pass (44 extractions): - New SmoothedLook: shared smoothed yaw/pitch look tick + view seeding (FirstPersonController, FreeCamController) - New CharacterTeleport: disable-CC-around-teleport idiom (rescue, owner position apply, free-cam sync) - New GeometryUtil: RescaleToUnitScale + TransformBounds (concealment meter/volume, skinned collider baker) - Paint family: canonical map read/write shared between Net and Persistence, snapshot decode, backend stroke dispatch, RebuildBackend, ResolveSeed, chunked RPC send/gate/copy, CamoKnobs/HeightKnobs aggregates, SanitizeFileKey, NewMaskTexture, AdoptAlbedo, FindSlider - Player/modes: GameMode SetPlayerControl/SetPainting/SetOrbitInputEnabled, InputUtil.GameplayKeyPressed, NearestClimbHit, ComputeXRay delegates to ComputeResolutionFreeze, FingerIK TryInPlaneAngle - Match/misc: UnhookSessionCallbacks, SendEmptyNamedMessage, SwapScenes, RunSessionOpAsync, CovarianceEigen, CollectClusterPoints, MatchHud GUIStyle cache, HsvColorPicker rehook, VoxelCenter Documentation pass: remove 6 triplicated doc blocks and 2 restating inline comments left after the previous reduction. Behavior-neutral; hot-path rasterizer loops intentionally untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
6.7 KiB
C#
158 lines
6.7 KiB
C#
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;
|
|
|
|
/// <summary>Points the active stamp mask at the resolved standard mask (normal painting).</summary>
|
|
private void UseResolvedStampMask()
|
|
{
|
|
_stampMask = _resolvedMask;
|
|
_stampOpacity = _maskOpacity;
|
|
_stampW = _maskW;
|
|
_stampH = _maskH;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Rasterizes one soft dot (center/radius in uv, value = alpha) into the opacity buffer, compositing by max.</summary>
|
|
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;
|
|
}
|
|
}
|