9a8a946931
arcology updated ship models in particle fx is broken on kestrel
320 lines
11 KiB
GDScript
320 lines
11 KiB
GDScript
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
|