Paint-Partikel beim Malen: Emission pro Stamp, Scale = Brush-Radius, Farbe = Brush-Albedo mit Helligkeits-Streuung

- MeshPainter: Paint Particles Prefab, lazy Instanz, Play/Stop am Stroke, MinMaxGradient (Albedo +-25%/15% dunkel/hell)
- PaintSplatter.prefab + Paint Particle Material/Textur, Wiring am Player.prefab
- FurShellRenderer: Kill-Switch-Destroy nur im Play-Mode (Edit-Mode-Error beim Prefab-Load)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-11 22:13:16 +02:00
1 parent a5438cf5a0
commit 094cf3a493
9 files changed
+5169 -3

No files matched your search

@@ -153,7 +153,9 @@ namespace Chameleon.Components
return;
}
Destroy(this);
// Play mode only: in the editor Awake also runs on edit-mode loads (LoadPrefabContents,
// [ExecuteAlways]) where Destroy is illegal - there OnEnable's disable path handles it.
if (Application.isPlaying) Destroy(this);
}
void OnEnable()
@@ -328,6 +328,14 @@ public partial class MeshPainter : MonoBehaviour
[Range(0f, 4f)]
public float previewOpacity = 0.5f;
[Header("Paint particles")]
[Tooltip("Optional particle effect played at the brush while a stroke deposits paint. Instantiated " +
"once on first use. Author the system for brush world-radius 1 with scaling mode Hierarchy: " +
"the instance is scaled to the brush's world radius and tinted with the brush albedo color " +
"(Start Color is overwritten). +Z of the instance points along the surface normal.")]
public ParticleSystem paintParticlesPrefab;
private ParticleSystem _paintParticles;
[Header("Canvas")]
[Tooltip("Resolution of the paint textures (square).")]
public int textureSize = 1024;
@@ -829,6 +837,7 @@ public partial class MeshPainter : MonoBehaviour
if (!PaintingEnabled)
{
EndStrokeIfActive(); // finalize an in-progress stroke if the mode was left mid-paint
StopPaintParticles();
_hasLastScreen = false;
return;
}
@@ -859,6 +868,7 @@ public partial class MeshPainter : MonoBehaviour
if (!TryGetMouse(out bool pressed, out Vector2 screenPos))
{
StrokeInProgress = false;
StopPaintParticles();
return;
}
@@ -869,6 +879,7 @@ public partial class MeshPainter : MonoBehaviour
// While the button is held on the paint path the hardware cursor is hidden entirely
// (CursorController); the preview decal itself stays visible during the stroke.
StrokeInProgress = pressed && !pipette;
if (!StrokeInProgress) StopPaintParticles();
// Brush preview at the hover point.
if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera)
@@ -975,11 +986,46 @@ public partial class MeshPainter : MonoBehaviour
else UseResolvedStampMask();
DrawBackendStamp(pLocal, radiusLocal, u, v, nLocal);
UpdatePaintParticles(pLocal, radiusLocal, nLocal);
EmitStamp(pLocal, radiusLocal, u, v, nLocal);
MarkStrokePainted(); // a stamp landed -> this stroke is undoable
}
/// <summary>Moves/scales/tints the stroke particle effect at a landed stamp and keeps it playing.
/// Scale = brush world radius (author the prefab for radius 1, scaling mode Hierarchy), tint =
/// brush albedo color with a small per-particle random offset (alpha forced opaque - the brush
/// alpha is paint opacity, not particle fade).</summary>
private void UpdatePaintParticles(Vector3 pLocal, float radiusLocal, Vector3 nLocal)
{
if (!paintParticlesPrefab) return;
if (!_paintParticles)
_paintParticles = Instantiate(paintParticlesPrefab);
Transform t = _paintParticles.transform;
t.SetPositionAndRotation(transform.TransformPoint(pLocal),
Quaternion.LookRotation(transform.TransformDirection(nLocal)));
t.localScale = Vector3.one * transform.TransformVector(nLocal * radiusLocal).magnitude;
var main = _paintParticles.main;
Color c = brushColor;
c.a = 1f;
// Per-particle random tint offset around the albedo (darker <-> lighter).
Color dark = Color.Lerp(c, Color.black, 0.13f);
Color light = Color.Lerp(c, Color.white, 0.09f);
dark.a = light.a = 1f;
main.startColor = new ParticleSystem.MinMaxGradient(dark, light);
if (!_paintParticles.isPlaying) _paintParticles.Play();
}
/// <summary>Stops emitting stroke particles (already-spawned ones live out their lifetime).</summary>
private void StopPaintParticles()
{
if (_paintParticles && _paintParticles.isPlaying)
_paintParticles.Stop();
}
/// <summary>
/// True if a brush dab at this placement would wrap excessively around a corner: more than
/// <see cref="cornerGuardMaxFraction"/> of its footprint sits on surface whose face normal
@@ -1235,6 +1281,7 @@ public partial class MeshPainter : MonoBehaviour
if (_proceduralMask) Destroy(_proceduralMask);
if (_previewMat) Destroy(_previewMat);
if (_previewQuad) Destroy(_previewQuad);
if (_paintParticles) Destroy(_paintParticles.gameObject);
TeardownSpray();
TeardownCpu();
TeardownGpu();