diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index f028ea001..9cbd7196c 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -40,6 +40,12 @@ RasterizerOpenGL::RasterizerOpenGL() : shader_dirty(true) {
         state.texture_units[i].sampler = texture_samplers[i].sampler.handle;
     }
 
+    // Create cubemap texture and sampler objects
+    texture_cube_sampler.Create();
+    state.texture_cube_unit.sampler = texture_cube_sampler.sampler.handle;
+    texture_cube.Create();
+    state.texture_cube_unit.texture_cube = texture_cube.handle;
+
     // Generate VBO, VAO and UBO
     vertex_buffer.Create();
     vertex_array.Create();
@@ -352,6 +358,25 @@ void RasterizerOpenGL::DrawTriangles() {
         const auto& texture = pica_textures[texture_index];
 
         if (texture.enabled) {
+            if (texture_index == 0) {
+                using TextureType = Pica::TexturingRegs::TextureConfig::TextureType;
+                switch (texture.config.type.Value()) {
+                case TextureType::TextureCube:
+                    using CubeFace = Pica::TexturingRegs::CubeFace;
+                    res_cache.FillTextureCube(
+                        texture_cube.handle, texture,
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::PositiveX),
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::NegativeX),
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::PositiveY),
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::NegativeY),
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::PositiveZ),
+                        regs.texturing.GetCubePhysicalAddress(CubeFace::NegativeZ));
+                    texture_cube_sampler.SyncWithConfig(texture.config);
+                    state.texture_units[texture_index].texture_2d = 0;
+                    continue; // Texture unit 0 setup finished. Continue to next unit
+                }
+            }
+
             texture_samplers[texture_index].SyncWithConfig(texture.config);
             Surface surface = res_cache.GetTextureSurface(texture);
             if (surface != nullptr) {
@@ -1225,6 +1250,10 @@ void RasterizerOpenGL::SetShader() {
         if (uniform_tex != -1) {
             glUniform1i(uniform_tex, TextureUnits::PicaTexture(2).id);
         }
+        uniform_tex = glGetUniformLocation(shader->shader.handle, "tex_cube");
+        if (uniform_tex != -1) {
+            glUniform1i(uniform_tex, TextureUnits::TextureCube.id);
+        }
 
         // Set the texture samplers to correspond to different lookup table texture units
         GLint uniform_lut = glGetUniformLocation(shader->shader.handle, "lighting_lut");
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h
index 18808b1e4..72af50f11 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer.h
@@ -287,6 +287,10 @@ private:
     OGLBuffer uniform_buffer;
     OGLFramebuffer framebuffer;
 
+    // TODO (wwylele): consider caching texture cube in the rasterizer cache
+    OGLTexture texture_cube;
+    SamplerInfo texture_cube_sampler;
+
     OGLBuffer lighting_lut_buffer;
     OGLTexture lighting_lut;
     std::array<std::array<GLvec2, 256>, Pica::LightingRegs::NumLightingSampler> lighting_lut_data{};
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
index aef06873d..81ba557d9 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
@@ -3,6 +3,7 @@
 // Refer to the license.txt file included.
 
 #include <algorithm>
+#include <array>
 #include <atomic>
 #include <cstring>
 #include <iterator>
@@ -1192,10 +1193,14 @@ SurfaceRect_Tuple RasterizerCacheOpenGL::GetSurfaceSubRect(const SurfaceParams&
 }
 
 Surface RasterizerCacheOpenGL::GetTextureSurface(
-    const Pica::TexturingRegs::FullTextureConfig& config) {
+    const Pica::TexturingRegs::FullTextureConfig& config, PAddr addr_override) {
     Pica::Texture::TextureInfo info =
         Pica::Texture::TextureInfo::FromPicaRegister(config.config, config.format);
 
+    if (addr_override != 0) {
+        info.physical_address = addr_override;
+    }
+
     SurfaceParams params;
     params.addr = info.physical_address;
     params.width = info.width;
@@ -1223,6 +1228,70 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(
     return GetSurface(params, ScaleMatch::Ignore, true);
 }
 
+void RasterizerCacheOpenGL::FillTextureCube(GLuint dest_handle,
+                                            const Pica::TexturingRegs::FullTextureConfig& config,
+                                            PAddr px, PAddr nx, PAddr py, PAddr ny, PAddr pz,
+                                            PAddr nz) {
+    ASSERT(config.config.width == config.config.height);
+    struct FaceTuple {
+        PAddr address;
+        GLenum gl_face;
+        Surface surface;
+    };
+    std::array<FaceTuple, 6> faces{{
+        {px, GL_TEXTURE_CUBE_MAP_POSITIVE_X},
+        {nx, GL_TEXTURE_CUBE_MAP_NEGATIVE_X},
+        {py, GL_TEXTURE_CUBE_MAP_POSITIVE_Y},
+        {ny, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y},
+        {pz, GL_TEXTURE_CUBE_MAP_POSITIVE_Z},
+        {nz, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z},
+    }};
+
+    u16 res_scale = 1;
+    for (auto& face : faces) {
+        face.surface = GetTextureSurface(config, face.address);
+        res_scale = std::max(res_scale, face.surface->res_scale);
+    }
+
+    u32 scaled_size = res_scale * config.config.width;
+
+    OpenGLState state = OpenGLState::GetCurState();
+
+    OpenGLState prev_state = state;
+    SCOPE_EXIT({ prev_state.Apply(); });
+
+    state.texture_cube_unit.texture_cube = dest_handle;
+    state.Apply();
+    glActiveTexture(TextureUnits::TextureCube.Enum());
+    FormatTuple format_tuple = GetFormatTuple(faces[0].surface->pixel_format);
+    for (auto& face : faces) {
+        glTexImage2D(face.gl_face, 0, format_tuple.internal_format, scaled_size, scaled_size, 0,
+                     format_tuple.format, format_tuple.type, nullptr);
+    }
+
+    state.draw.read_framebuffer = read_framebuffer.handle;
+    state.draw.draw_framebuffer = draw_framebuffer.handle;
+    state.ResetTexture(dest_handle);
+
+    for (auto& face : faces) {
+        state.ResetTexture(face.surface->texture.handle);
+        state.Apply();
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                               face.surface->texture.handle, 0);
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
+                               0);
+
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, face.gl_face, dest_handle,
+                               0);
+        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
+                               0);
+
+        auto src_rect = face.surface->GetScaledRect();
+        glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top, 0, 0,
+                          scaled_size, scaled_size, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+    }
+}
+
 SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces(
     bool using_color_fb, bool using_depth_fb, const MathUtil::Rectangle<s32>& viewport_rect) {
     const auto& regs = Pica::g_state.regs;
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
index 7ef6a9498..719f713a1 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h
@@ -322,7 +322,12 @@ public:
                                         bool load_if_create);
 
     /// Get a surface based on the texture configuration
-    Surface GetTextureSurface(const Pica::TexturingRegs::FullTextureConfig& config);
+    Surface GetTextureSurface(const Pica::TexturingRegs::FullTextureConfig& config,
+                              PAddr addr_override = 0);
+
+    /// Copy surfaces to a cubemap texture based on the texture configuration
+    void FillTextureCube(GLuint dest_handle, const Pica::TexturingRegs::FullTextureConfig& config,
+                         PAddr px, PAddr nx, PAddr py, PAddr ny, PAddr pz, PAddr nz);
 
     /// Get the color and depth surfaces based on the framebuffer configuration
     SurfaceSurfaceRect_Tuple GetFramebufferSurfaces(bool using_color_fb, bool using_depth_fb,
diff --git a/src/video_core/renderer_opengl/gl_shader_gen.cpp b/src/video_core/renderer_opengl/gl_shader_gen.cpp
index a9f7ecbfe..04ffa926b 100644
--- a/src/video_core/renderer_opengl/gl_shader_gen.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_gen.cpp
@@ -202,6 +202,8 @@ static std::string SampleTexture(const PicaShaderConfig& config, unsigned textur
             return "texture(tex[0], texcoord[0])";
         case TexturingRegs::TextureConfig::Projection2D:
             return "textureProj(tex[0], vec3(texcoord[0], texcoord0_w))";
+        case TexturingRegs::TextureConfig::TextureCube:
+            return "texture(tex_cube, vec3(texcoord[0], texcoord0_w))";
         default:
             LOG_CRITICAL(HW_GPU, "Unhandled texture type %x",
                          static_cast<int>(state.texture0_type));
@@ -1060,6 +1062,7 @@ in vec4 gl_FragCoord;
 out vec4 color;
 
 uniform sampler2D tex[3];
+uniform samplerCube tex_cube;
 uniform samplerBuffer lighting_lut;
 uniform samplerBuffer fog_lut;
 uniform samplerBuffer proctex_noise_lut;
diff --git a/src/video_core/renderer_opengl/gl_state.cpp b/src/video_core/renderer_opengl/gl_state.cpp
index 76354b842..efb302da2 100644
--- a/src/video_core/renderer_opengl/gl_state.cpp
+++ b/src/video_core/renderer_opengl/gl_state.cpp
@@ -52,6 +52,9 @@ OpenGLState::OpenGLState() {
         texture_unit.sampler = 0;
     }
 
+    texture_cube_unit.texture_cube = 0;
+    texture_cube_unit.sampler = 0;
+
     lighting_lut.texture_buffer = 0;
 
     fog_lut.texture_buffer = 0;
@@ -201,6 +204,14 @@ void OpenGLState::Apply() const {
         }
     }
 
+    if (texture_cube_unit.texture_cube != cur_state.texture_cube_unit.texture_cube) {
+        glActiveTexture(TextureUnits::TextureCube.Enum());
+        glBindTexture(GL_TEXTURE_CUBE_MAP, texture_cube_unit.texture_cube);
+    }
+    if (texture_cube_unit.sampler != cur_state.texture_cube_unit.sampler) {
+        glBindSampler(TextureUnits::TextureCube.id, texture_cube_unit.sampler);
+    }
+
     // Lighting LUTs
     if (lighting_lut.texture_buffer != cur_state.lighting_lut.texture_buffer) {
         glActiveTexture(TextureUnits::LightingLUT.Enum());
@@ -311,6 +322,8 @@ OpenGLState& OpenGLState::ResetTexture(GLuint handle) {
             unit.texture_2d = 0;
         }
     }
+    if (texture_cube_unit.texture_cube == handle)
+        texture_cube_unit.texture_cube = 0;
     if (lighting_lut.texture_buffer == handle)
         lighting_lut.texture_buffer = 0;
     if (fog_lut.texture_buffer == handle)
@@ -334,6 +347,9 @@ OpenGLState& OpenGLState::ResetSampler(GLuint handle) {
             unit.sampler = 0;
         }
     }
+    if (texture_cube_unit.sampler == handle) {
+        texture_cube_unit.sampler = 0;
+    }
     return *this;
 }
 
diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h
index 033d417bc..a8a2f1e7d 100644
--- a/src/video_core/renderer_opengl/gl_state.h
+++ b/src/video_core/renderer_opengl/gl_state.h
@@ -27,6 +27,7 @@ constexpr TextureUnit ProcTexColorMap{6};
 constexpr TextureUnit ProcTexAlphaMap{7};
 constexpr TextureUnit ProcTexLUT{8};
 constexpr TextureUnit ProcTexDiffLUT{9};
+constexpr TextureUnit TextureCube{10};
 
 } // namespace TextureUnits
 
@@ -87,6 +88,11 @@ public:
         GLuint sampler;    // GL_SAMPLER_BINDING
     } texture_units[3];
 
+    struct {
+        GLuint texture_cube; // GL_TEXTURE_BINDING_CUBE_MAP
+        GLuint sampler;      // GL_SAMPLER_BINDING
+    } texture_cube_unit;
+
     struct {
         GLuint texture_buffer; // GL_TEXTURE_BINDING_BUFFER
     } lighting_lut;