1018 lines
37 KiB
GDScript
1018 lines
37 KiB
GDScript
extends Node3D
|
|
class_name AnimeMechaKit
|
|
|
|
## High-detail, game-native anime mecha frame layered over an authored pilot.
|
|
##
|
|
## Each equipment group is built from many small mechanical forms, then merged
|
|
## into one ArrayMesh surface per material. The result keeps panel seams,
|
|
## vents, pistons, cabling, fasteners and nested thruster hardware without
|
|
## turning every visual detail into a draw call. Bone-rest correction makes the
|
|
## same frame fit Mixamo, Auto-Rig Pro and Rigify-style skeleton axes.
|
|
|
|
const ARMOR := 0
|
|
const ARMOR_ALT := 1
|
|
const FRAME := 2
|
|
const ACCENT := 3
|
|
const GLOW := 4
|
|
const METAL := 5
|
|
|
|
const THEMES := {
|
|
"sakura": {
|
|
"armor": Color("e8edf4"),
|
|
"armor_alt": Color("737d9e"),
|
|
"frame": Color("303852"),
|
|
"accent": Color("d95291"),
|
|
"glow": Color("ff9fd0"),
|
|
"metal": Color("171b2e"),
|
|
},
|
|
"akiba": {
|
|
"armor": Color("242a3a"),
|
|
"armor_alt": Color("dfe7ef"),
|
|
"frame": Color("111622"),
|
|
"accent": Color("20c9df"),
|
|
"glow": Color("ff45ba"),
|
|
"metal": Color("080b12"),
|
|
},
|
|
}
|
|
|
|
|
|
class Assembly:
|
|
var parent: Node3D
|
|
var surfaces: Dictionary = {}
|
|
var materials: Dictionary = {}
|
|
var part_count := 0
|
|
|
|
func _init(target: Node3D) -> void:
|
|
parent = target
|
|
|
|
func add(mesh: Mesh, transform: Transform3D, slot: int,
|
|
material: Material) -> void:
|
|
var surface: SurfaceTool = surfaces.get(slot)
|
|
if surface == null:
|
|
surface = SurfaceTool.new()
|
|
surfaces[slot] = surface
|
|
materials[slot] = material
|
|
surface.append_from(mesh, 0, transform)
|
|
part_count += 1
|
|
|
|
func commit(node_name: String) -> MeshInstance3D:
|
|
var combined := ArrayMesh.new()
|
|
var slots: Array = surfaces.keys()
|
|
slots.sort()
|
|
for slot in slots:
|
|
var surface: SurfaceTool = surfaces[slot]
|
|
surface.set_material(materials[slot])
|
|
surface.commit(combined)
|
|
var instance := MeshInstance3D.new()
|
|
instance.name = node_name
|
|
instance.mesh = combined
|
|
parent.add_child(instance)
|
|
return instance
|
|
|
|
|
|
var _skeleton: Skeleton3D
|
|
var _roles: Dictionary = {}
|
|
var _theme: Dictionary = {}
|
|
var _materials: Dictionary = {}
|
|
var _nozzles: Array[Node3D] = []
|
|
var _attachments: Array[BoneAttachment3D] = []
|
|
var _detail_parts := 0
|
|
var _helmet_progress := 0.0
|
|
var _helmet_target := 0.0
|
|
var _helmet_fx_left := 0.0
|
|
var _visor_left: MeshInstance3D
|
|
var _visor_right: MeshInstance3D
|
|
var _jaw_left: MeshInstance3D
|
|
var _jaw_right: MeshInstance3D
|
|
var _helmet_scan: MeshInstance3D
|
|
var _helmet_sweep: MeshInstance3D
|
|
var _helmet_light: OmniLight3D
|
|
var _helmet_fx_material: StandardMaterial3D
|
|
|
|
|
|
func setup(skeleton: Skeleton3D, roles: Dictionary, theme_name: String) -> void:
|
|
_skeleton = skeleton
|
|
_roles = roles
|
|
_theme = THEMES.get(theme_name, THEMES["sakura"])
|
|
_build_materials()
|
|
if theme_name == "sakura":
|
|
# The hero pilot is one continuous robotic second skin. Its thin shells
|
|
# follow the authored anatomy instead of hanging equipment boxes from it.
|
|
_build_second_skin_torso()
|
|
_build_second_skin_waist()
|
|
_build_second_skin_limbs()
|
|
_build_second_skin_nozzles()
|
|
_build_deployable_helmet()
|
|
else:
|
|
# Preserve the alternate skin while the hero design is being focused.
|
|
_build_torso()
|
|
_build_waist()
|
|
_build_limbs()
|
|
_build_nozzles()
|
|
set_process(true)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not is_instance_valid(_visor_left):
|
|
return
|
|
var blend := 1.0 - exp(-12.0 * delta)
|
|
_helmet_progress = lerpf(_helmet_progress, _helmet_target, blend)
|
|
if absf(_helmet_progress - _helmet_target) < 0.001:
|
|
_helmet_progress = _helmet_target
|
|
_update_helmet_parts(_helmet_progress)
|
|
if _helmet_fx_left <= 0.0:
|
|
_helmet_scan.visible = false
|
|
_helmet_sweep.visible = false
|
|
_helmet_light.light_energy = 0.0
|
|
return
|
|
_helmet_fx_left = maxf(0.0, _helmet_fx_left - delta)
|
|
var phase := 1.0 - _helmet_fx_left / 0.38
|
|
var flash := sin(phase * PI)
|
|
_helmet_scan.visible = true
|
|
_helmet_scan.scale = Vector3.ONE * lerpf(0.55, 1.65, phase)
|
|
_helmet_scan.rotation.z = phase * PI * 0.65
|
|
_helmet_sweep.visible = true
|
|
_helmet_sweep.position.x = lerpf(-0.105, 0.105, phase)
|
|
_helmet_light.light_energy = 2.8 * flash
|
|
var alpha := flash * 0.82
|
|
_helmet_fx_material.albedo_color.a = alpha
|
|
|
|
|
|
func set_helmet_closed(closed: bool, immediate: bool = false) -> void:
|
|
var wanted := 1.0 if closed else 0.0
|
|
if is_equal_approx(wanted, _helmet_target) and not immediate:
|
|
return
|
|
_helmet_target = wanted
|
|
if immediate:
|
|
_helmet_progress = wanted
|
|
_update_helmet_parts(wanted)
|
|
return
|
|
_helmet_fx_left = 0.38
|
|
|
|
|
|
func toggle_helmet() -> void:
|
|
set_helmet_closed(_helmet_target < 0.5)
|
|
|
|
|
|
func helmet_closed() -> bool:
|
|
return _helmet_target > 0.5
|
|
|
|
|
|
func helmet_progress_debug() -> float:
|
|
return _helmet_progress
|
|
|
|
|
|
func _build_second_skin_torso() -> void:
|
|
var chest := _attachment(_chest_bone(), "SecondSkinTorso")
|
|
if chest == null:
|
|
return
|
|
var a := Assembly.new(chest)
|
|
# A continuous dermal chassis sits only millimetres outside the authored
|
|
# bodysuit. Broad ellipsoids preserve the waist/chest curves; all brighter
|
|
# geometry below is inset-looking surfacing rather than a floating breastplate.
|
|
_ellipsoid(a, Vector3(0.335, 0.350, 0.176),
|
|
Vector3(0.0, -0.030, 0.0), FRAME)
|
|
_ellipsoid(a, Vector3(0.300, 0.265, 0.220),
|
|
Vector3(0.0, -0.235, -0.018), FRAME)
|
|
_capsule_plate(a, Vector3(0.046, 0.225, 0.010),
|
|
Vector3(0.0, -0.018, -0.095), METAL)
|
|
_capsule_plate(a, Vector3(0.012, 0.178, 0.005),
|
|
Vector3(0.0, -0.010, -0.103), GLOW)
|
|
for side in [-1.0, 1.0]:
|
|
# Three flexible pectoral laminates follow muscle flow into the ribs.
|
|
for panel in 3:
|
|
var y := 0.072 - panel * 0.067
|
|
var width := 0.128 - panel * 0.018
|
|
_capsule_plate(a, Vector3(width, 0.043, 0.009),
|
|
Vector3((0.070 + width * 0.14) * side, y, -0.092),
|
|
ARMOR if panel == 0 else ARMOR_ALT,
|
|
Vector3(0.0, 0.0, (-0.16 + panel * 0.035) * side))
|
|
_capsule_plate(a, Vector3(width * 0.62, 0.005, 0.004),
|
|
Vector3((0.078 + width * 0.10) * side, y - 0.024, -0.100),
|
|
ACCENT, Vector3(0.0, 0.0, -0.13 * side))
|
|
# Collar laminates grow out of the chest shell and terminate at the arm.
|
|
_capsule_plate(a, Vector3(0.142, 0.036, 0.012),
|
|
Vector3(0.078 * side, 0.142, -0.062), ARMOR,
|
|
Vector3(0.0, 0.0, -0.20 * side))
|
|
_capsule_plate(a, Vector3(0.090, 0.006, 0.004),
|
|
Vector3(0.084 * side, 0.151, -0.073), GLOW,
|
|
Vector3(0.0, 0.0, -0.20 * side))
|
|
|
|
# The rear reads as an artificial spinal system, not a backpack.
|
|
_capsule_plate(a, Vector3(0.052, 0.270, 0.014),
|
|
Vector3(0.0, -0.018, 0.096), METAL)
|
|
for vertebra in 7:
|
|
var y := 0.108 - vertebra * 0.037
|
|
_ellipsoid(a, Vector3(0.054, 0.022, 0.015),
|
|
Vector3(0.0, y, 0.105), ARMOR_ALT)
|
|
_sphere(a, 0.0045, Vector3(0.0, y, 0.115), GLOW, 8)
|
|
for side in [-1.0, 1.0]:
|
|
for rail in 4:
|
|
_capsule_plate(a, Vector3(0.012, 0.040, 0.008),
|
|
Vector3((0.070 + rail * 0.022) * side,
|
|
0.075 - rail * 0.052, 0.091), ACCENT,
|
|
Vector3(0.0, 0.0, 0.16 * side))
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkinTorsoCombined")
|
|
|
|
|
|
func _build_second_skin_waist() -> void:
|
|
var hips := _attachment(_role_bone("hips"), "SecondSkinWaist")
|
|
if hips == null:
|
|
return
|
|
var a := Assembly.new(hips)
|
|
_ellipsoid(a, Vector3(0.310, 0.320, 0.205),
|
|
Vector3(0.0, 0.115, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.095, 0.060, 0.018),
|
|
Vector3(0.0, 0.012, -0.067), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.038, 0.038, 0.008),
|
|
Vector3(0.0, 0.014, -0.080), GLOW)
|
|
for side in [-1.0, 1.0]:
|
|
_ellipsoid(a, Vector3(0.112, 0.145, 0.094),
|
|
Vector3(0.120 * side, -0.060, -0.002), FRAME)
|
|
_capsule_plate(a, Vector3(0.060, 0.105, 0.008),
|
|
Vector3(0.145 * side, -0.052, -0.044), ARMOR_ALT,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
_capsule_plate(a, Vector3(0.010, 0.090, 0.005),
|
|
Vector3(0.158 * side, -0.050, -0.053), ACCENT,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
for seam in 3:
|
|
_sphere(a, 0.006, Vector3((0.094 + seam * 0.024) * side,
|
|
-0.002, 0.060), GLOW, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkinWaistCombined")
|
|
|
|
|
|
func _build_second_skin_limbs() -> void:
|
|
for side_name in ["L", "R"]:
|
|
var side := -1.0 if side_name == "L" else 1.0
|
|
_build_second_skin_shoulder(side_name, side)
|
|
_build_second_skin_segment("upper_arm.%s" % side_name,
|
|
"UpperArm%s" % side_name, 0.165, 0.130, 0.245, 0.145, side)
|
|
_build_second_skin_segment("forearm.%s" % side_name,
|
|
"Forearm%s" % side_name, 0.158, 0.112, 0.265, 0.138, side)
|
|
_build_second_skin_hand(side_name, side)
|
|
_build_second_skin_segment("thigh.%s" % side_name,
|
|
"Thigh%s" % side_name, 0.195, 0.148, 0.315, 0.178, side, true)
|
|
_build_second_skin_segment("shin.%s" % side_name,
|
|
"Shin%s" % side_name, 0.158, 0.112, 0.350, 0.142, side, true)
|
|
_build_second_skin_boot(side_name, side)
|
|
|
|
|
|
func _build_second_skin_shoulder(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("upper_arm.%s" % side_name),
|
|
"SecondSkinShoulder%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
# A soft deltoid shell, continuous with the upper-arm membrane. The only
|
|
# hard edge is the clavicle seam; there is no independent shoulder pauldron.
|
|
_ellipsoid(a, Vector3(0.172, 0.112, 0.148),
|
|
Vector3(0.0, -0.010, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.128, 0.040, 0.009),
|
|
Vector3(0.0, 0.032, -0.071), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.090, 0.006, 0.004),
|
|
Vector3(0.0, 0.042, -0.078), GLOW)
|
|
for seam in [-1.0, 1.0]:
|
|
_capsule_plate(a, Vector3(0.008, 0.062, 0.004),
|
|
Vector3(0.044 * seam, -0.005, -0.073), ACCENT,
|
|
Vector3(0.0, 0.0, 0.15 * seam * side))
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkinShoulderCombined")
|
|
|
|
|
|
func _build_second_skin_segment(role: String, node_name: String,
|
|
top_width: float, bottom_width: float, length: float, depth: float,
|
|
side: float, leg: bool = false) -> void:
|
|
var mount := _attachment(_role_bone(role), "SecondSkin%s" % node_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
var center := -length * (0.25 if node_name.begins_with("Thigh") \
|
|
else (0.47 if leg else 0.47))
|
|
# One smooth dermal sleeve. A rounded anatomical volume avoids the flat ends
|
|
# and ruler-straight sides that make a tapered cylinder read as a strapped-on
|
|
# greave; the authored animation still supplies the underlying muscle motion.
|
|
_ellipsoid(a, Vector3((top_width + bottom_width) * 0.54, length, depth),
|
|
Vector3(0.0, center, 0.0), FRAME)
|
|
# Flush pearlescent laminates occupy only the front/outer quadrant and share
|
|
# the underlying limb curvature instead of forming a box around it.
|
|
_capsule_plate(a, Vector3(top_width * 0.30, length * 0.48, 0.007),
|
|
Vector3(0.0, center, -depth * 0.515), ARMOR)
|
|
_capsule_plate(a, Vector3(top_width * 0.15, length * 0.34, 0.005),
|
|
Vector3(top_width * 0.20 * side, center - length * 0.02, -depth * 0.53),
|
|
ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.007, length * 0.70, 0.004),
|
|
Vector3(top_width * 0.13 * side, center, -depth * 0.56), GLOW)
|
|
# Two transverse flex seams articulate the synthetic muscle without adding
|
|
# rings around the silhouette.
|
|
for seam_at in [0.34, 0.68]:
|
|
var seam_y: float = -length * float(seam_at)
|
|
_capsule_plate(a, Vector3(top_width * 0.40, 0.005, 0.0035),
|
|
Vector3(-top_width * 0.05 * side, seam_y, -depth * 0.545), METAL,
|
|
Vector3(0.0, 0.0, 0.08 * side))
|
|
_sphere(a, 0.0045 if leg else 0.0038,
|
|
Vector3(top_width * 0.37 * side, seam_y, -depth * 0.40), ACCENT, 8)
|
|
# A calf/thigh rear tendon gives the long leg a technological line without
|
|
# broadening it into greaves.
|
|
if leg:
|
|
_capsule_plate(a, Vector3(0.010, length * 0.60, 0.006),
|
|
Vector3(-top_width * 0.28 * side, center, depth * 0.49), ACCENT)
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkin%sCombined" % node_name)
|
|
|
|
|
|
func _build_second_skin_hand(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("hand.%s" % side_name),
|
|
"SecondSkinHand%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
# A flexible palm/knuckle membrane leaves finger articulation readable while
|
|
# visually continuing the forearm shell through the wrist.
|
|
_ellipsoid(a, Vector3(0.092, 0.118, 0.050),
|
|
Vector3(0.0, -0.043, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.050, 0.070, 0.005),
|
|
Vector3(0.010 * side, -0.048, -0.028), ARMOR_ALT,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
_capsule_plate(a, Vector3(0.005, 0.068, 0.003),
|
|
Vector3(-0.020 * side, -0.048, -0.032), GLOW)
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkinHandCombined%s" % side_name)
|
|
|
|
|
|
func _build_second_skin_boot(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("foot.%s" % side_name),
|
|
"SecondSkinBoot%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_ellipsoid(a, Vector3(0.138, 0.086, 0.214),
|
|
Vector3(0.0, -0.012, -0.050), FRAME)
|
|
_ellipsoid(a, Vector3(0.116, 0.032, 0.166),
|
|
Vector3(0.0, 0.024, -0.069), ARMOR)
|
|
_capsule_plate(a, Vector3(0.010, 0.008, 0.112),
|
|
Vector3(0.026 * side, 0.034, -0.070), GLOW)
|
|
for tread in 4:
|
|
_capsule_plate(a, Vector3(0.074, 0.007, 0.016),
|
|
Vector3(0.0, -0.047, -0.116 + tread * 0.039), METAL)
|
|
_detail_parts += a.part_count
|
|
a.commit("SecondSkinBootCombined")
|
|
|
|
|
|
func _build_second_skin_nozzles() -> void:
|
|
for i in 4:
|
|
var nozzle := Node3D.new()
|
|
nozzle.name = "DorsalVectorJet%s" % ("L" if i == 0 else "R") \
|
|
if i < 2 else "HeelVectorJet%s" % ("L" if i == 2 else "R")
|
|
nozzle.set_meta("mecha_thruster", true)
|
|
add_child(nozzle)
|
|
_nozzles.append(nozzle)
|
|
var a := Assembly.new(nozzle)
|
|
var k := 1.0 if i < 2 else 0.66
|
|
# A compact vector nozzle is the one unapologetically external tool. Its
|
|
# tapered root appears to grow through a reinforced pore in the second skin;
|
|
# no flower ring or floating satellite housing competes with the body.
|
|
_ellipsoid(a, Vector3(0.082, 0.064, 0.108) * k,
|
|
Vector3(0.0, 0.0, 0.050 * k), FRAME)
|
|
_cylinder(a, 0.042 * k, 0.032 * k, 0.072 * k,
|
|
Vector3(0.0, 0.0, 0.025 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
METAL, 16)
|
|
_torus(a, 0.022 * k, 0.043 * k, Vector3.ZERO,
|
|
Vector3(PI * 0.5, 0.0, 0.0), ACCENT, 16, 6)
|
|
_cylinder(a, 0.022 * k, 0.016 * k, 0.040 * k,
|
|
Vector3(0.0, 0.0, 0.018 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
METAL, 16)
|
|
_cylinder(a, 0.015 * k, 0.015 * k, 0.006 * k,
|
|
Vector3.ZERO, Vector3(PI * 0.5, 0.0, 0.0), GLOW, 16)
|
|
for shroud in 3:
|
|
var angle := shroud * TAU / 3.0 + PI * 0.5
|
|
_capsule_plate(a, Vector3(0.010, 0.034, 0.006) * k,
|
|
Vector3(cos(angle) * 0.039 * k, sin(angle) * 0.039 * k,
|
|
0.052 * k), ARMOR, Vector3(0.0, 0.0, angle))
|
|
_sphere(a, 0.004 * k,
|
|
Vector3(cos(angle) * 0.032 * k, sin(angle) * 0.032 * k,
|
|
0.074 * k), GLOW, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("VectorJetCombined")
|
|
|
|
|
|
func _build_deployable_helmet() -> void:
|
|
var head := _attachment(_role_bone("head"), "DeployableHelmet")
|
|
if head == null:
|
|
return
|
|
var shell := Assembly.new(head)
|
|
# Rear crown, temple rails, and cheek hinges stay visible when open; they
|
|
# make the visor feel stored inside a robotic cranial frame.
|
|
# Open-sided crown rails leave hair and face readable. A full ellipsoid here
|
|
# would become a solid mask under cel rendering even while the visor is open.
|
|
for side in [-1.0, 1.0]:
|
|
_capsule_plate(shell, Vector3(0.034, 0.135, 0.020),
|
|
Vector3(0.074 * side, 0.062, 0.080), FRAME,
|
|
Vector3(0.10, 0.0, 0.18 * side))
|
|
_capsule_plate(shell, Vector3(0.022, 0.105, 0.012),
|
|
Vector3(0.058 * side, 0.100, 0.094), ARMOR,
|
|
Vector3(0.14, 0.0, 0.28 * side))
|
|
_capsule_plate(shell, Vector3(0.060, 0.082, 0.015),
|
|
Vector3(0.0, 0.104, -0.070), ARMOR_ALT)
|
|
_capsule_plate(shell, Vector3(0.013, 0.066, 0.006),
|
|
Vector3(0.0, 0.108, -0.081), GLOW)
|
|
for side in [-1.0, 1.0]:
|
|
_capsule_plate(shell, Vector3(0.027, 0.110, 0.018),
|
|
Vector3(0.091 * side, 0.018, -0.006), ARMOR_ALT,
|
|
Vector3(0.0, 0.0, 0.12 * side))
|
|
_torus(shell, 0.013, 0.030, Vector3(0.095 * side, 0.002, -0.050),
|
|
Vector3(PI * 0.5, 0.0, 0.0), ACCENT, 12, 5)
|
|
for port in 3:
|
|
_sphere(shell, 0.005, Vector3(0.096 * side,
|
|
0.067 - port * 0.028, -0.054), GLOW, 8)
|
|
_detail_parts += shell.part_count
|
|
shell.commit("HelmetCranialFrameCombined")
|
|
|
|
var visor_mat := _visor_material(_theme["glow"])
|
|
_visor_left = _helmet_ellipsoid(head, "VisorLeft", visor_mat)
|
|
_visor_right = _helmet_ellipsoid(head, "VisorRight", visor_mat)
|
|
_jaw_left = _helmet_ellipsoid(head, "JawSealLeft", _materials[FRAME])
|
|
_jaw_right = _helmet_ellipsoid(head, "JawSealRight", _materials[FRAME])
|
|
|
|
_helmet_fx_material = _visor_material(_theme["glow"])
|
|
_helmet_fx_material.albedo_color = Color(_theme["glow"], 0.78)
|
|
_helmet_fx_material.emission_energy_multiplier = 4.0
|
|
_helmet_scan = MeshInstance3D.new()
|
|
_helmet_scan.name = "HelmetDeployRing"
|
|
var ring := TorusMesh.new()
|
|
ring.inner_radius = 0.072
|
|
ring.outer_radius = 0.078
|
|
ring.rings = 24
|
|
ring.ring_segments = 5
|
|
_helmet_scan.mesh = ring
|
|
_helmet_scan.material_override = _helmet_fx_material
|
|
_helmet_scan.rotation.x = PI * 0.5
|
|
_helmet_scan.position = Vector3(0.0, 0.120, 0.235)
|
|
_helmet_scan.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
head.add_child(_helmet_scan)
|
|
|
|
_helmet_sweep = MeshInstance3D.new()
|
|
_helmet_sweep.name = "HelmetVisorSweep"
|
|
var sweep_mesh := CapsuleMesh.new()
|
|
sweep_mesh.radius = 0.004
|
|
sweep_mesh.height = 0.080
|
|
sweep_mesh.radial_segments = 8
|
|
_helmet_sweep.mesh = sweep_mesh
|
|
_helmet_sweep.material_override = _helmet_fx_material
|
|
_helmet_sweep.position = Vector3(0.0, 0.120, 0.242)
|
|
_helmet_sweep.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
head.add_child(_helmet_sweep)
|
|
|
|
_helmet_light = OmniLight3D.new()
|
|
_helmet_light.name = "HelmetDeployFlash"
|
|
_helmet_light.light_color = _theme["glow"]
|
|
_helmet_light.omni_range = 1.25
|
|
_helmet_light.shadow_enabled = false
|
|
_helmet_light.position = Vector3(0.0, 0.120, 0.275)
|
|
head.add_child(_helmet_light)
|
|
_detail_parts += 8
|
|
_update_helmet_parts(0.0)
|
|
|
|
|
|
func _helmet_ellipsoid(parent: Node3D, node_name: String,
|
|
material: Material) -> MeshInstance3D:
|
|
var node := MeshInstance3D.new()
|
|
node.name = node_name
|
|
var mesh := SphereMesh.new()
|
|
mesh.radius = 0.5
|
|
mesh.height = 1.0
|
|
mesh.radial_segments = 16
|
|
mesh.rings = 8
|
|
node.mesh = mesh
|
|
node.material_override = material
|
|
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
parent.add_child(node)
|
|
return node
|
|
|
|
|
|
func _update_helmet_parts(amount: float) -> void:
|
|
var t := smoothstep(0.0, 1.0, amount)
|
|
# Two translucent robotic lenses iris out from the temples and lock on the
|
|
# centerline. Opening reverses the same physical path into the crown rails.
|
|
_visor_left.position = Vector3(lerpf(-0.082, -0.026, t),
|
|
lerpf(0.105, 0.142, t), lerpf(-0.040, 0.205, t))
|
|
_visor_right.position = Vector3(lerpf(0.082, 0.026, t),
|
|
lerpf(0.105, 0.142, t), lerpf(-0.040, 0.205, t))
|
|
_visor_left.scale = Vector3(0.060, lerpf(0.009, 0.022, t), 0.014)
|
|
_visor_right.scale = Vector3(0.060, lerpf(0.009, 0.022, t), 0.014)
|
|
_visor_left.rotation.z = lerpf(-0.72, -0.24, t)
|
|
_visor_right.rotation.z = lerpf(0.72, 0.24, t)
|
|
_jaw_left.position = Vector3(lerpf(-0.110, -0.075, t),
|
|
lerpf(-0.010, 0.018, t), lerpf(-0.025, 0.205, t))
|
|
_jaw_right.position = Vector3(lerpf(0.110, 0.075, t),
|
|
lerpf(-0.010, 0.018, t), lerpf(-0.025, 0.205, t))
|
|
_jaw_left.scale = Vector3(0.038, lerpf(0.030, 0.082, t), 0.018)
|
|
_jaw_right.scale = Vector3(0.038, lerpf(0.030, 0.082, t), 0.018)
|
|
_jaw_left.rotation.z = lerpf(-0.65, -0.28, t)
|
|
_jaw_right.rotation.z = lerpf(0.65, 0.28, t)
|
|
|
|
|
|
func _visor_material(color: Color) -> StandardMaterial3D:
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
|
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
material.albedo_color = Color(_theme["frame"], 0.84)
|
|
material.emission_enabled = true
|
|
material.emission = _theme["accent"]
|
|
material.emission_energy_multiplier = 1.15
|
|
return material
|
|
|
|
|
|
func _build_materials() -> void:
|
|
_materials = {
|
|
ARMOR: _armor_material(_theme["armor"]),
|
|
ARMOR_ALT: _armor_material(_theme["armor_alt"]),
|
|
FRAME: _armor_material(_theme["frame"]),
|
|
ACCENT: _armor_material(_theme["accent"]),
|
|
GLOW: _glow_material(_theme["glow"]),
|
|
METAL: _armor_material(_theme["metal"]),
|
|
}
|
|
|
|
|
|
func _build_torso() -> void:
|
|
var chest := _attachment(_chest_bone(), "MechaTorso")
|
|
if chest == null:
|
|
return
|
|
var a := Assembly.new(chest)
|
|
|
|
# Body-hugging backpack frame and central reactor spine. Rounded capsules
|
|
# overlap like vertebral armor instead of reading as a refrigerator slab.
|
|
_capsule_plate(a, Vector3(0.235, 0.255, 0.052),
|
|
Vector3(0.0, 0.0, 0.112), FRAME)
|
|
_capsule_plate(a, Vector3(0.195, 0.215, 0.027),
|
|
Vector3(0.0, 0.0, 0.157), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.075, 0.175, 0.025),
|
|
Vector3(0.0, 0.012, 0.180), ARMOR)
|
|
_capsule_plate(a, Vector3(0.026, 0.135, 0.010),
|
|
Vector3(0.0, 0.015, 0.200), ACCENT)
|
|
_cylinder(a, 0.044, 0.050, 0.026, Vector3(0.0, 0.025, 0.213),
|
|
Vector3(PI * 0.5, 0.0, 0.0), FRAME, 16)
|
|
_torus(a, 0.023, 0.046, Vector3(0.0, 0.025, 0.229),
|
|
Vector3(PI * 0.5, 0.0, 0.0), ACCENT)
|
|
_cylinder(a, 0.021, 0.021, 0.007, Vector3(0.0, 0.025, 0.236),
|
|
Vector3(PI * 0.5, 0.0, 0.0), GLOW, 16)
|
|
|
|
# Radiator vanes make the backpack read at distance and break up the slab.
|
|
for vane in 7:
|
|
var y := -0.090 + vane * 0.030
|
|
_capsule_plate(a, Vector3(0.165, 0.009, 0.016),
|
|
Vector3(0.0, y, 0.205), METAL)
|
|
|
|
for side in [-1.0, 1.0]:
|
|
# Shoulder-side power modules, coolant bottles and exposed conduits.
|
|
_tapered_shell(a, 0.070, 0.050, 0.170, 0.056,
|
|
Vector3(0.145 * side, -0.005, 0.126), FRAME,
|
|
Vector3(0.0, 0.0, -0.08 * side))
|
|
_capsule_plate(a, Vector3(0.058, 0.145, 0.020),
|
|
Vector3(0.147 * side, 0.0, 0.169), ARMOR,
|
|
Vector3(0.0, 0.0, -0.08 * side))
|
|
_capsule_plate(a, Vector3(0.025, 0.095, 0.009),
|
|
Vector3(0.149 * side, 0.005, 0.186), ACCENT)
|
|
_cylinder(a, 0.021, 0.021, 0.135,
|
|
Vector3(0.116 * side, -0.020, 0.190),
|
|
Vector3(0.0, 0.0, 0.0), METAL, 10)
|
|
_cylinder(a, 0.014, 0.014, 0.112,
|
|
Vector3(0.178 * side, -0.025, 0.178),
|
|
Vector3(0.0, 0.0, 0.0), ACCENT, 10)
|
|
for bolt_y in [-0.072, 0.072]:
|
|
_cylinder(a, 0.010, 0.010, 0.008,
|
|
Vector3(0.178 * side, bolt_y, 0.194),
|
|
Vector3(PI * 0.5, 0.0, 0.0), METAL, 8)
|
|
|
|
# Front chest armor: floating plate, inset seam and collar latch.
|
|
_capsule_plate(a, Vector3(0.105, 0.125, 0.025),
|
|
Vector3(0.060 * side, -0.020, -0.076), ARMOR,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
_capsule_plate(a, Vector3(0.072, 0.080, 0.010),
|
|
Vector3(0.056 * side, -0.018, -0.094), ARMOR_ALT,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
_capsule_plate(a, Vector3(0.056, 0.010, 0.006),
|
|
Vector3(0.052 * side, 0.004, -0.103), ACCENT,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
_capsule_plate(a, Vector3(0.015, 0.045, 0.006),
|
|
Vector3(0.015 * side, 0.045, -0.104), GLOW,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
|
|
# Articulated-looking dorsal fin: spar, armor leaves and luminous edge.
|
|
_capsule_plate(a, Vector3(0.018, 0.195, 0.016),
|
|
Vector3(0.195 * side, 0.015, 0.137), METAL,
|
|
Vector3(0.0, 0.0, -0.18 * side))
|
|
for segment in 3:
|
|
var sy := -0.058 + segment * 0.058
|
|
var reach := 0.064 - segment * 0.008
|
|
_capsule_plate(a, Vector3(reach, 0.040, 0.014),
|
|
Vector3((0.211 + reach * 0.28) * side, sy, 0.137),
|
|
ARMOR if segment % 2 == 0 else ARMOR_ALT,
|
|
Vector3(0.0, 0.0, -0.22 * side))
|
|
_capsule_plate(a, Vector3(0.008, 0.172, 0.007),
|
|
Vector3(0.230 * side, 0.014, 0.148), ACCENT,
|
|
Vector3(0.0, 0.0, -0.18 * side))
|
|
|
|
_detail_parts += a.part_count
|
|
a.commit("TorsoFrameCombined")
|
|
|
|
|
|
func _build_waist() -> void:
|
|
var hips := _attachment(_role_bone("hips"), "MechaWaist")
|
|
if hips == null:
|
|
return
|
|
var a := Assembly.new(hips)
|
|
_capsule_plate(a, Vector3(0.245, 0.060, 0.072),
|
|
Vector3(0.0, 0.015, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.082, 0.072, 0.022),
|
|
Vector3(0.0, 0.015, -0.052), ARMOR)
|
|
_capsule_plate(a, Vector3(0.042, 0.035, 0.008),
|
|
Vector3(0.0, 0.018, -0.068), ACCENT)
|
|
_sphere(a, 0.010, Vector3(0.0, 0.018, -0.075), GLOW, 8)
|
|
for side in [-1.0, 1.0]:
|
|
_capsule_plate(a, Vector3(0.066, 0.165, 0.025),
|
|
Vector3(0.150 * side, -0.078, 0.012), ARMOR,
|
|
Vector3(0.04, 0.0, 0.15 * side))
|
|
_capsule_plate(a, Vector3(0.044, 0.118, 0.011),
|
|
Vector3(0.153 * side, -0.074, -0.004), ARMOR_ALT,
|
|
Vector3(0.04, 0.0, 0.15 * side))
|
|
_capsule_plate(a, Vector3(0.010, 0.096, 0.006),
|
|
Vector3(0.174 * side, -0.072, -0.012), ACCENT,
|
|
Vector3(0.04, 0.0, 0.15 * side))
|
|
_cylinder(a, 0.014, 0.014, 0.090,
|
|
Vector3(0.116 * side, -0.072, 0.028),
|
|
Vector3(0.0, 0.0, 0.12 * side), METAL, 8)
|
|
for seam in 3:
|
|
_capsule_plate(a, Vector3(0.050, 0.006, 0.005),
|
|
Vector3(0.153 * side, -0.118 + seam * 0.048, -0.020), METAL,
|
|
Vector3(0.04, 0.0, 0.15 * side))
|
|
_detail_parts += a.part_count
|
|
a.commit("WaistFrameCombined")
|
|
|
|
|
|
func _build_limbs() -> void:
|
|
for side_name in ["L", "R"]:
|
|
var side := -1.0 if side_name == "L" else 1.0
|
|
_build_shoulder(side_name, side)
|
|
_build_forearm(side_name, side)
|
|
_build_thigh(side_name, side)
|
|
_build_shin(side_name, side)
|
|
_build_boot(side_name, side)
|
|
|
|
|
|
func _build_shoulder(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("upper_arm.%s" % side_name),
|
|
"MechaShoulder%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_ellipsoid(a, Vector3(0.145, 0.080, 0.125),
|
|
Vector3(0.0, 0.025, 0.0), FRAME)
|
|
_ellipsoid(a, Vector3(0.165, 0.052, 0.100),
|
|
Vector3(0.0, 0.062, 0.0), ARMOR)
|
|
_capsule_plate(a, Vector3(0.120, 0.017, 0.060),
|
|
Vector3(0.0, 0.094, 0.0), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.080, 0.008, 0.010),
|
|
Vector3(0.0, 0.108, -0.025), ACCENT)
|
|
_cylinder(a, 0.027, 0.027, 0.180, Vector3.ZERO,
|
|
Vector3(0.0, 0.0, PI * 0.5), METAL, 12)
|
|
_torus(a, 0.018, 0.036, Vector3(0.090 * side, 0.0, 0.0),
|
|
Vector3(0.0, 0.0, PI * 0.5), ACCENT)
|
|
for vent in 4:
|
|
_capsule_plate(a, Vector3(0.010, 0.006, 0.038),
|
|
Vector3(-0.038 + vent * 0.025, 0.118, 0.018), METAL)
|
|
for bolt_x in [-0.060, 0.060]:
|
|
for bolt_z in [-0.040, 0.040]:
|
|
_sphere(a, 0.008, Vector3(bolt_x, 0.137, bolt_z), ACCENT, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("ShoulderFrameCombined")
|
|
|
|
|
|
func _build_forearm(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("forearm.%s" % side_name),
|
|
"MechaForearm%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_tapered_shell(a, 0.100, 0.074, 0.215, 0.080,
|
|
Vector3(0.0, -0.105, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.088, 0.145, 0.025),
|
|
Vector3(0.0, -0.105, -0.060), ARMOR)
|
|
_capsule_plate(a, Vector3(0.055, 0.100, 0.011),
|
|
Vector3(0.0, -0.095, -0.078), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.011, 0.105, 0.006),
|
|
Vector3(0.026 * side, -0.095, -0.087), ACCENT)
|
|
_capsule_plate(a, Vector3(0.014, 0.038, 0.007),
|
|
Vector3(-0.019 * side, -0.125, -0.089), GLOW)
|
|
for rib in 4:
|
|
_torus(a, 0.045, 0.060, Vector3(0.0, -0.035 - rib * 0.052, 0.0),
|
|
Vector3.ZERO, METAL, 10, 5)
|
|
for bolt_side in [-1.0, 1.0]:
|
|
for bolt_y in [-0.055, -0.155]:
|
|
_sphere(a, 0.006, Vector3(0.049 * bolt_side, bolt_y, -0.060), ACCENT, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("ForearmFrameCombined")
|
|
|
|
|
|
func _build_thigh(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("thigh.%s" % side_name),
|
|
"MechaThigh%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_tapered_shell(a, 0.120, 0.088, 0.235, 0.082,
|
|
Vector3(0.0, -0.115, 0.0), FRAME)
|
|
_capsule_plate(a, Vector3(0.086, 0.175, 0.025),
|
|
Vector3(0.0, -0.105, -0.060), ARMOR)
|
|
_capsule_plate(a, Vector3(0.018, 0.140, 0.007),
|
|
Vector3(0.029 * side, -0.105, -0.078), ACCENT)
|
|
_capsule_plate(a, Vector3(0.036, 0.090, 0.018),
|
|
Vector3(0.070 * side, -0.115, 0.0), ARMOR_ALT)
|
|
_cylinder(a, 0.018, 0.018, 0.165, Vector3(-0.075 * side, -0.110, 0.025),
|
|
Vector3.ZERO, METAL, 8)
|
|
for seam in 3:
|
|
_capsule_plate(a, Vector3(0.060, 0.006, 0.006),
|
|
Vector3(0.0, -0.055 - seam * 0.062, -0.079), METAL)
|
|
for bolt_y in [-0.055, -0.175]:
|
|
_sphere(a, 0.008, Vector3(0.070 * side, bolt_y, -0.090), ACCENT, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("ThighFrameCombined")
|
|
|
|
|
|
func _build_shin(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("shin.%s" % side_name),
|
|
"MechaShin%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_tapered_shell(a, 0.118, 0.082, 0.305, 0.086,
|
|
Vector3(0.0, -0.155, 0.012), FRAME)
|
|
_capsule_plate(a, Vector3(0.096, 0.235, 0.026),
|
|
Vector3(0.0, -0.155, -0.062), ARMOR)
|
|
_capsule_plate(a, Vector3(0.056, 0.180, 0.011),
|
|
Vector3(0.0, -0.150, -0.081), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.013, 0.205, 0.006),
|
|
Vector3(0.029 * side, -0.150, -0.090), ACCENT)
|
|
_capsule_plate(a, Vector3(0.013, 0.062, 0.007),
|
|
Vector3(-0.022 * side, -0.195, -0.092), GLOW)
|
|
# Calf actuator, protective rail and vent bank.
|
|
_cylinder(a, 0.022, 0.022, 0.230, Vector3(-0.080 * side, -0.150, 0.035),
|
|
Vector3.ZERO, METAL, 10)
|
|
_cylinder(a, 0.011, 0.011, 0.175, Vector3(-0.080 * side, -0.150, 0.067),
|
|
Vector3.ZERO, ACCENT, 8)
|
|
for vent in 5:
|
|
_capsule_plate(a, Vector3(0.038, 0.007, 0.010),
|
|
Vector3(0.060 * side, -0.255 + vent * 0.042, 0.030), METAL,
|
|
Vector3(0.0, 0.0, 0.10 * side))
|
|
for bolt_y in [-0.055, -0.255]:
|
|
for bolt_x in [-0.045, 0.045]:
|
|
_sphere(a, 0.006, Vector3(bolt_x, bolt_y, -0.080), ACCENT, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("ShinFrameCombined")
|
|
|
|
|
|
func _build_boot(side_name: String, side: float) -> void:
|
|
var mount := _attachment(_role_bone("foot.%s" % side_name),
|
|
"MechaBoot%s" % side_name)
|
|
if mount == null:
|
|
return
|
|
var a := Assembly.new(mount)
|
|
_ellipsoid(a, Vector3(0.120, 0.072, 0.205),
|
|
Vector3(0.0, -0.020, -0.058), FRAME)
|
|
_ellipsoid(a, Vector3(0.108, 0.038, 0.165),
|
|
Vector3(0.0, 0.020, -0.068), ARMOR)
|
|
_capsule_plate(a, Vector3(0.074, 0.015, 0.080),
|
|
Vector3(0.0, 0.043, -0.092), ARMOR_ALT)
|
|
_capsule_plate(a, Vector3(0.012, 0.008, 0.105),
|
|
Vector3(0.031 * side, 0.052, -0.068), ACCENT)
|
|
for tread in 4:
|
|
_capsule_plate(a, Vector3(0.088, 0.010, 0.019),
|
|
Vector3(0.0, -0.061, -0.130 + tread * 0.047), METAL)
|
|
for bolt_x in [-0.045, 0.045]:
|
|
_sphere(a, 0.007, Vector3(bolt_x, 0.060, -0.155), ACCENT, 8)
|
|
_detail_parts += a.part_count
|
|
a.commit("BootFrameCombined")
|
|
|
|
|
|
func _build_nozzles() -> void:
|
|
for i in 4:
|
|
var nozzle := Node3D.new()
|
|
nozzle.name = "ShoulderThruster%s" % ("L" if i == 0 else "R") \
|
|
if i < 2 else "HeelThruster%s" % ("L" if i == 2 else "R")
|
|
nozzle.set_meta("mecha_thruster", true)
|
|
add_child(nozzle)
|
|
_nozzles.append(nozzle)
|
|
|
|
var a := Assembly.new(nozzle)
|
|
var k := 1.0 if i < 2 else 0.72
|
|
# Gimbal neck and nested convergent nozzle.
|
|
_cylinder(a, 0.052 * k, 0.052 * k, 0.060 * k,
|
|
Vector3(0.0, 0.0, 0.090 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
METAL, 16)
|
|
_torus(a, 0.038 * k, 0.066 * k, Vector3(0.0, 0.0, 0.075 * k),
|
|
Vector3(PI * 0.5, 0.0, 0.0), FRAME, 14, 6)
|
|
_cylinder(a, 0.070 * k, 0.052 * k, 0.115 * k,
|
|
Vector3(0.0, 0.0, 0.015 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
FRAME, 16)
|
|
_cylinder(a, 0.051 * k, 0.035 * k, 0.092 * k,
|
|
Vector3(0.0, 0.0, -0.010 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
METAL, 16)
|
|
_torus(a, 0.035 * k, 0.072 * k, Vector3(0.0, 0.0, -0.052 * k),
|
|
Vector3(PI * 0.5, 0.0, 0.0), ACCENT, 16, 6)
|
|
_cylinder(a, 0.032 * k, 0.032 * k, 0.008 * k,
|
|
Vector3(0.0, 0.0, -0.060 * k), Vector3(PI * 0.5, 0.0, 0.0),
|
|
GLOW, 16)
|
|
|
|
# Four armored petals, actuator rods and fasteners around each mouth.
|
|
for petal in 4:
|
|
var angle := petal * TAU / 4.0
|
|
var x := cos(angle) * 0.082 * k
|
|
var y := sin(angle) * 0.082 * k
|
|
_capsule_plate(a, Vector3(0.028, 0.065, 0.016) * k,
|
|
Vector3(x, y, -0.005 * k),
|
|
ARMOR if petal % 2 == 0 else ARMOR_ALT,
|
|
Vector3(0.0, 0.0, angle))
|
|
_cylinder(a, 0.007 * k, 0.007 * k, 0.080 * k,
|
|
Vector3(x * 0.72, y * 0.72, 0.035 * k),
|
|
Vector3(PI * 0.5, 0.0, angle), METAL, 8)
|
|
_sphere(a, 0.009 * k, Vector3(x, y, -0.026 * k), ACCENT, 8)
|
|
# Cooling slots around the outer housing.
|
|
for slot in 8:
|
|
var angle := slot * TAU / 8.0
|
|
_capsule_plate(a, Vector3(0.008, 0.024, 0.012) * k,
|
|
Vector3(cos(angle) * 0.105 * k, sin(angle) * 0.105 * k,
|
|
0.040 * k), METAL, Vector3(0.0, 0.0, angle))
|
|
|
|
_detail_parts += a.part_count
|
|
a.commit("ThrusterHardwareCombined")
|
|
|
|
|
|
func update_nozzles(world_positions: Array[Vector3], body_basis: Basis) -> void:
|
|
if world_positions.size() != _nozzles.size():
|
|
return
|
|
for i in _nozzles.size():
|
|
_nozzles[i].global_transform = Transform3D(body_basis.orthonormalized(),
|
|
world_positions[i])
|
|
|
|
|
|
func nozzle_world_positions() -> Array[Vector3]:
|
|
var result: Array[Vector3] = []
|
|
for nozzle in _nozzles:
|
|
result.append(nozzle.global_position)
|
|
return result
|
|
|
|
|
|
func nozzle_count() -> int:
|
|
return _nozzles.size()
|
|
|
|
|
|
func detail_part_count_debug() -> int:
|
|
return _detail_parts
|
|
|
|
|
|
func _attachment(bone: int, node_name: String) -> BoneAttachment3D:
|
|
if bone < 0 or not is_instance_valid(_skeleton):
|
|
return null
|
|
var attachment := BoneAttachment3D.new()
|
|
attachment.name = node_name
|
|
attachment.bone_idx = bone
|
|
# Cancel each rig's authored rest-bone roll. Equipment is modeled in a
|
|
# consistent character-local frame, then follows only the animated delta.
|
|
var rest := _skeleton.get_bone_global_rest(bone)
|
|
attachment.transform = Transform3D(rest.basis.inverse(), Vector3.ZERO)
|
|
_skeleton.add_child(attachment)
|
|
_attachments.append(attachment)
|
|
return attachment
|
|
|
|
|
|
func _role_bone(role: String) -> int:
|
|
if not is_instance_valid(_skeleton):
|
|
return -1
|
|
var name := String(_roles.get(role, ""))
|
|
return RigRoles.find_imported_bone(_skeleton, name)
|
|
|
|
|
|
func _chest_bone() -> int:
|
|
var spine: Array = _roles.get("spine", [])
|
|
var neck_name := String(_roles.get("neck", ""))
|
|
var neck_index := spine.find(neck_name)
|
|
if neck_index > 0:
|
|
return _skeleton.find_bone(String(spine[neck_index - 1]))
|
|
for i in range(spine.size() - 1, -1, -1):
|
|
var name := String(spine[i])
|
|
if name.findn("chest") >= 0 or name.findn("spine_03") >= 0:
|
|
return _skeleton.find_bone(name)
|
|
return _role_bone("neck")
|
|
|
|
|
|
func _box(a: Assembly, size: Vector3, pos: Vector3, slot: int,
|
|
rotation: Vector3 = Vector3.ZERO) -> void:
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = size
|
|
a.add(mesh, Transform3D(Basis.from_euler(rotation), pos), slot,
|
|
_materials[slot])
|
|
|
|
|
|
## A flattened capsule used as a close-fitting armor plate. Its rounded ends
|
|
## and elliptical cross-section keep the silhouette continuous with the pilot
|
|
## beneath it while still accepting the same panel/vent overlays as a box.
|
|
func _capsule_plate(a: Assembly, size: Vector3, pos: Vector3, slot: int,
|
|
rotation: Vector3 = Vector3.ZERO) -> void:
|
|
var mesh := CapsuleMesh.new()
|
|
var vertical := size.y >= size.x
|
|
var diameter := maxf(minf(size.x, size.y), 0.002)
|
|
mesh.radius = diameter * 0.5
|
|
mesh.height = maxf(maxf(size.x, size.y), diameter)
|
|
mesh.radial_segments = 10
|
|
mesh.rings = 2
|
|
var flatten := maxf(size.z / diameter, 0.04)
|
|
var axis := Basis.IDENTITY if vertical \
|
|
else Basis.from_euler(Vector3(0.0, 0.0, PI * 0.5))
|
|
var basis := Basis.from_euler(rotation) * axis \
|
|
* Basis.from_scale(Vector3(1.0, 1.0, flatten))
|
|
a.add(mesh, Transform3D(basis, pos), slot, _materials[slot])
|
|
|
|
|
|
## Faceted tapered shell around an arm or leg. Ten sides preserve the graphic
|
|
## mecha language, but the changing radius follows natural limb taper instead
|
|
## of placing a rectangular crate around every joint.
|
|
func _tapered_shell(a: Assembly, top_width: float, bottom_width: float,
|
|
height: float, depth: float, pos: Vector3, slot: int,
|
|
rotation: Vector3 = Vector3.ZERO) -> void:
|
|
var mesh := CylinderMesh.new()
|
|
mesh.top_radius = top_width * 0.5
|
|
mesh.bottom_radius = bottom_width * 0.5
|
|
mesh.height = height
|
|
mesh.radial_segments = 10
|
|
var average_width := maxf((top_width + bottom_width) * 0.5, 0.002)
|
|
var basis := Basis.from_euler(rotation) \
|
|
* Basis.from_scale(Vector3(1.0, 1.0, depth / average_width))
|
|
a.add(mesh, Transform3D(basis, pos), slot, _materials[slot])
|
|
|
|
|
|
## Smooth joint/boot housing. The mesh is deliberately low-ring rather than a
|
|
## perfect sphere so it remains anime-mechanical instead of looking organic.
|
|
func _ellipsoid(a: Assembly, size: Vector3, pos: Vector3, slot: int,
|
|
rotation: Vector3 = Vector3.ZERO) -> void:
|
|
var mesh := SphereMesh.new()
|
|
mesh.radius = 0.5
|
|
mesh.height = 1.0
|
|
mesh.radial_segments = 12
|
|
mesh.rings = 6
|
|
var basis := Basis.from_euler(rotation) * Basis.from_scale(size)
|
|
a.add(mesh, Transform3D(basis, pos), slot, _materials[slot])
|
|
|
|
|
|
func _cylinder(a: Assembly, top_radius: float, bottom_radius: float,
|
|
height: float, pos: Vector3, rotation: Vector3, slot: int,
|
|
segments: int = 12) -> void:
|
|
var mesh := CylinderMesh.new()
|
|
mesh.top_radius = top_radius
|
|
mesh.bottom_radius = bottom_radius
|
|
mesh.height = height
|
|
mesh.radial_segments = segments
|
|
a.add(mesh, Transform3D(Basis.from_euler(rotation), pos), slot,
|
|
_materials[slot])
|
|
|
|
|
|
func _torus(a: Assembly, inner_radius: float, outer_radius: float,
|
|
pos: Vector3, rotation: Vector3, slot: int, rings: int = 12,
|
|
segments: int = 6) -> void:
|
|
var mesh := TorusMesh.new()
|
|
mesh.inner_radius = inner_radius
|
|
mesh.outer_radius = outer_radius
|
|
mesh.rings = rings
|
|
mesh.ring_segments = segments
|
|
a.add(mesh, Transform3D(Basis.from_euler(rotation), pos), slot,
|
|
_materials[slot])
|
|
|
|
|
|
func _sphere(a: Assembly, radius: float, pos: Vector3, slot: int,
|
|
segments: int = 10) -> void:
|
|
var mesh := SphereMesh.new()
|
|
mesh.radius = radius
|
|
mesh.height = radius * 2.0
|
|
mesh.radial_segments = segments
|
|
mesh.rings = max(4, segments / 2)
|
|
a.add(mesh, Transform3D(Basis.IDENTITY, pos), slot, _materials[slot])
|
|
|
|
|
|
func _glow_material(color: Color) -> StandardMaterial3D:
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.albedo_color = color
|
|
material.emission_enabled = true
|
|
material.emission = color
|
|
material.emission_energy_multiplier = 3.0
|
|
return material
|
|
|
|
|
|
func _armor_material(color: Color) -> Material:
|
|
if DisplayServer.get_name() == "headless":
|
|
return LevelMaterials.unlit(color)
|
|
return LevelMaterials.cel(color, LevelMaterials.RAMP_3,
|
|
Color(0.72, 0.76, 0.90))
|