using System.Collections.Generic; using UnityEngine; /// /// 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 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. /// [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; /// The mesh baked from the current pose, in local space. Valid after the first bake. public Mesh BakedMesh => _baked; private SkinnedMeshRenderer _skinned; private MeshCollider _collider; private Mesh _baked; private float _timer; private readonly List _vbuf = new List(); // reused vertex scratch so the scale fixup allocates nothing per bake private void Awake() { _skinned = GetComponent(); if (!_skinned) { Debug.LogError("[SkinnedColliderBaker] No SkinnedMeshRenderer on this GameObject.", this); enabled = false; return; } _collider = GetComponent(); if (!_collider) _collider = gameObject.AddComponent(); _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); } }