diff --git a/models/skybox.obj b/models/skybox.obj
new file mode 100644
index 0000000000000000000000000000000000000000..efca9edd0d9b4141fd45d9da514b346068dd1844
--- /dev/null
+++ b/models/skybox.obj
@@ -0,0 +1,38 @@
+# Blender 4.2.3 LTS
+# www.blender.org
+o Cube
+v 1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 1.000000
+v -1.000000 1.000000 -1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 1.000000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vt 0.625000 0.500000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.125000 0.500000
+vt 0.375000 0.500000
+vt 0.125000 0.750000
+s 0
+f 1/1/1 5/2/1 7/3/1 3/4/1
+f 4/5/2 3/4/2 7/6/2 8/7/2
+f 8/8/3 7/9/3 5/10/3 6/11/3
+f 6/12/4 2/13/4 4/5/4 8/14/4
+f 2/13/5 1/1/5 3/4/5 4/5/5
+f 6/11/6 5/10/6 1/1/6 2/13/6
diff --git a/shaders/skybox.frag b/shaders/skybox.frag
new file mode 100644
index 0000000000000000000000000000000000000000..3e105bbb588eb52bb6d851b8e64a6b484fa97340
--- /dev/null
+++ b/shaders/skybox.frag
@@ -0,0 +1,24 @@
+#version 150
+
+in vec3 position;
+
+out vec4 out_Color;
+
+uniform sampler2D sky;
+
+const float PI = 3.1415926535897f;
+
+vec2 sphere_uv(vec3 direction)
+{
+    float u = 0.5 + atan(direction.z, direction.x) / (2 * PI);
+    float v = 0.5 + asin(direction.y) / PI;
+
+    return vec2(u, v);
+}
+
+void main(void)
+{
+	out_Color = texture(sky, sphere_uv(normalize(position)));
+	// out_Color = vec4(0, sphere_uv(normalize(position)), 1.0);
+}
+
diff --git a/shaders/skybox.vert b/shaders/skybox.vert
new file mode 100644
index 0000000000000000000000000000000000000000..2feb43e1cc1f56a35ce9b389f0bd5c9b2eae2ec6
--- /dev/null
+++ b/shaders/skybox.vert
@@ -0,0 +1,18 @@
+#version 150
+
+in  vec3  in_Position;
+in  vec3  in_Normal;
+in  vec2  in_TexCoord;
+
+uniform mat4 projectionMatrix;
+uniform mat4 modelToWorldToView;
+
+out vec3 position;
+
+void main(void)
+{
+    position = in_Position;
+    vec4 view_pos = vec4(mat3(modelToWorldToView) * in_Position.xyz, 1.0);
+	gl_Position = projectionMatrix * view_pos;
+}
+
diff --git a/src/main.cpp b/src/main.cpp
index ae8883a13c31341825c64dc0ac78000ed1027f5e..51b4f34594bf5743786b035a6508504f6688232a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -6,6 +6,7 @@
 #define MAIN
 #include "GL_utilities.h"
 #include "LittleOBJLoader.h"
+#include "LoadTGA.h"
 #include "MicroGlut.h"
 #include "VectorUtils4.h"
 // uses framework OpenGL
@@ -17,11 +18,13 @@ struct Object {
 
     Object() = default;
 
-    template <typename Func>
-    void draw(Func func) const
+    void use() const
     {
         glUseProgram(program);
-        func(program);
+    }
+
+    void draw() const
+    {
         DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord");
     }
 };
@@ -30,51 +33,143 @@ 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");
-        std::cout << "ground prog: " << ground_program << "\n";
-        std::cout << "surf prog: " << surface_program << "\n";
-        std::cout << "wfall prog: " << waterfall_program << "\n";
 
         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");
+    }
+
+    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);
+    }
+
+    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, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
+
+        waterfall.draw();
     }
 
     void draw()
     {
-        ground.draw([&](GLuint program) {
-            // Add uniforms, texture calls etc. here
-            glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-            glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-        });
-        surface.draw([&](GLuint program) {
-            // Add uniforms, texture calls etc. here
-            glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-            glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-        });
-        waterfall.draw([&](GLuint program) {
-            // Add uniforms, texture calls etc. here
-            glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m);
-            glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m);
-        });
+        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)
 {
@@ -108,6 +203,35 @@ void display(void)
     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;
+    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);
@@ -118,6 +242,9 @@ int main(int argc, char* argv[])
     glutDisplayFunc(display);
     glutRepeatingTimer(20);
     init();
+    glutMotionFunc(on_mouse_move);
+    glutMouseFunc(on_mouse_button);
+
     glutMainLoop();
     exit(0);
 }
diff --git a/textures/sky.tga b/textures/sky.tga
new file mode 100644
index 0000000000000000000000000000000000000000..2cc35a2c7bd7b03f392c4e8438b82514ef51ab03
Binary files /dev/null and b/textures/sky.tga differ
diff --git a/textures/sky4k.tga b/textures/sky4k.tga
new file mode 100644
index 0000000000000000000000000000000000000000..898ea12aef48efe78cbe1d423dd02ef1cf29d4db
Binary files /dev/null and b/textures/sky4k.tga differ