feat(dolphin): proper 3D mesh + swim animations (procedural)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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="."]
|
||||
|
||||
|
||||
298
scripts/dolphin/DolphinMeshBuilder.gd
Normal file
298
scripts/dolphin/DolphinMeshBuilder.gd
Normal file
@@ -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)
|
||||
1
scripts/dolphin/DolphinMeshBuilder.gd.uid
Normal file
1
scripts/dolphin/DolphinMeshBuilder.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ulffyf0uy1ub
|
||||
Reference in New Issue
Block a user