using UnityEngine; public partial class MeshPainter { // --- Setup (shared) --- private void SetupCollider() { _collider = GetComponent(); if (!_collider) _collider = gameObject.AddComponent(); _collider.convex = false; // non-convex provides triangleIndex / textureCoord // An always-on SkinnedColliderBaker (skinned rigs only) owns the bake even outside paint // mode; we delegate to it and only borrow its mesh. Without one we keep our own legacy bake. _baker = _skinned ? GetComponent() : null; if (!_baker) _bakedMesh = new Mesh { name = "MeshPainter_Baked" }; BakeColliderMesh(); } private void BakeColliderMesh() { if (_baker) { // Baker already baked (negative execution order) and keeps _collider.sharedMesh fresh; // we only track its mesh for the GPU raster path. No bake here - that's the whole point. _bakedMesh = _baker.BakedMesh; } else if (_skinned) { // Static skinned mesh: bake into local coordinates (scale = 1). _skinned.BakeMesh(_bakedMesh); _collider.sharedMesh = null; _collider.sharedMesh = _bakedMesh; } else { var mf = GetComponent(); if (mf) _collider.sharedMesh = mf.sharedMesh; } } /// Reads vertices/triangles/UVs of the collider mesh and computes culling data. private void BuildMeshCaches() { Mesh m = _collider.sharedMesh; if (!m) return; _verts = m.vertices; _tris = m.triangles; _uvs = m.uv; // Fallback to the original mesh UVs if the baked mesh dropped them. if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned && _skinned.sharedMesh) _uvs = _skinned.sharedMesh.uv; if (_uvs == null || _uvs.Length != _verts.Length) return; int triCount = _tris.Length / 3; _triCenter = new Vector3[triCount]; _triRadius = new float[triCount]; double sumArea3D = 0.0, sumAreaUv = 0.0; for (int t = 0; t < triCount; t++) { int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; Vector3 a = _verts[i0], b = _verts[i1], c = _verts[i2]; Vector3 center = (a + b + c) / 3f; _triCenter[t] = center; float ra = (a - center).sqrMagnitude; float rb = (b - center).sqrMagnitude; float rc = (c - center).sqrMagnitude; _triRadius[t] = Mathf.Sqrt(Mathf.Max(ra, Mathf.Max(rb, rc))); sumArea3D += 0.5 * Vector3.Cross(b - a, c - a).magnitude; Vector2 ua = _uvs[i0], ub = _uvs[i1], uc = _uvs[i2]; sumAreaUv += 0.5 * Mathf.Abs(Cross2D(ub - ua, uc - ua)); } if (sumAreaUv > 1e-9) _avgUvToLocal = (float)System.Math.Sqrt(sumArea3D / sumAreaUv); } /// /// Rasterizes the mesh UV islands into a per-texel coverage marker (true = the texel lies /// inside a UV triangle). This is the dilation SOURCE / overwrite guard: island texels are /// real content the edge-padding pass must never overwrite, while the gutter (false) stays /// paddable. Deriving the marker from the STATIC UV layout - instead of from transient /// per-stroke coverage - makes every seed (fresh start, loaded maps, backend switch) mark /// the same real content as painted, so the first stroke after a load/switch no longer /// bleeds over the existing paint. Built once and cached (UV layout + textureSize are fixed). /// private bool[] GetIslandMask() { int n = textureSize * textureSize; if (_islandMask != null && _islandMask.Length == n) return _islandMask; var mask = new bool[n]; if (_tris == null || _uvs == null) { _islandMask = mask; return mask; } float ts = textureSize; const float eps = -1e-4f; // matches PaintStamp3D: tolerate 1px cracks between triangles int triCount = _tris.Length / 3; for (int t = 0; t < triCount; t++) { int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; Vector2 uv0 = _uvs[i0], uv1 = _uvs[i1], uv2 = _uvs[i2]; float p0x = uv0.x * ts, p0y = uv0.y * ts; float p1x = uv1.x * ts, p1y = uv1.y * ts; float p2x = uv2.x * ts, p2y = uv2.y * ts; int minX = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0x, Mathf.Min(p1x, p2x))), 0, textureSize - 1); int maxX = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(p0x, Mathf.Max(p1x, p2x))), 0, textureSize - 1); int minY = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0y, Mathf.Min(p1y, p2y))), 0, textureSize - 1); int maxY = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(p0y, Mathf.Max(p1y, p2y))), 0, textureSize - 1); if (maxX < minX || maxY < minY) continue; float d = (p1y - p2y) * (p0x - p2x) + (p2x - p1x) * (p0y - p2y); if (Mathf.Abs(d) < 1e-7f) continue; float invD = 1f / d; for (int y = minY; y <= maxY; y++) { float py = y + 0.5f; int row = y * textureSize; for (int x = minX; x <= maxX; x++) { float px = x + 0.5f; float a = ((p1y - p2y) * (px - p2x) + (p2x - p1x) * (py - p2y)) * invD; if (a < eps) continue; float b = ((p2y - p0y) * (px - p2x) + (p0x - p2x) * (py - p2y)) * invD; if (b < eps) continue; if (1f - a - b < eps) continue; mask[row + x] = true; } } } _islandMask = mask; return mask; } /// Builds the per-map seed colors (in canonical/material layout) from the existing material. private void BuildSeedColors() { _seedCanonical = new Color32[MAP_COUNT][]; _seedCanonical[MAP_ALBEDO] = BuildBaseColors(); // Mask map seed from the material scalars: R = metallic, G = occlusion (255 = // unoccluded, so the look is unchanged until painted), B = 0, A = smoothness. float metallic0 = _materialInstance.HasProperty(MetallicId) ? _materialInstance.GetFloat(MetallicId) : 0f; float smoothness0 = _materialInstance.HasProperty(SmoothnessId) ? _materialInstance.GetFloat(SmoothnessId) : 0.5f; _seedCanonical[MAP_MSO] = FillCanonical(new Color32( (byte)(Mathf.Clamp01(metallic0) * 255f), 255, 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; } /// Builds the albedo base-color texels from the existing material texture or baseColor. private Color32[] BuildBaseColors() { var px = new Color32[textureSize * textureSize]; Texture sourceTex = null; var shared = _renderer.sharedMaterial; if (shared) { if (shared.HasProperty(BaseMapId)) sourceTex = shared.GetTexture(BaseMapId); if (!sourceTex && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId); } var src = sourceTex as Texture2D; if (src) { try { if (src.width == textureSize && src.height == textureSize && src.isReadable) return src.GetPixels32(); } catch { /* not readable -> fall back to baseColor */ } } Color32 c = baseColor; for (int i = 0; i < px.Length; i++) px[i] = c; return px; } /// Enables the URP keywords for the painted PBR maps and seeds the material scalars. 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(); } // Occlusion map: URP samples the G channel (LerpWhiteTo(map.g, _OcclusionStrength)). // Strength 1 so the painted value drives the occlusion as-is. if (_materialInstance.HasProperty(OcclusionMapId)) { _materialInstance.EnableKeyword("_OCCLUSIONMAP"); if (_materialInstance.HasProperty(OcclusionStrengthId)) _materialInstance.SetFloat(OcclusionStrengthId, 1f); } } /// Pushes the global emission intensity to the material's _EmissionColor (live-tunable). private void RefreshEmissionColor() { if (_materialInstance && _materialInstance.HasProperty(EmissionColorId)) _materialInstance.SetColor(EmissionColorId, Color.white * Mathf.Max(0f, emissionIntensity)); } }