Files
chameleon/Assets/Scripts/MeshPainter.Setup.cs
T
adminandClaude Opus 4.8 c7ec12e6d9 Split mesh painter into partials and add occlusion via combined mask map
- Split MeshPainter into partial classes (Setup/Backend/Gpu/Cpu) for maintainability
- Add ambient occlusion painting, packed with metallic/smoothness into one
  MSO mask map (R=metallic, G=occlusion, A=smoothness) bound to both
  _MetallicGlossMap and _OcclusionMap, kept independently paintable via
  per-channel ColorMask
- Gate painting from a single IsChannelActive source so AO-only strokes work
- Blend albedo/emission in linear space on CPU to match the GPU framebuffer
- Pre-compensate data-channel writes for sRGB->linear so GPU matches CPU
- Harden GPU shader fallback to reflect real backend state

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:31:52 +02:00

174 lines
6.8 KiB
C#

using UnityEngine;
public partial class MeshPainter
{
// --- Setup (shared) ---
private void SetupCollider()
{
_collider = GetComponent<MeshCollider>();
if (!_collider) _collider = gameObject.AddComponent<MeshCollider>();
_collider.convex = false; // non-convex provides triangleIndex / textureCoord
_bakedMesh = new Mesh { name = "MeshPainter_Baked" };
BakeColliderMesh();
}
private void BakeColliderMesh()
{
if (_skinned)
{
// Static skinned mesh: bake into local coordinates (scale = 1).
_skinned.BakeMesh(_bakedMesh);
_collider.sharedMesh = null;
_collider.sharedMesh = _bakedMesh;
}
else
{
var mf = GetComponent<MeshFilter>();
if (mf) _collider.sharedMesh = mf.sharedMesh;
}
}
/// <summary>Reads vertices/triangles/UVs of the collider mesh and computes culling data.</summary>
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);
}
/// <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();
// 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;
}
/// <summary>Builds the albedo base-color texels from the existing material texture or baseColor.</summary>
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;
}
/// <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();
}
// 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);
}
}
/// <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));
}
}