mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat: replace ship and ball placeholder meshes with Blender-modeled assets
Ship gets a greebled hull, tapered nose, swept canopy, twin engine nacelles, and tail fin (built via the vendored Blender MCP, generator committed at tools/blender/gen_ship.py) in place of the 5 flat primitives. The ball is fully remodeled as a smooth round sphere with a crossed emissive accent pattern (gen_ball.py), replacing the old flat-shaded gold_ball rather than just tweaking its material. Node names (Nose/TailFin) are preserved for Ship._apply_team_color(), and the RigidBody3D/CollisionShape3D physics on both ship.tscn and ball.tscn are untouched so RL-trained bots and flight feel stay valid. Each part's mesh is extracted to a standalone .res (tools/blender/extract_meshes.gd) rather than referenced via glb::ArrayMesh_xxx sub-paths, which don't reliably resolve across scene files and were silently rendering both models invisible.
This commit is contained in:
Binary file not shown.
+6
-6
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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")
|
||||
|
||||
+21
-58
@@ -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")
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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 <repo_root>/tools/blender/gen_ship.py.
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
build_ship()
|
||||
export(repo_root)
|
||||
Reference in New Issue
Block a user