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..3774ffde1272d28cb47aa541d97805d61eafe113 100644
--- a/shaders/basic.vert
+++ b/shaders/basic.vert
@@ -9,9 +9,9 @@ uniform mat4 modelToWorldToView;
 
 out float shade;
 
+
 void main(void)
 {
 	shade = (mat3(modelToWorldToView)*in_Normal).z; // Fake shading
 	gl_Position=projectionMatrix*modelToWorldToView*vec4(in_Position, 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/main.cpp b/src/main.cpp
index 51b4f34594bf5743786b035a6508504f6688232a..813928e1878e31e472b8ae987531bc5ecd650684 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,8 +1,14 @@
 // Revised 2019 with a bit better variable names.
 // Experimental C++ version 2022. Almost no code changes.
-#include <iostream>
+#include <cstdlib>
+
+#include "object.h"
+#include "waterfall.h"
 
 #include <GL/gl.h>
+#include <GL/glext.h>
+#include <iostream>
+#include <ostream>
 #define MAIN
 #include "GL_utilities.h"
 #include "LittleOBJLoader.h"
@@ -12,239 +18,226 @@
 // 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 Scene {
-    Object ground;
-    Object 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 = Object { 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_WRAP_S, GL_MIRRORED_REPEAT);
-        //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-        printError("tex parameteri");
+  Object ground;
+  Object surface;
+  Waterfall 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/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 = Object{surface_model, surface_program};
+    waterfall = Waterfall{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_WRAP_S,
+    // GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D,
+    // GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
+    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.3;
+
+    if (glutKeyIsDown('w')) {
+      pos += speed * fwd;
     }
-
-    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 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);
+    if (glutKeyIsDown('s')) {
+      pos -= speed * fwd;
     }
-
-    void draw_surface()
-    {
-        surface.use();
-        GLuint program = surface.program;
-        glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-        glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-
-        surface.draw();
+    if (glutKeyIsDown('d')) {
+      pos += speed * right;
     }
-
-    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();
+    if (glutKeyIsDown('a')) {
+      pos -= speed * right;
     }
-
-    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();
+    if (glutKeyIsDown('r')) {
+      pos += speed * up;
     }
-
-    void draw()
-    {
-        do_keyboard_input();
-        update_view_matrix();
-
-        draw_skybox();
-        draw_surface();
-        draw_ground();
-        draw_waterfall();
+    if (glutKeyIsDown('f')) {
+      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;
+    glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1,
+                       GL_TRUE, proj_matrix.m);
+    glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1,
+                       GL_TRUE, view_matrix.m);
+
+    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, "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() {
+    do_keyboard_input();
+    update_view_matrix();
+
+    draw_skybox();
+    draw_surface();
+    draw_ground();
+    draw_waterfall();
+  }
 };
 
-Scene scene {};
+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 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");
+void display(void) {
+  printError("pre display");
 
-    // clear the screen
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+  // clear the screen
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
-    // activate the program, and set its variables
-    scene.draw();
+  // activate the program, and set its variables
+  scene.draw();
 
-    printError("display");
+  printError("display");
 
-    glutSwapBuffers();
+  glutSwapBuffers();
 }
 
-void on_mouse_move(int x, int y)
-{
-    const float sensitivity = 0.005f;
-    if (mouse_y == -1 && mouse_x == -1) {
-        mouse_x = x;
-        mouse_y = y;
-
-        return;
-    }
-
-    int dx = x - mouse_x;
-    int dy = y - mouse_y;
+void on_mouse_move(int x, int y) {
+  const float sensitivity = 0.005f;
+  if (mouse_y == -1 && mouse_x == -1) {
     mouse_x = x;
     mouse_y = y;
 
-    scene.pitch -= dy * sensitivity;
-    scene.yaw += dx * sensitivity;
+    return;
+  }
 
-    scene.pitch = std::max(-1.5f, std::min(1.5f, scene.pitch));
+  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;
-    }
+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);
+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/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;
+};