Trim comments and use implicit null checks for Unity objects
Reduce comments to non-obvious "why" notes and convert UnityEngine.Object null checks to implicit bool (if (obj) / if (!obj)). Input System devices (Mouse.current, Keyboard.current) and plain arrays keep explicit != null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
3da613e1a1
commit
f758c96dc5
8 files changed
+220
-427
No files matched your search
+83
-137
@@ -5,46 +5,28 @@ using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Live mesh painting in Play mode.
|
||||
/// Paints a fixed color (brush) onto the mesh's texture with the mouse.
|
||||
///
|
||||
/// TWO BACKENDS (switchable via <see cref="useGpu"/>, also live in Play mode):
|
||||
///
|
||||
/// * GPU (default): True shader painting into a RenderTexture. The brush is
|
||||
/// drawn via a vertex/fragment shader - the mesh is unwrapped into the texture
|
||||
/// layout using its UVs, and per texel the actual 3D position is tested against
|
||||
/// the brush sphere. Edge padding against seams runs as a GPU blit.
|
||||
/// Fast, because the GPU rasterizes all texels in parallel.
|
||||
///
|
||||
/// * CPU: Rasterization of the hit triangles on the CPU; per texel the 3D
|
||||
/// position is computed barycentrically and tested against the brush sphere.
|
||||
/// Edge padding via CPU dilation. Identical visual result, but more expensive.
|
||||
///
|
||||
/// BOTH backends paint seamlessly: a 3D sphere around the hit point captures
|
||||
/// triangles on both sides of a UV seam at once (front/back share the same 3D
|
||||
/// edge), no matter where they lie in the UV layout.
|
||||
///
|
||||
/// Simply attach to the GameObject you want to paint (e.g. paintman_UV/paintman_UV).
|
||||
/// 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 a 3D
|
||||
/// sphere around the hit point captures triangles on both sides of a seam at once.
|
||||
/// Attach to the GameObject you want to paint.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MeshPainter : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// When false, mouse painting is ignored (the canvas/material stays as-is).
|
||||
/// Toggled by the GameModeManager (painting is only enabled in paint mode).
|
||||
/// </summary>
|
||||
/// <summary>When false, mouse painting is ignored. Toggled by the GameModeManager.</summary>
|
||||
public bool PaintingEnabled { get; set; } = true;
|
||||
|
||||
[Header("Backend")]
|
||||
[Tooltip("ON = GPU painting (shader/RenderTexture, fast). OFF = CPU painting. " +
|
||||
"Can be toggled live in Play mode - the painted content is carried over.")]
|
||||
[Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
|
||||
public bool useGpu = true;
|
||||
|
||||
[Header("Brush")]
|
||||
[Tooltip("Color used for painting.")]
|
||||
public Color brushColor = Color.red;
|
||||
|
||||
[Tooltip("Brush radius in texels (visual size). Converted into a 3D radius per hit.")]
|
||||
[Tooltip("Brush radius in texels. Converted into a 3D radius per hit.")]
|
||||
[Range(1, 256)]
|
||||
public int brushSize = 16;
|
||||
|
||||
@@ -55,9 +37,7 @@ public class MeshPainter : MonoBehaviour
|
||||
[Tooltip("Base color of the texture at startup (if none exists).")]
|
||||
public Color baseColor = Color.white;
|
||||
|
||||
[Tooltip("Edge padding (texels) against visible UV seams. The color is 'bled' beyond the " +
|
||||
"UV island edges so that bilinear sampling there does not pull in the unpainted " +
|
||||
"base color. 0 = off.")]
|
||||
[Tooltip("Edge padding (texels) bled beyond UV island edges so bilinear sampling does not pull in the base color. 0 = off.")]
|
||||
[Range(0, 16)]
|
||||
public int paddingTexels = 4;
|
||||
|
||||
@@ -75,43 +55,41 @@ public class MeshPainter : MonoBehaviour
|
||||
private Mesh _bakedMesh;
|
||||
private Material _materialInstance;
|
||||
|
||||
// Mesh caches (in the transform's local space = collider space).
|
||||
// Mesh caches (in local/collider space).
|
||||
private Vector3[] _verts;
|
||||
private int[] _tris;
|
||||
private Vector2[] _uvs;
|
||||
private Vector3[] _triCenter; // centroid per triangle (for fast CPU culling)
|
||||
private float[] _triRadius; // circumscribed radius per triangle (for fast CPU culling)
|
||||
private float _avgUvToLocal = 1f; // average conversion: UV unit -> local unit
|
||||
private Vector3[] _triCenter; // centroid per triangle (CPU culling)
|
||||
private float[] _triRadius; // circumscribed radius per triangle (CPU culling)
|
||||
private float _avgUvToLocal = 1f; // average UV unit -> local unit
|
||||
|
||||
private bool _hasLastScreen;
|
||||
private Vector2 _lastScreen;
|
||||
private bool _gpuActive; // currently configured backend
|
||||
private bool _gpuActive;
|
||||
private bool _setupValid;
|
||||
|
||||
// --- CPU backend ---
|
||||
private Texture2D _paintTexture;
|
||||
private Color32[] _pixels;
|
||||
private bool[] _painted; // true = painted by the brush (persistent dilation source)
|
||||
private int[] _fillStamp; // transient marker for padding texels per dilation call
|
||||
private int _dilateStamp; // incremented per call -> padding from previous frames does not count as a source
|
||||
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
|
||||
private readonly List<int> _dilateScratch = new List<int>(512);
|
||||
private readonly RaycastHit[] _rayHits = new RaycastHit[32]; // buffer so other colliders (player capsule, etc.) cannot block painting
|
||||
private readonly RaycastHit[] _rayHits = new RaycastHit[32];
|
||||
private int _dx0, _dy0, _dx1, _dy1; // dirty rectangle (CPU upload)
|
||||
|
||||
// --- GPU backend ---
|
||||
public Shader brushShader; // optional; otherwise Shader.Find("Hidden/MeshPaintBrush")
|
||||
public Shader dilateShader; // optional; otherwise Shader.Find("Hidden/MeshPaintDilate")
|
||||
public Shader brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
|
||||
public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
|
||||
private Material _brushMat;
|
||||
private Material _dilateMat;
|
||||
private RenderTexture _displayRT; // assigned to the material as _BaseMap
|
||||
private RenderTexture _displayRT;
|
||||
private RenderTexture _tempRT; // scratch for the dilation blit (ping-pong)
|
||||
private bool _drewThisStroke;
|
||||
private bool _prevSRGB;
|
||||
|
||||
// Step size (in screen pixels) for interpolation along the mouse movement.
|
||||
private const float ScreenStepPixels = 5f;
|
||||
// Upper bound of raycasts per frame (safeguard for very fast movements).
|
||||
private const int MaxStepsPerFrame = 256;
|
||||
private const float ScreenStepPixels = 5f; // interpolation step along mouse movement
|
||||
private const int MaxStepsPerFrame = 256; // raycast safeguard for fast movement
|
||||
|
||||
private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap");
|
||||
private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
|
||||
@@ -124,11 +102,11 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (paintCamera == null) paintCamera = Camera.main;
|
||||
if (!paintCamera) paintCamera = Camera.main;
|
||||
|
||||
_renderer = GetComponent<Renderer>();
|
||||
_skinned = GetComponent<SkinnedMeshRenderer>();
|
||||
if (_renderer == null)
|
||||
if (!_renderer)
|
||||
{
|
||||
Debug.LogError("[MeshPainter] No Renderer found on this GameObject.", this);
|
||||
enabled = false;
|
||||
@@ -147,9 +125,9 @@ public class MeshPainter : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Own material instance so the shared asset is not modified.
|
||||
_materialInstance = _renderer.material; // automatically creates an instance
|
||||
// Tint to white so the painted color appears unaltered.
|
||||
// Own material instance so the shared asset is not modified; tint 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);
|
||||
|
||||
@@ -160,14 +138,12 @@ public class MeshPainter : MonoBehaviour
|
||||
"Hold the left mouse button and drag over the mesh.", this);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Setup (shared)
|
||||
// ------------------------------------------------------------------
|
||||
// --- Setup (shared) ---
|
||||
|
||||
private void SetupCollider()
|
||||
{
|
||||
_collider = GetComponent<MeshCollider>();
|
||||
if (_collider == null) _collider = gameObject.AddComponent<MeshCollider>();
|
||||
if (!_collider) _collider = gameObject.AddComponent<MeshCollider>();
|
||||
_collider.convex = false; // non-convex provides triangleIndex / textureCoord
|
||||
|
||||
_bakedMesh = new Mesh { name = "MeshPainter_Baked" };
|
||||
@@ -176,7 +152,7 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void BakeColliderMesh()
|
||||
{
|
||||
if (_skinned != null)
|
||||
if (_skinned)
|
||||
{
|
||||
// Static skinned mesh: bake into local coordinates (scale = 1).
|
||||
_skinned.BakeMesh(_bakedMesh);
|
||||
@@ -186,7 +162,7 @@ public class MeshPainter : MonoBehaviour
|
||||
else
|
||||
{
|
||||
var mf = GetComponent<MeshFilter>();
|
||||
if (mf != null) _collider.sharedMesh = mf.sharedMesh;
|
||||
if (mf) _collider.sharedMesh = mf.sharedMesh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,14 +170,14 @@ public class MeshPainter : MonoBehaviour
|
||||
private void BuildMeshCaches()
|
||||
{
|
||||
Mesh m = _collider.sharedMesh;
|
||||
if (m == null) return;
|
||||
if (!m) return;
|
||||
|
||||
_verts = m.vertices;
|
||||
_tris = m.triangles;
|
||||
_uvs = m.uv;
|
||||
|
||||
// Fallback: UVs from the original mesh (BakeMesh usually keeps them, but better safe than sorry).
|
||||
if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned != null && _skinned.sharedMesh != null)
|
||||
// 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;
|
||||
@@ -232,21 +208,21 @@ public class MeshPainter : MonoBehaviour
|
||||
if (sumAreaUv > 1e-9) _avgUvToLocal = (float)System.Math.Sqrt(sumArea3D / sumAreaUv);
|
||||
}
|
||||
|
||||
/// <summary>Builds the base-color texels (copy of the existing material texture or baseColor).</summary>
|
||||
/// <summary>Builds the 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 != null)
|
||||
if (shared)
|
||||
{
|
||||
if (shared.HasProperty(BaseMapId)) sourceTex = shared.GetTexture(BaseMapId);
|
||||
if (sourceTex == null && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId);
|
||||
if (!sourceTex && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId);
|
||||
}
|
||||
|
||||
var src = sourceTex as Texture2D;
|
||||
if (src != null)
|
||||
if (src)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -261,11 +237,8 @@ public class MeshPainter : MonoBehaviour
|
||||
return px;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Backend selection / switching
|
||||
// ------------------------------------------------------------------
|
||||
// --- Backend selection / switching ---
|
||||
|
||||
/// <summary>Sets up the desired backend and carries over the initial image content.</summary>
|
||||
private void SetupBackend(bool gpu, Color32[] fromColor32)
|
||||
{
|
||||
if (gpu) SetupGpu(fromColor32);
|
||||
@@ -273,10 +246,9 @@ public class MeshPainter : MonoBehaviour
|
||||
_gpuActive = gpu;
|
||||
}
|
||||
|
||||
/// <summary>Switches the backend at runtime and carries over the currently painted content.</summary>
|
||||
/// <summary>Switches the backend at runtime, carrying over the painted content.</summary>
|
||||
private void SwitchBackend(bool toGpu)
|
||||
{
|
||||
// save the current content as Color32
|
||||
Color32[] current = (_gpuActive ? ReadGpuPixels() : (Color32[])_pixels.Clone());
|
||||
|
||||
if (_gpuActive) TeardownGpu();
|
||||
@@ -288,9 +260,7 @@ public class MeshPainter : MonoBehaviour
|
||||
Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// CPU backend
|
||||
// ------------------------------------------------------------------
|
||||
// --- CPU backend ---
|
||||
|
||||
private void SetupCpu(Color32[] initial)
|
||||
{
|
||||
@@ -317,22 +287,20 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void TeardownCpu()
|
||||
{
|
||||
if (_paintTexture != null) { Destroy(_paintTexture); _paintTexture = null; }
|
||||
if (_paintTexture) { Destroy(_paintTexture); _paintTexture = null; }
|
||||
_pixels = null;
|
||||
_painted = null;
|
||||
_fillStamp = null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// GPU backend
|
||||
// ------------------------------------------------------------------
|
||||
// --- GPU backend ---
|
||||
|
||||
private void SetupGpu(Color32[] initial)
|
||||
{
|
||||
if (brushShader == null) brushShader = Shader.Find("Hidden/MeshPaintBrush");
|
||||
if (dilateShader == null) dilateShader = Shader.Find("Hidden/MeshPaintDilate");
|
||||
if (!brushShader) brushShader = Shader.Find("Hidden/MeshPaintBrush");
|
||||
if (!dilateShader) dilateShader = Shader.Find("Hidden/MeshPaintDilate");
|
||||
|
||||
if (brushShader == null || dilateShader == null)
|
||||
if (!brushShader || !dilateShader)
|
||||
{
|
||||
Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush or " +
|
||||
"Hidden/MeshPaintDilate). Falling back to CPU painting.", this);
|
||||
@@ -348,8 +316,8 @@ public class MeshPainter : MonoBehaviour
|
||||
_displayRT = CreateRT("MeshPainter_DisplayRT");
|
||||
_tempRT = CreateRT("MeshPainter_TempRT");
|
||||
|
||||
// Bring the initial image content into the display RT. Alpha = 0 marks "unpainted"
|
||||
// (so the base color is not mistaken for a dilation source).
|
||||
// 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);
|
||||
|
||||
@@ -358,10 +326,10 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void TeardownGpu()
|
||||
{
|
||||
if (_displayRT != null) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; }
|
||||
if (_tempRT != null) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; }
|
||||
if (_brushMat != null) { Destroy(_brushMat); _brushMat = null; }
|
||||
if (_dilateMat != null) { Destroy(_dilateMat); _dilateMat = null; }
|
||||
if (_displayRT) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; }
|
||||
if (_tempRT) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; }
|
||||
if (_brushMat) { Destroy(_brushMat); _brushMat = null; }
|
||||
if (_dilateMat) { Destroy(_dilateMat); _dilateMat = null; }
|
||||
}
|
||||
|
||||
private RenderTexture CreateRT(string rtName)
|
||||
@@ -383,14 +351,13 @@ public class MeshPainter : MonoBehaviour
|
||||
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha)
|
||||
{
|
||||
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false);
|
||||
// Alpha marks painted (255) vs unpainted (0) for the dilation pass.
|
||||
if (paintedAlpha)
|
||||
{
|
||||
// mark the entire content as "painted" (alpha = 255) -> no dilation needed.
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = 255;
|
||||
}
|
||||
else
|
||||
{
|
||||
// mark the base color as "unpainted" (alpha = 0).
|
||||
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
|
||||
}
|
||||
tmp.SetPixels32(colors);
|
||||
@@ -413,26 +380,21 @@ public class MeshPainter : MonoBehaviour
|
||||
return px;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Material
|
||||
// ------------------------------------------------------------------
|
||||
// --- Material ---
|
||||
|
||||
private void AssignTextureToMaterial(Texture tex)
|
||||
{
|
||||
if (_materialInstance == null) return;
|
||||
if (!_materialInstance) return;
|
||||
if (_materialInstance.HasProperty(BaseMapId)) _materialInstance.SetTexture(BaseMapId, tex);
|
||||
if (_materialInstance.HasProperty(MainTexId)) _materialInstance.SetTexture(MainTexId, tex);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Update / input
|
||||
// ------------------------------------------------------------------
|
||||
// --- Update / input ---
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_setupValid) return;
|
||||
|
||||
// Live backend switching.
|
||||
if (useGpu != _gpuActive) SwitchBackend(useGpu);
|
||||
|
||||
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
|
||||
@@ -442,7 +404,7 @@ public class MeshPainter : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateColliderEveryFrame && _skinned != null)
|
||||
if (updateColliderEveryFrame && _skinned)
|
||||
{
|
||||
BakeColliderMesh();
|
||||
BuildMeshCaches();
|
||||
@@ -457,15 +419,15 @@ public class MeshPainter : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (paintCamera == null) return;
|
||||
if (!paintCamera) return;
|
||||
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; }
|
||||
|
||||
if (_hasLastScreen)
|
||||
{
|
||||
// Interpolate along the mouse movement in screen space and re-raycast at
|
||||
// each step -> the stroke follows the mesh surface.
|
||||
// Interpolate along the mouse movement and re-raycast at each step so the
|
||||
// stroke follows the mesh surface.
|
||||
float dist = Vector2.Distance(_lastScreen, screenPos);
|
||||
int steps = Mathf.Clamp(Mathf.CeilToInt(dist / ScreenStepPixels), 1, MaxStepsPerFrame);
|
||||
for (int s = 1; s <= steps; s++)
|
||||
@@ -487,19 +449,14 @@ public class MeshPainter : MonoBehaviour
|
||||
_hasLastScreen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raycast at a screen position. If the ray hits the mesh, a 3D sphere is painted
|
||||
/// around the hit point (seamless) - via GPU or CPU depending on the backend.
|
||||
/// </summary>
|
||||
/// <summary>Raycasts at a screen position and paints a 3D sphere around the mesh hit point.</summary>
|
||||
private void PaintAtScreen(Vector2 screenPos)
|
||||
{
|
||||
Ray ray = paintCamera.ScreenPointToRay(screenPos);
|
||||
|
||||
// Collect all hits and pick the nearest one that belongs to the paint mesh.
|
||||
// A plain Physics.Raycast returns only the closest collider, so anything in
|
||||
// front of the mesh (the player's CharacterController capsule, future props,
|
||||
// the camera-collision proxy) would block painting. Filtering by collider
|
||||
// keeps painting working from every camera angle.
|
||||
// Pick the nearest hit that belongs to the paint mesh. A plain raycast would
|
||||
// return only the closest collider, so anything in front of the mesh (the
|
||||
// player capsule, props, the camera-collision proxy) could block painting.
|
||||
int hitCount = Physics.RaycastNonAlloc(ray, _rayHits, 1000f, ~0, QueryTriggerInteraction.Ignore);
|
||||
RaycastHit hit = default;
|
||||
float nearest = float.MaxValue;
|
||||
@@ -517,11 +474,10 @@ public class MeshPainter : MonoBehaviour
|
||||
if (!found) return;
|
||||
if (_verts == null || hit.triangleIndex < 0) return;
|
||||
|
||||
// Bring the hit point into local mesh space (= collider/vertex space).
|
||||
Vector3 pLocal = transform.InverseTransformPoint(hit.point);
|
||||
|
||||
// Convert the brush radius (texels) into a local 3D radius using the UV
|
||||
// density of the hit triangle -> the brush stays visually the same size.
|
||||
// Convert the brush radius (texels) into a local 3D radius using the hit
|
||||
// triangle's UV density so the brush keeps a constant visual size.
|
||||
float scale = _avgUvToLocal;
|
||||
int t = hit.triangleIndex;
|
||||
if (t * 3 + 2 < _tris.Length)
|
||||
@@ -532,8 +488,7 @@ public class MeshPainter : MonoBehaviour
|
||||
if (areaUv > 1e-9f)
|
||||
{
|
||||
float s = Mathf.Sqrt(area3D / areaUv);
|
||||
// guard against degenerate UVs: keep close to the average.
|
||||
scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f);
|
||||
scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f); // guard degenerate UVs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,15 +499,13 @@ public class MeshPainter : MonoBehaviour
|
||||
else PaintSphere3D(pLocal, radiusLocal);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// GPU painting
|
||||
// ------------------------------------------------------------------
|
||||
// --- GPU painting ---
|
||||
|
||||
private void BeginGpuStroke()
|
||||
{
|
||||
_drewThisStroke = false;
|
||||
_prevSRGB = GL.sRGBWrite;
|
||||
GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT (linear project)
|
||||
GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT
|
||||
Graphics.SetRenderTarget(_displayRT);
|
||||
_brushMat.SetColor(BrushColorId, brushColor);
|
||||
}
|
||||
@@ -574,8 +527,7 @@ public class MeshPainter : MonoBehaviour
|
||||
{
|
||||
Graphics.SetRenderTarget(null);
|
||||
|
||||
// Edge padding against UV seams: blit the display RT through the dilation
|
||||
// shader into _tempRT and then swap (the result ends up in _displayRT again).
|
||||
// Edge padding against UV seams: dilate the display RT into _tempRT, then swap.
|
||||
if (_drewThisStroke && paddingTexels > 0)
|
||||
{
|
||||
_dilateMat.SetInt(PadId, paddingTexels);
|
||||
@@ -588,13 +540,11 @@ public class MeshPainter : MonoBehaviour
|
||||
GL.sRGBWrite = _prevSRGB;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// CPU painting (rasterization + dilation)
|
||||
// ------------------------------------------------------------------
|
||||
// --- CPU painting (rasterization + dilation) ---
|
||||
|
||||
/// <summary>
|
||||
/// Paints a sphere in local 3D space: rasterizes all triangles in range into
|
||||
/// the texture and colors every texel whose 3D position lies inside the sphere.
|
||||
/// Paints a sphere in local 3D space: rasterizes triangles in range and colors
|
||||
/// every texel whose 3D position lies inside the sphere.
|
||||
/// </summary>
|
||||
private void PaintSphere3D(Vector3 pLocal, float radiusLocal)
|
||||
{
|
||||
@@ -605,7 +555,7 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
for (int t = 0; t < triCount; t++)
|
||||
{
|
||||
// Fast culling: sphere against the triangle's circumscribed circle.
|
||||
// Fast culling: sphere vs. the triangle's circumscribed circle.
|
||||
float reach = radiusLocal + _triRadius[t];
|
||||
if ((_triCenter[t] - pLocal).sqrMagnitude > reach * reach) continue;
|
||||
|
||||
@@ -618,7 +568,6 @@ public class MeshPainter : MonoBehaviour
|
||||
float p1x = uv1.x * ts, p1y = uv1.y * ts;
|
||||
float p2x = uv2.x * ts, p2y = uv2.y * ts;
|
||||
|
||||
// Bounding box in pixel space.
|
||||
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);
|
||||
@@ -630,7 +579,7 @@ public class MeshPainter : MonoBehaviour
|
||||
if (Mathf.Abs(d) < 1e-7f) continue;
|
||||
float invD = 1f / d;
|
||||
|
||||
const float eps = -1e-4f; // small tolerance against 1px cracks between triangles
|
||||
const float eps = -1e-4f; // tolerance against 1px cracks between triangles
|
||||
|
||||
for (int y = minY; y <= maxY; y++)
|
||||
{
|
||||
@@ -668,9 +617,8 @@ public class MeshPainter : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edge padding / texture dilation against visible UV seams (CPU).
|
||||
/// Colors a thin border (pad texels) around the freshly painted areas with their
|
||||
/// color. The dilation source is exclusively brush-painted texels (_painted).
|
||||
/// 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)
|
||||
{
|
||||
@@ -681,7 +629,7 @@ public class MeshPainter : MonoBehaviour
|
||||
int x1 = Mathf.Min(textureSize - 1, _dx1 + pad);
|
||||
int y1 = Mathf.Min(textureSize - 1, _dy1 + pad);
|
||||
|
||||
_dilateStamp++; // unique per call -> padding from previous frames does not count as a source
|
||||
_dilateStamp++;
|
||||
|
||||
for (int iter = 0; iter < pad; iter++)
|
||||
{
|
||||
@@ -755,9 +703,7 @@ public class MeshPainter : MonoBehaviour
|
||||
_paintTexture.Apply(false);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
// --- Helpers ---
|
||||
|
||||
private static float Cross2D(Vector2 a, Vector2 b) => a.x * b.y - a.y * b.x;
|
||||
|
||||
@@ -785,8 +731,8 @@ public class MeshPainter : MonoBehaviour
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_bakedMesh != null) Destroy(_bakedMesh);
|
||||
if (_materialInstance != null) Destroy(_materialInstance);
|
||||
if (_bakedMesh) Destroy(_bakedMesh);
|
||||
if (_materialInstance) Destroy(_materialInstance);
|
||||
TeardownCpu();
|
||||
TeardownGpu();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user