using Godot; namespace Cirno.Scripts.Interactables; /// /// Animates a tiled texture by shifting the UV1 vertical offset on a mesh surface material. /// Designed for Blockbench-exported meshes whose texture is a vertical sprite sheet /// (e.g. 16×64 with 4 frames stacked top-to-bottom). /// /// Usage: /// 1. Attach this node as a child of (or sibling to) the MeshInstance3D you want to animate. /// 2. Optionally assign in the inspector; if left empty the node /// will search its parent for the first MeshInstance3D automatically. /// 3. Set to the number of frames in the sprite sheet. /// 4. Drive from an AnimationPlayer track — the script handles /// the UV offset calculation. /// [Tool] public partial class FrameAnimator3D : Node3D { /// /// The mesh whose surface_0 material UV offset will be updated. /// If null, the node searches its parent for the first MeshInstance3D on _Ready. /// [Export] public MeshInstance3D TargetMesh { get; set; } /// /// Total number of animation frames stacked vertically in the texture. /// [Export] public int FrameCount { get; set; } = 4; /// /// Surface index on the mesh to apply the UV offset to. /// Defaults to 0, matching Blockbench's typical single-surface export. /// [Export] public int SurfaceIndex { get; set; } = 0; private int _currentFrame; /// /// The active frame index (0-based). Set this from an AnimationPlayer track to animate. /// Clamped to [0, - 1]. /// [Export] public int CurrentFrame { get => _currentFrame; set { _currentFrame = Mathf.Clamp(value, 0, Mathf.Max(0, FrameCount - 1)); ApplyFrame(); } } public override void _Ready() { // Auto-discover the mesh from the parent if none was assigned in the inspector. if (TargetMesh is null) { TargetMesh = GetParent() as MeshInstance3D ?? GetParentOrNull()?.FindChild("*", recursive: false, owned: false) as MeshInstance3D; } ApplyFrame(); } /// /// Calculates the normalised V offset for and writes it to the /// override material on 's surface. /// Each frame occupies an equal vertical slice: offset = frame / frameCount. /// private void ApplyFrame() { if (TargetMesh is null || FrameCount <= 0) return; // Ensure we have an override material to write to so we don't mutate shared resources. if (TargetMesh.GetSurfaceOverrideMaterial(SurfaceIndex) is not StandardMaterial3D mat) { var baseMat = TargetMesh.GetActiveMaterial(SurfaceIndex); // Duplicate the base material so sibling nodes keep their own state. mat = baseMat?.Duplicate() as StandardMaterial3D; if (mat is null) return; TargetMesh.SetSurfaceOverrideMaterial(SurfaceIndex, mat); } // Each frame is a 1/FrameCount slice; V offset moves down one slice per frame. float vOffset = (float)_currentFrame / FrameCount; mat.Uv1Offset = new Vector3(mat.Uv1Offset.X, vOffset, mat.Uv1Offset.Z); } }