diff --git a/shaders/basic.vert b/shaders/basic.vert
index 3774ffde1272d28cb47aa541d97805d61eafe113..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 18f3dfa2131de80ac0ef82a7bef8ae6bfd9a0e21..69128ec03c561823e8898112b69623d33923c684 100644
--- a/shaders/surface.frag
+++ b/shaders/surface.frag
@@ -2,14 +2,21 @@
 
 in vec3 world_pos;
 in vec3 normal;
+in vec2 tex_coord;
 
 out vec4 out_Color;
 
 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;
+
 
 vec2 sphere_uv(vec3 direction)
 {
@@ -25,16 +32,55 @@ 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.05, 0.1);
+    // 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 4e09f949d6a6875c23f955805f2ba96ec2c916cb..4bc9c68843659575cd1426c029d328e95cbbca96 100644
--- a/shaders/surface.vert
+++ b/shaders/surface.vert
@@ -10,12 +10,14 @@ uniform float time;
 
 out vec3 world_pos;
 out vec3 normal;
+out vec2 tex_coord;
 
 void main(void)
 {
     vec3 offset_pos = vec3(in_Position.x, in_Position.y + 0.05 * sin(10.0 * in_Position.x + time), in_Position.z);
     world_pos = offset_pos;
     normal = normalize(vec3(-0.05 * cos(10.0 * in_Position.x + time), 1.0, 0.0));
+    tex_coord = in_TexCoord;
 
 	gl_Position = projectionMatrix * modelToWorldToView * vec4(offset_pos, 1.0);
 }
diff --git a/src/main.cpp b/src/main.cpp
index 19ef9e98351706d76bfcb8cb9e046beca75ab609..283dc510e988f18ca0b4f45c7c9d467e9d164019 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -26,6 +26,8 @@ struct Scene {
 
   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);
@@ -64,6 +66,10 @@ struct Scene {
     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() {
@@ -94,18 +100,27 @@ struct Scene {
 
   void draw_ground_fbo()
   {
-    useFBO(ground_fbo, nullptr, nullptr);
-    ground.use();
-    GLuint program = ground.program;
+      useFBO(ground_fbo, nullptr, nullptr);
+      ground.use();
+      GLuint program = ground.program;
+
+      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
-    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);
 
-    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);
 
-    ground.draw();
+      glActiveTexture(GL_TEXTURE1);
+      glBindTexture(GL_TEXTURE_2D, dirt_tex);
 
-    useFBO(nullptr, nullptr, nullptr);
+      glUniform1i(glGetUniformLocation(program, "grass"), 0);
+      glUniform1i(glGetUniformLocation(program, "dirt"), 1);
+
+      ground.draw();
+
+      useFBO(nullptr, nullptr, nullptr);
   }
 
   void draw_surface()
@@ -118,25 +133,23 @@ struct Scene {
     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 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 update_view_matrix() {
     vec3 up{0.0, 1.0, 0.0};
     mat4 rot = Rx(pitch) * Ry(yaw);
@@ -163,7 +176,6 @@ struct Scene {
     glEnable(GL_CULL_FACE);
   }
 
-
   void draw_waterfall() {
     waterfall.use();
     GLuint program = waterfall.program;
@@ -179,11 +191,32 @@ struct Scene {
     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);
 
-  void draw() {
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_2D, dirt_tex);
+
+    glUniform1i(glGetUniformLocation(program, "grass"), 0);
+    glUniform1i(glGetUniformLocation(program, "dirt"), 1);
+
+    waterfall.draw();
+  }
+
+
+  void draw()
+  {
     do_keyboard_input();
     update_view_matrix();
 
+    draw_ground_fbo();
+
     draw_skybox();
     draw_surface();
     draw_ground();
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