diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000000000000000000000000000000000000..b68024fe2b70431a415b9215e1e292bacf070050
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,246 @@
+---
+Language:        Cpp
+# BasedOnStyle:  LLVM
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveBitFields:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    false
+AlignConsecutiveDeclarations:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    false
+AlignConsecutiveMacros:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    false
+AlignConsecutiveShortCaseStatements:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCaseColons: false
+AlignEscapedNewlines: Right
+AlignOperands:   Align
+AlignTrailingComments:
+  Kind:            Always
+  OverEmptyLines:  0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: MultiLine
+AttributeMacros:
+  - __capability
+BinPackArguments: true
+BinPackParameters: true
+BitFieldColonSpacing: Both
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterExternBlock: false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakArrays:     true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializers: BeforeColon
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+ColumnLimit:     80
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '^"(llvm|llvm-c|clang|clang-c)/'
+    Priority:        2
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: true
+IndentWidth:     2
+IndentWrappedFunctionNames: false
+InsertBraces:    false
+InsertNewlineAtEOF: false
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+  Binary:          0
+  BinaryMinDigits: 0
+  Decimal:         0
+  DecimalMinDigits: 0
+  Hex:             0
+  HexMinDigits:    0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+KeepEmptyLinesAtEOF: false
+LambdaBodyIndentation: Signature
+LineEnding:      DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+PPIndentWidth:   -1
+QualifierAlignment: Leave
+ReferenceAlignment: Pointer
+ReflowComments:  true
+RemoveBracesLLVM: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:    CaseSensitive
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+  AfterControlStatements: true
+  AfterForeachMacros: true
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   true
+  AfterOverloadedOperator: false
+  AfterPlacementOperator: true
+  AfterRequiresInClause: false
+  AfterRequiresInExpression: false
+  BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParens:  Never
+SpacesInParensOptions:
+  InCStyleCasts:   false
+  InConditionalStatements: false
+  InEmptyParentheses: false
+  Other:           false
+SpacesInSquareBrackets: false
+Standard:        Latest
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        8
+UseTab:          Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+  - BOOST_PP_STRINGIZE
+  - CF_SWIFT_NAME
+  - NS_SWIFT_NAME
+  - PP_STRINGIZE
+  - STRINGIZE
+...
+
diff --git a/models/cube.obj b/models/cube.obj
new file mode 100644
index 0000000000000000000000000000000000000000..909ab55982d94da97be8abe38d072ffea61a230f
--- /dev/null
+++ b/models/cube.obj
@@ -0,0 +1,38 @@
+# Blender 4.2.3 LTS
+# www.blender.org
+o Cube
+v -0.500000 -0.500000 0.500000
+v -0.500000 0.500000 0.500000
+v -0.500000 -0.500000 -0.500000
+v -0.500000 0.500000 -0.500000
+v 0.500000 -0.500000 0.500000
+v 0.500000 0.500000 0.500000
+v 0.500000 -0.500000 -0.500000
+v 0.500000 0.500000 -0.500000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -0.0000 -1.0000 -0.0000
+vn -0.0000 1.0000 -0.0000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.625000 0.500000
+vt 0.375000 0.500000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.125000 0.500000
+vt 0.125000 0.750000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+s 0
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/4/2 4/3/2 8/5/2 7/6/2
+f 7/6/3 8/5/3 6/7/3 5/8/3
+f 5/8/4 6/7/4 2/9/4 1/10/4
+f 3/11/5 7/6/5 5/8/5 1/12/5
+f 8/5/6 4/13/6 2/14/6 6/7/6
diff --git a/models/wide_cube.obj b/models/wide_cube.obj
new file mode 100644
index 0000000000000000000000000000000000000000..1c547958254aebe312fe44264f145e0c0625883d
--- /dev/null
+++ b/models/wide_cube.obj
@@ -0,0 +1,38 @@
+# Blender 4.2.3 LTS
+# www.blender.org
+o Cube
+v 3.089371 -0.034801 0.761726
+v 3.089371 1.644930 0.761726
+v 3.089371 -0.034801 -0.761726
+v 3.089371 1.644930 -0.761726
+v 3.699787 -0.034801 0.761726
+v 3.699787 1.644930 0.761726
+v 3.699787 -0.034801 -0.761726
+v 3.699787 1.644930 -0.761726
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -0.0000 -1.0000 -0.0000
+vn -0.0000 1.0000 -0.0000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.625000 0.500000
+vt 0.375000 0.500000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.125000 0.500000
+vt 0.125000 0.750000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+s 0
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/4/2 4/3/2 8/5/2 7/6/2
+f 7/6/3 8/5/3 6/7/3 5/8/3
+f 5/8/4 6/7/4 2/9/4 1/10/4
+f 3/11/5 7/6/5 5/8/5 1/12/5
+f 8/5/6 4/13/6 2/14/6 6/7/6
diff --git a/shaders/basic.vert b/shaders/basic.vert
index a3adbe90a7a42e871e52704cbed2f275d47f6f1e..f7e6ed93a25b21dc54b66347b068d0d139447ee0 100644
--- a/shaders/basic.vert
+++ b/shaders/basic.vert
@@ -7,11 +7,17 @@ in  vec2  in_TexCoord;
 uniform mat4 projectionMatrix;
 uniform mat4 modelToWorldToView;
 
-out float shade;
+
+out vec3 position;
+out vec3 normal;
+out vec2 tex_coord;
+
 
 void main(void)
 {
-	shade = (mat3(modelToWorldToView)*in_Normal).z; // Fake shading
+    position = in_Position;
+    normal = in_Normal;
+    tex_coord = in_TexCoord;
+
 	gl_Position=projectionMatrix*modelToWorldToView*vec4(in_Position, 1.0);
 }
-
diff --git a/shaders/ground.frag b/shaders/ground.frag
index 7d92382799e0b4e4370ba9fc4c86ebcb5e8c3c01..cc59c06dbaf47026028eeac2e8239763cc5d8365 100644
--- a/shaders/ground.frag
+++ b/shaders/ground.frag
@@ -1,11 +1,30 @@
 #version 150
 
-in float shade;
+in vec3 position;
+in vec3 normal;
+in vec2 tex_coord;
+
+uniform sampler2D grass;
+uniform sampler2D dirt;
 
 out vec4 out_Color;
 
+const float tex_scale = 4.0;
+
+const vec3 sun = normalize(vec3(0, 1, 0));
+
+float unlerp(float lo, float hi, float t)
+{
+    return clamp((t - lo) / (hi - lo), 0.0, 1.0);
+}
+
 void main(void)
 {
-	out_Color=vec4(shade,shade,shade,1.0);
+    vec3 nnormal = normalize(normal);
+    float light = max(0.0, dot(nnormal, sun));
+
+    vec3 albedo = mix(texture(dirt, tex_coord * tex_scale).rgb, texture(grass, tex_coord * tex_scale).rgb, unlerp(-0.2, 0.2, position.y));
+
+    out_Color = vec4(albedo * light, 1.0);
 }
 
diff --git a/shaders/surface.frag b/shaders/surface.frag
index 6a72d43822fbc5151b15ac581936eab112e872c8..e5bbed52d5c3d8572daca35dff3ca6baadc51cd7 100644
--- a/shaders/surface.frag
+++ b/shaders/surface.frag
@@ -2,15 +2,25 @@
 
 in vec3 world_pos;
 in vec3 normal;
+in vec2 tex_coord;
 
-out vec4 out_Color;
+in float test;
 
 uniform sampler2D sky;
+uniform sampler2D ground;
+uniform sampler2D ground_depth;
 uniform vec3 camera_pos;
 
 const float PI = 3.1415926535897f;
 const float R0 = 0.04;
 
+const float MIN_T = 0;
+const float MAX_T = 10.0;
+const float DT = 0.1;
+
+out vec4 out_Color;
+
+
 vec2 sphere_uv(vec3 direction)
 {
     float u = 0.5 + atan(direction.z, direction.x) / (2 * PI);
@@ -25,16 +35,56 @@ float fresnel(vec3 normal, vec3 view) {
     return mix(R0, 1.0, x * x * x * x * x);
 }
 
+vec2 tex_at(vec3 pos) {
+    return pos.xz * vec2(0.1f) + vec2(0.5f);
+}
+
+float depth_at(vec3 pos) {
+    return mix(4.5, -2.0, texture(ground_depth, tex_at(pos)).r);
+}
+
+/**
+ * Adapted from Inigo Quilez article on raymarching terrain
+ * https://iquilezles.org/articles/terrainmarching/
+ */
+vec3 refraction_ray(vec3 pos, vec3 dir) {
+    float lh = 0.0f;
+    float ly = 0.0f;
+
+    for (float t = MIN_T; t < MAX_T; t += DT) {
+        vec3 cur_pos = pos + t * dir;
+
+        float y_at = depth_at(cur_pos);
+        if (cur_pos.y < y_at) {
+            float res_t = t - DT + DT * (lh - ly) / (cur_pos.y - ly - y_at + lh);
+            
+            return texture(ground, tex_at(pos + res_t * dir)).rgb;
+        }
+
+        lh = y_at;
+        ly = cur_pos.y;
+    }
+
+    return vec3(1, 0, 1);
+}
+
+
 void main(void)
 {
     vec3 view = normalize(world_pos - camera_pos);
     vec3 nnormal = normalize(normal);
     vec3 reflected = reflect(view, nnormal);
     float R = fresnel(nnormal, view);
-    vec3 water_color = vec3(0.0, 0.1, 0.4);
+    // vec3 water_color = vec3(0, 0.05, 0.1);
 
     vec3 sky_color = texture(sky, sphere_uv(reflected)).rgb;
+    vec3 refracted = refract(view, nnormal, 0.667);
+    vec3 water_color = refraction_ray(world_pos, refracted);
 
+    //out_Color = vec4(1,0,1,1);
+    // out_Color = vec4(refracted * vec3(0.5) + vec3(0.5), 1.0);
     out_Color = vec4(mix(water_color, sky_color, R), 1.0);
+
+    // out_Color = texture(ground_depth, tex_coord);
 }
 
diff --git a/shaders/surface.vert b/shaders/surface.vert
index a7e5ff88a5a492a53bf64aab03e474ec9ad3c76e..69f69d92e4e14c7f0d9fd4a2d91965bd521e400b 100644
--- a/shaders/surface.vert
+++ b/shaders/surface.vert
@@ -12,6 +12,8 @@ uniform int in_WavesNum;
 
 out vec3 world_pos;
 out vec3 normal;
+out vec2 tex_coord;
+out float test;
 
 const float pi = 3.14159265358979323846;
 const float g = 9.82;
@@ -82,6 +84,9 @@ void main(void)
     vec3 offset_pos = get_waves(in_Position, time);
     world_pos = offset_pos;
     normal = compute_normal(in_Position, time);
+    tex_coord = in_TexCoord;
+
+    test = in_WavesNum;
 
 	gl_Position = projectionMatrix * modelToWorldToView * vec4(offset_pos, 1.0);
 }
diff --git a/shaders/waterfall.frag b/shaders/waterfall.frag
index 7d92382799e0b4e4370ba9fc4c86ebcb5e8c3c01..3244e1710286b70dbf76c27ecbc9f4b66dd5f6d7 100644
--- a/shaders/waterfall.frag
+++ b/shaders/waterfall.frag
@@ -1,11 +1,91 @@
 #version 150
 
-in float shade;
+const float step_size = 0.1;
+const int NUMBER_OF_STEPS = 32;
+const float MINIMUM_HIT_DISTANCE = 0.0001;
+const float MAXIMUM_TRACE_DISTANCE = 1000.0;
 
+uniform int num_balls;
+uniform float screenWidth;
+uniform float screenHeight;
+
+in vec3 world_pos;
+uniform vec3 camera_pos;
 out vec4 out_Color;
 
+struct Ball {
+  vec4 pos;
+  float radius;
+};
+
+layout(std140) uniform BallBuffer {
+    Ball balls[30];  // TODO: Need to manually update size
+};
+
+float distance_from_sphere(vec3 pos, Ball ball) {
+  return length(pos - ball.pos.xyz) - ball.radius;
+}
+
+// Function taken from: https://iquilezles.org/articles/distfunctions/
+float opSmoothUnion(float d1, float d2, float k) {
+    float h = clamp(0.5 + 0.5*(d2-d1)/k, 0.0, 1.0);
+    return mix(d2, d1, h) - k*h*(1.0-h);
+}
+
+float SDF(vec3 position) {
+    float min_dist = distance_from_sphere(position, balls[0]);
+
+    // TODO: iterate over uniform num_balls
+    for (int i = 1; i < 30; ++i) {
+      float dist = distance_from_sphere(position, balls[i]);
+
+      min_dist = opSmoothUnion(min_dist, dist, 0.2);
+    }
+
+    return min_dist;
+}
+
+vec3 calculate_normal(vec3 position) {
+  const vec3 small_step = vec3(0.001, 0, 0);
+  return normalize(
+    vec3(
+      SDF(position + small_step.xyy) - SDF(position - small_step.xyy),
+      SDF(position + small_step.yxy) - SDF(position - small_step.yxy),
+      SDF(position + small_step.yyx) - SDF(position - small_step.yyx)
+    )
+  );
+}
+
+vec3 ray_march(vec3 ro, vec3 rd) {
+
+  float total_distance_traveled = 0.0;
+
+  for (int i = 0; i < NUMBER_OF_STEPS; ++i) {
+    
+    vec3 current_position = ro + total_distance_traveled * rd;
+
+    float min_dist = SDF(current_position);
+
+    // hit
+    if (min_dist < MINIMUM_HIT_DISTANCE) {
+      const vec3 color = vec3(0.1, 0.2, 0.7);
+      vec3 normal = normalize(calculate_normal(current_position));
+      return color;
+    }
+
+    if (total_distance_traveled > MAXIMUM_TRACE_DISTANCE) {
+      discard;
+    }
+
+    total_distance_traveled += min_dist;
+  }
+}
+
 void main(void)
 {
-	out_Color=vec4(shade,shade,shade,1.0);
+  vec3 dir = normalize(world_pos - camera_pos);
+  vec3 shaded_color = ray_march(world_pos, dir);
+	out_Color = vec4(shaded_color, 1.0);
+	//out_Color = vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, 1);
+	//out_Color = vec4(shade, shade, shade, 0.0);
 }
-
diff --git a/shaders/waterfall.vert b/shaders/waterfall.vert
new file mode 100644
index 0000000000000000000000000000000000000000..655b91d54b661f3108c5595e0a7267a7ceb835f8
--- /dev/null
+++ b/shaders/waterfall.vert
@@ -0,0 +1,21 @@
+#version 150
+
+in  vec3  in_Position;
+in  vec3  in_Normal;
+in  vec2  in_TexCoord;
+
+uniform mat4 projectionMatrix;
+uniform mat4 modelToWorldToView;
+uniform mat4 invModelToWorld;
+uniform int num_balls;
+uniform float screenWidth;
+uniform float screenHeight;
+uniform vec3 camera_pos;
+
+out vec3 world_pos;
+
+void main(void)
+{
+	gl_Position = projectionMatrix * modelToWorldToView * vec4(in_Position, 1.0);
+	world_pos = in_Position;
+}
diff --git a/src/hdr.h b/src/hdr.h
new file mode 100644
index 0000000000000000000000000000000000000000..a44c4acf114a97ae588b94cdc44f7636b70d3ba4
--- /dev/null
+++ b/src/hdr.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <GL/gl.h>
+#include <array>
+#include <string>
+#include <vector>
+
+struct HDRImage {
+    using Pixel = std::array<float, 3>;
+
+    GLuint width;
+    GLuint height;
+    std::vector<Pixel> pixels;
+    GLuint texId;
+
+    HDRImage(GLuint width, GLuint height, std::vector<Pixel> pixels);
+
+    static HDRImage load_hdr(const std::string& path);
+};
+
diff --git a/src/main-waves.cpp b/src/main-waves.cpp
deleted file mode 100644
index 4da357a0f987d799f0ef96070fa8005e626d4b93..0000000000000000000000000000000000000000
--- a/src/main-waves.cpp
+++ /dev/null
@@ -1,369 +0,0 @@
-// Revised 2019 with a bit better variable names.
-// Experimental C++ version 2022. Almost no code changes.
-#include <iostream>
-#include <GL/gl.h>
-#include <sys/types.h>
-#define MAIN
-#include "GL_utilities.h"
-#include "LittleOBJLoader.h"
-#include "LoadTGA.h"
-#include "MicroGlut.h"
-#include "VectorUtils4.h"
-#include "waves.h"
-#include <vector>
-#include <random>
-// uses framework OpenGL
-// uses framework Cocoa
-
-struct Object {
-    Model* model;
-    GLuint program;
-
-    Object() = default;
-
-    void use() const
-    {
-        glUseProgram(program);
-    }
-
-    void draw() const
-    {
-        DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
-    }
-};
-
-
-struct Surface : public Object {
-    static GLuint const BINDING_POINT {0};
-    char const* UNIFORM_BLOCK = "WaveBuffer";
-    static int const MAX_WAVES {16};
-    static constexpr float const MEDIAN_WAVELENGTH {2.6};
-    static constexpr float const MEDIAN_AMPLITUDE {0.006};
-    static constexpr float const MEDIAN_SPEED {0.6};
-    static constexpr float const MEDIAN_DIR {3.14/4.0};
-
-    struct Wave {
-        GLfloat L; // Wavelength (distance between waves in world space)
-        GLfloat A; // Amplitude (height from surface to wave crest)
-        GLfloat S; // Speed (distance the crest moves per second)
-        char padding1[4];
-        vec2 D; // Direction (horizontal vector perpendicular to the wave front)
-        char padding2[8];
-    };
-
-    GLuint ubo;
-    GLuint block_index;
-
-    std::vector<Wave> waves {};
-    // Direction of the wind [0-2π]
-    float wind_dir {0.0};
-
-    Surface() = default;
-
-    Wave get_wave() {
-        std::random_device rd;
-        std::mt19937 gen(rd());
-
-        std::uniform_real_distribution<float> rd_wavelength(
-            0.5 * MEDIAN_WAVELENGTH, 2 * MEDIAN_WAVELENGTH
-        );
-
-        std::uniform_real_distribution<float> rd_amplitude(
-            0.5 * MEDIAN_AMPLITUDE, 2 * MEDIAN_AMPLITUDE
-        );
-
-        std::uniform_real_distribution<float> rd_speed(
-            0.5 * MEDIAN_SPEED, 2 * MEDIAN_SPEED
-        );
-
-        std::uniform_real_distribution<float> rd_dir(
-            wind_dir - MEDIAN_DIR, wind_dir + MEDIAN_DIR
-        );
-
-        float const dir {rd_dir(rd)};
-        return {
-            rd_wavelength(rd),
-            rd_amplitude(rd),
-            rd_speed(rd),
-            {},
-            vec2(cos(dir), sin(dir)),
-        };
-    }
-
-    Surface(Model* model, GLuint program)
-        : Object{model, program} {
-
-        use();
-
-        // Create a uniform buffer object, used to send waves.
-        glGenBuffers(1, &ubo);
-        glBindBuffer(GL_UNIFORM_BUFFER, ubo);
-
-        // Allocate memory in the buffer.
-        glBufferData(
-            GL_UNIFORM_BUFFER, sizeof(Wave) * MAX_WAVES, nullptr, GL_STATIC_DRAW
-        );
-
-        block_index = glGetUniformBlockIndex(program, "WaveBuffer");
-
-        // Bind the buffer to a binding point.
-        glBindBufferBase(GL_UNIFORM_BUFFER, BINDING_POINT, ubo);
-
-        // // Link the UBO to the shader’s uniform block
-        glUniformBlockBinding(program, block_index, BINDING_POINT);
-
-        for (int i = 0; i < MAX_WAVES; i++) {
-            waves.push_back(get_wave());
-        }
-    }
-
-    void draw() const
-    {
-        if (waves.size() > MAX_WAVES) {
-            std::cerr << "Error: The max limit of waves has been exceeded." << std::endl;
-            return;
-        }
-
-        // Bind the buffer before updating it.
-        glBindBuffer(GL_UNIFORM_BUFFER, ubo);
-        glBufferSubData(GL_UNIFORM_BUFFER, 0, waves.size() * sizeof(Wave), waves.data());
-
-        // Upload the number of waves.
-        glUniform1i(glGetUniformLocation(program, "in_WavesNum"), waves.size());
-
-        DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
-    }
-};
-
-struct Scene {
-    Object ground;
-    Surface surface;
-    Object waterfall;
-    Object skybox;
-
-    GLuint skybox_tex;
-
-    mat4 proj_matrix = perspective(70.0, 1.0, 0.2, 20.0);
-    vec3 pos;
-    float yaw;
-    float pitch;
-    mat4 view_matrix = lookAtv(vec3(-5, 4, -5), vec3(0, 0, 0), vec3(0, 1, 0));
-
-    void init()
-    {
-        Model* ground_model = LoadModel("models/ground.obj");
-        Model* surface_model = LoadModel("models/surface.obj");
-        Model* waterfall_model = LoadModel("models/waterfall.obj");
-        Model* skybox_model = LoadModel("models/skybox.obj");
-
-        // This is not a ground logic program
-        GLuint ground_program = loadShaders("shaders/basic.vert", "shaders/ground.frag");
-        GLuint surface_program = loadShaders("shaders/surface.vert", "shaders/surface.frag");
-        GLuint waterfall_program = loadShaders("shaders/basic.vert", "shaders/waterfall.frag");
-        GLuint skybox_program = loadShaders("shaders/skybox.vert", "shaders/skybox.frag");
-        printError("compiled shaders");
-
-        ground = Object { ground_model, ground_program };
-        surface = Surface { surface_model, surface_program };
-        waterfall = Object { waterfall_model, waterfall_program };
-        skybox = Object { skybox_model, skybox_program };
-
-        LoadTGATextureSimple("textures/sky4k.tga", &skybox_tex);
-        glBindTexture(GL_TEXTURE_2D, skybox_tex);
-        printError("bind texture");
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-        printError("tex parameteri");
-    }
-
-    void do_keyboard_input()
-    {
-        vec3 fwd = vec3(sin(yaw), 0, -cos(yaw));
-        vec3 right = vec3(cos(yaw), 0, sin(yaw));
-        vec3 up = vec3(0, 1, 0);
-        float speed = 0.15;
-
-        if (glutKeyIsDown('w')) {
-            pos += speed * fwd;
-        }
-        if (glutKeyIsDown('s')) {
-            pos -= speed * fwd;
-        }
-        if (glutKeyIsDown('d')) {
-            pos += speed * right;
-        }
-        if (glutKeyIsDown('a')) {
-            pos -= speed * right;
-        }
-        if (glutKeyIsDown('r')) {
-            pos += speed * up;
-        }
-        if (glutKeyIsDown('f')) {
-            pos -= speed * up;
-        }
-        if (glutKeyIsDown('e')) {
-            pos += speed * up;
-        }
-        if (glutKeyIsDown('q')) {
-            pos -= speed * up;
-        }
-    }
-
-    void update_view_matrix()
-    {
-        vec3 up { 0.0, 1.0, 0.0 };
-        mat4 rot = Rx(pitch) * Ry(yaw);
-        mat4 translation = T(-pos.x, -pos.y, -pos.z);
-        view_matrix = rot * translation;
-    }
-
-    void draw_skybox()
-    {
-        skybox.use();
-        GLuint program = skybox.program;
-        glDisable(GL_DEPTH_TEST);
-        glDisable(GL_CULL_FACE);
-
-        glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-        glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-        glUniform1i(glGetUniformLocation(program, "sky"), 0);
-        glActiveTexture(GL_TEXTURE0);
-        glBindTexture(GL_TEXTURE_2D, skybox_tex);
-
-
-        skybox.draw();
-        glEnable(GL_DEPTH_TEST);
-        glEnable(GL_CULL_FACE);
-    }
-
-    void draw_surface()
-    {
-        surface.use();
-        GLuint program = surface.program;
-        int elapsed_millis = glutGet(GLUT_ELAPSED_TIME);
-        float time = elapsed_millis * 0.001f;
-
-        glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-        glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-        glUniform1i(glGetUniformLocation(program, "sky"), 0);
-        glUniform3f(glGetUniformLocation(program, "camera_pos"), pos.x, pos.y, pos.z);
-        glUniform1f(glGetUniformLocation(program, "time"), time);
-
-        surface.draw();
-    }
-
-
-    void draw_ground()
-    {
-        ground.use();
-        GLuint program = ground.program;
-        glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-        glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-
-        ground.draw();
-    }
-
-    void draw_waterfall()
-    {
-        waterfall.use();
-        GLuint program = waterfall.program;
-        glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-        glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-
-        waterfall.draw();
-    }
-
-    void draw()
-    {
-        do_keyboard_input();
-        update_view_matrix();
-
-        draw_skybox();
-        draw_surface();
-        draw_ground();
-        draw_waterfall();
-    }
-};
-
-Scene scene {};
-int mouse_x = -1;
-int mouse_y = -1;
-
-void init(void)
-{
-    dumpInfo();
-
-    // GL inits
-    glClearColor(0.2, 0.2, 0.5, 0);
-    glEnable(GL_DEPTH_TEST);
-    glEnable(GL_CULL_FACE);
-    glCullFace(GL_BACK);
-    printError("GL inits"); // This is merely a vague indication of where something might be wrong
-                            //
-    scene.init();
-
-    // Load and compile shader
-    printError("init shader");
-}
-
-void display(void)
-{
-    printError("pre display");
-
-    // clear the screen
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
-    // activate the program, and set its variables
-    scene.draw();
-
-    printError("display");
-
-    glutSwapBuffers();
-}
-
-void on_mouse_move(int x, int y)
-{
-    const float sensitivity = 0.0025f;
-    if (mouse_y == -1 && mouse_x == -1) {
-        mouse_x = x;
-        mouse_y = y;
-
-        return;
-    }
-
-    int dx = x - mouse_x;
-    int dy = y - mouse_y;
-    mouse_x = x;
-    mouse_y = y;
-
-    scene.pitch += dy * sensitivity;
-    scene.yaw += dx * sensitivity;
-
-    scene.pitch = std::max(-1.5f, std::min(1.5f, scene.pitch));
-}
-
-void on_mouse_button(int button, int state, int x, int y)
-{
-    if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
-        mouse_x = x;
-        mouse_y = y;
-    }
-}
-
-int main(int argc, char* argv[])
-{
-    glutInit(&argc, argv);
-    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
-    glutInitContextVersion(3, 2);
-    glutInitWindowSize(800, 800);
-    glutCreateWindow("TSBK03 Project");
-    glutDisplayFunc(display);
-    glutRepeatingTimer(20);
-    init();
-    glutMotionFunc(on_mouse_move);
-    glutMouseFunc(on_mouse_button);
-
-    Waves::init();
-
-    glutMainLoop();
-    exit(0);
-}
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0ad1f7837731942183fb50dd95b02a1ae811b03a
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,304 @@
+// Revised 2019 with a bit better variable names.
+// Experimental C++ version 2022. Almost no code changes.
+
+#include "object.h"
+#include "waterfall.h"
+#include "surface.h"
+
+#include <GL/gl.h>
+#include <cstdlib>
+#include <sys/types.h>
+#include <GL/glext.h>
+#define MAIN
+#include "GL_utilities.h"
+#include "LittleOBJLoader.h"
+#include "LoadTGA.h"
+#include "MicroGlut.h"
+#include "VectorUtils4.h"
+// uses framework OpenGL
+// uses framework Cocoa
+
+
+
+struct Scene {
+  Object ground;
+  Surface surface;
+  Waterfall waterfall;
+  Object skybox;
+
+  GLuint skybox_tex;
+  FBOstruct *ground_fbo;
+  GLuint dirt_tex;
+  GLuint grass_tex;
+
+  mat4 proj_matrix = perspective(70.0, 1.0, 0.2, 20.0);
+  mat4 orth_matrix = ortho(-5.0, 5.0, -5.0, 5.0, -2.0, 4.5);
+
+  vec3 pos;
+  float yaw;
+  float pitch;
+  mat4 view_matrix = lookAtv(vec3(-5, 4, -5), vec3(0, 0, 0), vec3(0, 1, 0));
+  mat4 top_view_matrix = lookAtv(vec3(0, 0, 0), vec3(0, -1, 0), vec3(0, 0, 1));
+
+  void init() {
+    Model *ground_model = LoadModel("models/ground.obj");
+    Model *surface_model = LoadModel("models/surface.obj");
+    Model *waterfall_model = LoadModel("models/wide_cube.obj");
+    Model *skybox_model = LoadModel("models/skybox.obj");
+
+    // This is not a ground logic program
+    GLuint ground_program =
+        loadShaders("shaders/basic.vert", "shaders/ground.frag");
+    GLuint surface_program =
+        loadShaders("shaders/surface.vert", "shaders/surface.frag");
+    GLuint waterfall_program =
+        loadShaders("shaders/waterfall.vert", "shaders/waterfall.frag");
+    GLuint skybox_program =
+        loadShaders("shaders/skybox.vert", "shaders/skybox.frag");
+    printError("compiled shaders");
+    ground = Object{ground_model, ground_program};
+    surface = Surface{surface_model, surface_program};
+    waterfall = Waterfall{waterfall_model, waterfall_program};
+    skybox = Object{skybox_model, skybox_program};
+
+    ground_fbo = initFBO2(1024, 1024, GL_LINEAR, true);
+
+    LoadTGATextureSimple("textures/sky4k.tga", &skybox_tex);
+    glBindTexture(GL_TEXTURE_2D, skybox_tex);
+    printError("bind texture");
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    printError("tex parameteri");
+
+    LoadTGATextureSimple("textures/grass.tga", &grass_tex);
+    LoadTGATextureSimple("textures/dirt.tga", &dirt_tex);
+    printError("tex parameteri");
+  }
+
+  void do_keyboard_input() {
+    vec3 fwd = vec3(sin(yaw), 0, -cos(yaw));
+    vec3 right = vec3(cos(yaw), 0, sin(yaw));
+    vec3 up = vec3(0, 1, 0);
+    float speed = 0.3;
+
+    if (glutKeyIsDown('w')) {
+      pos += speed * fwd;
+    }
+    if (glutKeyIsDown('s')) {
+      pos -= speed * fwd;
+    }
+    if (glutKeyIsDown('d')) {
+      pos += speed * right;
+    }
+    if (glutKeyIsDown('a')) {
+      pos -= speed * right;
+    }
+    if (glutKeyIsDown('r')) {
+      pos += speed * up;
+    }
+    if (glutKeyIsDown('f')) {
+      pos -= speed * up;
+    }
+  }
+
+  void draw_ground_fbo()
+  {
+      useFBO(ground_fbo, nullptr, nullptr);
+      ground.use();
+      GLuint program = ground.program;
+
+      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+      glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, orth_matrix.m);
+      glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, top_view_matrix.m);
+
+      glActiveTexture(GL_TEXTURE0);
+      glBindTexture(GL_TEXTURE_2D, grass_tex);
+
+      glActiveTexture(GL_TEXTURE1);
+      glBindTexture(GL_TEXTURE_2D, dirt_tex);
+
+      glUniform1i(glGetUniformLocation(program, "grass"), 0);
+      glUniform1i(glGetUniformLocation(program, "dirt"), 1);
+
+      ground.draw();
+
+      useFBO(nullptr, nullptr, nullptr);
+  }
+
+  void draw_surface()
+  {
+    surface.use();
+    GLuint program = surface.program;
+    int elapsed_millis = glutGet(GLUT_ELAPSED_TIME);
+    float time = elapsed_millis * 0.001f;
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, skybox_tex);
+
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_2D, ground_fbo->texid);
+
+    glActiveTexture(GL_TEXTURE2);
+    glBindTexture(GL_TEXTURE_2D, ground_fbo->depth);
+
+    glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
+    glUniform1i(glGetUniformLocation(program, "sky"), 0);
+    glUniform1i(glGetUniformLocation(program, "ground"), 1);
+    glUniform1i(glGetUniformLocation(program, "ground_depth"), 2);
+    glUniform3f(glGetUniformLocation(program, "camera_pos"), pos.x, pos.y, pos.z);
+    glUniform1f(glGetUniformLocation(program, "time"), time);
+
+    surface.draw();
+  }
+
+  void update_view_matrix() {
+    vec3 up{0.0, 1.0, 0.0};
+    mat4 rot = Rx(pitch) * Ry(yaw);
+    mat4 translation = T(-pos.x, -pos.y, -pos.z);
+    view_matrix = rot * translation;
+  }
+
+  void draw_skybox() {
+    skybox.use();
+    GLuint program = skybox.program;
+    glDisable(GL_DEPTH_TEST);
+    glDisable(GL_CULL_FACE);
+
+    glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1,
+                       GL_TRUE, proj_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1,
+                       GL_TRUE, view_matrix.m);
+    glUniform1i(glGetUniformLocation(program, "sky"), 0);
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, skybox_tex);
+
+    skybox.draw();
+    glEnable(GL_DEPTH_TEST);
+    glEnable(GL_CULL_FACE);
+  }
+
+  void draw_waterfall() {
+    waterfall.use();
+    GLuint program = waterfall.program;
+    glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1,
+                       GL_TRUE, proj_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "worldToView"), 1, GL_TRUE,
+                       view_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1,
+                       GL_TRUE, view_matrix.m);
+    glUniform3f(glGetUniformLocation(program, "camera_pos"), pos.x, pos.y,
+                pos.z);
+
+    waterfall.draw();
+    waterfall.move_waterfall_balls();
+  }
+  void draw_ground()
+  {
+    ground.use();
+    GLuint program = ground.program;
+    glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, grass_tex);
+
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_2D, dirt_tex);
+
+    glUniform1i(glGetUniformLocation(program, "grass"), 0);
+    glUniform1i(glGetUniformLocation(program, "dirt"), 1);
+
+    ground.draw();
+  }
+
+
+  void draw()
+  {
+    do_keyboard_input();
+    update_view_matrix();
+
+    draw_ground_fbo();
+
+    draw_skybox();
+    draw_surface();
+    draw_ground();
+    draw_waterfall();
+  }
+};
+
+Scene scene{};
+int mouse_x = -1;
+int mouse_y = -1;
+
+void init(void) {
+  dumpInfo();
+
+  // GL inits
+  glClearColor(0.2, 0.2, 0.5, 0);
+  glEnable(GL_DEPTH_TEST);
+  glEnable(GL_CULL_FACE);
+  glCullFace(GL_BACK);
+  printError("GL inits"); // This is merely a vague indication of where
+                          // something might be wrong
+                          //
+  scene.init();
+
+  // Load and compile shader
+  printError("init shader");
+}
+
+void display(void) {
+  printError("pre display");
+
+  // clear the screen
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  // activate the program, and set its variables
+  scene.draw();
+
+  printError("display");
+
+  glutSwapBuffers();
+}
+
+void on_mouse_move(int x, int y)
+{
+  const float sensitivity = 0.0025f;
+  if (mouse_y == -1 && mouse_x == -1) {
+      mouse_x = x;
+      mouse_y = y;
+      return;
+  }
+
+  int dx = x - mouse_x;
+  int dy = y - mouse_y;
+  mouse_x = x;
+  mouse_y = y;
+
+  scene.pitch += dy * sensitivity;
+  scene.yaw += dx * sensitivity;
+  scene.pitch = std::max(-1.5f, std::min(1.5f, scene.pitch));
+}
+
+void on_mouse_button(int button, int state, int x, int y) {
+  if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
+    mouse_x = x;
+    mouse_y = y;
+  }
+}
+
+int main(int argc, char *argv[]) {
+  glutInit(&argc, argv);
+  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
+  glutInitContextVersion(3, 2);
+  glutInitWindowSize(800, 800);
+  glutCreateWindow("TSBK03 Project");
+  glutDisplayFunc(display);
+  glutRepeatingTimer(20);
+  init();
+  glutMotionFunc(on_mouse_move);
+  glutMouseFunc(on_mouse_button);
+
+  glutMainLoop();
+  exit(0);
+}
diff --git a/src/object.h b/src/object.h
new file mode 100644
index 0000000000000000000000000000000000000000..48c805c674bcec12801cef3be1fbfdfdece26a25
--- /dev/null
+++ b/src/object.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "LittleOBJLoader.h"
+#include <GL/gl.h>
+
+struct Object {
+  Model *model;
+  GLuint program;
+
+  Object() = default;
+
+  void use() const { glUseProgram(program); }
+
+  void draw() const {
+    DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
+    // DrawModel(model, program, "in_Position", "in_Normal", NULL);
+  }
+};
diff --git a/src/surface.cpp b/src/surface.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5592da146889d09ce8bd925958482866b5f9855d
--- /dev/null
+++ b/src/surface.cpp
@@ -0,0 +1,79 @@
+#include "surface.h"
+#include <iostream>
+#include <ostream>
+#include <random>
+
+
+Surface::Wave Surface::get_wave() const {
+    std::random_device rd;
+    std::mt19937 gen(rd());
+
+    std::uniform_real_distribution<float> rd_wavelength(
+        0.5 * MEDIAN_WAVELENGTH, 2 * MEDIAN_WAVELENGTH
+    );
+
+    std::uniform_real_distribution<float> rd_amplitude(
+        0.5 * MEDIAN_AMPLITUDE, 2 * MEDIAN_AMPLITUDE
+    );
+
+    std::uniform_real_distribution<float> rd_speed(
+        0.5 * MEDIAN_SPEED, 2 * MEDIAN_SPEED
+    );
+
+    std::uniform_real_distribution<float> rd_dir(
+        wind_dir - MEDIAN_DIR, wind_dir + MEDIAN_DIR
+    );
+
+    float const dir {rd_dir(rd)};
+    return {
+        rd_wavelength(rd),
+        rd_amplitude(rd),
+        rd_speed(rd),
+        {},
+        vec2(cos(dir), sin(dir)),
+    };
+}
+
+Surface::Surface(Model* model, GLuint program)
+    : Object{model, program} {
+    use();
+
+    // Create a uniform buffer object, used to send waves.
+    glGenBuffers(1, &ubo);
+    glBindBuffer(GL_UNIFORM_BUFFER, ubo);
+
+    // Allocate memory in the buffer.
+    glBufferData(
+        GL_UNIFORM_BUFFER, sizeof(Wave) * MAX_WAVES, nullptr, GL_STATIC_DRAW
+    );
+
+    block_index = glGetUniformBlockIndex(program, "WaveBuffer");
+
+    // Bind the buffer to a binding point.
+    glBindBufferBase(GL_UNIFORM_BUFFER, BINDING_POINT, ubo);
+
+    // // Link the UBO to the shader’s uniform block
+    glUniformBlockBinding(program, block_index, BINDING_POINT);
+
+    for (int i = 0; i < MAX_WAVES; i++) {
+        waves.push_back(get_wave());
+    }
+}
+
+void Surface::draw() const
+{
+    if (waves.size() > MAX_WAVES) {
+        std::cerr << "Error: The max limit of waves has been exceeded." << std::endl;
+        return;
+    }
+
+    // Bind the buffer before updating it.
+    glBindBuffer(GL_UNIFORM_BUFFER, ubo);
+    glBufferSubData(GL_UNIFORM_BUFFER, 0, waves.size() * sizeof(Wave), waves.data());
+
+    // Upload the number of waves.
+    glUniform1i(glGetUniformLocation(program, "in_WavesNum"), waves.size());
+
+    DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
+}
+
diff --git a/src/surface.h b/src/surface.h
new file mode 100644
index 0000000000000000000000000000000000000000..9a50510124612da3cf6cccce99eebc4bf3f85c16
--- /dev/null
+++ b/src/surface.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "object.h"
+#include <vector>
+
+
+class Surface : public Object {
+public:
+
+    Surface() = default;
+    Surface(Model* model, GLuint program);
+    void draw() const;
+
+private:
+    static GLuint const BINDING_POINT {1};
+    char const* UNIFORM_BLOCK = "WaveBuffer";
+    static int const MAX_WAVES {16};
+    static constexpr float const MEDIAN_WAVELENGTH {2.6};
+    static constexpr float const MEDIAN_AMPLITUDE {0.006};
+    static constexpr float const MEDIAN_SPEED {0.6};
+    static constexpr float const MEDIAN_DIR {3.14/4.0};
+
+    struct Wave {
+        GLfloat L; // Wavelength (distance between waves in world space)
+        GLfloat A; // Amplitude (height from surface to wave crest)
+        GLfloat S; // Speed (distance the crest moves per second)
+        char padding1[4];
+        vec2 D; // Direction (horizontal vector perpendicular to the wave front)
+        char padding2[8];
+    };
+
+    GLuint ubo;
+    GLuint block_index;
+    std::vector<Wave> waves {};
+    float wind_dir {0.0}; // Direction of the wind [0-2π]
+
+    Wave get_wave() const;
+};
+
diff --git a/src/waterfall.cpp b/src/waterfall.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b1cfa99a8793c11e7556e0671e172db40ff8b9e3
--- /dev/null
+++ b/src/waterfall.cpp
@@ -0,0 +1,107 @@
+#include "waterfall.h"
+#include "GL_utilities.h"
+#include "object.h"
+#include <cmath>
+#include <iostream>
+#include <ostream>
+
+Waterfall::Waterfall(Model *model, GLuint program) : Object{model, program} {
+
+  float min_x = std::numeric_limits<float>::max();
+  float max_x = std::numeric_limits<float>::min();
+  float min_y = std::numeric_limits<float>::max();
+  float max_y = std::numeric_limits<float>::min();
+  float min_z = std::numeric_limits<float>::max();
+  float max_z = std::numeric_limits<float>::min();
+
+  for (int i = 0; i < model->numVertices; ++i) {
+    min_x = std::min(min_x, model->vertexArray[i].x);
+    max_x = std::max(max_x, model->vertexArray[i].x);
+    min_y = std::min(min_y, model->vertexArray[i].y);
+    max_y = std::max(max_y, model->vertexArray[i].y);
+    min_z = std::min(min_z, model->vertexArray[i].z);
+    max_z = std::max(max_z, model->vertexArray[i].z);
+  }
+
+  x = {min_x, max_x};
+  y = {min_y, max_y};
+  z = {min_z, max_z};
+
+  gen_balls();
+  use();
+
+  // Create a uniform buffer object, used to send waves.
+  glGenBuffers(1, &ubo);
+  glBindBuffer(GL_UNIFORM_BUFFER, ubo);
+
+  // Allocate memory in the buffer.
+  glBufferData(GL_UNIFORM_BUFFER, sizeof(balls), nullptr, GL_DYNAMIC_DRAW);
+
+  block_index = glGetUniformBlockIndex(program, UNIFORM_BLOCK);
+
+  if (block_index == GL_INVALID_INDEX) {
+    std::cout << "Waterfall: Block name could not be found." << std::endl;
+  }
+
+  // Bind the buffer to a binding point.
+  glBindBufferBase(GL_UNIFORM_BUFFER, BINDING_POINT, ubo);
+
+  // Link the UBO to the shader’s uniform block
+  glUniformBlockBinding(program, block_index, BINDING_POINT);
+}
+
+void Waterfall::draw() const {
+
+  // Bind the buffer before updating it.
+  glBindBuffer(GL_UNIFORM_BUFFER, ubo);
+  // Assign values to the `balls` array and update the UBO data
+  glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(balls), balls);
+
+  // Upload the number of balls.
+  glUniform1i(glGetUniformLocation(program, "num_balls"), NUM_BALLS);
+
+  DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
+  printError("Waterfall draw");
+}
+
+void Waterfall::move_waterfall_balls() {
+  // Moves the balls one distance in their velocity. If they fall
+  // below 0 (end of the waterfall) their height is reset of waterfall
+  // height.
+
+  for (int i{0}; i < NUM_BALLS; ++i) {
+    balls[i].pos += ball_velocities[i];
+
+    // TODO: check if balls are outside in x and z plane aswell
+    // maybe just generate a new ball if it falls outside
+    if (balls[i].pos.y < 0) {
+      balls[i].pos.y = y.second;
+    }
+  }
+}
+
+float lerp(float a, float b, float t) { return a + t * (b - a); }
+
+void Waterfall::gen_balls() {
+  for (int i{0}; i < NUM_BALLS; ++i) {
+
+    // pseudo random value between 0.01 and 0.1
+    float radius = ((rand() / (float)RAND_MAX) * 0.9 + 0.1) / 10.0;
+
+    // pseudo random value between min-radius and max-radius
+    float pos_x = lerp(x.first, x.second, (rand() / (float)RAND_MAX));
+    float pos_y = lerp(y.first, y.second, (rand() / (float)RAND_MAX));
+    float pos_z = lerp(z.first, z.second, (rand() / (float)RAND_MAX));
+    vec4 pos = vec4(pos_x, pos_y, pos_z, 0);
+
+    // pseudo random value between -0.001 and -0.01
+    float velocity_y = -((rand() / (float)RAND_MAX) * 0.9 + 0.1) / 50.0;
+    vec4 velocity = vec4(0, velocity_y, 0, 0);
+
+    balls[i] = Waterfall::Ball(pos, radius);
+    ball_velocities[i] = velocity;
+  }
+  // balls[0].pos = vec4(3.2, 1.0, 0, 0);
+  // balls[0].radius = 0.55;
+  // ball_velocities[0] = vec4(0);
+}
diff --git a/src/waterfall.h b/src/waterfall.h
new file mode 100644
index 0000000000000000000000000000000000000000..76f849ac1fadf9b9c2d39f7d4b38ec3d89ecab30
--- /dev/null
+++ b/src/waterfall.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "VectorUtils4.h"
+#include "object.h"
+
+struct Waterfall : Object {
+
+  Waterfall() = default;
+  Waterfall(Model *model, GLuint program);
+  void move_waterfall_balls();
+  void gen_balls();
+  void draw() const;
+
+  static GLuint const BINDING_POINT{0};
+  char const *UNIFORM_BLOCK = "BallBuffer";
+  static int const NUM_BALLS{30};
+
+  // Dimensions of the waterfall
+  // on the form <min, max>
+  std::pair<float, float> x;
+  std::pair<float, float> y;
+  std::pair<float, float> z;
+
+  // Properties of each ball
+  struct Ball {
+    vec4 pos;
+    float radius;
+
+    // Glsls padding to make struct a multiple of vec4 (cringe)
+    vec3 padding;
+
+    Ball(vec3 pos, float radius) : pos{pos}, radius{radius} {}
+    Ball() : pos{0, 0, 0, 0}, radius{0} {}
+  };
+
+  Ball balls[NUM_BALLS];
+  vec4 ball_velocities[NUM_BALLS];
+
+  GLuint ubo;
+  GLuint block_index;
+};
diff --git a/textures/autumn_field_puresky_4k.hdr b/textures/autumn_field_puresky_4k.hdr
new file mode 100644
index 0000000000000000000000000000000000000000..494be719db2d0523f390fa20d9c2575938455cb0
Binary files /dev/null and b/textures/autumn_field_puresky_4k.hdr differ
diff --git a/textures/dirt.tga b/textures/dirt.tga
new file mode 100644
index 0000000000000000000000000000000000000000..0b87bfe1c249e6b235d4ef3afc6380db70e2f260
Binary files /dev/null and b/textures/dirt.tga differ
diff --git a/textures/grass.tga b/textures/grass.tga
new file mode 100644
index 0000000000000000000000000000000000000000..841a4ecb5fa5ce8a3aa616491637eb655f5251b6
Binary files /dev/null and b/textures/grass.tga differ