extends Node ## Drives VRM facial expression blendshapes based on emotion signals. ## Smoothly interpolates between 6 base emotions. const NodeUtilsScript = preload("res://src/core/node_utils.gd") const BLEND_SPEED: float = 8.0 const EMOTION_SHAPES: Array[String] = [ "happy", "sad", "angry", "surprised", "relaxed", "neutral", ] var _mesh: MeshInstance3D var _blend_indices: Dictionary = {} var _target_weights: Dictionary = {} var _current_weights: Dictionary = {} var _current_emotion: String = "neutral" func setup(model: Node3D) -> void: _mesh = NodeUtilsScript.find_first_mesh_with_blendshapes(model) if _mesh == null: push_warning("ExpressionController: No mesh found") return for emotion: String in EMOTION_SHAPES: var idx := NodeUtilsScript.find_blend_shape_index(_mesh, emotion) if idx == -1: idx = NodeUtilsScript.find_blend_shape_index(_mesh, emotion.capitalize()) if idx == -1: idx = NodeUtilsScript.find_blend_shape_index(_mesh, emotion.to_upper()) if idx != -1: _blend_indices[emotion] = idx _target_weights[emotion] = 0.0 _current_weights[emotion] = 0.0 _set_emotion("neutral") EventBus.emotion_changed.connect(_on_emotion_changed) func _process(delta: float) -> void: if _mesh == null: return var speed := BLEND_SPEED * delta for emotion: String in EMOTION_SHAPES: var target: float = _target_weights[emotion] var current: float = _current_weights[emotion] if absf(current - target) > 0.001: current = lerpf(current, target, speed) _current_weights[emotion] = current if _blend_indices.has(emotion): _mesh.set_blend_shape_value(_blend_indices[emotion], current) func _set_emotion(emotion: String) -> void: _current_emotion = emotion for shape: String in EMOTION_SHAPES: _target_weights[shape] = (1.0 if shape == emotion else 0.0) func _on_emotion_changed(emotion: String) -> void: if emotion in EMOTION_SHAPES: _set_emotion(emotion) else: _set_emotion("neutral")