diff --git a/Cargo.lock b/Cargo.lock
index d461d0b..54175c9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2110,6 +2110,7 @@ dependencies = [
  "egui_winit_vulkano",
  "env_logger",
  "glam",
+ "image",
  "log",
  "thiserror 2.0.12",
  "vulkano",
diff --git a/Cargo.toml b/Cargo.toml
index 372b1a5..50be745 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,8 @@ vulkano-shaders = "0.35"
 vulkano-util = "0.35"
 egui_winit_vulkano = { version = "0.28" }
 
+image = { version = "0.25", features = ["png", "jpeg"] }
+
 # Math
 glam = { version = "0.30" }
 
diff --git a/res/shaders/vertex.frag b/res/shaders/vertex.frag
index 720d192..67831a9 100644
--- a/res/shaders/vertex.frag
+++ b/res/shaders/vertex.frag
@@ -1,9 +1,12 @@
 #version 450
 
-layout (location = 0) in vec3 color;
+layout (location = 0) in vec2 tex_coords;
 
 layout (location = 0) out vec4 f_color;
 
+layout(set = 1, binding = 0) uniform sampler mySampler;
+layout(set = 1, binding = 1) uniform texture2D myTexture;
+
 void main() {
-    f_color = vec4(color, 1.0);
-}
\ No newline at end of file
+    f_color = texture(sampler2D(myTexture, mySampler), tex_coords);
+}
diff --git a/res/shaders/vertex.vert b/res/shaders/vertex.vert
index bb1c261..0445e54 100644
--- a/res/shaders/vertex.vert
+++ b/res/shaders/vertex.vert
@@ -1,9 +1,9 @@
 #version 450
 
 layout (location = 0) in vec2 position;
-layout (location = 1) in vec3 color;
+layout (location = 1) in vec2 uv;
 
-layout (location = 0) out vec3 fragColor;
+layout (location = 0) out vec2 fragUv;
 
 layout (set = 0, binding = 0) uniform MVP {
     mat4 world;
@@ -14,5 +14,5 @@ layout (set = 0, binding = 0) uniform MVP {
 void main() {
     mat4 worldview = uniforms.view * uniforms.world;
     gl_Position = uniforms.projection * worldview * vec4(position, 0.0, 1.0);
-    fragColor = color;
+    fragUv = uv;
 }
diff --git a/res/textures/wooden-crate.jpg b/res/textures/wooden-crate.jpg
new file mode 100644
index 0000000..d1c8734
Binary files /dev/null and b/res/textures/wooden-crate.jpg differ
diff --git a/src/core/render/mod.rs b/src/core/render/mod.rs
index c550198..0a718e4 100644
--- a/src/core/render/mod.rs
+++ b/src/core/render/mod.rs
@@ -1,4 +1,5 @@
 pub mod pipelines;
 pub mod primitives;
 pub mod render_context;
+pub mod texture;
 pub mod vulkan_context;
diff --git a/src/core/render/pipelines/triangle_pipeline.rs b/src/core/render/pipelines/triangle_pipeline.rs
index b7ffce4..43d00d5 100644
--- a/src/core/render/pipelines/triangle_pipeline.rs
+++ b/src/core/render/pipelines/triangle_pipeline.rs
@@ -8,7 +8,7 @@ use vulkano::device::Device;
 use vulkano::format::Format;
 use vulkano::pipeline::graphics::GraphicsPipelineCreateInfo;
 use vulkano::pipeline::graphics::color_blend::{ColorBlendAttachmentState, ColorBlendState};
-use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
+use vulkano::pipeline::graphics::input_assembly::{InputAssemblyState, PrimitiveTopology};
 use vulkano::pipeline::graphics::multisample::MultisampleState;
 use vulkano::pipeline::graphics::rasterization::RasterizationState;
 use vulkano::pipeline::graphics::subpass::PipelineRenderingCreateInfo;
@@ -52,19 +52,42 @@ pub fn create_triangle_pipeline(
         PipelineShaderStageCreateInfo::new(fs),
     ];
 
-    let mut bindings = BTreeMap::<u32, DescriptorSetLayoutBinding>::new();
-    let mut descriptor_set_layout_binding =
-        DescriptorSetLayoutBinding::descriptor_type(DescriptorType::UniformBuffer);
-    descriptor_set_layout_binding.stages = ShaderStages::VERTEX;
-    bindings.insert(0, descriptor_set_layout_binding);
+    let vertex_bindings = BTreeMap::<u32, DescriptorSetLayoutBinding>::from_iter([(
+        0,
+        DescriptorSetLayoutBinding {
+            stages: ShaderStages::VERTEX,
+            ..DescriptorSetLayoutBinding::descriptor_type(DescriptorType::UniformBuffer)
+        },
+    )]);
+    let fragment_bindings = BTreeMap::<u32, DescriptorSetLayoutBinding>::from_iter([
+        (
+            0,
+            DescriptorSetLayoutBinding {
+                stages: ShaderStages::FRAGMENT,
+                ..DescriptorSetLayoutBinding::descriptor_type(DescriptorType::Sampler)
+            },
+        ),
+        (
+            1,
+            DescriptorSetLayoutBinding {
+                stages: ShaderStages::FRAGMENT,
+                ..DescriptorSetLayoutBinding::descriptor_type(DescriptorType::SampledImage)
+            },
+        ),
+    ]);
 
-    let descriptor_set_layout = DescriptorSetLayoutCreateInfo {
-        bindings,
+    let vertex_descriptor_set_layout = DescriptorSetLayoutCreateInfo {
+        bindings: vertex_bindings,
+        ..Default::default()
+    };
+
+    let fragment_descriptor_set_layout = DescriptorSetLayoutCreateInfo {
+        bindings: fragment_bindings,
         ..Default::default()
     };
 
     let create_info = PipelineDescriptorSetLayoutCreateInfo {
-        set_layouts: vec![descriptor_set_layout],
+        set_layouts: vec![vertex_descriptor_set_layout, fragment_descriptor_set_layout],
         flags: PipelineLayoutCreateFlags::default(),
         push_constant_ranges: vec![],
     }
@@ -83,7 +106,10 @@ pub fn create_triangle_pipeline(
         GraphicsPipelineCreateInfo {
             stages: stages.into_iter().collect(),
             vertex_input_state: Some(vertex_input_state),
-            input_assembly_state: Some(InputAssemblyState::default()),
+            input_assembly_state: Some(InputAssemblyState {
+                topology: PrimitiveTopology::TriangleStrip,
+                ..Default::default()
+            }),
             viewport_state: Some(ViewportState::default()),
             rasterization_state: Some(RasterizationState::default()),
             multisample_state: Some(MultisampleState::default()),
diff --git a/src/core/render/primitives/vertex.rs b/src/core/render/primitives/vertex.rs
index 9bf133e..b35588c 100644
--- a/src/core/render/primitives/vertex.rs
+++ b/src/core/render/primitives/vertex.rs
@@ -12,8 +12,8 @@ pub struct Vertex2D {
     #[format(R32G32_SFLOAT)]
     pub position: [f32; 2],
 
-    #[format(R32G32B32_SFLOAT)]
-    pub color: [f32; 3],
+    #[format(R32G32_SFLOAT)]
+    pub uv: [f32; 2],
 }
 
 impl Vertex2D {
diff --git a/src/core/render/texture.rs b/src/core/render/texture.rs
new file mode 100644
index 0000000..680ddbf
--- /dev/null
+++ b/src/core/render/texture.rs
@@ -0,0 +1,113 @@
+use std::{path::Path, sync::Arc};
+
+use anyhow::Error;
+use image::{DynamicImage, EncodableLayout};
+use vulkano::{
+    buffer::{Buffer, BufferCreateInfo, BufferUsage},
+    command_buffer::{AutoCommandBufferBuilder, CopyBufferToImageInfo, PrimaryAutoCommandBuffer},
+    device::Device,
+    format::Format,
+    image::{
+        Image, ImageCreateInfo, ImageType, ImageUsage,
+        sampler::{Filter, Sampler, SamplerAddressMode, SamplerCreateInfo},
+        view::ImageView,
+    },
+    memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator},
+};
+
+pub struct Texture {
+    texture: Arc<ImageView>,
+    sampler: Arc<Sampler>,
+}
+
+impl Texture {
+    fn new(texture: Arc<ImageView>, sampler: Arc<Sampler>) -> Self {
+        Self { texture, sampler }
+    }
+
+    pub fn from_file(
+        device: &Arc<Device>,
+        memory_allocator: &Arc<StandardMemoryAllocator>,
+        builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
+        path: &str,
+    ) -> Result<Self, Error> {
+        let image = image::open(path)?;
+        Self::from_dynamic_image(device, memory_allocator, builder, image)
+    }
+
+    pub fn from_bytes(
+        device: &Arc<Device>,
+        memory_allocator: &Arc<StandardMemoryAllocator>,
+        builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
+        bytes: &[u8],
+    ) -> Result<Self, Error> {
+        let image = image::load_from_memory(bytes)?;
+        Self::from_dynamic_image(device, memory_allocator, builder, image)
+    }
+
+    pub fn from_dynamic_image(
+        device: &Arc<Device>,
+        memory_allocator: &Arc<StandardMemoryAllocator>,
+        builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
+        image: DynamicImage,
+    ) -> Result<Self, Error> {
+        let image_data = image.to_rgba8();
+        let image_dimensions = image_data.dimensions();
+
+        let upload_buffer = Buffer::from_iter(
+            memory_allocator.clone(),
+            BufferCreateInfo {
+                usage: BufferUsage::TRANSFER_SRC,
+                ..Default::default()
+            },
+            AllocationCreateInfo {
+                memory_type_filter: MemoryTypeFilter::PREFER_HOST
+                    | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
+                ..Default::default()
+            },
+            image_data.to_vec(),
+        )?;
+
+        let image = Image::new(
+            memory_allocator.clone(),
+            ImageCreateInfo {
+                image_type: ImageType::Dim2d,
+                format: Format::R8G8B8A8_SRGB,
+                extent: [image_dimensions.0 as u32, image_dimensions.1 as u32, 1],
+                array_layers: 1,
+                usage: ImageUsage::TRANSFER_DST | ImageUsage::SAMPLED,
+                ..Default::default()
+            },
+            AllocationCreateInfo::default(),
+        )?;
+
+        builder.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(
+            upload_buffer,
+            image.clone(),
+        ))?;
+
+        let sampler = Sampler::new(
+            device.clone(),
+            SamplerCreateInfo {
+                mag_filter: Filter::Linear,
+                min_filter: Filter::Linear,
+                address_mode: [SamplerAddressMode::Repeat; 3],
+                ..Default::default()
+            },
+        )?;
+
+        let image_view = ImageView::new_default(image)?;
+
+        log::trace!("Texture loaded with dimensions {:?}", image_dimensions);
+
+        Ok(Self::new(image_view, sampler))
+    }
+
+    pub fn get_texture(&self) -> &Arc<ImageView> {
+        &self.texture
+    }
+
+    pub fn get_sampler(&self) -> &Arc<Sampler> {
+        &self.sampler
+    }
+}
diff --git a/src/game/main_scene.rs b/src/game/main_scene.rs
index 1f27f0b..c5a56a0 100644
--- a/src/game/main_scene.rs
+++ b/src/game/main_scene.rs
@@ -3,67 +3,35 @@ use crate::core::render::pipelines::triangle_pipeline::create_triangle_pipeline;
 use crate::core::render::primitives::camera::Camera;
 use crate::core::render::primitives::vertex::Vertex2D;
 use crate::core::render::render_context::RenderContext;
+use crate::core::render::texture::Texture;
 use crate::core::scene::Scene;
 use crate::core::timer::Timer;
 use glam::{Mat4, Quat, Vec3};
 use std::sync::Arc;
 use vulkano::buffer::Subbuffer;
-use vulkano::command_buffer::{AutoCommandBufferBuilder, PrimaryAutoCommandBuffer};
+use vulkano::command_buffer::{
+    AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer,
+    PrimaryCommandBufferAbstract,
+};
 use vulkano::descriptor_set::{DescriptorSet, WriteDescriptorSet};
 use vulkano::pipeline::{GraphicsPipeline, Pipeline, PipelineBindPoint};
 
-const VERTICES: [Vertex2D; 12] = [
-    // Triangle en haut à gauche
+const VERTICES: [Vertex2D; 4] = [
     Vertex2D {
-        position: [-0.5, -0.75],
-        color: [1.0, 0.0, 0.0],
+        position: [-0.5, -0.5],
+        uv: [0.0, 0.0],
     },
     Vertex2D {
-        position: [-0.75, -0.25],
-        color: [0.0, 1.0, 0.0],
+        position: [-0.5, 0.5],
+        uv: [0.0, 1.0],
     },
     Vertex2D {
-        position: [-0.25, -0.25],
-        color: [0.0, 0.0, 1.0],
-    },
-    // Triangle en bas à gauche
-    Vertex2D {
-        position: [-0.5, 0.25],
-        color: [0.5, 0.5, 0.5],
+        position: [0.5, -0.5],
+        uv: [1.0, 0.0],
     },
     Vertex2D {
-        position: [-0.75, 0.75],
-        color: [0.2, 0.8, 0.2],
-    },
-    Vertex2D {
-        position: [-0.25, 0.75],
-        color: [0.8, 0.2, 0.2],
-    },
-    // Triangle en haut à droite
-    Vertex2D {
-        position: [0.5, -0.75],
-        color: [1.0, 1.0, 0.0],
-    },
-    Vertex2D {
-        position: [0.25, -0.25],
-        color: [0.0, 1.0, 1.0],
-    },
-    Vertex2D {
-        position: [0.75, -0.25],
-        color: [1.0, 0.0, 1.0],
-    },
-    // Triangle en bas à droite
-    Vertex2D {
-        position: [0.5, 0.25],
-        color: [0.1, 0.5, 0.8],
-    },
-    Vertex2D {
-        position: [0.25, 0.75],
-        color: [0.8, 0.6, 0.1],
-    },
-    Vertex2D {
-        position: [0.75, 0.75],
-        color: [0.3, 0.4, 0.6],
+        position: [0.5, 0.5],
+        uv: [1.0, 1.0],
     },
 ];
 
@@ -71,6 +39,7 @@ pub struct MainSceneState {
     pipeline: Arc<GraphicsPipeline>,
     vertex_buffer: Subbuffer<[Vertex2D]>,
     camera: Camera,
+    texture: Texture,
 }
 
 #[derive(Default)]
@@ -105,11 +74,33 @@ impl Scene for MainScene {
             ),
         );
 
+        let mut uploads = AutoCommandBufferBuilder::primary(
+            render_context.command_buffer_allocator().clone(),
+            render_context.graphics_queue().queue_family_index(),
+            CommandBufferUsage::OneTimeSubmit,
+        )
+        .unwrap();
+
+        let texture = Texture::from_file(
+            render_context.device(),
+            render_context.memory_allocator(),
+            &mut uploads,
+            "res/textures/wooden-crate.jpg",
+        )
+        .unwrap();
+
+        let _ = uploads
+            .build()
+            .unwrap()
+            .execute(render_context.graphics_queue().clone())
+            .unwrap();
+
         self.state = Some(MainSceneState {
             pipeline,
             vertex_buffer,
             camera,
-        })
+            texture,
+        });
     }
 
     fn update(
@@ -157,21 +148,32 @@ impl Scene for MainScene {
     ) {
         let state = self.state.as_ref().unwrap();
         let vertex_count = state.vertex_buffer.len() as u32;
-        let instance_count = vertex_count / 3;
+        let instance_count = vertex_count / 4;
 
-        let layout = &state.pipeline.layout().set_layouts()[0];
+        let layouts = state.pipeline.layout().set_layouts();
         let uniform_buffer = state
             .camera
             .create_buffer(render_context.memory_allocator())
             .unwrap();
-        let descriptor_set = DescriptorSet::new(
+        let uniform_descriptor_set = DescriptorSet::new(
             render_context.descriptor_set_allocator().clone(),
-            layout.clone(),
+            layouts[0].clone(),
             [WriteDescriptorSet::buffer(0, uniform_buffer)],
             [],
         )
         .unwrap();
 
+        let texture_descriptor_set = DescriptorSet::new(
+            render_context.descriptor_set_allocator().clone(),
+            layouts[1].clone(),
+            [
+                WriteDescriptorSet::sampler(0, state.texture.get_sampler().clone()),
+                WriteDescriptorSet::image_view(1, state.texture.get_texture().clone()),
+            ],
+            [],
+        )
+        .unwrap();
+
         unsafe {
             builder
                 .bind_pipeline_graphics(state.pipeline.clone())
@@ -180,7 +182,7 @@ impl Scene for MainScene {
                     PipelineBindPoint::Graphics,
                     state.pipeline.layout().clone(),
                     0,
-                    descriptor_set,
+                    vec![uniform_descriptor_set, texture_descriptor_set],
                 )
                 .unwrap()
                 .bind_vertex_buffers(0, state.vertex_buffer.clone())
diff --git a/src/main.rs b/src/main.rs
index 6a4984b..7a4284c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,7 +21,7 @@ fn main() {
             vec![
                 VirtualBinding::Keyboard(PhysicalKey::Code(KeyCode::KeyW), AxisDirection::Normal),
                 VirtualBinding::Keyboard(PhysicalKey::Code(KeyCode::KeyS), AxisDirection::Invert),
-                VirtualBinding::Axis(0, AxisDirection::Normal, 1.0),
+                VirtualBinding::Axis(0, AxisDirection::Normal, 0.0),
             ],
         ),
         (
@@ -29,7 +29,7 @@ fn main() {
             vec![
                 VirtualBinding::Keyboard(PhysicalKey::Code(KeyCode::KeyD), AxisDirection::Normal),
                 VirtualBinding::Keyboard(PhysicalKey::Code(KeyCode::KeyA), AxisDirection::Invert),
-                VirtualBinding::Axis(1, AxisDirection::Normal, 1.0),
+                VirtualBinding::Axis(1, AxisDirection::Normal, 0.0),
             ],
         ),
         (