# WeaponTrace —— 40250 GameLib/WeaponTrace.cpp 的 Godot 适配层。 # # 40250 的刀光不是一次性 MSE 特效:AttachWeapon 为可追踪武器创建对象, # ActorInstance 每帧采样武器实例的刀根/刀尖世界坐标,RenderTrace 再把 # short/long 两条时间轨迹用 cubic spline 拼成 triangle strip。这里保留同一 # 状态边界和参数;渲染坐标在世界空间采样,在当前 PlayerView 局部渲染, # 因此角色移动时已经生成的尾迹不会被父节点位移二次带走。 extends Node3D const DEFAULT_LIFETIME := 0.18 const DEFAULT_SAMPLING_TIME := 0.003 const TRACE_COLOR := Color(0.3, 0.8, 1.0, 1.0) var lifetime := DEFAULT_LIFETIME var sampling_time := DEFAULT_SAMPLING_TIME var reach_scale := 1.0 # CActorInstance::__GetReachScale() var use_texture := false var texture_path := "" var _weapon: Node3D var _playing := false var _has_geometry := false var _tip_local := Vector3.ZERO var _short_points: Array = [] # newest first:刀根 var _long_points: Array = [] # newest first:刀尖 var _vertex_count := 0 var _mesh := ImmediateMesh.new() var _mesh_instance := MeshInstance3D.new() var _material := StandardMaterial3D.new() func _ready() -> void: _ensure_render_node() func _ensure_render_node() -> void: if _mesh_instance.get_parent() == null: _mesh_instance.name = "WeaponTraceMesh" _mesh_instance.mesh = _mesh _mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(_mesh_instance) _material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED _material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA _material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX _material.cull_mode = BaseMaterial3D.CULL_DISABLED _material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED _material.vertex_color_use_as_albedo = true _mesh_instance.material_override = _material _mesh_instance.visible = false func attach_to_weapon(weapon_node: Node3D) -> bool: if _weapon == weapon_node and is_instance_valid(_weapon): return _has_geometry turn_off() clear() _weapon = weapon_node _has_geometry = false _tip_local = Vector3.ZERO if not is_instance_valid(_weapon): return false _has_geometry = _resolve_tip_local() return _has_geometry func detach_weapon() -> void: turn_off() clear() _weapon = null _has_geometry = false _tip_local = Vector3.ZERO func turn_on() -> void: # 40250 TurnOn only changes the playing flag; the existing tail remains and # naturally fades, so combo segments do not pop the trace between swings. _playing = true func turn_off() -> void: # 40250 TurnOff deliberately does not Clear(): old samples remain visible # until lifetime expiry. Equipment replacement uses detach_weapon(), which # is the DestroyWeaponTrace/Clear path instead. _playing = false func set_lifetime(value: float) -> void: lifetime = maxf(value, 0.001) _trim_expired() func set_sampling_time(value: float) -> void: sampling_time = maxf(value, 0.0001) func set_reach_scale(value: float) -> void: # 40250 applies this scale when extending the trace from the bone matrix; # recompute the cached local endpoint so an actor-scale change takes effect # before the next sample. reach_scale = maxf(value, 0.0) if is_instance_valid(_weapon): _has_geometry = _resolve_tip_local() func set_texture(path: String) -> void: texture_path = path use_texture = path != "" _material.albedo_texture = _load_texture(path) if use_texture else null func use_alpha() -> void: use_texture = false _material.albedo_texture = null func use_texture_mode() -> void: use_texture = true _material.albedo_texture = _load_texture(texture_path) if texture_path != "" else null func is_playing() -> bool: return _playing func debug_state() -> Dictionary: return { "playing": _playing, "has_geometry": _has_geometry, "short_points": _short_points.size(), "long_points": _long_points.size(), "vertex_count": _vertex_count, "lifetime": lifetime, "sampling_time": sampling_time, "reach_scale": reach_scale, "tip_local": _tip_local, "tip_length": _tip_local.length(), "use_texture": use_texture, } func _process(delta: float) -> void: _ensure_render_node() var elapsed := maxf(delta, 0.0) _age_points(elapsed) if _playing and _has_geometry and is_instance_valid(_weapon): _sample_weapon() _render_trace() func _sample_weapon() -> void: var root_world := _weapon.global_transform.origin var tip_world := _weapon.global_transform * (_tip_local) _short_points.push_front({"age": 0.0, "pos": root_world}) _long_points.push_front({"age": 0.0, "pos": tip_world}) _trim_expired() func _age_points(delta: float) -> void: for sample in _short_points: sample["age"] = float(sample.get("age", 0.0)) + delta for sample in _long_points: sample["age"] = float(sample.get("age", 0.0)) + delta _trim_expired() func _trim_expired() -> void: while not _short_points.is_empty() and float(_short_points.back().get("age", 0.0)) > lifetime: _short_points.pop_back() while not _long_points.is_empty() and float(_long_points.back().get("age", 0.0)) > lifetime: _long_points.pop_back() func clear() -> void: _short_points.clear() _long_points.clear() _vertex_count = 0 if is_instance_valid(_mesh): _mesh.clear_surfaces() if is_instance_valid(_mesh_instance): _mesh_instance.visible = false func _resolve_tip_local() -> bool: # CWeaponTrace::SetWeaponInstance computes m_fLength from the maximum # distance between the weapon bone origin and GetBoundBox() corners. It does # not select that corner as the direction of the blade. Update() then moves # along the composite bone's local Z axis by m_fLength * reach_scale. var inverse := _weapon.global_transform.affine_inverse() var farthest := 0.0 var meshes: Array[MeshInstance3D] = [] if _weapon is MeshInstance3D: meshes.append(_weapon as MeshInstance3D) for child in _weapon.find_children("*", "MeshInstance3D", true, false): var mi := child as MeshInstance3D if mi != null and not meshes.has(mi): meshes.append(mi) for mi in meshes: if mi.mesh == null: continue var box := mi.get_aabb() for corner in 8: var local_point := box.position + Vector3( box.size.x if (corner & 1) != 0 else 0.0, box.size.y if (corner & 2) != 0 else 0.0, box.size.z if (corner & 4) != 0 else 0.0) var world_point: Vector3 = mi.global_transform * local_point var weapon_local: Vector3 = inverse * world_point var distance := weapon_local.length_squared() if distance > farthest: farthest = distance if farthest <= 0.000001 or is_zero_approx(reach_scale): return false _tip_local = Vector3(0.0, 0.0, sqrt(farthest) * reach_scale) return _tip_local.length_squared() > 0.000001 func _render_trace() -> void: if _short_points.size() <= 1 or _long_points.size() <= 1: _vertex_count = 0 _mesh.clear_surfaces() _mesh_instance.visible = false return var vertices := _build_vertices() _vertex_count = vertices.size() _mesh.clear_surfaces() if vertices.size() < 4: _mesh_instance.visible = false return _mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, _material) for vertex in vertices: _mesh.surface_set_color(Color(TRACE_COLOR.r, TRACE_COLOR.g, TRACE_COLOR.b, float(vertex.get("alpha", 0.0)))) _mesh.surface_set_uv(vertex.get("uv", Vector2.ZERO)) _mesh.surface_add_vertex(to_local(vertex.get("pos", Vector3.ZERO))) _mesh.surface_end() _mesh_instance.visible = true func _build_vertices() -> Array: var length := minf(lifetime, float(_long_points.back().get("age", 0.0))) var out: Array = [] # Reference BuildVertex renders the long track first (UV y=0), then the # short track (UV y=1), interleaving the pair into a triangle strip. var long_vertices := _build_spline(_long_points, true, length) var short_vertices := _build_spline(_short_points, false, length) var count := mini(long_vertices.size(), short_vertices.size()) for i in count: out.append(long_vertices[i]) out.append(short_vertices[i]) return out func _build_spline(input: Array, is_long: bool, length: float) -> Array: var out: Array = [] var n := input.size() - 1 if n <= 0: return out var h: Array = [] var r: Array = [] h.resize(n + 1) r.resize(n + 1) for i in n: var dt := float(input[i + 1].get("age", 0.0)) - float(input[i].get("age", 0.0)) if dt <= 0.000001: return out h[i] = dt var p0: Vector3 = input[i].get("pos", Vector3.ZERO) var p1: Vector3 = input[i + 1].get("pos", Vector3.ZERO) r[i] = (p1 - p0) * (3.0 / dt) r[n] = Vector3.ZERO for i in range(n, 0, -1): r[i] = r[i] + r[i - 1] var stack: Array = [] var rate := 0.5 r[0] = r[0] * 0.5 stack.append(rate) for i in range(1, n): r[i] = r[i] - r[i - 1] rate = 1.0 / (4.0 - rate) r[i] = r[i] * rate stack.append(rate) r[n] = r[n] - r[n - 1] rate = 1.0 / (2.0 - rate) r[n] = r[n] * rate for i in range(n - 1, -1, -1): r[i] = r[i] - float(stack.pop_back()) * r[i + 1] var base := 0 var timebase := 0.0 var timenext := float(h[0]) var delta := maxf(sampling_time, 0.0001) var a: Vector3 = input[0].get("pos", Vector3.ZERO) var b: Vector3 = r[0] var first_delta: Vector3 = input[1].get("pos", Vector3.ZERO) - a var c: Vector3 = (3.0 * first_delta - r[1] * float(h[0]) - 2.0 * float(h[0]) * r[0]) \ / (float(h[0]) * float(h[0])) var d: Vector3 = (-2.0 * first_delta + (r[1] + r[0]) * float(h[0])) \ / (float(h[0]) * float(h[0]) * float(h[0])) var t := 0.0 var life := maxf(lifetime, 0.001) while t <= length + 0.000001: while t > timenext and base < n: timebase = timenext base += 1 if base >= n: break var seg_delta: Vector3 = input[base + 1].get("pos", Vector3.ZERO) - input[base].get("pos", Vector3.ZERO) a = input[base].get("pos", Vector3.ZERO) b = r[base] c = (3.0 * seg_delta - r[base + 1] * float(h[base]) - 2.0 * float(h[base]) * r[base]) / (float(h[base]) * float(h[base])) d = (-2.0 * seg_delta + (r[base + 1] + r[base]) * float(h[base])) / \ (float(h[base]) * float(h[base]) * float(h[base])) timenext += float(h[base]) if base >= n: break var cc := t - timebase var position: Vector3 = a + cc * (b + cc * (c + cc * d)) var ttt := clampf((t + float(input[0].get("age", 0.0))) / life, 0.0, 1.0) var alpha := clampf((1.0 - ttt) * (1.0 - ttt) / 2.5 - 0.1, 0.0, 1.0) if is_long else 0.0 out.append({"pos": position, "uv": Vector2(t / life, 0.0 if is_long else 1.0), "alpha": alpha}) t += delta return out func _load_texture(path: String) -> Texture2D: if path == "": return null if ResourceLoader.exists(path): return ResourceLoader.load(path) as Texture2D if not FileAccess.file_exists(path): return null var image := Image.load_from_file(path) return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null