AnimatedSprite3D with normals
Add the shader as Material Override in geometry and then add the following script:
@tool
extends AnimatedSprite3D
var _is_connected := false
var _shader_material : ShaderMaterial
func _ready():
_connect_signals()
_shader_material = material_override as ShaderMaterial
func _connect_signals():
if _is_connected:
return
_is_connected = true
connect("frame_changed", Callable(self, "_on_frame_changed"))
_on_frame_changed()
func _on_frame_changed():
if _shader_material and sprite_frames:
var frame_texture = sprite_frames.get_frame_texture(animation, frame)
_shader_material.set_shader_parameter("tex", frame_texture)
var frame_texture_normal = sprite_frames.get_frame_texture(animation + "_normal", frame)
_shader_material.set_shader_parameter("frame_hash", float(frame))
_shader_material.set_shader_parameter("normal", frame_texture_normal)
func _exit_tree():
_disconnect_signals()
func _disconnect_signals():
if not _is_connected:
return
_is_connected = false
disconnect("frame_changed", Callable(self, "_on_frame_changed"))
How to use it:
for each animation you will need the animation with just the normals suffixed with _normal (see the screenshot bellow).
Shader code
shader_type spatial;
render_mode cull_disabled, depth_prepass_alpha;
uniform sampler2D tex: source_color, filter_nearest;
uniform sampler2D normal_map: source_color, hint_normal, filter_nearest;
uniform float frame_hash;
void fragment() {
vec4 color = texture(tex, UV);
if (color.a <= 0.0) {
discard;
}
ALBEDO = color.rgb;
ALPHA = color.a;
// Read normal from normal map (in tangent space)
vec3 n = texture(normal_map, UV).rgb * 2.0 - 1.0;
// Since AnimatedSprite3D doesn't provide tangent/bitangent, simulate normal blending
NORMAL = normalize(NORMAL + n * 0.5);
}


I cannot wait to try this. This is one of my white whales. Thank you so much, I love you forever!