changelog

arcology updated
ship models in
particle fx is broken on kestrel
This commit is contained in:
scorpion21211-cmd
2026-10-04 19:37:48 -04:00
parent 965f0d7584
commit 8efae1a0d0
606 changed files with 27599 additions and 10914 deletions
+37
View File
@@ -0,0 +1,37 @@
# Brachistochrone — NAF ship assets
Rebuilt 2026-09-20 in App Builder from the Expert-mode handoff. Procedural Three.js is the source of truth (`src/ship/*`, `src/capsule/buildCapsule.ts`). Do not hand-edit GLBs.
## Frame
- 1 glTF unit = 1 meter, Y-up
- **Kestrel EC-01:** heat shield at Y≈0, nose +Y, crew backs on the shield, gaze +Y, visors +Z
- **Resolute CVT-07:** +Y = nose / thrust up, decks = XZ stacked on Y, +X starboard, +Z dorsal
- Faction: North American Federation. Charcoal hull `#2a3038`, liner `#6a727c`
- No keel gun. No planet in the hangar preview. Default view is solid exterior.
## Resolute
- Length ~46 m, beam ~13 m, height ~11.4 m
- Superellipse loft, `SEGS=8`, `SUPER_EXP=0.22`
- `WALL=0.58 m`, floor extra inset `0.30 m`
- 11 decks, pitch 2.65 m, room height 2.42 m
- Companionway: well 0.95 × 3.35 m, rise 0.221 m, run 0.255 m, 12 treads, trunk walls, switchback per deck
- Cabins on decks 03/04 against port/starboard inner hull; corridor + stair in the center
- Dorsal airlock on storage deck 07, walk-through vestibule + round hatches
## Kestrel
- ~4.55 m capsule, four Gemini L-couches
- Cabin lights ~0.28 intensity (dim)
- Glass opacity 0.20, roughness 0.07, metalness 0, hull holes behind panes
- Sitting bulkhead at z≈−0.36, stow lids in a line
- Docking probe stays in the tunnel
## Clips (seek 0..1)
Kestrel: `gear_deploy` `hatch_crew_open` `hatch_ladder_deploy` `hatch_dock_open` `instruments_demo`
Resolute: `gear_deploy` `radiators_deploy` `hatch_airlock_open` `hatch_dock_open` `hatch_cargo_open` `torpedo_doors_open`
Named cameras: `SeatCamera` (Kestrel), `HelmCamera` + `PlayerSpawn` (Resolute)
+31
View File
@@ -0,0 +1,31 @@
Godot 4.7 project. Units: 1 = 1 meter. Open this folder (it has project.godot). Hongqi is the main scene. The other ships are resolute.tscn, kestrel.tscn, soyuz.tscn, and shrike.tscn. Do not re-import these GLBs into the hangar. These .tscn files are ordinary Godot 4.7 scenes; replace the whole folder if an older copy still says a scene is corrupt.
Each scene is a glb (mesh, embedded PNG textures, 0..1 animation clips) plus a script that spawns particles on the Vfx sockets when you press Play. Textures are inside the glb. Ribbons are not. Drive, RCS, and abort flames are crossed additive ribbons from naf_fx.gd and ribbon.gdshader. Chaff and abort sparks are the only GPUParticles. PDC is a stream of short tracer bolts, not one beam. The tscn adds a near-black world so the additive jets show. Godot's default grey sky washes them out.
Press Play, then use the inspector. Drive On plus Throttle runs the torch. The 0..1 sliders seek the imported clips. Gear starts down. Hatches start shut. Hongqi radiators start open.
Keep these files together. The scripts preload naf_fx.gd from this folder.
| Scene | Open | Axes |
|---|---|---|
| resolute.tscn | NAF Resolute | nose +Y, starboard +X, dorsal +Z |
| kestrel.tscn | Kestrel EC-01 | nose +Y, starboard +X, dorsal +Z |
| hongqi.tscn | 红旗-07 | nose +Y, starboard +X, dorsal +Z. Root scale 1.14, same as the hangar. |
| soyuz.tscn | Soyuz-MS | dock +Z, dorsal +Y, starboard +X. Nose thrust is +Z. |
| shrike.tscn | Phantom II | nose +Z, belly -Y, starboard +X. Nose thrust is +Z. |
Rest pose: gear down where the ship has gear, hatches shut, torpedo doors shut, canopy shut, effects off. Hongqi radiators start open. Sliders seek the imported AnimationPlayer. If Godot nests that player, the scripts look it up with find_child.
Hongqi clips: gear_deploy, radiators_deploy, hatch_boarding_port, hatch_boarding_stbd, hatch_airlock_open, hatch_deck_open, hatch_ramp_open, torpedo_doors_open.
Hongqi sockets: Vfx_DrivePlume (main bell, exhaust -Y), Vfx_Drive_Port, Vfx_Drive_Stbd, Vfx_Chaff, RCS *_Vfx on the grafted clusters, PDT_*_Muzzle.
Hongqi drive_on plus throttle is a gold core inside a cinnabar sheath. Outboard bells use a shorter copy of the same plume. battle_lights goes dim cinnabar, not the Resolute red.
Soyuz clips: hatch_crew_open, hatch_transfer_open, hatch_dock_open.
Soyuz sockets: Vfx_DrivePlume (exhaust -Z, short chemical plume), Vfx_RCS_*, Vfx_Abort_0..5 (the six soft-landing motors). abort_burn is that landing burn.
Shrike clips: gear_deploy, canopy_open.
Shrike sockets: Vfx_DrivePlume (exhaust -Z), Vfx_RCS_*, PDT_L_Muzzle, PDT_R_Muzzle, Vfx_Chaff.
Resolute and Kestrel are unchanged in behavior. See PARTICLE_FX.md for ribbon sizes, which bell fires, and what not to replace with a particle spray.
Exhaust direction is the socket's local position, rotated by the inverse of its quaternion. Do not aim the ribbons by the ship's nose axis. A bell fires only when that exhaust dots the negated thrust by more than 0.7.
+10
View File
@@ -0,0 +1,10 @@
NAF Kestrel EC-01 — Godot GLB
Units: 1 = 1 m. Y-up. Docking collar is +Y. Visors are +Z. Heat shield is -Y.
This file is the original mesh with hangar fixes baked into node transforms:
- Nav lights (port, starboard, white) sit outside the hull, above the heat shield.
- RCS bells seated on their housings and aimed outward.
- Abort motors yawed off the RCS line, fairings on the skin.
- Blade and VHF antennas pulled onto the hull. S-band seated on the shoulder.
- Docking hatch lid and probe seated. Probe stays a child of DockHatch, so hatch_dock_open still carries it.
Animations: gear_deploy, hatch_crew_open, hatch_ladder_deploy, hatch_dock_open, instruments_demo.
kestrel_ec01.gd drives those clips 0..1.
+164
View File
@@ -0,0 +1,164 @@
# Particle FX handoff — Resolute and Kestrel
The hangar look is **not a GPUParticles spray**. Drive, RCS, and the abort flame are additive ribbon meshes parented to the Vfx sockets. A spray of quads (what the old Godot scripts built) reads as smoke. The hangar reads as a solid jet.
Source of truth: `public/hangar-fx.js`, function `updateFx`. Several helpers in that file are **dead** and must not be ported:
- `makeDrivePlume` / `stepDrivePlume` — unused. The live drive is `makeSpear`.
- `makeRcsJet` / `stepRcsJet` — unused. The live RCS jet is `makeSpear`.
- `makePdcStream` / `stepPdcStream` — unused. The live PDC is `spawnTracer`.
- `burstChaff` — unused. The live chaff is `makeChaffCloud`.
The scripts in this folder already implement the live path:
- `naf_fx.gd` builds ribbons, abort sparks, chaff, tracer bolts, and the exhaust light.
- `ribbon.gdshader` is the additive gradient. UV.y 0 is the socket, 1 is the tip. Edges fade so the ribbon is a soft streak, not a rectangle.
- `naf_resolute.gd` arms drive, RCS, PDC, chaff.
- `kestrel_ec01.gd` arms RCS and the four abort motors.
Do not replace the ribbons with `GPUParticles3D`. Particles are only used where the hangar uses points: abort sparks and chaff.
## Axes and which bell fires
Ship space, same as the GLB: nose +Y, starboard +X, dorsal +Z. Port is -X. Ventral is -Z.
`rcs_direction` is a thrust, not an exhaust. A bell fires only when its exhaust direction (ribbon local +Y, in ship space) dots the **negated** thrust by more than 0.7. Off fires nothing. They do not all fire together.
| Direction | Thrust | Bells that fire |
|---|---|---|
| Nose | +Y | exhaust mostly -Y |
| Tail | -Y | exhaust mostly +Y |
| Starboard | +X | exhaust mostly -X |
| Port | -X | exhaust mostly +X |
| Dorsal | +Z | exhaust mostly -Z |
| Ventral | -Z | exhaust mostly +Z |
Exhaust direction comes from the socket, not from a guess. Take the socket's local `position`, rotate it by the inverse of the socket quaternion, and normalize. That is the nozzle axis. Aim the ribbon so its +Y matches. The socket stays put, so a gimballed drive bell carries the plume with it.
Burn does not pop. Ease toward the target with `1 - exp(-dt / tau)`. Tau is 0.08 s turning on, 0.14 s turning off.
Opacity flickers. For drive and RCS: `0.82 + 0.18 * sin(time * 37 + seed * 2.1)`. Hide the ribbon when the flickered opacity drops under 0.02.
## Resolute drive
Socket `Vfx_DrivePlume`. Two ribbons, each three double-sided planes spun by `i / 3 * PI` (0°, 60°, 120°). Additive, unshaded, no depth write, depth test on, no fog.
| Piece | Length | Width | Planes | Base opacity | Color along the jet |
|---|---|---|---|---|---|
| Core | 38 m | 1.35 m | 3 | 0.95 | white `f4f8ff` at the throat, ice blue at mid, clear at the tip |
| Sheath | 26 m | 3.2 m | 3 | 0.50 | saturated blue `5aa8ff`, wider and shorter than the core |
`drive_on` with `throttle` is the power. Spool eases toward 1 over 0.26 s when power > 0.04, and back to 0 over 0.07 s. Visible amount is `power * spool`. The sheath uses 0.82 of that amount.
If the world exhaust points down (`world_dir.y < -0.35`) and the bell is above the hangar pad, shorten both ribbons by the same factor so they stop at the deck instead of spearing it. Deck is world y = -2.45, with 0.22 m of clearance. Minimum reach 0.9 m. If the bell is already below that plane (the ship sitting at the origin in these scenes), do not clip: the torch stays full length. The sheath mesh is already the shorter one. Do **not** multiply its scale by 26/38 a second time. In the hangar the ship stands nose-up on the pad, so the torch points at the deck and this clip is why the plume is a short bright jet under the bell instead of a 38 m lance. In space, or with the ship at the scene origin, exhaust is not aimed at a pad under the bell and the full length shows.
Spot light on the socket, 0.35 m out along the exhaust, color `c5dcff`, cone half-angle 0.3 rad, range follows the clipped length (at least 2.2 m, at most 38 m), decay 2. Energy is `5 * plume amount`. Godot's tonemapper is harsher than the hangar. If it blooms white, lower the light energy. Do not dim the ribbon to compensate.
`DriveGlow` and `DrivePlasma` are materials, not particles. Emission color `(0.55, 0.72, 1.0) * power`, energy `power * 7`. Radiator fins stay cold unless throttle has been above 0.72 with the panels open. That heat path is already in `naf_resolute.gd`. Leave it.
## Resolute RCS
One ribbon per socket whose name ends in `_Vfx` or starts with `Vfx_RCS_`. Skip `Vfx_DrivePlume`, `Vfx_Chaff`, and anything named `Vfx_Abort_`.
Length 1.2 m, width 0.09 m, **two** planes (0° and 90°). Base opacity 0.82. Color `dcefff` at the throat, fading to pale blue, then clear. Same flicker and burn ease as the drive. No gravity, no spread. It should look like a pencil of light out of the bell, not a cone of sparks.
## Resolute PDC
Not a beam and not a particle system. Each `PDT_*_Muzzle` fires discrete bolts at 42 Hz, at most 2 per frame, 90 bolts alive across the whole ship.
Each bolt is a 5-sided additive cone, color `ffe6b0`, radius 0.016 at the tail and 0.008 at the nose, then scaled in length to a random 1.5–2.3 m. Life 0.12–0.2 s. Speed 90–140 m/s. Direction is the muzzle exhaust, jittered in a 1.15° cone. Spawn 0.18 m ahead of the socket. When `pdc_fire` turns off, stop spawning and let the live bolts finish.
Turret slew is not part of this handoff.
## Resolute chaff
Socket `Vfx_Chaff`. This one **is** particles, because the hangar cloud is points.
28 soft discs. Life about 1.55 s (the hangar rolls 1.35–1.75). Speed 3–8 m/s. Spread 22°. No gravity, no damping. Size 0.07–0.16 m. Color `(0.90, 0.93, 0.97)`, additive. Hold alpha until 55% of life, then fade to nothing. `chaff_fire` is the continuous cloud.
## Kestrel RCS
Same ribbon as Resolute RCS, on every `Vfx_RCS_*` socket. Kestrel has no drive, no PDC, and no chaff.
## Kestrel abort
Socket per motor: `Vfx_Abort_FR`, `Vfx_Abort_FL`, `Vfx_Abort_RR`, `Vfx_Abort_RL`. `abort_burn` ramps a hold value up over 0.22 s and down over 0.28 s. All four motors use that one value.
Each motor has:
| Piece | What it is |
|---|---|
| Core ribbon | Tapered, 2.1 m. Width 0.06 m at the throat, 0.20 m at the tip. Width along the length is `w0 + (w1 - w0) * t^0.7`. Three planes. White-hot `fff4e6` at the root, orange, then clear. Opacity `hold * 0.95 * flick`. |
| Sheath ribbon | Tapered, 5.2 m. Width 0.10 → 0.78 m, same exponent. Orange `ff6418`. Opacity `hold * 0.82 * flick`. Its length scale breathes: `0.9 + 0.1 * sin(time * 19)`. |
| Core sparks | 10 round additive points. Life 0.12–0.18 s (use 0.15). Speed 18–28 m/s. Spread 1.6°. Size about 0.05 m. |
| Sheath sparks | 18 points. Life 0.20–0.32 s (use 0.26). Speed 14–22 m/s. Spread 4.5°. Size about 0.09 m. |
| Color of sparks | White `(1, 0.97, 0.92)` at birth, orange `(1, 0.46, 0.12)` by mid-life, dark red `(0.55, 0.16, 0.05)` at the end. Opacity `hold * 0.55`. |
| Flash | Soft additive disc on the throat, color `fff1dd`, opacity `hold * 0.45`, scale `0.06 + hold * 0.04` m. |
| Light | Spot, color `ff6a28`, energy `hold * 8`, half-angle 0.5 rad, range 6.5 m, decay 1.8, sitting 0.08 m out from the socket. |
Abort flicker is **not** the RCS flicker. Use `0.86 + 0.14 * sin(time * 41)`.
The ribbons are the flame. The sparks are a thin hot core inside them. If you only keep the sparks, it will look like the old spray.
## Hongqi, Soyuz, Phantom II
Same ribbon code as above (`naf_fx.gd`). Do not invent a second particle style. Sizes match `hangar-fx.js`.
### Hongqi
Axes: nose +Y, starboard +X, dorsal +Z. Torch exhaust -Y.
| Socket | Piece | Length | Width | Planes | Color |
|---|---|---|---|---|---|
| Vfx_DrivePlume | Core | 30 m | 1.15 m | 3 | gold-white `fff6d4` |
| Vfx_DrivePlume | Sheath | 22 m | 2.7 m | 3 | cinnabar `e23b2f` |
| Vfx_Drive_Port / Vfx_Drive_Stbd | Core | 12 m | 0.55 m | 3 | same gold |
| Vfx_Drive_Port / Vfx_Drive_Stbd | Sheath | 8 m | 1.2 m | 3 | same cinnabar |
RCS is the Resolute pencil (1.2 m, two planes) on every `*_Vfx` / `Vfx_RCS_*` socket. PDC is the same 42 Hz tracer stream on `PDT_*_Muzzle`. Chaff is the Resolute cloud on `Vfx_Chaff`. `battle_lights` uses cinnabar `c4342c`, not Resolute red. Radiator fins glow only after a hard burn, same rule as Resolute.
### Soyuz-MS
Axes: dock +Z, dorsal +Y, starboard +X. Service-engine exhaust -Z. `rcs_direction` Nose is +Z thrust.
| Socket | Piece | Length | Width | Planes | Color |
|---|---|---|---|---|---|
| Vfx_DrivePlume | Core | 7 m | 0.5 m | 4 | warm `fff3d4` |
| Vfx_DrivePlume | Sheath | 5 m | 1.25 m | 3 | chemical orange `e85a12` |
`Vfx_Abort_0` … `Vfx_Abort_5` are the six soft-landing motors. They use the Kestrel abort recipe (tapered core, orange sheath, sparks). `abort_burn` holds all six.
### Phantom II (shrike)
Axes: nose +Z, belly -Y, starboard +X. Torch exhaust -Z. `rcs_direction` Nose is +Z thrust. Dorsal is +Y.
| Socket | Piece | Length | Width | Planes | Color |
|---|---|---|---|---|---|
| Vfx_DrivePlume | Core | 22 m | 0.68 m | 3 | ice `f4f8ff` |
| Vfx_DrivePlume | Sheath | 14 m | 1.7 m | 3 | blue `5aa8ff` |
RCS, the two `PDT_*_Muzzle` guns, and `Vfx_Chaff` match Resolute. `cabin_dim` only lowers the cabin lights. There is no battle-red swap.
## What to give Godot 4.7
One folder, with `project.godot`. Open it in Godot 4.7. Hongqi is the main scene. Shared: `naf_fx.gd`, `ribbon.gdshader`, this file.
- Resolute: `naf_resolute.glb`, `naf_resolute.gd`, `resolute.tscn`
- Kestrel: `kestrel_ec01_escape_capsule.glb`, `kestrel_ec01.gd`, `kestrel.tscn`
- Hongqi: `hongqi.glb`, `hongqi.gd`, `hongqi.tscn`
- Soyuz: `soyuz.glb`, `soyuz.gd`, `soyuz.tscn`
- Phantom II: `shrike.glb`, `shrike.gd`, `shrike.tscn`
Textures and the 0..1 clips are already in the glb. Play the scene before judging the jets. Additive ribbons disappear on the default grey sky. The tscn world is near-black on purpose.
Play, then:
- Resolute: `drive_on`, throttle 0.8. White core, blue sheath, flickering. Not a cloud of quads.
- Resolute: `rcs_direction` Starboard. Only the bells pushing that way light.
- Resolute: `pdc_fire`. Separate bolts, not one beam. `chaff_fire` is a small silver cloud.
- Kestrel: `abort_burn`. Four motors, white throat into an orange plume.
- Hongqi: `drive_on`, throttle 0.8. Gold core, cinnabar sheath, on the main bell and both outboards.
- Soyuz: `drive_on` is a short chemical plume on the service engine. `abort_burn` lights the six landing motors.
- Phantom II: `drive_on` is the ice/blue torch out the tail. `canopy_open` and `gear_deploy` are the two clips.
+312
View File
@@ -0,0 +1,312 @@
extends Node3D
## Hongqi 红旗-07. Godot 4.7. Units: 1 m.
## Axes: nose +Y, starboard +X, dorsal +Z. Aft exhaust is -Y.
## Clips are 0..1 on the AnimationPlayer imported with the glb.
## Particles are spawned on the Vfx sockets at runtime. They are not in the mesh.
## Drive is a cinnabar torch: white-gold core, red sheath, plus a smaller plume on each outboard bell.
## See PARTICLE_FX.md.
const Fx = preload("res://naf_fx.gd")
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
@export_range(0.0, 1.0) var radiators_deploy := 1.0 : set = set_radiators_deploy
@export_range(0.0, 1.0) var hatch_boarding_port := 0.0 : set = set_hatch_boarding_port
@export_range(0.0, 1.0) var hatch_boarding_stbd := 0.0 : set = set_hatch_boarding_stbd
@export_range(0.0, 1.0) var hatch_airlock_open := 0.0 : set = set_hatch_airlock_open
@export_range(0.0, 1.0) var hatch_deck_open := 0.0 : set = set_hatch_deck_open
@export_range(0.0, 1.0) var hatch_ramp_open := 0.0 : set = set_hatch_ramp_open
@export_range(0.0, 1.0) var torpedo_doors_open := 0.0 : set = set_torpedo_doors_open
@export_range(0.0, 1.0) var throttle := 0.0
@export var drive_on := false
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var pdc_fire := false
@export var chaff_fire := false
@export var battle_lights := false : set = set_battle_lights
var _heat := 0.0
var _spool := 0.0
var _time := 0.0
var _rad_mats: Array[StandardMaterial3D] = []
var _drive_socket: Node3D
var _drive_core: Node3D
var _drive_sheath: Node3D
var _drive_light: SpotLight3D
var _aux: Array[Dictionary] = []
var _rcs: Array[Node3D] = []
var _muzzles: Array[Node3D] = []
var _tracers: Array[Dictionary] = []
var _tracer_pool: Array[MeshInstance3D] = []
var _tracer_mesh: ArrayMesh
var _tracer_mat: StandardMaterial3D
var _chaff: Array[GPUParticles3D] = []
var _cabin_ready := false
var _cabin_lights: Array[Light3D] = []
var _cabin_rest: Array[Dictionary] = []
var _cabin_mats: Array[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),
]
func _ready() -> void:
_seek("gear_deploy", gear_deploy)
_seek("radiators_deploy", radiators_deploy)
_collect_glow()
_arm_fx()
_collect_cabin()
set_physics_process(true)
func _physics_process(delta: float) -> void:
_time += delta
var power := throttle if drive_on else 0.0
var open := 0.0 if radiators_deploy < 0.15 else clampf((radiators_deploy - 0.15) / 0.7, 0.0, 1.0)
var hard := 0.0 if power < 0.72 else (power - 0.72) / 0.28
_heat = lerpf(_heat, hard * open, 1.0 - exp(-delta / (18.0 if hard * open > _heat else 22.0)))
var glow := 0.0 if _heat < 0.55 else pow((_heat - 0.55) / 0.45, 1.7)
for mat in _rad_mats:
mat.emission_enabled = glow > 0.02
mat.emission = Color(1.0, 0.35, 0.08)
mat.emission_energy_multiplier = glow * 1.15
var spool_target := 1.0 if power > 0.04 else 0.0
_spool = Fx.smooth(_spool, spool_target, delta, 0.26 if spool_target > 0.0 else 0.07)
var plume := power * _spool
if _drive_core:
var scale := _drive_reach_scale(_drive_socket, _drive_core)
Fx.set_spear(_drive_core, plume, scale, _time)
Fx.set_spear(_drive_sheath, plume * 0.82, scale, _time)
if _drive_light:
_drive_light.light_energy = plume * 5.0
for aux in _aux:
var socket := aux.socket as Node3D
var core := aux.core as Node3D
var sheath := aux.sheath as Node3D
var scale := _drive_reach_scale(socket, core)
Fx.set_spear(core, plume, scale, _time + 0.4)
Fx.set_spear(sheath, plume * 0.7, scale, _time + 0.4)
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for ribbon in _rcs:
var want := 0.0
if thrust != Vector3.ZERO:
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
if exhaust.dot(-thrust) > 0.7:
want = 1.0
var burn := float(ribbon.get_meta("burn"))
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
ribbon.set_meta("burn", burn)
Fx.set_spear(ribbon, burn, 1.0, _time)
for cloud in _chaff:
cloud.emitting = chaff_fire
_step_pdc(delta)
func set_battle_lights(v: bool) -> void:
battle_lights = v
if _cabin_ready:
_apply_cabin()
func _collect_cabin() -> void:
for n in find_children("*", "Light3D", true, false):
var nm := String(n.name)
if nm.begins_with("Nav") or nm.begins_with("Vfx"):
continue
if not (nm.begins_with("PointLight_") or nm.begins_with("CabinLamp") or nm == "CabinFill"):
continue
_cabin_lights.append(n)
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
for n in find_children("*", "MeshInstance3D", true, false):
var nm := String(n.name)
if nm.find("Lamp") < 0 and not nm.begins_with("LightStrip_"):
continue
var mesh := n as MeshInstance3D
var mat := mesh.get_active_material(0)
if mat == null or not (mat is StandardMaterial3D):
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_ready = true
func _apply_cabin() -> void:
var red := Color(0.769, 0.204, 0.173)
for i in _cabin_lights.size():
var lamp := _cabin_lights[i]
var rest: Dictionary = _cabin_rest[i]
if battle_lights:
lamp.light_color = red
lamp.light_energy = maxf(0.05, float(rest.energy) * 0.2)
else:
lamp.light_color = rest.color
lamp.light_energy = float(rest.energy)
for row in _cabin_mats:
var mat := row.mat as StandardMaterial3D
if battle_lights:
mat.emission_enabled = true
mat.emission = red
mat.emission_energy_multiplier = maxf(0.28, float(row.energy) * 0.45)
else:
mat.emission_enabled = bool(row.on)
mat.emission = row.color
mat.emission_energy_multiplier = float(row.energy)
func set_gear_deploy(v: float) -> void:
gear_deploy = v
_seek("gear_deploy", v)
func set_radiators_deploy(v: float) -> void:
radiators_deploy = v
_seek("radiators_deploy", v)
func set_hatch_boarding_port(v: float) -> void:
hatch_boarding_port = v
_seek("hatch_boarding_port", v)
func set_hatch_boarding_stbd(v: float) -> void:
hatch_boarding_stbd = v
_seek("hatch_boarding_stbd", v)
func set_hatch_airlock_open(v: float) -> void:
hatch_airlock_open = v
_seek("hatch_airlock_open", v)
func set_hatch_deck_open(v: float) -> void:
hatch_deck_open = v
_seek("hatch_deck_open", v)
func set_hatch_ramp_open(v: float) -> void:
hatch_ramp_open = v
_seek("hatch_ramp_open", v)
func set_torpedo_doors_open(v: float) -> void:
torpedo_doors_open = v
_seek("torpedo_doors_open", v)
func _player() -> AnimationPlayer:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap:
return ap
return find_child("AnimationPlayer", true, false) as AnimationPlayer
func _seek(clip: StringName, t: float) -> void:
var ap := _player()
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 _collect_glow() -> void:
for n in find_children("*", "MeshInstance3D", true, false):
var nm := String(n.name)
if not nm.begins_with("Radiator_"):
continue
if nm.find("Fin_") < 0 and nm.find("Edge_") < 0:
continue
var mesh := n as MeshInstance3D
var mat := mesh.get_active_material(0)
if mat == null or not (mat is StandardMaterial3D):
continue
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
mesh.set_surface_override_material(0, copy)
_rad_mats.append(copy)
func _arm_fx() -> void:
_tracer_mesh = Fx.tracer_mesh()
_tracer_mat = Fx.tracer_mat()
var gold := Color(1.0, 0.965, 0.831, 0.95)
var gold_mid := Color(1.0, 0.72, 0.35, 0.4)
var gold_tip := Color(0.9, 0.4, 0.1, 0.0)
var red := Color(0.886, 0.231, 0.184, 0.9)
var red_mid := Color(0.55, 0.08, 0.05, 0.35)
var red_tip := Color(0.25, 0.02, 0.01, 0.0)
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
if drive:
_drive_socket = drive
_drive_core = Fx.spear(drive, 30.0, 1.15, gold, gold_mid, gold_tip, 0.92, 3, 1.0, "DriveCore")
_drive_sheath = Fx.spear(drive, 22.0, 2.7, red, red_mid, red_tip, 0.5, 3, 2.0, "DriveSheath")
_drive_light = Fx.spot(drive, 0.35, Color(1.0, 0.882, 0.69), 0.55, 7.5, 2.0)
for n in find_children("*", "Node3D", true, false):
var nm := String(n.name)
if nm == "Vfx_Drive_Port" or nm == "Vfx_Drive_Stbd":
_aux.append({
"socket": n,
"core": Fx.spear(n, 12.0, 0.55, gold, gold_mid, gold_tip, 0.9, 3, 3.0, "DriveCore"),
"sheath": Fx.spear(n, 8.0, 1.2, red, red_mid, red_tip, 0.45, 3, 4.0, "DriveSheath"),
})
elif _is_rcs_socket(n):
_rcs.append(Fx.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, 1.0, "Spear"))
elif nm.begins_with("PDT_") and nm.ends_with("_Muzzle"):
_muzzles.append(n)
n.set_meta("acc", randf() * 0.02)
elif nm == "Vfx_Chaff":
_chaff.append(Fx.chaff(n))
func _is_rcs_socket(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Vfx_Drive") or nm == "Vfx_Chaff" or nm.begins_with("Vfx_Abort"):
return false
return nm.ends_with("_Vfx") or nm.begins_with("Vfx_RCS_")
func _drive_reach_scale(socket: Node3D, core: Node3D) -> float:
if socket == null or core == null:
return 1.0
var length := float(core.get_meta("len"))
var exhaust: Vector3 = (socket.global_transform.basis * core.basis.y).normalized()
if exhaust.y >= -0.35:
return 1.0
# Pad is world y=-2.45. Clip only when the bell is above it.
# At the origin the bells sit below that plane, so the torch stays full length.
var room := (socket.global_position.y - (-2.45 + 0.22)) / -exhaust.y
if room <= 0.0:
return 1.0
return maxf(0.9, minf(length, room)) / length
func _step_pdc(delta: float) -> void:
if pdc_fire:
for mz in _muzzles:
var acc := float(mz.get_meta("acc")) - delta
var shots := 0
while acc <= 0.0 and shots < 2 and _tracers.size() < 90:
acc += 1.0 / 42.0
shots += 1
_spawn_tracer(mz)
mz.set_meta("acc", acc)
for i in range(_tracers.size() - 1, -1, -1):
var t: Dictionary = _tracers[i]
t.life = float(t.life) - delta
var mesh := t.mesh as MeshInstance3D
if float(t.life) <= 0.0:
mesh.visible = false
_tracer_pool.append(mesh)
_tracers.remove_at(i)
continue
mesh.position += (t.dir as Vector3) * float(t.speed) * delta
func _spawn_tracer(mz: Node3D) -> void:
var mesh := _take_tracer()
var world_dir: Vector3 = (mz.global_transform.basis * Fx.exhaust_local(mz)).normalized()
var shot := Fx.cone(world_dir, 1.15)
var origin := mz.global_position + world_dir * 0.18
var local_dir: Vector3 = global_transform.basis.inverse() * shot
mesh.position = to_local(origin)
mesh.basis = Fx.basis_y(local_dir)
mesh.scale = Vector3(1, randf_range(1.5, 2.3), 1)
mesh.visible = true
_tracers.append({"mesh": mesh, "dir": local_dir, "life": randf_range(0.12, 0.2), "speed": randf_range(90.0, 140.0)})
func _take_tracer() -> MeshInstance3D:
if _tracer_pool.size() > 0:
return _tracer_pool.pop_back()
var m := MeshInstance3D.new()
m.mesh = _tracer_mesh
m.material_override = _tracer_mat
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
m.visible = false
add_child(m)
return m
+1
View File
@@ -0,0 +1 @@
uid://d2ecdbay4q31q
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bc0c4fwo6oom2"
path="res://.godot/imported/hongqi.glb-60257baeac74884c31f76642ce55bf18.scn"
[deps]
source_file="res://hongqi.glb"
dest_files=["res://.godot/imported/hongqi.glb-60257baeac74884c31f76642ce55bf18.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+24
View File
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://xxp0p0vb72tn"]
[ext_resource type="PackedScene" uid="uid://bc0c4fwo6oom2" path="res://hongqi.glb" id="1_glb"]
[ext_resource type="Script" uid="uid://d2ecdbay4q31q" path="res://hongqi.gd" id="2_gd"]
[sub_resource type="Environment" id="env"]
background_mode = 1
background_color = Color(0.012, 0.014, 0.018, 1)
ambient_light_source = 2
ambient_light_color = Color(0.62, 0.7, 0.8, 1)
ambient_light_energy = 0.2
tonemap_mode = 2
[node name="Hongqi" instance=ExtResource("1_glb")]
script = ExtResource("2_gd")
[node name="HangarSky" type="WorldEnvironment" parent="."]
environment = SubResource("env")
[node name="Key" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.8, -0.5, 0.33, 0, 0.55, 0.84, -0.6, -0.67, 0.44, 0, 30, 12)
light_color = Color(0.82, 0.88, 1, 1)
light_energy = 1.15
shadow_enabled = true
+24
View File
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://xg7scvaiinq2"]
[ext_resource type="PackedScene" uid="uid://dcr7fsl5qx4ir" path="res://kestrel_ec01_escape_capsule.glb" id="1_glb"]
[ext_resource type="Script" uid="uid://c4ve1ai244iw6" path="res://kestrel_ec01.gd" id="2_gd"]
[sub_resource type="Environment" id="env"]
background_mode = 1
background_color = Color(0.012, 0.014, 0.018, 1)
ambient_light_source = 2
ambient_light_color = Color(0.62, 0.7, 0.8, 1)
ambient_light_energy = 0.22
tonemap_mode = 2
[node name="Kestrel" instance=ExtResource("1_glb")]
script = ExtResource("2_gd")
[node name="HangarSky" type="WorldEnvironment" parent="."]
environment = SubResource("env")
[node name="Key" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.8, -0.5, 0.33, 0, 0.55, 0.84, -0.6, -0.67, 0.44, 2, 8, 4)
light_color = Color(0.9, 0.86, 0.78, 1)
light_energy = 1.15
shadow_enabled = true
+223
View File
@@ -0,0 +1,223 @@
extends Node3D
## Kestrel EC-01 — analog 0..1 clip driver.
## Nodes: hatch_crew, hatch_ladder, hatch_dock, LandingGear, SeatCamera.
##
## Particle look is PARTICLE_FX.md. RCS is a short crossed ribbon. Abort is two tapered ribbons plus sparks.
## rcs_direction fires only the bells that push that way. Off fires nothing.
## abort_burn holds all four abort motors. The flame ramps in over 0.22 s and out over 0.28 s.
## Ship axes: nose +Y, starboard +X, dorsal +Z.
const Fx = preload("res://naf_fx.gd")
@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_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var abort_burn := false
## Cabin strips and fill. On drops them to a low watch. It does not swap to battle red.
@export var cabin_dim := false : set = set_cabin_dim
var _rcs: Array[Node3D] = []
var _aborts: Array[Dictionary] = []
var _abort_hold := 0.0
var _time := 0.0
var _cabin_ready := false
var _cabin_lights: Array[Light3D] = []
var _cabin_rest: Array[Dictionary] = []
var _cabin_mats: Array[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),
]
func _ready() -> void:
_seek("gear_deploy", gear_deploy)
_arm_fx()
_collect_cabin()
func _physics_process(delta: float) -> void:
_time += delta
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for ribbon in _rcs:
var want := 0.0
if thrust != Vector3.ZERO:
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
if exhaust.dot(-thrust) > 0.7:
want = 1.0
var burn := float(ribbon.get_meta("burn"))
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
ribbon.set_meta("burn", burn)
Fx.set_spear(ribbon, burn, 1.0, _time)
if abort_burn:
_abort_hold = minf(1.0, _abort_hold + delta / 0.22)
elif _abort_hold > 0.0:
_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
Fx.set_opacity(core, _abort_hold * 0.95 * flick if on else 0.0)
Fx.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 set_cabin_dim(v: bool) -> void:
cabin_dim = v
if _cabin_ready:
_apply_cabin()
func _collect_cabin() -> void:
_cabin_lights.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 p := n.get_parent()
var cabin := false
while p:
if String(p.name) == "CabinLights":
cabin = true
break
p = p.get_parent()
if not cabin:
continue
_cabin_lights.append(n)
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
for n in find_children("*", "MeshInstance3D", true, false):
var nm := String(n.name)
if not nm.begins_with("LightStrip"):
continue
var mesh := n as MeshInstance3D
var mat := mesh.get_active_material(0)
if mat == null or not (mat is StandardMaterial3D):
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_ready = true
_apply_cabin()
func _apply_cabin() -> void:
for i in _cabin_lights.size():
var lamp := _cabin_lights[i]
var rest: Dictionary = _cabin_rest[i]
if cabin_dim:
lamp.light_color = rest.color
lamp.light_energy = float(rest.energy) * 0.16
else:
lamp.light_color = rest.color
lamp.light_energy = float(rest.energy)
for row in _cabin_mats:
var mat := row.mat as StandardMaterial3D
if cabin_dim:
mat.emission_enabled = true
mat.emission = row.color
mat.emission_energy_multiplier = float(row.energy) * 0.18
else:
mat.emission_enabled = bool(row.on)
mat.emission = row.color
mat.emission_energy_multiplier = float(row.energy)
func set_gear_deploy(v: float) -> void:
gear_deploy = v
_seek("gear_deploy", v)
func set_hatch_crew_open(v: float) -> void:
hatch_crew_open = v
_seek("hatch_crew_open", v)
func set_hatch_ladder_deploy(v: float) -> void:
hatch_ladder_deploy = v
_seek("hatch_ladder_deploy", v)
func set_hatch_dock_open(v: float) -> void:
hatch_dock_open = v
_seek("hatch_dock_open", v)
func set_instruments_demo(v: float) -> void:
instruments_demo = v
_seek("instruments_demo", v)
func _seek(clip: StringName, t: float) -> void:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap == null:
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
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 _arm_fx() -> void:
for n in find_children("*", "Node3D", true, false):
var nm := String(n.name)
if nm.begins_with("Vfx_RCS_"):
_rcs.append(Fx.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, 1.0, "Spear"))
elif nm.begins_with("Vfx_Abort_"):
_aborts.append(_make_abort(n))
func _make_abort(socket: Node3D) -> Dictionary:
# Core is the white-hot throat. Sheath is the orange plume that opens toward the tip.
var core := Fx.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 := Fx.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")
var sparks := Fx.sparks(socket, "AbortCoreSparks", 10, 0.15, 18.0, 28.0, 1.6, 0.05, 6.0)
var sheath_sparks := Fx.sparks(socket, "AbortSheathSparks", 18, 0.26, 14.0, 22.0, 4.5, 0.09, 8.0)
for p in [sparks, sheath_sparks]:
var proc := p.process_material as ParticleProcessMaterial
if proc:
proc.color_ramp = _abort_ramp()
return {
"core": core,
"sheath": sheath,
"sparks": sparks,
"sheath_sparks": sheath_sparks,
"flash": Fx.abort_flash(socket),
"light": Fx.spot(socket, 0.08, Color(1.0, 0.416, 0.157), 0.5, 6.5, 1.8),
}
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
+1
View File
@@ -0,0 +1 @@
uid://c4ve1ai244iw6
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dcr7fsl5qx4ir"
path="res://.godot/imported/kestrel_ec01_escape_capsule.glb-515b5d663b088267edb64b261b63e5ee.scn"
[deps]
source_file="res://kestrel_ec01_escape_capsule.glb"
dest_files=["res://.godot/imported/kestrel_ec01_escape_capsule.glb-515b5d663b088267edb64b261b63e5ee.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+319
View File
@@ -0,0 +1,319 @@
extends RefCounted
## Hangar particle look for Resolute and Kestrel. See PARTICLE_FX.md.
## Drive, RCS, and abort flames are crossed additive ribbons. They are not GPUParticles.
const RIBBON := preload("res://ribbon.gdshader")
static func exhaust_local(socket: Node3D) -> Vector3:
var dir := socket.position
if dir.length() < 0.001:
dir = Vector3.UP
return (socket.quaternion.inverse() * dir).normalized()
static 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)
static func cone(dir: Vector3, deg: float) -> Vector3:
var axis := dir.normalized()
var up := Vector3.RIGHT if absf(axis.dot(Vector3.UP)) > 0.9 else Vector3.UP
var tangent := axis.cross(up).normalized()
var bitangent := axis.cross(tangent).normalized()
var a := randf() * TAU
var r := randf() * deg_to_rad(deg)
return (axis + tangent * (cos(a) * r) + bitangent * (sin(a) * r)).normalized()
static func _ribbon_mat(root: Color, mid: Color, tip: Color) -> ShaderMaterial:
var mat := ShaderMaterial.new()
mat.shader = RIBBON
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
static 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()
static 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
var v := t
st.set_uv(Vector2(0, v))
st.add_vertex(Vector3(-w, y, 0))
st.set_uv(Vector2(1, v))
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()
static func _crossed(socket: Node3D, mesh: ArrayMesh, mat: ShaderMaterial, planes: int, name: String, length: float, opacity: float, seed: float) -> Node3D:
var g := Node3D.new()
g.name = name
g.set_meta("mat", mat)
g.set_meta("len", length)
g.set_meta("opacity", opacity)
g.set_meta("seed", 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_local(socket))
g.visible = false
socket.add_child(g)
return g
static func spear(socket: Node3D, length: float, width: float, root: Color, mid: Color, tip: Color, opacity: float, planes: int, seed: float, node_name: String = "Spear") -> Node3D:
if socket.get_node_or_null(node_name):
return socket.get_node(node_name) as Node3D
return _crossed(socket, _sheet(length, width), _ribbon_mat(root, mid, tip), planes, node_name, length, opacity, seed)
static 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:
if socket.get_node_or_null(node_name):
return socket.get_node(node_name) as Node3D
return _crossed(socket, _taper(length, w0, w1), _ribbon_mat(root, mid, tip), planes, node_name, length, opacity, 1.0)
static func set_opacity(ribbon: Node3D, opacity: float) -> void:
if ribbon == null:
return
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)
static func set_spear(ribbon: Node3D, amount: float, length_scale: float, time: float) -> void:
if ribbon == null:
return
var a := maxf(amount, 0.0)
if a < 0.02:
set_opacity(ribbon, 0.0)
return
var seed := float(ribbon.get_meta("seed"))
var flick := 0.82 + 0.18 * sin(time * 37.0 + seed * 2.1)
var base := float(ribbon.get_meta("opacity"))
var s := maxf(length_scale, 0.001)
ribbon.scale = Vector3(1, s, 1)
set_opacity(ribbon, base * minf(a, 1.0) * flick)
static func spot(socket: Node3D, along: float, color: Color, angle_rad: float, range_m: float, decay: float) -> SpotLight3D:
var existing := socket.get_node_or_null("ExhaustLight") as SpotLight3D
if existing:
return existing
var aim := basis_y(exhaust_local(socket))
var light := SpotLight3D.new()
light.name = "ExhaustLight"
light.light_color = color
light.light_energy = 0.0
light.spot_range = range_m
light.spot_angle = rad_to_deg(angle_rad)
light.spot_attenuation = decay
light.shadow_enabled = false
light.position = aim.y * along
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
static 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)
static 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_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
static func _ramp(offsets: PackedFloat32Array, colors: PackedColorArray) -> GradientTexture1D:
var g := Gradient.new()
g.offsets = offsets
g.colors = colors
var tex := GradientTexture1D.new()
tex.gradient = g
return tex
static func sparks(socket: Node3D, node_name: String, amount: int, life: float, vmin: float, vmax: float, spread_deg: float, size: float, reach: float) -> GPUParticles3D:
var existing := socket.get_node_or_null(node_name) as GPUParticles3D
if existing:
return existing
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)
p.process_material = proc
p.material_override = _disc_mat()
var quad := QuadMesh.new()
quad.size = Vector2(size, size)
p.draw_pass_1 = quad
p.emitting = false
p.basis = basis_y(exhaust_local(socket))
socket.add_child(p)
return p
static func abort_flash(socket: Node3D) -> MeshInstance3D:
var existing := socket.get_node_or_null("AbortFlash") as MeshInstance3D
if existing:
return existing
var aim := basis_y(exhaust_local(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
static func tracer_mesh() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var sides := 5
var r0 := 0.016
var r1 := 0.008
for i in sides:
var a0 := TAU * float(i) / float(sides)
var a1 := TAU * float(i + 1) / float(sides)
var p0 := Vector3(cos(a0) * r0, 0.0, sin(a0) * r0)
var p1 := Vector3(cos(a1) * r0, 0.0, sin(a1) * r0)
var q0 := Vector3(cos(a0) * r1, 1.0, sin(a0) * r1)
var q1 := Vector3(cos(a1) * r1, 1.0, sin(a1) * r1)
st.add_vertex(p0)
st.add_vertex(q0)
st.add_vertex(p1)
st.add_vertex(p1)
st.add_vertex(q0)
st.add_vertex(q1)
return st.commit()
static func tracer_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.albedo_color = Color(1.0, 0.902, 0.690, 0.95)
draw.cull_mode = BaseMaterial3D.CULL_DISABLED
draw.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
return draw
static func chaff(socket: Node3D) -> GPUParticles3D:
var existing := socket.get_node_or_null("ChaffCloud") as GPUParticles3D
if existing:
return existing
var p := GPUParticles3D.new()
p.name = "ChaffCloud"
p.amount = 28
p.lifetime = 1.55
p.explosiveness = 0.0
p.local_coords = true
p.visibility_aabb = AABB(Vector3(-6, 0, -6), Vector3(12, 16, 12))
var proc := ParticleProcessMaterial.new()
proc.direction = Vector3(0, 1, 0)
proc.spread = 22.0
proc.initial_velocity_min = 3.0
proc.initial_velocity_max = 8.0
proc.gravity = Vector3.ZERO
proc.damping_min = 0.0
proc.damping_max = 0.0
proc.scale_min = 0.07
proc.scale_max = 0.16
proc.color = Color(0.90, 0.93, 0.97, 0.92)
proc.color_ramp = _ramp(
PackedFloat32Array([0.0, 0.55, 1.0]),
PackedColorArray([
Color(0.90, 0.93, 0.97, 0.92),
Color(0.90, 0.93, 0.97, 0.7),
Color(0.90, 0.93, 0.97, 0.0),
])
)
p.process_material = proc
p.material_override = _disc_mat()
var quad := QuadMesh.new()
quad.size = Vector2(1, 1)
p.draw_pass_1 = quad
p.emitting = false
p.basis = basis_y(exhaust_local(socket))
socket.add_child(p)
return p
static 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
+1
View File
@@ -0,0 +1 @@
uid://du4mcoyxwuwfl
+357
View File
@@ -0,0 +1,357 @@
extends Node3D
## NAF Resolute CVT-07 — analog 0..1 clip driver.
## Keep node names stable: LandingGear, Radiators, hatch_airlock_outer, HelmCamera, PlayerSpawn.
##
## Effects are inspector toggles, not clips.
## Particle look is PARTICLE_FX.md. Drive and RCS are crossed additive ribbons, not sprays.
## drive_on plus throttle runs the plume. Radiator panels stay grey metal unless the burn is hard and has been hard for a while.
## rcs_direction fires only the bells that push that way. Off fires nothing.
## pdc_fire is a stream of short tracer bolts, not one beam. chaff_fire dumps the dispensers.
## Torpedo bodies are not particles. Use the torpedo door clip, then fly a real round later.
## Sockets: Vfx_DrivePlume, *_Vfx, PDT_*_Muzzle, Vfx_Chaff.
const Fx = preload("res://naf_fx.gd")
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
@export_range(0.0, 1.0) var radiators_deploy := 1.0 : set = set_radiators_deploy
@export_range(0.0, 1.0) var hatch_airlock_open := 0.0 : set = set_hatch_airlock_open
@export_range(0.0, 1.0) var hatch_dock_open := 0.0 : set = set_hatch_dock_open
@export_range(0.0, 1.0) var hatch_cargo_open := 0.0 : set = set_hatch_cargo_open
@export_range(0.0, 1.0) var hatch_deck_open := 0.0 : set = set_hatch_deck_open
@export_range(0.0, 1.0) var torpedo_doors_open := 0.0 : set = set_torpedo_doors_open
@export_range(0.0, 1.0) var throttle := 0.0
@export var drive_on := false
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var pdc_fire := false
@export var chaff_fire := false
## Cabin practicals. Off is the normal warm watch. On drops the hab to dim red battle lighting.
@export var battle_lights := false : set = set_battle_lights
## Torch vector. Positive pitch swings the bell ventral and the nose down. Positive yaw swings the bell to starboard and the nose to port. No roll. Limit ±14°.
@export_range(-0.244, 0.244, 0.001) var gimbal_pitch := 0.0 : set = set_gimbal_pitch
@export_range(-0.244, 0.244, 0.001) var gimbal_yaw := 0.0 : set = set_gimbal_yaw
var _heat := 0.0
var _spool := 0.0
var _time := 0.0
var _rad_mats: Array[StandardMaterial3D] = []
var _drive_mats: Array[StandardMaterial3D] = []
var _drive_socket: Node3D
var _drive_core: Node3D
var _drive_sheath: Node3D
var _drive_light: SpotLight3D
var _rcs: Array[Node3D] = []
var _muzzles: Array[Node3D] = []
var _tracers: Array[Dictionary] = []
var _tracer_pool: Array[MeshInstance3D] = []
var _tracer_mesh: ArrayMesh
var _tracer_mat: StandardMaterial3D
var _chaff: Array[GPUParticles3D] = []
var _gimbal_rest: Basis
var _gimbal_ready := false
var _cabin_ready := false
var _cabin_lights: Array[Light3D] = []
var _cabin_rest: Array[Dictionary] = []
var _cabin_mats: Array[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),
]
func _ready() -> void:
_seek("gear_deploy", gear_deploy)
_seek("radiators_deploy", radiators_deploy)
_collect_glow()
_arm_fx()
var pivot := find_child("gimbal_pivot", true, false) as Node3D
if pivot:
_gimbal_rest = pivot.transform.basis
_gimbal_ready = true
_apply_gimbal()
_collect_cabin()
set_physics_process(true)
func set_gimbal_pitch(v: float) -> void:
gimbal_pitch = clampf(v, -0.244, 0.244)
if _gimbal_ready:
_apply_gimbal()
func set_gimbal_yaw(v: float) -> void:
gimbal_yaw = clampf(v, -0.244, 0.244)
if _gimbal_ready:
_apply_gimbal()
func _apply_gimbal() -> void:
var pivot := find_child("gimbal_pivot", true, false) as Node3D
if pivot == null:
return
var spin := Basis(Vector3.UP, gimbal_yaw) * Basis(Vector3.RIGHT, -gimbal_pitch)
pivot.transform.basis = _gimbal_rest * spin
func _physics_process(delta: float) -> void:
_time += delta
var power := throttle if drive_on else 0.0
var open := 0.0 if radiators_deploy < 0.15 else clampf((radiators_deploy - 0.15) / 0.7, 0.0, 1.0)
var hard := 0.0 if power < 0.72 else (power - 0.72) / 0.28
var target := hard * open
var tau := 18.0 if target > _heat else 22.0
_heat = lerpf(_heat, target, 1.0 - exp(-delta / tau))
var glow := 0.0 if _heat < 0.55 else pow((_heat - 0.55) / 0.45, 1.7)
var hot := Color(1.0, 0.35, 0.08)
for mat in _rad_mats:
mat.emission_enabled = glow > 0.02
mat.emission = hot
mat.emission_energy_multiplier = glow * 1.15
for mat in _drive_mats:
mat.emission_enabled = power > 0.02
mat.emission = Color(0.55 * power, 0.72 * power, power)
mat.emission_energy_multiplier = power * 7.0
var spool_target := 1.0 if power > 0.04 else 0.0
_spool = Fx.smooth(_spool, spool_target, delta, 0.26 if spool_target > 0.0 else 0.07)
var plume := power * _spool
if _drive_core:
var scale := _drive_reach_scale()
Fx.set_spear(_drive_core, plume, scale, _time)
Fx.set_spear(_drive_sheath, plume * 0.82, scale, _time)
if _drive_light:
_drive_light.light_energy = plume * 5.0 if plume > 0.04 else 0.0
var reach := float(_drive_core.get_meta("len")) * scale
_drive_light.spot_range = minf(float(_drive_core.get_meta("len")), maxf(2.2, reach))
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for ribbon in _rcs:
var want := 0.0
if thrust != Vector3.ZERO:
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
if exhaust.dot(-thrust) > 0.7:
want = 1.0
var burn := float(ribbon.get_meta("burn"))
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
ribbon.set_meta("burn", burn)
Fx.set_spear(ribbon, burn, 1.0, _time)
_step_pdc(delta)
for cloud in _chaff:
cloud.emitting = chaff_fire
func set_battle_lights(v: bool) -> void:
battle_lights = v
if _cabin_ready:
_apply_cabin()
func _cabin_light(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Nav"):
return false
if nm.begins_with("Light_") or nm == "HelmPracticalLight" or nm.begins_with("CabinLamp") or nm.begins_with("PointLight_") or nm == "CabinFill" or nm == "PanelLight" or nm == "AirlockFlood":
return true
var p := n.get_parent()
while p:
var pn := String(p.name)
if pn == "CompanionwayWalk" or pn == "Lights" or pn == "CabinLights":
return true
if pn.begins_with("Vfx"):
return false
p = p.get_parent()
return false
func _cabin_mesh(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Nav"):
return false
return "Lamp" in nm or nm.begins_with("LightStrip") or nm.begins_with("CabinGlow")
func _collect_cabin() -> void:
_cabin_lights.clear()
_cabin_rest.clear()
_cabin_mats.clear()
for n in find_children("*", "Light3D", true, false):
if not _cabin_light(n):
continue
_cabin_lights.append(n)
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
for n in find_children("*", "MeshInstance3D", true, false):
if not _cabin_mesh(n):
continue
var mat := _override_std(n)
if mat == null:
continue
_cabin_mats.append({"mat": mat, "energy": mat.emission_energy_multiplier, "color": mat.emission, "on": mat.emission_enabled})
_cabin_ready = true
_apply_cabin()
func _apply_cabin() -> void:
for i in _cabin_lights.size():
var lamp := _cabin_lights[i]
var rest: Dictionary = _cabin_rest[i]
if battle_lights:
lamp.light_color = Color(1.0, 0.14, 0.1)
lamp.light_energy = maxf(0.05, float(rest.energy) * 0.2)
else:
lamp.light_color = rest.color
lamp.light_energy = float(rest.energy)
for row in _cabin_mats:
var mat := row.mat as StandardMaterial3D
if battle_lights:
mat.emission_enabled = true
mat.emission = Color(1.0, 0.13, 0.09)
mat.emission_energy_multiplier = maxf(0.28, float(row.energy) * 0.45)
else:
mat.emission_enabled = bool(row.on)
mat.emission = row.color
mat.emission_energy_multiplier = float(row.energy)
func set_gear_deploy(v: float) -> void:
gear_deploy = v
_seek("gear_deploy", v)
func set_radiators_deploy(v: float) -> void:
radiators_deploy = v
_seek("radiators_deploy", v)
func set_hatch_airlock_open(v: float) -> void:
hatch_airlock_open = v
_seek("hatch_airlock_open", v)
func set_hatch_dock_open(v: float) -> void:
hatch_dock_open = v
_seek("hatch_dock_open", v)
func set_hatch_cargo_open(v: float) -> void:
hatch_cargo_open = v
_seek("hatch_cargo_open", v)
func set_hatch_deck_open(v: float) -> void:
hatch_deck_open = v
_seek("hatch_deck_open", v)
func set_torpedo_doors_open(v: float) -> void:
torpedo_doors_open = v
_seek("torpedo_doors_open", v)
func _seek(clip: StringName, t: float) -> void:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap == null:
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
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 _collect_glow() -> void:
for n in find_children("*", "MeshInstance3D", true, false):
var mat := _override_std(n)
if mat == null:
continue
if n.name.begins_with("Radiator_") and ("Fin_" in n.name or "Edge_" in n.name or "Rail" in n.name):
_rad_mats.append(mat)
elif n.name == "DriveGlow" or n.name == "DrivePlasma":
_drive_mats.append(mat)
func _override_std(mesh: MeshInstance3D) -> StandardMaterial3D:
var mat := mesh.get_active_material(0)
if mat == null or not (mat is StandardMaterial3D):
return null
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
mesh.set_surface_override_material(0, copy)
return copy
func _is_rcs_socket(n: Node) -> bool:
var nm := String(n.name)
if nm == "Vfx_DrivePlume" or nm == "Vfx_Chaff" or nm.begins_with("Vfx_Abort"):
return false
return nm.ends_with("_Vfx") or nm.begins_with("Vfx_RCS_")
func _is_muzzle(n: Node) -> bool:
var nm := String(n.name)
return nm.begins_with("PDT_") and nm.ends_with("_Muzzle")
func _arm_fx() -> void:
_tracer_mesh = Fx.tracer_mesh()
_tracer_mat = Fx.tracer_mat()
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
if drive:
_drive_socket = drive
# Ice-white core, 38 m, three crossed planes. Blue sheath is shorter and wider.
_drive_core = Fx.spear(drive, 38.0, 1.35, Color(0.957, 0.973, 1.0, 0.95), Color(0.47, 0.71, 1.0, 0.35), Color(0.24, 0.47, 1.0, 0.0), 0.95, 3, 1.0, "DriveCore")
_drive_sheath = Fx.spear(drive, 26.0, 3.2, Color(0.353, 0.659, 1.0, 0.95), Color(0.17, 0.47, 1.0, 0.35), Color(0.08, 0.22, 0.55, 0.0), 0.5, 3, 2.0, "DriveSheath")
_drive_light = Fx.spot(drive, 0.35, Color(0.773, 0.863, 1.0), 0.3, 7.5, 2.0)
for n in find_children("*", "Node3D", true, false):
if _is_rcs_socket(n):
# 1.2 m pale spear, two crossed planes. One per bell.
_rcs.append(Fx.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, 1.0, "Spear"))
elif _is_muzzle(n):
_muzzles.append(n)
n.set_meta("acc", randf() * 0.02)
elif String(n.name) == "Vfx_Chaff":
_chaff.append(Fx.chaff(n))
func _drive_reach_scale() -> float:
if _drive_socket == null or _drive_core == null:
return 1.0
var length := float(_drive_core.get_meta("len"))
var exhaust: Vector3 = (_drive_socket.global_transform.basis * _drive_core.basis.y).normalized()
if exhaust.y >= -0.35:
return 1.0
# Pad is world y=-2.45. Clip only when the bell is above it.
var room := (_drive_socket.global_position.y - (-2.45 + 0.22)) / -exhaust.y
if room <= 0.0:
return 1.0
var reach := maxf(0.9, minf(length, room))
return reach / length
func _take_tracer() -> MeshInstance3D:
if _tracer_pool.size() > 0:
return _tracer_pool.pop_back()
var m := MeshInstance3D.new()
m.mesh = _tracer_mesh
m.material_override = _tracer_mat
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
m.visible = false
add_child(m)
return m
func _step_pdc(delta: float) -> void:
if pdc_fire:
for mz in _muzzles:
var acc := float(mz.get_meta("acc")) - delta
var shots := 0
while acc <= 0.0 and shots < 2 and _tracers.size() < 90:
acc += 1.0 / 42.0
shots += 1
_spawn_tracer(mz)
mz.set_meta("acc", acc)
for i in range(_tracers.size() - 1, -1, -1):
var t: Dictionary = _tracers[i]
t.life = float(t.life) - delta
var mesh := t.mesh as MeshInstance3D
if float(t.life) <= 0.0:
mesh.visible = false
_tracer_pool.append(mesh)
_tracers.remove_at(i)
continue
var local_dir: Vector3 = t.dir
mesh.position += local_dir * float(t.speed) * delta
func _spawn_tracer(mz: Node3D) -> void:
var mesh := _take_tracer()
var world_dir: Vector3 = (mz.global_transform.basis * Fx.exhaust_local(mz)).normalized()
var shot := Fx.cone(world_dir, 1.15)
var origin := mz.global_position + world_dir * 0.18
var local_dir: Vector3 = global_transform.basis.inverse() * shot
mesh.position = to_local(origin)
mesh.basis = Fx.basis_y(local_dir)
mesh.scale = Vector3(1, randf_range(1.5, 2.3), 1)
mesh.visible = true
_tracers.append({
"mesh": mesh,
"dir": local_dir,
"life": randf_range(0.12, 0.2),
"speed": randf_range(90.0, 140.0),
})
+1
View File
@@ -0,0 +1 @@
uid://xe0h4b0jupg0
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dhoaoukb2mpvm"
path="res://.godot/imported/naf_resolute.glb-d143e75fe1b7e7fa78dade564883f596.scn"
[deps]
source_file="res://naf_resolute.glb"
dest_files=["res://.godot/imported/naf_resolute.glb-d143e75fe1b7e7fa78dade564883f596.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+15
View File
@@ -0,0 +1,15 @@
; Godot 4.7 fleet. Open this folder as a project. Main scene is Hongqi.
; Units are meters. See EXPORT_NOTES.txt.
config_version=5
[application]
config/name="NAF Fleet"
config/description="Hongqi, Resolute, Kestrel, Soyuz-MS, and Phantom II. Textures and 0..1 clips are in the glb files. Drive, RCS, abort, tracers, and chaff spawn at play."
config/features=PackedStringArray("4.7", "Forward Plus")
run/main_scene="res://hongqi.tscn"
[rendering]
renderer/rendering_method="forward_plus"
+24
View File
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://cgevlt7c231mf"]
[ext_resource type="PackedScene" uid="uid://dhoaoukb2mpvm" path="res://naf_resolute.glb" id="1_glb"]
[ext_resource type="Script" uid="uid://xe0h4b0jupg0" path="res://naf_resolute.gd" id="2_gd"]
[sub_resource type="Environment" id="env"]
background_mode = 1
background_color = Color(0.012, 0.014, 0.018, 1)
ambient_light_source = 2
ambient_light_color = Color(0.62, 0.7, 0.8, 1)
ambient_light_energy = 0.2
tonemap_mode = 2
[node name="Resolute" instance=ExtResource("1_glb")]
script = ExtResource("2_gd")
[node name="HangarSky" type="WorldEnvironment" parent="."]
environment = SubResource("env")
[node name="Key" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.8, -0.5, 0.33, 0, 0.55, 0.84, -0.6, -0.67, 0.44, 0, 30, 12)
light_color = Color(0.82, 0.88, 1, 1)
light_energy = 1.15
shadow_enabled = true
+20
View File
@@ -0,0 +1,20 @@
shader_type spatial;
render_mode blend_add, unshaded, cull_disabled, depth_draw_never, fog_disabled;
// UV.y = 0 at the socket, 1 at the tip. UV.x = 0..1 across the ribbon.
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);
}
+1
View File
@@ -0,0 +1 @@
uid://dkfm1ub4lbf33
+171
View File
@@ -0,0 +1,171 @@
extends Node3D
## NAF Phantom II (shrike). Godot 4.7. Units: 1 m.
## Axes: nose +Z, belly -Y, starboard +X. The torch exhausts -Z.
## rcs_direction uses those axes. Nose thrust is +Z. Dorsal thrust is +Y.
## See PARTICLE_FX.md.
const Fx = preload("res://naf_fx.gd")
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
@export_range(0.0, 1.0) var canopy_open := 0.0 : set = set_canopy_open
@export_range(0.0, 1.0) var throttle := 0.0
@export var drive_on := false
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var pdc_fire := false
@export var chaff_fire := false
@export var cabin_dim := false : set = set_cabin_dim
var _time := 0.0
var _spool := 0.0
var _drive_socket: Node3D
var _drive_core: Node3D
var _drive_sheath: Node3D
var _drive_light: SpotLight3D
var _rcs: Array[Node3D] = []
var _muzzles: Array[Node3D] = []
var _tracers: Array[Dictionary] = []
var _tracer_pool: Array[MeshInstance3D] = []
var _tracer_mesh: ArrayMesh
var _tracer_mat: StandardMaterial3D
var _chaff: Array[GPUParticles3D] = []
var _cabin_ready := false
var _cabin_lights: Array[Light3D] = []
var _cabin_rest: Array[Dictionary] = []
const _RCS_THRUST := [
Vector3.ZERO,
Vector3(0, 0, 1),
Vector3(0, 0, -1),
Vector3(1, 0, 0),
Vector3(-1, 0, 0),
Vector3(0, 1, 0),
Vector3(0, -1, 0),
]
func _ready() -> void:
_seek("gear_deploy", gear_deploy)
_arm_fx()
_collect_cabin()
set_physics_process(true)
func _physics_process(delta: float) -> void:
_time += delta
var power := throttle if drive_on else 0.0
var spool_target := 1.0 if power > 0.04 else 0.0
_spool = Fx.smooth(_spool, spool_target, delta, 0.26 if spool_target > 0.0 else 0.07)
var plume := power * _spool
if _drive_core:
Fx.set_spear(_drive_core, plume, 1.0, _time)
Fx.set_spear(_drive_sheath, plume * 0.82, 1.0, _time)
if _drive_light:
_drive_light.light_energy = plume * 5.0
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for ribbon in _rcs:
var want := 0.0
if thrust != Vector3.ZERO:
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
if exhaust.dot(-thrust) > 0.7:
want = 1.0
var burn := float(ribbon.get_meta("burn"))
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
ribbon.set_meta("burn", burn)
Fx.set_spear(ribbon, burn, 1.0, _time)
for cloud in _chaff:
cloud.emitting = chaff_fire
_step_pdc(delta)
func set_cabin_dim(v: bool) -> void:
cabin_dim = v
if _cabin_ready:
for i in _cabin_lights.size():
var lamp := _cabin_lights[i]
var rest: Dictionary = _cabin_rest[i]
lamp.light_energy = float(rest.energy) * (0.16 if cabin_dim else 1.0)
lamp.light_color = rest.color
func _collect_cabin() -> void:
for n in find_children("*", "Light3D", true, false):
var nm := String(n.name)
if nm.begins_with("PointLight_") or nm.begins_with("CabinLamp") or nm == "CabinFill":
_cabin_lights.append(n)
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
_cabin_ready = true
func set_gear_deploy(v: float) -> void:
gear_deploy = v
_seek("gear_deploy", v)
func set_canopy_open(v: float) -> void:
canopy_open = v
_seek("canopy_open", v)
func _seek(clip: StringName, t: float) -> void:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap == null:
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
if ap == null or not ap.has_animation(clip):
return
ap.play(clip)
ap.seek(clampf(t, 0.0, 1.0) * ap.get_animation(clip).length, true)
ap.pause()
func _arm_fx() -> void:
_tracer_mesh = Fx.tracer_mesh()
_tracer_mat = Fx.tracer_mat()
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
if drive:
_drive_socket = drive
_drive_core = Fx.spear(drive, 22.0, 0.68, Color(0.957, 0.973, 1.0, 0.95), Color(0.47, 0.71, 1.0, 0.35), Color(0.24, 0.47, 1.0, 0.0), 0.92, 3, 1.0, "DriveCore")
_drive_sheath = Fx.spear(drive, 14.0, 1.7, Color(0.353, 0.659, 1.0, 0.9), Color(0.17, 0.47, 1.0, 0.35), Color(0.08, 0.22, 0.55, 0.0), 0.5, 3, 2.0, "DriveSheath")
_drive_light = Fx.spot(drive, 0.3, Color(0.773, 0.863, 1.0), 0.35, 7.5, 2.0)
for n in find_children("*", "Node3D", true, false):
var nm := String(n.name)
if nm.begins_with("Vfx_RCS_"):
_rcs.append(Fx.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, 1.0, "Spear"))
elif nm.begins_with("PDT_") and nm.ends_with("_Muzzle"):
_muzzles.append(n)
n.set_meta("acc", randf() * 0.02)
elif nm == "Vfx_Chaff":
_chaff.append(Fx.chaff(n))
func _step_pdc(delta: float) -> void:
if pdc_fire:
for mz in _muzzles:
var acc := float(mz.get_meta("acc")) - delta
var shots := 0
while acc <= 0.0 and shots < 2 and _tracers.size() < 40:
acc += 1.0 / 42.0
shots += 1
var mesh := _take_tracer()
var world_dir: Vector3 = (mz.global_transform.basis * Fx.exhaust_local(mz)).normalized()
var shot := Fx.cone(world_dir, 1.15)
var origin := mz.global_position + world_dir * 0.18
var local_dir: Vector3 = global_transform.basis.inverse() * shot
mesh.position = to_local(origin)
mesh.basis = Fx.basis_y(local_dir)
mesh.scale = Vector3(1, randf_range(1.5, 2.3), 1)
mesh.visible = true
_tracers.append({"mesh": mesh, "dir": local_dir, "life": randf_range(0.12, 0.2), "speed": randf_range(90.0, 140.0)})
mz.set_meta("acc", acc)
for i in range(_tracers.size() - 1, -1, -1):
var t: Dictionary = _tracers[i]
t.life = float(t.life) - delta
var mesh := t.mesh as MeshInstance3D
if float(t.life) <= 0.0:
mesh.visible = false
_tracer_pool.append(mesh)
_tracers.remove_at(i)
continue
mesh.position += (t.dir as Vector3) * float(t.speed) * delta
func _take_tracer() -> MeshInstance3D:
if _tracer_pool.size() > 0:
return _tracer_pool.pop_back()
var m := MeshInstance3D.new()
m.mesh = _tracer_mesh
m.material_override = _tracer_mat
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
m.visible = false
add_child(m)
return m
+1
View File
@@ -0,0 +1 @@
uid://cxr0um6cwvfkv
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://crwynydci5r5y"
path="res://.godot/imported/shrike.glb-afe83605e4be6db26bf4e422fef66872.scn"
[deps]
source_file="res://shrike.glb"
dest_files=["res://.godot/imported/shrike.glb-afe83605e4be6db26bf4e422fef66872.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+24
View File
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://b228nn7odpif5"]
[ext_resource type="PackedScene" uid="uid://crwynydci5r5y" path="res://shrike.glb" id="1_glb"]
[ext_resource type="Script" uid="uid://cxr0um6cwvfkv" path="res://shrike.gd" id="2_gd"]
[sub_resource type="Environment" id="env"]
background_mode = 1
background_color = Color(0.012, 0.014, 0.018, 1)
ambient_light_source = 2
ambient_light_color = Color(0.62, 0.7, 0.8, 1)
ambient_light_energy = 0.2
tonemap_mode = 2
[node name="Shrike" instance=ExtResource("1_glb")]
script = ExtResource("2_gd")
[node name="HangarSky" type="WorldEnvironment" parent="."]
environment = SubResource("env")
[node name="Key" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.8, -0.5, 0.33, 0, 0.55, 0.84, -0.6, -0.67, 0.44, 6, 8, 4)
light_color = Color(0.82, 0.88, 1, 1)
light_energy = 1.15
shadow_enabled = true
+154
View File
@@ -0,0 +1,154 @@
extends Node3D
## Soyuz-MS. Godot 4.7. Units: 1 m.
## Axes: dock / forward +Z, dorsal +Y, starboard +X. The service engine exhausts -Z.
## rcs_direction uses those axes. Nose thrust is +Z, not +Y.
## Drive is a chemical torch (short, warm). Abort motors are the same ribbons as Kestrel.
## See PARTICLE_FX.md.
const Fx = preload("res://naf_fx.gd")
@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_transfer_open := 0.0 : set = set_hatch_transfer_open
@export_range(0.0, 1.0) var hatch_dock_open := 0.0 : set = set_hatch_dock_open
@export_range(0.0, 1.0) var throttle := 0.0
@export var drive_on := false
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var abort_burn := false
var _time := 0.0
var _spool := 0.0
var _abort_hold := 0.0
var _drive_socket: Node3D
var _drive_core: Node3D
var _drive_sheath: Node3D
var _drive_light: SpotLight3D
var _rcs: Array[Node3D] = []
var _aborts: Array[Dictionary] = []
const _RCS_THRUST := [
Vector3.ZERO,
Vector3(0, 0, 1),
Vector3(0, 0, -1),
Vector3(1, 0, 0),
Vector3(-1, 0, 0),
Vector3(0, 1, 0),
Vector3(0, -1, 0),
]
func _ready() -> void:
_arm_fx()
set_physics_process(true)
func _physics_process(delta: float) -> void:
_time += delta
var power := throttle if drive_on else 0.0
var spool_target := 1.0 if power > 0.04 else 0.0
_spool = Fx.smooth(_spool, spool_target, delta, 0.2 if spool_target > 0.0 else 0.12)
var plume := power * _spool
if _drive_core:
Fx.set_spear(_drive_core, plume, 1.0, _time)
Fx.set_spear(_drive_sheath, plume * 0.82, 1.0, _time)
if _drive_light:
_drive_light.light_energy = plume * 6.0
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for ribbon in _rcs:
var want := 0.0
if thrust != Vector3.ZERO:
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
if exhaust.dot(-thrust) > 0.7:
want = 1.0
var burn := float(ribbon.get_meta("burn"))
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
ribbon.set_meta("burn", burn)
Fx.set_spear(ribbon, burn, 1.0, _time)
if abort_burn:
_abort_hold = minf(1.0, _abort_hold + delta / 0.22)
elif _abort_hold > 0.0:
_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
Fx.set_opacity(core, _abort_hold * 0.95 * flick if on else 0.0)
Fx.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 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 set_hatch_crew_open(v: float) -> void:
hatch_crew_open = v
_seek("hatch_crew_open", v)
func set_hatch_transfer_open(v: float) -> void:
hatch_transfer_open = v
_seek("hatch_transfer_open", v)
func set_hatch_dock_open(v: float) -> void:
hatch_dock_open = v
_seek("hatch_dock_open", v)
func _seek(clip: StringName, t: float) -> void:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap == null:
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
if ap == null or not ap.has_animation(clip):
return
ap.play(clip)
ap.seek(clampf(t, 0.0, 1.0) * ap.get_animation(clip).length, true)
ap.pause()
func _arm_fx() -> void:
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
if drive:
_drive_socket = drive
_drive_core = Fx.spear(drive, 7.0, 0.5, Color(1.0, 0.953, 0.831, 0.95), Color(1.0, 0.72, 0.4, 0.4), Color(1.0, 0.45, 0.1, 0.0), 0.92, 4, 1.0, "DriveCore")
_drive_sheath = Fx.spear(drive, 5.0, 1.25, Color(0.91, 0.353, 0.07, 0.9), Color(0.7, 0.2, 0.02, 0.35), Color(0.3, 0.05, 0.0, 0.0), 0.6, 3, 2.0, "DriveSheath")
_drive_light = Fx.spot(drive, 0.2, Color(1.0, 0.627, 0.29), 0.5, 6.0, 2.0)
for n in find_children("*", "Node3D", true, false):
var nm := String(n.name)
if nm.begins_with("Vfx_RCS_"):
_rcs.append(Fx.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, 1.0, "Spear"))
elif nm.begins_with("Vfx_Abort_"):
_aborts.append(_make_abort(n))
func _make_abort(socket: Node3D) -> Dictionary:
var core := Fx.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 := Fx.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")
var sparks := Fx.sparks(socket, "AbortCoreSparks", 10, 0.15, 18.0, 28.0, 1.6, 0.05, 6.0)
var sheath_sparks := Fx.sparks(socket, "AbortSheathSparks", 18, 0.26, 14.0, 22.0, 4.5, 0.09, 8.0)
for p in [sparks, sheath_sparks]:
var proc := p.process_material as ParticleProcessMaterial
if proc:
proc.color_ramp = _abort_ramp()
var flash := Fx.abort_flash(socket)
var light := Fx.spot(socket, 0.08, Color(1.0, 0.416, 0.157), 0.5, 6.5, 1.8)
light.light_energy = 0.0
return {"core": core, "sheath": sheath, "sparks": sparks, "sheath_sparks": sheath_sparks, "flash": flash, "light": light}
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
+1
View File
@@ -0,0 +1 @@
uid://c8ywu0poa4ix8
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://1v5lob6vv8do"
path="res://.godot/imported/soyuz.glb-570645bd68031fcc55fb6f400b810e4d.scn"
[deps]
source_file="res://soyuz.glb"
dest_files=["res://.godot/imported/soyuz.glb-570645bd68031fcc55fb6f400b810e4d.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+24
View File
@@ -0,0 +1,24 @@
[gd_scene format=3 uid="uid://cy1epmtsn7v6l"]
[ext_resource type="PackedScene" uid="uid://1v5lob6vv8do" path="res://soyuz.glb" id="1_glb"]
[ext_resource type="Script" uid="uid://c8ywu0poa4ix8" path="res://soyuz.gd" id="2_gd"]
[sub_resource type="Environment" id="env"]
background_mode = 1
background_color = Color(0.012, 0.014, 0.018, 1)
ambient_light_source = 2
ambient_light_color = Color(0.62, 0.7, 0.8, 1)
ambient_light_energy = 0.2
tonemap_mode = 2
[node name="Soyuz" instance=ExtResource("1_glb")]
script = ExtResource("2_gd")
[node name="HangarSky" type="WorldEnvironment" parent="."]
environment = SubResource("env")
[node name="Key" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.8, -0.5, 0.33, 0, 0.55, 0.84, -0.6, -0.67, 0.44, 8, 6, 10)
light_color = Color(0.9, 0.86, 0.78, 1)
light_energy = 1.2
shadow_enabled = true