Houses ~8.5 m; chimps capped ~0.85 m with safer axis fitting. Large GLB/PBR packs stay local until Git LFS (Forgejo HTTP 413). Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.5 KiB
GDScript
119 lines
3.5 KiB
GDScript
@tool
|
||
extends Node3D
|
||
class_name BillboardSprite
|
||
## Mario-tree billboard: upright Quad, yaw-only toward camera.
|
||
## Size always via SpriteFit (world height), never raw pixels × constant.
|
||
## @tool: faces the editor viewport camera while editing.
|
||
|
||
@export var texture: Texture2D:
|
||
set(v):
|
||
texture = v
|
||
if is_node_ready():
|
||
_apply()
|
||
## Meters; prefer SpriteFit.WORLD_* constants when setting from code.
|
||
@export var target_height: float = 1.74:
|
||
set(v):
|
||
target_height = v
|
||
if is_node_ready():
|
||
_apply()
|
||
@export var max_width: float = 2.2:
|
||
set(v):
|
||
max_width = v
|
||
if is_node_ready():
|
||
_apply()
|
||
@export var modulate_color: Color = Color.WHITE:
|
||
set(v):
|
||
modulate_color = v
|
||
if is_node_ready():
|
||
_set_albedo(modulate_color if not _highlight else _highlight_color)
|
||
|
||
@onready var _mesh_instance: MeshInstance3D = $MeshInstance3D
|
||
|
||
var _quad_height: float = 1.0
|
||
var _highlight: bool = false
|
||
var _highlight_color: Color = Color(1.35, 1.25, 0.35)
|
||
|
||
func _ready() -> void:
|
||
_ensure_mesh()
|
||
_apply()
|
||
|
||
func _process(_delta: float) -> void:
|
||
_face_camera_yaw_only()
|
||
if Engine.is_editor_hint():
|
||
return
|
||
if _highlight and _mesh_instance and _mesh_instance.material_override:
|
||
var pulse := 0.85 + 0.15 * sin(Time.get_ticks_msec() * 0.012)
|
||
(_mesh_instance.material_override as BaseMaterial3D).albedo_color = _highlight_color * pulse
|
||
|
||
func set_texture(tex: Texture2D) -> void:
|
||
texture = tex
|
||
_apply()
|
||
|
||
func set_target_height(h: float) -> void:
|
||
target_height = h
|
||
_apply()
|
||
|
||
func set_highlight(enabled: bool, color: Color = Color(1.35, 1.25, 0.35)) -> void:
|
||
if Engine.is_editor_hint():
|
||
return
|
||
_highlight = enabled
|
||
_highlight_color = color
|
||
if not enabled:
|
||
_set_albedo(modulate_color)
|
||
|
||
func flash(color: Color = Color(2.0, 2.0, 1.2), duration: float = 0.12) -> void:
|
||
if Engine.is_editor_hint():
|
||
return
|
||
_set_albedo(color)
|
||
var tree := get_tree()
|
||
if tree == null:
|
||
tree = Engine.get_main_loop() as SceneTree
|
||
if tree:
|
||
await tree.create_timer(duration, true).timeout
|
||
if is_instance_valid(self) and not _highlight:
|
||
_set_albedo(modulate_color)
|
||
|
||
func _ensure_mesh() -> void:
|
||
if _mesh_instance == null:
|
||
_mesh_instance = get_node_or_null("MeshInstance3D") as MeshInstance3D
|
||
|
||
func _set_albedo(c: Color) -> void:
|
||
_ensure_mesh()
|
||
if _mesh_instance and _mesh_instance.material_override:
|
||
(_mesh_instance.material_override as BaseMaterial3D).albedo_color = c
|
||
|
||
func _apply() -> void:
|
||
_ensure_mesh()
|
||
if _mesh_instance == null:
|
||
return
|
||
var quad := QuadMesh.new()
|
||
var size := SpriteFit.world_quad_size(texture, target_height, max_width)
|
||
var w := size.x
|
||
var h := size.y
|
||
quad.size = Vector2(w, h)
|
||
_quad_height = h
|
||
_mesh_instance.mesh = quad
|
||
_mesh_instance.position = Vector3(0.0, h * 0.5, 0.0)
|
||
_mesh_instance.rotation = Vector3.ZERO
|
||
|
||
var mat := StandardMaterial3D.new()
|
||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_DISABLED
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||
mat.alpha_scissor_threshold = 0.1
|
||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||
mat.albedo_color = modulate_color if not _highlight else _highlight_color
|
||
if texture != null:
|
||
mat.albedo_texture = texture
|
||
_mesh_instance.material_override = mat
|
||
|
||
func _face_camera_yaw_only() -> void:
|
||
var cam := get_viewport().get_camera_3d()
|
||
if cam == null:
|
||
return
|
||
var to_cam := cam.global_position - global_position
|
||
to_cam.y = 0.0
|
||
if to_cam.length_squared() < 0.0001:
|
||
return
|
||
look_at(global_position - to_cam, Vector3.UP)
|