Files
chameleon/Assets/Scripts/Components/SkinnedColliderBaker.cs
T
adminandClaude Opus 4.8 e23fae0c50 Reorganize scripts into subfolders by responsibility
Move scripts under Assets/Scripts/ into Components, GameMode, Helper,
Network, Enum, Struct and ScriptableObject folders. Pure file moves
(plus folder .meta files); no code changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:02:15 +02:00

87 lines
3.9 KiB
C#

using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Sole owner of the animated character's collider bake: keeps a non-convex MeshCollider in sync with
/// the SkinnedMeshRenderer's current pose every frame, independent of any paint/game mode. MeshPainter's
/// own per-frame bake sits behind its PaintingEnabled gate, so the collider would otherwise freeze in
/// player-control / insect mode. When this component is present MeshPainter delegates to it (it stops
/// baking and only borrows <see cref="BakedMesh"/> for its GPU raster path), so the deformed-pose
/// collider stays fresh for everyone - paint raycasts, physics, future seeker / line-of-sight queries.
///
/// Bakes in the renderer's LOCAL space (BakeMesh, scale = 1) - the same convention MeshPainter and
/// ConcealmentMeter use (world = transform.localToWorldMatrix). Runs at a negative execution order so
/// the collider is fresh before MeshPainter (and other consumers) read it in the same frame. Owns the
/// baked Mesh (creates + destroys it); MeshPainter never destroys a borrowed mesh.
/// </summary>
[DisallowMultipleComponent]
[DefaultExecutionOrder(-50)]
[RequireComponent(typeof(SkinnedMeshRenderer))]
public class SkinnedColliderBaker : MonoBehaviour
{
[Min(0f)]
[Tooltip("Seconds between collider re-bakes. 0 = every frame (matches MeshPainter's per-frame bake). " +
"Raise it to trade collider freshness for CPU on a high-poly rig.")]
[SerializeField] private float bakeInterval = 0f;
/// <summary>The mesh baked from the current pose, in local space. Valid after the first bake.</summary>
public Mesh BakedMesh => _baked;
private SkinnedMeshRenderer _skinned;
private MeshCollider _collider;
private Mesh _baked;
private float _timer;
private readonly List<Vector3> _vbuf = new List<Vector3>(); // reused vertex scratch so the scale fixup allocates nothing per bake
private void Awake()
{
_skinned = GetComponent<SkinnedMeshRenderer>();
if (!_skinned)
{
Debug.LogError("[SkinnedColliderBaker] No SkinnedMeshRenderer on this GameObject.", this);
enabled = false;
return;
}
_collider = GetComponent<MeshCollider>();
if (!_collider) _collider = gameObject.AddComponent<MeshCollider>();
_collider.convex = false; // non-convex provides triangleIndex / textureCoord for paint raycasts
_baked = new Mesh { name = "SkinnedColliderBaker_Baked" };
Bake(); // valid collider on frame 0, before any consumer reads it
}
private void Update()
{
if (bakeInterval <= 0f) { Bake(); return; }
_timer += Time.deltaTime;
if (_timer < bakeInterval) return;
_timer = 0f;
Bake();
}
private void Bake()
{
if (!_skinned) return;
_skinned.BakeMesh(_baked);
// BakeMesh freezes the bone deformation — which already carries this transform's world scale —
// into the mesh. The MeshCollider below then applies the same lossyScale a SECOND time, so a
// scaled player (see PlayerScaler) would get a collider half/double the size of the visible
// mesh. Divide the scale back out so the baked mesh is true scale-1 local geometry (the contract
// this class already documents): the MeshCollider re-applies it exactly once (matching the
// renderer), and MeshPainter / ConcealmentMeter — which raise the baked mesh to world via
// localToWorldMatrix — stay correct at any player scale. No-op (and untouched) at scale 1; the
// uniform divide is rotation-independent, which the player's uniform scale guarantees.
GeometryUtil.RescaleToUnitScale(_baked, transform.lossyScale, _vbuf);
_collider.sharedMesh = null; // force a physics re-cook of the new geometry
_collider.sharedMesh = _baked;
}
private void OnDestroy()
{
if (_baked) Destroy(_baked);
}
}