8 body types (slim, average, stocky, tall_lean, short_stout, athletic, heavyset, petite) × male/female. Style-anchored concept images fed through Trellis and Blender postprocess. Adds body_showcase scene with grid display, highlight controls (1-8), and toon shader with cull_disabled for Trellis mesh normals. Finding: Trellis works well for props but produces rough character meshes — characters will likely need composite body parts approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
6.3 KiB
GDScript
200 lines
6.3 KiB
GDScript
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
|