From e1fda4d393973bf961fa9470f323f6161b38385a Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Sun, 19 Apr 2026 18:11:03 +0200 Subject: [PATCH] feat(dolphin): proper 3D mesh + swim animations (procedural) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DolphinMeshBuilder.gd: ArrayMesh procédural, 14 sections elliptiques, nageoire dorsale, pectorales, queue horizontale (caudale), gradient dos #4a6d82 / ventre #d8e2ea - Animations: battement caudale (sin), lean virage, flap pectorales, bob corps — fréquence x2 en boost - Dolphin.tscn: remplace 6 capsules CSG par DolphinMesh + DolphinMeshBuilder - DolphinController: corrige double-déclaration speed, pilote animate() - CREDITS.md: mesh procédural CC0 original Co-Authored-By: Claude Sonnet 4.6 --- CREDITS.md | 2 +- scenes/Dolphin.tscn | 73 +----- scripts/dolphin/DolphinMeshBuilder.gd | 298 ++++++++++++++++++++++ scripts/dolphin/DolphinMeshBuilder.gd.uid | 1 + 4 files changed, 305 insertions(+), 69 deletions(-) create mode 100644 scripts/dolphin/DolphinMeshBuilder.gd create mode 100644 scripts/dolphin/DolphinMeshBuilder.gd.uid diff --git a/CREDITS.md b/CREDITS.md index 416cd73..bf859ce 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,10 +4,10 @@ - Godot Engine 4.6.2 — MIT License — https://godotengine.org ## Assets -_À compléter au fur et à mesure des phases_ | Asset | Source | Licence | |-------|--------|---------| +| scripts/dolphin/DolphinMeshBuilder.gd | Mesh procédural original — aucune source externe | CC0 | | icon.svg | Créé manuellement | CC0 | | audio/music/underwater_theme.mp3 | https://opengameart.org/content/underwater-theme — Cleyton RX | CC-BY 3.0 | | audio/sfx/underwater_ambient.ogg | https://freesound.org/people/Zozzy/sounds/56678/ (nemoscape2.mp3) — Zozzy | CC0 | diff --git a/scenes/Dolphin.tscn b/scenes/Dolphin.tscn index 625b036..afeee3d 100644 --- a/scenes/Dolphin.tscn +++ b/scenes/Dolphin.tscn @@ -1,47 +1,14 @@ -[gd_scene load_steps=24 format=3 uid="uid://dolphin_main"] +[gd_scene load_steps=7 format=3 uid="uid://dolphin_main"] [ext_resource type="Script" path="res://scripts/dolphin/DolphinController.gd" id="1_controller"] [ext_resource type="Script" path="res://scripts/dolphin/HUD.gd" id="2_hud"] [ext_resource type="Script" path="res://scripts/dolphin/EcholocationPulse.gd" id="3_echo"] [ext_resource type="Script" path="res://scripts/dolphin/BubbleTrail.gd" id="4_bubble_trail"] +[ext_resource type="Script" path="res://scripts/dolphin/DolphinMeshBuilder.gd" id="5_builder"] [sub_resource type="CapsuleShape3D" id="1_colshape"] -radius = 0.4 -height = 2.0 - -[sub_resource type="CapsuleMesh" id="2_body_mesh"] -radius = 0.4 -height = 2.0 - -[sub_resource type="StandardMaterial3D" id="3_body_mat"] -albedo_color = Color(0.29, 0.478, 0.584, 1) - -[sub_resource type="CapsuleMesh" id="4_belly_mesh"] radius = 0.35 -height = 1.6 - -[sub_resource type="StandardMaterial3D" id="5_belly_mat"] -albedo_color = Color(0.91, 0.933, 0.949, 1) - -[sub_resource type="CapsuleMesh" id="6_tail_mesh"] -radius = 0.2 -height = 0.9 - -[sub_resource type="StandardMaterial3D" id="7_tail_mat"] -albedo_color = Color(0.29, 0.478, 0.584, 1) - -[sub_resource type="PrismMesh" id="8_fin_mesh"] -size = Vector3(0.15, 0.4, 0.08) - -[sub_resource type="StandardMaterial3D" id="9_fin_mat"] -albedo_color = Color(0.29, 0.478, 0.584, 1) - -[sub_resource type="CapsuleMesh" id="10_pec_mesh"] -radius = 0.1 -height = 0.5 - -[sub_resource type="StandardMaterial3D" id="11_pec_mat"] -albedo_color = Color(0.29, 0.478, 0.584, 1) +height = 1.8 [sub_resource type="StyleBoxFlat" id="12_panel_style"] bg_color = Color(0.05, 0.05, 0.05, 0.7) @@ -57,39 +24,9 @@ script = ExtResource("1_controller") shape = SubResource("1_colshape") [node name="DolphinBody" type="Node3D" parent="."] -rotation = Vector3(0, 0, 0) -[node name="Body" type="MeshInstance3D" parent="DolphinBody"] -rotation = Vector3(1.5708, 0, 0) -mesh = SubResource("2_body_mesh") -surface_material_override/0 = SubResource("3_body_mat") - -[node name="Belly" type="MeshInstance3D" parent="DolphinBody"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0.15) -rotation = Vector3(1.5708, 0, 0) -mesh = SubResource("4_belly_mesh") -surface_material_override/0 = SubResource("5_belly_mat") - -[node name="Tail" type="MeshInstance3D" parent="DolphinBody"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1.1) -rotation = Vector3(1.5708, 0, 0) -mesh = SubResource("6_tail_mesh") -surface_material_override/0 = SubResource("7_tail_mat") - -[node name="DorsalFin" type="MeshInstance3D" parent="DolphinBody"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.45, -0.1) -mesh = SubResource("8_fin_mesh") -surface_material_override/0 = SubResource("9_fin_mat") - -[node name="PecFinLeft" type="MeshInstance3D" parent="DolphinBody"] -transform = Transform3D(1, 0, 0, 0, 0.866, -0.5, 0, 0.5, 0.866, -0.45, 0.0, 0.1) -mesh = SubResource("10_pec_mesh") -surface_material_override/0 = SubResource("11_pec_mat") - -[node name="PecFinRight" type="MeshInstance3D" parent="DolphinBody"] -transform = Transform3D(1, 0, 0, 0, 0.866, 0.5, 0, -0.5, 0.866, 0.45, 0.0, 0.1) -mesh = SubResource("10_pec_mesh") -surface_material_override/0 = SubResource("11_pec_mat") +[node name="DolphinMesh" type="MeshInstance3D" parent="DolphinBody"] +script = ExtResource("5_builder") [node name="CameraPivot" type="Node3D" parent="."] diff --git a/scripts/dolphin/DolphinMeshBuilder.gd b/scripts/dolphin/DolphinMeshBuilder.gd new file mode 100644 index 0000000..555e6c5 --- /dev/null +++ b/scripts/dolphin/DolphinMeshBuilder.gd @@ -0,0 +1,298 @@ +@tool +extends MeshInstance3D + +## Procedural dolphin mesh — built from elliptic cross-sections along the body axis. +## Dos : #4a6d82 Ventre : #d8e2ea +## Aucun asset externe — CC0 absolu. + +const NUM_SECTIONS: int = 14 # sections along body axis (Z) +const NUM_RING_VERTS: int = 12 # vertices per ring ellipse +const BODY_LENGTH: float = 2.0 # total body length (metres) + +# Body profile: [z_norm (0..1), rx, ry] — normalised radii +# z_norm 0 = snout tip, 1 = tail root +const PROFILE: Array = [ + [0.00, 0.04, 0.03], # snout tip + [0.06, 0.12, 0.09], # rostrum + [0.14, 0.22, 0.17], # head front + [0.22, 0.34, 0.28], # melon / forehead + [0.32, 0.40, 0.33], # max girth front + [0.42, 0.38, 0.30], # chest (pecs here) + [0.52, 0.32, 0.25], # mid body + [0.62, 0.26, 0.20], # post-dorsal + [0.70, 0.20, 0.15], # waist start + [0.78, 0.14, 0.11], # peduncle + [0.85, 0.09, 0.07], # narrow peduncle + [0.91, 0.06, 0.04], # tail stock + [0.96, 0.04, 0.03], # tail root + [1.00, 0.02, 0.015], # tail tip +] + +const COLOR_BACK: Color = Color(0.290, 0.427, 0.510, 1.0) # #4a6d82 +const COLOR_BELLY: Color = Color(0.847, 0.886, 0.918, 1.0) # #d8e2ea + +var _tail_pivot: Node3D # animated separately +var _dorsal_pivot: Node3D +var _pec_left: Node3D +var _pec_right: Node3D + + +func _ready() -> void: + _build_body() + _build_dorsal_fin() + _build_pectoral_fins() + _build_tail() + _build_caudal_fluke() + + +# --------------------------------------------------------------------------- +# Body +# --------------------------------------------------------------------------- +func _build_body() -> void: + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + st.set_smooth_group(0) + + var rings: Array = [] + for i in range(NUM_SECTIONS): + var prof = PROFILE[i] + var z: float = (prof[0] as float) * BODY_LENGTH - BODY_LENGTH * 0.5 + var rx: float = prof[1] as float + var ry: float = prof[2] as float + var ring: Array = [] + for j in range(NUM_RING_VERTS): + var angle: float = (float(j) / float(NUM_RING_VERTS)) * TAU + var lx: float = cos(angle) * rx + var ly: float = sin(angle) * ry + # Colour: belly on the bottom (ly < 0), back on top + var t: float = clamp((ly / ry + 1.0) * 0.5, 0.0, 1.0) # 0=belly, 1=back + var col: Color = COLOR_BELLY.lerp(COLOR_BACK, t) + ring.append({"pos": Vector3(lx, ly, z), "col": col, "angle": angle}) + rings.append(ring) + + # Stitch rings + for i in range(NUM_SECTIONS - 1): + var r0: Array = rings[i] + var r1: Array = rings[i + 1] + for j in range(NUM_RING_VERTS): + var jn: int = (j + 1) % NUM_RING_VERTS + var v00 = r0[j] + var v01 = r0[jn] + var v10 = r1[j] + var v11 = r1[jn] + # Tri 1 + st.set_color(v00["col"]); st.add_vertex(v00["pos"]) + st.set_color(v10["col"]); st.add_vertex(v10["pos"]) + st.set_color(v11["col"]); st.add_vertex(v11["pos"]) + # Tri 2 + st.set_color(v00["col"]); st.add_vertex(v00["pos"]) + st.set_color(v11["col"]); st.add_vertex(v11["pos"]) + st.set_color(v01["col"]); st.add_vertex(v01["pos"]) + + # Cap snout + var snout_center := Vector3(0.0, 0.0, PROFILE[0][0] * BODY_LENGTH - BODY_LENGTH * 0.5) + for j in range(NUM_RING_VERTS): + var jn: int = (j + 1) % NUM_RING_VERTS + var v0 = rings[0][j] + var v1 = rings[0][jn] + st.set_color(COLOR_BACK); st.add_vertex(snout_center) + st.set_color(v1["col"]); st.add_vertex(v1["pos"]) + st.set_color(v0["col"]); st.add_vertex(v0["pos"]) + + st.generate_normals() + var mat := StandardMaterial3D.new() + mat.vertex_color_use_as_albedo = true + mat.roughness = 0.6 + mat.metallic = 0.1 + mesh = st.commit() + set_surface_override_material(0, mat) + + +# --------------------------------------------------------------------------- +# Dorsal fin +# --------------------------------------------------------------------------- +func _build_dorsal_fin() -> void: + var pivot := Node3D.new() + pivot.name = "DorsalFin" + # Position at ~55% along body, on top + pivot.position = Vector3(0.0, 0.28, -BODY_LENGTH * 0.5 + BODY_LENGTH * 0.52) + add_child(pivot) + _dorsal_pivot = pivot + + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + # Dorsal fin: curved triangle, base front-back, tip up + var pts: Array[Vector3] = [ + Vector3(-0.05, 0.0, 0.22), # base front + Vector3( 0.05, 0.0, 0.22), + Vector3(-0.03, 0.0, -0.18), # base rear + Vector3( 0.03, 0.0, -0.18), + Vector3( 0.00, 0.38, 0.05), # tip (slightly forward) + ] + var col := COLOR_BACK + # Front face + _add_tri(st, pts[0], pts[4], pts[2], col) + # Back face (reverse winding for double-sided appearance) + _add_tri(st, pts[1], pts[3], pts[4], col) + # Bridge between left and right at base + _add_quad(st, pts[0], pts[1], pts[4], pts[4], col) + _add_quad(st, pts[2], pts[4], pts[3], pts[4], col) + + st.generate_normals() + var mi := MeshInstance3D.new() + mi.name = "DorsalFinMesh" + var mat := StandardMaterial3D.new() + mat.albedo_color = COLOR_BACK + mat.roughness = 0.7 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + mi.mesh = st.commit() + mi.set_surface_override_material(0, mat) + pivot.add_child(mi) + + +# --------------------------------------------------------------------------- +# Pectoral fins +# --------------------------------------------------------------------------- +func _build_pectoral_fins() -> void: + _build_one_pec_fin(false) # left + _build_one_pec_fin(true) # right + + +func _build_one_pec_fin(right_side: bool) -> void: + var side: float = 1.0 if right_side else -1.0 + var pivot := Node3D.new() + pivot.name = "PecFinRight" if right_side else "PecFinLeft" + # At ~42% along body, sides of thorax + var base_x: float = 0.36 * side + pivot.position = Vector3(base_x, -0.05, -BODY_LENGTH * 0.5 + BODY_LENGTH * 0.42) + add_child(pivot) + + if right_side: + _pec_right = pivot + else: + _pec_left = pivot + + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + # Fin sweeps outward and slightly backward, flattened paddle shape + var tip := Vector3(side * 0.30, -0.12, -0.12) + var base_front := Vector3(0.0, 0.0, 0.12) + var base_rear := Vector3(0.0, 0.0, -0.12) + var col := COLOR_BACK + + _add_tri(st, base_front, tip, base_rear, col) + _add_tri(st, base_rear, tip, base_front, col) # double-sided + + st.generate_normals() + var mi := MeshInstance3D.new() + mi.name = "PecFinMesh" + var mat := StandardMaterial3D.new() + mat.albedo_color = COLOR_BACK + mat.roughness = 0.7 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + mi.mesh = st.commit() + mi.set_surface_override_material(0, mat) + pivot.add_child(mi) + + +# --------------------------------------------------------------------------- +# Tail peduncle pivot (for animation) +# --------------------------------------------------------------------------- +func _build_tail() -> void: + var pivot := Node3D.new() + pivot.name = "TailPivot" + # Place pivot at ~85% along body + pivot.position = Vector3(0.0, 0.0, -BODY_LENGTH * 0.5 + BODY_LENGTH * 0.85) + add_child(pivot) + _tail_pivot = pivot + + +# --------------------------------------------------------------------------- +# Caudal fluke (horizontal — dolphin/whale style) +# --------------------------------------------------------------------------- +func _build_caudal_fluke() -> void: + if _tail_pivot == null: + return + + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + # Horizontal bilobed fluke — symmetric left/right around X axis + # Offset from tail pivot (which is at 85%); fluke extends to 100% + var z_root: float = 0.0 # relative to pivot + var z_tip: float = -0.22 # tail tip relative to pivot + + var pts: Array[Vector3] = [ + Vector3( 0.0, 0.0, z_root), # centre root + Vector3( 0.45, 0.0, z_root - 0.06), # right lobe root + Vector3( 0.42, 0.0, z_tip), # right lobe tip + Vector3( 0.0, 0.0, z_tip + 0.08), # centre notch + Vector3(-0.42, 0.0, z_tip), # left lobe tip + Vector3(-0.45, 0.0, z_root - 0.06), # left lobe root + ] + var col := COLOR_BACK + + # Right lobe + _add_tri(st, pts[0], pts[1], pts[2], col) + _add_tri(st, pts[0], pts[2], pts[3], col) + # Left lobe + _add_tri(st, pts[0], pts[3], pts[4], col) + _add_tri(st, pts[0], pts[4], pts[5], col) + # Double-sided + _add_tri(st, pts[2], pts[1], pts[0], col) + _add_tri(st, pts[3], pts[2], pts[0], col) + _add_tri(st, pts[4], pts[3], pts[0], col) + _add_tri(st, pts[5], pts[4], pts[0], col) + + st.generate_normals() + var mi := MeshInstance3D.new() + mi.name = "CaudalFluke" + var mat := StandardMaterial3D.new() + mat.albedo_color = COLOR_BACK + mat.roughness = 0.7 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + mi.mesh = st.commit() + mi.set_surface_override_material(0, mat) + _tail_pivot.add_child(mi) + + +# --------------------------------------------------------------------------- +# Animation — called from DolphinController every _process frame +# --------------------------------------------------------------------------- +func animate(time: float, speed_factor: float, is_boosting: bool, turn_input: float) -> void: + if _tail_pivot == null: + return + + # Tail fluke oscillation (pitch = rotation X for horizontal fluke) + var freq: float = 8.0 if is_boosting else 4.5 + freq = lerp(freq * 0.4, freq, clamp(speed_factor, 0.0, 1.0)) + var amplitude: float = 0.55 if is_boosting else 0.38 + _tail_pivot.rotation.x = sin(time * freq) * amplitude + + # Secondary body wave — slight counter-rotation on body + rotation.x = sin(time * freq + 0.8) * amplitude * 0.18 + + # Turn lean — roll body when turning (Z axis = forward, lean on X or Z) + rotation.z = lerp(rotation.z, turn_input * 0.35, 0.12) + + # Pectoral fins flap gently + if _pec_left != null: + _pec_left.rotation.z = sin(time * freq * 0.5) * 0.12 + if _pec_right != null: + _pec_right.rotation.z = -sin(time * freq * 0.5) * 0.12 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, col: Color) -> void: + st.set_color(col); st.add_vertex(a) + st.set_color(col); st.add_vertex(b) + st.set_color(col); st.add_vertex(c) + + +func _add_quad(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, d: Vector3, col: Color) -> void: + _add_tri(st, a, b, c, col) + _add_tri(st, a, c, d, col) diff --git a/scripts/dolphin/DolphinMeshBuilder.gd.uid b/scripts/dolphin/DolphinMeshBuilder.gd.uid new file mode 100644 index 0000000..9a481b5 --- /dev/null +++ b/scripts/dolphin/DolphinMeshBuilder.gd.uid @@ -0,0 +1 @@ +uid://ulffyf0uy1ub