extends Node3D ## FAILED APPROACH: runtime per-segment clothing scaling. ## ## Hypothesis: clothing authored on the Average body can be runtime-scaled ## per bone segment to fit other body types, without pre-baked variants. ## ## Result: FAILED CATASTROPHICALLY. Bone scaling produces extreme mesh ## distortion, vertex interpenetration, and visual artifacts. The deformation ## is non-uniform in ways that break clothing geometry. This approach is ## NOT viable for body type variation. ## ## Alternatives validated: ## - Surface Deform in Blender (fit_outfits_to_bodies.py) -- partially validated ## - Hand-authored body type meshes -- not yet tested ## ## This file is preserved as documentation of the failed approach. ## Do not use this pattern in production code. const ANIM_PATH := "res://models/quaternius/animations/UAL2_Standard.glb" const CAM_DIST: float = 20.0 # Body types and their segment scale factors relative to average. # These match the bone scale ratios from create_body_types.py (also FAILED). const SEGMENT_SCALES := { "thin": { "spine_01": Vector3(1.0, 0.86, 0.86), "spine_02": Vector3(1.0, 0.86, 0.86), "spine_03": Vector3(1.0, 0.86, 0.86), "pelvis": Vector3(1.0, 0.86, 0.86), "clavicle_l": Vector3(1.0, 0.83, 0.83), "clavicle_r": Vector3(1.0, 0.83, 0.83), "upperarm_l": Vector3(1.0, 0.83, 0.83), "upperarm_r": Vector3(1.0, 0.83, 0.83), "thigh_l": Vector3(1.0, 0.83, 0.83), "thigh_r": Vector3(1.0, 0.83, 0.83), }, "average": {}, # identity -- clothing is authored for this "muscular": { "spine_01": Vector3(1.0, 1.14, 1.14), "spine_02": Vector3(1.0, 1.14, 1.14), "spine_03": Vector3(1.0, 1.14, 1.14), "pelvis": Vector3(1.0, 1.10, 1.10), "clavicle_l": Vector3(1.0, 1.14, 1.14), "clavicle_r": Vector3(1.0, 1.14, 1.14), "upperarm_l": Vector3(1.0, 1.22, 1.22), "upperarm_r": Vector3(1.0, 1.22, 1.22), "thigh_l": Vector3(1.0, 1.19, 1.19), "thigh_r": Vector3(1.0, 1.19, 1.19), }, "heavy": { "spine_01": Vector3(1.0, 1.43, 1.48), "spine_02": Vector3(1.0, 1.34, 1.39), "spine_03": Vector3(1.0, 1.34, 1.39), "pelvis": Vector3(1.0, 1.30, 1.27), "clavicle_l": Vector3(1.0, 1.26, 1.26), "clavicle_r": Vector3(1.0, 1.26, 1.26), "upperarm_l": Vector3(1.0, 1.46, 1.46), "upperarm_r": Vector3(1.0, 1.46, 1.46), "thigh_l": Vector3(1.0, 1.45, 1.45), "thigh_r": Vector3(1.0, 1.45, 1.45), }, } ## NOTE: These body-types/ paths reference GLBs produced by create_body_types.py, ## which is also a FAILED approach. This smoke test may not run without those files. const BODY_PATHS := { "thin": "res://models/quaternius/body-types/male_thin.glb", "average": "res://models/quaternius/body-types/male_average.glb", "muscular": "res://models/quaternius/body-types/male_muscular.glb", "heavy": "res://models/quaternius/body-types/male_heavy.glb", } const OUTFIT_PATH := "res://models/quaternius/outfits-fantasy/full/Male_Peasant.gltf" var _camera: Camera3D var _characters: Array[CharacterVisual] = [] var _out_dir: String var _shots: Array[Dictionary] = [] var _shot_index: int = 0 var _settle_frames: int = 0 func _ready() -> void: var git_output: Array = [] OS.execute("git", ["rev-parse", "--show-toplevel"], git_output) var root := (git_output[0] as String).strip_edges() var timestamp := Time.get_datetime_string_from_system().replace(":", "").replace("T", "-").substr(0, 15) _out_dir = "%s/.tmp/runtime-scale-%s" % [root, timestamp] DirAccess.make_dir_recursive_absolute(_out_dir) print("Output: %s" % _out_dir) _camera = Camera3D.new() _camera.projection = Camera3D.PROJECTION_ORTHOGONAL _camera.near = 0.1 _camera.far = 100.0 _camera.current = true add_child(_camera) _build_floor() _build_lighting() # Place 2 characters side by side (average vs heavy) var positions := [ Vector3(-1.0, 0.04, 0.0), Vector3(1.0, 0.04, 0.0), ] var types := ["average", "heavy"] for i in range(2): var type_name: String = types[i] var cv := CharacterVisual.new() cv.set_shaders( preload("res://shaders/spike/toon.gdshader"), preload("res://shaders/spike/toon_masked.gdshader"), preload("res://shaders/spike/outline.gdshader"), ) cv.position = positions[i] cv.rotation_degrees.y = 45.0 add_child(cv) cv.load_unsegmented_body(BODY_PATHS[type_name]) var anims := cv.load_animation_library(ANIM_PATH) for anim_name in anims: if "Idle" in anim_name: cv.play_animation(anim_name) break cv.set_body_visible(false) cv.attach_outfit(OUTFIT_PATH) # Apply per-segment bone scale to outfit meshes via pose bones if type_name != "average": _apply_segment_scales(cv, SEGMENT_SCALES[type_name]) _characters.append(cv) print(" Character %d: %s" % [i, type_name]) # Shots _shots.append({ "name": "lineup_dramatic_gameplay", "angle": Vector3(-30.0, 45.0, 0.0), "zoom": 8.0, "pivot": Vector3(0.0, 0.9, 0.0), }) _shots.append({ "name": "lineup_frontal_gameplay", "angle": Vector3(-5.0, 45.0, 0.0), "zoom": 8.0, "pivot": Vector3(0.0, 0.9, 0.0), }) _shots.append({ "name": "lineup_dramatic_heavy_zoom", "angle": Vector3(-30.0, 45.0, 0.0), "zoom": 4.0, "pivot": Vector3(0.0, 1.0, 0.0), }) for i in range(2): var type_name: String = types[i] _shots.append({ "name": "%s_frontal_closeup" % type_name, "angle": Vector3(-5.0, 45.0, 0.0), "zoom": 3.0, "pivot": Vector3(positions[i].x, 1.2, positions[i].z), }) _setup_shot(_shots[0]) print("Runtime scale test: %d shots" % _shots.size()) func _apply_segment_scales(cv: CharacterVisual, scales: Dictionary) -> void: ## Apply per-segment bone scaling via the skeleton's pose bones. var skel := cv.get_skeleton() if skel == null: print("WARNING: no skeleton for segment scaling") return for bone_name in scales: var bone_idx := skel.find_bone(bone_name) if bone_idx == -1: continue var scale: Vector3 = scales[bone_name] skel.set_bone_pose_scale(bone_idx, scale) func _process(_delta: float) -> void: if _shot_index >= _shots.size(): print("Runtime scale test complete") get_tree().quit() return _settle_frames += 1 if _settle_frames < 15: return var img := get_viewport().get_texture().get_image() var filename := "%02d-%s.png" % [_shot_index + 1, _shots[_shot_index]["name"]] img.save_png(_out_dir.path_join(filename)) print(" [%d/%d] %s" % [_shot_index + 1, _shots.size(), filename]) _shot_index += 1 if _shot_index < _shots.size(): _setup_shot(_shots[_shot_index]) _settle_frames = 0 func _setup_shot(shot: Dictionary) -> void: var zoom := float(shot["zoom"]) _camera.size = zoom var pivot := shot["pivot"] as Vector3 var rot := shot["angle"] as Vector3 _camera.rotation_degrees = rot var basis := Basis.from_euler(rot * (PI / 180.0)) _camera.position = pivot + basis * Vector3(0, 0, CAM_DIST) func _build_floor() -> void: for x in range(-6, 7): for z in range(-4, 5): var mi := MeshInstance3D.new() var quad := PlaneMesh.new() quad.size = Vector2(0.95, 0.95) mi.mesh = quad mi.position = Vector3(float(x), 0.0, float(z)) var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if (abs(x) + abs(z)) % 2 == 0: mat.albedo_color = Color(0.35, 0.38, 0.42) else: mat.albedo_color = Color(0.28, 0.30, 0.34) mi.material_override = mat add_child(mi) func _build_lighting() -> void: var light := DirectionalLight3D.new() light.transform = Transform3D( Basis(Vector3(0.866, -0.354, 0.354), Vector3(0, 0.707, 0.707), Vector3(-0.5, -0.612, 0.612)), Vector3(5, 10, 5) ) light.light_energy = 0.8 add_child(light) var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.18, 0.2, 0.24) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.5, 0.5, 0.55) env.ambient_light_energy = 0.6 var world_env := WorldEnvironment.new() world_env.environment = env add_child(world_env)