1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//! Surface morphing — smooth interpolation between surface types.
//!
//! Provides a [`SurfaceMorph`] that blends between two [`Surface`] implementations
//! over a configurable duration. The blend parameter `t ∈ [0, 1]` is driven by an
//! easing curve (smooth-step by default) to give a visually pleasing transition.
//!
//! # Mesh blending
//!
//! Both surfaces are sampled on the same UV grid, and vertex positions are
//! linearly interpolated:
//!
//! ```text
//! P(u, v, t) = (1 − t) · surface_a.position(u, v) + t · surface_b.position(u, v)
//! ```
//!
//! Christoffel symbols and the metric are also blended (numerically) so that
//! geodesics computed on the morphing surface are geometrically consistent.
//!
//! # GLSL integration
//!
//! The `morph_t` uniform is forwarded to the vertex shader which can use
//! `mix(pos_a, pos_b, morph_t)` for GPU-side interpolation when both meshes are
//! uploaded as separate vertex buffers.
#![allow(dead_code)]
use glam::Vec3;
use rand::RngCore;
use crate::surface::Surface;
// ── Easing ────────────────────────────────────────────────────────────────────
/// Smooth-step easing: S(t) = t² (3 − 2t).
#[inline]
pub fn smoothstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
/// Smoother-step easing: S(t) = t³ (6t² − 15t + 10).
#[inline]
pub fn smootherstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}
// ── Morph state ────────────────────────────────────────────────────────────────
/// Progress of a surface morph.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MorphState {
/// Not morphing; showing surface A entirely.
AtA,
/// Morphing from A → B.
MorphingAToB {
/// Raw (uneased) blend progress [0, 1].
t: f32,
},
/// Not morphing; showing surface B entirely.
AtB,
/// Morphing from B → A.
MorphingBToA {
/// Raw (uneased) progress from B toward A, [0, 1].
t: f32,
},
}
// ── Surface morph ─────────────────────────────────────────────────────────────
/// Interpolates between two surfaces over time.
///
/// Drive with [`SurfaceMorph::tick`] each frame and read the current blend
/// parameter via [`SurfaceMorph::blend_t`]. Implement GPU-side blending by
/// uploading both meshes and using the GLSL snippet:
///
/// ```glsl
/// vec3 pos = mix(pos_a, pos_b, morph_t);
/// ```
pub struct SurfaceMorph {
/// Surface shown when the blend is 0.
pub surface_a: Box<dyn Surface>,
/// Surface shown when the blend is 1.
pub surface_b: Box<dyn Surface>,
/// Duration of a full A → B or B → A morph in seconds.
pub duration_s: f32,
state: MorphState,
/// Accumulated progress in seconds.
elapsed: f32,
}
impl SurfaceMorph {
/// Create a new morph between `surface_a` and `surface_b`.
pub fn new(surface_a: Box<dyn Surface>, surface_b: Box<dyn Surface>, duration_s: f32) -> Self {
Self {
surface_a,
surface_b,
duration_s: duration_s.max(0.01),
state: MorphState::AtA,
elapsed: 0.0,
}
}
/// Advance the morph by `dt` seconds.
pub fn tick(&mut self, dt: f32) {
match self.state {
MorphState::AtA | MorphState::AtB => {} // no-op
MorphState::MorphingAToB { ref mut t } => {
self.elapsed += dt;
*t = (self.elapsed / self.duration_s).min(1.0);
if *t >= 1.0 {
self.state = MorphState::AtB;
self.elapsed = 0.0;
}
}
MorphState::MorphingBToA { ref mut t } => {
self.elapsed += dt;
*t = (self.elapsed / self.duration_s).min(1.0);
if *t >= 1.0 {
self.state = MorphState::AtA;
self.elapsed = 0.0;
}
}
}
}
/// Start a morph from A to B (or reverse if already at B).
pub fn start_morph(&mut self) {
match self.state {
MorphState::AtA => {
self.state = MorphState::MorphingAToB { t: 0.0 };
self.elapsed = 0.0;
}
MorphState::AtB => {
self.state = MorphState::MorphingBToA { t: 0.0 };
self.elapsed = 0.0;
}
_ => {} // already morphing
}
}
/// The eased blend parameter in `[0, 1]`.
///
/// 0 = fully surface A, 1 = fully surface B.
pub fn blend_t(&self) -> f32 {
match self.state {
MorphState::AtA => 0.0,
MorphState::AtB => 1.0,
MorphState::MorphingAToB { t } => smootherstep(t),
MorphState::MorphingBToA { t } => 1.0 - smootherstep(t),
}
}
/// The raw (uneased) progress in `[0, 1]`.
pub fn raw_t(&self) -> f32 {
match self.state {
MorphState::AtA => 0.0,
MorphState::AtB => 1.0,
MorphState::MorphingAToB { t } | MorphState::MorphingBToA { t } => t,
}
}
/// Current morph state.
pub fn state(&self) -> MorphState {
self.state
}
/// Whether a morph is in progress.
pub fn is_morphing(&self) -> bool {
matches!(
self.state,
MorphState::MorphingAToB { .. } | MorphState::MorphingBToA { .. }
)
}
/// Build interpolated mesh vertices for the current blend parameter.
///
/// Returns `(vertices, indices)` where vertices are blended between the
/// two surface meshes.
pub fn blended_mesh(&self, u_steps: u32, v_steps: u32) -> (Vec<[f32; 3]>, Vec<u32>) {
let t = self.blend_t();
let (verts_a, indices) = self.surface_a.mesh_vertices(u_steps, v_steps);
let (verts_b, _) = self.surface_b.mesh_vertices(u_steps, v_steps);
let verts: Vec<[f32; 3]> = verts_a
.iter()
.zip(verts_b.iter())
.map(|(&a, &b)| {
[
a[0] * (1.0 - t) + b[0] * t,
a[1] * (1.0 - t) + b[1] * t,
a[2] * (1.0 - t) + b[2] * t,
]
})
.collect();
(verts, indices)
}
}
/// [`SurfaceMorph`] also implements [`Surface`] itself so it can be used
/// transparently wherever a `&dyn Surface` is expected.
impl Surface for SurfaceMorph {
fn position(&self, u: f32, v: f32) -> Vec3 {
let t = self.blend_t();
let pa = self.surface_a.position(u, v);
let pb = self.surface_b.position(u, v);
pa.lerp(pb, t)
}
fn metric(&self, u: f32, v: f32) -> [[f32; 2]; 2] {
let t = self.blend_t();
let ga = self.surface_a.metric(u, v);
let gb = self.surface_b.metric(u, v);
[
[
ga[0][0] * (1.0 - t) + gb[0][0] * t,
ga[0][1] * (1.0 - t) + gb[0][1] * t,
],
[
ga[1][0] * (1.0 - t) + gb[1][0] * t,
ga[1][1] * (1.0 - t) + gb[1][1] * t,
],
]
}
fn christoffel(&self, u: f32, v: f32) -> [[[f32; 2]; 2]; 2] {
let t = self.blend_t();
let ca = self.surface_a.christoffel(u, v);
let cb = self.surface_b.christoffel(u, v);
let mut out = [[[0.0f32; 2]; 2]; 2];
for k in 0..2 {
for i in 0..2 {
for j in 0..2 {
out[k][i][j] = ca[k][i][j] * (1.0 - t) + cb[k][i][j] * t;
}
}
}
out
}
fn wrap(&self, u: f32, v: f32) -> (f32, f32) {
// Use surface A's wrapping when close to A, B's when close to B.
let t = self.blend_t();
if t < 0.5 {
self.surface_a.wrap(u, v)
} else {
self.surface_b.wrap(u, v)
}
}
fn normal(&self, u: f32, v: f32) -> Vec3 {
let t = self.blend_t();
let na = self.surface_a.normal(u, v);
let nb = self.surface_b.normal(u, v);
na.lerp(nb, t).normalize()
}
fn random_position(&self, rng: &mut dyn RngCore) -> (f32, f32) {
// Delegate to whichever surface is dominant.
if self.blend_t() < 0.5 {
self.surface_a.random_position(rng)
} else {
self.surface_b.random_position(rng)
}
}
fn random_tangent(&self, u: f32, v: f32, rng: &mut dyn RngCore) -> (f32, f32) {
if self.blend_t() < 0.5 {
self.surface_a.random_tangent(u, v, rng)
} else {
self.surface_b.random_tangent(u, v, rng)
}
}
fn mesh_vertices(&self, u_steps: u32, v_steps: u32) -> (Vec<[f32; 3]>, Vec<u32>) {
self.blended_mesh(u_steps, v_steps)
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::surface::{sphere::Sphere, torus::Torus};
fn make_morph() -> SurfaceMorph {
SurfaceMorph::new(
Box::new(Torus::new(2.0, 0.7)),
Box::new(Sphere::new(1.5)),
5.0,
)
}
#[test]
fn test_morph_starts_at_a() {
let m = make_morph();
assert_eq!(m.blend_t(), 0.0);
assert_eq!(m.state(), MorphState::AtA);
}
#[test]
fn test_morph_progresses_over_time() {
let mut m = make_morph();
m.start_morph();
assert!(m.is_morphing());
m.tick(2.5); // halfway through 5s
let t = m.blend_t();
assert!(
t > 0.0 && t < 1.0,
"blend_t should be in (0,1) halfway: {t}"
);
}
#[test]
fn test_morph_completes() {
let mut m = make_morph();
m.start_morph();
m.tick(10.0); // well past duration
assert_eq!(m.state(), MorphState::AtB);
assert_eq!(m.blend_t(), 1.0);
}
#[test]
fn test_morph_reverses() {
let mut m = make_morph();
m.start_morph();
m.tick(10.0); // reach B
m.start_morph(); // reverse
m.tick(10.0); // back to A
assert_eq!(m.state(), MorphState::AtA);
assert_eq!(m.blend_t(), 0.0);
}
#[test]
fn test_blended_position_at_t0_equals_a() {
let m = make_morph();
let pa = m.surface_a.position(0.5, 0.5);
let pm = m.position(0.5, 0.5);
let diff = (pa - pm).length();
assert!(
diff < 1e-4,
"at t=0, morphed position should equal surface A: {diff}"
);
}
#[test]
fn test_blended_mesh_vertex_count() {
let m = make_morph();
let (verts, _) = m.blended_mesh(4, 4);
assert_eq!(verts.len(), 5 * 5);
}
#[test]
fn test_blended_mesh_all_finite() {
let mut m = make_morph();
m.start_morph();
m.tick(2.5);
let (verts, _) = m.blended_mesh(8, 8);
for v in &verts {
assert!(v.iter().all(|x| x.is_finite()), "vertex not finite: {v:?}");
}
}
#[test]
fn test_smootherstep_endpoints() {
assert!((smootherstep(0.0) - 0.0).abs() < 1e-6);
assert!((smootherstep(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_smootherstep_monotone() {
let a = smootherstep(0.3);
let b = smootherstep(0.7);
assert!(a < b, "smootherstep should be monotone increasing");
}
}