198 lines
6.0 KiB
GDScript
198 lines
6.0 KiB
GDScript
|
|
# GIF capture: plays a showcase of UAL1 clips on the tattooed model while the
|
||
|
|
# camera orbits a full 360°, then quits. Run with movie-maker mode:
|
||
|
|
# tinqs...console.exe --path <this dir> res://gif_capture.tscn \
|
||
|
|
# --write-movie <frames dir>/capture.png --fixed-fps 15
|
||
|
|
extends Node3D
|
||
|
|
|
||
|
|
const GLTF_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/Regular_Male_FullBody.gltf"
|
||
|
|
const BAKED_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/baked_body.png"
|
||
|
|
const NORMAL_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/T_Regular_Male_Normal_png.png"
|
||
|
|
const EYE_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/T_Eye_Brown.png"
|
||
|
|
const HAIR_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/T_Hair_1_BaseColor.png"
|
||
|
|
const UAL1 := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/anim-lib-1/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb"
|
||
|
|
|
||
|
|
# [clip, seconds] — one full camera orbit over the whole timeline
|
||
|
|
const SCHEDULE := [
|
||
|
|
["Idle_Loop", 2.5],
|
||
|
|
["Walk_Loop", 2.5],
|
||
|
|
["Jog_Fwd_Loop", 2.5],
|
||
|
|
["Dance_Loop", 3.0],
|
||
|
|
["Punch_Jab", 1.2],
|
||
|
|
["Punch_Cross", 1.3],
|
||
|
|
]
|
||
|
|
|
||
|
|
var skel: Skeleton3D
|
||
|
|
var cam: Camera3D
|
||
|
|
var player: AnimationPlayer
|
||
|
|
var hud: Label
|
||
|
|
|
||
|
|
var elapsed := 0.0
|
||
|
|
var total := 0.0
|
||
|
|
var seg_idx := -1
|
||
|
|
|
||
|
|
var orbit_pitch := 0.12
|
||
|
|
var orbit_dist := 2.5
|
||
|
|
var orbit_target := Vector3(0, 1.0, 0)
|
||
|
|
|
||
|
|
func _ready() -> void:
|
||
|
|
var env := Environment.new()
|
||
|
|
env.background_mode = Environment.BG_COLOR
|
||
|
|
env.background_color = Color(0.12, 0.13, 0.16)
|
||
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||
|
|
env.ambient_light_color = Color(0.75, 0.78, 0.85)
|
||
|
|
env.ambient_light_energy = 0.7
|
||
|
|
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||
|
|
var we := WorldEnvironment.new()
|
||
|
|
we.environment = env
|
||
|
|
add_child(we)
|
||
|
|
|
||
|
|
var sun := DirectionalLight3D.new()
|
||
|
|
sun.rotation_degrees = Vector3(-40, -35, 0)
|
||
|
|
sun.light_energy = 1.5
|
||
|
|
add_child(sun)
|
||
|
|
var fill := DirectionalLight3D.new()
|
||
|
|
fill.rotation_degrees = Vector3(-15, 140, 0)
|
||
|
|
fill.light_energy = 0.6
|
||
|
|
add_child(fill)
|
||
|
|
var under := DirectionalLight3D.new()
|
||
|
|
under.rotation_degrees = Vector3(35, 0, 0)
|
||
|
|
under.light_energy = 0.35
|
||
|
|
add_child(under)
|
||
|
|
|
||
|
|
var ground := MeshInstance3D.new()
|
||
|
|
var plane := PlaneMesh.new()
|
||
|
|
plane.size = Vector2(12, 12)
|
||
|
|
ground.mesh = plane
|
||
|
|
var gmat := StandardMaterial3D.new()
|
||
|
|
gmat.albedo_color = Color(0.16, 0.17, 0.21)
|
||
|
|
gmat.roughness = 1.0
|
||
|
|
ground.set_surface_override_material(0, gmat)
|
||
|
|
add_child(ground)
|
||
|
|
|
||
|
|
var doc := GLTFDocument.new()
|
||
|
|
var state := GLTFState.new()
|
||
|
|
if doc.append_from_file(GLTF_PATH, state) != OK:
|
||
|
|
push_error("GLTF load failed")
|
||
|
|
get_tree().quit(1)
|
||
|
|
return
|
||
|
|
var model := doc.generate_scene(state)
|
||
|
|
add_child(model)
|
||
|
|
|
||
|
|
var baked := ImageTexture.create_from_image(Image.load_from_file(BAKED_PATH))
|
||
|
|
var body_normal := ImageTexture.create_from_image(Image.load_from_file(NORMAL_PATH))
|
||
|
|
var eye_tex := ImageTexture.create_from_image(Image.load_from_file(EYE_PATH))
|
||
|
|
var hair_tex := ImageTexture.create_from_image(Image.load_from_file(HAIR_PATH))
|
||
|
|
|
||
|
|
var stack: Array = [model]
|
||
|
|
while not stack.is_empty():
|
||
|
|
var n: Node = stack.pop_back()
|
||
|
|
if n is Skeleton3D and skel == null:
|
||
|
|
skel = n
|
||
|
|
if n is MeshInstance3D:
|
||
|
|
var lname := String(n.name).to_lower()
|
||
|
|
var mat := StandardMaterial3D.new()
|
||
|
|
mat.roughness = 0.85
|
||
|
|
if lname.contains("regularmale"):
|
||
|
|
mat.albedo_texture = baked
|
||
|
|
mat.normal_enabled = true
|
||
|
|
mat.normal_texture = body_normal
|
||
|
|
elif lname.contains("eye") and not lname.contains("brow"):
|
||
|
|
mat.albedo_texture = eye_tex
|
||
|
|
else:
|
||
|
|
mat.albedo_texture = hair_tex
|
||
|
|
for s in n.mesh.get_surface_count():
|
||
|
|
n.set_surface_override_material(s, mat)
|
||
|
|
for c in n.get_children():
|
||
|
|
stack.push_back(c)
|
||
|
|
|
||
|
|
_load_ual_clips()
|
||
|
|
|
||
|
|
cam = Camera3D.new()
|
||
|
|
cam.fov = 50
|
||
|
|
add_child(cam)
|
||
|
|
cam.current = true
|
||
|
|
|
||
|
|
hud = Label.new()
|
||
|
|
hud.position = Vector2(16, 12)
|
||
|
|
hud.add_theme_font_size_override("font_size", 26)
|
||
|
|
hud.add_theme_color_override("font_color", Color(0.95, 0.95, 0.9))
|
||
|
|
add_child(hud)
|
||
|
|
|
||
|
|
for spec in SCHEDULE:
|
||
|
|
total += spec[1]
|
||
|
|
|
||
|
|
func _load_ual_clips() -> void:
|
||
|
|
var doc := GLTFDocument.new()
|
||
|
|
var state := GLTFState.new()
|
||
|
|
if doc.append_from_file(UAL1, state) != OK:
|
||
|
|
push_error("UAL1 load failed")
|
||
|
|
get_tree().quit(1)
|
||
|
|
return
|
||
|
|
var ual := doc.generate_scene(state)
|
||
|
|
var src: AnimationPlayer = null
|
||
|
|
var stack: Array = [ual]
|
||
|
|
while not stack.is_empty():
|
||
|
|
var n: Node = stack.pop_back()
|
||
|
|
if n is AnimationPlayer:
|
||
|
|
src = n
|
||
|
|
break
|
||
|
|
for c in n.get_children():
|
||
|
|
stack.push_back(c)
|
||
|
|
if src == null:
|
||
|
|
push_error("no AnimationPlayer in UAL1")
|
||
|
|
get_tree().quit(1)
|
||
|
|
return
|
||
|
|
|
||
|
|
player = AnimationPlayer.new()
|
||
|
|
add_child(player)
|
||
|
|
var skel_path := String(get_path_to(skel))
|
||
|
|
var lib := AnimationLibrary.new()
|
||
|
|
for spec in SCHEDULE:
|
||
|
|
var clip_name: String = spec[0]
|
||
|
|
if lib.has_animation(clip_name):
|
||
|
|
continue
|
||
|
|
if not src.has_animation(clip_name):
|
||
|
|
push_warning("UAL1 missing clip: " + clip_name)
|
||
|
|
continue
|
||
|
|
var anim := src.get_animation(clip_name).duplicate() as Animation
|
||
|
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||
|
|
for i in anim.get_track_count():
|
||
|
|
var path := String(anim.track_get_path(i))
|
||
|
|
if path.contains("Armature/Skeleton3D"):
|
||
|
|
anim.track_set_path(i, path.replace("Armature/Skeleton3D", skel_path))
|
||
|
|
lib.add_animation(clip_name, anim)
|
||
|
|
player.add_animation_library("", lib)
|
||
|
|
ual.queue_free()
|
||
|
|
|
||
|
|
func _process(delta: float) -> void:
|
||
|
|
if player == null:
|
||
|
|
return
|
||
|
|
elapsed += delta
|
||
|
|
if elapsed >= total:
|
||
|
|
get_tree().quit()
|
||
|
|
return
|
||
|
|
|
||
|
|
# which schedule segment are we in?
|
||
|
|
var t := elapsed
|
||
|
|
var idx := 0
|
||
|
|
for spec in SCHEDULE:
|
||
|
|
if t < spec[1]:
|
||
|
|
break
|
||
|
|
t -= spec[1]
|
||
|
|
idx += 1
|
||
|
|
idx = mini(idx, SCHEDULE.size() - 1)
|
||
|
|
if idx != seg_idx:
|
||
|
|
seg_idx = idx
|
||
|
|
var clip: String = SCHEDULE[idx][0]
|
||
|
|
if player.has_animation(clip):
|
||
|
|
player.play(clip)
|
||
|
|
hud.text = clip.replace("_Loop", "").replace("_", " ")
|
||
|
|
|
||
|
|
# one full orbit over the whole timeline, starting from the front
|
||
|
|
var yaw := TAU * elapsed / total
|
||
|
|
var dir := Vector3(
|
||
|
|
sin(yaw) * cos(orbit_pitch),
|
||
|
|
sin(orbit_pitch),
|
||
|
|
cos(yaw) * cos(orbit_pitch))
|
||
|
|
cam.look_at_from_position(orbit_target + dir * orbit_dist, orbit_target)
|