# EffectPlayer (P5) —— 解析后的 `.mse` spec → Godot 节点树(GPUParticles3D 为主)。 # # var fx := preload("res://fx/effect_player.gd").new() # fx.build(spec, assets_root) # spec 来自 fx/mse.gd # add_child(fx) # 挂到要出特效的位置(可 reparent 到骨骼) # fx.play() # 或 fx.play(true) 一次性 # # 覆盖 Particle 组:发射形状 / 速率 / 寿命 / 方向 / 重力 / 缩放曲线 / 颜色渐变 / # billboard / 加法混合 / 旋转。纹理(.dds)暂用程序化径向渐变代替(GDScript 不解 DDS)。 # Mesh 组:占位(MeshFileName 是 .mde,另需解码器)。SimpleLight:本资产集未用。 extends Node3D var spec := {} var assets_root := "" var one_shot := false var _emitters: Array[GPUParticles3D] = [] var _mesh_nodes: Array[Node3D] = [] var _bsphere_r := 0.0 static var _glow_tex: Texture2D func build(mse_spec: Dictionary, assets := "") -> void: spec = mse_spec assets_root = assets _bsphere_r = float(spec.get("bsphere_r", 0.0)) for p in spec.get("particles", []): var e := _build_particle(p) if e: add_child(e) _emitters.append(e) for m in spec.get("meshes", []): var mn := _build_mesh(m) if mn: add_child(mn) _mesh_nodes.append(mn) func play(force_one_shot := false) -> void: for i in _emitters.size(): var e := _emitters[i] var st := float(_emitters[i].get_meta("start_time", 0.0)) if force_one_shot: e.one_shot = true var tree := get_tree() if st <= 0.0 or tree == null: e.restart() e.emitting = true else: tree.create_timer(st).timeout.connect(func(): if is_instance_valid(e): e.restart() e.emitting = true) var tr := get_tree() if (force_one_shot or one_shot) and tr: var total := _longest_life() + 0.5 tr.create_timer(maxf(total, 1.5)).timeout.connect(queue_free) func stop() -> void: for e in _emitters: e.emitting = false func _longest_life() -> float: var m := 1.0 for e in _emitters: m = maxf(m, e.lifetime + float(e.get_meta("start_time", 0.0))) return m # --- particle --------------------------------------------------------- func _build_particle(p: Dictionary) -> GPUParticles3D: var emit: Dictionary = p.get("emitter", {}) var prop: Dictionary = p.get("particle", {}) var g := GPUParticles3D.new() g.set_meta("start_time", float(p.get("start_time", 0.0))) g.amount = maxi(1, int(_n(emit.get("MaxEmissionCount", 16)))) g.explosiveness = 0.0 var life := _last_val(emit.get("TimeEventLifeTime", []), 1.0) g.lifetime = clampf(life, 0.05, 12.0) var cycle := _n(emit.get("CycleLength", 0.0)) var loop := int(_n(emit.get("CycleLoopEnable", 0))) g.one_shot = (loop == 0) if cycle > 0.0 and loop != 0: g.lifetime = maxf(g.lifetime, cycle) var pm := ParticleProcessMaterial.new() # 发射形状 var shape := int(_n(emit.get("EmitterShape", 0))) if shape == 3: pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE pm.emission_sphere_radius = maxf(0.01, _n(emit.get("EmittingRadius", 10.0)) * 0.01) else: pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_POINT # 方向 / 速度 var vel := _last_val(emit.get("TimeEventEmittingVelocity", []), 0.0) * 0.01 pm.direction = Vector3(0, 1, 0) pm.spread = 25.0 pm.initial_velocity_min = vel * 0.6 pm.initial_velocity_max = maxf(vel, 0.05) # 重力 var grav := _last_val(p.get("particle", {}).get("TimeEventGravity", []), 0.0) pm.gravity = Vector3(0, -grav * 0.01, 0) # 大小 var sx := _last_val(emit.get("TimeEventSizeX", []), 32.0) * 0.01 pm.scale_min = maxf(0.02, sx * 0.7) pm.scale_max = maxf(0.03, sx) pm.scale_curve = _curve_tex(prop.get("TimeEventScaleX", [[0, 1], [1, 0]])) # 颜色渐变(RGB 曲线各取末值 + Alpha 曲线) pm.color = _rgb(prop) var ramp := _alpha_ramp(prop.get("TimeEventAlpha", []), pm.color) if ramp: pm.color_ramp = ramp # 旋转 var rspeed := _n(prop.get("RotationSpeed", 0.0)) if rspeed != 0.0: pm.angular_velocity_min = deg_to_rad(rspeed) * 0.3 pm.angular_velocity_max = deg_to_rad(rspeed) g.process_material = pm # 绘制网格 + 材质(billboard + 混合) var qm := QuadMesh.new() qm.size = Vector2(1, 1) g.draw_pass_1 = qm var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.billboard_mode = _billboard(int(_n(prop.get("BillboardType", 1)))) mat.billboard_keep_scale = true mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.blend_mode = _blend(int(_n(prop.get("SrcBlendType", 5))), int(_n(prop.get("DestBlendType", 2)))) mat.albedo_texture = _glow() mat.vertex_color_use_as_albedo = true g.material_override = mat return g func _build_mesh(m: Dictionary) -> Node3D: # .mde 需要专门解码器;先占位一个小 box,位置对 var n := Node3D.new() n.name = "mesh_" + String(m.get("mesh_file", "?")).get_basename() var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(0.1, 0.1, 0.4) mi.mesh = bm var mat := StandardMaterial3D.new() mat.albedo_color = Color(1, 0.9, 0.5, 0.6) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mi.material_override = mat n.add_child(mi) return n # --- helpers --------------------------------------------------------- func _n(v) -> float: if v is Array: return float(v[0]) if v.size() > 0 else 0.0 if v is float or v is int: return float(v) return 0.0 # List 表末行的值(列 idx,默认 col 1 = 时间后第一个数) func _last_val(rows, def: float, col := 1) -> float: if rows is Array and rows.size() > 0: var r = rows[-1] if r is Array and r.size() > col and (r[col] is float or r[col] is int): return float(r[col]) return def func _rgb(prop: Dictionary) -> Color: return Color( _last_val(prop.get("TimeEventColorRed", []), 1.0), _last_val(prop.get("TimeEventColorGreen", []), 1.0), _last_val(prop.get("TimeEventColorBlue", []), 1.0), 1.0) # TimeEventScaleX rows [[t,v],...] -> CurveTexture func _curve_tex(rows) -> CurveTexture: var c := Curve.new() c.min_value = 0.0 c.max_value = 2.0 if rows is Array and rows.size() > 0: for r in rows: if r is Array and r.size() >= 2: c.add_point(Vector2(clampf(float(r[0]), 0, 1), float(r[1]))) else: c.add_point(Vector2(0, 1)) c.add_point(Vector2(1, 0)) var t := CurveTexture.new() t.curve = c return t func _alpha_ramp(rows, base: Color) -> GradientTexture1D: if not (rows is Array) or rows.size() < 1: return null var grad := Gradient.new() grad.offsets = PackedFloat32Array() grad.colors = PackedColorArray() var pts := [] for r in rows: if r is Array and r.size() >= 2: pts.append([clampf(float(r[0]), 0, 1), float(r[1])]) if pts.is_empty(): return null pts.sort_custom(func(a, b): return a[0] < b[0]) if pts[0][0] > 0.0: pts.push_front([0.0, pts[0][1]]) if pts[-1][0] < 1.0: pts.append([1.0, pts[-1][1]]) for pt in pts: grad.add_point(pt[0], Color(base.r, base.g, base.b, clampf(pt[1], 0, 1))) grad.remove_point(0) grad.remove_point(0) var gt := GradientTexture1D.new() gt.gradient = grad return gt func _billboard(bt: int) -> int: # 0 = none/local, 1 = 面向相机, 4 = Y 轴 if bt == 4: return BaseMaterial3D.BILLBOARD_FIXED_Y if bt == 0: return BaseMaterial3D.BILLBOARD_DISABLED return BaseMaterial3D.BILLBOARD_ENABLED func _blend(src: int, dst: int) -> int: # D3D blend: 5=SRCALPHA 2=ONE 4=INVSRCALPHA 3=SRCCOLOR if dst == 2: return BaseMaterial3D.BLEND_MODE_ADD if dst == 4: return BaseMaterial3D.BLEND_MODE_MIX return BaseMaterial3D.BLEND_MODE_ADD # 程序化径向渐变(代替 .dds 粒子纹理) static func _glow() -> Texture2D: if _glow_tex == null: var s := 48 var img := Image.create(s, s, false, Image.FORMAT_RGBA8) for y in s: for x in s: var d := Vector2(x - s / 2.0, y - s / 2.0).length() / (s / 2.0) var a := clampf(1.0 - d, 0.0, 1.0) a = a * a img.set_pixel(x, y, Color(1, 1, 1, a)) _glow_tex = ImageTexture.create_from_image(img) return _glow_tex