diff --git a/res/dpr/mesh_data.inc b/res/dpr/mesh_data.inc
new file mode 100644
index 0000000000000000000000000000000000000000..13e2785abd3f8863dbdfeec532213d491666eeb8
--- /dev/null
+++ b/res/dpr/mesh_data.inc
@@ -0,0 +1,3 @@
+struct MeshData {
+    mat4 localToWorldSpace;
+};
diff --git a/src/scene.cpp b/src/scene.cpp
index 6bc21b7f5863084bd97147656f3f01a9509c7913..93376797361500d3f87848cc1bf0e589bb44588f 100644
--- a/src/scene.cpp
+++ b/src/scene.cpp
@@ -4,6 +4,7 @@
 #include <assimp/scene.h>
 #include <filesystem>
 #include <glm/gtc/type_ptr.hpp>
+#include <glm/gtx/quaternion.hpp>
 
 using namespace lava;
 
@@ -18,6 +19,44 @@ void scene::set_vertex_input(lava::graphics_pipeline* pipeline) {
 }
 
 void scene::update(float delta_time) {
+    current_time += delta_time * animations[active_animation].ticks_per_second;
+
+    for (const auto& channel : animations[active_animation].channels) {
+      glm::vec3 position(0.0f);
+      glm::quat rotation(1.0f, 0.0f, 0.0f, 0.0f);
+      glm::vec3 scaling(1.0f);
+
+      std::pair<float, glm::vec3> last_position_frame = channel.position_frames.front();
+      for (const auto& position_frame : channel.position_frames) {
+          if (position_frame.first > current_time) {
+            const float t = (current_time - last_position_frame.first) / (position_frame.first - last_position_frame.first);
+            position = glm::mix(last_position_frame.second, position_frame.second, t);
+            break;
+          }
+          last_position_frame = position_frame;
+      }
+      std::pair<float, glm::quat> last_rotation_frame = channel.rotation_frames.front();
+      for (const auto& rotation_frame : channel.rotation_frames) {
+          if (rotation_frame.first > current_time) {
+            const float t = (current_time - last_rotation_frame.first) / (rotation_frame.first - last_rotation_frame.first);
+            rotation = glm::mix(last_rotation_frame.second, rotation_frame.second, t);
+            break;
+          }
+          last_rotation_frame = rotation_frame;
+      }
+      std::pair<float, glm::vec3> last_scaling_frame = channel.scaling_frames.front();
+      for (const auto& scaling_frame : channel.scaling_frames) {
+          if (scaling_frame.first > current_time) {
+            const float t = (current_time - last_scaling_frame.first) / (scaling_frame.first - last_scaling_frame.first);
+            scaling = glm::mix(last_scaling_frame.second, scaling_frame.second, t);
+            break;
+          }
+          last_scaling_frame = scaling_frame;
+      }
+
+      nodes[channel.node_index].local_transform =
+        glm::translate(glm::toMat4(rotation) * glm::scale(glm::mat4(1.0f), scaling), position);
+    }
 
     for (auto& node : nodes) {
         if (node.parent_index != std::numeric_limits<size_t>::max()) {
@@ -28,6 +67,7 @@ void scene::update(float delta_time) {
     }
     for (size_t i = 0; i < meshes.size(); ++i) {
         mesh_data[i].localToWorldSpace = nodes[meshes[i].node_index].global_transform;
+        // mesh_data[i].localToWorldSpace = glm::mat4(1.0f);
     }
     std::memcpy(mesh_data_buffer->get_mapped_data(), mesh_data.data(), sizeof(glsl::MeshData) * meshes.size());
 }
@@ -45,21 +85,23 @@ void print_node(const aiScene* scene, aiNode* node, std::string prefix = "") {
     }
 }
 
-void insert_node(scene* scene, aiNode* assimp_node, size_t parent_index) {
+void insert_node(scene* scene, aiNode* assimp_node, size_t parent_index, std::map<std::string, std::size_t>* node_indices) {
   ::scene::node node {
       .global_transform = glm::mat4(1.0f),
       .parent_index = parent_index,
   };
   std::memcpy(glm::value_ptr(node.local_transform), &assimp_node->mTransformation.a1, sizeof(glm::mat4));
+  node.local_transform = glm::transpose(node.local_transform);
   const size_t node_index = scene->nodes.size();
   scene->nodes.push_back(node);
+  node_indices->insert(std::make_pair(assimp_node->mName.C_Str(), node_index));
   for (unsigned int i = 0; i < assimp_node->mNumMeshes; ++i) {
     const size_t mesh_index = assimp_node->mMeshes[i];
     scene->meshes[mesh_index].node_index = node_index;
   }
 
   for (unsigned int i = 0; i < assimp_node->mNumChildren; ++i) {
-    insert_node(scene, assimp_node->mChildren[i], node_index);
+    insert_node(scene, assimp_node->mChildren[i], node_index, node_indices);
   }
 }
 
@@ -103,7 +145,77 @@ scene::ptr load_scene(lava::device_ptr device, lava::name filename) {
         return {};
     }
     scene->meshes.resize(assimp_scene->mNumMeshes);
-    insert_node(scene.get(), assimp_scene->mRootNode, std::numeric_limits<size_t>::max());
+
+    std::map<std::string, size_t> node_indices;
+    insert_node(scene.get(), assimp_scene->mRootNode, std::numeric_limits<size_t>::max(), &node_indices);
+
+    for (unsigned int i = 0; i < assimp_scene->mNumCameras; ++i) {
+        const auto assimp_camera = assimp_scene->mCameras[i];
+
+        if (!node_indices.contains(assimp_camera->mName.C_Str())) {
+            log()->warn("Unknown node: {}", assimp_camera->mName.C_Str());
+            continue;
+        }
+
+        scene_camera camera;
+        camera.node_index = node_indices[assimp_camera->mName.C_Str()];
+        // camera.projection_matrix = ;
+        scene->cameras.push_back(std::move(camera));
+    }
+
+    log()->info("{} lights", assimp_scene->mNumLights);
+    log()->info("{} cameras", assimp_scene->mNumCameras);
+
+    auto assimp_vector3_to_glm = [](aiVector3D v) {
+        return glm::vec3(v.x, v.y, v.z);
+    };
+    auto assimp_quaternion_to_glm = [](aiQuaternion v) {
+        return glm::quat(v.w, v.x, v.y, v.z);
+    };
+
+    for (unsigned int i = 0; i < assimp_scene->mNumAnimations; ++i) {
+        const auto assimp_animation = assimp_scene->mAnimations[i];
+        scene_animation animation;
+
+        animation.duration = assimp_animation->mDuration;
+        animation.ticks_per_second = assimp_animation->mTicksPerSecond;
+
+        for (unsigned int j = 0; j < assimp_animation->mNumChannels; ++j) {
+            const auto assimp_channel = assimp_animation->mChannels[j];
+
+            scene_animation_channel channel;
+
+            if (!node_indices.contains(assimp_channel->mNodeName.C_Str())) {
+                log()->warn("Unknown node: {}", assimp_channel->mNodeName.C_Str());
+                continue;
+            }
+
+            channel.node_index = node_indices[assimp_channel->mNodeName.C_Str()];
+
+            channel.position_frames.reserve(assimp_channel->mNumPositionKeys);
+            for (unsigned int k = 0; k < assimp_channel->mNumPositionKeys; ++k) {
+                const auto key = assimp_channel->mPositionKeys[k];
+                channel.position_frames.push_back(std::make_pair(key.mTime, assimp_vector3_to_glm(key.mValue)));
+            }
+
+            channel.rotation_frames.reserve(assimp_channel->mNumRotationKeys);
+            for (unsigned int k = 0; k < assimp_channel->mNumRotationKeys; ++k) {
+                const auto key = assimp_channel->mRotationKeys[k];
+                channel.rotation_frames.push_back(std::make_pair(key.mTime, assimp_quaternion_to_glm(key.mValue)));
+            }
+            
+            channel.scaling_frames.reserve(assimp_channel->mNumScalingKeys);
+            for (unsigned int k = 0; k < assimp_channel->mNumScalingKeys; ++k) {
+                const auto key = assimp_channel->mScalingKeys[k];
+                channel.scaling_frames.push_back(std::make_pair(key.mTime, assimp_vector3_to_glm(key.mValue)));
+            }
+
+            animation.channels.push_back(std::move(channel));
+        }
+
+        scene->animations.push_back(std::move(animation));
+    }
+    scene->active_animation = 0;
 
 
     scene->mesh_data.resize(assimp_scene->mNumMeshes, glsl::MeshData { .localToWorldSpace = glm::mat4(1.0f) });
@@ -112,9 +224,6 @@ scene::ptr load_scene(lava::device_ptr device, lava::name filename) {
         throw std::runtime_error("Failed to create mesh data buffer");
     }
 
-    auto assimp_vector3_to_glm = [](aiVector3D v) {
-        return glm::vec3(v.x, v.y, v.z);
-    };
 
     std::vector<VkWriteDescriptorSet> descriptor_set_writes;
     for (unsigned int i = 0; i < assimp_scene->mNumMeshes; ++i) {
@@ -124,14 +233,11 @@ scene::ptr load_scene(lava::device_ptr device, lava::name filename) {
         data.vertices.resize(assimp_mesh->mNumVertices);
         for (unsigned int vertex_index = 0; vertex_index < assimp_mesh->mNumVertices; ++vertex_index) {
             data.vertices[vertex_index].position = assimp_vector3_to_glm(assimp_mesh->mVertices[vertex_index]);
-            data.vertices[vertex_index].position.y *= -1;
             if (assimp_mesh->HasNormals()) {
                 data.vertices[vertex_index].normal = assimp_vector3_to_glm(assimp_mesh->mNormals[vertex_index]);
-                data.vertices[vertex_index].normal.y *= -1;
             }
             if (assimp_mesh->HasTangentsAndBitangents()) {
                 data.vertices[vertex_index].color = glm::vec4(assimp_vector3_to_glm(assimp_mesh->mTangents[vertex_index]), 0.0f);
-                data.vertices[vertex_index].color.y *= -1;
             }
             if (assimp_mesh->HasTextureCoords(0)) {
                 data.vertices[vertex_index].uv = glm::vec2(assimp_vector3_to_glm(assimp_mesh->mTextureCoords[0][vertex_index]));
diff --git a/src/scene.hpp b/src/scene.hpp
index 51b9a597074eed0764a11cb5ec1bc8e38578a00e..7a90c6d6dadbd4bc05af28cb271548fa6dc99818 100644
--- a/src/scene.hpp
+++ b/src/scene.hpp
@@ -25,11 +25,23 @@ struct scene_mesh {
     size_t material_index;
 };
 
-struct animation_channel {
+struct scene_animation_channel {
+    size_t node_index;
+    std::vector<std::pair<float, glm::vec3>> position_frames;
+    std::vector<std::pair<float, glm::quat>> rotation_frames;
+    std::vector<std::pair<float, glm::vec3>> scaling_frames;
 };
 
 struct scene_animation {
+    float duration;
+    float ticks_per_second;
+    std::vector<scene_animation_channel> channels;
+};
 
+struct scene_camera {
+    size_t node_index;
+    glm::mat4 projection_matrix;
+    glm::mat4 view_matrix;
 };
 
 class scene {
@@ -62,6 +74,10 @@ public:
     lava::texture::ptr dummy_texture;
 
     float current_time = 0.0f;
+    std::vector<scene_animation> animations;
+    size_t active_animation;
+
+    std::vector<scene_camera> cameras;
 };
 
 scene::ptr load_scene(lava::device_ptr device, lava::name filename);
diff --git a/src/vr_app.cpp b/src/vr_app.cpp
index c4cd355d66bd92aa3d3794850471c4dd62f8dfd0..8d6b494a33a629b52a29343e16fb76263e56f7ef 100644
--- a/src/vr_app.cpp
+++ b/src/vr_app.cpp
@@ -86,8 +86,8 @@ vr_app::vr_app(name name, argh::parser cmd_line) {
             throw std::runtime_error("Failed to create vulkan device");
         }
     }
-    app_->camera.position = glm::vec3(0.0f, 0.0f, -0.5f);
-    app_->camera.movement_speed = 100.0f;
+    // app_->camera.position = glm::vec3(0.0f, 0.0f, -0.5f);
+    app_->camera.movement_speed = 10.0f;
 
     app_->on_process = [this](VkCommandBuffer command_buffer, lava::index frame) {
         VkImageSubresourceRange const image_range{
@@ -128,8 +128,10 @@ vr_app::vr_app(name name, argh::parser cmd_line) {
         app_->camera.update_view(dt, app_->input.get_mouse_position());
         scene()->update(dt);
 
+        const glm::mat4 scene_view_matrix = glm::inverse(scene()->nodes[scene()->cameras[0].node_index].global_transform);
+
         for (auto& framebuffer : framebuffers_) {
-            framebuffer->per_frame_data.view = framebuffer->per_frame_data.headToEye * camera().view;
+            framebuffer->per_frame_data.view = framebuffer->per_frame_data.headToEye * scene_view_matrix;
             framebuffer->per_frame_data.viewProjection = framebuffer->per_frame_data.projection * framebuffer->per_frame_data.view;
             std::memcpy(framebuffer->per_frame_ubo->get_mapped_data(), &framebuffer->per_frame_data, sizeof(framebuffer->per_frame_data));
         }
@@ -261,6 +263,8 @@ bool vr_app::setup() {
             ImGui::InputFloat("Z-far", &app_->camera.z_far)) {
           app_->camera.update_projection();
         }
+
+        ImGui::DragFloat("Scene Time", &scene()->current_time);
         
 
         std::array<const char*, 2> strategy_names = { "Naive Stereo", "Depth Peeling Reprojection" };
@@ -446,7 +450,7 @@ stereo_framebuffer::ptr vr_app::create_framebuffer(glm::uvec2 size, vr::EVREye e
       framebuffer->per_frame_data.projection = glm::transpose(framebuffer->per_frame_data.projection);
     } else {
       framebuffer->per_frame_data.projection = glm::perspective(M_PI / 2, 16.0/9.0, 0.1, 1000000.0);
-      framebuffer->per_frame_data.headToEye = glm::translate(glm::mat4(1.0f), glm::vec3(10.0f, 0.0, 0.0f) * (eye == vr::Eye_Left ? 1.0f : -1.0f));
+      framebuffer->per_frame_data.headToEye = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 0.0, 0.0f) * (eye == vr::Eye_Left ? 1.0f : -1.0f));
     }
     framebuffer->per_frame_data.view = glm::identity<glm::mat4>();