Add metallic, smoothness, and emission painting to mesh painter
Extend the painter from a single albedo map to three paint maps: albedo (_BaseMap), packed metallic+smoothness (_MetallicGlossMap, R=metallic A=smoothness), and emission (_EmissionMap/_EmissionColor). - Per-channel toggles (paint albedo / metallic+smoothness / emission) - Metallic/Smoothness sliders and emission color + intensity brush params - One brush shader drives all maps; metallic-smoothness uses a work layout (R=metallic G=smoothness A=coverage) and a pack pass (Hidden/MeshPaintPackMS) to the URP layout after each stroke - GPU and CPU backends mirror all three maps with the same mask and seam-dilation logic - Material keywords (_METALLICSPECGLOSSMAP, _EMISSION) enabled and the new maps seeded from the original material values
This commit is contained in:
1 parent
261cd6795e
commit
86c0a33687
3 files changed
+557
-173
No files matched your search
+504
-173
@@ -5,18 +5,19 @@ using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Live mesh painting in Play mode: paints the brush color onto the mesh texture
|
||||
/// with the mouse. Two backends (switchable live via <see cref="useGpu"/>): GPU
|
||||
/// shader painting into a RenderTexture (fast, default) and CPU rasterization
|
||||
/// (slower, identical result). Both paint seamlessly across UV seams because the
|
||||
/// brush is evaluated in 3D local space around the hit point, capturing triangles
|
||||
/// on both sides of a seam at once.
|
||||
/// Live mesh painting in Play mode: paints the brush onto the mesh textures with the
|
||||
/// mouse. Three paint maps are driven at once (each can be toggled): albedo color
|
||||
/// (_BaseMap), metallic + smoothness (_MetallicGlossMap, R = metallic, A = smoothness)
|
||||
/// and emission (_EmissionMap). Two backends (switchable live via <see cref="useGpu"/>):
|
||||
/// GPU shader painting into RenderTextures (fast, default) and CPU rasterization
|
||||
/// (slower, identical result). Both paint seamlessly across UV seams because the brush
|
||||
/// is evaluated in 3D local space around the hit point, capturing triangles on both
|
||||
/// sides of a seam at once.
|
||||
///
|
||||
/// The brush is a square stamp in the surface tangent plane whose shape is given by
|
||||
/// a grayscale mask texture (white = paint, black = skip, hard threshold). A list of
|
||||
/// 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.
|
||||
/// The brush is a square stamp in the surface tangent plane whose shape is given by a
|
||||
/// grayscale mask texture (the mask value = per-texel opacity). A list of 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
|
||||
@@ -52,11 +53,39 @@ public class MeshPainter : MonoBehaviour
|
||||
[Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
|
||||
public bool useGpu = true;
|
||||
|
||||
[Header("Brush")]
|
||||
[Header("Brush - channels")]
|
||||
[Tooltip("Paint the albedo (base color) map.")]
|
||||
public bool paintAlbedo = true;
|
||||
|
||||
[Tooltip("Paint the metallic + smoothness map (_MetallicGlossMap).")]
|
||||
public bool paintMetallicSmoothness = false;
|
||||
|
||||
[Tooltip("Paint the emission map (_EmissionMap).")]
|
||||
public bool paintEmission = false;
|
||||
|
||||
[Header("Brush - albedo")]
|
||||
[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;
|
||||
|
||||
[Header("Brush - metallic / smoothness")]
|
||||
[Tooltip("Metallic value painted into the map (0 = dielectric, 1 = metal).")]
|
||||
[Range(0f, 1f)]
|
||||
public float metallic = 0f;
|
||||
|
||||
[Tooltip("Smoothness value painted into the map (0 = rough, 1 = glossy).")]
|
||||
[Range(0f, 1f)]
|
||||
public float smoothness = 0.5f;
|
||||
|
||||
[Header("Brush - emission")]
|
||||
[Tooltip("Emission color painted into the map. Alpha = global opacity multiplier on top of the mask.")]
|
||||
public Color emissionColor = Color.black;
|
||||
|
||||
[Tooltip("Global emission intensity multiplier on the painted emission (drives the material's _EmissionColor). " +
|
||||
"Above 1 makes the emission HDR/bloom-bright.")]
|
||||
public float emissionIntensity = 1f;
|
||||
|
||||
[Header("Brush - shape")]
|
||||
[Tooltip("Brush half-extent in texels (= half the square side). Converted into a 3D size per hit.")]
|
||||
[Range(1, 256)]
|
||||
public int brushSize = 16;
|
||||
@@ -92,10 +121,10 @@ public class MeshPainter : MonoBehaviour
|
||||
public float previewOpacity = 0.5f;
|
||||
|
||||
[Header("Canvas")]
|
||||
[Tooltip("Resolution of the paint texture (square).")]
|
||||
[Tooltip("Resolution of the paint textures (square).")]
|
||||
public int textureSize = 1024;
|
||||
|
||||
[Tooltip("Base color of the texture at startup (if none exists).")]
|
||||
[Tooltip("Base color of the albedo texture at startup (if none exists).")]
|
||||
public Color baseColor = Color.white;
|
||||
|
||||
[Tooltip("Edge padding (texels) bled beyond UV island edges so bilinear sampling does not pull in the base color. 0 = off.")]
|
||||
@@ -109,6 +138,33 @@ public class MeshPainter : MonoBehaviour
|
||||
[Tooltip("Only for animated meshes: re-bake the collider every frame (expensive!).")]
|
||||
public bool updateColliderEveryFrame = false;
|
||||
|
||||
// --- paint maps ---
|
||||
private const int MAP_ALBEDO = 0;
|
||||
private const int MAP_MS = 1; // metallic (R) + smoothness (A)
|
||||
private const int MAP_EM = 2; // emission (RGB)
|
||||
private const int MAP_COUNT = 3;
|
||||
|
||||
/// <summary>GPU backend per map: a display RenderTexture + scratch for the dilation ping-pong.</summary>
|
||||
private class GpuMap
|
||||
{
|
||||
public RenderTexture display; // painted into (work RT for the MS map)
|
||||
public RenderTexture temp; // dilation ping-pong scratch
|
||||
public RenderTexture final; // MS only: packed (R=metallic, A=smoothness) target; null otherwise
|
||||
public bool drew; // anything drawn this stroke?
|
||||
public bool sRGB; // sRGB color map (albedo/emission) vs linear data map (MS)
|
||||
}
|
||||
|
||||
/// <summary>CPU backend per map: a Texture2D mirror + rasterization/dilation state.</summary>
|
||||
private class CpuMap
|
||||
{
|
||||
public Texture2D tex;
|
||||
public Color32[] pixels;
|
||||
public bool[] painted; // true = painted by the brush (dilation source)
|
||||
public int[] fillStamp; // per-call marker for padding texels
|
||||
public int dilateStamp; // bumped per call so old padding is not a source
|
||||
public int dx0, dy0, dx1, dy1; // dirty rectangle (CPU upload)
|
||||
}
|
||||
|
||||
// --- shared references ---
|
||||
private Renderer _renderer;
|
||||
private SkinnedMeshRenderer _skinned;
|
||||
@@ -129,30 +185,30 @@ public class MeshPainter : MonoBehaviour
|
||||
private bool _gpuActive;
|
||||
private bool _setupValid;
|
||||
|
||||
// --- CPU backend ---
|
||||
private Texture2D _paintTexture;
|
||||
private Color32[] _pixels;
|
||||
private bool[] _painted; // true = painted by the brush (dilation source)
|
||||
private int[] _fillStamp; // per-call marker for padding texels
|
||||
private int _dilateStamp; // bumped per call so old padding is not a source
|
||||
// Seed colors per map in canonical (material) layout, built once at Start:
|
||||
// albedo -> RGB color
|
||||
// MS -> R = metallic, A = smoothness
|
||||
// emission-> RGB emission
|
||||
private Color32[][] _seedCanonical;
|
||||
|
||||
// --- backends ---
|
||||
private GpuMap[] _gpu;
|
||||
private CpuMap[] _cpu;
|
||||
private readonly List<int> _dilateScratch = new List<int>(512);
|
||||
private readonly RaycastHit[] _rayHits = new RaycastHit[32];
|
||||
private int _dx0, _dy0, _dx1, _dy1; // dirty rectangle (CPU upload)
|
||||
|
||||
// --- GPU backend ---
|
||||
public Shader brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
|
||||
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
|
||||
public Shader packMsShader; // optional; else Shader.Find("Hidden/MeshPaintPackMS")
|
||||
private Material _brushMat;
|
||||
private Material _dilateMat;
|
||||
private Material _packMat;
|
||||
private bool _prevSRGB;
|
||||
|
||||
// --- 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;
|
||||
private bool _prevSRGB;
|
||||
|
||||
// --- brush mask (shared) ---
|
||||
private Texture2D _proceduralMask; // built-in circle, used when the list is empty/invalid
|
||||
@@ -171,6 +227,11 @@ public class MeshPainter : MonoBehaviour
|
||||
private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
|
||||
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
|
||||
private static readonly int ColorId = Shader.PropertyToID("_Color");
|
||||
private static readonly int MetallicGlossMapId = Shader.PropertyToID("_MetallicGlossMap");
|
||||
private static readonly int MetallicId = Shader.PropertyToID("_Metallic");
|
||||
private static readonly int SmoothnessId = Shader.PropertyToID("_Smoothness");
|
||||
private static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
|
||||
private static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||||
private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor");
|
||||
private static readonly int BrushCenterId = Shader.PropertyToID("_BrushCenter");
|
||||
private static readonly int BrushRadiusId = Shader.PropertyToID("_BrushRadius");
|
||||
@@ -207,13 +268,15 @@ public class MeshPainter : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Own material instance so the shared asset is not modified; tint to white
|
||||
// so the painted color appears unaltered.
|
||||
// Own material instance so the shared asset is not modified; tint base color to
|
||||
// white so the painted color appears unaltered.
|
||||
_materialInstance = _renderer.material;
|
||||
if (_materialInstance.HasProperty(BaseColorId)) _materialInstance.SetColor(BaseColorId, Color.white);
|
||||
else if (_materialInstance.HasProperty(ColorId)) _materialInstance.SetColor(ColorId, Color.white);
|
||||
|
||||
SetupBackend(useGpu, fromColor32: BuildBaseColors());
|
||||
BuildSeedColors();
|
||||
SetupBackend(useGpu, _seedCanonical);
|
||||
ConfigureMaterialPbr();
|
||||
|
||||
EnsureProceduralMask();
|
||||
ResolveActiveMask();
|
||||
@@ -295,7 +358,37 @@ public class MeshPainter : MonoBehaviour
|
||||
if (sumAreaUv > 1e-9) _avgUvToLocal = (float)System.Math.Sqrt(sumArea3D / sumAreaUv);
|
||||
}
|
||||
|
||||
/// <summary>Builds the base-color texels from the existing material texture or baseColor.</summary>
|
||||
/// <summary>Builds the per-map seed colors (in canonical/material layout) from the existing material.</summary>
|
||||
private void BuildSeedColors()
|
||||
{
|
||||
_seedCanonical = new Color32[MAP_COUNT][];
|
||||
_seedCanonical[MAP_ALBEDO] = BuildBaseColors();
|
||||
|
||||
// Metallic / smoothness from the material scalars (no per-texel map here).
|
||||
float metallic0 = _materialInstance.HasProperty(MetallicId) ? _materialInstance.GetFloat(MetallicId) : 0f;
|
||||
float smoothness0 = _materialInstance.HasProperty(SmoothnessId) ? _materialInstance.GetFloat(SmoothnessId) : 0.5f;
|
||||
_seedCanonical[MAP_MS] = FillCanonical(new Color32(
|
||||
(byte)(Mathf.Clamp01(metallic0) * 255f), 0, 0, (byte)(Mathf.Clamp01(smoothness0) * 255f)));
|
||||
|
||||
// Emission: keep the existing look (black if the material's emission is off).
|
||||
Color emission0 = Color.black;
|
||||
if (_materialInstance.HasProperty(EmissionColorId) &&
|
||||
(_materialInstance.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0)
|
||||
emission0 = _materialInstance.GetColor(EmissionColorId);
|
||||
_seedCanonical[MAP_EM] = FillCanonical(new Color32(
|
||||
(byte)(Mathf.Clamp01(emission0.r) * 255f),
|
||||
(byte)(Mathf.Clamp01(emission0.g) * 255f),
|
||||
(byte)(Mathf.Clamp01(emission0.b) * 255f), 255));
|
||||
}
|
||||
|
||||
private Color32[] FillCanonical(Color32 c)
|
||||
{
|
||||
var px = new Color32[textureSize * textureSize];
|
||||
for (int i = 0; i < px.Length; i++) px[i] = c;
|
||||
return px;
|
||||
}
|
||||
|
||||
/// <summary>Builds the albedo base-color texels from the existing material texture or baseColor.</summary>
|
||||
private Color32[] BuildBaseColors()
|
||||
{
|
||||
var px = new Color32[textureSize * textureSize];
|
||||
@@ -324,104 +417,226 @@ public class MeshPainter : MonoBehaviour
|
||||
return px;
|
||||
}
|
||||
|
||||
/// <summary>Enables the URP keywords for the painted PBR maps and seeds the material scalars.</summary>
|
||||
private void ConfigureMaterialPbr()
|
||||
{
|
||||
if (!_materialInstance) return;
|
||||
|
||||
// Metallic-smoothness map: metallic from R, smoothness from the map's alpha
|
||||
// (NOT the albedo alpha) scaled by _Smoothness (= 1 so the map drives it as-is).
|
||||
if (_materialInstance.HasProperty(MetallicGlossMapId))
|
||||
{
|
||||
_materialInstance.EnableKeyword("_METALLICSPECGLOSSMAP");
|
||||
_materialInstance.DisableKeyword("_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A");
|
||||
if (_materialInstance.HasProperty(SmoothnessId)) _materialInstance.SetFloat(SmoothnessId, 1f);
|
||||
}
|
||||
|
||||
// Emission map x _EmissionColor (white * intensity). Clear the "emissive is black"
|
||||
// GI flag so the emission is actually used.
|
||||
if (_materialInstance.HasProperty(EmissionMapId))
|
||||
{
|
||||
_materialInstance.EnableKeyword("_EMISSION");
|
||||
_materialInstance.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
|
||||
RefreshEmissionColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pushes the global emission intensity to the material's _EmissionColor (live-tunable).</summary>
|
||||
private void RefreshEmissionColor()
|
||||
{
|
||||
if (_materialInstance && _materialInstance.HasProperty(EmissionColorId))
|
||||
_materialInstance.SetColor(EmissionColorId, Color.white * Mathf.Max(0f, emissionIntensity));
|
||||
}
|
||||
|
||||
// --- Backend selection / switching ---
|
||||
|
||||
private void SetupBackend(bool gpu, Color32[] fromColor32)
|
||||
private void SetupBackend(bool gpu, Color32[][] canonical)
|
||||
{
|
||||
if (gpu) SetupGpu(fromColor32);
|
||||
else SetupCpu(fromColor32);
|
||||
if (gpu) SetupGpu(canonical);
|
||||
else SetupCpu(canonical);
|
||||
_gpuActive = gpu;
|
||||
}
|
||||
|
||||
/// <summary>Switches the backend at runtime, carrying over the painted content.</summary>
|
||||
/// <summary>Switches the backend at runtime, carrying over all painted maps.</summary>
|
||||
private void SwitchBackend(bool toGpu)
|
||||
{
|
||||
Color32[] current = (_gpuActive ? ReadGpuPixels() : (Color32[])_pixels.Clone());
|
||||
var canonical = new Color32[MAP_COUNT][];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++) canonical[mi] = ReadCanonical(mi);
|
||||
|
||||
if (_gpuActive) TeardownGpu();
|
||||
else TeardownCpu();
|
||||
|
||||
SetupBackend(toGpu, current);
|
||||
SetupBackend(toGpu, canonical);
|
||||
_hasLastScreen = false;
|
||||
|
||||
Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this);
|
||||
}
|
||||
|
||||
/// <summary>Reads a map from the active backend into canonical (material) layout.</summary>
|
||||
private Color32[] ReadCanonical(int mi)
|
||||
{
|
||||
if (_gpuActive)
|
||||
{
|
||||
var gm = _gpu[mi];
|
||||
// MS: read the packed final RT (R=metallic, A=smoothness). Others: the display RT.
|
||||
return (mi == MAP_MS) ? ReadRtPixels(gm.final, linear: true)
|
||||
: ReadRtPixels(gm.display, linear: false);
|
||||
}
|
||||
return (Color32[])_cpu[mi].pixels.Clone();
|
||||
}
|
||||
|
||||
private static bool MapIsLinear(int mi) => mi == MAP_MS;
|
||||
|
||||
private string[] MapProps(int mi)
|
||||
{
|
||||
switch (mi)
|
||||
{
|
||||
case MAP_ALBEDO: return new[] { "_BaseMap", "_MainTex" };
|
||||
case MAP_MS: return new[] { "_MetallicGlossMap" };
|
||||
default: return new[] { "_EmissionMap" };
|
||||
}
|
||||
}
|
||||
|
||||
private void AssignMapTexture(int mi, Texture tex)
|
||||
{
|
||||
if (!_materialInstance) return;
|
||||
foreach (var p in MapProps(mi))
|
||||
{
|
||||
int id = Shader.PropertyToID(p);
|
||||
if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex);
|
||||
}
|
||||
}
|
||||
|
||||
// --- CPU backend ---
|
||||
|
||||
private void SetupCpu(Color32[] initial)
|
||||
private void SetupCpu(Color32[][] canonical)
|
||||
{
|
||||
_paintTexture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false)
|
||||
_cpu = new CpuMap[MAP_COUNT];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
name = "MeshPainter_Canvas",
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear
|
||||
};
|
||||
var cm = new CpuMap
|
||||
{
|
||||
tex = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, MapIsLinear(mi))
|
||||
{
|
||||
name = "MeshPainter_Canvas_" + mi,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear
|
||||
},
|
||||
pixels = SeedCpuPixels(mi, canonical),
|
||||
painted = new bool[textureSize * textureSize],
|
||||
fillStamp = new int[textureSize * textureSize],
|
||||
dilateStamp = 0
|
||||
};
|
||||
cm.tex.SetPixels32(cm.pixels);
|
||||
cm.tex.Apply(false);
|
||||
_cpu[mi] = cm;
|
||||
AssignMapTexture(mi, cm.tex);
|
||||
}
|
||||
}
|
||||
|
||||
_pixels = (initial != null && initial.Length == textureSize * textureSize)
|
||||
? initial
|
||||
: BuildBaseColors();
|
||||
|
||||
_painted = new bool[textureSize * textureSize];
|
||||
_fillStamp = new int[textureSize * textureSize];
|
||||
_dilateStamp = 0;
|
||||
|
||||
_paintTexture.SetPixels32(_pixels);
|
||||
_paintTexture.Apply(false);
|
||||
|
||||
AssignTextureToMaterial(_paintTexture);
|
||||
/// <summary>CPU pixels are stored directly in material layout; just guarantee a sane alpha for the color maps.</summary>
|
||||
private Color32[] SeedCpuPixels(int mi, Color32[][] canonical)
|
||||
{
|
||||
var src = (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
||||
? canonical[mi] : _seedCanonical[mi];
|
||||
var px = (Color32[])src.Clone();
|
||||
if (mi != MAP_MS) // albedo / emission: opaque (alpha unused by the material)
|
||||
for (int i = 0; i < px.Length; i++) px[i].a = 255;
|
||||
return px;
|
||||
}
|
||||
|
||||
private void TeardownCpu()
|
||||
{
|
||||
if (_paintTexture) { Destroy(_paintTexture); _paintTexture = null; }
|
||||
_pixels = null;
|
||||
_painted = null;
|
||||
_fillStamp = null;
|
||||
if (_cpu == null) return;
|
||||
foreach (var cm in _cpu) if (cm != null && cm.tex) Destroy(cm.tex);
|
||||
_cpu = null;
|
||||
}
|
||||
|
||||
// --- GPU backend ---
|
||||
|
||||
private void SetupGpu(Color32[] initial)
|
||||
private void SetupGpu(Color32[][] canonical)
|
||||
{
|
||||
if (!brushShader) brushShader = Shader.Find("Hidden/MeshPaintBrush");
|
||||
if (!dilateShader) dilateShader = Shader.Find("Hidden/MeshPaintDilate");
|
||||
if (!packMsShader) packMsShader = Shader.Find("Hidden/MeshPaintPackMS");
|
||||
|
||||
if (!brushShader || !dilateShader)
|
||||
if (!brushShader || !dilateShader || !packMsShader)
|
||||
{
|
||||
Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush or " +
|
||||
"Hidden/MeshPaintDilate). Falling back to CPU painting.", this);
|
||||
Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush, " +
|
||||
"Hidden/MeshPaintDilate or Hidden/MeshPaintPackMS). Falling back to CPU painting.", this);
|
||||
useGpu = false;
|
||||
SetupCpu(initial);
|
||||
SetupCpu(canonical);
|
||||
_gpuActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_brushMat = new Material(brushShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
_dilateMat = new Material(dilateShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
_packMat = new Material(packMsShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
|
||||
_displayRT = CreateRT("MeshPainter_DisplayRT");
|
||||
_tempRT = CreateRT("MeshPainter_TempRT");
|
||||
_gpu = new GpuMap[MAP_COUNT];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
bool linear = MapIsLinear(mi);
|
||||
var gm = new GpuMap
|
||||
{
|
||||
sRGB = !linear,
|
||||
display = CreateRT("MeshPainter_DisplayRT_" + mi, linear),
|
||||
temp = CreateRT("MeshPainter_TempRT_" + mi, linear),
|
||||
final = (mi == MAP_MS) ? CreateRT("MeshPainter_FinalRT_" + mi, linear) : null
|
||||
};
|
||||
_gpu[mi] = gm;
|
||||
|
||||
// Seed the display RT. Alpha = 0 marks "unpainted" so the base color is not
|
||||
// mistaken for a dilation source.
|
||||
var seed = initial ?? BuildBaseColors();
|
||||
UploadColorsToRT(seed, _displayRT, paintedAlpha: false);
|
||||
SeedGpuMap(mi, canonical);
|
||||
|
||||
AssignTextureToMaterial(_displayRT);
|
||||
// Color maps render to the display RT; the MS map exposes its packed final RT.
|
||||
AssignMapTexture(mi, gm.final != null ? gm.final : gm.display);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Seeds a GPU map's display RT from canonical data (alpha = 0 = unpainted), then packs MS to final.</summary>
|
||||
private void SeedGpuMap(int mi, Color32[][] canonical)
|
||||
{
|
||||
var src = (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
||||
? canonical[mi] : _seedCanonical[mi];
|
||||
var gm = _gpu[mi];
|
||||
|
||||
if (mi == MAP_MS)
|
||||
{
|
||||
// Canonical (R=metallic, A=smoothness) -> work layout (R=metallic, G=smoothness, B=0).
|
||||
var work = new Color32[src.Length];
|
||||
for (int i = 0; i < src.Length; i++) work[i] = new Color32(src[i].r, src[i].a, 0, 0);
|
||||
UploadColorsToRT(work, gm.display, paintedAlpha: false, linear: true);
|
||||
PackMs(gm); // build the final RT once so the material shows the seeded values
|
||||
}
|
||||
else
|
||||
{
|
||||
UploadColorsToRT((Color32[])src.Clone(), gm.display, paintedAlpha: false, linear: gm.sRGB == false);
|
||||
}
|
||||
}
|
||||
|
||||
private void TeardownGpu()
|
||||
{
|
||||
if (_displayRT) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; }
|
||||
if (_tempRT) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; }
|
||||
if (_gpu != null)
|
||||
{
|
||||
foreach (var gm in _gpu)
|
||||
{
|
||||
if (gm == null) continue;
|
||||
if (gm.display) { gm.display.Release(); Destroy(gm.display); }
|
||||
if (gm.temp) { gm.temp.Release(); Destroy(gm.temp); }
|
||||
if (gm.final) { gm.final.Release(); Destroy(gm.final); }
|
||||
}
|
||||
_gpu = null;
|
||||
}
|
||||
if (_brushMat) { Destroy(_brushMat); _brushMat = null; }
|
||||
if (_dilateMat) { Destroy(_dilateMat); _dilateMat = null; }
|
||||
if (_packMat) { Destroy(_packMat); _packMat = null; }
|
||||
}
|
||||
|
||||
private RenderTexture CreateRT(string rtName)
|
||||
private RenderTexture CreateRT(string rtName, bool linear)
|
||||
{
|
||||
var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB)
|
||||
var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGB32,
|
||||
linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.sRGB)
|
||||
{
|
||||
name = rtName,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
@@ -435,30 +650,28 @@ public class MeshPainter : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>Writes Color32 data into a RenderTexture via a temporary texture.</summary>
|
||||
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha)
|
||||
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha, bool linear)
|
||||
{
|
||||
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false);
|
||||
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
|
||||
// Alpha marks painted (255) vs unpainted (0) for the dilation pass.
|
||||
if (paintedAlpha)
|
||||
{
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = 255;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
|
||||
}
|
||||
byte a = paintedAlpha ? (byte)255 : (byte)0;
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = a;
|
||||
tmp.SetPixels32(colors);
|
||||
tmp.Apply(false);
|
||||
|
||||
bool prev = GL.sRGBWrite;
|
||||
GL.sRGBWrite = !linear;
|
||||
Graphics.Blit(tmp, rt);
|
||||
GL.sRGBWrite = prev;
|
||||
Destroy(tmp);
|
||||
}
|
||||
|
||||
/// <summary>Reads the display RT back as Color32 (for the backend switch).</summary>
|
||||
private Color32[] ReadGpuPixels()
|
||||
/// <summary>Reads a RenderTexture back as Color32 (for the backend switch).</summary>
|
||||
private Color32[] ReadRtPixels(RenderTexture rt, bool linear)
|
||||
{
|
||||
var prev = RenderTexture.active;
|
||||
RenderTexture.active = _displayRT;
|
||||
var tex = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false);
|
||||
RenderTexture.active = rt;
|
||||
var tex = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear);
|
||||
tex.ReadPixels(new Rect(0, 0, textureSize, textureSize), 0, 0, false);
|
||||
tex.Apply(false);
|
||||
RenderTexture.active = prev;
|
||||
@@ -467,13 +680,12 @@ public class MeshPainter : MonoBehaviour
|
||||
return px;
|
||||
}
|
||||
|
||||
// --- Material ---
|
||||
|
||||
private void AssignTextureToMaterial(Texture tex)
|
||||
private void PackMs(GpuMap gm)
|
||||
{
|
||||
if (!_materialInstance) return;
|
||||
if (_materialInstance.HasProperty(BaseMapId)) _materialInstance.SetTexture(BaseMapId, tex);
|
||||
if (_materialInstance.HasProperty(MainTexId)) _materialInstance.SetTexture(MainTexId, tex);
|
||||
bool prev = GL.sRGBWrite;
|
||||
GL.sRGBWrite = false; // linear data, no conversion
|
||||
Graphics.Blit(gm.display, gm.final, _packMat, 0);
|
||||
GL.sRGBWrite = prev;
|
||||
}
|
||||
|
||||
// --- Brush mask ---
|
||||
@@ -514,7 +726,7 @@ public class MeshPainter : MonoBehaviour
|
||||
_procH = s;
|
||||
}
|
||||
|
||||
/// <summary>Picks the active mask (list entry or procedural circle) and refreshes the CPU luminance cache on change.</summary>
|
||||
/// <summary>Picks the active mask (list entry or procedural circle) and refreshes the CPU opacity cache on change.</summary>
|
||||
private void ResolveActiveMask()
|
||||
{
|
||||
Texture2D desired = null;
|
||||
@@ -747,6 +959,8 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
if (useGpu != _gpuActive) SwitchBackend(useGpu);
|
||||
|
||||
RefreshEmissionColor(); // live-tunable emission intensity
|
||||
|
||||
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
|
||||
if (!PaintingEnabled)
|
||||
{
|
||||
@@ -778,16 +992,17 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
if (!paintCamera) return;
|
||||
|
||||
bool anyChannel = paintAlbedo || paintMetallicSmoothness || paintEmission;
|
||||
float moveDist = _hasLastScreen ? Vector2.Distance(_lastScreen, screenPos) : 0f;
|
||||
|
||||
// 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;
|
||||
bool stamp = anyChannel && (!_hasLastScreen || !paintOnMoveOnly || moveDist > MoveOnlyMinPixels);
|
||||
|
||||
if (stamp)
|
||||
{
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; }
|
||||
else ResetCpuDirty();
|
||||
|
||||
if (_hasLastScreen)
|
||||
{
|
||||
@@ -803,11 +1018,7 @@ public class MeshPainter : MonoBehaviour
|
||||
}
|
||||
|
||||
if (_gpuActive) EndGpuStroke();
|
||||
else
|
||||
{
|
||||
if (paddingTexels > 0) DilatePadding(paddingTexels);
|
||||
FlushDirty();
|
||||
}
|
||||
else EndCpuStroke();
|
||||
}
|
||||
|
||||
_lastScreen = screenPos;
|
||||
@@ -889,59 +1100,141 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void BeginGpuStroke()
|
||||
{
|
||||
_drewThisStroke = false;
|
||||
_prevSRGB = GL.sRGBWrite;
|
||||
GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT
|
||||
Graphics.SetRenderTarget(_displayRT);
|
||||
_brushMat.SetColor(BrushColorId, brushColor);
|
||||
_brushMat.SetTexture(MaskTexId, _resolvedMask);
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
if (IsChannelActive(mi)) _gpu[mi].drew = false;
|
||||
}
|
||||
|
||||
private void DrawBrushGpu(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
||||
{
|
||||
_brushMat.SetVector(BrushCenterId, pLocal);
|
||||
_brushMat.SetFloat(BrushRadiusId, radiusLocal);
|
||||
_brushMat.SetVector(BrushUId, u);
|
||||
_brushMat.SetVector(BrushVId, v);
|
||||
_brushMat.SetVector(BrushNormalId, n);
|
||||
if (!_brushMat.SetPass(0)) return;
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
if (!IsChannelActive(mi)) continue;
|
||||
var gm = _gpu[mi];
|
||||
|
||||
int sub = _bakedMesh.subMeshCount;
|
||||
for (int i = 0; i < sub; i++)
|
||||
Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
|
||||
Graphics.SetRenderTarget(gm.display);
|
||||
GL.sRGBWrite = gm.sRGB; // sRGB color map vs linear data map (MS)
|
||||
|
||||
_drewThisStroke = true;
|
||||
SetBrushColorForMap(mi);
|
||||
_brushMat.SetTexture(MaskTexId, _resolvedMask);
|
||||
_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);
|
||||
|
||||
gm.drew = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void EndGpuStroke()
|
||||
{
|
||||
Graphics.SetRenderTarget(null);
|
||||
|
||||
// Edge padding against UV seams: dilate the display RT into _tempRT, then swap.
|
||||
if (_drewThisStroke && paddingTexels > 0)
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
_dilateMat.SetInt(PadId, paddingTexels);
|
||||
Graphics.Blit(_displayRT, _tempRT, _dilateMat, 0);
|
||||
if (!IsChannelActive(mi)) continue;
|
||||
var gm = _gpu[mi];
|
||||
|
||||
var swap = _displayRT; _displayRT = _tempRT; _tempRT = swap;
|
||||
AssignTextureToMaterial(_displayRT);
|
||||
// Edge padding against UV seams: dilate the display RT into temp, then swap.
|
||||
if (gm.drew && paddingTexels > 0)
|
||||
{
|
||||
GL.sRGBWrite = gm.sRGB;
|
||||
_dilateMat.SetInt(PadId, paddingTexels);
|
||||
Graphics.Blit(gm.display, gm.temp, _dilateMat, 0);
|
||||
|
||||
var swap = gm.display; gm.display = gm.temp; gm.temp = swap;
|
||||
if (gm.final == null) AssignMapTexture(mi, gm.display);
|
||||
}
|
||||
|
||||
// MS: pack the work layout (R=metallic, G=smoothness) into the URP final layout.
|
||||
if (gm.final != null) PackMs(gm);
|
||||
}
|
||||
|
||||
GL.sRGBWrite = _prevSRGB;
|
||||
}
|
||||
|
||||
/// <summary>Sets _BrushColor for the given map. Albedo/emission are colors; MS packs (metallic, smoothness) as raw data.</summary>
|
||||
private void SetBrushColorForMap(int mi)
|
||||
{
|
||||
switch (mi)
|
||||
{
|
||||
case MAP_ALBEDO:
|
||||
_brushMat.SetColor(BrushColorId, brushColor);
|
||||
break;
|
||||
case MAP_MS:
|
||||
// SetVector (not SetColor) so the raw metallic/smoothness pass through unconverted.
|
||||
_brushMat.SetVector(BrushColorId, new Vector4(Mathf.Clamp01(metallic), Mathf.Clamp01(smoothness), 0f, 1f));
|
||||
break;
|
||||
default: // emission
|
||||
_brushMat.SetColor(BrushColorId, emissionColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsChannelActive(int mi)
|
||||
{
|
||||
switch (mi)
|
||||
{
|
||||
case MAP_ALBEDO: return paintAlbedo;
|
||||
case MAP_MS: return paintMetallicSmoothness;
|
||||
default: return paintEmission;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CPU painting (rasterization + dilation) ---
|
||||
|
||||
private void ResetCpuDirty()
|
||||
{
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
if (!IsChannelActive(mi)) continue;
|
||||
var cm = _cpu[mi];
|
||||
cm.dx0 = textureSize; cm.dy0 = textureSize; cm.dx1 = -1; cm.dy1 = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void EndCpuStroke()
|
||||
{
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
if (!IsChannelActive(mi)) continue;
|
||||
var cm = _cpu[mi];
|
||||
if (paddingTexels > 0) DilatePadding(cm, paddingTexels);
|
||||
FlushDirty(cm);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paints a square stamp in the surface tangent plane: rasterizes triangles in
|
||||
/// range and blends every texel whose 3D position lies inside the box (|du|,|dv|,
|
||||
/// |dn| <= radius) over the canvas, weighted by the mask opacity. Mirrors the
|
||||
/// GPU brush shader (mask value = per-texel opacity, gray = 50%).
|
||||
/// Paints a square stamp in the surface tangent plane: rasterizes triangles in range
|
||||
/// and blends every texel whose 3D position lies inside the box (|du|,|dv|,|dn| <=
|
||||
/// radius) over each active map, weighted by the mask opacity. Mirrors the GPU brush
|
||||
/// path (mask value = per-texel opacity, gray = 50%).
|
||||
/// </summary>
|
||||
private void PaintStamp3D(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
||||
{
|
||||
bool aAlb = paintAlbedo, aMS = paintMetallicSmoothness, aEM = paintEmission;
|
||||
if (!aAlb && !aMS && !aEM) return;
|
||||
|
||||
// Albedo target.
|
||||
float br = brushColor.r * 255f, bg = brushColor.g * 255f, bb = brushColor.b * 255f;
|
||||
float globalOpacity = Mathf.Clamp01(brushColor.a);
|
||||
float albOpMul = Mathf.Clamp01(brushColor.a);
|
||||
CpuMap alb = _cpu[MAP_ALBEDO];
|
||||
|
||||
// Metallic-smoothness target (R=metallic, A=smoothness).
|
||||
float msR = Mathf.Clamp01(metallic) * 255f, msA = Mathf.Clamp01(smoothness) * 255f;
|
||||
CpuMap ms = _cpu[MAP_MS];
|
||||
|
||||
// Emission target.
|
||||
float er = emissionColor.r * 255f, eg = emissionColor.g * 255f, eb = emissionColor.b * 255f;
|
||||
float emOpMul = Mathf.Clamp01(emissionColor.a);
|
||||
CpuMap em = _cpu[MAP_EM];
|
||||
|
||||
float invR = 1f / radiusLocal;
|
||||
int triCount = _tris.Length / 3;
|
||||
float ts = textureSize;
|
||||
@@ -1003,45 +1296,87 @@ public class MeshPainter : MonoBehaviour
|
||||
if (dv < -radiusLocal || dv > radiusLocal) continue;
|
||||
if (dn < -radiusLocal || dn > radiusLocal) continue;
|
||||
|
||||
// Mask -> opacity, then source-over blend (matches the GPU path).
|
||||
// Mask -> opacity.
|
||||
float mu = du * invR * 0.5f + 0.5f;
|
||||
float mv = dv * invR * 0.5f + 0.5f;
|
||||
float opacity = SampleMaskOpacity(mu, mv) * globalOpacity;
|
||||
if (opacity <= 0f) continue;
|
||||
float maskOp = SampleMaskOpacity(mu, mv);
|
||||
if (maskOp <= 0f) continue;
|
||||
|
||||
int idx = rowStart + x;
|
||||
Color32 dst = _pixels[idx];
|
||||
float it = 1f - opacity;
|
||||
_pixels[idx] = new Color32(
|
||||
(byte)(dst.r * it + br * opacity),
|
||||
(byte)(dst.g * it + bg * opacity),
|
||||
(byte)(dst.b * it + bb * opacity),
|
||||
255);
|
||||
_painted[idx] = true;
|
||||
|
||||
if (x < _dx0) _dx0 = x;
|
||||
if (x > _dx1) _dx1 = x;
|
||||
if (y < _dy0) _dy0 = y;
|
||||
if (y > _dy1) _dy1 = y;
|
||||
if (aAlb)
|
||||
{
|
||||
float op = maskOp * albOpMul;
|
||||
if (op > 0f)
|
||||
{
|
||||
Color32 dst = alb.pixels[idx];
|
||||
float it = 1f - op;
|
||||
alb.pixels[idx] = new Color32(
|
||||
(byte)(dst.r * it + br * op),
|
||||
(byte)(dst.g * it + bg * op),
|
||||
(byte)(dst.b * it + bb * op),
|
||||
255);
|
||||
alb.painted[idx] = true;
|
||||
ExpandDirty(alb, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (aMS)
|
||||
{
|
||||
// msOpacity = mask (no extra alpha multiplier).
|
||||
Color32 dst = ms.pixels[idx];
|
||||
float it = 1f - maskOp;
|
||||
ms.pixels[idx] = new Color32(
|
||||
(byte)(dst.r * it + msR * maskOp), // metallic
|
||||
0, 0,
|
||||
(byte)(dst.a * it + msA * maskOp)); // smoothness
|
||||
ms.painted[idx] = true;
|
||||
ExpandDirty(ms, x, y);
|
||||
}
|
||||
|
||||
if (aEM)
|
||||
{
|
||||
float op = maskOp * emOpMul;
|
||||
if (op > 0f)
|
||||
{
|
||||
Color32 dst = em.pixels[idx];
|
||||
float it = 1f - op;
|
||||
em.pixels[idx] = new Color32(
|
||||
(byte)(dst.r * it + er * op),
|
||||
(byte)(dst.g * it + eg * op),
|
||||
(byte)(dst.b * it + eb * op),
|
||||
255);
|
||||
em.painted[idx] = true;
|
||||
ExpandDirty(em, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CPU edge padding against UV seams: bleeds the freshly painted colors outward
|
||||
/// by 'pad' texels. The dilation source is only brush-painted texels (_painted).
|
||||
/// </summary>
|
||||
private void DilatePadding(int pad)
|
||||
private static void ExpandDirty(CpuMap cm, int x, int y)
|
||||
{
|
||||
if (_dx1 < _dx0 || _dy1 < _dy0) return; // nothing painted
|
||||
if (x < cm.dx0) cm.dx0 = x;
|
||||
if (x > cm.dx1) cm.dx1 = x;
|
||||
if (y < cm.dy0) cm.dy0 = y;
|
||||
if (y > cm.dy1) cm.dy1 = y;
|
||||
}
|
||||
|
||||
int x0 = Mathf.Max(0, _dx0 - pad);
|
||||
int y0 = Mathf.Max(0, _dy0 - pad);
|
||||
int x1 = Mathf.Min(textureSize - 1, _dx1 + pad);
|
||||
int y1 = Mathf.Min(textureSize - 1, _dy1 + pad);
|
||||
/// <summary>
|
||||
/// CPU edge padding against UV seams: bleeds the freshly painted colors outward by
|
||||
/// 'pad' texels. The dilation source is only brush-painted texels (cm.painted).
|
||||
/// </summary>
|
||||
private void DilatePadding(CpuMap cm, int pad)
|
||||
{
|
||||
if (cm.dx1 < cm.dx0 || cm.dy1 < cm.dy0) return; // nothing painted
|
||||
|
||||
_dilateStamp++;
|
||||
int x0 = Mathf.Max(0, cm.dx0 - pad);
|
||||
int y0 = Mathf.Max(0, cm.dy0 - pad);
|
||||
int x1 = Mathf.Min(textureSize - 1, cm.dx1 + pad);
|
||||
int y1 = Mathf.Min(textureSize - 1, cm.dy1 + pad);
|
||||
|
||||
cm.dilateStamp++;
|
||||
|
||||
for (int iter = 0; iter < pad; iter++)
|
||||
{
|
||||
@@ -1053,30 +1388,26 @@ public class MeshPainter : MonoBehaviour
|
||||
for (int x = x0; x <= x1; x++)
|
||||
{
|
||||
int idx = row + x;
|
||||
if (_painted[idx] || _fillStamp[idx] == _dilateStamp) continue;
|
||||
if (cm.painted[idx] || cm.fillStamp[idx] == cm.dilateStamp) continue;
|
||||
|
||||
if (TryFindPaddingSource(x, y, out Color32 c))
|
||||
if (TryFindPaddingSource(cm, x, y, out Color32 c))
|
||||
{
|
||||
_pixels[idx] = c;
|
||||
cm.pixels[idx] = c;
|
||||
_dilateScratch.Add(idx);
|
||||
|
||||
if (x < _dx0) _dx0 = x;
|
||||
if (x > _dx1) _dx1 = x;
|
||||
if (y < _dy0) _dy0 = y;
|
||||
if (y > _dy1) _dy1 = y;
|
||||
ExpandDirty(cm, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int k = 0; k < _dilateScratch.Count; k++)
|
||||
_fillStamp[_dilateScratch[k]] = _dilateStamp;
|
||||
cm.fillStamp[_dilateScratch[k]] = cm.dilateStamp;
|
||||
|
||||
if (_dilateScratch.Count == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Searches the 8-neighborhood for a source texel (painted or already padded) and returns its color.</summary>
|
||||
private bool TryFindPaddingSource(int x, int y, out Color32 col)
|
||||
private bool TryFindPaddingSource(CpuMap cm, int x, int y, out Color32 col)
|
||||
{
|
||||
for (int oy = -1; oy <= 1; oy++)
|
||||
{
|
||||
@@ -1089,9 +1420,9 @@ public class MeshPainter : MonoBehaviour
|
||||
int nx = x + ox;
|
||||
if (nx < 0 || nx >= textureSize) continue;
|
||||
int nidx = nrow + nx;
|
||||
if (_painted[nidx] || _fillStamp[nidx] == _dilateStamp)
|
||||
if (cm.painted[nidx] || cm.fillStamp[nidx] == cm.dilateStamp)
|
||||
{
|
||||
col = _pixels[nidx];
|
||||
col = cm.pixels[nidx];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1101,18 +1432,18 @@ public class MeshPainter : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>Uploads only the region changed this frame to the GPU once (CPU backend).</summary>
|
||||
private void FlushDirty()
|
||||
private void FlushDirty(CpuMap cm)
|
||||
{
|
||||
if (_dx1 < _dx0 || _dy1 < _dy0) return; // nothing painted
|
||||
if (cm.dx1 < cm.dx0 || cm.dy1 < cm.dy0) return; // nothing painted
|
||||
|
||||
int bw = _dx1 - _dx0 + 1;
|
||||
int bh = _dy1 - _dy0 + 1;
|
||||
int bw = cm.dx1 - cm.dx0 + 1;
|
||||
int bh = cm.dy1 - cm.dy0 + 1;
|
||||
var block = new Color32[bw * bh];
|
||||
for (int y = 0; y < bh; y++)
|
||||
System.Array.Copy(_pixels, (_dy0 + y) * textureSize + _dx0, block, y * bw, bw);
|
||||
System.Array.Copy(cm.pixels, (cm.dy0 + y) * textureSize + cm.dx0, block, y * bw, bw);
|
||||
|
||||
_paintTexture.SetPixels32(_dx0, _dy0, bw, bh, block);
|
||||
_paintTexture.Apply(false);
|
||||
cm.tex.SetPixels32(cm.dx0, cm.dy0, bw, bh, block);
|
||||
cm.tex.Apply(false);
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Packs the metallic-smoothness WORK layout into the URP _MetallicGlossMap layout.
|
||||
//
|
||||
// While painting, the metallic-smoothness map is kept in a "work" RenderTexture as
|
||||
// R = metallic, G = smoothness, A = coverage (painted marker for the dilation pass).
|
||||
// This lets the same brush + dilate shaders (which use alpha as the coverage marker)
|
||||
// drive it exactly like the albedo map. But URP's _MetallicGlossMap expects
|
||||
// R = metallic, A = smoothness.
|
||||
// This pass converts work -> final after each stroke: out = (work.r, 0, 0, work.g).
|
||||
//
|
||||
// Both work and final are linear RenderTextures (metallic/smoothness are data, not
|
||||
// color), so no gamma conversion happens.
|
||||
Shader "Hidden/MeshPaintPackMS"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_MainTex ("Texture", 2D) = "white" {}
|
||||
}
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType" = "Opaque" }
|
||||
Pass
|
||||
{
|
||||
ZTest Always
|
||||
ZWrite Off
|
||||
Cull Off
|
||||
Blend Off
|
||||
|
||||
CGPROGRAM
|
||||
#pragma vertex vert_img
|
||||
#pragma fragment frag
|
||||
#include "UnityCG.cginc"
|
||||
|
||||
sampler2D _MainTex;
|
||||
|
||||
fixed4 frag (v2f_img i) : SV_Target
|
||||
{
|
||||
fixed4 c = tex2D(_MainTex, i.uv);
|
||||
return fixed4(c.r, 0.0, 0.0, c.g); // R = metallic, A = smoothness
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
Fallback Off
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b7306b5c0c771147ab18037e40a820d
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user