using UnityEngine; public partial class MeshPainter { // --- GPU backend --- /// GPU backend per map: a display RenderTexture + scratch for the dilation ping-pong. private class GpuMap { public RenderTexture display; // painted into (work RT for the mask map) public RenderTexture temp; // dilation ping-pong scratch public RenderTexture final; // MSO only: packed mask map (R=metallic, G=occlusion, A=smoothness); null otherwise public bool drew; // anything drawn this stroke? public bool sRGB; // sRGB color map (albedo/emission) vs linear data map (MSO) } 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 GpuMap[] _gpu; private Material _brushMat; private Material _dilateMat; private Material _packMat; private bool _prevSRGB; /// /// Sets up the GPU backend (RenderTexture set + materials per map). Returns true on /// success; on missing shaders it falls back to the CPU backend and returns false so /// the caller leaves _gpuActive=false (and useGpu in sync) instead of crashing later. /// private bool 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 || !packMsShader) { Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush, " + "Hidden/MeshPaintDilate or Hidden/MeshPaintPackMS). Falling back to CPU painting.", this); useGpu = false; SetupCpu(canonical); return false; } _brushMat = new Material(brushShader) { hideFlags = HideFlags.HideAndDontSave }; _dilateMat = new Material(dilateShader) { hideFlags = HideFlags.HideAndDontSave }; _packMat = new Material(packMsShader) { hideFlags = HideFlags.HideAndDontSave }; _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_MSO) ? CreateRT("MeshPainter_FinalRT_" + mi, linear) : null }; _gpu[mi] = gm; SeedGpuMap(mi, canonical); // Color maps render to the display RT; the MS map exposes its packed final RT. AssignMapTexture(mi, gm.final != null ? gm.final : gm.display); } return true; } /// Seeds a GPU map's display RT from canonical data (alpha = 0 = unpainted), then packs MS to final. 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_MSO) { // Canonical (R=metallic, G=occlusion, A=smoothness) -> work layout // (R=metallic, G=smoothness, B=occlusion); alpha = coverage stays 0 (unpainted). var work = new Color32[src.Length]; for (int i = 0; i < src.Length; i++) work[i] = new Color32(src[i].r, src[i].a, src[i].g, 0); UploadColorsToRT(work, gm.display, paintedAlpha: false, linear: true); PackMs(gm); // build the final mask 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 (_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, bool linear) { var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGB32, linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.sRGB) { name = rtName, wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, antiAliasing = 1, useMipMap = false, autoGenerateMips = false }; rt.Create(); return rt; } /// Writes Color32 data into a RenderTexture via a temporary texture. private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha, bool linear) { var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, linear); // Alpha marks painted (255) vs unpainted (0) for the dilation pass. 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); } /// Reads a RenderTexture back as Color32 (for the backend switch). private Color32[] ReadRtPixels(RenderTexture rt, bool linear) { var prev = RenderTexture.active; 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; var px = tex.GetPixels32(); Destroy(tex); return px; } private void PackMs(GpuMap gm) { bool prev = GL.sRGBWrite; GL.sRGBWrite = false; // linear data, no conversion Graphics.Blit(gm.display, gm.final, _packMat, 0); GL.sRGBWrite = prev; } // --- GPU painting --- private void BeginGpuStroke() { _prevSRGB = GL.sRGBWrite; 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) { for (int mi = 0; mi < MAP_COUNT; mi++) { if (!IsChannelActive(mi)) continue; var gm = _gpu[mi]; 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); _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); for (int mi = 0; mi < MAP_COUNT; mi++) { if (!IsChannelActive(mi)) continue; var gm = _gpu[mi]; // 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; } /// Sets _BrushColor for the given map. Albedo/emission are colors; the mask map packs (metallic, smoothness, occlusion) as raw data. 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; } } /// /// 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. /// /// /// 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 /// the value round-trip exactly (so metallic/smoothness/occlusion are stored faithfully /// and match the CPU byte writes). In gamma color space no conversion happens, so pass through. /// private static float EncodeData(float v) { 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 } }