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:
adminandClaude Opus 4.8 committed 2026-06-24 19:07:28 +02:00
1 parent af3aa34d66
commit b07d9a22d2
9 files changed
+1227 -86

No files matched your search

+42 -46
View File
@@ -182,27 +182,59 @@ public partial class MeshPainter
Graphics.SetRenderTarget(gm.display);
GL.sRGBWrite = gm.sRGB; // sRGB color map vs linear data map (MS)
SetBrushColorForMap(mi);
// Per-channel write mask: lets the shared mask map (R=metallic, G=smoothness work,
// B=occlusion) be painted by metallic/smoothness and occlusion INDEPENDENTLY -
// untouched channels keep their value while coverage (A) still accumulates.
_brushMat.SetFloat(ColorWriteMaskId, BrushColorMaskForMap(mi));
_brushMat.SetTexture(MaskTexId, _resolvedMask);
// Stamp placement is shared by every draw on this map.
_brushMat.SetTexture(MaskTexId, _stampMask);
_brushMat.SetVector(BrushCenterId, pLocal);
_brushMat.SetFloat(BrushRadiusId, radiusLocal);
_brushMat.SetVector(BrushUId, u);
_brushMat.SetVector(BrushVId, v);
_brushMat.SetVector(BrushNormalId, n);
if (!_brushMat.SetPass(0)) continue;
int sub = _bakedMesh.subMeshCount;
for (int i = 0; i < sub; i++)
Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
if (mi == MAP_MSO)
{
// Metallic/smoothness and occlusion share one texture but paint with their OWN
// opacity, so each is a separate stamp (own _BrushColor.a + per-channel write
// mask). Untouched channels keep their value while coverage (A) accumulates.
if (paintMetallicSmoothness)
StampMso(Mathf.Clamp01(metallicSmoothnessOpacity), 13f); // R|G|A (skip B = occlusion)
if (paintOcclusion)
StampMso(Mathf.Clamp01(occlusionOpacity), 3f); // B|A (occlusion only)
}
else
{
// Albedo / emission: full RGBA, opacity carried by the colour's own alpha.
_brushMat.SetColor(BrushColorId, mi == MAP_ALBEDO ? brushColor : emissionColor);
_brushMat.SetFloat(ColorWriteMaskId, 15f);
DrawStamp();
}
gm.drew = true;
}
}
/// <summary>
/// Issues one mask-map stamp: raw (metallic, smoothness, occlusion) data with the given paint
/// opacity in alpha and a per-channel colour write mask, so metallic/smoothness and occlusion
/// stay independently paintable with their own opacity. EncodeData pre-compensates the Color
/// property's sRGB->linear upload so the values round-trip exactly (CPU parity); alpha is not converted.
/// </summary>
private void StampMso(float opacity, float colorMask)
{
_brushMat.SetVector(BrushColorId, new Vector4(
EncodeData(metallic), EncodeData(smoothness), EncodeData(occlusion), opacity));
_brushMat.SetFloat(ColorWriteMaskId, colorMask);
DrawStamp();
}
/// <summary>Draws the baked mesh once per sub-mesh with the current brush pass (UV-space rasterization).</summary>
private void DrawStamp()
{
if (!_brushMat.SetPass(0)) return;
int sub = _bakedMesh.subMeshCount;
for (int i = 0; i < sub; i++)
Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
}
private void EndGpuStroke()
{
Graphics.SetRenderTarget(null);
@@ -230,33 +262,6 @@ public partial class MeshPainter
GL.sRGBWrite = _prevSRGB;
}
/// <summary>Sets _BrushColor for the given map. Albedo/emission are colors; the mask map packs (metallic, smoothness, occlusion) as raw data.</summary>
private void SetBrushColorForMap(int mi)
{
switch (mi)
{
case MAP_ALBEDO:
_brushMat.SetColor(BrushColorId, brushColor);
break;
case MAP_MSO:
// Raw data via SetVector. Work layout: R=metallic, G=smoothness, B=occlusion
// (ColorMask picks the channels). _BrushColor is a Color property, so Unity
// applies sRGB->linear on upload in a linear project - we pre-apply the inverse
// so the data round-trips EXACTLY and matches the CPU's raw byte writes.
_brushMat.SetVector(BrushColorId, new Vector4(
EncodeData(metallic), EncodeData(smoothness), EncodeData(occlusion), 1f));
break;
default: // emission
_brushMat.SetColor(BrushColorId, emissionColor);
break;
}
}
/// <summary>
/// ColorMask bitmask for the brush draw (ColorWriteMask: R=8, G=4, B=2, A=1). Color maps
/// write everything; the mask map writes only the active sub-channels so metallic/smoothness
/// and occlusion can be painted independently into one texture. Coverage (A) is always written.
/// </summary>
/// <summary>
/// Pre-compensates a raw data value (0..1) for the brush's Color property, which Unity
/// converts sRGB->linear on upload in a linear project. Applying the inverse here makes
@@ -268,13 +273,4 @@ public partial class MeshPainter
v = Mathf.Clamp01(v);
return QualitySettings.activeColorSpace == ColorSpace.Linear ? Mathf.LinearToGammaSpace(v) : v;
}
private float BrushColorMaskForMap(int mi)
{
if (mi != MAP_MSO) return 15f; // RGBA
bool ms = paintMetallicSmoothness, occ = paintOcclusion;
if (ms && occ) return 15f; // R|G|B|A
if (ms) return 13f; // R|G|A (skip B = occlusion)
return 3f; // B|A (skip R,G = metallic/smoothness) -> occlusion only
}
}