diff --git a/3d-viewer/3d_canvas/board_adapter.h b/3d-viewer/3d_canvas/board_adapter.h
index ceb9350c81..9816096b85 100644
--- a/3d-viewer/3d_canvas/board_adapter.h
+++ b/3d-viewer/3d_canvas/board_adapter.h
@@ -3,7 +3,7 @@
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
  * Copyright (C) 2023 CERN
- * Copyright (C) 1992-2023 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -32,7 +32,7 @@
 #include "../3d_rendering/raytracing/accelerators/container_3d.h"
 #include "../3d_rendering/raytracing/shapes3D/bbox_3d.h"
 #include <gal/3d/camera.h>
-#include "../3d_enums.h"
+#include <3d_enums.h>
 #include "../3d_cache/3d_cache.h"
 #include "../common_ogl/ogl_attr_list.h"
 #include "../3d_viewer/eda_3d_viewer_settings.h"
diff --git a/3d-viewer/3d_canvas/eda_3d_canvas.cpp b/3d-viewer/3d_canvas/eda_3d_canvas.cpp
index cbd252c4f0..69793878d0 100644
--- a/3d-viewer/3d_canvas/eda_3d_canvas.cpp
+++ b/3d-viewer/3d_canvas/eda_3d_canvas.cpp
@@ -29,7 +29,7 @@
 #include "../common_ogl/ogl_utils.h"
 #include "eda_3d_canvas.h"
 #include <eda_3d_viewer_frame.h>
-#include <3d_rendering/raytracing/render_3d_raytrace.h>
+#include <3d_rendering/raytracing/render_3d_raytrace_gl.h>
 #include <3d_rendering/opengl/render_3d_opengl.h>
 #include <3d_viewer_id.h>
 #include <advanced_config.h>
@@ -119,7 +119,7 @@ EDA_3D_CANVAS::EDA_3D_CANVAS( wxWindow* aParent, const wxGLAttributes& aGLAttrib
 
     m_is_currently_painting.clear();
 
-    m_3d_render_raytracing = new RENDER_3D_RAYTRACE( this, m_boardAdapter, m_camera );
+    m_3d_render_raytracing = new RENDER_3D_RAYTRACE_GL( this, m_boardAdapter, m_camera );
     m_3d_render_opengl = new RENDER_3D_OPENGL( this, m_boardAdapter, m_camera );
 
     wxASSERT( m_3d_render_raytracing != nullptr );
@@ -979,66 +979,24 @@ bool EDA_3D_CANVAS::SetView3D( VIEW3D_TYPE aRequestedView )
         return true;
 
     case VIEW3D_TYPE::VIEW3D_RIGHT:
-        m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
-        m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        m_camera.RotateZ_T1( glm::radians( -90.0f ) );
-        m_camera.RotateX_T1( glm::radians( -90.0f ) );
-        request_start_moving_camera();
-        return true;
-
     case VIEW3D_TYPE::VIEW3D_LEFT:
-        m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
-        m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        m_camera.RotateZ_T1( glm::radians(  90.0f ) );
-        m_camera.RotateX_T1( glm::radians( -90.0f ) );
-        request_start_moving_camera();
-        return true;
-
     case VIEW3D_TYPE::VIEW3D_FRONT:
-        m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
-        m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        m_camera.RotateX_T1( glm::radians( -90.0f ) );
-        request_start_moving_camera();
-        return true;
-
     case VIEW3D_TYPE::VIEW3D_BACK:
+    case VIEW3D_TYPE::VIEW3D_FLIP:
         m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
         m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        m_camera.RotateX_T1( glm::radians( -90.0f ) );
-
-        // The rotation angle should be 180.
-        // We use 179.999 (180 - epsilon) to avoid a full 360 deg rotation when
-        // using 180 deg if the previous rotated position was already 180 deg
-        m_camera.RotateZ_T1( glm::radians( 179.999f ) );
+        m_camera.ViewCommand_T1( aRequestedView );
         request_start_moving_camera();
         return true;
 
     case VIEW3D_TYPE::VIEW3D_TOP:
-        m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
-        m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        request_start_moving_camera( glm::min( glm::max( m_camera.GetZoom(), 0.5f ), 1.125f ) );
-        return true;
-
     case VIEW3D_TYPE::VIEW3D_BOTTOM:
         m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
         m_camera.SetT0_and_T1_current_T();
-        m_camera.Reset_T1();
-        m_camera.RotateY_T1( glm::radians( 179.999f ) );    // Rotation = 180 - epsilon
+        m_camera.ViewCommand_T1( aRequestedView );
         request_start_moving_camera( glm::min( glm::max( m_camera.GetZoom(), 0.5f ), 1.125f ) );
         return true;
 
-    case VIEW3D_TYPE::VIEW3D_FLIP:
-        m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::BEZIER );
-        m_camera.SetT0_and_T1_current_T();
-        m_camera.RotateY_T1( glm::radians( 179.999f ) );
-        request_start_moving_camera();
-        return true;
-
     default:
         return false;
     }
diff --git a/3d-viewer/3d_canvas/eda_3d_canvas.h b/3d-viewer/3d_canvas/eda_3d_canvas.h
index ed6fd21efe..6eef97c641 100644
--- a/3d-viewer/3d_canvas/eda_3d_canvas.h
+++ b/3d-viewer/3d_canvas/eda_3d_canvas.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -38,7 +38,7 @@
 class WX_INFOBAR;
 class wxStatusBar;
 class BOARD;
-class RENDER_3D_RAYTRACE;
+class RENDER_3D_RAYTRACE_GL;
 class RENDER_3D_OPENGL;
 
 
@@ -97,7 +97,7 @@ public:
     }
 
     /**
-     * @return the current render ( a RENDER_3D_RAYTRACE* or a RENDER_3D_OPENGL* render )
+     * @return the current render ( a RENDER_3D_RAYTRACE_GL* or a RENDER_3D_OPENGL* render )
      */
     RENDER_3D_BASE* GetCurrentRender() const { return m_3d_render; }
 
@@ -311,7 +311,7 @@ private:
 
     BOARD_ADAPTER&         m_boardAdapter;            // Pre-computed 3D info and settings
     RENDER_3D_BASE*        m_3d_render;
-    RENDER_3D_RAYTRACE*    m_3d_render_raytracing;
+    RENDER_3D_RAYTRACE_GL* m_3d_render_raytracing;
     RENDER_3D_OPENGL*      m_3d_render_opengl;
 
     bool                   m_opengl_supports_raytracing;
diff --git a/3d-viewer/3d_rendering/opengl/3d_model.h b/3d-viewer/3d_rendering/opengl/3d_model.h
index 1663e35ab5..275a0ec8cb 100644
--- a/3d-viewer/3d_rendering/opengl/3d_model.h
+++ b/3d-viewer/3d_rendering/opengl/3d_model.h
@@ -3,7 +3,7 @@
  *
  * Copyright (C) 2020 Oleg Endo <olegendo@gcc.gnu.org>
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2020 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -30,7 +30,7 @@
 #include <plugins/3dapi/c3dmodel.h>
 #include "../../common_ogl/openGL_includes.h"
 #include "../raytracing/shapes3D/bbox_3d.h"
-#include "../../3d_enums.h"
+#include <3d_enums.h>
 
 #include <wx/chartype.h>
 
diff --git a/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp b/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
index b05bbd34bb..a34574eb81 100644
--- a/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
+++ b/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
@@ -47,7 +47,8 @@
 
 RENDER_3D_OPENGL::RENDER_3D_OPENGL( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aAdapter,
                                     CAMERA& aCamera ) :
-        RENDER_3D_BASE( aCanvas, aAdapter, aCamera )
+        RENDER_3D_BASE( aAdapter, aCamera ),
+        m_canvas( aCanvas )
 {
     wxLogTrace( m_logTrace, wxT( "RENDER_3D_OPENGL::RENDER_3D_OPENGL" ) );
 
@@ -454,7 +455,7 @@ bool RENDER_3D_OPENGL::Redraw( bool aIsMoving, REPORTER* aStatusReporter,
                                REPORTER* aWarningReporter )
 {
     // Initialize OpenGL
-    if( !m_is_opengl_initialized )
+    if( !m_canvasInitialized )
     {
         if( !initializeOpenGL() )
             return false;
@@ -823,7 +824,7 @@ bool RENDER_3D_OPENGL::initializeOpenGL()
 
     // Use this mode if you want see the triangle lines (debug proposes)
     //glPolygonMode( GL_FRONT_AND_BACK,  GL_LINE );
-    m_is_opengl_initialized = true;
+    m_canvasInitialized = true;
 
     return true;
 }
@@ -1021,7 +1022,7 @@ void RENDER_3D_OPENGL::get3dModelsFromFootprint( std::list<MODELTORENDER> &aDstR
                     const SFVEC3F offset = SFVEC3F( sM.m_Offset.x, sM.m_Offset.y, sM.m_Offset.z );
                     const SFVEC3F rotation = SFVEC3F( sM.m_Rotation.x, sM.m_Rotation.y, sM.m_Rotation.z );
                     const SFVEC3F scale = SFVEC3F( sM.m_Scale.x, sM.m_Scale.y, sM.m_Scale.z );
-                    
+
                     std::vector<float> key = { offset.x, offset.y, offset.z,
                                                rotation.x, rotation.y, rotation.z,
                                                scale.x, scale.y, scale.z };
@@ -1134,7 +1135,7 @@ void RENDER_3D_OPENGL::renderTransparentModels( const glm::mat4 &aCameraViewMatr
         const SFVEC3F bBoxWorld = mtr.m_modelWorldMat * glm::vec4( bBoxCenter, 1.0f );
 
         const float distanceToCamera = glm::length( cameraPos - bBoxWorld );
-        
+
         transparentModelList.emplace_back( &mtr, distanceToCamera );
     }
 
@@ -1224,7 +1225,7 @@ void RENDER_3D_OPENGL::renderModel( const glm::mat4 &aCameraViewMatrix,
         aModelToRender.m_model->DrawBbox();
 
         glEnable( GL_LIGHTING );
-        
+
         if( !wasBlendEnabled )
             glDisable( GL_BLEND );
     }
diff --git a/3d-viewer/3d_rendering/opengl/render_3d_opengl.h b/3d-viewer/3d_rendering/opengl/render_3d_opengl.h
index c109d30e72..13ad861590 100644
--- a/3d-viewer/3d_rendering/opengl/render_3d_opengl.h
+++ b/3d-viewer/3d_rendering/opengl/render_3d_opengl.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2021 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -208,6 +208,8 @@ private:
         SMATERIAL m_GrayMaterial;
     } m_materials;
 
+    EDA_3D_CANVAS* m_canvas;
+
     MAP_OGL_DISP_LISTS  m_layers;
     OPENGL_RENDER_LIST* m_platedPadsFront;
     OPENGL_RENDER_LIST* m_platedPadsBack;
diff --git a/3d-viewer/3d_rendering/raytracing/create_scene.cpp b/3d-viewer/3d_rendering/raytracing/create_scene.cpp
index 9a7a6e3c1f..a2cbc78c56 100644
--- a/3d-viewer/3d_rendering/raytracing/create_scene.cpp
+++ b/3d-viewer/3d_rendering/raytracing/create_scene.cpp
@@ -23,7 +23,7 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
  */
 
-#include "render_3d_raytrace.h"
+#include "render_3d_raytrace_base.h"
 #include "shapes3D/plane_3d.h"
 #include "shapes3D/round_segment_3d.h"
 #include "shapes3D/layer_item_3d.h"
@@ -72,7 +72,7 @@ static float TransparencyControl( float aGrayColorValue, float aTransparency )
 #define UNITS3D_TO_UNITSPCB ( pcbIUScale.IU_PER_MM )
 
 
-void RENDER_3D_RAYTRACE::setupMaterials()
+void RENDER_3D_RAYTRACE_BASE::setupMaterials()
 {
     MATERIAL::SetDefaultRefractionRayCount( m_boardAdapter.m_Cfg->m_Render.raytrace_nrsamples_refractions );
     MATERIAL::SetDefaultReflectionRayCount( m_boardAdapter.m_Cfg->m_Render.raytrace_nrsamples_reflections );
@@ -176,7 +176,7 @@ void RENDER_3D_RAYTRACE::setupMaterials()
 }
 
 
-void RENDER_3D_RAYTRACE::createObject( CONTAINER_3D& aDstContainer, const OBJECT_2D* aObject2D,
+void RENDER_3D_RAYTRACE_BASE::createObject( CONTAINER_3D& aDstContainer, const OBJECT_2D* aObject2D,
                                        float aZMin, float aZMax, const MATERIAL* aMaterial,
                                        const SFVEC3F& aObjColor )
 {
@@ -227,7 +227,7 @@ void RENDER_3D_RAYTRACE::createObject( CONTAINER_3D& aDstContainer, const OBJECT
 }
 
 
-void RENDER_3D_RAYTRACE::createItemsFromContainer( const BVH_CONTAINER_2D* aContainer2d,
+void RENDER_3D_RAYTRACE_BASE::createItemsFromContainer( const BVH_CONTAINER_2D* aContainer2d,
                                                    PCB_LAYER_ID aLayer_id,
                                                    const MATERIAL* aMaterialLayer,
                                                    const SFVEC3F& aLayerColor,
@@ -358,7 +358,7 @@ void RENDER_3D_RAYTRACE::createItemsFromContainer( const BVH_CONTAINER_2D* aCont
 extern void buildBoardBoundingBoxPoly( const BOARD* aBoard, SHAPE_POLY_SET& aOutline );
 
 
-void RENDER_3D_RAYTRACE::Reload( REPORTER* aStatusReporter, REPORTER* aWarningReporter,
+void RENDER_3D_RAYTRACE_BASE::Reload( REPORTER* aStatusReporter, REPORTER* aWarningReporter,
                                  bool aOnlyLoadCopperAndShapes )
 {
     m_reloadRequested = false;
@@ -962,7 +962,7 @@ void RENDER_3D_RAYTRACE::Reload( REPORTER* aStatusReporter, REPORTER* aWarningRe
 }
 
 
-void RENDER_3D_RAYTRACE::insertHole( const PCB_VIA* aVia )
+void RENDER_3D_RAYTRACE_BASE::insertHole( const PCB_VIA* aVia )
 {
     PCB_LAYER_ID top_layer, bottom_layer;
     int          radiusBUI = ( aVia->GetDrillValue() / 2 );
@@ -993,7 +993,7 @@ void RENDER_3D_RAYTRACE::insertHole( const PCB_VIA* aVia )
 }
 
 
-void RENDER_3D_RAYTRACE::insertHole( const PAD* aPad )
+void RENDER_3D_RAYTRACE_BASE::insertHole( const PAD* aPad )
 {
     const OBJECT_2D* object2d_A = nullptr;
 
@@ -1164,7 +1164,7 @@ void RENDER_3D_RAYTRACE::insertHole( const PAD* aPad )
 }
 
 
-void RENDER_3D_RAYTRACE::addPadsAndVias()
+void RENDER_3D_RAYTRACE_BASE::addPadsAndVias()
 {
     if( !m_boardAdapter.GetBoard() )
         return;
@@ -1193,7 +1193,7 @@ void RENDER_3D_RAYTRACE::addPadsAndVias()
 }
 
 
-void RENDER_3D_RAYTRACE::load3DModels( CONTAINER_3D& aDstContainer, bool aSkipMaterialInformation )
+void RENDER_3D_RAYTRACE_BASE::load3DModels( CONTAINER_3D& aDstContainer, bool aSkipMaterialInformation )
 {
     if( !m_boardAdapter.GetBoard() )
         return;
@@ -1315,7 +1315,7 @@ void RENDER_3D_RAYTRACE::load3DModels( CONTAINER_3D& aDstContainer, bool aSkipMa
 }
 
 
-MODEL_MATERIALS* RENDER_3D_RAYTRACE::getModelMaterial( const S3DMODEL* a3DModel )
+MODEL_MATERIALS* RENDER_3D_RAYTRACE_BASE::getModelMaterial( const S3DMODEL* a3DModel )
 {
     MODEL_MATERIALS* materialVector;
 
@@ -1418,7 +1418,7 @@ MODEL_MATERIALS* RENDER_3D_RAYTRACE::getModelMaterial( const S3DMODEL* a3DModel
 }
 
 
-void RENDER_3D_RAYTRACE::addModels( CONTAINER_3D& aDstContainer, const S3DMODEL* a3DModel,
+void RENDER_3D_RAYTRACE_BASE::addModels( CONTAINER_3D& aDstContainer, const S3DMODEL* a3DModel,
                                     const glm::mat4& aModelMatrix, float aFPOpacity,
                                     bool aSkipMaterialInformation, BOARD_ITEM* aBoardItem )
 {
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace.cpp b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.cpp
similarity index 89%
rename from 3d-viewer/3d_rendering/raytracing/render_3d_raytrace.cpp
rename to 3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.cpp
index 8e516d323e..497bae8e15 100644
--- a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace.cpp
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.cpp
@@ -22,32 +22,28 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
  */
 
-#include <gal/opengl/kiglew.h>    // Must be included first
-
 #include <algorithm>
 #include <atomic>
 #include <chrono>
 #include <thread>
 
-#include "render_3d_raytrace.h"
+#include "render_3d_raytrace_base.h"
 #include "mortoncodes.h"
 #include "../color_rgba.h"
 #include "3d_fastmath.h"
 #include "3d_math.h"
-#include "../common_ogl/ogl_utils.h"
 #include <core/profile.h>        // To use GetRunningMicroSecs or another profiling utility
 #include <wx/log.h>
 
 
-RENDER_3D_RAYTRACE::RENDER_3D_RAYTRACE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aAdapter, CAMERA& aCamera ) :
-    RENDER_3D_BASE( aCanvas, aAdapter, aCamera ),
-    m_postShaderSsao( aCamera )
+RENDER_3D_RAYTRACE_BASE::RENDER_3D_RAYTRACE_BASE( BOARD_ADAPTER& aAdapter, CAMERA& aCamera ) :
+        RENDER_3D_BASE( aAdapter, aCamera ),
+        m_postShaderSsao( aCamera )
 {
-    wxLogTrace( m_logTrace, wxT( "RENDER_3D_RAYTRACE::RENDER_3D_RAYTRACE" ) );
+    wxLogTrace( m_logTrace, wxT( "RENDER_3D_RAYTRACE_BASE::RENDER_3D_RAYTRACE_BASE" ) );
 
-    m_openglSupportsVertexBufferObjects = false;
-    m_pboId       = GL_NONE;
-    m_pboDataSize = 0;
+    //m_pboId       = GL_NONE;
+    //m_pboDataSize = 0;
     m_accelerator = nullptr;
     m_convertedDummyBlockCount = 0;
     m_converted2dRoundSegmentCount = 0;
@@ -62,6 +58,7 @@ RENDER_3D_RAYTRACE::RENDER_3D_RAYTRACE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& a
     m_xoffset = 0;
     m_yoffset = 0;
 
+    m_is_canvas_initialized = false;
     m_isPreview = false;
     m_renderState = RT_RENDER_STATE_MAX; // Set to an initial invalid state
     m_renderStartTime = 0;
@@ -69,9 +66,9 @@ RENDER_3D_RAYTRACE::RENDER_3D_RAYTRACE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& a
 }
 
 
-RENDER_3D_RAYTRACE::~RENDER_3D_RAYTRACE()
+RENDER_3D_RAYTRACE_BASE::~RENDER_3D_RAYTRACE_BASE()
 {
-    wxLogTrace( m_logTrace, wxT( "RENDER_3D_RAYTRACE::~RENDER_3D_RAYTRACE" ) );
+    wxLogTrace( m_logTrace, wxT( "RENDER_3D_RAYTRACE_BASE::~RENDER_3D_RAYTRACE_BASE" ) );
 
     delete m_accelerator;
     m_accelerator = nullptr;
@@ -84,43 +81,16 @@ RENDER_3D_RAYTRACE::~RENDER_3D_RAYTRACE()
 
     delete[] m_shaderBuffer;
     m_shaderBuffer = nullptr;
-
-    deletePbo();
 }
 
 
-int RENDER_3D_RAYTRACE::GetWaitForEditingTimeOut()
+int RENDER_3D_RAYTRACE_BASE::GetWaitForEditingTimeOut()
 {
     return 200; // ms
 }
 
 
-void RENDER_3D_RAYTRACE::deletePbo()
-{
-    // Delete PBO if it was created
-    if( m_openglSupportsVertexBufferObjects )
-    {
-        if( glIsBufferARB( m_pboId ) )
-            glDeleteBuffers( 1, &m_pboId );
-
-        m_pboId = GL_NONE;
-    }
-}
-
-
-void RENDER_3D_RAYTRACE::SetCurWindowSize( const wxSize& aSize )
-{
-    if( m_windowSize != aSize )
-    {
-        m_windowSize = aSize;
-        glViewport( 0, 0, m_windowSize.x, m_windowSize.y );
-
-        initializeNewWindowSize();
-    }
-}
-
-
-void RENDER_3D_RAYTRACE::restartRenderState()
+void RENDER_3D_RAYTRACE_BASE::restartRenderState()
 {
     m_renderStartTime = GetRunningMicroSecs();
 
@@ -145,151 +115,13 @@ static inline void SetPixel( GLubyte* p, const COLOR_RGBA& v )
 }
 
 
-static inline SFVEC4F premultiplyAlpha( const SFVEC4F& aInput )
+SFVEC4F RENDER_3D_RAYTRACE_BASE::premultiplyAlpha( const SFVEC4F& aInput )
 {
     return SFVEC4F( aInput.r * aInput.a, aInput.g * aInput.a, aInput.b * aInput.a, aInput.a );
 }
 
 
-bool RENDER_3D_RAYTRACE::Redraw( bool aIsMoving, REPORTER* aStatusReporter,
-                                 REPORTER* aWarningReporter )
-{
-    bool requestRedraw = false;
-
-    // Initialize openGL if need
-    if( !m_is_opengl_initialized )
-    {
-        if( !initializeOpenGL() )
-            return false;
-
-        //aIsMoving = true;
-        requestRedraw = true;
-
-        // It will assign the first time the windows size, so it will now
-        // revert to preview mode the first time the Redraw is called
-        m_oldWindowsSize = m_windowSize;
-        initializeBlockPositions();
-    }
-
-    std::unique_ptr<BUSY_INDICATOR> busy = CreateBusyIndicator();
-
-    // Reload board if it was requested
-    if( m_reloadRequested )
-    {
-        if( aStatusReporter )
-            aStatusReporter->Report( _( "Loading..." ) );
-
-        //aIsMoving = true;
-        requestRedraw = true;
-        Reload( aStatusReporter, aWarningReporter, false );
-    }
-
-
-    // Recalculate constants if windows size was changed
-    if( m_windowSize != m_oldWindowsSize )
-    {
-        m_oldWindowsSize = m_windowSize;
-        aIsMoving = true;
-        requestRedraw = true;
-
-        initializeBlockPositions();
-    }
-
-
-    // Clear buffers
-    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
-    glClearDepth( 1.0f );
-    glClearStencil( 0x00 );
-    glClear( GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
-
-    // 4-byte pixel alignment
-    glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
-
-    glDisable( GL_STENCIL_TEST );
-    glDisable( GL_LIGHTING );
-    glDisable( GL_COLOR_MATERIAL );
-    glDisable( GL_DEPTH_TEST );
-    glDisable( GL_TEXTURE_2D );
-    glDisable( GL_BLEND );
-    glDisable( GL_MULTISAMPLE );
-
-    const bool was_camera_changed = m_camera.ParametersChanged();
-
-    if( requestRedraw || aIsMoving || was_camera_changed )
-        m_renderState = RT_RENDER_STATE_MAX; // Set to an invalid state,
-                                             // so it will restart again latter
-
-    // This will only render if need, otherwise it will redraw the PBO on the screen again
-    if( aIsMoving || was_camera_changed )
-    {
-        // Set head light (camera view light) with the opposite direction of the camera
-        if( m_cameraLight )
-            m_cameraLight->SetDirection( -m_camera.GetDir() );
-
-        OglDrawBackground( premultiplyAlpha( m_boardAdapter.m_BgColorTop ),
-                           premultiplyAlpha( m_boardAdapter.m_BgColorBot ) );
-
-        // Bind PBO
-        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
-
-        // Get the PBO pixel pointer to write the data
-        GLubyte* ptrPBO = (GLubyte *)glMapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB,
-                                                     GL_WRITE_ONLY_ARB );
-
-        if( ptrPBO )
-        {
-            renderPreview( ptrPBO );
-
-            // release pointer to mapping buffer, this initialize the coping to PBO
-            glUnmapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB );
-        }
-
-        glWindowPos2i( m_xoffset, m_yoffset );
-    }
-    else
-    {
-        // Bind PBO
-        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
-
-        if( m_renderState != RT_RENDER_STATE_FINISH )
-        {
-            // Get the PBO pixel pointer to write the data
-            GLubyte* ptrPBO = (GLubyte *)glMapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB,
-                                                         GL_WRITE_ONLY_ARB );
-
-            if( ptrPBO )
-            {
-                render( ptrPBO, aStatusReporter );
-
-                if( m_renderState != RT_RENDER_STATE_FINISH )
-                    requestRedraw = true;
-
-                // release pointer to mapping buffer, this initialize the coping to PBO
-                glUnmapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB );
-            }
-        }
-
-        if( m_renderState == RT_RENDER_STATE_FINISH )
-        {
-            glClear( GL_COLOR_BUFFER_BIT );
-        }
-
-        glWindowPos2i( m_xoffset, m_yoffset );
-    }
-
-    // This way it will blend the progress rendering with the last buffer. eg:
-    // if it was called after a openGL.
-    glEnable( GL_BLEND );
-    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
-    glEnable( GL_ALPHA_TEST );
-    glDrawPixels( m_realBufferSize.x, m_realBufferSize.y, GL_RGBA, GL_UNSIGNED_BYTE, 0 );
-    glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, 0 );
-
-    return requestRedraw;
-}
-
-
-void RENDER_3D_RAYTRACE::render( GLubyte* ptrPBO, REPORTER* aStatusReporter )
+void RENDER_3D_RAYTRACE_BASE::render( GLubyte* ptrPBO, REPORTER* aStatusReporter )
 {
     if( ( m_renderState == RT_RENDER_STATE_FINISH ) || ( m_renderState >= RT_RENDER_STATE_MAX ) )
     {
@@ -342,14 +174,14 @@ void RENDER_3D_RAYTRACE::render( GLubyte* ptrPBO, REPORTER* aStatusReporter )
     if( aStatusReporter && ( m_renderState == RT_RENDER_STATE_FINISH ) )
     {
         // Calculation time in seconds
-        const double elapsed_time = (double)( GetRunningMicroSecs() - m_renderStartTime ) / 1e6;
+        const double elapsed_time = (double) ( GetRunningMicroSecs() - m_renderStartTime ) / 1e6;
 
         aStatusReporter->Report( wxString::Format( _( "Rendering time %.3f s" ), elapsed_time ) );
     }
 }
 
 
-void RENDER_3D_RAYTRACE::renderTracing( GLubyte* ptrPBO, REPORTER* aStatusReporter )
+void RENDER_3D_RAYTRACE_BASE::renderTracing( GLubyte* ptrPBO, REPORTER* aStatusReporter )
 {
     m_isPreview = false;
 
@@ -364,6 +196,8 @@ void RENDER_3D_RAYTRACE::renderTracing( GLubyte* ptrPBO, REPORTER* aStatusReport
             std::max<size_t>( std::thread::hardware_concurrency(), 2 ),
             m_blockPositions.size() );
 
+    const int timeLimit = m_blockPositions.size() > 40000 ? 500 : 200;
+
     for( size_t ii = 0; ii < parallelThreadCount; ++ii )
     {
         std::thread t = std::thread( [&]()
@@ -380,8 +214,10 @@ void RENDER_3D_RAYTRACE::renderTracing( GLubyte* ptrPBO, REPORTER* aStatusReport
 
                     // Check if it spend already some time render and request to exit
                     // to display the progress
-                    if( std::chrono::duration_cast<std::chrono::milliseconds>(
-                            std::chrono::steady_clock::now() - startTime ).count() > 150 )
+                    auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(
+                            std::chrono::steady_clock::now() - startTime );
+
+                    if( diff.count() > timeLimit )
                         breakLoop = true;
                 }
             }
@@ -459,7 +295,7 @@ SFVEC4F ConvertSRGBAToLinear( const SFVEC4F& aSRGBAcolor )
 #endif
 
 
-void RENDER_3D_RAYTRACE::renderFinalColor( GLubyte* ptrPBO, const SFVEC4F& rgbColor,
+void RENDER_3D_RAYTRACE_BASE::renderFinalColor( GLubyte* ptrPBO, const SFVEC4F& rgbColor,
                                          bool applyColorSpaceConversion )
 {
     SFVEC4F color = rgbColor;
@@ -494,7 +330,7 @@ static void HITINFO_PACKET_init( HITINFO_PACKET* aHitPacket )
 }
 
 
-void RENDER_3D_RAYTRACE::renderRayPackets( const SFVEC4F* bgColorY, const RAY* aRayPkt,
+void RENDER_3D_RAYTRACE_BASE::renderRayPackets( const SFVEC4F* bgColorY, const RAY* aRayPkt,
                                            HITINFO_PACKET* aHitPacket, bool is_testShadow,
                                            SFVEC4F* aOutHitColor )
 {
@@ -516,7 +352,7 @@ void RENDER_3D_RAYTRACE::renderRayPackets( const SFVEC4F* bgColorY, const RAY* a
 }
 
 
-void RENDER_3D_RAYTRACE::renderAntiAliasPackets( const SFVEC4F* aBgColorY,
+void RENDER_3D_RAYTRACE_BASE::renderAntiAliasPackets( const SFVEC4F* aBgColorY,
                                                  const HITINFO_PACKET* aHitPck_X0Y0,
                                                  const HITINFO_PACKET* aHitPck_AA_X1Y1,
                                                  const RAY* aRayPck, SFVEC4F* aOutHitColor )
@@ -640,7 +476,7 @@ void RENDER_3D_RAYTRACE::renderAntiAliasPackets( const SFVEC4F* aBgColorY,
 #define DISP_FACTOR 0.075f
 
 
-void RENDER_3D_RAYTRACE::renderBlockTracing( GLubyte* ptrPBO, signed int iBlock )
+void RENDER_3D_RAYTRACE_BASE::renderBlockTracing( GLubyte* ptrPBO, signed int iBlock )
 {
     // Initialize ray packets
     const SFVEC2UI& blockPos = m_blockPositions[iBlock];
@@ -851,7 +687,7 @@ void RENDER_3D_RAYTRACE::renderBlockTracing( GLubyte* ptrPBO, signed int iBlock
 }
 
 
-void RENDER_3D_RAYTRACE::postProcessShading( GLubyte* /* ptrPBO */, REPORTER* aStatusReporter )
+void RENDER_3D_RAYTRACE_BASE::postProcessShading( GLubyte* /* ptrPBO */, REPORTER* aStatusReporter )
 {
     if( m_boardAdapter.m_Cfg->m_Render.raytrace_post_processing )
     {
@@ -903,7 +739,7 @@ void RENDER_3D_RAYTRACE::postProcessShading( GLubyte* /* ptrPBO */, REPORTER* aS
 }
 
 
-void RENDER_3D_RAYTRACE::postProcessBlurFinish( GLubyte* ptrPBO, REPORTER* /* aStatusReporter */ )
+void RENDER_3D_RAYTRACE_BASE::postProcessBlurFinish( GLubyte* ptrPBO, REPORTER* /* aStatusReporter */ )
 {
     if( m_boardAdapter.m_Cfg->m_Render.raytrace_post_processing )
     {
@@ -960,7 +796,7 @@ void RENDER_3D_RAYTRACE::postProcessBlurFinish( GLubyte* ptrPBO, REPORTER* /* aS
 }
 
 
-void RENDER_3D_RAYTRACE::renderPreview( GLubyte* ptrPBO )
+void RENDER_3D_RAYTRACE_BASE::renderPreview( GLubyte* ptrPBO )
 {
     m_isPreview = true;
 
@@ -1558,7 +1394,7 @@ void RENDER_3D_RAYTRACE::renderPreview( GLubyte* ptrPBO )
 
 #define USE_EXPERIMENTAL_SOFT_SHADOWS 1
 
-SFVEC4F RENDER_3D_RAYTRACE::shadeHit( const SFVEC4F& aBgColor, const RAY& aRay, HITINFO& aHitInfo,
+SFVEC4F RENDER_3D_RAYTRACE_BASE::shadeHit( const SFVEC4F& aBgColor, const RAY& aRay, HITINFO& aHitInfo,
                                       bool aIsInsideObject, unsigned int aRecursiveLevel,
                                       bool is_testShadow ) const
 {
@@ -1734,6 +1570,10 @@ SFVEC4F RENDER_3D_RAYTRACE::shadeHit( const SFVEC4F& aBgColor, const RAY& aRay,
 
                     sum_color += add;
                 }
+                else
+                {
+                    sum_color += aBgColor;
+                }
             }
 
             outColor += (sum_color / SFVEC4F( (float)reflection_number_of_samples) );
@@ -1825,49 +1665,6 @@ SFVEC4F RENDER_3D_RAYTRACE::shadeHit( const SFVEC4F& aBgColor, const RAY& aRay,
 }
 
 
-void RENDER_3D_RAYTRACE::initializeNewWindowSize()
-{
-    initPbo();
-}
-
-
-void RENDER_3D_RAYTRACE::initPbo()
-{
-    if( GLEW_ARB_pixel_buffer_object )
-    {
-        m_openglSupportsVertexBufferObjects = true;
-
-        // Try to delete vbo if it was already initialized
-        deletePbo();
-
-        // Learn about Pixel buffer objects at:
-        // http://www.songho.ca/opengl/gl_pbo.html
-        // http://web.eecs.umich.edu/~sugih/courses/eecs487/lectures/25-PBO+Mipmapping.pdf
-        // "create 2 pixel buffer objects, you need to delete them when program exits.
-        // glBufferDataARB with NULL pointer reserves only memory space."
-
-        // This sets the number of RGBA pixels
-        m_pboDataSize =  m_realBufferSize.x * m_realBufferSize.y * 4;
-
-        glGenBuffersARB( 1, &m_pboId );
-        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
-        glBufferDataARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboDataSize, 0, GL_STREAM_DRAW_ARB );
-        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, 0 );
-
-        wxLogTrace( m_logTrace,
-                    wxT( "RENDER_3D_RAYTRACE:: GLEW_ARB_pixel_buffer_object is supported" ) );
-    }
-}
-
-
-bool RENDER_3D_RAYTRACE::initializeOpenGL()
-{
-    m_is_opengl_initialized = true;
-
-    return true;
-}
-
-
 static float distance( const SFVEC2UI& a, const SFVEC2UI& b )
 {
     const float dx = (float) a.x - (float) b.x;
@@ -1876,7 +1673,7 @@ static float distance( const SFVEC2UI& a, const SFVEC2UI& b )
 }
 
 
-void RENDER_3D_RAYTRACE::initializeBlockPositions()
+void RENDER_3D_RAYTRACE_BASE::initializeBlockPositions()
 {
     m_realBufferSize = SFVEC2UI( 0 );
 
@@ -1959,7 +1756,7 @@ void RENDER_3D_RAYTRACE::initializeBlockPositions()
 }
 
 
-BOARD_ITEM* RENDER_3D_RAYTRACE::IntersectBoardItem( const RAY& aRay )
+BOARD_ITEM* RENDER_3D_RAYTRACE_BASE::IntersectBoardItem( const RAY& aRay )
 {
     HITINFO hitInfo;
     hitInfo.m_tHit = std::numeric_limits<float>::infinity();
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace.h b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.h
similarity index 91%
rename from 3d-viewer/3d_rendering/raytracing/render_3d_raytrace.h
rename to 3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.h
index 92eeac2309..7c4a424ac9 100644
--- a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace.h
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.h
@@ -22,10 +22,9 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
  */
 
-#ifndef RENDER_3D_RAYTRACE_H
-#define RENDER_3D_RAYTRACE_H
+#ifndef RENDER_3D_RAYTRACE_BASE_H
+#define RENDER_3D_RAYTRACE_BASE_H
 
-#include "../../common_ogl/openGL_includes.h"
 #include "accelerators/container_3d.h"
 #include "accelerators/accelerator_3d.h"
 #include "../render_3d_base.h"
@@ -52,19 +51,16 @@ typedef enum
 } RT_RENDER_STATE;
 
 
-class RENDER_3D_RAYTRACE : public RENDER_3D_BASE
+class RENDER_3D_RAYTRACE_BASE : public RENDER_3D_BASE
 {
 public:
     // TODO: Take into account board thickness so that the camera won't move inside of the board
     // when facing it perpendicularly.
     static constexpr float MIN_DISTANCE_IU = 4 * PCB_IU_PER_MM;
 
-    explicit RENDER_3D_RAYTRACE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aAdapter, CAMERA& aCamera );
+    explicit RENDER_3D_RAYTRACE_BASE( BOARD_ADAPTER& aAdapter, CAMERA& aCamera );
 
-    ~RENDER_3D_RAYTRACE();
-
-    void SetCurWindowSize( const wxSize& aSize ) override;
-    bool Redraw( bool aIsMoving, REPORTER* aStatusReporter, REPORTER* aWarningReporter ) override;
+    ~RENDER_3D_RAYTRACE_BASE();
 
     int GetWaitForEditingTimeOut() override;
 
@@ -73,11 +69,9 @@ public:
 
     BOARD_ITEM *IntersectBoardItem( const RAY& aRay );
 
-private:
-    bool initializeOpenGL();
-    void initializeNewWindowSize();
-    void initPbo();
-    void deletePbo();
+protected:
+    virtual void initPbo() = 0;
+    virtual void deletePbo() = 0;
     void createItemsFromContainer( const BVH_CONTAINER_2D* aContainer2d, PCB_LAYER_ID aLayer_id,
                                    const MATERIAL* aMaterialLayer, const SFVEC3F& aLayerColor,
                                    float aLayerZOffset );
@@ -128,6 +122,8 @@ private:
     void render( GLubyte* ptrPBO, REPORTER* aStatusReporter );
     void renderPreview( GLubyte* ptrPBO );
 
+    static SFVEC4F premultiplyAlpha( const SFVEC4F& aInput );
+
     struct
     {
         BLINN_PHONG_MATERIAL m_Paste;
@@ -148,6 +144,7 @@ private:
     BRUSHED_METAL_NORMAL m_brushedMetalMaterial;
     SILK_SCREEN_NORMAL   m_silkScreenMaterial;
 
+    bool m_is_canvas_initialized;
     bool m_isPreview;
 
     /// State used on quality render
@@ -165,10 +162,8 @@ private:
 
     DIRECTIONAL_LIGHT* m_cameraLight;
 
-    bool m_openglSupportsVertexBufferObjects;
-
-    GLuint m_pboId;
-    GLuint m_pboDataSize;
+    /*GLuint m_pboId;
+    GLuint m_pboDataSize;*/
 
     CONTAINER_3D m_objectContainer;
 
@@ -224,4 +219,4 @@ extern SFVEC4F ConvertSRGBAToLinear( const SFVEC4F& aSRGBAcolor );
 #define ConvertSRGBAToLinear( v ) ( v )
 #endif
 
-#endif // RENDER_3D_RAYTRACE_H
+#endif // RENDER_3D_RAYTRACE_BASE_H
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.cpp b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.cpp
new file mode 100644
index 0000000000..883f6d0551
--- /dev/null
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.cpp
@@ -0,0 +1,243 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2015-2020 Mario Luzeiro <mrluzeiro@ua.pt>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <gal/opengl/kiglew.h>    // Must be included first
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <thread>
+
+#include "render_3d_raytrace_gl.h"
+#include "../common_ogl/ogl_utils.h"
+#include <core/profile.h>        // To use GetRunningMicroSecs or another profiling utility
+#include <wx/log.h>
+
+
+RENDER_3D_RAYTRACE_GL::RENDER_3D_RAYTRACE_GL( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aAdapter, CAMERA& aCamera ) :
+    RENDER_3D_RAYTRACE_BASE( aAdapter, aCamera )
+{
+    wxLogTrace( m_logTrace, wxT( "RENDER_3D_RAYTRACE_GL::RENDER_3D_RAYTRACE_GL" ) );
+
+    m_openglSupportsVertexBufferObjects = false;
+    m_pboId       = GL_NONE;
+    m_pboDataSize = 0;
+}
+
+
+RENDER_3D_RAYTRACE_GL::~RENDER_3D_RAYTRACE_GL()
+{
+    deletePbo();
+}
+
+
+void RENDER_3D_RAYTRACE_GL::deletePbo()
+{
+    // Delete PBO if it was created
+    if( m_openglSupportsVertexBufferObjects )
+    {
+        if( glIsBufferARB( m_pboId ) )
+            glDeleteBuffers( 1, &m_pboId );
+
+        m_pboId = GL_NONE;
+    }
+}
+
+
+void RENDER_3D_RAYTRACE_GL::SetCurWindowSize( const wxSize& aSize )
+{
+    if( m_windowSize != aSize )
+    {
+        m_windowSize = aSize;
+        glViewport( 0, 0, m_windowSize.x, m_windowSize.y );
+
+        initPbo();
+    }
+}
+
+
+bool RENDER_3D_RAYTRACE_GL::Redraw( bool aIsMoving, REPORTER* aStatusReporter,
+                                 REPORTER* aWarningReporter )
+{
+    bool requestRedraw = false;
+
+    // Initialize openGL if need
+    if( !m_canvasInitialized )
+    {
+        m_canvasInitialized = true;
+
+        //aIsMoving = true;
+        requestRedraw = true;
+
+        // It will assign the first time the windows size, so it will now
+        // revert to preview mode the first time the Redraw is called
+        m_oldWindowsSize = m_windowSize;
+        initializeBlockPositions();
+    }
+
+    std::unique_ptr<BUSY_INDICATOR> busy = CreateBusyIndicator();
+
+    // Reload board if it was requested
+    if( m_reloadRequested )
+    {
+        if( aStatusReporter )
+            aStatusReporter->Report( _( "Loading..." ) );
+
+        //aIsMoving = true;
+        requestRedraw = true;
+        Reload( aStatusReporter, aWarningReporter, false );
+    }
+
+
+    // Recalculate constants if windows size was changed
+    if( m_windowSize != m_oldWindowsSize )
+    {
+        m_oldWindowsSize = m_windowSize;
+        aIsMoving = true;
+        requestRedraw = true;
+
+        initializeBlockPositions();
+    }
+
+    // Clear buffers
+    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
+    glClearDepth( 1.0f );
+    glClearStencil( 0x00 );
+    glClear( GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
+
+    // 4-byte pixel alignment
+    glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
+
+    glDisable( GL_STENCIL_TEST );
+    glDisable( GL_LIGHTING );
+    glDisable( GL_COLOR_MATERIAL );
+    glDisable( GL_DEPTH_TEST );
+    glDisable( GL_TEXTURE_2D );
+    glDisable( GL_BLEND );
+    glDisable( GL_MULTISAMPLE );
+
+    const bool was_camera_changed = m_camera.ParametersChanged();
+
+    if( requestRedraw || aIsMoving || was_camera_changed )
+        m_renderState = RT_RENDER_STATE_MAX; // Set to an invalid state,
+                                             // so it will restart again latter
+
+    // This will only render if need, otherwise it will redraw the PBO on the screen again
+    if( aIsMoving || was_camera_changed )
+    {
+        // Set head light (camera view light) with the opposite direction of the camera
+        if( m_cameraLight )
+            m_cameraLight->SetDirection( -m_camera.GetDir() );
+
+        OglDrawBackground( premultiplyAlpha( m_boardAdapter.m_BgColorTop ),
+                           premultiplyAlpha( m_boardAdapter.m_BgColorBot ) );
+
+        // Bind PBO
+        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
+
+        // Get the PBO pixel pointer to write the data
+        GLubyte* ptrPBO = (GLubyte *)glMapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB,
+                                                     GL_WRITE_ONLY_ARB );
+
+        if( ptrPBO )
+        {
+            renderPreview( ptrPBO );
+
+            // release pointer to mapping buffer, this initialize the coping to PBO
+            glUnmapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB );
+        }
+
+        glWindowPos2i( m_xoffset, m_yoffset );
+    }
+    else
+    {
+        // Bind PBO
+        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
+
+        if( m_renderState != RT_RENDER_STATE_FINISH )
+        {
+            // Get the PBO pixel pointer to write the data
+            GLubyte* ptrPBO = (GLubyte *)glMapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB,
+                                                         GL_WRITE_ONLY_ARB );
+
+            if( ptrPBO )
+            {
+                render( ptrPBO, aStatusReporter );
+
+                if( m_renderState != RT_RENDER_STATE_FINISH )
+                    requestRedraw = true;
+
+                // release pointer to mapping buffer, this initialize the coping to PBO
+                glUnmapBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB );
+            }
+        }
+
+        if( m_renderState == RT_RENDER_STATE_FINISH )
+        {
+            glClear( GL_COLOR_BUFFER_BIT );
+        }
+
+        glWindowPos2i( m_xoffset, m_yoffset );
+    }
+
+    // This way it will blend the progress rendering with the last buffer. eg:
+    // if it was called after a openGL.
+    glEnable( GL_BLEND );
+    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
+    glEnable( GL_ALPHA_TEST );
+    glDrawPixels( m_realBufferSize.x, m_realBufferSize.y, GL_RGBA, GL_UNSIGNED_BYTE, 0 );
+    glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, 0 );
+
+    return requestRedraw;
+}
+
+
+void RENDER_3D_RAYTRACE_GL::initPbo()
+{
+    if( GLEW_ARB_pixel_buffer_object )
+    {
+        m_openglSupportsVertexBufferObjects = true;
+
+        // Try to delete vbo if it was already initialized
+        deletePbo();
+
+        // Learn about Pixel buffer objects at:
+        // http://www.songho.ca/opengl/gl_pbo.html
+        // http://web.eecs.umich.edu/~sugih/courses/eecs487/lectures/25-PBO+Mipmapping.pdf
+        // "create 2 pixel buffer objects, you need to delete them when program exits.
+        // glBufferDataARB with NULL pointer reserves only memory space."
+
+        // This sets the number of RGBA pixels
+        m_pboDataSize =  m_realBufferSize.x * m_realBufferSize.y * 4;
+
+        glGenBuffersARB( 1, &m_pboId );
+        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboId );
+        glBufferDataARB( GL_PIXEL_UNPACK_BUFFER_ARB, m_pboDataSize, 0, GL_STREAM_DRAW_ARB );
+        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_ARB, 0 );
+
+        wxLogTrace( m_logTrace,
+                    wxT( "RENDER_3D_RAYTRACE_GL:: GLEW_ARB_pixel_buffer_object is supported" ) );
+    }
+}
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.h b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.h
new file mode 100644
index 0000000000..90f59c8047
--- /dev/null
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_gl.h
@@ -0,0 +1,54 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2015-2020 Mario Luzeiro <mrluzeiro@ua.pt>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef RENDER_3D_RAYTRACE_GL_H
+#define RENDER_3D_RAYTRACE_GL_H
+
+#include "../../common_ogl/openGL_includes.h"
+#include "render_3d_raytrace_base.h"
+
+
+class RENDER_3D_RAYTRACE_GL : public RENDER_3D_RAYTRACE_BASE
+{
+public:
+    explicit RENDER_3D_RAYTRACE_GL( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aAdapter,
+                                    CAMERA& aCamera );
+
+    ~RENDER_3D_RAYTRACE_GL();
+
+    void SetCurWindowSize( const wxSize& aSize ) override;
+    bool Redraw( bool aIsMoving, REPORTER* aStatusReporter, REPORTER* aWarningReporter ) override;
+
+protected:
+    void initPbo() override;
+    void deletePbo() override;
+
+    bool   m_openglSupportsVertexBufferObjects;
+    GLuint m_pboId;
+    GLuint m_pboDataSize;
+};
+
+
+#endif // RENDER_3D_RAYTRACE_GL_H
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.cpp b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.cpp
new file mode 100644
index 0000000000..80ef2a3cac
--- /dev/null
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.cpp
@@ -0,0 +1,159 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2015-2020 Mario Luzeiro <mrluzeiro@ua.pt>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include "render_3d_raytrace_ram.h"
+#include <wx/log.h>
+
+
+RENDER_3D_RAYTRACE_RAM::RENDER_3D_RAYTRACE_RAM( BOARD_ADAPTER& aAdapter, CAMERA& aCamera ) :
+        RENDER_3D_RAYTRACE_BASE( aAdapter, aCamera ),
+    m_outputBuffer( nullptr ),
+    m_pboDataSize( 0 )
+{
+}
+
+
+RENDER_3D_RAYTRACE_RAM::~RENDER_3D_RAYTRACE_RAM()
+{
+    deletePbo();
+}
+
+
+GLubyte* RENDER_3D_RAYTRACE_RAM::GetBuffer()
+{
+    return m_outputBuffer;
+}
+
+
+wxSize RENDER_3D_RAYTRACE_RAM::GetRealBufferSize()
+{
+    return wxSize( m_realBufferSize.x, m_realBufferSize.y );
+}
+
+
+void RENDER_3D_RAYTRACE_RAM::deletePbo()
+{
+    delete[] m_outputBuffer;
+    m_outputBuffer = nullptr;
+}
+
+
+void RENDER_3D_RAYTRACE_RAM::SetCurWindowSize( const wxSize& aSize )
+{
+    if( m_windowSize != aSize )
+    {
+        m_windowSize = aSize;
+
+        initPbo();
+    }
+}
+
+
+bool RENDER_3D_RAYTRACE_RAM::Redraw( bool aIsMoving, REPORTER* aStatusReporter,
+                                     REPORTER* aWarningReporter )
+{
+    bool requestRedraw = false;
+
+    // Initialize openGL if need
+    if( !m_canvasInitialized )
+    {
+        m_canvasInitialized = true;
+
+        //aIsMoving = true;
+        requestRedraw = true;
+
+        // It will assign the first time the windows size, so it will now
+        // revert to preview mode the first time the Redraw is called
+        m_oldWindowsSize = m_windowSize;
+        initializeBlockPositions();
+    }
+
+    std::unique_ptr<BUSY_INDICATOR> busy = CreateBusyIndicator();
+
+    // Reload board if it was requested
+    if( m_reloadRequested )
+    {
+        if( aStatusReporter )
+            aStatusReporter->Report( _( "Loading..." ) );
+
+        //aIsMoving = true;
+        requestRedraw = true;
+        Reload( aStatusReporter, aWarningReporter, false );
+    }
+
+
+    // Recalculate constants if windows size was changed
+    if( m_windowSize != m_oldWindowsSize )
+    {
+        m_oldWindowsSize = m_windowSize;
+        aIsMoving = true;
+        requestRedraw = true;
+
+        initializeBlockPositions();
+    }
+
+    const bool was_camera_changed = m_camera.ParametersChanged();
+
+    if( requestRedraw || aIsMoving || was_camera_changed )
+        m_renderState = RT_RENDER_STATE_MAX; // Set to an invalid state,
+                                             // so it will restart again latter
+
+    // This will only render if need, otherwise it will redraw the PBO on the screen again
+    if( aIsMoving || was_camera_changed )
+    {
+        // Set head light (camera view light) with the opposite direction of the camera
+        if( m_cameraLight )
+            m_cameraLight->SetDirection( -m_camera.GetDir() );
+
+        if( m_outputBuffer )
+        {
+            renderPreview( m_outputBuffer );
+        }
+    }
+    else
+    {
+        if( m_renderState != RT_RENDER_STATE_FINISH )
+        {
+            if( m_outputBuffer )
+            {
+                render( m_outputBuffer, aStatusReporter );
+
+                if( m_renderState != RT_RENDER_STATE_FINISH )
+                    requestRedraw = true;
+            }
+        }
+    }
+
+    return requestRedraw;
+}
+
+
+void RENDER_3D_RAYTRACE_RAM::initPbo()
+{
+    deletePbo();
+
+    m_pboDataSize = m_realBufferSize.x * m_realBufferSize.y * 4;
+    m_outputBuffer = new GLubyte[m_pboDataSize]();
+}
diff --git a/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.h b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.h
new file mode 100644
index 0000000000..6283f24c14
--- /dev/null
+++ b/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_ram.h
@@ -0,0 +1,58 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2015-2020 Mario Luzeiro <mrluzeiro@ua.pt>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef RENDER_3D_RAYTRACE_RAM_H
+#define RENDER_3D_RAYTRACE_RAM_H
+
+#include "render_3d_raytrace_base.h"
+
+
+class RENDER_3D_RAYTRACE_RAM : public RENDER_3D_RAYTRACE_BASE
+{
+public:
+    // TODO: Take into account board thickness so that the camera won't move inside of the board
+    // when facing it perpendicularly.
+    static constexpr float MIN_DISTANCE_IU = 4 * PCB_IU_PER_MM;
+
+    explicit RENDER_3D_RAYTRACE_RAM( BOARD_ADAPTER& aAdapter, CAMERA& aCamera );
+
+    ~RENDER_3D_RAYTRACE_RAM();
+
+    GLubyte* GetBuffer();
+    wxSize   GetRealBufferSize();
+
+    void SetCurWindowSize( const wxSize& aSize ) override;
+    bool Redraw( bool aIsMoving, REPORTER* aStatusReporter, REPORTER* aWarningReporter ) override;
+
+private:
+    void initPbo() override;
+    void deletePbo() override;
+
+    GLubyte* m_outputBuffer;
+    GLuint   m_pboDataSize;
+};
+
+
+#endif // RENDER_3D_RAYTRACE_RAM_H
diff --git a/3d-viewer/3d_rendering/render_3d_base.cpp b/3d-viewer/3d_rendering/render_3d_base.cpp
index 4215321adc..6e9dc81a75 100644
--- a/3d-viewer/3d_rendering/render_3d_base.cpp
+++ b/3d-viewer/3d_rendering/render_3d_base.cpp
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 1992-2016 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -44,13 +44,12 @@
 const wxChar* RENDER_3D_BASE::m_logTrace = wxT( "KI_TRACE_3D_RENDER" );
 
 
-RENDER_3D_BASE::RENDER_3D_BASE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aBoardAdapter, CAMERA& aCamera ) :
+RENDER_3D_BASE::RENDER_3D_BASE( BOARD_ADAPTER& aBoardAdapter, CAMERA& aCamera ) :
         m_boardAdapter( aBoardAdapter ),
         m_camera( aCamera )
 {
     wxLogTrace( m_logTrace, wxT( "RENDER_3D_BASE::RENDER_3D_BASE" ) );
-    m_canvas                = aCanvas;
-    m_is_opengl_initialized = false;
+    m_canvasInitialized     = false;
     m_windowSize            = wxSize( -1, -1 );
     m_reloadRequested       = true;
 }
diff --git a/3d-viewer/3d_rendering/render_3d_base.h b/3d-viewer/3d_rendering/render_3d_base.h
index c088e536f4..fa0c6ede60 100644
--- a/3d-viewer/3d_rendering/render_3d_base.h
+++ b/3d-viewer/3d_rendering/render_3d_base.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 1992-2016 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -42,7 +42,7 @@
 class RENDER_3D_BASE
 {
 public:
-    explicit RENDER_3D_BASE( EDA_3D_CANVAS* aCanvas, BOARD_ADAPTER& aBoardAdapter, CAMERA& aCamera );
+    explicit RENDER_3D_BASE( BOARD_ADAPTER& aBoardAdapter, CAMERA& aCamera );
 
     virtual ~RENDER_3D_BASE() = 0;
 
@@ -98,16 +98,13 @@ protected:
      */
     std::unique_ptr<BUSY_INDICATOR> CreateBusyIndicator() const;
 
-    ///< the canvas to display the scene
-    EDA_3D_CANVAS* m_canvas;
-
     ///< Settings reference in use for this render.
     BOARD_ADAPTER& m_boardAdapter;
 
     CAMERA&        m_camera;
 
-    ///< Flag if the opengl specific for this render was already initialized.
-    bool m_is_opengl_initialized;
+    ///< Flag if the canvas specific for this render was already initialized.
+    bool m_canvasInitialized;
 
     ///< @todo This must be reviewed in order to flag change types.
     bool m_reloadRequested;
diff --git a/3d-viewer/3d_rendering/track_ball.cpp b/3d-viewer/3d_rendering/track_ball.cpp
index 8895fb7cb4..f1ede58d3b 100644
--- a/3d-viewer/3d_rendering/track_ball.cpp
+++ b/3d-viewer/3d_rendering/track_ball.cpp
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2020 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -38,11 +38,24 @@
 // stdlib
 #include <algorithm>
 
-TRACK_BALL::TRACK_BALL( float aInitialDistance ) :
-    CAMERA( aInitialDistance )
+
+TRACK_BALL::TRACK_BALL( float aInitialDistance ) : CAMERA( aInitialDistance )
 {
     wxLogTrace( m_logTrace, wxT( "TRACK_BALL::TRACK_BALL" ) );
+    initQuat();
+}
 
+
+TRACK_BALL::TRACK_BALL( SFVEC3F aInitPos, SFVEC3F aLookat, PROJECTION_TYPE aProjectionType ) :
+        CAMERA( aInitPos, aLookat, aProjectionType )
+{
+    wxLogTrace( m_logTrace, wxT( "TRACK_BALL::TRACK_BALL" ) );
+    initQuat();
+}
+
+
+void TRACK_BALL::initQuat()
+{
     memset( m_quat_t0, 0, sizeof( m_quat_t0 ) );
     memset( m_quat_t1, 0, sizeof( m_quat_t1 ) );
 
@@ -50,6 +63,7 @@ TRACK_BALL::TRACK_BALL( float aInitialDistance ) :
     trackball( m_quat_t1, 0.0, 0.0, 0.0, 0.0 );
 }
 
+
 void TRACK_BALL::Drag( const wxPoint& aNewMousePosition )
 {
     m_parametersChanged = true;
diff --git a/3d-viewer/3d_rendering/track_ball.h b/3d-viewer/3d_rendering/track_ball.h
index ed3b262c1d..a98586596b 100644
--- a/3d-viewer/3d_rendering/track_ball.h
+++ b/3d-viewer/3d_rendering/track_ball.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2020 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -37,6 +37,7 @@ class TRACK_BALL : public CAMERA
 {
 public:
     explicit TRACK_BALL( float aInitialDistance );
+    explicit TRACK_BALL( SFVEC3F aInitPos, SFVEC3F aLookat, PROJECTION_TYPE aProjectionType );
 
     virtual ~TRACK_BALL()
     {
@@ -57,6 +58,8 @@ public:
     void Interpolate( float t ) override;
 
 private:
+    void initQuat();
+
     /**
      *  interpolate quaternions of the trackball
      */
diff --git a/3d-viewer/3d_viewer/eda_3d_viewer_settings.h b/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
index c21dd2f5bb..a466144c8d 100644
--- a/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
+++ b/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
@@ -3,7 +3,7 @@
  *
  * Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
  * Copyright (C) 2023 CERN
- * Copyright (C) 2020-2023 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2020-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software: you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -23,7 +23,6 @@
 #define EDA_3D_VIEWER_SETTINGS_H_
 
 #include <3d_enums.h>
-#include <common_ogl/ogl_attr_list.h>
 #include <plugins/3dapi/xv3d_types.h>
 #include <settings/app_settings.h>
 #include <settings/parameters.h>
@@ -34,6 +33,8 @@
 #define FOLLOW_PLOT_SETTINGS wxT( "follow_plot_settings" )
 #define LEGACY_PRESET_FLAG   wxT( "legacy_preset_flag" )
 
+enum class ANTIALIASING_MODE;
+
 
 struct LAYER_PRESET_3D
 {
diff --git a/3d-viewer/CMakeLists.txt b/3d-viewer/CMakeLists.txt
index ccbda1c591..73db487ad9 100644
--- a/3d-viewer/CMakeLists.txt
+++ b/3d-viewer/CMakeLists.txt
@@ -45,7 +45,9 @@ set(3D-VIEWER_SRCS
     ${DIR_RAY_ACC}/container_2d.cpp
     ${DIR_RAY}/PerlinNoise.cpp
     ${DIR_RAY}/create_scene.cpp
-    ${DIR_RAY}/render_3d_raytrace.cpp
+    ${DIR_RAY}/render_3d_raytrace_base.cpp
+    ${DIR_RAY}/render_3d_raytrace_gl.cpp
+    ${DIR_RAY}/render_3d_raytrace_ram.cpp
     ${DIR_RAY}/frustum.cpp
     ${DIR_RAY}/material.cpp
     ${DIR_RAY}/mortoncodes.cpp
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 5505b187f5..16642e1c9f 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -88,6 +88,7 @@ set( KICOMMON_SRCS
     jobs/job_export_sch_pythonbom.cpp
     jobs/job_fp_export_svg.cpp
     jobs/job_fp_upgrade.cpp
+    jobs/job_pcb_render.cpp
     jobs/job_pcb_drc.cpp
     jobs/job_sch_erc.cpp
     jobs/job_sym_export_svg.cpp
diff --git a/common/gal/3d/camera.cpp b/common/gal/3d/camera.cpp
index 7886ff6913..7035af2322 100644
--- a/common/gal/3d/camera.cpp
+++ b/common/gal/3d/camera.cpp
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2020 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -29,6 +29,7 @@
 #include <gal/3d/camera.h>
 #include <wx/log.h>
 #include <algorithm>
+#include <3d_enums.h>
 
 // A helper function to normalize aAngle between -2PI and +2PI
 inline void normalise2PI( float& aAngle )
@@ -50,15 +51,22 @@ const float CAMERA::DEFAULT_MIN_ZOOM = 0.020f;
 const float CAMERA::DEFAULT_MAX_ZOOM = 2.0f;
 
 
-CAMERA::CAMERA( float aInitialDistance )
+CAMERA::CAMERA( float aInitialDistance ) :
+        CAMERA( SFVEC3F( 0.0f, 0.0f, -aInitialDistance ), SFVEC3F( 0.0f ),
+                PROJECTION_TYPE::PERSPECTIVE )
+{
+}
+
+
+CAMERA::CAMERA( SFVEC3F aInitPos, SFVEC3F aLookat, PROJECTION_TYPE aProjectionType )
 {
     wxLogTrace( m_logTrace, wxT( "CAMERA::CAMERA" ) );
 
-    m_camera_pos_init       = SFVEC3F( 0.0f, 0.0f, -aInitialDistance );
-    m_board_lookat_pos_init = SFVEC3F( 0.0f );
-    m_windowSize            = SFVEC2I( 0, 0 );
-    m_projectionType        = PROJECTION_TYPE::PERSPECTIVE;
-    m_interpolation_mode    = CAMERA_INTERPOLATION::BEZIER;
+    m_camera_pos_init = aInitPos;
+    m_board_lookat_pos_init = aLookat;
+    m_windowSize = SFVEC2I( 0, 0 );
+    m_projectionType = aProjectionType;
+    m_interpolation_mode = CAMERA_INTERPOLATION::BEZIER;
 
     m_minZoom = DEFAULT_MIN_ZOOM;
     m_maxZoom = DEFAULT_MAX_ZOOM;
@@ -99,6 +107,57 @@ void CAMERA::Reset()
 }
 
 
+bool CAMERA::ViewCommand_T1( VIEW3D_TYPE aRequestedView )
+{
+    switch( aRequestedView )
+    {
+    case VIEW3D_TYPE::VIEW3D_RIGHT:
+        SetT0_and_T1_current_T();
+        Reset_T1();
+        RotateZ_T1( glm::radians( -90.0f ) );
+        RotateX_T1( glm::radians( -90.0f ) );
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_LEFT:
+        Reset_T1();
+        RotateZ_T1( glm::radians(  90.0f ) );
+        RotateX_T1( glm::radians( -90.0f ) );
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_FRONT:
+        Reset_T1();
+        RotateX_T1( glm::radians( -90.0f ) );
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_BACK:
+        Reset_T1();
+        RotateX_T1( glm::radians( -90.0f ) );
+
+        // The rotation angle should be 180.
+        // We use 179.999 (180 - epsilon) to avoid a full 360 deg rotation when
+        // using 180 deg if the previous rotated position was already 180 deg
+        RotateZ_T1( glm::radians( 179.999f ) );
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_TOP:
+        Reset_T1();
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_BOTTOM:
+        Reset_T1();
+        RotateY_T1( glm::radians( 179.999f ) );    // Rotation = 180 - epsilon
+        return true;
+
+    case VIEW3D_TYPE::VIEW3D_FLIP:
+        RotateY_T1( glm::radians( 179.999f ) );
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+
 void CAMERA::Reset_T1()
 {
     m_camera_pos_t1        = m_camera_pos_init;
diff --git a/common/jobs/job_pcb_render.cpp b/common/jobs/job_pcb_render.cpp
new file mode 100644
index 0000000000..376ab2201d
--- /dev/null
+++ b/common/jobs/job_pcb_render.cpp
@@ -0,0 +1,28 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2023 Mark Roszko <mark.roszko@gmail.com>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 2023-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <jobs/job_pcb_render.h>
+
+
+JOB_PCB_RENDER::JOB_PCB_RENDER( bool aIsCli ) :
+        JOB( "render", aIsCli ), m_filename(), m_outputFile()
+{
+}
\ No newline at end of file
diff --git a/common/jobs/job_pcb_render.h b/common/jobs/job_pcb_render.h
new file mode 100644
index 0000000000..7cf52b85bd
--- /dev/null
+++ b/common/jobs/job_pcb_render.h
@@ -0,0 +1,84 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef JOB_PCB_RENDER_H
+#define JOB_PCB_RENDER_H
+
+#include <kicommon.h>
+#include <wx/string.h>
+#include "job.h"
+#include <optional>
+#include <math/vector3.h>
+
+class KICOMMON_API JOB_PCB_RENDER : public JOB
+{
+public:
+    JOB_PCB_RENDER( bool aIsCli );
+
+    wxString m_filename;
+    wxString m_outputFile;
+
+    enum class FORMAT
+    {
+        PNG,
+        JPEG
+    };
+
+    enum class QUALITY
+    {
+        BASIC,
+        HIGH,
+        USER
+    };
+
+    enum class BG_STYLE
+    {
+        DEFAULT,
+        TRANSPARENT,
+        OPAQUE
+    };
+
+    enum class SIDE
+    {
+        TOP,
+        BOTTOM,
+        LEFT,
+        RIGHT,
+        FRONT,
+        BACK
+    };
+
+    FORMAT      m_format = FORMAT::PNG;
+    QUALITY     m_quality = QUALITY::BASIC;
+    BG_STYLE    m_bgStyle = BG_STYLE::DEFAULT;
+    int         m_width = 0;
+    int         m_height = 0;
+    std::string m_colorPreset;
+    SIDE        m_side = SIDE::TOP;
+    double      m_zoom = 1.0;
+    bool        m_perspective = false;
+    VECTOR3D    m_rotation;
+    VECTOR3D    m_pan;
+    VECTOR3D    m_pivot;
+    bool        m_floor = false;
+};
+
+#endif
\ No newline at end of file
diff --git a/3d-viewer/3d_enums.h b/include/3d_enums.h
similarity index 100%
rename from 3d-viewer/3d_enums.h
rename to include/3d_enums.h
diff --git a/include/gal/3d/camera.h b/include/gal/3d/camera.h
index 5595e4d197..7e2060089e 100644
--- a/include/gal/3d/camera.h
+++ b/include/gal/3d/camera.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
- * Copyright (C) 2015-2023 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2015-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -34,6 +34,7 @@
 #include <plugins/3dapi/xv3d_types.h>
 #include <wx/gdicmn.h>  // for wxSize
 #include <vector>
+#include <3d_enums.h>
 
 enum class PROJECTION_TYPE
 {
@@ -110,6 +111,7 @@ public:
      * @param aInitialDistance Initial Z-distance to the board
      */
     explicit CAMERA( float aInitialDistance );
+    explicit CAMERA( SFVEC3F aInitPos, SFVEC3F aLookat, PROJECTION_TYPE aProjectionType );
 
     virtual ~CAMERA()
     {
@@ -232,6 +234,8 @@ public:
         zoomChanged();
     }
 
+    bool ViewCommand_T1( VIEW3D_TYPE aRequestedView );
+
     void RotateX( float aAngleInRadians );
     void RotateY( float aAngleInRadians );
     void RotateZ( float aAngleInRadians );
diff --git a/kicad/CMakeLists.txt b/kicad/CMakeLists.txt
index b64f2138e1..7480c7dae4 100644
--- a/kicad/CMakeLists.txt
+++ b/kicad/CMakeLists.txt
@@ -42,6 +42,7 @@ set( KICAD_CLI_SRCS
     cli/command.cpp
     cli/command_pcb_export_base.cpp
     cli/command_pcb_drc.cpp
+    cli/command_pcb_render.cpp
     cli/command_pcb_export_3d.cpp
     cli/command_pcb_export_drill.cpp
     cli/command_pcb_export_dxf.cpp
diff --git a/kicad/cli/command_pcb_render.cpp b/kicad/cli/command_pcb_render.cpp
new file mode 100644
index 0000000000..5a227e3fcd
--- /dev/null
+++ b/kicad/cli/command_pcb_render.cpp
@@ -0,0 +1,302 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
+ * Copyright (C) 2024 Alex Shvartzkop <dudesuchamazing@gmail.com>
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "command_pcb_render.h"
+#include <cli/exit_codes.h>
+#include "jobs/job_pcb_render.h"
+#include <kiface_base.h>
+#include <layer_ids.h>
+#include <string_utils.h>
+#include <wx/crt.h>
+#include <magic_enum.hpp>
+
+#include <macros.h>
+#include <wx/tokenzr.h>
+#include "../../3d-viewer/3d_viewer/eda_3d_viewer_settings.h"
+#include <math/vector3.h>
+
+#define ARG_BACKGROUND "--background"
+#define ARG_QUALITY "--quality"
+
+#define ARG_WIDTH "--width"
+#define ARG_WIDTH_SHORT "-w"
+
+#define ARG_HEIGHT "--height"
+#define ARG_HEIGHT_SHORT "-h"
+
+#define ARG_SIDE "--side"
+#define ARG_PRESET "--preset"
+#define ARG_PAN "--pan"
+#define ARG_PIVOT "--pivot"
+#define ARG_ROTATE "--rotate"
+#define ARG_ZOOM "--zoom"
+#define ARG_PERSPECTIVE "--perspective"
+#define ARG_FLOOR "--floor"
+
+
+template <typename T>
+static wxString enumString()
+{
+    wxString str;
+    auto     names = magic_enum::enum_names<T>();
+
+    for( size_t i = 0; i < names.size(); i++ )
+    {
+        std::string name = { names[i].begin(), names[i].end() };
+
+        if( i > 0 )
+            str << ", ";
+
+        std::transform( name.begin(), name.end(), name.begin(),
+                        []( unsigned char c )
+                        {
+                            return std::tolower( c );
+                        } );
+
+        str << name;
+    }
+
+    return str;
+}
+
+
+template <typename T>
+static std::vector<std::string> enumChoices()
+{
+    std::vector<std::string> out;
+
+    for( auto& strView : magic_enum::enum_names<T>() )
+    {
+        std::string name = { strView.begin(), strView.end() };
+
+        std::transform( name.begin(), name.end(), name.begin(),
+                        []( unsigned char c )
+                        {
+                            return std::tolower( c );
+                        } );
+
+        out.emplace_back( name );
+    }
+
+    return out;
+}
+
+
+template <typename T>
+static std::optional<T> strToEnum( std::string& aInput )
+{
+    return magic_enum::enum_cast<T>( aInput, magic_enum::case_insensitive );
+}
+
+
+template <typename T>
+static bool getToEnum( const std::string& aInput, T& aOutput )
+{
+    // If not specified, leave at default
+    if( aInput.empty() )
+        return true;
+
+    if( auto opt = magic_enum::enum_cast<T>( aInput, magic_enum::case_insensitive ) )
+    {
+        aOutput = *opt;
+        return true;
+    }
+
+    return false;
+}
+
+
+static bool getToVector3( const std::string& aInput, VECTOR3D& aOutput )
+{
+    // If not specified, leave at default
+    if( aInput.empty() )
+        return true;
+
+    // Remove potential quotes
+    wxString wxStr = From_UTF8( aInput );
+
+    if( wxStr[0] == '\'' )
+        wxStr = wxStr.AfterFirst( '\'' );
+
+    if( wxStr[wxStr.length() - 1] == '\'' )
+        wxStr = wxStr.BeforeLast( '\'' );
+
+    wxArrayString arr = wxSplit( wxStr, ',', 0 );
+
+    if( arr.size() != 3 )
+        return false;
+
+    VECTOR3D vec;
+    bool     success = true;
+    success &= arr[0].Trim().ToCDouble( &vec.x );
+    success &= arr[1].Trim().ToCDouble( &vec.y );
+    success &= arr[2].Trim().ToCDouble( &vec.z );
+
+    if( !success )
+        return false;
+
+    aOutput = vec;
+    return true;
+}
+
+
+CLI::PCB_RENDER_COMMAND::PCB_RENDER_COMMAND() : COMMAND( "render" )
+{
+    addCommonArgs( true, true, false, false );
+    addDefineArg();
+
+    m_argParser.add_description(
+            UTF8STDSTR( _( "Renders the PCB in 3D view to PNG or JPEG image" ) ) );
+
+    m_argParser.add_argument( ARG_WIDTH, ARG_WIDTH_SHORT )
+            .default_value( 1600 )
+            .scan<'i', int>()
+            .metavar( "WIDTH" )
+            .help( UTF8STDSTR( _( "Image width" ) ) );
+
+    m_argParser.add_argument( ARG_HEIGHT, ARG_HEIGHT_SHORT )
+            .default_value( 900 )
+            .scan<'i', int>()
+            .metavar( "HEIGHT" )
+            .help( UTF8STDSTR( _( "Image height" ) ) );
+
+    m_argParser.add_argument( ARG_SIDE )
+            .default_value( std::string( "top" ) )
+            .add_choices( enumChoices<JOB_PCB_RENDER::SIDE>() )
+            .metavar( "SIDE" )
+            .help( UTF8STDSTR( wxString::Format( _( "Render from side. Options: %s" ),
+                                                 enumString<JOB_PCB_RENDER::SIDE>() ) ) );
+
+    m_argParser.add_argument( ARG_BACKGROUND )
+            .default_value( std::string( "" ) )
+            .help( UTF8STDSTR( _( "Image background. Options: transparent, opaque. Default: "
+                                  "transparent for PNG, opaque for JPEG" ) ) )
+            .metavar( "BG" );
+
+    m_argParser.add_argument( ARG_QUALITY )
+            .default_value( std::string( "basic" ) )
+            .add_choices( enumChoices<JOB_PCB_RENDER::QUALITY>() )
+            .metavar( "QUALITY" )
+            .help( UTF8STDSTR( wxString::Format( _( "Render quality. Options: %s" ),
+                                                 enumString<JOB_PCB_RENDER::QUALITY>() ) ) );
+
+    m_argParser.add_argument( ARG_PRESET )
+            .default_value( std::string( wxString( FOLLOW_PLOT_SETTINGS ) ) )
+            .metavar( "PRESET" )
+            .help( UTF8STDSTR( wxString::Format( _( "Color preset. Options: %s, %s, %s, ..." ),
+                                                 FOLLOW_PCB, FOLLOW_PLOT_SETTINGS,
+                                                 LEGACY_PRESET_FLAG ) ) );
+
+    m_argParser.add_argument( ARG_FLOOR )
+            .flag()
+            .help( UTF8STDSTR( _( "Enables floor, shadows and post-processing, even if disabled in "
+                                  "quality preset" ) ) );
+
+    m_argParser.add_argument( ARG_PERSPECTIVE )
+            .flag()
+            .help( UTF8STDSTR( _( "Use perspective projection instead of orthogonal" ) ) );
+
+    m_argParser.add_argument( ARG_ZOOM )
+            .default_value( 1.0 )
+            .scan<'g', double>()
+            .metavar( "ZOOM" )
+            .help( UTF8STDSTR( _( "Camera zoom" ) ) );
+
+    m_argParser.add_argument( ARG_PAN )
+            .default_value( std::string( "" ) )
+            .metavar( "VECTOR" )
+            .help( UTF8STDSTR( _( "Pan camera, format 'X,Y,Z' e.g.: '3,0,0'" ) ) );
+
+    m_argParser.add_argument( ARG_PIVOT )
+            .default_value( std::string( "" ) )
+            .metavar( "PIVOT" )
+            .help( UTF8STDSTR( _( "Set pivot point relative to the board center in centimeters, format 'X,Y,Z' "
+                                  "e.g.: '-10,2,0'" ) ) );
+
+    m_argParser.add_argument( ARG_ROTATE )
+            .default_value( std::string( "" ) )
+            .metavar( "ANGLES" )
+            .help( UTF8STDSTR(
+                    _( "Rotate board, format 'X,Y,Z' e.g.: '-45,0,45' for isometric view" ) ) );
+}
+
+
+int CLI::PCB_RENDER_COMMAND::doPerform( KIWAY& aKiway )
+{
+    std::unique_ptr<JOB_PCB_RENDER> renderJob( new JOB_PCB_RENDER( true ) );
+
+    renderJob->m_outputFile = m_argOutput;
+    renderJob->m_filename = m_argInput;
+    renderJob->SetVarOverrides( m_argDefineVars );
+
+    renderJob->m_colorPreset = m_argParser.get<std::string>( ARG_PRESET );
+    renderJob->m_width = m_argParser.get<int>( ARG_WIDTH );
+    renderJob->m_height = m_argParser.get<int>( ARG_HEIGHT );
+    renderJob->m_zoom = m_argParser.get<double>( ARG_ZOOM );
+    renderJob->m_perspective = m_argParser.get<bool>( ARG_PERSPECTIVE );
+    renderJob->m_floor = m_argParser.get<bool>( ARG_FLOOR );
+
+    getToEnum( m_argParser.get<std::string>( ARG_QUALITY ), renderJob->m_quality );
+    getToEnum( m_argParser.get<std::string>( ARG_SIDE ), renderJob->m_side );
+
+    if( !getToEnum( m_argParser.get<std::string>( ARG_BACKGROUND ), renderJob->m_bgStyle ) )
+    {
+        wxFprintf( stderr, _( "Invalid background\n" ) );
+        return EXIT_CODES::ERR_ARGS;
+    }
+
+    if( !getToVector3( m_argParser.get<std::string>( ARG_ROTATE ), renderJob->m_rotation ) )
+    {
+        wxFprintf( stderr, _( "Invalid rotation format\n" ) );
+        return EXIT_CODES::ERR_ARGS;
+    }
+
+    if( !getToVector3( m_argParser.get<std::string>( ARG_PAN ), renderJob->m_pan ) )
+    {
+        wxFprintf( stderr, _( "Invalid pan format\n" ) );
+        return EXIT_CODES::ERR_ARGS;
+    }
+
+    if( !getToVector3( m_argParser.get<std::string>( ARG_PIVOT ), renderJob->m_pivot ) )
+    {
+        wxFprintf( stderr, _( "Invalid pivot format\n" ) );
+        return EXIT_CODES::ERR_ARGS;
+    }
+
+    if( m_argOutput.Lower().EndsWith( wxS( ".png" ) ) )
+    {
+        renderJob->m_format = JOB_PCB_RENDER::FORMAT::PNG;
+    }
+    else if( m_argOutput.Lower().EndsWith( wxS( ".jpg" ) )
+             || m_argOutput.Lower().EndsWith( wxS( ".jpeg" ) ) )
+    {
+        renderJob->m_format = JOB_PCB_RENDER::FORMAT::JPEG;
+    }
+    else
+    {
+        wxFprintf( stderr, _( "Invalid image format\n" ) );
+        return EXIT_CODES::ERR_ARGS;
+    }
+
+    int exitCode = aKiway.ProcessJob( KIWAY::FACE_PCB, renderJob.get() );
+
+    return exitCode;
+}
diff --git a/kicad/cli/command_pcb_render.h b/kicad/cli/command_pcb_render.h
new file mode 100644
index 0000000000..5b3161cd39
--- /dev/null
+++ b/kicad/cli/command_pcb_render.h
@@ -0,0 +1,38 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COMMAND_EXPORT_PCB_RENDER_H
+#define COMMAND_EXPORT_PCB_RENDER_H
+
+#include "command.h"
+
+namespace CLI
+{
+class PCB_RENDER_COMMAND : public COMMAND
+{
+public:
+    PCB_RENDER_COMMAND();
+
+protected:
+    int doPerform( KIWAY& aKiway ) override;
+};
+} // namespace CLI
+
+#endif
\ No newline at end of file
diff --git a/kicad/kicad_cli.cpp b/kicad/kicad_cli.cpp
index 6e99bf6bc0..fde643f717 100644
--- a/kicad/kicad_cli.cpp
+++ b/kicad/kicad_cli.cpp
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2004-2015 Jean-Pierre Charras, jp.charras at wanadoo.fr
- * Copyright (C) 2004-2021 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2004-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -49,6 +49,7 @@
 #include "cli/command_pcb.h"
 #include "cli/command_pcb_export.h"
 #include "cli/command_pcb_drc.h"
+#include "cli/command_pcb_render.h"
 #include "cli/command_pcb_export_3d.h"
 #include "cli/command_pcb_export_drill.h"
 #include "cli/command_pcb_export_dxf.h"
@@ -128,6 +129,7 @@ struct COMMAND_ENTRY
 
 static CLI::PCB_COMMAND                  pcbCmd{};
 static CLI::PCB_DRC_COMMAND              pcbDrcCmd{};
+static CLI::PCB_RENDER_COMMAND           pcbRenderCmd{};
 static CLI::PCB_EXPORT_DRILL_COMMAND     exportPcbDrillCmd{};
 static CLI::PCB_EXPORT_DXF_COMMAND       exportPcbDxfCmd{};
 static CLI::PCB_EXPORT_3D_COMMAND        exportPcbGlbCmd{ "glb", UTF8STDSTR( _( "Export GLB (binary GLTF)" ) ), JOB_EXPORT_PCB_3D::FORMAT::GLB };
@@ -183,6 +185,9 @@ static std::vector<COMMAND_ENTRY> commandStack = {
             {
                 &pcbDrcCmd
             },
+            {
+                &pcbRenderCmd
+            },
             {
                 &exportPcbCmd,
                 {
diff --git a/pcbnew/pcbnew_jobs_handler.cpp b/pcbnew/pcbnew_jobs_handler.cpp
index 0ad66cf616..83f84fa942 100644
--- a/pcbnew/pcbnew_jobs_handler.cpp
+++ b/pcbnew/pcbnew_jobs_handler.cpp
@@ -37,6 +37,7 @@
 #include <jobs/job_export_pcb_pos.h>
 #include <jobs/job_export_pcb_svg.h>
 #include <jobs/job_export_pcb_3d.h>
+#include <jobs/job_pcb_render.h>
 #include <jobs/job_pcb_drc.h>
 #include <cli/exit_codes.h>
 #include <exporters/place_file_exporter.h>
@@ -62,6 +63,9 @@
 #include <pcbnew_settings.h>
 #include <pcbplot.h>
 #include <pgm_base.h>
+#include <3d_rendering/raytracing/render_3d_raytrace_ram.h>
+#include <3d_rendering/track_ball.h>
+#include <project_pcb.h>
 #include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
 #include <reporter.h>
 #include <string_utf8_map.h>
@@ -73,10 +77,18 @@
 #include "pcbnew_scripting_helpers.h"
 
 
+#ifdef _WIN32
+#ifdef TRANSPARENT
+#undef TRANSPARENT
+#endif
+#endif
+
+
 PCBNEW_JOBS_HANDLER::PCBNEW_JOBS_HANDLER( KIWAY* aKiway ) :
         JOB_DISPATCHER( aKiway )
 {
     Register( "3d", std::bind( &PCBNEW_JOBS_HANDLER::JobExportStep, this, std::placeholders::_1 ) );
+    Register( "render", std::bind( &PCBNEW_JOBS_HANDLER::JobExportRender, this, std::placeholders::_1 ) );
     Register( "svg", std::bind( &PCBNEW_JOBS_HANDLER::JobExportSvg, this, std::placeholders::_1 ) );
     Register( "dxf", std::bind( &PCBNEW_JOBS_HANDLER::JobExportDxf, this, std::placeholders::_1 ) );
     Register( "pdf", std::bind( &PCBNEW_JOBS_HANDLER::JobExportPdf, this, std::placeholders::_1 ) );
@@ -212,6 +224,186 @@ int PCBNEW_JOBS_HANDLER::JobExportStep( JOB* aJob )
 }
 
 
+int PCBNEW_JOBS_HANDLER::JobExportRender( JOB* aJob )
+{
+    JOB_PCB_RENDER* aRenderJob = dynamic_cast<JOB_PCB_RENDER*>( aJob );
+
+    if( aRenderJob == nullptr )
+        return CLI::EXIT_CODES::ERR_UNKNOWN;
+
+    if( aJob->IsCli() )
+        m_reporter->Report( _( "Loading board\n" ), RPT_SEVERITY_INFO );
+
+    BOARD* brd = LoadBoard( aRenderJob->m_filename, true );
+    brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
+
+    BOARD_ADAPTER m_boardAdapter;
+
+    m_boardAdapter.SetBoard( brd );
+    m_boardAdapter.m_IsBoardView = false;
+    m_boardAdapter.m_IsPreviewer =
+            true; // Force display 3D models, regardless the 3D viewer options
+
+    EDA_3D_VIEWER_SETTINGS* cfg =
+            Pgm().GetSettingsManager().GetAppSettings<EDA_3D_VIEWER_SETTINGS>();
+
+    if( aRenderJob->m_bgStyle == JOB_PCB_RENDER::BG_STYLE::TRANSPARENT
+        || aRenderJob->m_bgStyle == JOB_PCB_RENDER::BG_STYLE::DEFAULT
+                   && aRenderJob->m_format == JOB_PCB_RENDER::FORMAT::PNG )
+    {
+        BOARD_ADAPTER::g_DefaultBackgroundTop = COLOR4D( 1.0, 1.0, 1.0, 0.0 );
+        BOARD_ADAPTER::g_DefaultBackgroundBot = COLOR4D( 1.0, 1.0, 1.0, 0.0 );
+    }
+
+    if( aRenderJob->m_quality == JOB_PCB_RENDER::QUALITY::BASIC )
+    {
+        // Silkscreen is pixelated without antialiasing
+        cfg->m_Render.raytrace_anti_aliasing = true;
+
+        cfg->m_Render.raytrace_backfloor = false;
+        cfg->m_Render.raytrace_post_processing = false;
+
+        cfg->m_Render.raytrace_procedural_textures = false;
+        cfg->m_Render.raytrace_reflections = false;
+        cfg->m_Render.raytrace_shadows = false;
+
+        // Better colors
+        cfg->m_Render.differentiate_plated_copper = true;
+
+        // Tracks below soldermask are not visible without refractions
+        cfg->m_Render.raytrace_refractions = true;
+        cfg->m_Render.raytrace_recursivelevel_refractions = 1;
+    }
+    else if( aRenderJob->m_quality == JOB_PCB_RENDER::QUALITY::HIGH )
+    {
+        cfg->m_Render.raytrace_anti_aliasing = true;
+        cfg->m_Render.raytrace_backfloor = true;
+        cfg->m_Render.raytrace_post_processing = true;
+        cfg->m_Render.raytrace_procedural_textures = true;
+        cfg->m_Render.raytrace_reflections = true;
+        cfg->m_Render.raytrace_shadows = true;
+        cfg->m_Render.raytrace_refractions = true;
+        cfg->m_Render.differentiate_plated_copper = true;
+    }
+
+    if( aRenderJob->m_floor )
+    {
+        cfg->m_Render.raytrace_backfloor = true;
+        cfg->m_Render.raytrace_shadows = true;
+        cfg->m_Render.raytrace_post_processing = true;
+    }
+
+    cfg->m_CurrentPreset = aRenderJob->m_colorPreset;
+    m_boardAdapter.m_Cfg = cfg;
+
+    m_boardAdapter.Set3dCacheManager( PROJECT_PCB::Get3DCacheManager( brd->GetProject() ) );
+
+    static std::map<JOB_PCB_RENDER::SIDE, VIEW3D_TYPE> s_viewCmdMap = {
+        { JOB_PCB_RENDER::SIDE::TOP, VIEW3D_TYPE::VIEW3D_TOP },
+        { JOB_PCB_RENDER::SIDE::BOTTOM, VIEW3D_TYPE::VIEW3D_BOTTOM },
+        { JOB_PCB_RENDER::SIDE::LEFT, VIEW3D_TYPE::VIEW3D_LEFT },
+        { JOB_PCB_RENDER::SIDE::RIGHT, VIEW3D_TYPE::VIEW3D_RIGHT },
+        { JOB_PCB_RENDER::SIDE::FRONT, VIEW3D_TYPE::VIEW3D_FRONT },
+        { JOB_PCB_RENDER::SIDE::BACK, VIEW3D_TYPE::VIEW3D_BACK },
+    };
+
+    PROJECTION_TYPE projection =
+            aRenderJob->m_perspective ? PROJECTION_TYPE::PERSPECTIVE : PROJECTION_TYPE::ORTHO;
+
+    wxSize     windowSize( aRenderJob->m_width, aRenderJob->m_height );
+    TRACK_BALL camera( 2 * RANGE_SCALE_3D );
+
+    camera.SetProjection( projection );
+    camera.SetCurWindowSize( windowSize );
+
+    RENDER_3D_RAYTRACE_RAM raytrace( m_boardAdapter, camera );
+    raytrace.SetCurWindowSize( windowSize );
+
+    for( bool first = true; raytrace.Redraw( false, m_reporter, m_reporter ); first = false )
+    {
+        if( first )
+        {
+            const float cmTo3D = m_boardAdapter.BiuTo3dUnits() * pcbIUScale.mmToIU( 10.0 );
+
+            // First redraw resets lookat point to the board center, so set up the camera here
+            camera.ViewCommand_T1( s_viewCmdMap[aRenderJob->m_side] );
+
+            camera.SetLookAtPos_T1(
+                    camera.GetLookAtPos_T1()
+                    + SFVEC3F( aRenderJob->m_pivot.x, aRenderJob->m_pivot.y, aRenderJob->m_pivot.z )
+                              * cmTo3D );
+
+            camera.Pan_T1(
+                    SFVEC3F( aRenderJob->m_pan.x, aRenderJob->m_pan.y, aRenderJob->m_pan.z ) );
+
+            camera.Zoom_T1( aRenderJob->m_zoom );
+
+            camera.RotateX_T1( DEG2RAD( aRenderJob->m_rotation.x ) );
+            camera.RotateY_T1( DEG2RAD( aRenderJob->m_rotation.y ) );
+            camera.RotateZ_T1( DEG2RAD( aRenderJob->m_rotation.z ) );
+
+            camera.Interpolate( 1.0f );
+            camera.SetT0_and_T1_current_T();
+            camera.ParametersChanged();
+        }
+    }
+
+    GLubyte* rgbaBuffer = raytrace.GetBuffer();
+    wxSize   realSize = raytrace.GetRealBufferSize();
+    bool     success = !!rgbaBuffer;
+
+    if( rgbaBuffer )
+    {
+        const unsigned int wxh = realSize.x * realSize.y;
+
+        unsigned char* rgbBuffer = (unsigned char*) malloc( wxh * 3 );
+        unsigned char* alphaBuffer = (unsigned char*) malloc( wxh );
+
+        unsigned char* rgbaPtr = rgbaBuffer;
+        unsigned char* rgbPtr = rgbBuffer;
+        unsigned char* alphaPtr = alphaBuffer;
+
+        for( int y = 0; y < realSize.y; y++ )
+        {
+            for( int x = 0; x < realSize.x; x++ )
+            {
+                rgbPtr[0] = rgbaPtr[0];
+                rgbPtr[1] = rgbaPtr[1];
+                rgbPtr[2] = rgbaPtr[2];
+                alphaPtr[0] = rgbaPtr[3];
+
+                rgbaPtr += 4;
+                rgbPtr += 3;
+                alphaPtr += 1;
+            }
+        }
+
+        wxImage image( realSize );
+        image.SetData( rgbBuffer );
+        image.SetAlpha( alphaBuffer );
+        image = image.Mirror( false );
+
+        image.SetOption( wxIMAGE_OPTION_QUALITY, 90 );
+        image.SaveFile( aRenderJob->m_outputFile,
+                        aRenderJob->m_format == JOB_PCB_RENDER::FORMAT::PNG ? wxBITMAP_TYPE_PNG
+                                                                            : wxBITMAP_TYPE_JPEG );
+    }
+
+    m_reporter->Report( wxString::Format( _( "Actual image size: %dx%d" ), realSize.x, realSize.y )
+                                + wxS( "\n" ),
+                        RPT_SEVERITY_INFO );
+
+    if( success )
+        m_reporter->Report( _( "Successfully created 3D render image" ) + wxS( "\n" ),
+                            RPT_SEVERITY_INFO );
+    else
+        m_reporter->Report( _( "Error creating 3D render image" ) + wxS( "\n" ),
+                            RPT_SEVERITY_ERROR );
+
+    return CLI::EXIT_CODES::OK;
+}
+
+
 int PCBNEW_JOBS_HANDLER::JobExportSvg( JOB* aJob )
 {
     JOB_EXPORT_PCB_SVG* aSvgJob = dynamic_cast<JOB_EXPORT_PCB_SVG*>( aJob );
diff --git a/pcbnew/pcbnew_jobs_handler.h b/pcbnew/pcbnew_jobs_handler.h
index 33f7019099..39e50eda71 100644
--- a/pcbnew/pcbnew_jobs_handler.h
+++ b/pcbnew/pcbnew_jobs_handler.h
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
- * Copyright (C) 1992-2023 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software: you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -36,6 +36,7 @@ class PCBNEW_JOBS_HANDLER : public JOB_DISPATCHER
 public:
     PCBNEW_JOBS_HANDLER( KIWAY* aKiway );
     int JobExportStep( JOB* aJob );
+    int JobExportRender( JOB* aJob );
     int JobExportSvg( JOB* aJob );
     int JobExportDxf( JOB* aJob );
     int JobExportPdf( JOB* aJob );