diff --git a/Game/assets/blender_models/ball.blend b/Game/assets/blender_models/ball.blend new file mode 100644 index 00000000..8c5401cd Binary files /dev/null and b/Game/assets/blender_models/ball.blend differ diff --git a/Game/assets/blender_models/gold_ball.blend.import b/Game/assets/blender_models/ball.blend.import similarity index 82% rename from Game/assets/blender_models/gold_ball.blend.import rename to Game/assets/blender_models/ball.blend.import index ced3118c..814db1cc 100644 --- a/Game/assets/blender_models/gold_ball.blend.import +++ b/Game/assets/blender_models/ball.blend.import @@ -3,13 +3,13 @@ importer="scene" importer_version=1 type="PackedScene" -uid="uid://85qbs3rignos" -path="res://.godot/imported/gold_ball.blend-c2b8b4e1fb4c47cd0c20f77e5daf34c2.scn" +uid="uid://ckohaa5ebxym2" +path="res://.godot/imported/ball.blend-22aebbee9e0a3f479241b5a042aee325.scn" [deps] -source_file="res://assets/blender_models/gold_ball.blend" -dest_files=["res://.godot/imported/gold_ball.blend-c2b8b4e1fb4c47cd0c20f77e5daf34c2.scn"] +source_file="res://assets/blender_models/ball.blend" +dest_files=["res://.godot/imported/ball.blend-22aebbee9e0a3f479241b5a042aee325.scn"] [params] @@ -46,7 +46,7 @@ blender/nodes/punctual_lights=true blender/nodes/cameras=true blender/nodes/custom_properties=true blender/nodes/modifiers=1 -blender/meshes/vertex_colors=2 +blender/meshes/vertex_colors=1 blender/meshes/uvs=true blender/meshes/normals=true blender/meshes/export_geometry_nodes_instances=false @@ -60,4 +60,4 @@ blender/animation/limit_playback=true blender/animation/always_sample=true blender/animation/group_tracks=true gltf/naming_version=2 -gltf/texture_map_mode=0 +gltf/texture_map_mode=1 diff --git a/Game/assets/blender_models/gold_ball.blend b/Game/assets/blender_models/gold_ball.blend deleted file mode 100644 index 1d3d7dbf..00000000 Binary files a/Game/assets/blender_models/gold_ball.blend and /dev/null differ diff --git a/Game/assets/blender_models/ship.blend b/Game/assets/blender_models/ship.blend new file mode 100644 index 00000000..097f0fd6 Binary files /dev/null and b/Game/assets/blender_models/ship.blend differ diff --git a/Game/assets/blender_models/ship.blend.import b/Game/assets/blender_models/ship.blend.import new file mode 100644 index 00000000..829ba46e --- /dev/null +++ b/Game/assets/blender_models/ship.blend.import @@ -0,0 +1,63 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bp2eqsu8o3082" +path="res://.godot/imported/ship.blend-1bdca1ba6b72cf6be2f2eb32002cf7c8.scn" + +[deps] + +source_file="res://assets/blender_models/ship.blend" +dest_files=["res://.godot/imported/ship.blend-1bdca1ba6b72cf6be2f2eb32002cf7c8.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +blender/nodes/visible=0 +blender/nodes/active_collection_only=false +blender/nodes/punctual_lights=true +blender/nodes/cameras=true +blender/nodes/custom_properties=true +blender/nodes/modifiers=1 +blender/meshes/vertex_colors=1 +blender/meshes/uvs=true +blender/meshes/normals=true +blender/meshes/export_geometry_nodes_instances=false +blender/meshes/gpu_instances=false +blender/meshes/tangents=true +blender/meshes/skins=2 +blender/meshes/export_bones_deforming_mesh_only=false +blender/materials/unpack_enabled=true +blender/materials/export_materials=1 +blender/animation/limit_playback=true +blender/animation/always_sample=true +blender/animation/group_tracks=true +gltf/naming_version=2 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ball.glb b/Game/assets/models/ball.glb new file mode 100644 index 00000000..a6afbbb6 Binary files /dev/null and b/Game/assets/models/ball.glb differ diff --git a/Game/assets/models/gold_ball.glb.import b/Game/assets/models/ball.glb.import similarity index 53% rename from Game/assets/models/gold_ball.glb.import rename to Game/assets/models/ball.glb.import index 4fb7b345..adce8c1b 100644 --- a/Game/assets/models/gold_ball.glb.import +++ b/Game/assets/models/ball.glb.import @@ -3,18 +3,18 @@ importer="scene" importer_version=1 type="PackedScene" -uid="uid://cpawiqbrjyip5" -path="res://.godot/imported/gold_ball.glb-704aa0b9ad2b63d580121e20c4482485.scn" +uid="uid://dj4x6k2phqsas" +path="res://.godot/imported/ball.glb-54e57a8d163cfef28e5e007118be03c5.scn" [deps] -source_file="res://assets/models/gold_ball.glb" -dest_files=["res://.godot/imported/gold_ball.glb-704aa0b9ad2b63d580121e20c4482485.scn"] +source_file="res://assets/models/ball.glb" +dest_files=["res://.godot/imported/ball.glb-54e57a8d163cfef28e5e007118be03c5.scn"] [params] -nodes/root_type="Node3D" -nodes/root_name="Scene Root" +nodes/root_type="" +nodes/root_name="" nodes/root_script=null mesh_library/use_node_names_as_mesh_names=false array_mesh/deduplicate_surfaces=true @@ -39,21 +39,7 @@ import_script/path="" materials/extract=0 materials/extract_format=0 materials/extract_path="" -_subresources={ -"meshes": { -"gold_ball_Sphere": { -"generate/lightmap_uv": 0, -"generate/lods": 0, -"generate/shadow_meshes": 0, -"lods/normal_merge_angle": 60.0, -"lods/normal_split_angle": 25.0, -"save_to_file/enabled": true, -"save_to_file/fallback_path": "res://assets/models/gold_ball.res", -"save_to_file/make_streamable": "", -"save_to_file/path": "uid://ucw1lyo43yi4" -} -} -} -gltf/naming_version=0 +_subresources={} +gltf/naming_version=2 gltf/embedded_image_handling=1 -gltf/texture_map_mode=0 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ball.res b/Game/assets/models/ball.res new file mode 100644 index 00000000..dc6676a0 Binary files /dev/null and b/Game/assets/models/ball.res differ diff --git a/Game/assets/models/gold_ball.glb b/Game/assets/models/gold_ball.glb deleted file mode 100644 index 5d5ff66f..00000000 Binary files a/Game/assets/models/gold_ball.glb and /dev/null differ diff --git a/Game/assets/models/gold_ball.res b/Game/assets/models/gold_ball.res deleted file mode 100644 index 0aa9c5c4..00000000 Binary files a/Game/assets/models/gold_ball.res and /dev/null differ diff --git a/Game/assets/models/ship_canopy.glb b/Game/assets/models/ship_canopy.glb new file mode 100644 index 00000000..0d193988 Binary files /dev/null and b/Game/assets/models/ship_canopy.glb differ diff --git a/Game/assets/models/ship_canopy.glb.import b/Game/assets/models/ship_canopy.glb.import new file mode 100644 index 00000000..1d8c567a --- /dev/null +++ b/Game/assets/models/ship_canopy.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dqsruccgu70n6" +path="res://.godot/imported/ship_canopy.glb-eb012be4d2aaadd3ad4ce782ee3cac26.scn" + +[deps] + +source_file="res://assets/models/ship_canopy.glb" +dest_files=["res://.godot/imported/ship_canopy.glb-eb012be4d2aaadd3ad4ce782ee3cac26.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_canopy.res b/Game/assets/models/ship_canopy.res new file mode 100644 index 00000000..6908f8b2 Binary files /dev/null and b/Game/assets/models/ship_canopy.res differ diff --git a/Game/assets/models/ship_engine_l.glb b/Game/assets/models/ship_engine_l.glb new file mode 100644 index 00000000..e0953cff Binary files /dev/null and b/Game/assets/models/ship_engine_l.glb differ diff --git a/Game/assets/models/ship_engine_l.glb.import b/Game/assets/models/ship_engine_l.glb.import new file mode 100644 index 00000000..411d3b06 --- /dev/null +++ b/Game/assets/models/ship_engine_l.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://3kf7d62qivgv" +path="res://.godot/imported/ship_engine_l.glb-36865e4bdf2ded2bf550380b5e5a31da.scn" + +[deps] + +source_file="res://assets/models/ship_engine_l.glb" +dest_files=["res://.godot/imported/ship_engine_l.glb-36865e4bdf2ded2bf550380b5e5a31da.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_engine_l.res b/Game/assets/models/ship_engine_l.res new file mode 100644 index 00000000..cf10e141 Binary files /dev/null and b/Game/assets/models/ship_engine_l.res differ diff --git a/Game/assets/models/ship_engine_r.glb b/Game/assets/models/ship_engine_r.glb new file mode 100644 index 00000000..4e2c33d5 Binary files /dev/null and b/Game/assets/models/ship_engine_r.glb differ diff --git a/Game/assets/models/ship_engine_r.glb.import b/Game/assets/models/ship_engine_r.glb.import new file mode 100644 index 00000000..7a651f17 --- /dev/null +++ b/Game/assets/models/ship_engine_r.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://w3ps73chyt2t" +path="res://.godot/imported/ship_engine_r.glb-214ece30da53490ddc1cb491c37e947a.scn" + +[deps] + +source_file="res://assets/models/ship_engine_r.glb" +dest_files=["res://.godot/imported/ship_engine_r.glb-214ece30da53490ddc1cb491c37e947a.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_engine_r.res b/Game/assets/models/ship_engine_r.res new file mode 100644 index 00000000..5cb999c6 Binary files /dev/null and b/Game/assets/models/ship_engine_r.res differ diff --git a/Game/assets/models/ship_hull.glb b/Game/assets/models/ship_hull.glb new file mode 100644 index 00000000..3ba22401 Binary files /dev/null and b/Game/assets/models/ship_hull.glb differ diff --git a/Game/assets/models/ship_hull.glb.import b/Game/assets/models/ship_hull.glb.import new file mode 100644 index 00000000..4011af02 --- /dev/null +++ b/Game/assets/models/ship_hull.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://31h3nccel10k" +path="res://.godot/imported/ship_hull.glb-e35cc22e21d218aa71b911f28b0dd14f.scn" + +[deps] + +source_file="res://assets/models/ship_hull.glb" +dest_files=["res://.godot/imported/ship_hull.glb-e35cc22e21d218aa71b911f28b0dd14f.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_hull.res b/Game/assets/models/ship_hull.res new file mode 100644 index 00000000..bf45906b Binary files /dev/null and b/Game/assets/models/ship_hull.res differ diff --git a/Game/assets/models/ship_nose.glb b/Game/assets/models/ship_nose.glb new file mode 100644 index 00000000..90fe60d2 Binary files /dev/null and b/Game/assets/models/ship_nose.glb differ diff --git a/Game/assets/models/ship_nose.glb.import b/Game/assets/models/ship_nose.glb.import new file mode 100644 index 00000000..2103bb44 --- /dev/null +++ b/Game/assets/models/ship_nose.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://yio14umwtku1" +path="res://.godot/imported/ship_nose.glb-84e1b2cefdde564b2174201282e01d4e.scn" + +[deps] + +source_file="res://assets/models/ship_nose.glb" +dest_files=["res://.godot/imported/ship_nose.glb-84e1b2cefdde564b2174201282e01d4e.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_nose.res b/Game/assets/models/ship_nose.res new file mode 100644 index 00000000..c97bfdc3 Binary files /dev/null and b/Game/assets/models/ship_nose.res differ diff --git a/Game/assets/models/ship_tailfin.glb b/Game/assets/models/ship_tailfin.glb new file mode 100644 index 00000000..66d33510 Binary files /dev/null and b/Game/assets/models/ship_tailfin.glb differ diff --git a/Game/assets/models/ship_tailfin.glb.import b/Game/assets/models/ship_tailfin.glb.import new file mode 100644 index 00000000..46755733 --- /dev/null +++ b/Game/assets/models/ship_tailfin.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://c1i2lsx58u146" +path="res://.godot/imported/ship_tailfin.glb-d9d8f37674e2cf04cd3606e5c7a5c641.scn" + +[deps] + +source_file="res://assets/models/ship_tailfin.glb" +dest_files=["res://.godot/imported/ship_tailfin.glb-d9d8f37674e2cf04cd3606e5c7a5c641.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Game/assets/models/ship_tailfin.res b/Game/assets/models/ship_tailfin.res new file mode 100644 index 00000000..0ea7b3d7 Binary files /dev/null and b/Game/assets/models/ship_tailfin.res differ diff --git a/Game/objects/ball.tscn b/Game/objects/ball.tscn index 61c7e9f9..5f968f8e 100644 --- a/Game/objects/ball.tscn +++ b/Game/objects/ball.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=5 format=3 uid="uid://27u3tdc5yqnl"] -[ext_resource type="ArrayMesh" uid="uid://ucw1lyo43yi4" path="res://assets/models/gold_ball.res" id="1_ct1s3"] +[ext_resource type="ArrayMesh" path="res://assets/models/ball.res" id="1_ball"] [ext_resource type="Script" path="res://scripts/ball.gd" id="2_ball"] [sub_resource type="SphereShape3D" id="SphereShape3D_c5p07"] @@ -24,5 +24,5 @@ metadata/_edit_group_ = true shape = SubResource("SphereShape3D_c5p07") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] -transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 2.08165e-12, 2.08165e-12, 2.08165e-12) -mesh = ExtResource("1_ct1s3") +transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0) +mesh = ExtResource("1_ball") diff --git a/Game/objects/ship.tscn b/Game/objects/ship.tscn index 7dd15378..cac93cfa 100644 --- a/Game/objects/ship.tscn +++ b/Game/objects/ship.tscn @@ -1,58 +1,17 @@ [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 bounce = 0.15 -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hull"] -albedo_color = Color(0.35, 0.37, 0.42, 1) -metallic = 0.6 -roughness = 0.4 - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_accent"] -albedo_color = Color(0.25, 0.55, 1, 1) -metallic = 0.3 -roughness = 0.5 -emission_enabled = true -emission = Color(0.25, 0.55, 1, 1) -emission_energy_multiplier = 0.35 - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_canopy"] -albedo_color = Color(0.15, 0.85, 1, 1) -metallic = 0.8 -roughness = 0.1 -emission_enabled = true -emission = Color(0.15, 0.85, 1, 1) -emission_energy_multiplier = 0.5 - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_engine"] -albedo_color = Color(1, 0.55, 0.15, 1) -emission_enabled = true -emission = Color(1, 0.55, 0.15, 1) -emission_energy_multiplier = 2.0 - -[sub_resource type="BoxMesh" id="BoxMesh_hull"] -material = SubResource("StandardMaterial3D_hull") -size = Vector3(1, 0.75, 2.7) - -[sub_resource type="PrismMesh" id="PrismMesh_nose"] -material = SubResource("StandardMaterial3D_accent") -size = Vector3(1, 1.4, 0.75) - -[sub_resource type="BoxMesh" id="BoxMesh_canopy"] -material = SubResource("StandardMaterial3D_canopy") -size = Vector3(0.55, 0.28, 0.8) - -[sub_resource type="BoxMesh" id="BoxMesh_fin"] -material = SubResource("StandardMaterial3D_accent") -size = Vector3(0.1, 0.5, 0.8) - -[sub_resource type="BoxMesh" id="BoxMesh_engine"] -material = SubResource("StandardMaterial3D_engine") -size = Vector3(0.7, 0.5, 0.25) - [sub_resource type="BoxShape3D" id="BoxShape3D_dsjou"] size = Vector3(1, 1, 4) @@ -63,24 +22,28 @@ 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.125, 0.35) -mesh = SubResource("BoxMesh_hull") +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, 0, 1, 0, -1, 0, 0, -0.125, -1.3) -mesh = SubResource("PrismMesh_nose") +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.36, -0.5) -mesh = SubResource("BoxMesh_canopy") +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.25, 1.25) -mesh = SubResource("BoxMesh_fin") +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.24, 0.72) +mesh = ExtResource("5_talfin") -[node name="EngineGlow" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.125, 1.85) -mesh = SubResource("BoxMesh_engine") +[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") diff --git a/TODO.md b/TODO.md index a0b0b630..8be7e84b 100644 --- a/TODO.md +++ b/TODO.md @@ -47,5 +47,5 @@ A subagent ran the game and critiqued arena_02 head-on against Rocket League 2 i - [ ] **Particle lighting response for the nebula dust ("weather") effect** (no models — particle material/shader only). The `NebulaDust` `GPUParticles3D` motes are generic soft glow sprites with no lighting interaction, no depth-based fade, and no secondary motion (no sparkle/color shift). Prompt: "In `arena_02.tscn`'s `NebulaDust` particle system, replace the current unshaded glow-sprite material with a custom shader that fakes lighting response — note the sprites are camera-facing billboards, so they have no fixed world-space normal and true per-pixel PBR lighting won't behave like it would on a solid mesh. A practical approach: derive a fake per-pixel normal from the quad's local UV (as if each sprite were a small sphere/puff, same trick used for 2D lit particle effects) and light that against the directional light + a fixed 'glow color' bias toward the nebula core direction, rather than relying on the mesh's real (camera-facing) normal. Add soft-particle depth fade (compare particle depth to the depth buffer) so motes don't hard-clip through the boundary/decoration geometry, and add subtle per-particle brightness flicker via the color ramp or shader for sparkle. Verify visually via godot-mcp screenshots that the dust reads as lit rather than flat-glowing, from a couple of different camera angles (billboard lighting tricks can look wrong from some angles even when right from others)." -- [ ] **Ship + ball visual pass** (Blender remodel — explicit models to make). The ship (`Game/objects/ship.tscn`) is *not* a single placeholder box — it's already 5 separate primitive `MeshInstance3D` parts (`Hull` a `BoxMesh`, `Nose` a `PrismMesh`, `Canopy` and `EngineGlow` boxes, `TailFin` a thin box), several with emission already (the cyan canopy, the orange `EngineGlow`, team-tinted `Nose`/`TailFin`). It's the same underlying critique as everything else, though: plain primitives, no greebling/panel detail, no imported model — just built further already than the write-up above implied. +- [x] **Ship + ball visual pass**: `Game/objects/ship.tscn`'s 5 primitives replaced with a Blender-greebled hull/nose/canopy/tailfin/twin nacelles (source `Game/assets/blender_models/ship.blend`, generator `tools/blender/gen_ship.py`); ball fully remodeled as a smooth round icosphere with a crossed emissive accent pattern (`ball.blend`, `gen_ball.py`) rather than just a material tweak, replacing the old flat-shaded `gold_ball`. `Nose`/`TailFin` node names preserved for `_apply_team_color()`; `CollisionShape3D`/`RigidBody3D` physics on both ship and ball untouched (verified). Each part's mesh is extracted to a standalone `.res` (`tools/blender/extract_meshes.gd`) rather than referenced via `glb::ArrayMesh_xxx`, which doesn't reliably resolve across scene files. Verified via in-game screenshots (both team colors, orientation) and headless runs of `free_play`/`training`/`match`. Prompt: "Using the blender MCP, model an actual low-poly-but-detailed ship hull (greebled fuselage panel lines, a proper cockpit canopy shape, 1-2 engine nacelles with emissive exhaust glow) to replace `Game/objects/ship.tscn`'s current 5 primitive `MeshInstance3D` parts (`Hull`/`Nose`/`Canopy`/`TailFin`/`EngineGlow`), built the same procedural way as the arena decoration (bpy/bmesh primitives + bevels + inset/extrude detail — kept original, no borrowed/asset-store art). Two hard constraints this must respect: (1) **`scripts/ship.gd`'s `_apply_team_color()` recolors two child nodes by exact name, `get_node_or_null(\"Nose\")` and `get_node_or_null(\"TailFin\")`, at runtime per-team** (team 0 blue, team 1 orange, set via `game_mode.gd`'s `ship.team = team`) — either keep two `MeshInstance3D` parts of the new model literally named `Nose` and `TailFin` so this keeps working untouched, or update `_apply_team_color()`'s node-name list in the same change if the new model's parts are named differently; don't let this silently break. (2) **Do not touch `ship.tscn`'s single `CollisionShape3D` (`BoxShape3D`, size `1 x 1 x 4`) or the `RigidBody3D`'s `mass = 5.0`/`inertia = (1,1,1)`/`physics_material_override`** — those are exactly what the trained RL bots and flight feel are tuned against (per CLAUDE.md). Keep the new visual mesh's silhouette reasonably close to that `1 x 1 x 4` box so the ship doesn't visually stick out of or float inside its own collision bounds. Separately, audit the existing gold ball's material (`Game/assets/models/gold_ball.glb`/`.res`, referenced from `Game/objects/ball.tscn`'s `MeshInstance3D` with no scene-level material override, so its look currently comes entirely from whatever's embedded in the imported mesh) and upgrade it to a proper metallic/emissive-accent PBR look if it turns out flat/unshaded once the lighting pass above lands — don't assume it needs work without checking first. Verify both visually (screenshot, including a look at team-color recoloring in Match/Spectate) and via a headless run of `free_play.tscn`/`training.tscn` to confirm physics/RL behavior is unaffected." diff --git a/tools/blender/extract_meshes.gd b/tools/blender/extract_meshes.gd new file mode 100644 index 00000000..d9123d48 --- /dev/null +++ b/tools/blender/extract_meshes.gd @@ -0,0 +1,35 @@ +## Extracts baked ArrayMesh resources out of the per-part ship/ball glb files +## into standalone .res files, which is what ship.tscn/ball.tscn actually +## reference (see gen_ship.py/gen_ball.py for why: a fresh glTF import's +## PackedScene wraps its single mesh object in a synthetic Node3D root, so +## the mesh itself needs pulling out to be usable as a direct MeshInstance3D +## child of Ship/Ball). +## +## Run after re-exporting the glbs from Blender (gen_ship.py / gen_ball.py): +## godot --headless --path Game --script ../tools/blender/extract_meshes.gd +## (from the repo root; adjust the relative --script path to wherever you run +## the command from). Overwrites the existing ship_*.res / ball.res files. +extends SceneTree + + +const SOURCES = { + "res://assets/models/ship_hull.glb": "res://assets/models/ship_hull.res", + "res://assets/models/ship_nose.glb": "res://assets/models/ship_nose.res", + "res://assets/models/ship_canopy.glb": "res://assets/models/ship_canopy.res", + "res://assets/models/ship_tailfin.glb": "res://assets/models/ship_tailfin.res", + "res://assets/models/ship_engine_l.glb": "res://assets/models/ship_engine_l.res", + "res://assets/models/ship_engine_r.glb": "res://assets/models/ship_engine_r.res", + "res://assets/models/ball.glb": "res://assets/models/ball.res", +} + + +func _initialize() -> void: + for glb_path in SOURCES: + var res_path: String = SOURCES[glb_path] + var packed: PackedScene = load(glb_path) + var instance: Node = packed.instantiate() + var mesh_node: MeshInstance3D = instance.get_child(0) + var err := ResourceSaver.save(mesh_node.mesh, res_path) + print("%s -> %s (err=%d)" % [glb_path, res_path, err]) + instance.free() + quit() diff --git a/tools/blender/gen_ball.py b/tools/blender/gen_ball.py new file mode 100644 index 00000000..46c8c316 --- /dev/null +++ b/tools/blender/gen_ball.py @@ -0,0 +1,132 @@ +"""Procedurally generates the Cosmic Clash match ball model. + +Run inside Blender (e.g. via the blender-mcp `execute_blender_code` tool, or +`blender --background --python gen_ball.py`). Builds a single "Ball" mesh +object: a smooth-shaded, undeformed high-resolution icosphere (every vertex +stays exactly on the radius-1.0 sphere, so the silhouette is perfectly round +and matches the untouched SphereShape3D collision) with a metallic gold body +and a pair of crossed emissive "energy seam" rings applied purely via +per-face material assignment — no geometry is pushed off the sphere, unlike +an earlier beveled/faceted revision that read as an angular gem rather than +a round ball. + +Built at native radius 1.0 to match `ball.tscn`'s existing MeshInstance3D +scale of 0.5 (world radius 0.5). Keep this convention if regenerating, so +`ball.tscn` doesn't need its scale transform changed. + +After running this script, run `extract_meshes.gd` (in this same directory) +inside Godot to produce `ball.res`, which is what `ball.tscn` actually +references — not `ball.glb` directly. A glTF import's PackedScene wraps its +mesh object in a synthetic Node3D root, and referencing a named sub-mesh of +that scene via `path.glb::ArrayMesh_xxx` from a *different* .tscn doesn't +reliably resolve (it silently returns a null mesh unless that exact glTF +scene has already been instanced elsewhere first) — extracting a standalone +`.res` avoids that entirely. +""" + +import bpy +import bmesh + +BLEND_PATH = "Game/assets/blender_models/ball.blend" +GLB_PATH = "Game/assets/models/ball.glb" + + +def new_mesh_object(name, bm): + mesh = bpy.data.meshes.new(name) + bm.to_mesh(mesh) + bm.free() + obj = bpy.data.objects.new(name, mesh) + bpy.context.collection.objects.link(obj) + return obj + + +def clear_scene(): + for obj in list(bpy.data.objects): + if obj.type in {"MESH", "EMPTY"}: + bpy.data.objects.remove(obj, do_unlink=True) + for block in list(bpy.data.meshes): + if block.users == 0: + bpy.data.meshes.remove(block) + for block in list(bpy.data.materials): + if block.users == 0: + bpy.data.materials.remove(block) + + +def build_ball_mesh(): + bm = bmesh.new() + # No bevel/inset — geometry stays an undeformed sphere so the silhouette + # is perfectly round; all surface detail below is material-only. + bmesh.ops.create_icosphere(bm, subdivisions=4, radius=1.0) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + ball = new_mesh_object("Ball", bm) + for p in ball.data.polygons: + p.use_smooth = True + return ball + + +def make_material(name, base_rgb, metallic, roughness, emission_rgb=None, emission_strength=0.0): + existing = bpy.data.materials.get(name) + if existing: + bpy.data.materials.remove(existing) + mat = bpy.data.materials.new(name) + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs["Base Color"].default_value = (*base_rgb, 1.0) + bsdf.inputs["Metallic"].default_value = metallic + bsdf.inputs["Roughness"].default_value = roughness + if emission_rgb is not None: + bsdf.inputs["Emission Color"].default_value = (*emission_rgb, 1.0) + bsdf.inputs["Emission Strength"].default_value = emission_strength + return mat + + +def assign_materials(ball): + mat_body = make_material("Mat_GoldBody", (0.85, 0.65, 0.15), 0.92, 0.18) + mat_accent = make_material( + "Mat_GoldAccent", (1.0, 0.85, 0.4), 0.7, 0.2, (1.0, 0.8, 0.35), 3.0 + ) + + ball.data.materials.clear() + ball.data.materials.append(mat_body) # 0 + ball.data.materials.append(mat_accent) # 1 + + # Two crossed great-circle "energy seam" rings, selected purely by face + # center position — a deliberate accent pattern rather than random speckle, + # with zero effect on vertex positions. + for p in ball.data.polygons: + cx, _, cz = p.center + on_equator_ring = abs(cz) < 0.09 + on_meridian_ring = abs(cx) < 0.09 + p.material_index = 1 if (on_equator_ring or on_meridian_ring) else 0 + + +def build_ball(): + clear_scene() + ball = build_ball_mesh() + assign_materials(ball) + + +def export(repo_root): + import os + + blend_path = os.path.join(repo_root, BLEND_PATH) + glb_path = os.path.join(repo_root, GLB_PATH) + + bpy.ops.wm.save_as_mainfile(filepath=blend_path) + + bpy.ops.object.select_all(action="SELECT") + bpy.ops.export_scene.gltf( + filepath=glb_path, + export_format="GLB", + use_selection=True, + export_apply=True, + export_materials="EXPORT", + ) + + +if __name__ == "__main__": + import os + + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + build_ball() + export(repo_root) diff --git a/tools/blender/gen_ship.py b/tools/blender/gen_ship.py new file mode 100644 index 00000000..6ad990f9 --- /dev/null +++ b/tools/blender/gen_ship.py @@ -0,0 +1,341 @@ +"""Procedurally generates the Cosmic Clash ship hull model. + +Run inside Blender (e.g. via the blender-mcp `execute_blender_code` tool, or +Blender's own script editor / `blender --background --python gen_ship.py`). +Builds six named mesh objects — Hull, Nose, Canopy, TailFin, EngineGlowL, +EngineGlowR — greebled from bmesh primitives (bevel + inset/extrude), saves +ship.blend, then exports each part as its OWN glb file (ship_hull.glb, +ship_nose.glb, etc.) rather than one combined file. + +Why per-part files: Godot cannot reliably resolve a single named sub-mesh +out of a multi-object glTF via `path.glb::ArrayMesh_xxx` addressing from a +*different* .tscn — that syntax only resolves once the whole glTF scene has +already been instanced elsewhere in the same session; a fresh load of just +that ext_resource reference silently returns a null mesh. Exporting one +object per glb file avoids that (each file's root is a clean, individually +loadable node), but each still imports as a synthetic Node3D wrapper with +ONE MeshInstance3D child — not the MeshInstance3D itself. `ship.tscn` needs +the mesh nodes as *direct* children of the Ship root (see node-name note +below), so after running this script, run `extract_meshes.gd` (in this same +directory) inside Godot to pull each part's ArrayMesh out into its own +`.res` file — that's what `ship.tscn` actually references. + +Node names matter: `scripts/ship.gd`'s `_apply_team_color()` recolors direct +children of the Ship node named exactly "Nose" and "TailFin" at runtime, so +those two object names must be preserved. + +Orientation: empirically, Blender's -Y axis (this script builds the nose at +-Y) maps to Godot's **+Z** on glTF export (not -Z as the naive Blender-Z-up +vs. Godot-Y-up mapping might suggest) — verified in-engine, not assumed. +Since ship forward is Godot -Z (see `ship.gd`), `ship.tscn` applies a +180-degree rotation (`Transform3D(-1,0,0, 0,1,0, 0,0,-1, ...)`) to every +part's instance node to correct this. If you regenerate the parts, that +correction stays in `ship.tscn` — don't rebuild it into the Blender geometry +too, or the two fixes will cancel out and the ship will face backwards again. + +Silhouette budget: the ship's RigidBody3D collision shape (`ship.tscn`, +BoxShape3D) is (1, 1, 4) in Godot space — X width, Y height, Z length. In +this script's local (pre-export) space X=width, Y=length (nose at -Y), +Z=height, and the budget is X +-0.5, Y(length) +-2.0, Z(height) +-0.5. The +built silhouette is kept reasonably close to that budget, not an exact fit +(matching the original primitive ship, which was also slightly over at a +few extremities). +""" + +import math +import random + +import bmesh +import bpy +from mathutils import Matrix, Vector + +BLEND_PATH = "Game/assets/blender_models/ship.blend" +PART_GLB_PATHS = { + "Hull": "Game/assets/models/ship_hull.glb", + "Nose": "Game/assets/models/ship_nose.glb", + "Canopy": "Game/assets/models/ship_canopy.glb", + "TailFin": "Game/assets/models/ship_tailfin.glb", + "EngineGlowL": "Game/assets/models/ship_engine_l.glb", + "EngineGlowR": "Game/assets/models/ship_engine_r.glb", +} + + +def new_mesh_object(name, bm): + mesh = bpy.data.meshes.new(name) + bm.to_mesh(mesh) + bm.free() + obj = bpy.data.objects.new(name, mesh) + bpy.context.collection.objects.link(obj) + return obj + + +def remove_object(name): + obj = bpy.data.objects.get(name) + if obj: + mesh = obj.data + bpy.data.objects.remove(obj, do_unlink=True) + if mesh.users == 0: + bpy.data.meshes.remove(mesh) + + +def clear_scene(): + for obj in list(bpy.data.objects): + if obj.type in {"MESH", "EMPTY"}: + bpy.data.objects.remove(obj, do_unlink=True) + for block in list(bpy.data.meshes): + if block.users == 0: + bpy.data.meshes.remove(block) + for block in list(bpy.data.materials): + if block.users == 0: + bpy.data.materials.remove(block) + + +def build_hull(): + random.seed(7) + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=1.0) + # Full dims: width 0.9, length 2.6, height 0.6 (collision budget is 1 x 4 x 1 full) + bmesh.ops.scale(bm, vec=(0.9, 2.6, 0.6), verts=bm.verts) + + bm.edges.ensure_lookup_table() + long_edges = [e for e in bm.edges if (e.verts[0].co - e.verts[1].co).length > 2.0] + bmesh.ops.bevel(bm, geom=long_edges, offset=0.1, segments=3, affect="EDGES") + + bm.faces.ensure_lookup_table() + panel_target_faces = [f for f in bm.faces if abs(f.normal.z) > 0.6 or abs(f.normal.x) > 0.6] + edges_to_cut = list({e for f in panel_target_faces for e in f.edges}) + bmesh.ops.subdivide_edges(bm, edges=edges_to_cut, cuts=3, use_grid_fill=True) + + def greeble_pass(seed, fraction, thickness, depth_range): + random.seed(seed) + bm.faces.ensure_lookup_table() + candidates = [ + f + for f in bm.faces + if (abs(f.normal.z) > 0.55 or abs(f.normal.x) > 0.55) and 0.01 < f.calc_area() < 0.35 + ] + random.shuffle(candidates) + chosen = candidates[: max(1, int(len(candidates) * fraction))] + for f in chosen: + if not f.is_valid: + continue + res = bmesh.ops.inset_individual(bm, faces=[f], thickness=thickness) + new_faces = res["faces"] + if new_faces: + nf = new_faces[0] + depth = random.uniform(*depth_range) + bmesh.ops.translate(bm, verts=nf.verts, vec=nf.normal * depth) + + greeble_pass(seed=7, fraction=1 / 3, thickness=0.02, depth_range=(-0.03, 0.022)) + greeble_pass(seed=42, fraction=0.25, thickness=0.018, depth_range=(-0.028, 0.018)) + + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + return new_mesh_object("Hull", bm) + + +def build_nose(): + random.seed(11) + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=1.0) + bmesh.ops.scale(bm, vec=(0.82, 0.25, 0.5), verts=bm.verts) + bmesh.ops.translate(bm, vec=(0.0, -1.425, 0.0), verts=bm.verts) + + def front_face(): + bm.faces.ensure_lookup_table() + return min(bm.faces, key=lambda f: f.calc_center_median().y) + + # Stage 1 taper: extrude forward, narrow to ~half width/height. + f = front_face() + r = bmesh.ops.extrude_face_region(bm, geom=[f]) + new_verts = [v for v in r["geom"] if isinstance(v, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=(0, -0.32, 0.02)) + pivot = sum((v.co for v in new_verts), Vector()) / len(new_verts) + bmesh.ops.scale(bm, verts=new_verts, vec=(0.5, 1.0, 0.55), space=Matrix.Translation(-pivot)) + + # Stage 2 taper: extrude further to a near-point tip. + bm.faces.ensure_lookup_table() + f2 = front_face() + r2 = bmesh.ops.extrude_face_region(bm, geom=[f2]) + new_verts2 = [v for v in r2["geom"] if isinstance(v, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts2, vec=(0, -0.28, 0.0)) + pivot2 = sum((v.co for v in new_verts2), Vector()) / len(new_verts2) + bmesh.ops.scale(bm, verts=new_verts2, vec=(0.08, 1.0, 0.08), space=Matrix.Translation(-pivot2)) + + bm.edges.ensure_lookup_table() + sharp_edges = [e for e in bm.edges if e.calc_face_angle(1.0) > 0.35] + bmesh.ops.bevel(bm, geom=sharp_edges, offset=0.02, segments=2, affect="EDGES") + + bm.faces.ensure_lookup_table() + side_faces = [f for f in bm.faces if abs(f.normal.x) > 0.5 and f.calc_area() > 0.01][:2] + for f in side_faces: + if not f.is_valid: + continue + res = bmesh.ops.inset_individual(bm, faces=[f], thickness=0.02) + nf = res["faces"][0] if res["faces"] else None + if nf: + bmesh.ops.translate(bm, verts=nf.verts, vec=nf.normal * -0.015) + + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + return new_mesh_object("Nose", bm) + + +def build_canopy(): + bm = bmesh.new() + bmesh.ops.create_icosphere(bm, subdivisions=2, radius=0.3) + bm.faces.ensure_lookup_table() + faces_to_del = [f for f in bm.faces if f.calc_center_median().z < -0.02] + bmesh.ops.delete(bm, geom=faces_to_del, context="FACES") + bm.edges.ensure_lookup_table() + boundary_edges = [e for e in bm.edges if len(e.link_faces) == 1] + if boundary_edges: + bmesh.ops.holes_fill(bm, edges=boundary_edges) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + + # Swept teardrop canopy: wider, stretched fore-aft, low profile, tapered at the rear. + for v in bm.verts: + v.co.x *= 1.5 + v.co.y *= 2.2 + v.co.z *= 0.82 + if v.co.y > 0: + t = min(v.co.y / 0.62, 1.0) + v.co.z *= 1.0 - 0.35 * t + v.co.x *= 1.0 - 0.25 * t + + obj = new_mesh_object("Canopy", bm) + # Front-upper on the hull; kept low so the top stays close to the +-0.5 height budget. + obj.location = (0.0, -0.55, 0.31) + return obj + + +def build_nacelle(name, x_offset): + bm = bmesh.new() + bmesh.ops.create_cone( + bm, cap_ends=True, cap_tris=False, segments=12, radius1=0.11, radius2=0.095, depth=0.85 + ) + # Cone is built along Z by default; rotate -90 deg about X to align its axis with Y (length). + rot = Matrix.Rotation(-math.pi / 2, 4, "X") + bmesh.ops.transform(bm, matrix=rot, verts=bm.verts) + + bm.edges.ensure_lookup_table() + ring_edges = [ + e + for e in bm.edges + if len(e.link_faces) == 2 and abs(e.verts[0].co.y - e.verts[1].co.y) < 0.01 + ] + if ring_edges: + bmesh.ops.bevel(bm, geom=ring_edges, offset=0.012, segments=2, affect="EDGES") + + obj = new_mesh_object(name, bm) + # Rear of the ship, flanking the hull, slightly below centerline. + obj.location = (x_offset, 0.95, -0.05) + return obj + + +def build_tailfin(): + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=1.0) + bmesh.ops.scale(bm, vec=(0.06, 0.65, 0.42), verts=bm.verts) + # Sweep the top-rear edge back, taper the top edge thin. + for v in bm.verts: + if v.co.z > 0: + v.co.y += 0.16 if v.co.y > 0 else 0.04 + v.co.x *= 0.35 + + bm.edges.ensure_lookup_table() + sharp_edges = [e for e in bm.edges if e.calc_face_angle(1.0) > 0.3] + bmesh.ops.bevel(bm, geom=sharp_edges, offset=0.012, segments=2, affect="EDGES") + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + + obj = new_mesh_object("TailFin", bm) + obj.location = (0.0, 0.72, 0.24) + return obj + + +def make_material(name, base_rgb, metallic, roughness, emission_rgb=None, emission_strength=0.0): + existing = bpy.data.materials.get(name) + if existing: + bpy.data.materials.remove(existing) + mat = bpy.data.materials.new(name) + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs["Base Color"].default_value = (*base_rgb, 1.0) + bsdf.inputs["Metallic"].default_value = metallic + bsdf.inputs["Roughness"].default_value = roughness + if emission_rgb is not None: + bsdf.inputs["Emission Color"].default_value = (*emission_rgb, 1.0) + bsdf.inputs["Emission Strength"].default_value = emission_strength + return mat + + +def assign_materials(): + mat_hull = make_material("Mat_Hull", (0.35, 0.37, 0.42), 0.6, 0.4) + # Accent starts team-blue; Ship._apply_team_color() overrides this per-team at runtime. + mat_accent = make_material("Mat_Accent", (0.25, 0.55, 1.0), 0.3, 0.5, (0.25, 0.55, 1.0), 0.9) + mat_canopy = make_material("Mat_Canopy", (0.15, 0.85, 1.0), 0.8, 0.1, (0.15, 0.85, 1.0), 1.4) + mat_nacelle_body = make_material("Mat_NacelleBody", (0.22, 0.24, 0.28), 0.7, 0.35) + mat_engine_glow = make_material( + "Mat_EngineGlow", (1.0, 0.55, 0.15), 0.1, 0.4, (1.0, 0.55, 0.15), 4.0 + ) + + def assign_single(obj_name, mat): + obj = bpy.data.objects[obj_name] + obj.data.materials.clear() + obj.data.materials.append(mat) + + assign_single("Hull", mat_hull) + assign_single("Nose", mat_accent) + assign_single("TailFin", mat_accent) + assign_single("Canopy", mat_canopy) + + for name in ["EngineGlowL", "EngineGlowR"]: + obj = bpy.data.objects[name] + obj.data.materials.clear() + obj.data.materials.append(mat_nacelle_body) # slot 0: body + obj.data.materials.append(mat_engine_glow) # slot 1: rear exhaust glow cap + polys = obj.data.polygons + max_y = max(p.center.y for p in polys) + for p in polys: + p.material_index = 1 if p.center.y > max_y - 0.01 else 0 + + +def build_ship(): + clear_scene() + build_hull() + build_nose() + build_canopy() + build_nacelle("EngineGlowL", -0.42) + build_nacelle("EngineGlowR", 0.42) + build_tailfin() + assign_materials() + + +def export(repo_root): + import os + + blend_path = os.path.join(repo_root, BLEND_PATH) + bpy.ops.wm.save_as_mainfile(filepath=blend_path) + + for obj_name, rel_path in PART_GLB_PATHS.items(): + bpy.ops.object.select_all(action="DESELECT") + obj = bpy.data.objects[obj_name] + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.export_scene.gltf( + filepath=os.path.join(repo_root, rel_path), + export_format="GLB", + use_selection=True, + export_apply=True, + export_materials="EXPORT", + ) + + print("Exported per-part glbs. Now run extract_meshes.gd inside Godot") + print("to produce the ship_*.res files that ship.tscn references.") + + +if __name__ == "__main__": + import os + + # Assumes this script lives at /tools/blender/gen_ship.py. + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + build_ship() + export(repo_root)