the real update!
ive never broken anything, ever!
This commit is contained in:
+462
-197
@@ -1,175 +1,206 @@
|
||||
@tool
|
||||
extends Node3D
|
||||
## Kestrel EC-01 — analog 0..1 clip driver + socket plumes.
|
||||
## Attach to the Kestrel child (inherited hull scene), NOT the CharacterBody3D.
|
||||
## Sockets: Vfx_RCS_* and Vfx_Abort_* must be Node3D (not GPUParticles).
|
||||
## Ship axes: nose +Y, starboard +X, dorsal +Z.
|
||||
## Kestrel EC-01 plume driver. Godot 4.7.
|
||||
## Attach to the Kestrel child. Leave escape_capsule.gd on EscapeCapsule.
|
||||
## Nose +Y, starboard +X, dorsal +Z. The four abort motors are the drive plume.
|
||||
##
|
||||
## This script does not move the hull. escape_capsule.gd owns velocity.
|
||||
## listen_keys is off so walking the player does not light jets or shove the craft.
|
||||
## Capsule sets abort_burn, att_pitch/yaw/roll, rcs_direction, hatches, cabin_dim.
|
||||
|
||||
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
|
||||
@export_range(0.0, 1.0) var hatch_crew_open := 0.0 : set = set_hatch_crew_open
|
||||
@export_range(0.0, 1.0) var hatch_ladder_deploy := 0.0 : set = set_hatch_ladder_deploy
|
||||
@export_range(0.0, 1.0) var hatch_dock_open := 0.0 : set = set_hatch_dock_open
|
||||
@export_range(0.0, 1.0) var instruments_demo := 0.4 : set = set_instruments_demo
|
||||
@export var listen_keys := false
|
||||
@export_range(0.0, 1.0) var gear_deploy := 1.0
|
||||
@export_range(0.0, 1.0) var hatch_crew_open := 0.0
|
||||
@export_range(0.0, 1.0) var hatch_ladder_deploy := 0.0
|
||||
@export_range(0.0, 1.0) var hatch_dock_open := 0.0
|
||||
@export_range(0.0, 1.0) var instruments_demo := 0.4
|
||||
@export var cabin_lights := true
|
||||
@export var cabin_dim := false
|
||||
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
|
||||
@export var att_pitch := 0.0
|
||||
@export var att_yaw := 0.0
|
||||
@export var att_roll := 0.0
|
||||
@export var abort_burn := false
|
||||
@export var cabin_dim := false : set = set_cabin_dim
|
||||
@export var clip_speed := 1.35
|
||||
## Tick this in the inspector (or Remote tab while running) to self-test every jet.
|
||||
@export var debug_fire := false
|
||||
|
||||
var _rcs: Array[GPUParticles3D] = []
|
||||
var _abort: Array[GPUParticles3D] = []
|
||||
const _RIBBON := "shader_type spatial;
|
||||
render_mode blend_add, unshaded, cull_disabled, depth_draw_never, fog_disabled;
|
||||
uniform vec4 root_color : source_color = vec4(1.0, 1.0, 1.0, 0.95);
|
||||
uniform vec4 mid_color : source_color = vec4(0.47, 0.71, 1.0, 0.35);
|
||||
uniform vec4 tip_color : source_color = vec4(0.16, 0.31, 1.0, 0.0);
|
||||
uniform float opacity : hint_range(0.0, 2.0, 0.001) = 0.0;
|
||||
void fragment() {
|
||||
float v = UV.y;
|
||||
float edge = abs(UV.x - 0.5) * 2.0;
|
||||
float side = 1.0 - smoothstep(0.36, 1.0, edge);
|
||||
vec3 col = mix(root_color.rgb, mid_color.rgb, smoothstep(0.0, 0.45, v));
|
||||
col = mix(col, tip_color.rgb, smoothstep(0.35, 1.0, v));
|
||||
float a = mix(root_color.a, mid_color.a, smoothstep(0.05, 0.45, v));
|
||||
a = mix(a, tip_color.a, smoothstep(0.45, 1.0, v));
|
||||
ALBEDO = col;
|
||||
ALPHA = clamp(a * side * opacity, 0.0, 1.0);
|
||||
}"
|
||||
|
||||
var _player: AnimationPlayer
|
||||
var _shader: Shader
|
||||
var _rcs: Array[Node3D] = []
|
||||
var _aborts: Array[Dictionary] = []
|
||||
var _abort_hold := 0.0
|
||||
var _time := 0.0
|
||||
var _prev := {}
|
||||
var _clip_cache := {}
|
||||
var _warm_frames := 12
|
||||
var _cabin_ready := false
|
||||
var _cabin_lights: Array[Light3D] = []
|
||||
var _applied_dim := false
|
||||
var _applied_lights := true
|
||||
var _cabin_lights_n: Array[Light3D] = []
|
||||
var _cabin_rest: Array[Dictionary] = []
|
||||
var _cabin_mats: Array[Dictionary] = []
|
||||
var _clip_pos: Dictionary = {}
|
||||
var _clip_tgt: Dictionary = {}
|
||||
|
||||
const RCS_THRUST := [
|
||||
Vector3.ZERO,
|
||||
Vector3(0, 1, 0),
|
||||
Vector3(0, -1, 0),
|
||||
Vector3(1, 0, 0),
|
||||
Vector3(-1, 0, 0),
|
||||
Vector3(0, 0, 1),
|
||||
Vector3(0, 0, -1),
|
||||
const _DIRS: Array[Vector3] = [
|
||||
Vector3.ZERO, Vector3(0, 1, 0), Vector3(0, -1, 0),
|
||||
Vector3(1, 0, 0), Vector3(-1, 0, 0), Vector3(0, 0, 1), Vector3(0, 0, -1),
|
||||
]
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_clip_tgt = {
|
||||
"gear_deploy": gear_deploy,
|
||||
"hatch_crew_open": hatch_crew_open,
|
||||
"hatch_ladder_deploy": hatch_ladder_deploy,
|
||||
"hatch_dock_open": hatch_dock_open,
|
||||
"instruments_demo": instruments_demo,
|
||||
}
|
||||
_clip_pos = _clip_tgt.duplicate()
|
||||
for clip in _clip_tgt.keys():
|
||||
_apply_clip(clip, float(_clip_pos[clip]))
|
||||
_arm_all("Vfx_RCS_*", 18, 0.09, 14.0, 22.0, 2.2, Color(0.82, 0.93, 1.0), 1.4, _rcs, false)
|
||||
_arm_all("Vfx_Abort_*", 36, 0.16, 28.0, 46.0, 1.6, Color(1.0, 0.58, 0.22), 5.0, _abort, true)
|
||||
_shader = Shader.new()
|
||||
_shader.code = _RIBBON
|
||||
_player = get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if _player == null:
|
||||
_player = find_child("AnimationPlayer", true, false) as AnimationPlayer
|
||||
_arm()
|
||||
_collect_cabin()
|
||||
set_process(true)
|
||||
print("Kestrel driver ready rcs=", _rcs.size(), " abort=", _abort.size())
|
||||
_apply_clips()
|
||||
print("Kestrel driver ready: rcs=", _rcs.size(), " abort=", _aborts.size(),
|
||||
" cabin_lights=", _cabin_lights_n.size(), " strips=", _cabin_mats.size(),
|
||||
" anim_player=", _player != null)
|
||||
if _rcs.is_empty():
|
||||
push_warning("Kestrel driver: no Vfx_RCS_* nodes found under " + str(get_path()))
|
||||
if _aborts.is_empty():
|
||||
push_warning("Kestrel driver: no Vfx_Abort_* nodes found under " + str(get_path()))
|
||||
if _player == null:
|
||||
push_warning("Kestrel driver: no AnimationPlayer found (gear/hatches will not move)")
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_tick_clips(delta)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var cmd := Vector3(att_pitch, att_roll, -att_yaw)
|
||||
var xlat: Vector3 = RCS_THRUST[clampi(rcs_direction, 0, RCS_THRUST.size() - 1)]
|
||||
var origin := global_position
|
||||
var ship := global_transform.basis
|
||||
var turning := cmd.length() > 0.15
|
||||
for p in _rcs:
|
||||
if not is_instance_valid(p):
|
||||
continue
|
||||
var exhaust: Vector3 = (ship.inverse() * p.global_transform.basis.y).normalized()
|
||||
var fire := false
|
||||
if turning:
|
||||
var local_pos: Vector3 = ship.inverse() * (p.global_position - origin)
|
||||
if local_pos.length() < 0.05:
|
||||
local_pos = -exhaust
|
||||
var torque: Vector3 = local_pos.cross(-exhaust)
|
||||
fire = torque.dot(cmd) > 0.12
|
||||
if not fire and xlat != Vector3.ZERO:
|
||||
fire = exhaust.dot(-xlat) > 0.55
|
||||
p.emitting = fire
|
||||
for p in _abort:
|
||||
if is_instance_valid(p):
|
||||
p.emitting = abort_burn
|
||||
|
||||
|
||||
func set_cabin_dim(v: bool) -> void:
|
||||
cabin_dim = v
|
||||
if _cabin_ready:
|
||||
func _physics_process(delta: float) -> void:
|
||||
_time += delta
|
||||
if listen_keys:
|
||||
_read_keys()
|
||||
if debug_fire:
|
||||
abort_burn = true
|
||||
rcs_direction = 1 + int(_time) % 6
|
||||
_apply_clips()
|
||||
if _warm_frames > 0:
|
||||
_warm_frames -= 1
|
||||
# FIX: the capsule sets cabin_dim as a plain variable, so nothing re-applied it.
|
||||
if cabin_dim != _applied_dim or cabin_lights != _applied_lights:
|
||||
_apply_cabin()
|
||||
_burn(_wish(), delta)
|
||||
|
||||
|
||||
func set_gear_deploy(v: float) -> void:
|
||||
gear_deploy = v
|
||||
_clip_tgt["gear_deploy"] = clampf(v, 0.0, 1.0)
|
||||
func _read_keys() -> void:
|
||||
if _pressed(KEY_G):
|
||||
gear_deploy = 0.0 if gear_deploy > 0.5 else 1.0
|
||||
if _pressed(KEY_C):
|
||||
hatch_crew_open = 0.0 if hatch_crew_open > 0.5 else 1.0
|
||||
if _pressed(KEY_L):
|
||||
hatch_ladder_deploy = 0.0 if hatch_ladder_deploy > 0.5 else 1.0
|
||||
if _pressed(KEY_H):
|
||||
hatch_dock_open = 0.0 if hatch_dock_open > 0.5 else 1.0
|
||||
if _pressed(KEY_I):
|
||||
instruments_demo = 0.15 if instruments_demo > 0.5 else 1.0
|
||||
if _pressed(KEY_K):
|
||||
cabin_lights = not cabin_lights
|
||||
abort_burn = Input.is_physical_key_pressed(KEY_SPACE)
|
||||
|
||||
|
||||
func set_hatch_crew_open(v: float) -> void:
|
||||
hatch_crew_open = v
|
||||
_clip_tgt["hatch_crew_open"] = clampf(v, 0.0, 1.0)
|
||||
func _pressed(key: Key) -> bool:
|
||||
var down := Input.is_physical_key_pressed(key)
|
||||
var was: bool = _prev.get(key, false)
|
||||
_prev[key] = down
|
||||
return down and not was
|
||||
|
||||
|
||||
func set_hatch_ladder_deploy(v: float) -> void:
|
||||
hatch_ladder_deploy = v
|
||||
_clip_tgt["hatch_ladder_deploy"] = clampf(v, 0.0, 1.0)
|
||||
func _wish() -> Vector3:
|
||||
var local := _DIRS[clampi(rcs_direction, 0, _DIRS.size() - 1)]
|
||||
if listen_keys:
|
||||
var pad := Vector3.ZERO
|
||||
if Input.is_physical_key_pressed(KEY_W):
|
||||
pad.y += 1.0
|
||||
if Input.is_physical_key_pressed(KEY_S):
|
||||
pad.y -= 1.0
|
||||
if Input.is_physical_key_pressed(KEY_D):
|
||||
pad.x += 1.0
|
||||
if Input.is_physical_key_pressed(KEY_A):
|
||||
pad.x -= 1.0
|
||||
if Input.is_physical_key_pressed(KEY_E):
|
||||
pad.z += 1.0
|
||||
if Input.is_physical_key_pressed(KEY_Q):
|
||||
pad.z -= 1.0
|
||||
if pad.length_squared() > 0.0:
|
||||
local = pad.normalized()
|
||||
if local == Vector3.ZERO:
|
||||
return Vector3.ZERO
|
||||
return (global_transform.basis * local).normalized()
|
||||
|
||||
|
||||
func set_hatch_dock_open(v: float) -> void:
|
||||
hatch_dock_open = v
|
||||
_clip_tgt["hatch_dock_open"] = clampf(v, 0.0, 1.0)
|
||||
func _apply_clips() -> void:
|
||||
_seek("gear_deploy", gear_deploy)
|
||||
_seek("hatch_crew_open", hatch_crew_open)
|
||||
_seek("hatch_ladder_deploy", hatch_ladder_deploy)
|
||||
_seek("hatch_dock_open", hatch_dock_open)
|
||||
_seek("instruments_demo", instruments_demo)
|
||||
|
||||
|
||||
func set_instruments_demo(v: float) -> void:
|
||||
instruments_demo = v
|
||||
_clip_tgt["instruments_demo"] = clampf(v, 0.0, 1.0)
|
||||
|
||||
|
||||
func _tick_clips(delta: float) -> void:
|
||||
for clip in _clip_tgt.keys():
|
||||
var tgt := float(_clip_tgt[clip])
|
||||
var pos := float(_clip_pos.get(clip, tgt))
|
||||
if absf(pos - tgt) < 0.001:
|
||||
if pos != tgt:
|
||||
_clip_pos[clip] = tgt
|
||||
_apply_clip(clip, tgt)
|
||||
continue
|
||||
pos = move_toward(pos, tgt, clip_speed * delta)
|
||||
_clip_pos[clip] = pos
|
||||
_apply_clip(clip, pos)
|
||||
|
||||
|
||||
func _apply_clip(clip: StringName, t: float) -> void:
|
||||
var ap := _ap()
|
||||
if ap == null or not ap.has_animation(clip):
|
||||
return
|
||||
var len := ap.get_animation(clip).length
|
||||
ap.play(clip)
|
||||
ap.seek(clampf(t, 0.0, 1.0) * len, true)
|
||||
ap.pause()
|
||||
|
||||
|
||||
func _ap() -> AnimationPlayer:
|
||||
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if ap == null:
|
||||
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
|
||||
return ap
|
||||
## FIX: only re-seek when the value actually changed. The old version called
|
||||
## play()+seek()+pause() on five clips every physics frame.
|
||||
func _clip_name(clip: StringName) -> StringName:
|
||||
if _player.has_animation(clip):
|
||||
return clip
|
||||
for a in _player.get_animation_list():
|
||||
if String(a).ends_with("/" + String(clip)):
|
||||
return a
|
||||
return &""
|
||||
|
||||
|
||||
## Re-seeks only when the value changed, but keeps re-applying for the first few
|
||||
## frames so the pose sticks after the AnimationPlayer finishes initializing.
|
||||
func _seek(clip: StringName, t: float) -> void:
|
||||
_apply_clip(clip, t)
|
||||
if _player == null:
|
||||
return
|
||||
var clip_id := _clip_name(clip)
|
||||
if clip_id == &"":
|
||||
return
|
||||
t = clampf(t, 0.0, 1.0)
|
||||
if _warm_frames <= 0 and _clip_cache.has(clip) and is_equal_approx(float(_clip_cache[clip]), t):
|
||||
return
|
||||
_clip_cache[clip] = t
|
||||
var clip_len := _player.get_animation(clip_id).length
|
||||
_player.play(clip_id)
|
||||
_player.seek(t * clip_len, true)
|
||||
_player.pause()
|
||||
|
||||
|
||||
func _collect_cabin() -> void:
|
||||
_cabin_lights.clear()
|
||||
_cabin_lights_n.clear()
|
||||
_cabin_rest.clear()
|
||||
_cabin_mats.clear()
|
||||
for n in find_children("*", "Light3D", true, false):
|
||||
var nm := String(n.name)
|
||||
if nm.begins_with("Nav"):
|
||||
continue
|
||||
if not (nm.begins_with("PointLight_") or nm == "CabinFill" or nm == "PanelLight" or nm.begins_with("CabinLamp")):
|
||||
var keep := nm.begins_with("PointLight_") or nm == "CabinFill" or nm == "PanelLight" or nm.begins_with("CabinLamp")
|
||||
if not keep:
|
||||
var p := n.get_parent()
|
||||
var cabin := false
|
||||
while p:
|
||||
if String(p.name) == "CabinLights":
|
||||
cabin = true
|
||||
keep = true
|
||||
break
|
||||
p = p.get_parent()
|
||||
if not cabin:
|
||||
continue
|
||||
_cabin_lights.append(n)
|
||||
if not keep:
|
||||
continue
|
||||
_cabin_lights_n.append(n)
|
||||
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
|
||||
for n in find_children("*", "MeshInstance3D", true, false):
|
||||
if not String(n.name).begins_with("LightStrip"):
|
||||
@@ -180,100 +211,334 @@ func _collect_cabin() -> void:
|
||||
continue
|
||||
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
|
||||
mesh.set_surface_override_material(0, copy)
|
||||
_cabin_mats.append({
|
||||
"mat": copy,
|
||||
"energy": copy.emission_energy_multiplier,
|
||||
"color": copy.emission,
|
||||
"on": copy.emission_enabled
|
||||
})
|
||||
_cabin_mats.append({"mat": copy, "energy": copy.emission_energy_multiplier, "color": copy.emission, "on": copy.emission_enabled})
|
||||
_cabin_ready = true
|
||||
_apply_cabin()
|
||||
|
||||
|
||||
func _apply_cabin() -> void:
|
||||
for i in _cabin_lights.size():
|
||||
var lamp := _cabin_lights[i]
|
||||
if not _cabin_ready:
|
||||
return
|
||||
_applied_dim = cabin_dim
|
||||
_applied_lights = cabin_lights
|
||||
var dim := cabin_dim or not cabin_lights
|
||||
for i in _cabin_lights_n.size():
|
||||
var lamp := _cabin_lights_n[i]
|
||||
var rest: Dictionary = _cabin_rest[i]
|
||||
lamp.light_color = rest.color
|
||||
lamp.light_energy = float(rest.energy) * (0.16 if cabin_dim else 1.0)
|
||||
lamp.light_energy = float(rest.energy) * (0.08 if dim else 1.0)
|
||||
for row in _cabin_mats:
|
||||
var mat := row.mat as StandardMaterial3D
|
||||
mat.emission_enabled = true if cabin_dim else bool(row.on)
|
||||
mat.emission_enabled = true if dim else bool(row.on)
|
||||
mat.emission = row.color
|
||||
mat.emission_energy_multiplier = float(row.energy) * (0.18 if cabin_dim else 1.0)
|
||||
mat.emission_energy_multiplier = float(row.energy) * (0.05 if dim else 1.0)
|
||||
|
||||
|
||||
func _arm_all(pattern: String, amount: int, life: float, vmin: float, vmax: float, spread: float, color: Color, reach: float, into: Array[GPUParticles3D], streak: bool) -> void:
|
||||
for n in find_children(pattern, "Node3D", true, false):
|
||||
if String(n.name) == "Plume":
|
||||
continue
|
||||
if n is GPUParticles3D:
|
||||
into.append(n as GPUParticles3D)
|
||||
continue
|
||||
var authored := _find_authored_plume(n)
|
||||
if authored:
|
||||
into.append(authored)
|
||||
continue
|
||||
var p := _make_plume(n, amount, life, vmin, vmax, spread, color, reach, streak)
|
||||
if p:
|
||||
into.append(p)
|
||||
func _arm() -> void:
|
||||
var i := 0
|
||||
for n in find_children("*", "Node3D", true, false):
|
||||
var nm := String(n.name)
|
||||
if nm.begins_with("Vfx_RCS_"):
|
||||
_rcs.append(_spear(n, 1.2, 0.09, Color(0.863, 0.937, 1.0, 0.95), Color(0.41, 0.67, 1.0, 0.35), Color(0.25, 0.45, 0.8, 0.0), 0.82, 2, float(i)))
|
||||
i += 1
|
||||
elif nm.begins_with("Vfx_Abort_"):
|
||||
_aborts.append(_make_abort(n))
|
||||
|
||||
|
||||
func _find_authored_plume(socket: Node3D) -> GPUParticles3D:
|
||||
if socket is GPUParticles3D:
|
||||
return socket as GPUParticles3D
|
||||
for child in socket.get_children():
|
||||
if child is GPUParticles3D and String(child.name) != "Plume":
|
||||
return child as GPUParticles3D
|
||||
var ours := socket.get_node_or_null("Plume")
|
||||
if ours is GPUParticles3D:
|
||||
return ours as GPUParticles3D
|
||||
return null
|
||||
func _burn(wish: Vector3, delta: float) -> void:
|
||||
var ship := global_transform.basis
|
||||
var ship_inv := ship.inverse()
|
||||
# FIX: capsule yaws about +Z by +att_yaw (rotate_object_local(FORWARD, -yaw) == +Z, +yaw),
|
||||
# so the commanded angular velocity is (pitch, roll, +yaw). The old -att_yaw fired the
|
||||
# opposite-side jets for yaw.
|
||||
var cmd := Vector3(att_pitch, att_roll, att_yaw)
|
||||
var turning := cmd.length() > 0.15
|
||||
var origin := global_position
|
||||
for ribbon in _rcs:
|
||||
var want := 0.0
|
||||
var exhaust: Vector3 = (ship_inv * ribbon.global_transform.basis.y).normalized()
|
||||
if turning:
|
||||
var local_pos: Vector3 = ship_inv * (ribbon.global_position - origin)
|
||||
if local_pos.length() < 0.05:
|
||||
local_pos = -exhaust
|
||||
var torque: Vector3 = local_pos.cross(-exhaust)
|
||||
if torque.dot(cmd) > 0.12:
|
||||
want = 1.0
|
||||
if want < 0.5 and wish != Vector3.ZERO:
|
||||
if exhaust.dot(-(ship_inv * wish)) > 0.7:
|
||||
want = 1.0
|
||||
var burn := float(ribbon.get_meta("burn"))
|
||||
burn = _smooth(burn, want, delta, 0.08 if want > burn else 0.14)
|
||||
ribbon.set_meta("burn", burn)
|
||||
_set_spear(ribbon, burn, _time)
|
||||
if abort_burn:
|
||||
_abort_hold = minf(1.0, _abort_hold + delta / 0.22)
|
||||
else:
|
||||
_abort_hold = maxf(0.0, _abort_hold - delta / 0.28)
|
||||
var on := _abort_hold > 0.03
|
||||
var flick := 0.86 + 0.14 * sin(_time * 41.0)
|
||||
var sheath_scale := 0.9 + 0.1 * sin(_time * 19.0)
|
||||
for a in _aborts:
|
||||
var core := a.core as Node3D
|
||||
var sheath := a.sheath as Node3D
|
||||
_set_opacity(core, _abort_hold * 0.95 * flick if on else 0.0)
|
||||
_set_opacity(sheath, _abort_hold * 0.82 * flick if on else 0.0)
|
||||
sheath.scale = Vector3(1, sheath_scale, 1)
|
||||
var sparks := a.sparks as GPUParticles3D
|
||||
var sheath_sparks := a.sheath_sparks as GPUParticles3D
|
||||
sparks.emitting = on
|
||||
sheath_sparks.emitting = on
|
||||
var proc := sparks.process_material as ParticleProcessMaterial
|
||||
var proc2 := sheath_sparks.process_material as ParticleProcessMaterial
|
||||
if proc:
|
||||
proc.color = Color(1, 1, 1, _abort_hold * 0.55)
|
||||
if proc2:
|
||||
proc2.color = Color(1, 1, 1, _abort_hold * 0.55)
|
||||
var flash := a.flash as MeshInstance3D
|
||||
flash.visible = on
|
||||
var fmat := flash.material_override as StandardMaterial3D
|
||||
if fmat:
|
||||
var tint := fmat.albedo_color
|
||||
tint.a = _abort_hold * 0.45
|
||||
fmat.albedo_color = tint
|
||||
flash.scale = Vector3.ONE * (0.06 + _abort_hold * 0.04)
|
||||
var light := a.light as SpotLight3D
|
||||
light.light_energy = _abort_hold * 8.0 if on else 0.0
|
||||
|
||||
|
||||
func _make_plume(socket: Node3D, amount: int, life: float, vmin: float, vmax: float, spread: float, color: Color, reach: float, streak: bool) -> GPUParticles3D:
|
||||
var existing := socket.get_node_or_null("Plume")
|
||||
if existing is GPUParticles3D:
|
||||
return existing as GPUParticles3D
|
||||
if existing:
|
||||
existing.free()
|
||||
var p := GPUParticles3D.new()
|
||||
p.name = "Plume"
|
||||
p.amount = amount
|
||||
p.lifetime = life
|
||||
p.local_coords = true
|
||||
p.visibility_aabb = AABB(Vector3(-reach, 0, -reach), Vector3(reach * 2.0, reach * 4.0, reach * 2.0))
|
||||
var proc := ParticleProcessMaterial.new()
|
||||
proc.direction = Vector3(0, 1, 0)
|
||||
proc.spread = spread
|
||||
proc.initial_velocity_min = vmin
|
||||
proc.initial_velocity_max = vmax
|
||||
proc.gravity = Vector3.ZERO
|
||||
proc.scale_min = 0.55 if streak else 0.04
|
||||
proc.scale_max = 0.95 if streak else 0.09
|
||||
proc.color = color
|
||||
if streak:
|
||||
proc.particle_flag_align_y = true
|
||||
p.process_material = proc
|
||||
## Exhaust direction in the socket's parent space. The model puts each Vfx_ socket's
|
||||
## translation along its exhaust direction (RCS = +Y, Abort = -Y), so position is the aim.
|
||||
func _exhaust(socket: Node3D) -> Vector3:
|
||||
var dir := socket.position
|
||||
if dir.length() < 0.001:
|
||||
dir = Vector3.UP
|
||||
return (socket.quaternion.inverse() * dir).normalized()
|
||||
|
||||
|
||||
func _basis_y(dir: Vector3) -> Basis:
|
||||
var y := dir.normalized()
|
||||
if y.length() < 0.001:
|
||||
y = Vector3.UP
|
||||
var up := Vector3.FORWARD if absf(y.dot(Vector3.UP)) > 0.9 else Vector3.UP
|
||||
var x := up.cross(y).normalized()
|
||||
var z := x.cross(y).normalized()
|
||||
return Basis(x, y, z)
|
||||
|
||||
|
||||
func _ribbon_mat(root: Color, mid: Color, tip: Color) -> ShaderMaterial:
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = _shader
|
||||
mat.set_shader_parameter("root_color", root)
|
||||
mat.set_shader_parameter("mid_color", mid)
|
||||
mat.set_shader_parameter("tip_color", tip)
|
||||
mat.set_shader_parameter("opacity", 0.0)
|
||||
return mat
|
||||
|
||||
|
||||
func _sheet(length: float, width: float) -> ArrayMesh:
|
||||
var half := width * 0.5
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st.set_uv(Vector2(0, 0)); st.add_vertex(Vector3(-half, 0, 0))
|
||||
st.set_uv(Vector2(1, 0)); st.add_vertex(Vector3(half, 0, 0))
|
||||
st.set_uv(Vector2(1, 1)); st.add_vertex(Vector3(half, length, 0))
|
||||
st.set_uv(Vector2(0, 1)); st.add_vertex(Vector3(-half, length, 0))
|
||||
st.add_index(0); st.add_index(1); st.add_index(2)
|
||||
st.add_index(0); st.add_index(2); st.add_index(3)
|
||||
return st.commit()
|
||||
|
||||
|
||||
func _taper(length: float, w0: float, w1: float) -> ArrayMesh:
|
||||
var rows := 11
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
for i in rows:
|
||||
var t := float(i) / float(rows - 1)
|
||||
var y := t * length
|
||||
var w := (w0 + (w1 - w0) * pow(t, 0.7)) * 0.5
|
||||
st.set_uv(Vector2(0, t)); st.add_vertex(Vector3(-w, y, 0))
|
||||
st.set_uv(Vector2(1, t)); st.add_vertex(Vector3(w, y, 0))
|
||||
for i in rows - 1:
|
||||
var a := i * 2
|
||||
st.add_index(a); st.add_index(a + 1); st.add_index(a + 2)
|
||||
st.add_index(a + 1); st.add_index(a + 3); st.add_index(a + 2)
|
||||
return st.commit()
|
||||
|
||||
|
||||
func _crossed(socket: Node3D, mesh: ArrayMesh, mat: ShaderMaterial, planes: int, node_name: String, length: float, opacity: float, flick_seed: float) -> Node3D:
|
||||
var g := Node3D.new()
|
||||
g.name = node_name
|
||||
g.set_meta("mat", mat)
|
||||
g.set_meta("len", length)
|
||||
g.set_meta("opacity", opacity)
|
||||
g.set_meta("seed", flick_seed)
|
||||
g.set_meta("burn", 0.0)
|
||||
for i in planes:
|
||||
var m := MeshInstance3D.new()
|
||||
m.mesh = mesh
|
||||
m.material_override = mat
|
||||
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
m.extra_cull_margin = 8.0
|
||||
m.rotation.y = (float(i) / float(planes)) * PI
|
||||
g.add_child(m)
|
||||
g.basis = _basis_y(_exhaust(socket))
|
||||
g.visible = false
|
||||
socket.add_child(g)
|
||||
return g
|
||||
|
||||
|
||||
func _spear(socket: Node3D, length: float, width: float, root: Color, mid: Color, tip: Color, opacity: float, planes: int, flick_seed: float) -> Node3D:
|
||||
return _crossed(socket, _sheet(length, width), _ribbon_mat(root, mid, tip), planes, "Spear", length, opacity, flick_seed)
|
||||
|
||||
|
||||
func _taper_ribbon(socket: Node3D, length: float, w0: float, w1: float, root: Color, mid: Color, tip: Color, opacity: float, planes: int, node_name: String) -> Node3D:
|
||||
return _crossed(socket, _taper(length, w0, w1), _ribbon_mat(root, mid, tip), planes, node_name, length, opacity, 1.0)
|
||||
|
||||
|
||||
func _set_opacity(ribbon: Node3D, opacity: float) -> void:
|
||||
var mat := ribbon.get_meta("mat") as ShaderMaterial
|
||||
var shown := opacity > 0.02
|
||||
ribbon.visible = shown
|
||||
mat.set_shader_parameter("opacity", opacity if shown else 0.0)
|
||||
|
||||
|
||||
func _set_spear(ribbon: Node3D, amount: float, time: float) -> void:
|
||||
var a := maxf(amount, 0.0)
|
||||
if a < 0.02:
|
||||
_set_opacity(ribbon, 0.0)
|
||||
return
|
||||
var flick_seed := float(ribbon.get_meta("seed"))
|
||||
var flick := 0.82 + 0.18 * sin(time * 37.0 + flick_seed * 2.1)
|
||||
ribbon.scale = Vector3.ONE
|
||||
_set_opacity(ribbon, float(ribbon.get_meta("opacity")) * minf(a, 1.0) * flick)
|
||||
|
||||
|
||||
func _smooth(current: float, target: float, delta: float, seconds: float) -> float:
|
||||
var k := 1.0 - exp(-delta / maxf(0.05, seconds))
|
||||
return current + (target - current) * k
|
||||
|
||||
|
||||
func _soft_disc() -> Texture2D:
|
||||
var img := Image.create(64, 64, false, Image.FORMAT_RGBA8)
|
||||
for y in 64:
|
||||
for x in 64:
|
||||
var d := Vector2(x - 31.5, y - 31.5).length() / 32.0
|
||||
var t := clampf(d, 0.0, 1.0)
|
||||
var s := t * t * (3.0 - 2.0 * t)
|
||||
var a := (1.0 - s) * (1.0 - s)
|
||||
img.set_pixel(x, y, Color(1, 1, 1, a))
|
||||
return ImageTexture.create_from_image(img)
|
||||
|
||||
|
||||
func _disc_mat() -> StandardMaterial3D:
|
||||
var draw := StandardMaterial3D.new()
|
||||
draw.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
draw.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
draw.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
draw.billboard_mode = BaseMaterial3D.BILLBOARD_DISABLED if streak else BaseMaterial3D.BILLBOARD_PARTICLES
|
||||
draw.albedo_color = color
|
||||
p.material_override = draw
|
||||
draw.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
||||
draw.vertex_color_use_as_albedo = true
|
||||
draw.albedo_texture = _soft_disc()
|
||||
draw.albedo_color = Color.WHITE
|
||||
draw.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
draw.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
return draw
|
||||
|
||||
|
||||
func _abort_ramp() -> GradientTexture1D:
|
||||
var g := Gradient.new()
|
||||
g.offsets = PackedFloat32Array([0.0, 0.15, 0.45, 1.0])
|
||||
g.colors = PackedColorArray([
|
||||
Color(1.0, 0.97, 0.92, 1.0),
|
||||
Color(1.0, 0.46, 0.12, 0.9),
|
||||
Color(0.55, 0.16, 0.05, 0.45),
|
||||
Color(0.25, 0.05, 0.01, 0.0),
|
||||
])
|
||||
var tex := GradientTexture1D.new()
|
||||
tex.gradient = g
|
||||
return tex
|
||||
|
||||
|
||||
func _sparks(socket: Node3D, node_name: String, amount: int, life: float, vmin: float, vmax: float, spread_deg: float, size: float, reach: float) -> GPUParticles3D:
|
||||
var p := GPUParticles3D.new()
|
||||
p.name = node_name
|
||||
p.amount = amount
|
||||
p.lifetime = life
|
||||
p.explosiveness = 0.0
|
||||
p.local_coords = true
|
||||
p.visibility_aabb = AABB(Vector3(-reach, 0, -reach), Vector3(reach * 2.0, reach * 3.0, reach * 2.0))
|
||||
var proc := ParticleProcessMaterial.new()
|
||||
proc.direction = Vector3(0, 1, 0)
|
||||
proc.spread = spread_deg
|
||||
proc.initial_velocity_min = vmin
|
||||
proc.initial_velocity_max = vmax
|
||||
proc.gravity = Vector3.ZERO
|
||||
proc.scale_min = 0.85
|
||||
proc.scale_max = 1.15
|
||||
proc.color = Color(1, 1, 1, 0.55)
|
||||
proc.color_ramp = _abort_ramp()
|
||||
p.process_material = proc
|
||||
p.material_override = _disc_mat()
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(0.07, 0.55) if streak else Vector2(0.035, 0.035)
|
||||
quad.size = Vector2(size, size)
|
||||
p.draw_pass_1 = quad
|
||||
p.emitting = false
|
||||
var dir := socket.position
|
||||
if dir.length() < 0.001:
|
||||
dir = Vector3.UP
|
||||
dir = (socket.quaternion.inverse() * dir).normalized()
|
||||
var y := dir
|
||||
var up := Vector3.FORWARD if absf(y.dot(Vector3.UP)) > 0.9 else Vector3.UP
|
||||
var x := up.cross(y).normalized()
|
||||
var z := x.cross(y).normalized()
|
||||
p.basis = Basis(x, y, z)
|
||||
p.basis = _basis_y(_exhaust(socket))
|
||||
socket.add_child(p)
|
||||
return p
|
||||
|
||||
|
||||
func _abort_flash(socket: Node3D) -> MeshInstance3D:
|
||||
var aim := _basis_y(_exhaust(socket))
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.albedo_texture = _soft_disc()
|
||||
mat.albedo_color = Color(1.0, 0.945, 0.867, 0.0)
|
||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
var m := MeshInstance3D.new()
|
||||
m.name = "AbortFlash"
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(1, 1)
|
||||
m.mesh = quad
|
||||
m.material_override = mat
|
||||
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
m.position = aim.y * 0.02
|
||||
m.scale = Vector3.ONE * 0.08
|
||||
m.visible = false
|
||||
socket.add_child(m)
|
||||
return m
|
||||
|
||||
|
||||
func _spot(socket: Node3D) -> SpotLight3D:
|
||||
var aim := _basis_y(_exhaust(socket))
|
||||
var light := SpotLight3D.new()
|
||||
light.name = "ExhaustLight"
|
||||
light.light_color = Color(1.0, 0.416, 0.157)
|
||||
light.light_energy = 0.0
|
||||
light.spot_range = 6.5
|
||||
light.spot_angle = rad_to_deg(0.5)
|
||||
light.spot_attenuation = 1.8
|
||||
light.shadow_enabled = false
|
||||
light.position = aim.y * 0.08
|
||||
var z := -(aim.y)
|
||||
var up := aim.x
|
||||
var x := up.cross(z).normalized()
|
||||
var y := z.cross(x).normalized()
|
||||
light.basis = Basis(x, y, z)
|
||||
socket.add_child(light)
|
||||
return light
|
||||
|
||||
|
||||
func _make_abort(socket: Node3D) -> Dictionary:
|
||||
var core := _taper_ribbon(socket, 2.1, 0.06, 0.20, Color(1.0, 0.97, 0.92, 1.0), Color(1.0, 0.45, 0.12, 0.72), Color(0.40, 0.08, 0.02, 0.0), 0.95, 3, "AbortCore")
|
||||
var sheath := _taper_ribbon(socket, 5.2, 0.10, 0.78, Color(1.0, 0.55, 0.18, 1.0), Color(1.0, 0.28, 0.06, 0.55), Color(0.35, 0.06, 0.01, 0.0), 0.82, 3, "AbortSheath")
|
||||
return {
|
||||
"core": core,
|
||||
"sheath": sheath,
|
||||
"sparks": _sparks(socket, "AbortCoreSparks", 10, 0.15, 18.0, 28.0, 1.6, 0.05, 6.0),
|
||||
"sheath_sparks": _sparks(socket, "AbortSheathSparks", 18, 0.26, 14.0, 22.0, 4.5, 0.09, 8.0),
|
||||
"flash": _abort_flash(socket),
|
||||
"light": _spot(socket),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user