-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathvogelsimulator.html
More file actions
1092 lines (839 loc) · 84.7 KB
/
Copy pathvogelsimulator.html
File metadata and controls
1092 lines (839 loc) · 84.7 KB
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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="max-image-preview:large">
<title>Fourier & Ozeanwellen – die FFT interaktiv erklärt</title>
<meta name="description" content="Wie formt eine Idee von 1822 heute Wasser, Bilder und Musik? Die schnelle Fourier-Transformation (FFT) – interaktiv erklärt im Ozean-Renderer.">
<meta name="author" content="Mathias Leonhardt">
<meta name="fediverse:creator" content="@ki_mathias@mathstodon.xyz">
<link rel="canonical" href="https://ki-mathias.de/vogelsimulator.html">
<link rel="alternate" hreflang="de" href="https://ki-mathias.de/vogelsimulator.html">
<link rel="alternate" hreflang="en" href="https://ki-mathias.de/en/flight-simulator.html">
<link rel="alternate" hreflang="x-default" href="https://ki-mathias.de/vogelsimulator.html">
<!-- Open Graph -->
<meta property="og:type" content="article">
<meta property="og:title" content="Fourier, Ozeanwellen und 65.536 Frequenzen — Wie eine Idee von 1822 heute Wasser, Bilder und Musik formt">
<meta property="og:description" content="Wie die Fourier-Transformation von 1822 heute Ozeanwellen auf der GPU erzeugt, JPEG-Bilder komprimiert und Musik kodiert. Mit interaktiven Visualisierungen und der Geschichte von FFT bis Tessendorf.">
<meta property="og:url" content="https://ki-mathias.de/vogelsimulator.html">
<meta property="og:locale" content="de_DE">
<meta property="og:site_name" content="Mathias Leonhardt Blog">
<meta property="article:published_time" content="2026-04-13">
<meta property="article:modified_time" content="2026-04-23">
<meta property="article:author" content="Mathias Leonhardt">
<meta property="article:tag" content="Fourier-Transformation">
<meta property="article:tag" content="FFT">
<meta property="article:tag" content="Computergrafik">
<meta property="article:tag" content="Mathematik">
<meta property="article:tag" content="Three.js">
<meta property="article:tag" content="WebGPU">
<meta property="article:tag" content="Ocean4">
<!-- Twitter Card -->
<meta property="og:image" content="https://ki-mathias.de/img/og/vogelsimulator-de.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://ki-mathias.de/img/og/vogelsimulator-de.png">
<meta name="twitter:title" content="Fourier, Ozeanwellen und 65.536 Frequenzen">
<meta name="twitter:description" content="Wie eine Idee von 1822 heute Wasser, Bilder und Musik formt — die Geschichte der Fourier-Transformation, interaktiv erklärt.">
<!-- All resources self-hosted — zero external requests, fully DSGVO-compliant -->
<link rel="stylesheet" href="vendor/tailwind.css">
<link rel="stylesheet" href="vendor/katex/katex.min.css">
<link rel="preload" href="fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<script defer src="vendor/katex/katex.min.js"></script>
<script defer src="vendor/katex/auto-render.min.js"></script>
<script src="vendor/react.production.min.js"></script>
<script src="vendor/react-dom.production.min.js"></script>
<script defer src="vogelsimulator-components.js"></script>
<!-- Self-hosted Inter font (WOFF2, DSGVO-konform) -->
<style>
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url(fonts/inter-400.woff2) format('woff2'), url(fonts/inter-400.ttf) format('truetype'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap; src: url(fonts/inter-500.woff2) format('woff2'), url(fonts/inter-500.ttf) format('truetype'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url(fonts/inter-600.woff2) format('woff2'), url(fonts/inter-600.ttf) format('truetype'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 700; font-display: swap; src: url(fonts/inter-700.woff2) format('woff2'), url(fonts/inter-700.ttf) format('truetype'); }
</style>
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Fourier, Ozeanwellen und 65.536 Frequenzen \u2014 Wie eine Idee von 1822 heute Wasser, Bilder und Musik formt",
"description": "Wie die Fourier-Transformation von 1822 heute Ozeanwellen auf der GPU erzeugt, JPEG-Bilder komprimiert und Musik kodiert. Mit interaktiven Visualisierungen, KaTeX-Formeln und der Geschichte von FFT bis Tessendorf.",
"author": { "@type": "Person", "@id": "https://ki-mathias.de/#mathias", "name": "Mathias Leonhardt", "url": "https://ki-mathias.de/ueber-mich.html", "sameAs": ["https://mathstodon.xyz/@ki_mathias", "https://orcid.org/0009-0009-7154-5351", "https://philpeople.org/profiles/mathias-leonhardt", "https://github.com/pmmathias", "https://www.linkedin.com/in/mathias-leonhardt-96b885100/", "https://scholar.google.com/citations?user=hd8CgpsAAAAJ", "https://pmagentur.com"] },
"publisher": { "@id": "https://ki-mathias.de/#mathias" },
"datePublished": "2026-04-13",
"dateModified": "2026-04-23",
"mainEntityOfPage": { "@type": "WebPage", "@id": "https://ki-mathias.de/vogelsimulator.html" },
"inLanguage": "de",
"isAccessibleForFree": true,
"keywords": ["Fourier-Transformation", "FFT", "iFFT", "Octree", "Frustum Culling", "Ozean-Shader", "WebGPU", "Ocean4", "Three.js", "Computergrafik", "JPEG", "DCT"],
"articleSection": "Mathematik \u00b7 Computergrafik \u00b7 Fourier",
"wordCount": 9700,
"timeRequired": "PT43M"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Startseite",
"item": "https://ki-mathias.de/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Fourier, Ozeanwellen und 65.536 Frequenzen — Wie eine Idee von 1822 heute Wasser, Bilder und Musik formt",
"item": "https://ki-mathias.de/vogelsimulator.html"
}
]
}
</script>
<style>
html { scroll-behavior: smooth; }
body { font-family: 'Inter', system-ui, sans-serif; }
/* Scroll offset for fixed nav */
section[id] { scroll-margin-top: 4.5rem; }
/* Progress bar */
#progress-bar {
position: fixed; top: 0; left: 0; height: 3px; z-index: 100;
background: linear-gradient(90deg, #22d3ee, #60a5fa, #f59e0b);
transition: width 50ms linear;
}
/* KaTeX dark theme overrides */
.katex { color: #e2e8f0; }
.katex .mord, .katex .mbin, .katex .mrel, .katex .mopen, .katex .mclose,
.katex .mpunct, .katex .minner { color: #e2e8f0; }
/* Prose styling */
.prose-dark p { margin-bottom: 1.25rem; line-height: 1.8; color: #d1d5db; }
.prose-dark h2 {
font-size: 1.75rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem;
background: linear-gradient(90deg, #22d3ee, #60a5fa, #f59e0b);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.prose-dark h3 {
font-size: 1.25rem; font-weight: 600; color: #f1f5f9;
margin-top: 2.5rem; margin-bottom: 0.75rem;
}
.prose-dark strong { color: #f1f5f9; font-weight: 600; }
.prose-dark em { color: #94a3b8; font-style: italic; }
/* Interactive prompt styling */
.try-it {
border-left: 3px solid #22d3ee; padding: 1rem 1.25rem;
background: rgba(34, 211, 238, 0.05); border-radius: 0 0.75rem 0.75rem 0;
margin: 2rem 0;
}
.try-it p { color: #94a3b8; margin-bottom: 0.5rem; }
.try-it strong { color: #22d3ee; }
/* Summary box */
.summary-box {
border: 1px solid #374151; padding: 1.25rem 1.5rem;
border-radius: 0.75rem; background: rgba(55, 65, 81, 0.2);
margin: 2rem 0;
}
/* Glossary tooltip */
.glossary {
border-bottom: 1px dashed #60a5fa; cursor: help;
color: #93c5fd;
}
.glossary:hover { color: #bfdbfe; }
/* Section divider */
.section-divider {
width: 60px; height: 2px; margin: 3rem auto;
background: linear-gradient(90deg, #22d3ee, #f59e0b);
border-radius: 1px;
}
/* Nav active */
.nav-link { transition: color 0.2s; }
.nav-link.active { color: #22d3ee; }
/* Illustration figures */
.illus {
margin: 2rem auto; border-radius: 0.75rem; overflow: hidden;
border: 1px solid #1f2937; background: #111827;
}
.illus img {
width: 100%; max-width: 100%; height: auto; display: block;
opacity: 0.92; transition: opacity 0.3s;
image-rendering: auto;
}
.illus { width: fit-content; }
.illus img:hover { opacity: 1; }
.illus figcaption {
padding: 0.5rem 0.75rem; font-size: 0.7rem; color: #6b7280;
line-height: 1.4; border-top: 1px solid #1f2937;
}
.illus figcaption a { color: #6b7280; text-decoration: underline; }
.illus figcaption a:hover { color: #9ca3af; }
/* Size variants */
.illus-sm { max-width: 260px; }
.illus-md { max-width: 420px; }
.illus-lg { max-width: 580px; }
/* Float variants for inline placement */
@media (min-width: 640px) {
.illus-float-right { float: right; margin: 0.5rem 0 1rem 1.5rem; }
.illus-float-left { float: left; margin: 0.5rem 1.5rem 1rem 0; }
}
/* Clear floats */
.clearfix::after { content: ''; display: table; clear: both; }
.illus-row {
display: grid; gap: 1rem; margin: 2rem 0;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.illus-row { grid-template-columns: 1fr 1fr; }
}
.illus-row .illus { margin: 0; }
/* Responsive component containers */
.component-wrap {
margin: 2.5rem -0.5rem;
padding: 0;
}
@media (min-width: 640px) {
.component-wrap { margin: 2.5rem -2rem; }
}
@media (min-width: 1024px) {
.component-wrap { margin: 2.5rem -4rem; }
}
/* Display math centering */
.math-display { text-align: center; margin: 1.5rem 0; overflow-x: auto; }
/* Code block styling */
pre {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.25rem;
overflow-x: auto;
margin: 1.5rem 0;
font-size: 0.85rem;
line-height: 1.6;
}
code {
font-family: 'Menlo', 'Consolas', monospace;
color: #e2e8f0;
}
p code {
background: #1e293b;
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.85em;
}
.code-keyword { color: #c084fc; }
.code-string { color: #34d399; }
.code-number { color: #fbbf24; }
.code-comment { color: #64748b; }
/* CTA button */
.cta-btn {
display: inline-block;
background: linear-gradient(135deg, #22d3ee, #3b82f6);
color: #fff; font-weight: 600; font-size: 1.1rem;
padding: 0.85rem 2rem; border-radius: 0.75rem;
text-decoration: none; transition: transform 0.15s, box-shadow 0.15s;
box-shadow: 0 4px 20px rgba(34, 211, 238, 0.25);
}
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 30px rgba(34, 211, 238, 0.35);
}
/* Keyboard key styling */
kbd {
display: inline-block; background: #1e293b; border: 1px solid #374151;
border-radius: 0.25rem; padding: 0.1rem 0.45rem; font-size: 0.8em;
font-family: 'Menlo', 'Consolas', monospace; color: #e2e8f0;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
/* Cross-ref link */
.crossref {
border-bottom: 1px dashed #f59e0b; color: #fcd34d;
}
.crossref:hover { color: #fde68a; }
</style>
<link rel="alternate" type="application/atom+xml" title="Mathias Leonhardt Blog — RSS (Atom)" href="/feed.xml">
</head>
<body class="bg-gray-950 text-gray-100 antialiased">
<!-- Progress bar -->
<div id="progress-bar" style="width: 0%"></div>
<!-- TOC items for nav.js -->
<div id="nav-toc-items" style="display:none;">
<a href="#demo" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">1 · Probier es selbst</a>
<a href="#landschaft" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">2 · Eine Welt aus dem Nichts</a>
<a href="#vogelflug" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">3 · Der Vogel fliegt</a>
<a href="#octree" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">4 · 100.000 Bäume</a>
<a href="#fourier" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">5 · Die Fourier-Idee</a>
<a href="#fourier-uberall" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">6 · Fourier überall</a>
<a href="#ozean" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">7 · Vom Spektrum zum Ozean</a>
<a href="#bauprozess" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">8 · Wie wir gebaut haben</a>
<a href="#ki-reflexion" class="toc-link block text-sm py-1.5 px-3 text-gray-300 rounded-lg hover:bg-gray-800/60 transition">9 · Was das über KI sagt</a>
<a href="#faq" class="toc-link block text-sm py-1.5 px-3 text-gray-500 italic rounded-lg hover:bg-gray-800/60 transition">FAQ</a>
</div>
<!-- Navigation (generated by nav.js) -->
<nav class="fixed top-0 left-0 right-0 z-50 border-b border-gray-800/50 bg-gray-950/90 backdrop-blur-md">
<div id="nav-bar"></div>
</nav>
<div id="nav-dropdown" class="fixed top-14 left-0 right-0 z-40 hidden"></div>
<script src="/nav.js"></script>
<!-- ============================================================ -->
<!-- MAIN CONTENT -->
<!-- ============================================================ -->
<div class="max-w-4xl mx-auto">
<!-- HERO -->
<header id="hero" class="pt-24 pb-12 px-4 sm:px-6">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-3">Blogbeitrag · Mathematik · Computergrafik · Fourier</p>
<h1 class="text-3xl sm:text-4xl font-bold mb-4 bg-gradient-to-r from-cyan-400 via-blue-400 to-amber-400 bg-clip-text text-transparent leading-tight" style="-webkit-background-clip:text; -webkit-text-fill-color:transparent;">
Fourier, Ozeanwellen und 65.536 Frequenzen
</h1>
<p class="text-gray-400 max-w-2xl text-base sm:text-lg leading-relaxed mb-6">
Wie eine Idee von 1822 heute Wasser, Bilder und Musik formt — die Geschichte der Fourier-Transformation, von der mathematischen Einsicht über den FFT-Algorithmus bis zu 65.536 Wellen pro Frame auf der GPU.
</p>
<div class="flex items-center gap-3 text-xs text-gray-500">
<span>KI-Mathias</span>
<span>·</span>
<time datetime="2026-04-13">April 2026</time>
<span>·</span>
<span>~40 Min. Lesezeit</span>
</div>
</header>
<!-- YouTube Video Banner -->
<section class="mb-12 mx-auto max-w-4xl px-4 sm:px-6">
<div class="rounded-xl overflow-hidden border border-gray-700/50 bg-gray-900/50">
<div class="relative w-full" style="padding-bottom:56.25%">
<iframe class="absolute inset-0 w-full h-full" src="https://www.youtube-nocookie.com/embed/Tk-UhsrEnY4"
title="Ich steuere einen Vogel mit meinen Armen — KI hat den Code geschrieben" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="px-5 py-4 flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-200">Den Simulator in Aktion sehen?</p>
<p class="text-xs text-gray-500">90 Sekunden · Vogelflug über FFT-generierte Ozeanwellen</p>
</div>
<a href="https://youtu.be/Tk-UhsrEnY4" target="_blank" rel="noopener"
class="flex items-center gap-2 px-4 py-2 rounded-lg bg-red-600 hover:bg-red-500 text-white text-sm font-medium transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.5 6.2a3 3 0 0 0-2.1-2.1C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.4.6A3 3 0 0 0 .5 6.2 31.5 31.5 0 0 0 0 12a31.5 31.5 0 0 0 .5 5.8 3 3 0 0 0 2.1 2.1c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2.1A31.5 31.5 0 0 0 24 12a31.5 31.5 0 0 0-.5-5.8zM9.6 15.6V8.4l6.3 3.6-6.3 3.6z"/></svg>
Auf YouTube
</a>
</div>
</div>
</section>
<main class="max-w-4xl px-4 sm:px-6">
<!-- ============================================================ -->
<!-- SECTION 1: Probier es selbst -->
<!-- ============================================================ -->
<section id="demo" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 1</p>
<h2>Probier es selbst</h2>
<p>Bevor wir reden — flieg erst mal. Und schau dir dabei das Wasser an.</p>
<div class="try-it" style="text-align: center; padding: 2rem 1.5rem;">
<p style="margin-bottom: 1.25rem;">Der Simulator läuft komplett im Browser. Keine Installation, kein Download.</p>
<a href="https://pmmathias.github.io/birdybird/" target="_blank" rel="noopener" class="cta-btn">Jetzt fliegen →</a>
</div>
<p><strong>Steuerung per Tastatur:</strong></p>
<p>
<kbd>SPACE</kbd> Flügelschlag
<kbd>A</kbd>/<kbd>D</kbd> Drehen
<kbd>W</kbd> Sturzflug
<kbd>S</kbd> Steigen
<kbd>T</kbd> Webcam an/aus
</p>
<figure class="illus">
<a href="https://pmmathias.github.io/birdybird/" target="_blank" rel="noopener" title="Jetzt fliegen!">
<img src="img/vogel-hero.jpg" alt="Berglandschaft mit Seen, Wäldern und Nebel — prozedural generiert, mit FFT-Ozean im Vordergrund" loading="lazy" style="cursor:pointer;">
</a>
<figcaption>Die Welt des Vogelsimulators: prozedural generierte Landschaft, FFT-basierte Ozeanwellen, 100.000 Bäume. <strong><a href="https://pmmathias.github.io/birdybird/" target="_blank" rel="noopener" style="color:#22d3ee;">Klick zum Fliegen →</a></strong></figcaption>
</figure>
<p>Fliegst du über das Meer und siehst, wie sich die Wellen an jedem Kamm anders brechen, wie der Horizont ein gezacktes Band ist statt einer sauberen Linie — dann schaust du auf Fourier.</p>
<p>Dies ist die Geschichte einer mathematischen Idee. Joseph Fourier hatte 1822 eine Einsicht, die so fundamental war, dass sie 200 Jahre später buchstäblich in jedem JPEG-Bild, in jeder MP3-Datei, in jedem AAA-Ozean-Shader und auf jedem modernen Chip steckt. Die Idee selbst ist verblüffend einfach. Ihre Konsequenzen sind nahezu grenzenlos.</p>
<p>Wir werden sie auseinandernehmen. Aber erst erkläre ich dir, was du gerade geflogen bist.</p>
<div class="summary-box" style="text-align:center;">
<p class="text-sm text-gray-300"><strong>Open Source:</strong> Der gesamte Quellcode des Simulators ist öffentlich.</p>
<p class="text-sm" style="margin-bottom:0;">
<a href="https://github.com/pmmathias/birdybird" style="color:#22d3ee; text-decoration:underline;">github.com/pmmathias/birdybird</a>
</p>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 2: Eine Welt aus dem Nichts -->
<!-- ============================================================ -->
<section id="landschaft" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 2</p>
<h2>Eine Welt aus dem Nichts — Prozedurale Landschaft</h2>
<h3>Terrain ohne einen einzigen Vertex von Hand</h3>
<p>Die Landschaft, über die du geflogen bist, wurde nie von einem Menschen modelliert. Kein 3D-Künstler hat einen Hügel platziert, kein Designer hat ein Tal gezogen. Stattdessen beschreibt Mathematik, was wo entstehen soll, und der Computer generiert bei jedem Laden eine leicht andere, aber immer plausible Insel.</p>
<p>Das Grundprinzip ist überraschend schlicht: <strong>Parabeln als Bausteine.</strong> Stell dir einen flachen Tisch vor. Jetzt legst du unsichtbare Hügel darauf — jeden definiert durch einen Mittelpunkt \((c_x, c_z)\), einen Radius \(r\) und eine Höhe \(h\). An jedem Punkt des Terrains addierst du alle Beiträge:</p>
<div class="math-display">
$$ H(x,z) = \sum_i h_i \cdot \max\!\left(0,\; 1 - \frac{(x-c_{x,i})^2 + (z-c_{z,i})^2}{r_i^2}\right) $$
</div>
<p>900 positive Parabeln formen die Hügel. 270 negative Parabeln schneiden Täler hinein. Ein radialer Falloff lässt die Insel an den Rändern sanft ins Meer absinken. Das Ergebnis wirkt natürlich, weil Hügel in der Natur tatsächlich durch Überlagerung verschiedener Prozesse entstehen — Tektonik, Erosion, Ablagerung. Wir approximieren diesen Prozess mit Tausenden einfacher Funktionen.</p>
<figure class="illus">
<img src="img/vogel-terrain.jpg" alt="Dramatischer Berghang: Gras, Fels und Schnee — prozedural aus 900 Parabeln generiert" loading="lazy">
<figcaption>900 positive + 270 negative Parabeln + radialer Insel-Falloff = eine natürliche Berglandschaft.</figcaption>
</figure>
<h3>Fünf Texturschichten</h3>
<p>Geometrie allein erzeugt Klumpen, keine Landschaft. Farbe und Textur machen den Unterschied. Ein <span class="glossary" title="GLSL: OpenGL Shading Language. Eine C-ähnliche Programmiersprache, die direkt auf der GPU läuft.">GLSL</span>-Fragment-Shader berechnet pro Pixel, welche der fünf Texturschichten dominant ist: Sand am Strand, Gras auf den Hügeln, Erde in mittleren Höhen, Fels an Steilhängen, Schnee auf den Gipfeln. Die Übergänge verlaufen über eine <code>smoothstep</code>-Funktion, die scharfe Kanten vermeidet.</p>
<div class="illus-row">
<figure class="illus">
<img src="img/vogel-schnee.jpg" alt="Schneebedeckte Gipfel — weiße Kappe aus der Nähe" loading="lazy">
<figcaption>Schneebedeckte Gipfel — fünfte Texturschicht auf den höchsten Erhebungen.</figcaption>
</figure>
<figure class="illus">
<img src="img/vogel-textures.jpg" alt="Fels-zu-Schnee-Überblendung — fünf Schichten im Wechsel" loading="lazy">
<figcaption>Fels, Erde, Wald — fließende Übergänge zwischen den Schichten.</figcaption>
</figure>
</div>
<p>Gebäude entstehen ähnlich prozedural: fünf Typen (Wohnhäuser, Scheunen, Türme, Hotels, Hochhäuser), platziert durch Noise-Funktionen in flachen Uferbereichen. Eine zweite Noise-Ebene trennt Stadt- von Waldzonen, damit Bäume nicht durch Hausdächer wachsen. 900 Gebäude, 100.000 Bäume — alle generiert, keines von Hand gesetzt.</p>
<figure class="illus">
<img src="img/vogel-5layers.jpg" alt="Küstenlandschaft mit allen fünf Texturschichten: Sand, Gras, Fels, Schnee und Wasser" loading="lazy">
<figcaption>Alle fünf Schichten gleichzeitig sichtbar: Sand, Gras, Erde, Fels, Schnee.</figcaption>
</figure>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Zusammenfassung</p>
<p><strong>900 Parabeln + negative Arcs + radialer Insel-Falloff = eine natürliche Landschaft, ohne je einen Vertex manuell zu setzen. Fünf Texturschichten, 100.000 Bäume und 900 Gebäude — alles prozedural.</strong></p>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 3: Der Vogel fliegt -->
<!-- ============================================================ -->
<section id="vogelflug" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 3</p>
<h2>Der Vogel fliegt — Flugphysik und Webcam</h2>
<h3>Von der Kamera mit Höhenangst zum echten Gleiten</h3>
<p>Das Terrain war da. Aber ein Vogel, der nicht fliegt, ist nur eine Kamera mit Höhenangst. Der erste Ansatz war ein simples Arcade-Modell: Drück eine Taste, steig nach oben. Es funktionierte technisch. Aber es fühlte sich an wie ein Aufzug mit Panoramafenster — kein Gleiten, kein Auftrieb, kein <em>Fliegen</em>.</p>
<p>Also bauten wir ein aerodynamisches Modell. Die Zentraleinsicht: <strong>Auftrieb hängt vom Quadrat der Geschwindigkeit ab.</strong> Das ist der dynamische Druck — dieselbe Physik, die echte Vögel und Flugzeuge in der Luft hält.</p>
<div class="math-display">
$$ L = \frac{1}{2} \rho v^2 \cdot A_{\text{wing}} \cdot C_L $$
</div>
<p>Der erste Flügelschlag brachte nichts. <em>Gravity</em> gewann. Immer. Das Problem: Wir hatten den <strong>Wing-Incidence-Winkel</strong> vergessen. Echte Vogelflügel sind nicht flach — sie haben einen eingebauten Anstellwinkel von etwa 3°, der auch im Geradeausflug minimalen Auftrieb erzeugt. Ohne ihn ist der Flügel ein Brett, und ein Brett fällt. Sechs Iterationen des Physikmodells später stimmte das Verhältnis von Schwerkraft, Auftrieb und Flügelschlag. Bis ein Sturzflug sich gefährlich anfühlte und ein Looping den Magen zusammenzog.</p>
<figure class="illus">
<img src="img/vogel-coast.jpg" alt="Tiefflug über Küste mit Bäumen, Gebäuden und Wasser" loading="lazy">
<figcaption>Tiefflug durch die Siedlung — nach sechs Physikiterationen fühlte es sich wie Fliegen an.</figcaption>
</figure>
<h3>Arme als Flügel: MediaPipe Pose Detection</h3>
<p>Und dann der verrückte Teil: Was wäre, wenn du nicht die Tastatur benutzt, sondern deine Arme? <strong>MediaPipe Pose Detection</strong> macht das möglich. Googles ML-Modell erkennt 33 Körperpunkte in Echtzeit — direkt im Browser, ohne Server, ohne Cloud. Schultern, Ellbogen, Handgelenke: alles, was wir brauchen.</p>
<p>Die Logik: Wir messen die Höhe beider Handgelenke relativ zu den Schultern. Hohe Hände = Flügel oben. Niedrige Hände = Flügel unten. Die Änderungsrate dazwischen = Flügelschlag-Stärke. Das Webcam-Bild ist gespiegelt, Landmarks können verschwinden, die Erkennungsrate schwankt mit der Beleuchtung. Unsere Lösung war <strong>Graceful Degradation</strong>: Wenn MediaPipe keine Pose erkennt, fällt das System leise auf Tastatursteuerung zurück. Kein Fehler, kein Popup.</p>
<p>Das Gefühl, wenn du zum ersten Mal die Arme hebst und der Vogel steigt — das ist der Moment, in dem aus einem technischen Projekt etwas wird, das man Leuten zeigen möchte.</p>
<h3>Vom Webcam-Spiel zum Phone-Tilt: Mobile Steuerung</h3>
<p>Die Webcam-Steuerung funktioniert auf dem Desktop wunderschön — aber auf dem Handy ist sie unpraktisch. Also haben wir für <code>birdybird</code>, den mobile-first Ableger, eine zweite Steuerungsebene gebaut: <strong>Tilt-Fliegen</strong>. Das Handy ist der Steuerknüppel. Neigen nach links = Linkskurve. Neigen nach vorne = Sturzflug. Schütteln = Flügelschlag.</p>
<p>Die Browser-API heißt <code>DeviceOrientationEvent</code> und liefert drei Winkel: <em>alpha</em> (Kompass-Drehung), <em>beta</em> (Vor-/Zurückneigen), <em>gamma</em> (Seitneigung). Klingt einfach — in der Praxis hatten wir zwei größere Stolpersteine.</p>
<p><strong>Problem 1: Die Achsen bedeuten bei jedem Spieler etwas anderes.</strong> Hältst du das Handy flach vor dir wie ein Tablett, oder hochkant wie einen Comic? Links oder rechts gehaltet? Die sensorischen Rohdaten unterscheiden sich drastisch je nach Haltung — eine fest verdrahtete Achsenzuordnung scheitert bei der Hälfte aller Nutzer. Lösung: Ein <strong>6-Schritte-Kalibrierungs-Assistent</strong> beim ersten Start. Der Spieler hält das Handy in Ruhe-, Links-, Rechts-, Steig-, Sturz- und schüttelt einmal — wir messen jeweils die Achsen-Deltas und bestimmen daraus, welche Sensor-Achse (<em>beta</em> oder <em>gamma</em>) die größte Spannweite bei welcher Geste aufweist. Das Ergebnis wird in <code>localStorage</code> gespeichert; beim nächsten Start darf der Spieler entscheiden, ob er das Profil wiederverwenden oder neu kalibrieren will.</p>
<p><strong>Problem 2: iOS 13.4+ hat eine doppelte Permission-Falle.</strong> Seit Apple Nutzertracking über Bewegungssensoren fürchtet, müssen Web-Apps auf iOS <em>zwei separate</em> Permissions anfordern: <code>DeviceOrientationEvent.requestPermission()</code> für die Neigungswinkel, <code>DeviceMotionEvent.requestPermission()</code> für die Beschleunigung (und damit den Schütteln-zum-Flappen-Gest). In unserem ersten Release haben wir nur die erste angefordert — mit der Folge, dass der Kalibrierungs-Schritt „Flügelschlag!“ unendlich lange wartete, weil <code>devicemotion</code>-Events nie feuerten. Zweiteilefix: beide Permissions parallel anfordern, plus ein 8-Sekunden-Timeout mit „Skip“-Button für den Fall, dass Events trotzdem ausbleiben.</p>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Zusammenfassung</p>
<p><strong>Aerodynamisches Physikmodell (Bernoulli-Auftrieb), 6 Iterationen bis zum richtigen Gefühl, MediaPipe-Webcam-Steuerung mit Graceful Degradation — plus ein Tilt-Steuerungs-Pfad mit 6-Schritte-Kalibrierung für Mobilgeräte.</strong></p>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 4: 100.000 Bäume — Octree und Frustum Culling -->
<!-- ============================================================ -->
<section id="octree" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 4</p>
<h2>100.000 Bäume — Octree und Frustum Culling</h2>
<h3>Das Problem: 60 FPS mit 100.000 Objekten</h3>
<p>Eine Insel ohne Wälder wirkt kahl. Also platzierten wir Bäume. Viele Bäume. 100.000 Bäume.</p>
<p>Das naïve Vorgehen: Jeden Frame alle 100.000 Bäume an die GPU schicken und zeichnen lassen. Ergebnis: 4 FPS. Der Rechner kam nicht hinterher. Das überrascht nicht — selbst wenn ein Baum nur 200 Polygone hat, sind 100.000 Bäume 20 Millionen Polygone pro Frame. Eine moderne GPU kann das zwar theoretisch verarbeiten, aber der <em>Overhead pro Draw Call</em> ist das eigentliche Problem. Jede Einzel-Zeichenanweisung verursacht CPU-GPU-Kommunikation. 100.000 Draw Calls können auch die schnellste GPU in die Knie zwingen.</p>
<p>Die Lösung ist zweistufig: Erst entscheiden, <em>welche</em> Objekte überhaupt sichtbar sein können. Dann alle sichtbaren auf einmal zeichnen. Der erste Schritt ist <strong>Frustum Culling</strong>. Der zweite Schritt braucht eine räumliche Datenstruktur: den <strong>Octree</strong>.</p>
<h3>Das View Frustum: Sechs Ebenen definieren die Welt</h3>
<p>Eine <span class="glossary" title="Perspektivkamera: Kamera, bei der entfernte Objekte kleiner erscheinen als nahe. Das Sichtfeld hat die Form eines Kegelstumpfs.">Perspektivkamera</span> sieht keinen Vollkreis, sondern einen <strong><span class="glossary" title="Frustum: Geometrischer Körper, der durch zwei parallele Schnittebenen eines Kegels entsteht — wie ein Kegel mit abgeschnittener Spitze.">Frustum</span></strong> — einen Kegelstumpf, begrenzt durch sechs Ebenen:</p>
<ul class="list-disc pl-6 mb-4 text-gray-300 leading-relaxed">
<li><strong>Near Plane</strong> und <strong>Far Plane</strong>: minimale und maximale Sichtdistanz</li>
<li><strong>Left, Right, Top, Bottom</strong>: die vier Seitenflächen des Sichtkegels</li>
</ul>
<p>Jede dieser Ebenen lässt sich als Normalenvektor \(\mathbf{n}\) und einen Offset \(d\) schreiben. Ein Punkt \(\mathbf{p}\) liegt auf der sichtbaren Seite einer Ebene, wenn:</p>
<div class="math-display">
$$ \mathbf{n} \cdot \mathbf{p} + d \geq 0 $$
</div>
<p>Ein Objekt ist sichtbar, wenn es auf der sichtbaren Seite <em>aller sechs Ebenen</em> liegt — oder genauer: wenn seine <span class="glossary" title="AABB: Axis-Aligned Bounding Box. Ein achsenparalleler Quader, der ein Objekt vollständig einschließt. Schnelle Kollisions- und Sichtbarkeitsprüfungen.">AABB</span> (achsenparalleler Begrenzungsquader) zumindest teilweise innerhalb des Frustums liegt. Die Frustum-Ebenen werden aus der Projektionsmatrix der Kamera extrahiert. Pro Frame: sechs Ebenengleichungen aus der aktuellen Kameraposition berechnen, fertig.</p>
<figure class="my-8">
<img src="img/view-frustum.svg" alt="View Frustum: Ein Kegelstumpf aus sechs Ebenen, der das Sichtfeld einer 3D-Kamera definiert." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:500px; background:#0f172a;" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">View Frustum einer Perspektivkamera. <a href="https://commons.wikimedia.org/wiki/File:ViewFrustum.svg" class="text-gray-500 hover:text-gray-400" target="_blank" rel="noopener">Wikimedia Commons</a></figcaption>
</figure>
<p>Der naive Frustum-Check hat Komplexität \(O(n)\) — wir müssen jedes der 100.000 Objekte gegen die sechs Ebenen prüfen. Bei 60 FPS sind das 6 Millionen Ebenenprüfungen pro Sekunde. Das ist überraschend schnell — CPUs können das. Aber die Frage ist, ob wir klug vorgehen können: Wenn wir wissen, dass alle Bäume in einem bestimmten Raumbereich außerhalb des Frustums liegen, müssen wir die einzelnen Bäume gar nicht erst prüfen.</p>
<h3>Der Octree: Von \(O(n)\) zu \(O(\log n)\)</h3>
<p>Ein <strong><span class="glossary" title="Octree: Räumliche Datenstruktur, die einen Würfel rekursiv in 8 gleich große Teilwürfel unterteilt. Jeder Knoten hat genau 8 Kinder.">Octree</span></strong> unterteilt den Raum rekursiv in acht gleichgroße Würfel. Jeder Würfel, der Objekte enthält, wird weiter unterteilt — bis zu einer Mindestgröße oder einer Maximaltiefe. Das Ergebnis ist ein Baum: die Wurzel repräsentiert die gesamte Welt, die Blätter kleine Raumzellen mit je wenigen Objekten.</p>
<figure class="my-8">
<img src="img/octree-diagram.svg" alt="Octree-Diagramm: Rekursive Raumunterteilung mit Kamera-Frustum. Links die räumliche Ansicht, rechts die Baumstruktur. O(log n) statt O(n) Tests." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:600px" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">Octree-Abfrage: Nur Knoten innerhalb des Frustums werden besucht — von 100.000 auf ~4.000 Objekte.</figcaption>
</figure>
<p>Beim Frustum Culling durchläuft der Algorithmus den Octree von der Wurzel nach unten:</p>
<ol class="list-decimal pl-6 mb-4 text-gray-300 leading-relaxed">
<li>Prüfe, ob die AABB des aktuellen Knotens <em>komplett außerhalb</em> des Frustums liegt. Wenn ja: gesamten Teilbaum verwerfen. Kein einziges Kind wird noch betrachtet.</li>
<li>Liegt der Knoten <em>komplett innerhalb</em>? Dann alle Objekte darin rendern, ohne weitere Prüfungen.</li>
<li>Liegt er <em>teilweise innerhalb</em>? Alle acht Kinder rekursiv prüfen.</li>
</ol>
<p>Im Idealfall — wenn ein großer Teil der Welt hinter der Kamera liegt — verwirft der Algorithmus ganze Äste des Baums auf hohen Ebenen. Von 100.000 Objekten werden dann nur ein paar tausend Octree-Knoten überhaupt besucht. Die effektive Komplexität nähert sich \(O(\log n)\) — logarithmisch statt linear.</p>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Technische Kennzahlen</p>
<ul class="list-disc pl-6 text-gray-300 leading-relaxed">
<li>100.000 Bäume in der Szene</li>
<li>Octree-Tiefe: 6 Ebenen, bis zu 8&sup6; = 262.144 Blätter (in der Praxis deutlich weniger, da die Insel nicht kubisch ist)</li>
<li>Typisch gerendert pro Frame: 3.000 – 5.000 Bäume</li>
<li><strong>Faktoreinsparung: ~25×</strong> weniger Draw Calls gegenüber dem naïven Ansatz</li>
</ul>
</div>
<p>Der Octree-Aufbau geschieht einmal beim Laden, in etwa 80 ms. Danach ist er statisch — Bäume bewegen sich nicht. Die Abfrage pro Frame dauert unter 0,5 ms. Das gibt uns 60 FPS mit einer Welt, die naïv unrenderable wäre.</p>
<h3>Wenn ein InstancedMesh zu groß wird: Per-Cluster-Split</h3>
<p>Beim Port auf WebGPU stießen wir auf eine zweite Perf-Mauer — diesmal nicht wegen der Bäumezahl, sondern wegen der <em>Struktur</em> unseres Meshes. Der Forest wird aus einem <strong>InstancedMesh</strong> gerendert: <em>eine</em> Zeichenanweisung für alle Bäume, mit per-Instanz Positions-Matrix. Das ist effizienter als 100.000 separate Draw Calls — aber es macht Frustum-Culling unmöglich, denn ein InstancedMesh hat nur <em>eine</em> Bounding-Sphere, die alle Instanzen umschließen muss. In unserem Fall war diese Sphere so groß wie die gesamte Welt. Three.js konnte sie also nie verwerfen, egal wohin die Kamera schaute. Der Vertex-Shader lief pro Frame über alle ~1 Million Instanzen, selbst wenn die Kamera in die leere Ecke schaute.</p>
<p>Die Lösung: den monolithischen InstancedMesh nach der Generierung in <strong>20 kleinere Sub-Meshes</strong> aufteilen, einen pro Baumcluster. Jede Sub-Mesh behält dieselbe Material- und Geometrie-Referenz (keine Speicherverdopplung), hat aber nur die Instanz-Matrizen für ihren Cluster — und damit eine <em>enge</em> Bounding-Sphere. Three.js' Frustum-Culler greift jetzt cluster-genau zu: Bäume außerhalb des Sichtfelds werden komplett aus der Pipeline geworfen, Vertex-Shader inklusive.</p>
<p>Gemessener Effekt in unserem Perf-Benchmark: <strong>~3× mehr FPS auf WebGPU</strong> in allen Szenarien (z.B. 36 → 124 FPS über Ozean, 34 → 71 FPS im Wald). Die klassische Lektion: <em>Bounding-Volume-Granularität ist nicht Implementierungsdetail, sondern architekturelle Entscheidung.</em></p>
<h3>Warum das für Fourier relevant ist</h3>
<p>Octree und Frustum Culling sind Raumpartitionierungstechniken: Sie lösen das Problem, <em>welche Teile</em> einer komplexen Struktur relevant sind. Die Fourier-Transformation löst dasselbe Problem — aber im Frequenzraum statt im geometrischen Raum. Beide Ideen basieren auf derselben Grundintuition: <em>Nicht alles gleichzeitig betrachten. Verständnis kommt durch Zerlegung.</em></p>
<div class="component-wrap"><div id="mount-octree-query"></div></div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 5: Die Fourier-Idee -->
<!-- ============================================================ -->
<section id="fourier" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 5</p>
<h2>Die Fourier-Idee</h2>
<h3>Eine Behauptung, die vernünftige Mathematiker empörte</h3>
<p>Im Jahr 1807 legte Joseph Fourier der Pariser Académie des Sciences ein Manuskript vor. Seine Behauptung war schlicht und gleichzeitig ungeheuerlich: <strong>Jede periodische Funktion lässt sich als unendliche Summe von Sinus- und Kosinusfunktionen darstellen.</strong> Nicht manche Funktionen. Alle. Auch eckige Rechteckfunktionen. Auch Sägezähne. Auch sprunghafte Stufenfunktionen, die offensichtlich nicht glatt sind.</p>
<p>Lagrange, einer der größten Mathematiker seiner Zeit, hielt die Behauptung für falsch und blockierte die Veröffentlichung. Erst 1822 erschien Fouriers Buch <em>Théorie analytique de la chaleur</em> — und die Mathematik der nächsten zwei Jahrhunderte war eine andere.</p>
<p>Was ist so besonders an Sinus und Kosinus? Sie sind die <strong>natürlichen Basisfunktionen periodischer Phänomene</strong>. Jedes Ding, das schwingt — eine Gitarrensaite, eine Meereswelle, das elektrische Feld eines Lasers — schwingt im Kern wie ein Sinus. Die Fourier-Reihe sagt: Jede beliebige periodische Form ist eine Mischung solcher reiner Schwingungen.</p>
<h3>Die Fourier-Reihe</h3>
<p>Sei \(f(t)\) eine periodische Funktion mit Periode \(T\). Dann gilt:</p>
<div class="math-display">
$$ f(t) = a_0 + \sum_{n=1}^{\infty} \left[ a_n \cos\!\left(\frac{2\pi n}{T} t\right) + b_n \sin\!\left(\frac{2\pi n}{T} t\right) \right] $$
</div>
<p>Die Koeffizienten \(a_n\) und \(b_n\) bestimmen, wie stark jede Frequenz \(n/T\) in der Funktion vertreten ist. Sie lassen sich durch Integration berechnen:</p>
<div class="math-display">
$$ a_n = \frac{2}{T} \int_0^T f(t)\,\cos\!\left(\frac{2\pi n}{T} t\right) dt \qquad b_n = \frac{2}{T} \int_0^T f(t)\,\sin\!\left(\frac{2\pi n}{T} t\right) dt $$
</div>
<p>Was bedeuten diese Integrale geometrisch? Sie messen, <em>wie ähnlich</em> die Funktion \(f\) jeweils einer Sinusschwingung mit Frequenz \(n/T\) ist. Multipliziere \(f\) mit einem Kosinus und integriere — du erhälst die Korrelation. <strong>Fourier-Analyse ist systematische Korrelation mit allen möglichen Frequenzen.</strong></p>
<h3>Die Fourier-Transformation</h3>
<p>Für nicht-periodische Funktionen verallgemeinert sich die Reihe zur <strong>Fourier-Transformation</strong>:</p>
<div class="math-display">
$$ \hat{f}(\xi) = \int_{-\infty}^{\infty} f(t)\, e^{-2\pi i \xi t}\, dt $$
</div>
<p>Hier ist \(\hat{f}(\xi)\) eine komplexe Zahl für jede Frequenz \(\xi\). Der Betrag \(|\hat{f}(\xi)|\) ist die <em>Amplitude</em>, das Argument \(\arg(\hat{f}(\xi))\) ist die <em>Phase</em> der jeweiligen Frequenzkomponente. Und die Umkehrung — die <strong>inverse Fourier-Transformation</strong> — rekonstruiert die Originalfunktion:</p>
<div class="math-display">
$$ f(t) = \int_{-\infty}^{\infty} \hat{f}(\xi)\, e^{2\pi i \xi t}\, d\xi $$
</div>
<p>Damit ist die Fourier-Transformation ein <strong>vollständig umkehrbarer Koordinatenwechsel</strong>: von der Zeit- oder Ortsdomäne in die Frequenzdomäne und zurück. Kein Information geht verloren. Es ist buchstäblich nur eine andere Sichtweise auf dieselben Daten.</p>
<h3>Die Analogie zu PCA: ein rotiertes Koordinatensystem</h3>
<p>Wer den <a href="eigenwerte.html" class="crossref">Blogpost über Eigenwerte und KI</a> gelesen hat, erkennt eine tiefe Parallele. <span class="glossary" title="PCA: Principal Component Analysis. Methode, die die Richtungen größter Varianz in einem Datensatz als neues Koordinatensystem verwendet.">PCA</span> dreht das Koordinatensystem so, dass die Achsen mit den Richtungen größter Varianz im Datensatz ausgerichtet sind. Die neuen Achsen sind die Eigenvektoren der Kovarianzmatrix — <em>orthogonale Basisvektoren, die optimal zu den Daten passen.</em></p>
<p>Die Fourier-Transformation tut strukturell dasselbe: Sie rotiert das Funktionenraum-Koordinatensystem in die Basis der Sinusfunktionen. Diese Basis ist orthogonal im Sinne des Funktionen-Skalarprodukts — zwei Sinusfunktionen verschiedener Frequenzen sind senkrecht aufeinander:</p>
<div class="math-display">
$$ \int_0^T \sin(2\pi m t / T)\,\sin(2\pi n t / T)\, dt = \begin{cases} T/2 & m = n \\ 0 & m \neq n \end{cases} $$
</div>
<p>Der Unterschied: <strong>PCA findet die optimale Basis aus den Daten selbst. FFT verwendet eine feste, universelle Basis — die Frequenzen.</strong> PCA zerlegt in Richtungen maximaler Varianz; FFT zerlegt in Schwingungsfrequenzen. Beide sind orthogonale Projektionen auf eine neue Basis.</p>
<figure class="my-8">
<img src="img/pca-fourier-analogy.svg" alt="PCA und Fourier-Transformation im Vergleich: Beides sind orthogonale Rotationen, die verborgene Struktur sichtbar machen." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:600px" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">PCA rotiert Koordinatenachsen in die Daten. Fourier rotiert in Frequenzen. Dieselbe Idee.</figcaption>
</figure>
<p>Eine weitere Parallele findet sich in der <a href="musik.html" class="crossref">Cochlea des menschlichen Ohrs</a>: Die Schnecke des Innenohrs ist eine mechanische Fourier-Analyse. Unterschiedliche Stellen der Basilarmembran resonieren auf unterschiedliche Frequenzen — das Ohr zerlegt den Schall in seine Fourier-Komponenten, bevor das Gehirn ihn verarbeitet. Evolution hat dieselbe Mathematik 500 Millionen Jahre früher „entdeckt“ als Fourier.</p>
<h3>Die Fast Fourier Transform: von \(O(N^2)\) zu \(O(N \log N)\)</h3>
<p>Die diskrete Fourier-Transformation (DFT) übersetzt die Fourier-Analyse auf endliche Folgen von \(N\) Messwerten:</p>
<div class="math-display">
$$ X[k] = \sum_{n=0}^{N-1} x[n]\, e^{-2\pi i k n / N}, \qquad k = 0, 1, \ldots, N-1 $$
</div>
<p>Naïv berechnet: für jeden der \(N\) Ausgabewerte \(X[k]\) muss über alle \(N\) Eingabewerte summiert werden. Das ist \(O(N^2)\). Bei \(N = 65.536\) — der Auflösung unseres Ozeansimulators — wären das \(4{,}3 \cdot 10^9\) komplexe Multiplikationen. Pro Frame. Unmöglich.</p>
<p>1965 publizierten <strong>James Cooley</strong> und <strong>John Tukey</strong> einen Algorithmus, der die DFT in \(O(N \log N)\) berechnet: die <strong>Fast Fourier Transform (FFT)</strong>. Die Idee: Die DFT einer Folge der Länge \(N\) kann in zwei DFTs der Länge \(N/2\) zerlegt werden — eine für die geraden, eine für die ungeraden Indizes. Dieses Divide-and-Conquer-Schema wird rekursiv angewendet, bis man bei DFTs der Länge 1 ankommt (Trivialergebnis). Mit \(\log_2(N)\) Rekursionsstufen und \(N/2\) Operationen pro Stufe ergibt sich:</p>
<div class="math-display">
$$ T_{\text{FFT}}(N) = \frac{N}{2} \log_2 N \quad \text{vs.} \quad T_{\text{DFT}}(N) = N^2 $$
</div>
<p>Bei \(N = 65.536 = 2^{16}\): DFT bräuchte \(4{,}3 \cdot 10^9\) Operationen, FFT braucht \(5{,}2 \cdot 10^5\). <strong>Faktor 8.000 schneller.</strong></p>
<p>Der Datenfluss des Cooley-Tukey-Algorithmus sieht bei grafischer Darstellung aus wie ein Schmetterling: Jede Stufe kombiniert Ergebnisse paarweise über komplexe Rotationen um die <span class="glossary" title="Twiddle-Faktoren: Komplexe Einheitswurzeln e^{-2\pi i k/N}, die bei jedem Schritt des FFT-Butterfly-Algorithmus als Rotationsfaktoren dienen.">Twiddle-Faktoren</span> \(W_N^k = e^{-2\pi ik/N}\). Dieser visuelle Eindruck hat dem Verfahren seinen Namen gegeben: <strong>Butterfly-Algorithmus</strong>.</p>
<figure class="my-8">
<img src="img/fft-butterfly.svg" alt="FFT-Butterfly-Diagramm für N=8: Drei Stufen mit Twiddle-Faktoren. O(N log N) statt O(N²)." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:600px" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">Das Butterfly-Diagramm der FFT: 3 Stufen für N=8 statt 64 einzelner Multiplikationen.</figcaption>
</figure>
<div class="component-wrap"><div id="mount-fourier-decomposition"></div></div>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Die Kernidee</p>
<p><strong>Fourier 1822: Jede Funktion = Summe von Sinusfunktionen. Cooley & Tukey 1965: Diese Summe lässt sich in O(N log N) statt O(N²) berechnen. Diese beiden Ideen zusammen haben Signalverarbeitung, Bildkompression, Audiokodierung und GPU-basierte Ozeanphysik erst möglich gemacht.</strong></p>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 6: Fourier überall -->
<!-- ============================================================ -->
<section id="fourier-uberall" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 6</p>
<h2>Fourier überall — Bilder, JPEG, MP3, Phasenkorrelation</h2>
<h3>Bilder im Frequenzraum</h3>
<p>Die Fourier-Transformation ist nicht auf zeitabhängige Signale beschränkt. Bilder sind zweidimensionale Funktionen — die Helligkeit an jedem Pixel als Funktion von \((x, y)\). Also gibt es auch eine <strong>2D-FFT</strong>:</p>
<div class="math-display">
$$ F(u, v) = \sum_{x=0}^{M-1} \sum_{y=0}^{N-1} f(x,y)\, e^{-2\pi i (ux/M + vy/N)} $$
</div>
<p>Was bedeuten die Frequenzen bei Bildern? <strong>Niedrige Frequenzen</strong> — Punkte nahe dem Zentrum des Frequenzraums — kodieren langsame Änderungen: Hintergründe, Farbverläufe, die grobe Struktur des Bildes. <strong>Hohe Frequenzen</strong> — Punkte am Rand — kodieren schnelle Änderungen: Kanten, feine Texturen, Details.</p>
<p>Diese Zerlegung hat eine wichtige praktische Konsequenz: Die meisten natürlichen Bilder haben eine <strong>stark geclusterte Energieverteilung</strong>. Fast alle Information steckt in den niedrigen Frequenzen. Die hohen Frequenzen tragen wenig bei — und sie können deshalb weggeworfen oder vergröbert werden, ohne dass das menschliche Auge den Unterschied bemerkt.</p>
<p>Die 2D-FFT ermöglicht auch <strong>schnelle Faltung</strong>: Einen Weichzeichner auf ein Bild anzuwenden ist im Ortsraum eine teure Faltungsoperation (\(O(N^2 \cdot K^2)\) für ein \(N\times N\)-Bild mit einem \(K\times K\)-Kernel). Im Frequenzraum ist es eine einfache <em>punktweise Multiplikation</em> (\(O(N^2)\)). Damit ist der typische Weg: FFT berechnen, multiplizieren, inverse FFT — und das schon bei moderat großen Bildern deutlich schneller als direkte Faltung.</p>
<figure class="my-8">
<img src="img/jpeg-compression.svg" alt="JPEG-Kompression: Bild in 8×8 Blöcke aufteilen, DCT anwenden, Quantisierung der hohen Frequenzen, Entropie-Codierung." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:650px" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">Die JPEG-Pipeline: DCT transformiert jeden 8×8-Block in Frequenzen, Quantisierung entfernt Details, Entropie-Codierung komprimiert.</figcaption>
</figure>
<h3>JPEG: Fourier in 8×8-Blöcken</h3>
<p>Jedes JPEG-Bild, das du je gesehen hast, steckt voller Fourier-Mathematik — in einer leicht modifizierten Form. JPEG verwendet die <strong>Diskrete Kosinus-Transformation (DCT)</strong>, eine nahe Verwandte der DFT, die nur reelle Zahlen produziert und sich für die Bildkompression als besser geeignet erwies.</p>
<p>Der JPEG-Kompressionsalgorithmus arbeitet in fünf Schritten:</p>
<ol class="list-decimal pl-6 mb-4 text-gray-300 leading-relaxed">
<li><strong>Aufteilung</strong>: Das Bild wird in 8×8-Pixel-Blöcke zerlegt.</li>
<li><strong>DCT</strong>: Jeder Block wird in 64 Frequenzkoeffizienten verwandelt — von DC (Gleichanteil, mittlere Helligkeit) bis zu den höchsten Ortsfrequenzen.</li>
<li><strong>Quantisierung</strong>: Die Koeffizienten werden durch eine Quantisierungsmatrix geteilt und gerundet. Niedrige Frequenzen werden fein erhalten, hohe Frequenzen grob. <em>Das ist der verlustbehaftete Schritt.</em></li>
<li><strong>Runlängen-Kodierung</strong>: Koeffizienten werden in Zickzack-Reihenfolge gelesen — von niedrigen zu hohen Frequenzen. Da hohe Frequenzen nach der Quantisierung oft Null sind, entstehen lange Nullfolgen, die kompakt kodiert werden.</li>
<li><strong>Huffman-Kodierung</strong>: Lossless-Kompression der quantisierten Werte.</li>
</ol>
<p>Das Ergebnis: Ein Bild, das mit Qualität 80 gespeichert wird, behält präzise die niedrigen Frequenzen (grobe Struktur) und quantisiert die hohen Frequenzen (feine Details) grob. Das menschliche Auge bemerkt meist nur den Unterschied, wenn die Qualität sehr niedrig gesetzt wird und die typischen JPEG-Artefakte — blockige 8×8-Muster — sichtbar werden. Das sind buchstäblich die 8×8-DCT-Blöcke, die sich bei extremer Kompression durchdrücken.</p>
<h3>MP3 und MDCT: Fourier für das Ohr</h3>
<p>MP3 macht Ähnliches für Audio. Die <strong>Modified Discrete Cosine Transform (MDCT)</strong> zerlegt das Audiosignal in Frequenzbänder — ähnlich wie die Cochlea des Innenohrs. Dann nutzt das psychoakustische Modell eine Einsicht: <strong>Leise Frequenzen, die neben lauten liegen, werden vom Gehör maskiert</strong> und können weggeworfen werden, ohne das die meisten Menschen den Unterschied bemerken. Eine detailliertere Diskussion der Audioperzeption, Cochlea und Obertonreihe findet sich im <a href="musik.html" class="crossref">Blogpost über die Mathematik der Musik</a>.</p>
<h3>Phasenkorrelation: automatische Video-Musik-Synchronisation</h3>
<p>Eine weniger bekannte Anwendung: <strong>Phasenkorrelation</strong> für die automatische Suche nach passenden Musikteilen. Beim Schnitt von Videos kommt es vor, dass man zwei ähnliche Audiosequenzen hat — zwei Takes desselben Musikstücks — und exakt bestimmen möchte, an welcher Stelle der eine zum anderen passt.</p>
<p>Im Zeitraum wäre das eine teure Kreuzkorrelation: Alle möglichen Verschiebungen durchprobieren, für jede die Korrelation berechnen. Im Frequenzraum ist es ein Einzeiler: FFT berechnen, Quotienten der normalisierten Spektren bilden, inverse FFT — das Ergebnis ist ein Impuls genau an der Verschiebung mit maximaler Übereinstimmung. <strong>Exakte Synchronisationssuche in \(O(N \log N)\) statt \(O(N^2)\).</strong></p>
<p>Und damit kommen wir zur entscheidenden Frage: Was passiert, wenn man die Fourier-Transformation rückwärts anwendet — wenn man nicht ein Signal <em>analysiert</em>, sondern aus einem Frequenzspektrum ein Signal <em>synthetisiert</em>?</p>
<div class="component-wrap"><div id="mount-wave-spectrum"></div></div>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Fourier in der Praxis</p>
<ul class="list-disc pl-6 text-gray-300 leading-relaxed">
<li><strong>2D-FFT:</strong> Schnelle Bildfilterung, Faltung als Multiplikation im Frequenzraum</li>
<li><strong>JPEG (DCT):</strong> 8×8-Blöcke, Quantisierung hoher Frequenzen, Blockierartefakte bei niedriger Qualität</li>
<li><strong>MP3 (MDCT):</strong> Psychoakustische Maskierung, Frequenzbänder wie die Cochlea</li>
<li><strong>Phasenkorrelation:</strong> Automatische Synchronisation in \(O(N \log N)\)</li>
</ul>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 7: Von der Statistik zum Ozean — iFFT auf der GPU -->
<!-- ============================================================ -->
<section id="ozean" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 7</p>
<h2>Von der Statistik zum Ozean — iFFT auf der GPU</h2>
<h3>Was passiert, wenn man Fourier rückwärts dreht?</h3>
<p>Fourier-Analyse nimmt ein Signal und findet die enthaltenen Frequenzen. Die <strong>inverse Fourier-Transformation (iFFT)</strong> macht das Gegenteil: Sie nimmt ein Frequenzspektrum und synthetisiert daraus ein Signal. Du definierst, welche Frequenzen mit welcher Amplitude und Phase vorhanden sein sollen — und die iFFT baut daraus die zugehörige Wellenform.</p>
<p>Genau das macht der Ozeansimulator. Nicht mit Audiosignalen, sondern mit <em>Wasseroberflächen</em>.</p>
<h3>Das Phillips-Spektrum: Ozeane im Frequenzraum</h3>
<p>Owen Phillips formulierte 1957 ein statistisches Modell dafür, wie sich Wellenenergie auf verschiedene Frequenzen und Richtungen verteilt — abhängig von Windgeschwindigkeit, Windrichtung und dem sogenannten <span class="glossary" title="Fetch: Die Strecke, über die der Wind ungehindert über das Wasser geweht hat. Je größer der Fetch, desto größer die Wellen.">Fetch</span> (der Strecke, über die der Wind geweht hat). Das Ergebnis: das <strong>Phillips-Spektrum</strong>:</p>
<div class="math-display">
$$ P(\mathbf{k}) = A \, \frac{\exp\!\bigl(-1/(kL)^2\bigr)}{k^4} \, \bigl|\hat{\mathbf{k}} \cdot \hat{\mathbf{w}}\bigr|^2 $$
</div>
<p>Dabei ist \(\mathbf{k}\) der Wellenvektor im Frequenzraum, \(k = |\mathbf{k}|\) seine Länge, \(L = v^2/g\) die charakteristische Wellenlänge bei Windgeschwindigkeit \(v\) und \(g = 9{,}81\,\text{m/s}^2\), und \(\hat{\mathbf{w}}\) die Windrichtung. Das Skalarprodukts-Quadrat \(|\hat{\mathbf{k}} \cdot \hat{\mathbf{w}}|^2\) unterdrückt Wellen senkrecht zum Wind.</p>
<p>Dieses Spektrum lebt vollständig im Frequenzraum: Jeder Punkt in einem 2D-Gitter entspricht einer Wellenfrequenz und -richtung — nicht einer Position auf dem Wasser. Es beschreibt keine konkrete Welle, sondern eine <em>Wahrscheinlichkeitsverteilung</em> von Wellen.</p>
<figure class="my-8">
<img src="img/ifft-ocean-pipeline.svg" alt="iFFT-Ozean-Pipeline: Phillips-Spektrum → komplexe Amplituden → iFFT Butterfly auf GPU → Displacement Map + Normal Map → gerenderte Wasseroberfläche." class="mx-auto rounded-lg border border-gray-800/50" style="max-width:700px" loading="lazy">
<figcaption class="text-xs text-gray-600 mt-2 text-center">Von der Statistik zum Ozean: 65.536 Frequenzen, in Echtzeit auf der GPU. Implementierung: Phil Crowther, Ocean3.js (<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/" class="text-gray-500 hover:text-gray-400" target="_blank" rel="noopener">CC BY-NC-SA 3.0</a>).</figcaption>
</figure>
<h3>Von der Statistik zur Welle: Tessendorfs Methode</h3>
<p>Der britische Mathematiker und Ozeanograph <strong>Owen Phillips</strong> lieferte 1957 die Statistik. Die erste FFT-basierte Ozean-Visualisierung kam 1987: <strong>Gary Mastin, Peter Watterberg und John Mareda</strong> publizierten im <em>IEEE Computer Graphics and Applications</em> den Aufsatz <em>"Fourier Synthesis of Ocean Scenes"</em> — die erste Anwendung der FFT-Methode in der Computergrafik.</p>
<p>Der große Durchbruch für Film und Games kam 2001: <strong>Jerry Tessendorf</strong> von Industrial Light & Magic veröffentlichte für die SIGGRAPH-Kurse sein Paper <em>"Simulating Ocean Water"</em> — bis heute die Standard-Referenz. Tessendorf hat die Methode nicht erfunden, aber er hat sie so klar ausgearbeitet und auf die Anforderungen der Filmproduktion abgestimmt, dass sein Paper zum De-facto-Lehrbuch wurde.</p>
<p>Die Methode in fünf Schritten:</p>
<ol class="list-decimal pl-6 mb-4 text-gray-300 leading-relaxed">
<li><strong>Spektrum initialisieren:</strong> Für jeden Punkt \((k_x, k_y)\) eines 512×512-Frequenzgitters berechne \(P(\mathbf{k})\) aus dem Phillips-Spektrum. Multipliziere mit zufälligem Gauß-Rauschen, um realistische Phasenvarianz zu erhalten.</li>
<li><strong>Zeitentwicklung:</strong> Jede Frequenz breitet sich mit der Dispersionsrelation von Tiefseewellen aus: \(\omega(\mathbf{k}) = \sqrt{g\, k}\). Multipliziere jeden Spektralwert mit \(e^{i\omega t}\) für den aktuellen Zeitpunkt \(t\).</li>
<li><strong>iFFT auf der GPU:</strong> Wende die inverse 2D-FFT an. Das Ergebnis ist eine 512×512-Heightmap — die Wellenhöhe an jedem Punkt des Rasters.</li>
<li><strong>Displacement und Normalen:</strong> Ein zweiter iFFT-Durchgang erzeugt horizontale Verschiebungen (für die charakteristischen spitzen Wellenkämme) und ein dritter die Oberflächennormalen für korrekte Lichtbrechung.</li>
<li><strong>Rendering:</strong> Die Displacement-Map verschiebt die Vertices des Wasser-Meshes; die Normal-Map steuert die Reflexion.</li>
</ol>
<p>Bei einer 512×512-Auflösung sind das \(512^2 = 262.144\) Spektralwerte. Jeder Wert repräsentiert eine einzelne Wellenfrequenz und -richtung. Das iFFT kombiniert alle 262.144 Frequenzen zu einer einzigen, kohärenten Wasseroberfläche. <strong>65.536 sichtbare Wellen pro Frame</strong> ist ein konservatives Maß für das, was der Ozean des Simulators tatsächlich darstellt.</p>
<h3>Der Butterfly-Algorithmus auf der GPU</h3>
<p>Auf der CPU wäre selbst die FFT zu langsam für 60 FPS: Eine 512×512-2D-FFT umfasst 512 zeilenweise 1D-FFTs der Länge 512, dann 512 spaltenweise 1D-FFTs — zusammen 1.024 FFTs à 1.310.720 Operationen. Auf der GPU wird daraus eine <strong>Pipeline aus Fragment-Shadern</strong>:</p>
<p>Eine 1D-FFT der Länge 512 hat \(\log_2(512) = 9\) Butterfly-Stufen. Jede Stufe rendert in ein Float-Framebuffer-Texture-Target. Das Ergebnis wird als Eingabe für die nächste Stufe verwendet — <strong>Ping-Pong-Buffering</strong> zwischen zwei Texturen. Neun Render-Passes, und die Zeilen sind fertig. Dasselbe transponiert für die Spalten. Am Ende stehen die Displacement-Map und die Normal-Map als Texturen bereit, die direkt als Vertex-Shader-Uniforms eingesetzt werden.</p>
<p>Die JavaScript-Implementierungskette, die im Simulator läuft, ist das Ergebnis eines Jahrzehnts Open-Source-Arbeit:</p>
<ul class="list-disc pl-6 mb-4 text-gray-300 leading-relaxed">
<li><strong>2013</strong> — WebGL-Demo von <em>David Li</em> (<a href="https://david.li/waves/" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">david.li/waves/</a>), Umsetzung der Tessendorf-Mathematik in Fragment-Shadern</li>
<li><strong>2014</strong> — Three.js-Portierung durch <em>Aleksandr Albert</em></li>
<li><strong>2015</strong> — wiederverwendbares Three.js-Modul von <em>Jérémy Bouny</em> (<a href="https://github.com/jbouny/ocean" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">github.com/jbouny/ocean</a>)</li>
<li><strong>2023</strong> — Three.js-Aktualisierung durch <em>Phil Crowther</em>, veröffentlicht auf <a href="http://philcrowther.com/Aviation/3JS_demos.html" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">philcrowther.com/Aviation</a></li>
<li><strong>2023</strong> — GLSL3/WebGL2-Upgrade der Shader durch <em>Attila Schroeder</em></li>
</ul>
<p>Das Modul <code>Ocean3.js</code> — ca. 570 Zeilen WebGL2-Code — steht unter <strong>CC BY-NC-SA 3.0</strong>. Die Lizenz ist explizit: Verwendung erlaubt für nichtkommerzielle Zwecke mit Namensnennung. Die Autoren sind David Li, Aleksandr Albert, Jérémy Bouny, Phil Crowther und Attila Schroeder; die wissenschaftliche Grundlage ist Tessendorf (2001) und Mastin et al. (1987), die Wellenstatistik stammt von Phillips (1957). Der Simulator gibt diese Kette vollständig an.</p>
<h3>Die Integration</h3>
<p>Die eigentliche Integrationsarbeit war mechanisch aber präzise: Die <code>Water</code>-Klasse von Three.js musste die FFT-Normal-Map statt ihrer Default-Textur verwenden, und die FFT-Displacement-Map musste per Shader-String-Injection in den Vertex-Shader eingeschleust werden:</p>
<pre><code><span class="code-comment">// WaterPlane.js — hybride Konstruktion</span>
<span class="code-keyword">const</span> ocean = <span class="code-keyword">new</span> Ocean(renderer, {
Res: <span class="code-number">512</span>, Siz: <span class="code-number">2400</span>, <span class="code-comment">// 512×512 Gitter, 2.4 km Tile-Größe</span>
WSp: <span class="code-number">18</span>, WHd: <span class="code-number">295</span>, Chp: <span class="code-number">1.5</span> <span class="code-comment">// Wind 18 m/s, Richtung 295°, Choppiness 1.5</span>
});
<span class="code-keyword">const</span> water = <span class="code-keyword">new</span> Water(geometry, {
waterNormals: ocean.normalMapFramebuffer.texture, <span class="code-comment">// FFT-Normal-Map</span>
sunColor: <span class="code-number">0xffeedd</span>,
sunDirection: sunPos.normalize(),
});
<span class="code-comment">// Displacement-Map per Shader-Injection in den Vertex-Shader einschleusen</span>
water.material.uniforms.oceanDisplacement = {
value: ocean.displacementMapFramebuffer.texture
};
water.material.vertexShader =
<span class="code-string">'uniform sampler2D oceanDisplacement;\n'</span> +
water.material.vertexShader
.replace(<span class="code-string">'void main() {'</span>,
<span class="code-string">'void main() { vec3 oceanDisp = texture2D(oceanDisplacement, uv).rgb;'</span>)
.replace(<span class="code-string">/vec4\s*\(\s*position\s*,\s*1\.0\s*\)/g</span>,
<span class="code-string">'vec4(position + oceanDisp, 1.0)'</span>);</code></pre>
<p>Elf Zeilen Glue-Code für eine Ozean-Simulation, die im Hintergrund eine iFFT-Pipeline auf der GPU betreibt. Die <code>Water</code>-Klasse liefert die Sonnenreflexion; die iFFT erzeugt die physikalisch fundierte, animierte Wellenoberfläche.</p>
<h3>Von Ocean3 zu Ocean4: Die Migration auf WebGPU</h3>
<p>Alles bisher Beschriebene läuft auf WebGL2 — der Grafik-API, die 2017 die meisten Browser erreichte. Inzwischen gibt es <strong>WebGPU</strong>, eine deutlich modernere API, die direkte Compute-Shader erlaubt (also nicht nur Fragment- und Vertex-Shader) und eine dramatisch effizientere Pipeline-Konfiguration ermöglicht. Chrome und Firefox unterstützen WebGPU seit 2023, Safari seit iOS 18.</p>
<p>Die WebGPU-Nachfolgevariante <code>Ocean4.js</code> ersetzt die Fragment-Shader-Pipeline durch <strong>WGSL-Compute-Shader</strong>. Statt neun Render-Passes mit Ping-Pong-Buffering pro 1D-FFT gibt es jetzt Compute-Dispatches, die direkt in StorageTextures schreiben. Für uns bedeutete die Migration: den ganzen Renderer-Pfad doppeln, <code>ShaderMaterial</code> durch <strong>TSL NodeMaterial</strong> ersetzen, Ocean3 gegen Ocean4 austauschen. Für <code>birdybird</code> ist WebGPU jetzt Default; WebGL2 läuft weiter als Fallback für ältere Geräte.</p>
<h3>Cascaded Spectra: Drei Wellenspektren überlagert</h3>
<p>Ein einzelnes FFT-Tile macht überraschend einheitliche Wellen — du siehst über einen weiten Ozean dominant eine Wellenlänge und ihre Oberschwingungen. Reale Meere überlagern viel mehr Skalen: Grund-Dünung von 100+ m, Wind-Chop von 10-30 m, Kleinst-Ripples unter 1 m. Eine <strong>cascaded</strong>-Architektur löst das (siehe <a href="https://spiri0.github.io/Threejs-WebGPU-IFFT-Ocean/" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">Threejs-WebGPU-IFFT-Ocean</a> von Attila Schroeder): drei parallele FFT-Spektren mit verschiedenen Tile-Größen (z. B. 250 m / 17 m / 5 m), deren Displacement- und Normal-Maps in der Wellenoberfläche summiert werden.</p>
<p>Eine vollständige Integration seines Systems wäre mehrere Tage Arbeit gewesen (Quadtree-Chunks, eigenes ECS-Framework, neun WGSL-Shader-Dateien). Als Zwischenschritt haben wir einen „Poor-man's Cascaded Ocean“ gebaut: drei Instanzen von Ocean4 laufen parallel mit unterschiedlichen Tile-Größen (2400 m, 300 m, 40 m) und ihre Outputs werden im Vertex- bzw. Fragment-Shader summiert. Das ist zu Testen verfügbar via <code>?ocean=cascaded</code> — mit etwa 60 % FPS-Kosten, aber einer spürbar lebendigeren Oberfläche: große Dünung und feine Sparkle-Ripples koexistieren auf demselben Wasser.</p>
<div class="summary-box">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Quellen & Attribution</p>
<ul class="list-disc pl-6 text-gray-300 leading-relaxed">
<li>Phillips, O. M. (1957): <em>„On the generation of waves by turbulent wind“</em></li>
<li>Mastin, G., Watterberg, P., Mareda, J. (1987): <em>„Fourier Synthesis of Ocean Scenes“</em>, IEEE CG&A</li>
<li>Tessendorf, J. (2001): <em>„Simulating Ocean Water“</em> — Standard-Referenz für Film und Games</li>
<li>David Li’s WebGL-Demo (2013): <a href="https://david.li/waves/" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">david.li/waves</a></li>
<li>Phil Crowthers Three.js-Demos (2023): <a href="http://philcrowther.com/Aviation/3JS_demos.html" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300">philcrowther.com/Aviation</a></li>
<li><strong>Ocean3.js, CC BY-NC-SA 3.0</strong> — Autoren: David Li, Aleksandr Albert, Jérémy Bouny, Phil Crowther, Attila Schroeder</li>
</ul>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 8: Wie wir gebaut haben -->
<!-- ============================================================ -->
<section id="bauprozess" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 8</p>
<h2>Wie wir gebaut haben</h2>
<h3>Ein Ticket-System als Fundament</h3>
<p>Das Projekt begann mit einer leeren Leinwand und einer Entscheidung: kein Unity, kein Unreal, sondern pures JavaScript mit Three.js direkt im Browser. Keine App, kein Store, kein Download. Der Nutzer öffnet eine URL und fliegt.</p>
<p>Ich war der <strong>Product Owner</strong>. Die Vision, die ästhetischen Entscheidungen, das „das fühlt sich falsch an, mach es anders“ — das kam von mir. <strong>Claude Code</strong> war alles andere: Projektmanager, Entwickler, Tester in einer Instanz.</p>
<p>Claude Code setzte zu Beginn ein <strong>Ticket-System in Markdown</strong> auf — 40 Tickets, aufgeteilt in 5 Phasen, jedes mit Priorität, Abhängigkeiten und Acceptance Criteria. Die Tickets wanderten durch Ordner: <code>backlog/</code> → <code>in-progress/</code> → <code>done/</code>. Kein Jira, kein Trello — nur Dateien in einem Git-Repository.</p>
<pre style="background:#111827; border:1px solid #1f2937; border-radius:0.5rem; padding:1rem; overflow-x:auto; font-size:0.8rem; color:#d1d5db; margin:1.5rem 0;"><code># T018: Octree construction
<strong style="color:#f59e0b;">Priority:</strong> P0 | <strong style="color:#f59e0b;">Phase:</strong> 2 | <strong style="color:#f59e0b;">Size:</strong> L
<strong style="color:#f59e0b;">Depends on:</strong> T011
## Acceptance Criteria
- [ ] Octree builds from chunk AABBs
- [ ] Correct recursive subdivision
- [ ] Unit test with known geometry</code></pre>
<p>40 Tickets, alle abgearbeitet. Von „Projekt aufsetzen“ (T001) bis „Webcam-Kalibrierung glätten“ (T040). Claude Code hat sie geschrieben, implementiert und mit <strong>Playwright</strong> automatisiert getestet.</p>
<h3>Der Prozess der iFFT-Integration</h3>
<p>Die schwierigste Integration war der Ozean-Shader. In klassischer Entwicklung hätte das Einbinden von <code>Ocean3.js</code> mindestens zwei Tage gedauert: Framebuffer-Management verstehen, UV-Koordinaten für Tiling anpassen (die Insel ist 24 km breit, das Wellen-Tile nur 2,4 km), und vor allem die <code>Water</code>-Klasse von Three.js überlisten, damit sie die FFT-Normal-Map akzeptiert.</p>
<p>Mit Claude Code: <strong>unter zwanzig Minuten.</strong> Der Punkt ist nicht, dass die KI die Mathematik des Ozeans tiefer versteht als ein Lehrbuch — sondern dass sie die <em>mechanische Arbeit der Integration</em> eliminiert. Der Mensch sagt, was er will; die KI findet die drei Stellen im Code, an denen die Änderung passieren muss.</p>
<div class="summary-box" style="text-align:center;">
<p class="text-sm text-gray-300">Das gesamte Ticket-System ist öffentlich einsehbar:</p>
<p class="text-sm" style="margin-bottom:0;">
<a href="https://github.com/pmmathias/birdybird/tree/main/tickets" style="color:#22d3ee; text-decoration:underline;">github.com/pmmathias/birdybird/tree/main/tickets</a>
</p>
</div>
</div>
<div class="section-divider"></div>
</section>
<!-- ============================================================ -->
<!-- SECTION 9: Was das über KI sagt -->
<!-- ============================================================ -->
<section id="ki-reflexion" class="pb-16">
<div class="prose-dark">
<p class="text-xs uppercase tracking-widest text-cyan-400/60 mb-2">Kapitel 9</p>
<h2>Was das über KI sagt</h2>
<h3>Die neue Rollenverteilung</h3>
<p>Dieses Projekt hat eine Frage konkret beantwortet: Was passiert, wenn mathematisches Verständnis und maschinelle Ausführungsgeschwindigkeit zusammenkommen?</p>
<p>Der Mensch hat die Verbindungen gesehen. Dass die FFT, die 1965 die Signalverarbeitung revolutionierte, 2001 von Tessendorf auf Ozeanwellen angewendet wurde. Dass dasselbe mathematische Prinzip in jedem JPEG, in jeder MP3 und in der Cochlea des menschlichen Ohrs steckt. Diese konzeptuelle Verbindung — <em>alles ist Frequenzzerlegung</em> — ist etwas, das nur durch Verständnis sichtbar wird, nicht durch Suche.</p>
<p>Die KI hat die Brücken gebaut. Wenn der Mensch sagte „integriere Ocean3.js so, dass die Normal-Map direkt in den Vertex-Shader fließt“, fand die KI die drei Codestellen, schrieb den Glue-Code, identifizierte den UV-Tiling-Fehler. Wochenlange mechanische Arbeit wurde zu einem Abend.</p>
<p>Aber — und das ist entscheidend — die KI hat nicht von sich aus nach einer physikalisch korrekten Ozean-Implementierung gesucht. Sie hat eine flache Ebene mit einer animierten Normal-Map gebaut und war zufrieden. Erst als der Mensch erkannte, dass das Meer flach <em>aussieht</em>, und das Konzept eines FFT-Ozeans einbrachte, kam der Quantensprung in der Visualisierungsqualität.</p>
<h3>Mathematische Alphabetisierung als neuer Hebel</h3>
<p>Die entscheidende Ressource in diesem Projekt war nicht Programmierkompetenz — die liefert die KI. Die entscheidende Ressource war <strong>mathematische Alphabetisierung</strong>: das Wissen, dass die Fourier-Transformation existiert, was sie konzeptuell tut, und in welchen Kontexten sie eingesetzt wird.</p>
<p>Wer weiß, dass Bilder im Frequenzraum eine geclusterte Energieverteilung haben, kann JPEG-Kompression verstehen. Wer weiß, dass Ozeanwellen ein statistisches Spektrum haben, kann die Tessendorf-Methode verstehen. Wer weiß, dass Eigenvektoren und Fourier-Basisvektoren beide orthogonale Koordinatensysteme definieren, sieht die tiefe Verwandtschaft zwischen PCA und FFT.</p>
<p>In einer Welt, in der KI die mechanische Implementierungsarbeit übernimmt, wird das konzeptuelle Verständnis wertvoller, nicht wertloser. Es ist der Faktor, der bestimmt, welche Fragen überhaupt gestellt werden.</p>
<p>Der Vogel fliegt über einen Ozean, dessen Wellen auf Statistik aus dem Jahr 1957 basieren, komprimiert durch einen Algorithmus aus dem Jahr 1965, gerendert in einem Browser mit einer Technologie, die 2023 von fünf Open-Source-Entwicklern aktualisiert wurde. Jede Verbindung in dieser Kette ist sichtbar — wenn man weiß, wonach man schaut.</p>
<div class="try-it" style="text-align: center; padding: 2rem 1.5rem; margin-top: 3rem;">
<p style="margin-bottom: 1.25rem;">Jetzt, wo du weißt, was du siehst — flieg nochmal.</p>
<a href="https://pmmathias.github.io/birdybird/" target="_blank" rel="noopener" class="cta-btn">Zum Simulator →</a>
</div>
</div>
<div class="section-divider"></div>
</section>
<section class="pb-8">
<div class="prose-dark">
<p class="text-sm text-gray-400"><em>Verwandter Beitrag:</em> Im <a href="deepfakes.html" class="text-cyan-400 hover:text-cyan-300 underline">Deepfakes-Post</a> taucht die DCT wieder auf — diesmal als Beispiel für Dimensionsreduktion: 10.000 Pixel werden zu 2.500 Frequenzkoeffizienten, ohne sichtbaren Verlust. Dieselbe Mathematik, anderer Zweck.</p>
</div>
</section>
<!-- ============================================================ -->