cirnogodot/Scripts/Interactables/FrameAnimator3D.cs

94 lines
3.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Godot;
namespace Cirno.Scripts.Interactables;
/// <summary>
/// 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 <see cref="TargetMesh"/> in the inspector; if left empty the node
/// will search its parent for the first MeshInstance3D automatically.
/// 3. Set <see cref="FrameCount"/> to the number of frames in the sprite sheet.
/// 4. Drive <see cref="CurrentFrame"/> from an AnimationPlayer track — the script handles
/// the UV offset calculation.
/// </summary>
[Tool]
public partial class FrameAnimator3D : Node3D
{
/// <summary>
/// The mesh whose surface_0 material UV offset will be updated.
/// If null, the node searches its parent for the first MeshInstance3D on _Ready.
/// </summary>
[Export] public MeshInstance3D TargetMesh { get; set; }
/// <summary>
/// Total number of animation frames stacked vertically in the texture.
/// </summary>
[Export] public int FrameCount { get; set; } = 4;
/// <summary>
/// Surface index on the mesh to apply the UV offset to.
/// Defaults to 0, matching Blockbench's typical single-surface export.
/// </summary>
[Export] public int SurfaceIndex { get; set; } = 0;
private int _currentFrame;
/// <summary>
/// The active frame index (0-based). Set this from an AnimationPlayer track to animate.
/// Clamped to [0, <see cref="FrameCount"/> - 1].
/// </summary>
[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<Node>()?.FindChild("*", recursive: false, owned: false) as MeshInstance3D;
}
ApplyFrame();
}
/// <summary>
/// Calculates the normalised V offset for <see cref="CurrentFrame"/> and writes it to the
/// override material on <see cref="TargetMesh"/>'s surface.
/// Each frame occupies an equal vertical slice: offset = frame / frameCount.
/// </summary>
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);
}
}