691 lines
24 KiB
GDScript
691 lines
24 KiB
GDScript
extends SceneTree
|
|
## Generates the placeholder avatar asset pack (GLB files) referenced by
|
|
## assets/avatars/catalog.json.
|
|
##
|
|
## These are deliberately crude primitive stand-ins so the avatar loader /
|
|
## customization / in-world rendering can be built and tested now; real art
|
|
## swaps in later as a pure content change (catalog paths are unchanged).
|
|
##
|
|
## The base body is a Roblox-classic (R6) style blocky figure: six chunky box
|
|
## parts (head, torso, two arms, two legs) skinned one-part-per-bone onto the
|
|
## 24-bone canonical rig, arms hanging at the sides, plus a painted-on face.
|
|
##
|
|
## Run: godot --headless --path . --script res://scripts/tools/gen_placeholder_avatars.gd
|
|
## Output: res://assets/avatars/*.glb (13 attachments + 1 rigged base body)
|
|
|
|
const OUT_DIR := "res://assets/avatars/"
|
|
|
|
# Canonical rig -- must match avatar_loader.gd CANONICAL_BONES. Each entry:
|
|
# name -> [parent_name, global_rest_position] (A-pose, arms down, Y-up, metres)
|
|
# The character faces -Z (Godot's node "forward"), so the face, toes and
|
|
# front-mounted attachments live at negative Z.
|
|
const BONES := {
|
|
"Root": ["", Vector3(0.00, 0.00, 0.00)],
|
|
"Hips": ["Root", Vector3(0.00, 0.76, 0.00)],
|
|
"Spine": ["Hips", Vector3(0.00, 1.00, 0.00)],
|
|
"Chest": ["Spine", Vector3(0.00, 1.30, 0.00)],
|
|
"Neck": ["Chest", Vector3(0.00, 1.50, 0.00)],
|
|
# Head bone sits at the visual head's centre and HeadTop just above its
|
|
# crown, so hats / hair / helmets mount where you'd expect. (The body mesh
|
|
# is rigidly bound, so moving these bones doesn't move the mesh.)
|
|
"Head": ["Neck", Vector3(0.00, 1.74, 0.00)],
|
|
"HeadTop": ["Head", Vector3(0.00, 2.06, 0.00)],
|
|
"Jaw": ["Head", Vector3(0.00, 1.66, -0.22)],
|
|
"LeftEye": ["Head", Vector3(0.12, 1.80, -0.28)],
|
|
"RightEye": ["Head", Vector3(-0.12, 1.80, -0.28)],
|
|
"LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)],
|
|
"LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)],
|
|
"LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)],
|
|
"LeftHand": ["LeftLowerArm", Vector3(0.60, 0.82, 0.00)],
|
|
"RightShoulder": ["Chest", Vector3(-0.42, 1.50, 0.00)],
|
|
"RightUpperArm": ["RightShoulder", Vector3(-0.60, 1.44, 0.00)],
|
|
"RightLowerArm": ["RightUpperArm", Vector3(-0.60, 1.14, 0.00)],
|
|
"RightHand": ["RightLowerArm", Vector3(-0.60, 0.82, 0.00)],
|
|
"LeftUpperLeg": ["Hips", Vector3(0.21, 0.74, 0.00)],
|
|
"LeftLowerLeg": ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00)],
|
|
"LeftFoot": ["LeftLowerLeg", Vector3(0.21, 0.05, -0.06)],
|
|
"RightUpperLeg": ["Hips", Vector3(-0.21, 0.74, 0.00)],
|
|
"RightLowerLeg": ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00)],
|
|
"RightFoot": ["RightLowerLeg", Vector3(-0.21, 0.05, -0.06)],
|
|
}
|
|
|
|
# Order matters: parents before children.
|
|
const BONE_ORDER := [
|
|
"Root", "Hips", "Spine", "Chest", "Neck", "Head", "HeadTop", "Jaw",
|
|
"LeftEye", "RightEye",
|
|
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
|
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
|
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
|
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
|
]
|
|
|
|
# Roblox-classic colour blocks.
|
|
const C_SKIN := Color(0.96, 0.80, 0.19) # bright yellow (head + arms)
|
|
const C_SHIRT := Color(0.05, 0.41, 0.67) # bright blue (torso)
|
|
const C_PANTS := Color(0.18, 0.49, 0.30) # bright green (legs)
|
|
const C_FACE := Color(0.05, 0.05, 0.06) # near-black (eyes + smile)
|
|
|
|
# Body parts: [bone, centre, size, colour]. Each part is one rigid box, fully
|
|
# weighted to its bone -- so the rig still drives it, but it reads as a clean
|
|
# solid block (no banding, no capsule taper).
|
|
const PARTS := [
|
|
# Torso, arms and legs all share the same 0.46 depth so a limb always fully
|
|
# covers the slice of torso behind it -- otherwise a brightly-lit sliver of
|
|
# the torso side peeks past the arm and reads as a cyan seam.
|
|
["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.58), C_SKIN],
|
|
["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.46), C_SHIRT],
|
|
["LeftUpperArm", Vector3(0.57, 1.14, 0.00), Vector3(0.34, 0.78, 0.46), C_SKIN],
|
|
["RightUpperArm", Vector3(-0.57, 1.14, 0.00), Vector3(0.34, 0.78, 0.46), C_SKIN],
|
|
["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.46), C_PANTS],
|
|
["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.46), C_PANTS],
|
|
# painted-on face -- thin plates on the front of the head (-Z), rigged to Head
|
|
["Head", Vector3(0.13, 1.78, -0.30), Vector3(0.09, 0.12, 0.02), C_FACE],
|
|
["Head", Vector3(-0.13, 1.78, -0.30), Vector3(0.09, 0.12, 0.02), C_FACE],
|
|
["Head", Vector3(0.00, 1.64, -0.30), Vector3(0.32, 0.05, 0.02), C_FACE],
|
|
]
|
|
|
|
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
|
# Mesh is built in _attachment_mesh().
|
|
const ATTACHMENTS := {
|
|
"head_cyber_01": ["head", Vector3(0, 0.12, 0.0), Color(0.20, 0.85, 0.95)],
|
|
"hair_mohawk_01": ["mohawk", Vector3(0, 0.10, 0.0), Color(0.90, 0.20, 0.35)],
|
|
"hair_ponytail_01": ["ponytail", Vector3(0, -0.22, 0.24), Color(0.45, 0.30, 0.15)],
|
|
"face_glasses_01": ["glasses", Vector3(0, 0.12, -0.07), Color(0.10, 0.10, 0.12)],
|
|
"back_backpack_01": ["box", Vector3(0, -0.06, 0.28), Color(0.30, 0.35, 0.25)],
|
|
"accessory_necklace_01": ["necklace", Vector3(0, 1.34, -0.10), Color(0.85, 0.75, 0.20)],
|
|
}
|
|
|
|
var _fail := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
var abs_dir := ProjectSettings.globalize_path(OUT_DIR)
|
|
DirAccess.make_dir_recursive_absolute(abs_dir)
|
|
|
|
_gen_base_body()
|
|
for id in ATTACHMENTS:
|
|
var spec: Array = ATTACHMENTS[id]
|
|
_gen_attachment(id, spec[0], spec[1], spec[2])
|
|
|
|
_gen_chest_bandit()
|
|
_gen_pants_cargo()
|
|
_gen_glove("left_hand_glove_01", 1.0)
|
|
_gen_glove("right_hand_glove_01", -1.0)
|
|
_gen_wristband("left_hand_wristband_01", 1.0)
|
|
_gen_wristband("right_hand_wristband_01", -1.0)
|
|
_gen_boot("left_foot_boot_01", 1.0)
|
|
_gen_boot("right_foot_boot_01", -1.0)
|
|
_gen_sneaker("left_foot_sneaker_01", 1.0)
|
|
_gen_sneaker("right_foot_sneaker_01", -1.0)
|
|
_gen_belt()
|
|
_gen_water_buffalo_hat()
|
|
|
|
print("\n%s" % ("DONE — all placeholder avatar GLBs written" if _fail == 0
|
|
else "FAILED (%d errors)" % _fail))
|
|
quit(_fail)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Base body -- rigged, skinned
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _global_rest(bone: String) -> Transform3D:
|
|
return Transform3D(Basis.IDENTITY, BONES[bone][1])
|
|
|
|
|
|
func _gen_base_body() -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AvatarRoot"
|
|
|
|
var skel := Skeleton3D.new()
|
|
skel.name = "Skeleton3D"
|
|
root.add_child(skel)
|
|
|
|
var idx := {}
|
|
for bone in BONE_ORDER:
|
|
var i := skel.add_bone(bone)
|
|
idx[bone] = i
|
|
for bone in BONE_ORDER:
|
|
var parent: String = BONES[bone][0]
|
|
var g := _global_rest(bone)
|
|
if parent == "":
|
|
skel.set_bone_rest(idx[bone], g)
|
|
else:
|
|
skel.set_bone_parent(idx[bone], idx[parent])
|
|
skel.set_bone_rest(idx[bone], _global_rest(parent).affine_inverse() * g)
|
|
for bone in BONE_ORDER:
|
|
skel.set_bone_pose_position(idx[bone], skel.get_bone_rest(idx[bone]).origin)
|
|
|
|
# Skin: one bind per bone, bind pose = inverse global rest.
|
|
var skin := Skin.new()
|
|
for bone in BONE_ORDER:
|
|
skin.add_named_bind(bone, _global_rest(bone).affine_inverse())
|
|
|
|
var bind_index := {}
|
|
for b in range(BONE_ORDER.size()):
|
|
bind_index[BONE_ORDER[b]] = b
|
|
|
|
# One surface per distinct colour so parts stay flat-shaded solids.
|
|
var groups: Dictionary = {} # Color (as key string) -> {color, verts, normals, bones, weights}
|
|
for part in PARTS:
|
|
var bone: String = part[0]
|
|
var centre: Vector3 = part[1]
|
|
var size: Vector3 = part[2]
|
|
var col: Color = part[3]
|
|
var key := col.to_html()
|
|
if not groups.has(key):
|
|
groups[key] = {
|
|
"color": col,
|
|
"verts": PackedVector3Array(),
|
|
"normals": PackedVector3Array(),
|
|
"bones": PackedInt32Array(),
|
|
"weights": PackedFloat32Array(),
|
|
}
|
|
var gg = groups[key]
|
|
_append_box(gg.verts, gg.normals, gg.bones, gg.weights,
|
|
centre, size, bind_index[bone])
|
|
|
|
var mesh := ArrayMesh.new()
|
|
var surf := 0
|
|
for key in groups:
|
|
var gg = groups[key]
|
|
var arr := []
|
|
arr.resize(Mesh.ARRAY_MAX)
|
|
arr[Mesh.ARRAY_VERTEX] = gg.verts
|
|
arr[Mesh.ARRAY_NORMAL] = gg.normals
|
|
arr[Mesh.ARRAY_BONES] = gg.bones
|
|
arr[Mesh.ARRAY_WEIGHTS] = gg.weights
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = gg.color
|
|
mat.roughness = 0.9
|
|
mesh.surface_set_material(surf, mat)
|
|
surf += 1
|
|
|
|
var mi := MeshInstance3D.new()
|
|
mi.name = "Body"
|
|
mi.mesh = mesh
|
|
mi.skin = skin
|
|
skel.add_child(mi)
|
|
mi.skeleton = mi.get_path_to(skel)
|
|
|
|
_write_glb(root, "base_human_male_01")
|
|
root.free()
|
|
|
|
|
|
# Append an axis-aligned box (centre, size) with every vert weighted 1.0 to bone_idx.
|
|
func _append_box(verts: PackedVector3Array, normals: PackedVector3Array,
|
|
bones: PackedInt32Array, weights: PackedFloat32Array,
|
|
centre: Vector3, size: Vector3, bone_idx: int) -> void:
|
|
var h := size * 0.5
|
|
# 8 corners
|
|
var c: Array[Vector3] = [
|
|
centre + Vector3(-h.x, -h.y, -h.z), centre + Vector3(h.x, -h.y, -h.z),
|
|
centre + Vector3(h.x, -h.y, h.z), centre + Vector3(-h.x, -h.y, h.z),
|
|
centre + Vector3(-h.x, h.y, -h.z), centre + Vector3(h.x, h.y, -h.z),
|
|
centre + Vector3(h.x, h.y, h.z), centre + Vector3(-h.x, h.y, h.z),
|
|
]
|
|
# Vertex order per face is CCW seen from outside -> the computed normal
|
|
# points outward. (Getting this backwards renders the whole mesh as an
|
|
# inside-out black shell: every face is lit from behind.)
|
|
var faces := [
|
|
[3, 2, 6, 7], # +Z front
|
|
[1, 0, 4, 5], # -Z back
|
|
[7, 6, 5, 4], # +Y top
|
|
[0, 1, 2, 3], # -Y bottom
|
|
[2, 1, 5, 6], # +X right
|
|
[3, 7, 4, 0], # -X left
|
|
]
|
|
for f in faces:
|
|
var q: Array[Vector3] = [c[f[0]], c[f[1]], c[f[2]], c[f[3]]]
|
|
var n: Vector3 = (q[1] - q[0]).cross(q[2] - q[0]).normalized()
|
|
for tri in [[0, 2, 1], [0, 3, 2]]:
|
|
for vi in tri:
|
|
verts.append(q[vi])
|
|
normals.append(n)
|
|
bones.append_array([bone_idx, 0, 0, 0])
|
|
weights.append_array([1.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Attachments -- plain static meshes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _gen_attachment(id: String, kind: String, offset: Vector3, col: Color) -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
var mi := MeshInstance3D.new()
|
|
mi.name = id
|
|
mi.mesh = _attachment_mesh(kind)
|
|
mi.position = offset
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = col
|
|
mat.roughness = 0.9
|
|
mi.material_override = mat
|
|
root.add_child(mi)
|
|
_write_glb(root, id)
|
|
root.free()
|
|
|
|
|
|
func _attachment_mesh(kind: String) -> Mesh:
|
|
match kind:
|
|
"head":
|
|
# A cyber visor band across the upper half of the ~0.58 head box.
|
|
var m := BoxMesh.new(); m.size = Vector3(0.64, 0.30, 0.62); return m
|
|
"mohawk":
|
|
var m := PrismMesh.new(); m.size = Vector3(0.10, 0.24, 0.50); return m
|
|
"ponytail":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.16, 0.36, 0.20); return m
|
|
"glasses":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.48, 0.11, 0.05); return m
|
|
"torso":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.80, 0.66, 0.42); return m
|
|
"box":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.34, 0.40, 0.16); return m
|
|
"hips":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.66, 0.34, 0.40); return m
|
|
"blob":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.30, 0.24, 0.34); return m
|
|
"boot":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.34, 0.16, 0.42); return m
|
|
"sneaker":
|
|
var m := BoxMesh.new(); m.size = Vector3(0.32, 0.14, 0.40); return m
|
|
"band":
|
|
var m := TorusMesh.new(); m.inner_radius = 0.10; m.outer_radius = 0.16; return m
|
|
"belt":
|
|
var m := TorusMesh.new(); m.inner_radius = 0.22; m.outer_radius = 0.30; return m
|
|
"necklace":
|
|
var m := TorusMesh.new(); m.inner_radius = 0.10; m.outer_radius = 0.14; return m
|
|
_:
|
|
return BoxMesh.new()
|
|
|
|
|
|
func _gen_water_buffalo_hat() -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
var cap_mat := StandardMaterial3D.new()
|
|
cap_mat.albedo_color = Color(0.13, 0.20, 0.58) # Flintstones Water Buffalo blue
|
|
cap_mat.roughness = 0.95
|
|
|
|
# 1. Hat Cap Base (twice as tall cylindrical body)
|
|
var cap := MeshInstance3D.new()
|
|
cap.name = "HatCap"
|
|
var cap_mesh := CylinderMesh.new()
|
|
cap_mesh.top_radius = 0.29
|
|
cap_mesh.bottom_radius = 0.32
|
|
cap_mesh.height = 0.60
|
|
cap.mesh = cap_mesh
|
|
cap.position = Vector3(0.0, 0.30, 0.0)
|
|
cap.material_override = cap_mat
|
|
root.add_child(cap)
|
|
|
|
# 1b. Rounded Dome Top (tall rounded crown)
|
|
var dome := MeshInstance3D.new()
|
|
dome.name = "HatDome"
|
|
var dome_mesh := SphereMesh.new()
|
|
dome_mesh.radius = 0.29
|
|
dome_mesh.height = 0.36
|
|
dome.mesh = dome_mesh
|
|
dome.position = Vector3(0.0, 0.56, 0.0)
|
|
dome.material_override = cap_mat
|
|
root.add_child(dome)
|
|
|
|
# 2. Fur Brim / Rim at bottom edge of cap
|
|
var brim := MeshInstance3D.new()
|
|
brim.name = "FurBrim"
|
|
var brim_mesh := TorusMesh.new()
|
|
brim_mesh.inner_radius = 0.28
|
|
brim_mesh.outer_radius = 0.36
|
|
brim.mesh = brim_mesh
|
|
brim.position = Vector3(0.0, 0.01, 0.0)
|
|
var brim_mat := StandardMaterial3D.new()
|
|
brim_mat.albedo_color = Color(0.24, 0.18, 0.13) # Fur trim
|
|
brim_mat.roughness = 1.0
|
|
brim.material_override = brim_mat
|
|
root.add_child(brim)
|
|
|
|
# 3. Lodge Gold Medallion on front (-Z is forward)
|
|
var medal := MeshInstance3D.new()
|
|
medal.name = "LodgeBadge"
|
|
var medal_mesh := BoxMesh.new()
|
|
medal_mesh.size = Vector3(0.12, 0.10, 0.04)
|
|
medal.mesh = medal_mesh
|
|
medal.position = Vector3(0.0, 0.22, -0.32)
|
|
var medal_mat := StandardMaterial3D.new()
|
|
medal_mat.albedo_color = Color(0.96, 0.82, 0.18) # Gold lodge crest
|
|
medal_mat.metallic = 0.7
|
|
medal_mat.roughness = 0.3
|
|
medal.material_override = medal_mat
|
|
root.add_child(medal)
|
|
|
|
# Horn material: first part is ivory/white, tip is brown (no middle piece)
|
|
var horn_mat := StandardMaterial3D.new()
|
|
horn_mat.albedo_color = Color(0.93, 0.90, 0.82) # Bone/ivory white
|
|
horn_mat.roughness = 0.45
|
|
|
|
var tip_mat := StandardMaterial3D.new()
|
|
tip_mat.albedo_color = Color(0.28, 0.18, 0.12) # Buffalo horn brown
|
|
tip_mat.roughness = 0.55
|
|
|
|
# 4. Short Water Buffalo Horns: white base + brown tip only
|
|
for side in [-1.0, 1.0]:
|
|
var side_name := "Right" if side < 0.0 else "Left"
|
|
|
|
# First part: white base segment projecting outward from side of hat
|
|
var h_base := MeshInstance3D.new()
|
|
h_base.name = "Horn_%s_Base" % side_name
|
|
var base_mesh := CylinderMesh.new()
|
|
base_mesh.bottom_radius = 0.08
|
|
base_mesh.top_radius = 0.055
|
|
base_mesh.height = 0.17
|
|
h_base.mesh = base_mesh
|
|
h_base.position = Vector3(side * 0.34, 0.22, 0.01)
|
|
h_base.rotation_degrees = Vector3(5.0, side * -10.0, side * -60.0)
|
|
h_base.material_override = horn_mat
|
|
root.add_child(h_base)
|
|
|
|
# Second part: brown tip segment curving upward (middle piece removed)
|
|
var h_tip := MeshInstance3D.new()
|
|
h_tip.name = "Horn_%s_Tip" % side_name
|
|
var tip_mesh := CylinderMesh.new()
|
|
tip_mesh.bottom_radius = 0.055
|
|
tip_mesh.top_radius = 0.005
|
|
tip_mesh.height = 0.15
|
|
h_tip.mesh = tip_mesh
|
|
h_tip.position = Vector3(side * 0.46, 0.32, -0.01)
|
|
h_tip.rotation_degrees = Vector3(12.0, 0.0, side * 10.0)
|
|
h_tip.material_override = tip_mat
|
|
root.add_child(h_tip)
|
|
|
|
_write_glb(root, "hair_waterbuffalo_01")
|
|
root.free()
|
|
|
|
|
|
func _gen_chest_bandit() -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
# Main vest/shirt covering torso (torso is 0.90 x 0.80 x 0.46)
|
|
var vest := MeshInstance3D.new()
|
|
vest.name = "Vest"
|
|
var vest_mesh := BoxMesh.new()
|
|
vest_mesh.size = Vector3(0.94, 0.82, 0.50)
|
|
vest.mesh = vest_mesh
|
|
vest.position = Vector3(0.0, -0.16, 0.0) # Relative to Chest bone (Y=1.30, torso center Y=1.14)
|
|
var vest_mat := StandardMaterial3D.new()
|
|
vest_mat.albedo_color = Color(0.55, 0.20, 0.20) # Bandit crimson
|
|
vest_mat.roughness = 0.85
|
|
vest.material_override = vest_mat
|
|
root.add_child(vest)
|
|
|
|
# Collar trim at top
|
|
var collar := MeshInstance3D.new()
|
|
collar.name = "Collar"
|
|
var collar_mesh := BoxMesh.new()
|
|
collar_mesh.size = Vector3(0.96, 0.12, 0.52)
|
|
collar.mesh = collar_mesh
|
|
collar.position = Vector3(0.0, 0.20, 0.0)
|
|
var trim_mat := StandardMaterial3D.new()
|
|
trim_mat.albedo_color = Color(0.22, 0.16, 0.12) # Dark leather trim
|
|
trim_mat.roughness = 0.8
|
|
collar.material_override = trim_mat
|
|
root.add_child(collar)
|
|
|
|
# Bottom hem trim
|
|
var hem := MeshInstance3D.new()
|
|
hem.name = "Hem"
|
|
var hem_mesh := BoxMesh.new()
|
|
hem_mesh.size = Vector3(0.96, 0.08, 0.52)
|
|
hem.mesh = hem_mesh
|
|
hem.position = Vector3(0.0, -0.53, 0.0)
|
|
hem.material_override = trim_mat
|
|
root.add_child(hem)
|
|
|
|
_write_glb(root, "chest_bandit_01")
|
|
root.free()
|
|
|
|
|
|
func _gen_pants_cargo() -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
var pants_mat := StandardMaterial3D.new()
|
|
pants_mat.albedo_color = Color(0.28, 0.34, 0.44) # Cargo slate blue
|
|
pants_mat.roughness = 0.9
|
|
|
|
var pocket_mat := StandardMaterial3D.new()
|
|
pocket_mat.albedo_color = Color(0.20, 0.24, 0.32) # Dark pocket trim
|
|
pocket_mat.roughness = 0.9
|
|
|
|
# 1. Waist section (covering hips across both legs)
|
|
var waist := MeshInstance3D.new()
|
|
waist.name = "Waist"
|
|
var waist_mesh := BoxMesh.new()
|
|
waist_mesh.size = Vector3(0.86, 0.20, 0.50)
|
|
waist.mesh = waist_mesh
|
|
waist.position = Vector3(0.0, -0.06, 0.0) # Relative to Hips bone (Y=0.76)
|
|
waist.material_override = pants_mat
|
|
root.add_child(waist)
|
|
|
|
# 2. Left and Right Pant Legs (legs are 0.40 wide, 0.78 tall, 0.46 deep, centers X = +-0.21)
|
|
for side in [-1.0, 1.0]:
|
|
var sname := "Right" if side < 0.0 else "Left"
|
|
var leg := MeshInstance3D.new()
|
|
leg.name = "PantLeg_%s" % sname
|
|
var leg_mesh := BoxMesh.new()
|
|
leg_mesh.size = Vector3(0.44, 0.64, 0.50) # Covers down to ankle
|
|
leg.mesh = leg_mesh
|
|
leg.position = Vector3(side * 0.21, -0.34, 0.0)
|
|
leg.material_override = pants_mat
|
|
root.add_child(leg)
|
|
|
|
# Cargo pocket on outer side
|
|
var pocket := MeshInstance3D.new()
|
|
pocket.name = "Pocket_%s" % sname
|
|
var pocket_mesh := BoxMesh.new()
|
|
pocket_mesh.size = Vector3(0.08, 0.20, 0.24)
|
|
pocket.mesh = pocket_mesh
|
|
pocket.position = Vector3(side * 0.44, -0.32, 0.0)
|
|
pocket.material_override = pocket_mat
|
|
root.add_child(pocket)
|
|
|
|
_write_glb(root, "pants_cargo_01")
|
|
root.free()
|
|
|
|
|
|
func _gen_glove(id: String, side: float) -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
# Arm is width 0.34, depth 0.46. Bone X is at side * 0.60, arm center is at side * 0.57.
|
|
var x_off := -0.03 * side
|
|
|
|
var glove := MeshInstance3D.new()
|
|
glove.name = "GloveBody"
|
|
var glove_mesh := BoxMesh.new()
|
|
glove_mesh.size = Vector3(0.38, 0.28, 0.50)
|
|
glove.mesh = glove_mesh
|
|
glove.position = Vector3(x_off, -0.06, 0.0)
|
|
var glove_mat := StandardMaterial3D.new()
|
|
glove_mat.albedo_color = Color(0.15, 0.15, 0.18)
|
|
glove_mat.roughness = 0.85
|
|
glove.material_override = glove_mat
|
|
root.add_child(glove)
|
|
|
|
# Glove cuff at top
|
|
var cuff := MeshInstance3D.new()
|
|
cuff.name = "GloveCuff"
|
|
var cuff_mesh := BoxMesh.new()
|
|
cuff_mesh.size = Vector3(0.40, 0.06, 0.52)
|
|
cuff.mesh = cuff_mesh
|
|
cuff.position = Vector3(x_off, 0.08, 0.0)
|
|
var cuff_mat := StandardMaterial3D.new()
|
|
cuff_mat.albedo_color = Color(0.25, 0.24, 0.28)
|
|
cuff_mat.roughness = 0.7
|
|
cuff.material_override = cuff_mat
|
|
root.add_child(cuff)
|
|
|
|
_write_glb(root, id)
|
|
root.free()
|
|
|
|
|
|
func _gen_wristband(id: String, side: float) -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
var x_off := -0.03 * side
|
|
var band := MeshInstance3D.new()
|
|
band.name = "Wristband"
|
|
var band_mesh := BoxMesh.new()
|
|
band_mesh.size = Vector3(0.38, 0.12, 0.50)
|
|
band.mesh = band_mesh
|
|
band.position = Vector3(x_off, 0.02, 0.0)
|
|
var band_mat := StandardMaterial3D.new()
|
|
band_mat.albedo_color = Color(0.96, 0.82, 0.18)
|
|
band_mat.metallic = 0.8
|
|
band_mat.roughness = 0.25
|
|
band.material_override = band_mat
|
|
root.add_child(band)
|
|
|
|
_write_glb(root, id)
|
|
root.free()
|
|
|
|
|
|
func _gen_boot(id: String, side: float) -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
# Boot Upper (encases lower leg and extends forward to toe)
|
|
var boot := MeshInstance3D.new()
|
|
boot.name = "BootUpper"
|
|
var boot_mesh := BoxMesh.new()
|
|
boot_mesh.size = Vector3(0.44, 0.22, 0.58)
|
|
boot.mesh = boot_mesh
|
|
boot.position = Vector3(0.0, 0.06, 0.02)
|
|
var boot_mat := StandardMaterial3D.new()
|
|
boot_mat.albedo_color = Color(0.14, 0.11, 0.10) # Dark boot leather
|
|
boot_mat.roughness = 0.8
|
|
boot.material_override = boot_mat
|
|
root.add_child(boot)
|
|
|
|
# Boot Sole
|
|
var sole := MeshInstance3D.new()
|
|
sole.name = "BootSole"
|
|
var sole_mesh := BoxMesh.new()
|
|
sole_mesh.size = Vector3(0.46, 0.06, 0.60)
|
|
sole.mesh = sole_mesh
|
|
sole.position = Vector3(0.0, -0.03, 0.02)
|
|
var sole_mat := StandardMaterial3D.new()
|
|
sole_mat.albedo_color = Color(0.07, 0.07, 0.08) # Black rubber tread
|
|
sole_mat.roughness = 0.95
|
|
sole.material_override = sole_mat
|
|
root.add_child(sole)
|
|
|
|
_write_glb(root, id)
|
|
root.free()
|
|
|
|
|
|
func _gen_sneaker(id: String, side: float) -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
var sneaker_mat := StandardMaterial3D.new()
|
|
sneaker_mat.albedo_color = Color(0.85, 0.25, 0.25) # Sneaker red
|
|
sneaker_mat.roughness = 0.8
|
|
|
|
var white_mat := StandardMaterial3D.new()
|
|
white_mat.albedo_color = Color(0.92, 0.92, 0.92) # White rubber
|
|
white_mat.roughness = 0.85
|
|
|
|
# Sneaker Upper
|
|
var upper := MeshInstance3D.new()
|
|
upper.name = "SneakerUpper"
|
|
var upper_mesh := BoxMesh.new()
|
|
upper_mesh.size = Vector3(0.44, 0.18, 0.58)
|
|
upper.mesh = upper_mesh
|
|
upper.position = Vector3(0.0, 0.04, 0.02)
|
|
upper.material_override = sneaker_mat
|
|
root.add_child(upper)
|
|
|
|
# White Rubber Sole
|
|
var sole := MeshInstance3D.new()
|
|
sole.name = "SneakerSole"
|
|
var sole_mesh := BoxMesh.new()
|
|
sole_mesh.size = Vector3(0.46, 0.06, 0.60)
|
|
sole.mesh = sole_mesh
|
|
sole.position = Vector3(0.0, -0.03, 0.02)
|
|
sole.material_override = white_mat
|
|
root.add_child(sole)
|
|
|
|
# White Rubber Toe Cap
|
|
var toe := MeshInstance3D.new()
|
|
toe.name = "ToeCap"
|
|
var toe_mesh := BoxMesh.new()
|
|
toe_mesh.size = Vector3(0.44, 0.08, 0.14)
|
|
toe.mesh = toe_mesh
|
|
toe.position = Vector3(0.0, 0.01, -0.21)
|
|
toe.material_override = white_mat
|
|
root.add_child(toe)
|
|
|
|
_write_glb(root, id)
|
|
root.free()
|
|
|
|
|
|
func _gen_belt() -> void:
|
|
var root := Node3D.new()
|
|
root.name = "AttachmentRoot"
|
|
|
|
var belt := MeshInstance3D.new()
|
|
belt.name = "BeltStrap"
|
|
var belt_mesh := BoxMesh.new()
|
|
belt_mesh.size = Vector3(0.94, 0.10, 0.50)
|
|
belt.mesh = belt_mesh
|
|
belt.position = Vector3(0.0, 0.74, 0.0)
|
|
var belt_mat := StandardMaterial3D.new()
|
|
belt_mat.albedo_color = Color(0.20, 0.16, 0.12)
|
|
belt_mat.roughness = 0.8
|
|
belt.material_override = belt_mat
|
|
root.add_child(belt)
|
|
|
|
var buckle := MeshInstance3D.new()
|
|
buckle.name = "BeltBuckle"
|
|
var buckle_mesh := BoxMesh.new()
|
|
buckle_mesh.size = Vector3(0.14, 0.12, 0.04)
|
|
buckle.mesh = buckle_mesh
|
|
buckle.position = Vector3(0.0, 0.74, -0.26)
|
|
var buckle_mat := StandardMaterial3D.new()
|
|
buckle_mat.albedo_color = Color(0.96, 0.82, 0.18)
|
|
buckle_mat.metallic = 0.8
|
|
buckle_mat.roughness = 0.25
|
|
buckle.material_override = buckle_mat
|
|
root.add_child(buckle)
|
|
|
|
_write_glb(root, "accessory_belt_01")
|
|
root.free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GLB export
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _write_glb(scene_root: Node, basename: String) -> void:
|
|
# append_from_scene needs the node in a tree.
|
|
get_root().add_child(scene_root)
|
|
var doc := GLTFDocument.new()
|
|
var state := GLTFState.new()
|
|
var err := doc.append_from_scene(scene_root, state)
|
|
if err != OK:
|
|
push_error("append_from_scene failed for %s: %s" % [basename, error_string(err)])
|
|
_fail += 1
|
|
get_root().remove_child(scene_root)
|
|
return
|
|
var out_path := ProjectSettings.globalize_path("%s%s.glb" % [OUT_DIR, basename])
|
|
err = doc.write_to_filesystem(state, out_path)
|
|
get_root().remove_child(scene_root)
|
|
if err != OK:
|
|
push_error("write_to_filesystem failed for %s: %s" % [basename, error_string(err)])
|
|
_fail += 1
|
|
return
|
|
print(" wrote %s.glb (%d bytes)" % [basename, FileAccess.get_file_as_bytes(out_path).size()])
|