feat(assets): add body type showcase with 16 Trellis-generated bodies
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>
This commit is contained in:
@@ -5,3 +5,5 @@
|
||||
|
||||
# Extracted textures (regenerated by Godot import)
|
||||
*_Image_*.png
|
||||
*_[0-9].png
|
||||
*_[0-9][0-9].png
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user