-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathOcean.cpp
More file actions
4545 lines (3761 loc) · 195 KB
/
Ocean.cpp
File metadata and controls
4545 lines (3761 loc) · 195 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
/* SimShip by Edouard Halbert
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
http://creativecommons.org/licenses/by-nc-nd/4.0/ */
#include "Ocean.h"
extern uint32_t g_FramesInFlight;
extern sChrono Chronos[10];
extern unique_ptr<VulkanTexture> g_TexReflectionColor;
extern unique_ptr<VulkanTexture> g_TexShadowDepth;
extern VkSampler g_TexShadowDepthSampler;
extern mat4 LightViewProjection;
extern VulkanTexture TexContourShip;
extern int TexContourShipW;
extern int TexContourShipH;
extern bool g_bShip;
extern bool g_bShipShadow; // Display the shadow of the ship
extern bool g_bShipReflection; // Display the reflection of the ship on water
extern bool g_bShipWake;
extern unique_ptr<VulkanTexture> g_TexWake0;
extern unique_ptr<VulkanTexture> g_TexWake2;
extern int g_WakeSize;
int TexWakeBufferSize = 512;
pair<string, uint64_t> OceanTimeStamps[5];
Ocean::Ocean(shared_ptr<VulkanDevice>& vulkanDevice, VkRenderPass renderPass, VkExtent2D extent)
{
mVulkanDevice = vulkanDevice;
mRenderPass = renderPass;
mExtent = extent;
ComputeFinishedSem.resize(g_FramesInFlight);
mComputeFence.resize(g_FramesInFlight);
mComputeCmd.resize(g_FramesInFlight);
}
Ocean::~Ocean()
{
// Time buffer
if (mTimeBuffer) vkDestroyBuffer(mVulkanDevice->device, mTimeBuffer, nullptr);
if (mTimeMemory) vkFreeMemory(mVulkanDevice->device, mTimeMemory, nullptr);
// Spectrum
if (mSpectrumDescriptorPool) vkDestroyDescriptorPool(mVulkanDevice->device, mSpectrumDescriptorPool, nullptr);
if (mSpectrumDescriptorSetLayout) vkDestroyDescriptorSetLayout(mVulkanDevice->device, mSpectrumDescriptorSetLayout, nullptr);
if (mSpectrumPipeline) vkDestroyPipeline(mVulkanDevice->device, mSpectrumPipeline, nullptr);
// IFFT
if (mIfftDescriptorPool) vkDestroyDescriptorPool(mVulkanDevice->device, mIfftDescriptorPool, nullptr);
if (mIfftDescriptorSetLayout) vkDestroyDescriptorSetLayout(mVulkanDevice->device, mIfftDescriptorSetLayout, nullptr);
if (mIfftPipeline) vkDestroyPipeline(mVulkanDevice->device, mIfftPipeline, nullptr);
// Displacement texture
if (mDisplacementsDescPool) vkDestroyDescriptorPool(mVulkanDevice->device, mDisplacementsDescPool, nullptr);
if (mDisplacementsDescSetLayout) vkDestroyDescriptorSetLayout(mVulkanDevice->device, mDisplacementsDescSetLayout, nullptr);
if (mDisplacementsPipeline) vkDestroyPipeline(mVulkanDevice->device, mDisplacementsPipeline, nullptr);
// Gradients
if (mGradientsDescriptorPool) vkDestroyDescriptorPool(mVulkanDevice->device, mGradientsDescriptorPool, nullptr);
if (mGradientsDescriptorSetLayout) vkDestroyDescriptorSetLayout(mVulkanDevice->device, mGradientsDescriptorSetLayout, nullptr);
if (mGradientsPipeline) vkDestroyPipeline(mVulkanDevice->device, mGradientsPipeline, nullptr);
if (mTextureSampler) vkDestroySampler(mVulkanDevice->device, mTextureSampler, nullptr);
// Mesh original
mVertexBuffer.reset();
mIndexBuffer.reset();
// Graphics pipelines
mWireframePipeline.destroy(mVulkanDevice->device);
mOneMeshPipeline.destroy(mVulkanDevice->device);
mvLODPatches.clear();
mLODPipeline.destroy(mVulkanDevice->device);
mPipelineTexture.destroy(mVulkanDevice->device);
// Frames
for (int i = 0; i < g_FramesInFlight; i++)
mFrames[i].ubo.reset();
// Displacement pixels
if (mReadbackFence) vkDestroyFence(mVulkanDevice->device, mReadbackFence, nullptr);
if (mStagingMem) vkUnmapMemory(mVulkanDevice->device, mStagingMem);
if (mStagingBuffer) vkDestroyBuffer(mVulkanDevice->device, mStagingBuffer, nullptr);
if (mStagingMem) vkFreeMemory(mVulkanDevice->device, mStagingMem, nullptr);
}
void Ocean::Init(vec2 wind)
{
// Sort the ocean colors by their hue
//int color = 0;
//for (auto& c : vOceanColors)
//{
// float h, s, l;
// rgb_to_hsl(c, h, s, l);
// cout << color++ << " : " << h << endl;
//}
SetWind(wind);
EvaluatePersistence(PersistenceSec);
SetSpectrum(9);
// All meshes (normal and lod sizes)
CreateMesh();
CreateLODMeshes();
// FFT
CreateTextures();
InitFrequencies();
InitDisplacementsBuffer();
InitFoamBuffer();
CreateTimeBuffer();
CreateSpectrumPipeline();
CreateSpectrumDescriptors();
UpdateSpectrumDescriptors();
CreateIfftPipeline();
CreateIfftDescriptors();
PrecomputeAllIfftDescriptors();
CreateDisplacementsPipeline();
CreateDisplacementsDescriptors();
UpdateDisplacementsDescriptors();
CreateGradientsPipeline();
CreateGradientsDescriptors();
UpdateGradientsDescriptors();
mTextureSampler = CreateTextureSamplerColor(mVulkanDevice->device);
// Ocean colors
for (auto& c : vOceanColors) c = color_255_to_1(c);
OceanColor = vOceanColors[iOceanColor];
// Kelvin waves
CreateTexture2DArray();
// Pipelines
CreatePipeline0(); // Wireframe
CreatePipeline1(); // One mesh
CreatePipeline2(); // Lod meshes
CreatePipeline3(); // Lod with wake around the the ship
// Timestamps
CreateQueryPool();
// Recrée sémaphores et fences compute après un resize
VkSemaphoreCreateInfo semInfo{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (int i = 0; i < g_FramesInFlight; i++)
{
vkCreateSemaphore(mVulkanDevice->device, &semInfo, nullptr, &ComputeFinishedSem[i]);
vkCreateFence(mVulkanDevice->device, &fenceInfo, nullptr, &mComputeFence[i]);
}
}
void Ocean::SetWind(vec2 wind)
{
Wind = wind;
HeightMax = 0.0f;
HeightMin = 0.0f;
// Monahan and O'Brien (2014) : empirical formula for whitecap coverage as a function of wind speed
float windSpeed = glm::length(Wind);
WhitecapCoverageTheoretical = 3.84e-6f * powf(windSpeed, 3.41f) * 100.f;
mvFoamHistory.clear();
};
void Ocean::EvaluatePersistence(float seconds)
{
PersistenceSec = seconds;
PersistenceFactor = -std::log(0.01f) / PersistenceSec;
}
void Ocean::SetSpectrum(int spectre)
{
static const SpectrumFunc spectrumFuncs[] = {
&Ocean::Phillips,
&Ocean::Bretschneider,
&Ocean::PiersonMoskowitz,
&Ocean::JONSWAP,
&Ocean::OchiHubble,
&Ocean::TexelMarsenArsloe,
&Ocean::DonelanBanner,
&Ocean::Torsethaugen,
&Ocean::Elfouhaily,
&Ocean::Horvath
};
if (spectre >= 0 && spectre < (int)std::size(spectrumFuncs))
{
Spectre = spectre;
CurrentSpectrum = spectrumFuncs[spectre];
Amplitude = 1.0f;
Lambda = 0.5f;
mvFoamHistory.clear();
}
}
void Ocean::ComputeNormFactor()
{
float savedNorm = mNormFactor;
mNormFactor = 1.0f; // calcul brut, sans pondération
float sumCurrent = 0.0f;
float sumJONSWAP = 0.0f;
float dk = 2.0f * M_PI / LengthWave;
for (int m = 0; m <= FFT_SIZE; ++m)
for (int n = 0; n <= FFT_SIZE; ++n)
{
vec2 kk(dk * (n - FFT_SIZE / 2), dk * (m - FFT_SIZE / 2));
sumCurrent += (this->*CurrentSpectrum)(kk);
sumJONSWAP += JONSWAP(kk);
}
mNormFactor = (0.0000375f / 4.0f) * sumJONSWAP / sumCurrent;
}
void Ocean::InitFrequencies()
{
if (Spectre != 0)
ComputeNormFactor();
mt19937 gen(12345);
normal_distribution<> gaussian(0.0, 1.0);
VkDeviceSize h0Size = FFT_SIZE_1 * FFT_SIZE_1 * 2 * sizeof(float); // complex<float> = 8 bytes
VkDeviceSize wSize = FFT_SIZE_1 * FFT_SIZE_1 * sizeof(float); // float = 4 bytes
complex<float>* h0data = reinterpret_cast<complex<float>*>(mTexInitialSpectrum.cpuData);
float* wdata = reinterpret_cast<float*>(mTexFrequencies.cpuData);
vec2 k;
float sqrt_S;
for (int m = 0; m <= FFT_SIZE; ++m)
{
for (int n = 0; n <= FFT_SIZE; ++n)
{
k.x = 2.0 * M_PI * (n - FFT_SIZE / 2) / LengthWave;
k.y = 2.0 * M_PI * (m - FFT_SIZE / 2) / LengthWave;
sqrt_S = sqrtf(mNormFactor * (this->*CurrentSpectrum)(k));
int index = m * FFT_SIZE_1 + n;
h0data[index].real(gaussian(gen) * sqrt_S);
h0data[index].imag(gaussian(gen) * sqrt_S);
wdata[index] = sqrtf(mGravity * glm::length(k));
}
}
mTexInitialSpectrum.CopyStagingToGPU();
mTexFrequencies.CopyStagingToGPU();
ClearRecords();
}
float Ocean::Phillips(vec2 k)
{
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Spectre de Phillips ───────────────────────────────────────────────
// S(k) = A · exp(-1 / (k²·L²)) / k⁴
// L = V² / g : plus grande vague possible pour la vitesse de vent V
float L = windSpeed * windSpeed / mGravity;
float L2 = L * L;
float S_phillips = expf(-1.0f / (k_length2 * L2)) / k_length4;
// ── Distribution directionnelle cos^n ─────────────────────────────────
float D = powf(glm::max(k_dot_w, 0.0f), DirSpread);
// ── Suppression des petites vagues (capillaires) ──────────────────────
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
float S = S_phillips * D * suppress;
return S;
}
float Ocean::Bretschneider(vec2 k)
{
// ─── Bretschneider (ISSC, 1959 / 1967) ──────────────────────────────────────
//
// Reformulation paramétrique du Pierson-Moskowitz utilisant la hauteur
// significative Hs et la période de pic Tp plutôt que la vitesse du vent.
// Standard ISO/ISSC pour l'ingénierie offshore (structures, navires).
// Identique à PM quand Hs et Tp sont dérivés de U via les relations P-M,
// mais bien plus pratique quand on impose directement l'état de mer.
//
// Formulation en fréquence angulaire ω :
// S(ω) = (5/16) · Hs² · ωp⁴ / ω⁵ · exp[-5/4 · (ωp/ω)⁴]
// Ici transposée en k via la relation de dispersion ω² = g·k (eau profonde).
//
// Paramètres membres à ajouter :
// float Hs — hauteur significative (m), ex. 2.5
// float Tp — période de pic (s), ex. 10.0
// (Wind, DirSpread, mGravity : inchangés)
// ─────────────────────────────────────────────────────────────────────────────
float windSpeed = glm::length(Wind);
auto waveParams = JONSWAPModel::GetWaveParameters(windSpeed, Fetch);
float Hs = waveParams.significantWaveHeight;
float Tp = waveParams.peakPeriod;
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire (eau profonde) ────────────────────────────────
float omega = sqrtf(mGravity * k_length); // ω = sqrt(g·|k|)
float omega_p = 2.0f * M_PI / Tp; // ωp = 2π / Tp
// ── Spectre de Bretschneider ──────────────────────────────────────────
// S(ω) = (5/16) · Hs² · ωp⁴ / ω⁵ · exp[-5/4 · (ωp/ω)⁴]
// Conversion ω → k : dω/dk = g/(2ω), donc S(k) = S(ω) · dω/dk
float ratio = omega_p / omega; // ωp / ω
float ratio4 = ratio * ratio * ratio * ratio;
float S_omega = (5.0f / 16.0f) * Hs * Hs * powf(omega_p, 4.0f) / powf(omega, 5.0f) * expf(-1.25f * ratio4);
// Jacobien dω/dk = g / (2ω)
float dw_dk = mGravity / (2.0f * omega);
float S_k = S_omega * dw_dk;
// ── Distribution directionnelle cos^n ─────────────────────────────────
// Bretschneider original est omnidirectionnel ; on ajoute cos^n
float D = powf(glm::max(k_dot_w, 0.0f), DirSpread);
// ── Suppression des petites vagues ────────────────────────────────────
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
return S_k * D * suppress;
}
float Ocean::PiersonMoskowitz(vec2 k)
{
/*
Pierson-Moskowitz est simple — pas de fetch, pas de pic de résonance γ.
La fréquence de pic ωp = 0.855·g/U est uniquement fonction de la vitesse du vent, ce qui correspond à une mer pleinement développée
où le vent souffle depuis suffisamment longtemps pour que les vagues aient atteint leur état d'équilibre.
C'est pour ça que JONSWAP converge vers Pierson-Moskowitz quand γ = 1 et que le fetch tend vers l'infini. Pierson-Moskowitz est le cas limite de JONSWAP.
*/
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire ───────────────────────────────────────────────
float omega = sqrtf(mGravity * k_length);
float omega_p = 0.855f * mGravity / windSpeed; // fréquence de pic P-M
// correspond à mer pleinement développée
// ── Spectre Pierson-Moskowitz ─────────────────────────────────────────
// S(k) = α·g²/k⁴ · exp[-β·(ωp/ω)⁴]
// α = 0.0081 (constante de Phillips)
// β = 1.25 (constante empirique P-M)
const float alpha = 0.0081f;
const float beta = 1.25f;
float S_pm = alpha * mGravity * mGravity / k_length4 * expf(-beta * powf(omega_p / omega, 4.0f));
// ── Distribution directionnelle cos^n ─────────────────────────────────
// P-M original est isotrope mais on ajoute une directionnalité pour avoir des vagues alignées sur le vent
float cosTheta = glm::max(k_dot_w, 0.0f);
float D = powf(cosTheta, DirSpread); // exposant 2-6 typique
// ── Suppression des petites vagues ────────────────────────────────────
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
float S = S_pm * D * suppress;
return S;
}
float Ocean::JONSWAP(vec2 k)
{
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
// Direction du vent normalisée
vec2 windDir = glm::normalize(Wind);
float windSpeed = glm::length(Wind);
float k_dot_w = glm::dot(glm::normalize(k), windDir);
// Supprime les vagues allant contre le vent
if (k_dot_w < 0.0f)
return 0.0f;
// Fréquence angulaire de dispersion (deep water)
float omega = sqrtf(mGravity * k_length); // ω = sqrt(g * |k|)
// Fréquence de pic JONSWAP (plus le fetch - en m - est grand, plus les vagues sont longues)
float omega_p = 22.0f * cbrtf(mGravity * mGravity / (windSpeed * Fetch));
// Spectre de Pierson-Moskowitz (base de JONSWAP)
float alpha = 0.0081f; // constante de Phillips généralisée
float S_pm = alpha * mGravity * mGravity / k_length4 * expf(-1.25f * powf(omega_p / omega, 4.0f));
// Pic de résonance JONSWAP
float sigma = (omega <= omega_p) ? 0.07f : 0.09f; // largeur du pic
float r = expf(-powf(omega - omega_p, 2.0f) / (2.0f * sigma * sigma * omega_p * omega_p));
float S_jonswap = S_pm * powf(Maturity, r);
// Distribution directionnelle (même approche que Phillips : cos^n aligné sur le vent)
float dirSpread = powf(glm::max(k_dot_w, 0.0f), DirSpread); // exposant : 2-6 typique
// Suppression des petites vagues (capillaires)
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
return S_jonswap * dirSpread * suppress;
}
float Ocean::OchiHubble(vec2 k)
{
// ─── Ochi-Hubble (1976) ──────────────────────────────────────────────────────
//
// Spectre bimodal à six paramètres (trois par composante), développé par
// Michel Ochi et Earl Hubble (SSPA Research, Göteborg) à partir de 800 états
// de mer mesurés en Atlantique Nord.
// Publication : "Six-Parameter Wave Spectra", Coastal Engineering Conference 1976.
//
// Modélise simultanément :
// • La houle longue distante (swell) — composante basse fréquence
// • La mer de vent locale — composante haute fréquence
// Très utilisé en tenue à la mer et fatigue des structures offshore.
//
// Formulation à deux pics (j = 1 : swell, j = 2 : wind sea) :
// S_j(ω) = [ (4λ_j + 1)/4 · ωp_j⁴ ]^λ_j / Γ(λ_j) · Hs_j² / ω^(4λ_j+1) · exp[-(4λ_j+1)/4 · (ωp_j/ω)⁴]
//
// Paramètres membres à ajouter :
// float OH_Hs1, OH_Tp1, OH_lambda1 — swell (ex. 1.5, 15.0, 3.0)
// float OH_Hs2, OH_Tp2, OH_lambda2 — mer de vent (ex. 1.0, 8.0, 1.5)
// λ typiques : 3.0 (swell bien formé) / 1.5 (mer de vent jeune)
// ─────────────────────────────────────────────────────────────────────────────
float windSpeed = glm::length(Wind);
auto waveParams = JONSWAPModel::GetWaveParameters(windSpeed, Fetch);
float OH_Hs2 = waveParams.significantWaveHeight;
float OH_Tp2 = waveParams.peakPeriod;
float OH_lambda2 = 1.5f;
float OH_Hs1 = 1.5f * OH_Hs2;
float OH_Tp1 = 3.0f * OH_Tp2;
float OH_lambda1 = 3.0f; // swell bien formé
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire ───────────────────────────────────────────────
float omega = sqrtf(mGravity * k_length);
// ── Composante générique Ochi-Hubble ──────────────────────────────────
// Calcule S_j(ω) pour un seul pic (Hs_j, Tp_j, λ_j) puis applique le jacobien dω/dk pour obtenir S_j(k)
auto ochi_component = [&](float Hs_j, float Tp_j, float lambda_j) -> float
{
float omega_p_j = 2.0f * M_PI / Tp_j;
float ratio_j = omega_p_j / omega; // ωp_j / ω
float ratio4_j = powf(ratio_j, 4.0f);
// Coefficient de forme
float coeff = powf((4.0f * lambda_j + 1.0f) / 4.0f * ratio4_j, lambda_j) / tgammaf(lambda_j); // Γ(λ) via tgamma
float S_omega_j = coeff * (Hs_j * Hs_j / 4.0f) / powf(omega, 4.0f * lambda_j + 1.0f) * expf(-(4.0f * lambda_j + 1.0f) / 4.0f * ratio4_j);
// Jacobien dω/dk
float dw_dk = mGravity / (2.0f * omega);
return S_omega_j * dw_dk;
};
// ── Deux composantes ──────────────────────────────────────────────────
float S_swell = ochi_component(OH_Hs1, OH_Tp1, OH_lambda1);
float S_windsea = ochi_component(OH_Hs2, OH_Tp2, OH_lambda2);
float S_total = S_swell + S_windsea;
// ── Distribution directionnelle cos^n ─────────────────────────────────
// Ochi-Hubble original est omnidirectionnel ; on applique la même pondération directionnelle que les autres spectres
float D = powf(glm::max(k_dot_w, 0.0f), DirSpread);
return S_total * D;
}
float Ocean::TexelMarsenArsloe(vec2 k)
{
/*
La seule différence est Phi_TMA = cg_shallow / cg_deep.
En eau profonde kh → ∞, tanh(kh) → 1 donc cg_shallow → cg_deep et Phi → 1 — TMA est strictement équivalent à JONSWAP.
En eau peu profonde kh → 0, tanh(kh) → kh donc cg_shallow → sqrt(g·h) (vitesse en eau peu profonde) et Phi → 0 — le spectre s'effondre.
C'est physiquement correct : en eau très peu profonde les vagues ne peuvent plus se propager librement et le spectre de surface est atténué.
En pratique pour SimShip tu obtiendras une mer notablement plus calme dans les zones portuaires si WaterDepth < 20m, et identique à JONSWAP en pleine mer.
*/
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Profondeur d'eau ──────────────────────────────────────────────────
// WaterDepth = 0.0 → eau profonde (TMA = JONSWAP)
// WaterDepth = 10.0 → eau peu profonde (côte, port)
float h = glm::max(Depth, 0.1f); // évite division par zéro
// ── Relation de dispersion eau peu profonde ───────────────────────────
// En eau profonde : ω² = g·k
// En eau peu profonde : ω² = g·k·tanh(k·h)
float kh = k_length * h;
float tanh_kh = tanhf(kh);
float omega = sqrtf(mGravity * k_length * tanh_kh); // ← différence vs eau profonde
// Vitesse de groupe en eau peu profonde
// cg = dω/dk = (g/2ω) · [tanh(kh) + kh·(1 - tanh²(kh))]
float cg_deep = mGravity / (2.0f * sqrtf(mGravity * k_length)); // eau profonde
float cg_shallow = (mGravity / (2.0f * omega)) * (tanh_kh + kh * (1.0f - tanh_kh * tanh_kh)); // eau peu profonde
// ── Fréquence de pic ──────────────────────────────────────────────────
float omega_p = 22.0f * cbrtf(mGravity * mGravity / (windSpeed * Fetch));
// ── Spectre JONSWAP de base ───────────────────────────────────────────
// TMA est une transformation de JONSWAP → on part de la même base
const float alpha = 0.0081f;
float S_pm = alpha * mGravity * mGravity / k_length4 * expf(-1.25f * powf(omega_p / omega, 4.0f));
// Pic de résonance JONSWAP
float sigma = (omega <= omega_p) ? 0.07f : 0.09f;
float r = expf(-powf(omega - omega_p, 2.0f) / (2.0f * sigma * sigma * omega_p * omega_p));
float peak = powf(Maturity, r);
float S_jonswap = S_pm * peak;
// ── Fonction de transformation TMA ────────────────────────────────────
// C'est LE terme spécifique à TMA (Kitaigородский et al. 1975)
// Φ(kh) = cg_shallow / cg_deep
// Atténue le spectre quand kh < π/2 (eau peu profonde)
// Φ → 1 quand kh → ∞ (eau profonde : TMA = JONSWAP)
// Φ → 0 quand kh → 0 (très faible profondeur)
float Phi_TMA = cg_shallow / cg_deep;
Phi_TMA = glm::clamp(Phi_TMA, 0.0f, 1.0f);
// ── Distribution directionnelle Donelan-Banner ────────────────────────
float theta = acosf(glm::clamp(k_dot_w, -1.0f, 1.0f));
float ratio = omega / omega_p;
float beta_dir;
if (ratio < 0.95f)
beta_dir = 2.61f * powf(ratio, 1.3f);
else if (ratio <= 1.6f)
beta_dir = 2.28f * powf(ratio, -1.3f);
else
beta_dir = powf(10.0f, -0.4f + 0.8393f * expf(-0.567f * logf(ratio * ratio)));
float sech_val = 2.0f / (expf(beta_dir * theta) + expf(-beta_dir * theta));
float D = (beta_dir / 2.0f) * sech_val * sech_val;
// ── Suppression des petites vagues ────────────────────────────────────
float L = windSpeed * windSpeed / mGravity;
float l2 = L * L * 0.0001f * 0.0001f;
float suppress = expf(-k_length2 * l2);
// ── Résultat final ────────────────────────────────────────────────────
// Φ atténue progressivement le spectre quand la profondeur diminue
float S = S_jonswap * Phi_TMA * D * suppress;
return S;
}
float Ocean::DonelanBanner(vec2 k)
{
float k_length = glm::length(k);
if (k_length < 0.000001f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire ───────────────────────────────────────────────
float omega = sqrtf(mGravity * k_length);
float omega_p = 22.0f * cbrtf(mGravity * mGravity / (windSpeed * Fetch));
// ── Spectre de base Pierson-Moskowitz (même base que JONSWAP) ─────────
float alpha = 0.0081f;
float L = windSpeed * windSpeed / mGravity;
float L2 = L * L;
float S_pm = alpha * mGravity * mGravity / k_length4 * expf(-1.25f * powf(omega_p / omega, 4.0f));
// ── Pic de résonance JONSWAP (inchangé) ───────────────────────────────
float sigma = (omega <= omega_p) ? 0.07f : 0.09f;
float r = expf(-powf(omega - omega_p, 2.0f)
/ (2.0f * sigma * sigma * omega_p * omega_p));
float peak = powf(Maturity, r);
float S_base = S_pm * peak;
// ── Distribution directionnelle Donelan-Banner ────────────────────────
// Remplace le cos^n de JONSWAP par sech²(β·θ) (carré de la sécante hyperbolique)
// β varie selon ω/ωp pour coller aux mesures expérimentales
float theta = acosf(glm::clamp(k_dot_w, -1.0f, 1.0f));
float ratio = omega / omega_p;
float beta_dir;
if (ratio < 0.95f)
beta_dir = 2.61f * powf(ratio, 1.3f);
else if (ratio <= 1.6f)
beta_dir = 2.28f * powf(ratio, -1.3f);
else
beta_dir = powf(10.0f, -0.4f + 0.8393f * expf(-0.567f * logf(ratio * ratio)));
// sech²(β·θ) — distribution directionnelle normalisée
float sech_val = 2.0f / (expf(beta_dir * theta) + expf(-beta_dir * theta));
float D = (beta_dir / 2.0f) * sech_val * sech_val;
// ── Suppression des petites vagues ────────────────────────────────────
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
// ── Résultat ──────────────────────────────────────────────────────────
// On utilise D directement à la place de cos^n, sans conversion dω/dk
// pour garder la même échelle que JONSWAP
float S = S_base * D * suppress;
return S;
}
float Ocean::Torsethaugen(vec2 k)
{
// ─── Torsethaugen (1993 / 1996) ──────────────────────────────────────────────
//
// Spectre bimodal entièrement paramétrique développé par Knut Torsethaugen
// (SINTEF, Trondheim) à partir de mesures en mer de Norvège et mer du Nord.
// Publication : "Two Peak Wave Spectrum Model", OMAE 1993 + rapport SINTEF 1996.
//
// Conçu pour les conditions nord-atlantiques sévères où swell et mer de vent
// coexistent fréquemment. Contrairement à Ochi-Hubble, tous les paramètres
// se dérivent automatiquement de Hs et Tp — un seul état de mer à fournir.
//
// Architecture :
// • Pic primaire (j=1) : composante dominante selon régime de mer
// • Pic secondaire (j=2) : composante sous-dominante (swell ou vent)
// Le régime est déterminé par Tp vs Tf = 6.6·Hs^(1/3) (période limite)
//
// Paramètres membres à ajouter :
// float Hs — hauteur significative totale (m), ex. 4.0
// float Tp — période de pic dominante (s), ex. 12.0
// ─────────────────────────────────────────────────────────────────────────────
float windSpeed = glm::length(Wind);
auto waveParams = JONSWAPModel::GetWaveParameters(windSpeed, Fetch);
float Hs = waveParams.significantWaveHeight;
float Tp = waveParams.peakPeriod;
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire ───────────────────────────────────────────────
float omega = sqrtf(mGravity * k_length);
// ── Période seuil et régime ───────────────────────────────────────────
// Tf = 6.6 · Hs^(1/3) : séparation régime vent / régime swell
float Tf = 6.6f * cbrtf(Hs);
bool swell_dominated = (Tp > Tf); // swell domine si Tp > Tf
// ── Paramétrage du pic primaire ───────────────────────────────────────
// Calibration empirique SINTEF (valeurs tableau Torsethaugen 1996)
float Hs1, Tp1, gamma1;
if (swell_dominated)
{
// Régime swell : pic primaire = swell longue période
float Rp = Tp / Tf;
Hs1 = Hs * (1.0f - 0.35f * (Rp - 1.0f)); // Hs réduit pour le vent
Hs1 = glm::clamp(Hs1, 0.3f * Hs, Hs);
Tp1 = Tp;
gamma1 = glm::max(1.0f + 6.0f * powf(Hs / (Tp * Tp), 0.3f), 1.0f);
}
else
{
// Régime mer de vent : pic primaire = mer de vent courte période
Hs1 = Hs * 0.95f;
Tp1 = Tp;
gamma1 = glm::max(35.0f * powf(Hs / (Tp * Tp), 0.5f), 1.0f);
}
float Hs2 = sqrtf(glm::max(Hs * Hs - Hs1 * Hs1, 0.0f)); // conservation Hs
float Tp2 = swell_dominated
? 0.7f * Tf // vent sous-dominant
: 1.3f * Tf; // swell sous-dominant
float gamma2 = 1.0f; // pic secondaire élargi
// ── Forme spectrale JONSWAP généralisée pour chaque pic ───────────────
// S_j(ω) = alpha_j · g² / ω⁵ · exp[-5/4·(ωp_j/ω)⁴] · γ_j^r_j avec alpha_j calibré à partir de Hs_j et Tp_j
auto tors_component = [&](float Hs_j, float Tp_j, float gamma_j) -> float
{
float omega_p_j = 2.0f * M_PI / Tp_j;
// α normalisé sur Hs_j (intégrale du spectre = Hs²/16)
// α_j = (5π⁴/g²) · Hs_j² · ωp_j⁴
float alpha_j = (5.0f * powf(M_PI, 4.0f) / (mGravity * mGravity))
* Hs_j * Hs_j
* powf(omega_p_j, 4.0f);
// Forme P-M
float S_pm_j = alpha_j * mGravity * mGravity
/ powf(omega, 5.0f)
* expf(-1.25f * powf(omega_p_j / omega, 4.0f));
// Pic de résonance JONSWAP
float sigma_j = (omega <= omega_p_j) ? 0.07f : 0.09f;
float r_j = expf(-powf(omega - omega_p_j, 2.0f)
/ (2.0f * sigma_j * sigma_j * omega_p_j * omega_p_j));
float peak_j = powf(glm::max(gamma_j, 1.0f), r_j);
float S_omega_j = S_pm_j * peak_j;
// Jacobien dω/dk
float dw_dk = mGravity / (2.0f * omega);
return S_omega_j * dw_dk;
};
float S_primary = tors_component(Hs1, Tp1, gamma1);
float S_secondary = tors_component(Hs2, Tp2, gamma2);
float S_total = S_primary + S_secondary;
// ── Distribution directionnelle Donelan-Banner ────────────────────────
// Torsethaugen est conçu pour des conditions nord-atlantiques sévères ;
// on conserve la directionnalité Donelan-Banner (sech²) comme pour TMA
float omega_p_dom = 2.0f * M_PI / Tp1;
float theta = acosf(glm::clamp(k_dot_w, -1.0f, 1.0f));
float ratio_dir = omega / omega_p_dom;
float beta_dir;
if (ratio_dir < 0.95f)
beta_dir = 2.61f * powf(ratio_dir, 1.3f);
else if (ratio_dir <= 1.6f)
beta_dir = 2.28f * powf(ratio_dir, -1.3f);
else
beta_dir = powf(10.0f, -0.4f + 0.8393f * expf(-0.567f * logf(ratio_dir * ratio_dir)));
float sech_val = 2.0f / (expf(beta_dir * theta) + expf(-beta_dir * theta));
float D = (beta_dir / 2.0f) * sech_val * sech_val;
// ── Suppression des petites vagues ────────────────────────────────────
float l_small = 0.01f * windSpeed * windSpeed / mGravity;
float suppress = expf(-k_length2 * l_small * l_small);
return S_total * D * suppress;
}
float Ocean::Elfouhaily(vec2 k)
{
/*
La différence fondamentale est la séparation en deux spectres Bl et Bh qui couvrent des gammes de fréquences différentes.
Bl (grandes vagues de gravité) reprend la structure JONSWAP avec le pic Gamma_j et Lpm, mais avec α variable comme Horvath.
Le terme cp/c module l'énergie selon le rapport des vitesses de phase — les vagues lentes (longues) reçoivent plus d'énergie que les rapides.
Bh (capillaires) est piloté par ux/c — le rapport vent de friction / vitesse de phase.
Les capillaires sont générés directement par le frottement du vent sur la surface, pas par résonance comme les grandes vagues.
Le terme Fm centre ce spectre autour de k_m ≈ 363 rad / m, la fréquence où la tension de surface domine la gravité.
La transition entre les deux régimes est continue — il n'y a pas de coupure artificielle comme dans les autres modèles.
C'est ce qui rend Elfouhaily particulièrement adapté pour les simulations radar et optiques,
et c'est pourquoi il est utilisé dans les productions Pixar et ILM où les détails de surface à toutes les échelles sont visibles.
*/
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_min = 2.0f * M_PI / 100.0f;
if (k_length < k_min)
return 0.0f;
float k_length2 = k_length * k_length;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
const float sigma_t = 0.074f;
const float rho = 1025.0f;
const float Omega_c = 0.84f;
float c_phase = sqrtf(mGravity / k_length + sigma_t * k_length / rho);
float u_star = 0.025f * windSpeed;
u_star = glm::max(u_star, 0.001f);
float omega_p = 22.0f * cbrtf(mGravity * mGravity / (windSpeed * Fetch));
float k_p = omega_p * omega_p / mGravity;
float c_p = sqrtf(mGravity / k_p + sigma_t * k_p / rho);
float Omega = windSpeed / c_p;
Omega = glm::clamp(Omega, 0.84f, 5.0f);
float alpha_e = 0.006f * sqrtf(Omega);
alpha_e = glm::clamp(alpha_e, 0.0028f, 0.015f);
float beta_e = 0.229f * expf(-0.4f * powf(Omega / Omega_c - 1.0f, 2.0f));
float Lpm = expf(-1.25f * powf(k_p / k_length, 2.0f));
float gamma = Maturity;
float sigma_j = (k_length <= k_p) ? 0.07f : 0.09f;
float r = expf(-powf(sqrtf(k_length / k_p) - 1.0f, 2.0f) / (2.0f * sigma_j * sigma_j));
float Gamma_j = powf(gamma, r);
float Bl = 0.5f * alpha_e * (c_p / c_phase) * Lpm * Gamma_j;
const float k_m = sqrtf(rho * mGravity / sigma_t);
float Fm = expf(-0.25f * powf(k_length / k_m - 1.0f, 2.0f));
float Bh = 0.5f * beta_e * (u_star / c_phase) * Fm;
float cap_sat = expf(-k_length2 / (k_m * k_m));
Bh *= cap_sat;
// ── Conversion vers densité d'énergie ─────────────────────────────────
// Elfouhaily est formulé en saturation B(k) adimensionnelle
// On convertit vers S(k) en densité d'énergie comme JONSWAP : S(k) = B(k) / k⁴ (au lieu de / k² dans la formulation saturation)
float k_length4 = k_length2 * k_length2;
//float W_k = (Bl + Bh) / (k_length2 * k_length); // k³ (formule originale)
float W_k = (Bl + Bh) / k_length4; // k⁴
// ── Distribution directionnelle Donelan-Banner ────────────────────────
float omega = sqrtf(mGravity * k_length);
float theta = acosf(glm::clamp(k_dot_w, -1.0f, 1.0f));
float ratio = omega / omega_p;
float beta_dir;
if (ratio < 0.95f)
beta_dir = 2.61f * powf(ratio, 1.3f);
else if (ratio <= 1.6f)
beta_dir = 2.28f * powf(ratio, -1.3f);
else
beta_dir = powf(10.0f, -0.4f + 0.8393f * expf(-0.567f * logf(ratio * ratio)));
float sech_val = 2.0f / (expf(beta_dir * theta) + expf(-beta_dir * theta));
float D = (beta_dir / 2.0f) * sech_val * sech_val;
// ── Suppression des petites vagues ────────────────────────────────────
float L = windSpeed * windSpeed / mGravity;
float l2 = L * L * 0.0001f * 0.0001f;
float suppress = expf(-k_length2 * l2);
// Coupure à k_cutoff = π / résolution_texel = π / 0.39 ≈ 8 rad/m
float k_cutoff = M_PI * FFT_SIZE / LengthWave * 0.25f; // 25% de la fréquence max
float lowpass = expf(-k_length2 / (k_cutoff * k_cutoff));
float S = W_k * D * suppress * lowpass;
return S;
}
float Ocean::Horvath(vec2 k)
{
float k_length = glm::length(k);
// Avoid division by zero
if (k_length < 1e-6f)
return 0.0f;
float k_length2 = k_length * k_length;
float k_length4 = k_length2 * k_length2;
float windSpeed = glm::length(Wind);
vec2 windDir = glm::normalize(Wind);
// Direction : supprime les vagues contre le vent
float k_dot_w = glm::dot(glm::normalize(k), windDir);
if (k_dot_w < 0.0f)
return 0.0f;
// ── Fréquence angulaire ───────────────────────────────────────────────
float omega = sqrtf(mGravity * k_length); // ω = sqrt(g·|k|)
float omega2 = omega * omega;
// ── Vitesse de phase et nombre de Froude local ────────────────────────
float cp = omega / k_length; // vitesse de phase c = ω/k
float Omega = windSpeed / cp; // inverse Froude : U/c
// ── Fréquence de pic (Donelan empirique) ─────────────────────────────
float omega_p = 22.0f * cbrtf(mGravity * mGravity / (windSpeed * Fetch)); // ωp dépend du fetch via la relation de similitude
float cp_p = mGravity / omega_p; // vitesse de phase au pic
// ── Coefficient α de Horvath (remplace α=0.0081 fixe) ────────────────
// α varie avec le développement de la mer (Omega = U/cp) - Calibré sur données JONSWAP + mesures Donelan
float alpha_h = 0.006f * sqrtf(Omega); // α = 0.006 * sqrt(U/cp)
alpha_h = glm::clamp(alpha_h, 0.0028f, 0.015f); // bornes physiques
// ── Spectre de base Horvath ───────────────────────────────────────────
float S_base = alpha_h * mGravity * mGravity / k_length4; // Même forme que P-M mais avec α variable et exposant corrigé
// ── Pic de résonance (JONSWAP γ) ─────────────────────────────────────
float gamma = Maturity;
float sigma = (omega <= omega_p) ? 0.07f : 0.09f;
float r = expf(-powf(omega - omega_p, 2.0f) / (2.0f * sigma * sigma * omega_p * omega_p));
float peak = powf(gamma, r);
// ── Terme de vieillissement de la mer (swell aging) ──────────────────
// Horvath introduit un terme L_pm qui modélise la saturation du spectre quand la mer est pleinement développée (Omega → 1)
float L_pm = expf(-1.25f * powf(omega_p / omega, 4.0f)); // Pierson-Moskowitz shape
float Gamma_h = expf(-powf(Omega - 1.0f, 2.0f) / 0.04f); // saturation curve
float J_p = powf(gamma, Gamma_h); // pic de Horvath
float S_horvath = S_base * J_p * L_pm;
// ── Distribution directionnelle Donelan-Banner (Horvath la conserve) ──
float theta = acosf(glm::clamp(k_dot_w, -1.0f, 1.0f));
float ratio = omega / omega_p;
float beta_dir;
if (ratio < 0.95f)
beta_dir = 2.61f * powf(ratio, 1.3f);
else if (ratio <= 1.6f)
beta_dir = 2.28f * powf(ratio, -1.3f);
else
beta_dir = powf(10.0f, -0.4f + 0.8393f * expf(-0.567f * logf(ratio * ratio)));