mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
perf(ship): merge non-tinted hull meshes into one node
Hull/Canopy/EngineGlowL/EngineGlowR are runtime-baked into one ArrayMesh in _ready via SurfaceTool.append_from, dropping 4 MeshInstance3D children to 1 (Nose/TailFin stay separate, they're retinted per-team). Skipped in headless mode like the goal/arena_boundary visual builds, since physics only cares about CollisionShape3D. All 6 source surfaces (hull, canopy, engine_l x2, engine_r x2) carry distinct materials, so this doesn't literally cut draw calls 6 to 3 as TODO.md assumed — Godot still issues one draw call per surface regardless of node count. The real win is scene-tree/transform overhead, not batching.
This commit is contained in:
+57
-3
@@ -32,6 +32,32 @@ const TEAM_COLORS := {
|
||||
1: Color(1.0, 0.5, 0.15),
|
||||
}
|
||||
|
||||
# Non-tinted hull meshes, runtime-merged into one ArrayMesh by
|
||||
# _build_merged_hull() (Nose/TailFin stay separate MeshInstance3Ds since
|
||||
# _apply_team_color() retints them per-team and must keep addressing them by
|
||||
# name). Verified via get_surface_count()/surface_get_material() before
|
||||
# writing this: hull and canopy are each a single surface with their own
|
||||
# distinct opaque StandardMaterial3D (canopy is NOT alpha/transparent despite
|
||||
# the name), and engine_l/engine_r are each 2 surfaces, also all distinct
|
||||
# materials — none of the 6 source surfaces share a material with any other,
|
||||
# including the L/R engine pair. So this merge does not collapse draw calls
|
||||
# the way TODO.md's "6 draw calls down to 3" assumed (Godot still issues one
|
||||
# draw call per surface regardless of how many MeshInstance3Ds they're spread
|
||||
# across); the real win is scene-tree node count, 4 MeshInstance3D children
|
||||
# down to 1, cutting per-frame transform/visibility overhead.
|
||||
const MERGED_MESH_PATHS := [
|
||||
"res://assets/models/ship_hull.res",
|
||||
"res://assets/models/ship_canopy.res",
|
||||
"res://assets/models/ship_engine_l.res",
|
||||
"res://assets/models/ship_engine_r.res",
|
||||
]
|
||||
const MERGED_MESH_TRANSFORMS := [
|
||||
Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0, 0, 0)), # Hull
|
||||
Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0, 0.31, -0.55)), # Canopy
|
||||
Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(-0.42, -0.05, 0.95)), # EngineGlowL
|
||||
Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0.42, -0.05, 0.95)), # EngineGlowR
|
||||
]
|
||||
|
||||
# Which team this ship plays for (0 or 1). Set by the game mode on spawn.
|
||||
var team: int = 0:
|
||||
set(value):
|
||||
@@ -103,11 +129,13 @@ func _ready():
|
||||
|
||||
_boundary = get_tree().get_first_node_in_group("arena_boundary")
|
||||
|
||||
# Telemetry emission is HUD-only work; headless (RL/CI) instances never
|
||||
# have one, so skip the per-tick cost entirely rather than relying on
|
||||
# the listener check below to no-op every frame.
|
||||
# Telemetry emission and the merged-hull visual mesh are both render-only
|
||||
# work; headless (RL/CI) instances never render or have a HUD watching
|
||||
# them, so skip both rather than relying on later no-ops.
|
||||
if DisplayServer.get_name() == "headless":
|
||||
set_physics_process(false)
|
||||
else:
|
||||
_build_merged_hull()
|
||||
|
||||
|
||||
func _apply_team_color() -> void:
|
||||
@@ -120,6 +148,32 @@ func _apply_team_color() -> void:
|
||||
mesh.material_override = accent
|
||||
|
||||
|
||||
# Runtime-bakes Hull/Canopy/EngineGlowL/EngineGlowR (see MERGED_MESH_PATHS
|
||||
# comment above) into one ArrayMesh, one destination surface per source
|
||||
# surface via SurfaceTool.append_from, each keeping its own original
|
||||
# material — preserves current visuals exactly regardless of surface count.
|
||||
# Mirrors the runtime-bake pattern already used by goal.gd/arena_boundary.gd.
|
||||
# Skipped in headless mode (see _ready): purely visual, costs nothing
|
||||
# physics/RL cares about.
|
||||
func _build_merged_hull() -> void:
|
||||
var mesh := ArrayMesh.new()
|
||||
var dest_idx := 0
|
||||
for i in MERGED_MESH_PATHS.size():
|
||||
var src: ArrayMesh = load(MERGED_MESH_PATHS[i])
|
||||
var xform: Transform3D = MERGED_MESH_TRANSFORMS[i]
|
||||
for surf in src.get_surface_count():
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st.append_from(src, surf, xform)
|
||||
st.commit(mesh)
|
||||
mesh.surface_set_material(dest_idx, src.surface_get_material(surf))
|
||||
dest_idx += 1
|
||||
var instance := MeshInstance3D.new()
|
||||
instance.name = "MergedHull"
|
||||
instance.mesh = mesh
|
||||
add_child(instance)
|
||||
|
||||
|
||||
# Attach the node that drives this ship (player, AI, or network). Replaces
|
||||
# any existing controller; parents the new one under the ship if needed.
|
||||
func set_controller(new_controller: ShipController) -> void:
|
||||
|
||||
Reference in New Issue
Block a user