extends Node3D ## Body Type Showcase — displays all 16 body types in a grid ## Controls: ## Mouse wheel: zoom in/out ## WASD: pan camera (screen-aligned) ## T: toggle camera angle ## 1-8: highlight a body type pair (dims others) ## 0: show all @onready var camera: Camera3D = $Camera3D var camera_pivot := Vector3(7.0, 0.0, 3.0) var camera_size_target: float = 12.0 var camera_angle: int = 2 var camera_target_rot := Vector3.ZERO var camera_current_rot := Vector3.ZERO const ZOOM_MIN: float = 3.0 const ZOOM_MAX: float = 30.0 const ZOOM_SPEED: float = 1.5 const PAN_SPEED: float = 15.0 const TILT_SPEED: float = 3.0 const CAM_DIST: float = 30.0 const ANGLE_PRESETS := [ Vector3(-90.0, 0.0, 0.0), Vector3(-45.0, 45.0, 0.0), Vector3(-30.0, 45.0, 0.0), ] ## Body types in display order — male top row, female bottom row const BODY_TYPES := [ "slim", "average", "stocky", "tall_lean", "short_stout", "athletic", "heavyset", "petite", ] var body_nodes: Dictionary = {} # "slim_m" -> Node3D func _ready() -> void: camera.projection = Camera3D.PROJECTION_ORTHOGONAL camera.size = camera_size_target camera.near = 0.1 camera.far = 100.0 camera_target_rot = ANGLE_PRESETS[camera_angle] camera_current_rot = camera_target_rot _apply_camera() _build_floor() _load_bodies() _add_labels() print("Body Showcase ready! Controls: scroll=zoom, WASD=pan, T=tilt, 1-8=highlight, 0=all") func _apply_camera() -> void: camera.rotation_degrees = camera_current_rot var basis := Basis.from_euler(camera_current_rot * (PI / 180.0)) camera.position = camera_pivot + basis * Vector3(0, 0, CAM_DIST) func _process(delta: float) -> void: camera.size = lerp(camera.size, camera_size_target, 8.0 * delta) camera_current_rot = camera_current_rot.lerp(camera_target_rot, TILT_SPEED * delta) _apply_camera() var input_dir := Vector2.ZERO if Input.is_key_pressed(KEY_W): input_dir.y += 1.0 if Input.is_key_pressed(KEY_S): input_dir.y -= 1.0 if Input.is_key_pressed(KEY_A): input_dir.x -= 1.0 if Input.is_key_pressed(KEY_D): input_dir.x += 1.0 if input_dir.length() > 0: input_dir = input_dir.normalized() var cam_right := camera.global_basis.x var cam_up := camera.global_basis.y var pan_right := Vector3(cam_right.x, 0, cam_right.z).normalized() var pan_up := Vector3(cam_up.x, 0, cam_up.z).normalized() camera_pivot += (pan_right * input_dir.x + pan_up * input_dir.y) * PAN_SPEED * delta _apply_camera() func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed: camera_size_target = max(ZOOM_MIN, camera_size_target - ZOOM_SPEED) elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed: camera_size_target = min(ZOOM_MAX, camera_size_target + ZOOM_SPEED) if event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_T: camera_angle = (camera_angle + 1) % ANGLE_PRESETS.size() camera_target_rot = ANGLE_PRESETS[camera_angle] elif event.keycode >= KEY_1 and event.keycode <= KEY_8: _highlight_pair(event.keycode - KEY_1) elif event.keycode == KEY_0: _show_all() func _build_floor() -> void: for x in range(16): for z in range(8): 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 (x + z) % 2 == 0: mat.albedo_color = Color(0.30, 0.32, 0.36) else: mat.albedo_color = Color(0.25, 0.27, 0.30) mi.material_override = mat add_child(mi) func _load_bodies() -> void: var spacing := 2.0 for i in range(BODY_TYPES.size()): var type_name: String = BODY_TYPES[i] var x_pos: float = float(i) * spacing # Male — top row (z=1) var m_key: String = type_name + "_m" var m_path: String = "res://models/characters/bodies/%s.glb" % m_key _place_body(m_path, m_key, Vector3(x_pos, 0, 2.0)) # Female — bottom row (z=4) var f_key: String = type_name + "_f" var f_path: String = "res://models/characters/bodies/%s.glb" % f_key _place_body(f_path, f_key, Vector3(x_pos, 0, 5.0)) var _toon_shader: Shader = preload("res://shaders/spike/toon_masked.gdshader") func _place_body(path: String, key: String, pos: Vector3) -> void: if not ResourceLoader.exists(path): print(" Missing: ", path) return var scene: PackedScene = load(path) if scene == null: print(" Failed to load: ", path) return var inst: Node3D = scene.instantiate() inst.position = pos _apply_shader(inst) add_child(inst) body_nodes[key] = inst print(" Loaded: ", key) func _apply_shader(node: Node) -> void: if node is MeshInstance3D: var mi := node as MeshInstance3D var mesh: Mesh = mi.mesh if mesh: for surf_idx in range(mesh.get_surface_count()): var existing: Material = mi.get_active_material(surf_idx) var albedo_tex: Texture2D = _extract_albedo(existing) var shader_mat := ShaderMaterial.new() shader_mat.shader = _toon_shader if albedo_tex: shader_mat.set_shader_parameter("albedo_tex", albedo_tex) # No mask — show original texture as-is shader_mat.set_shader_parameter("tint_color", Color(0.9, 0.85, 0.8)) mi.set_surface_override_material(surf_idx, shader_mat) for child in node.get_children(): _apply_shader(child) func _extract_albedo(mat: Material) -> Texture2D: if mat == null: return null if mat is BaseMaterial3D: var tex := (mat as BaseMaterial3D).albedo_texture if tex: return tex if mat is ShaderMaterial: var sm := mat as ShaderMaterial for param_name in ["albedo_tex", "albedo_texture", "texture_albedo", "base_color_texture"]: var val = sm.get_shader_parameter(param_name) if val is Texture2D: return val as Texture2D return null func _highlight_pair(index: int) -> void: if index >= BODY_TYPES.size(): return var type_name: String = BODY_TYPES[index] print("Highlight: ", type_name) for key in body_nodes: var node: Node3D = body_nodes[key] if key.begins_with(type_name): node.visible = true else: node.visible = false func _show_all() -> void: for key in body_nodes: body_nodes[key].visible = true print("Showing all") func _add_labels() -> void: # Labels are just 3D text at the top of each column # Godot doesn't have easy 3D text, so we skip for now pass