- 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>
299 lines
9.9 KiB
GDScript
299 lines
9.9 KiB
GDScript
@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)
|