diff --git a/spikes/3dpipeline/.gitignore b/spikes/3dpipeline/.gitignore index 1462cc00e..9cd9a1be3 100644 --- a/spikes/3dpipeline/.gitignore +++ b/spikes/3dpipeline/.gitignore @@ -5,3 +5,5 @@ # Extracted textures (regenerated by Godot import) *_Image_*.png +*_[0-9].png +*_[0-9][0-9].png diff --git a/spikes/3dpipeline/models/characters/bodies/athletic_f.glb b/spikes/3dpipeline/models/characters/bodies/athletic_f.glb new file mode 100644 index 000000000..c175d0f5c Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/athletic_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/athletic_m.glb b/spikes/3dpipeline/models/characters/bodies/athletic_m.glb new file mode 100644 index 000000000..beddf4c0d Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/athletic_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/average_f.glb b/spikes/3dpipeline/models/characters/bodies/average_f.glb new file mode 100644 index 000000000..9e084d84d Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/average_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/average_m.glb b/spikes/3dpipeline/models/characters/bodies/average_m.glb new file mode 100644 index 000000000..3f78012db Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/average_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/heavyset_f.glb b/spikes/3dpipeline/models/characters/bodies/heavyset_f.glb new file mode 100644 index 000000000..dc85b132a Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/heavyset_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/heavyset_m.glb b/spikes/3dpipeline/models/characters/bodies/heavyset_m.glb new file mode 100644 index 000000000..dd18e1c1b Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/heavyset_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/petite_f.glb b/spikes/3dpipeline/models/characters/bodies/petite_f.glb new file mode 100644 index 000000000..9e7129af8 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/petite_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/petite_m.glb b/spikes/3dpipeline/models/characters/bodies/petite_m.glb new file mode 100644 index 000000000..35950b05a Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/petite_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/short_stout_f.glb b/spikes/3dpipeline/models/characters/bodies/short_stout_f.glb new file mode 100644 index 000000000..7f71407f5 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/short_stout_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/short_stout_m.glb b/spikes/3dpipeline/models/characters/bodies/short_stout_m.glb new file mode 100644 index 000000000..a03eda249 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/short_stout_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/slim_f.glb b/spikes/3dpipeline/models/characters/bodies/slim_f.glb new file mode 100644 index 000000000..ed048b90e Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/slim_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/slim_m.glb b/spikes/3dpipeline/models/characters/bodies/slim_m.glb new file mode 100644 index 000000000..5bad4aee4 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/slim_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/stocky_f.glb b/spikes/3dpipeline/models/characters/bodies/stocky_f.glb new file mode 100644 index 000000000..d578acda8 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/stocky_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/stocky_m.glb b/spikes/3dpipeline/models/characters/bodies/stocky_m.glb new file mode 100644 index 000000000..df39b10f4 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/stocky_m.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/tall_lean_f.glb b/spikes/3dpipeline/models/characters/bodies/tall_lean_f.glb new file mode 100644 index 000000000..2cf1daa23 Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/tall_lean_f.glb differ diff --git a/spikes/3dpipeline/models/characters/bodies/tall_lean_m.glb b/spikes/3dpipeline/models/characters/bodies/tall_lean_m.glb new file mode 100644 index 000000000..67b83f4ff Binary files /dev/null and b/spikes/3dpipeline/models/characters/bodies/tall_lean_m.glb differ diff --git a/spikes/3dpipeline/project.godot b/spikes/3dpipeline/project.godot index 6fc56e28f..d8211c2c8 100644 --- a/spikes/3dpipeline/project.godot +++ b/spikes/3dpipeline/project.godot @@ -4,7 +4,7 @@ [application] config/name="SR 3D Pipeline Spike" -run/main_scene="res://scenes/spike_3d.tscn" +run/main_scene="res://scenes/body_showcase.tscn" config/features=PackedStringArray("4.6") [display] diff --git a/spikes/3dpipeline/scenes/body_showcase.tscn b/spikes/3dpipeline/scenes/body_showcase.tscn new file mode 100644 index 000000000..5b885d26d --- /dev/null +++ b/spikes/3dpipeline/scenes/body_showcase.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/spike/body_showcase.gd" id="1"] + +[node name="BodyShowcase" type="Node3D"] +script = ExtResource("1") + +[node name="Camera3D" type="Camera3D" parent="."] +projection = 1 +current = true +size = 12.0 +near = 0.1 +far = 100.0 diff --git a/spikes/3dpipeline/scripts/spike/body_showcase.gd b/spikes/3dpipeline/scripts/spike/body_showcase.gd new file mode 100644 index 000000000..0251b7b8d --- /dev/null +++ b/spikes/3dpipeline/scripts/spike/body_showcase.gd @@ -0,0 +1,199 @@ +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 diff --git a/spikes/3dpipeline/shaders/spike/toon_masked.gdshader b/spikes/3dpipeline/shaders/spike/toon_masked.gdshader index 5dfa82100..51be10d98 100644 --- a/spikes/3dpipeline/shaders/spike/toon_masked.gdshader +++ b/spikes/3dpipeline/shaders/spike/toon_masked.gdshader @@ -1,5 +1,5 @@ shader_type spatial; -render_mode unshaded; +render_mode unshaded, cull_disabled; // Original Trellis atlas texture uniform sampler2D albedo_tex : source_color;