diff --git a/3D/Scenes/Props/control_pad_shoot_3d.tscn b/3D/Scenes/Props/control_pad_shoot_3d.tscn index 7602b5c3..4eb49b87 100644 --- a/3D/Scenes/Props/control_pad_shoot_3d.tscn +++ b/3D/Scenes/Props/control_pad_shoot_3d.tscn @@ -5,10 +5,60 @@ [ext_resource type="AudioStream" uid="uid://bjvklk7qmlivd" path="res://SFX/288963__littlerobotsoundfactory__click_electronic_14.wav" id="3_pmslt"] [ext_resource type="AudioStream" uid="uid://myr6n2c1u503" path="res://SFX/581602__samsterbirdies__beep-error.mp3" id="4_4smss"] [ext_resource type="PackedScene" uid="uid://w6jg5rx6d5gp" path="res://3D/BlockbenchModels/ControlPad/Control_Pad_Shoot.gltf" id="5_uhypp"] +[ext_resource type="Texture2D" uid="uid://b7i8madir447q" path="res://3D/BlockbenchModels/ControlPad/Control_Pad_Shoot_0.png" id="6_pmslt"] +[ext_resource type="Script" uid="uid://b46e1ft1scvwt" path="res://Scripts/Interactables/FrameAnimator3D.cs" id="6_uok2k"] [sub_resource type="SphereShape3D" id="SphereShape3D_itd0i"] radius = 0.868968 +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4smss"] +resource_name = "material_0" +transparency = 2 +alpha_scissor_threshold = 0.05 +alpha_antialiasing_mode = 0 +cull_mode = 2 +albedo_texture = ExtResource("6_pmslt") +texture_filter = 0 +texture_repeat = false + +[sub_resource type="Animation" id="Animation_pmslt"] +resource_name = "Flash" +length = 0.8 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("AnimatorManager:CurrentFrame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.20016107, 0.40089402, 0.60219884), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3] +} + +[sub_resource type="Animation" id="Animation_4smss"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("AnimatorManager:CurrentFrame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_fmo5v"] +_data = { +&"Flash": SubResource("Animation_pmslt"), +&"RESET": SubResource("Animation_4smss") +} + [node name="ControlPad" type="Area3D" unique_id=454991887 groups=["Interactable"]] collision_layer = 0 collision_mask = 136 @@ -38,3 +88,16 @@ stream = ExtResource("4_4smss") bus = &"Effects" [node name="blockbench_export2" parent="." unique_id=920256639 instance=ExtResource("5_uhypp")] + +[node name="cylinder" parent="blockbench_export2" index="0"] +surface_material_override/0 = SubResource("StandardMaterial3D_4smss") + +[node name="AnimatorManager" type="Node3D" parent="." unique_id=1796988427 node_paths=PackedStringArray("TargetMesh")] +script = ExtResource("6_uok2k") +TargetMesh = NodePath("../blockbench_export2/cylinder") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=38747516] +libraries/ = SubResource("AnimationLibrary_fmo5v") +autoplay = &"Flash" + +[editable path="blockbench_export2"] diff --git a/Scripts/Interactables/FrameAnimator3D.cs b/Scripts/Interactables/FrameAnimator3D.cs new file mode 100644 index 00000000..95fe5338 --- /dev/null +++ b/Scripts/Interactables/FrameAnimator3D.cs @@ -0,0 +1,94 @@ +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); + } +} + + diff --git a/Scripts/Interactables/FrameAnimator3D.cs.uid b/Scripts/Interactables/FrameAnimator3D.cs.uid new file mode 100644 index 00000000..47177a69 --- /dev/null +++ b/Scripts/Interactables/FrameAnimator3D.cs.uid @@ -0,0 +1 @@ +uid://b46e1ft1scvwt