mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +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:
@@ -1,12 +1,8 @@
|
||||
[gd_scene load_steps=13 format=3 uid="uid://p07epxnh8wwp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_hull.res" id="2_hull"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_nose.res" id="3_nose"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_canopy.res" id="4_canopy"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_tailfin.res" id="5_talfin"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_engine_l.res" id="6_enginel"]
|
||||
[ext_resource type="ArrayMesh" path="res://assets/models/ship_engine_r.res" id="7_enginer"]
|
||||
|
||||
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_ship"]
|
||||
friction = 0.1
|
||||
@@ -21,29 +17,13 @@ physics_material_override = SubResource("PhysicsMaterial_ship")
|
||||
inertia = Vector3(1, 1, 1)
|
||||
script = ExtResource("1_efag7")
|
||||
|
||||
[node name="Hull" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0)
|
||||
mesh = ExtResource("2_hull")
|
||||
|
||||
[node name="Nose" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0)
|
||||
mesh = ExtResource("3_nose")
|
||||
|
||||
[node name="Canopy" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.31, -0.55)
|
||||
mesh = ExtResource("4_canopy")
|
||||
|
||||
[node name="TailFin" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.24, 0.72)
|
||||
mesh = ExtResource("5_talfin")
|
||||
|
||||
[node name="EngineGlowL" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -0.42, -0.05, 0.95)
|
||||
mesh = ExtResource("6_enginel")
|
||||
|
||||
[node name="EngineGlowR" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0.42, -0.05, 0.95)
|
||||
mesh = ExtResource("7_enginer")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_dsjou")
|
||||
|
||||
+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:
|
||||
|
||||
@@ -25,7 +25,6 @@ Bugs found in an adversarial review. None are gameplay- or physics-affecting, so
|
||||
|
||||
## Performance
|
||||
|
||||
- [ ] Merge the four non-tinted ship meshes (Hull/Canopy/EngineGlowL/R) into one — 6 draw calls per ship down to 3. Irrelevant at 1v1; 36 calls before VFX at 3v3.
|
||||
- [ ] Name collision layers in `project.godot` and assign them — nothing configures `collision_layer`/`collision_mask` today, so every body tests against every other.
|
||||
- [ ] Measure `nebula_dust.gdshader`'s per-fragment depth-texture sample across 500 large soft billboards before adding more particle work.
|
||||
- [ ] Bake `ArenaBoundary`'s ~160 runtime-generated `CollisionShape3D` nodes into the scene. Costs a load hitch on every arena entry and repeats in every parallel headless training env. **Blocked on the trained-bot decision below** — `arena_boundary.gd:235` notes this geometry is what the shipped policies were fitted against, so the bake must be verified byte-identical.
|
||||
|
||||
Reference in New Issue
Block a user