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) // 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 (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; } } /// /// 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. /// private void StampMso(float opacity, float colorMask) { _brushMat.SetVector(BrushColorId, new Vector4( EncodeData(metallic), EncodeData(smoothness), EncodeData(occlusion), opacity)); _brushMat.SetFloat(ColorWriteMaskId, colorMask); DrawStamp(); } /// Draws the baked mesh once per sub-mesh with the current brush pass (UV-space rasterization). 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); 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; } /// /// 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; } }