Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,40 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Add pybind11
add_subdirectory(external/pybind11)
# Add pybind11 (use submodule if available, otherwise fetch)
include(FetchContent)
set(PYBIND11_SOURCE_DIR ${CMAKE_SOURCE_DIR}/external/pybind11)
if(EXISTS ${PYBIND11_SOURCE_DIR}/CMakeLists.txt)
add_subdirectory(${PYBIND11_SOURCE_DIR})
else()
message(WARNING "pybind11 submodule not found, fetching release archive")
FetchContent_Declare(
pybind11
URL https://github.com/pybind/pybind11/archive/refs/tags/v2.12.0.tar.gz
)
FetchContent_MakeAvailable(pybind11)
endif()

find_package(OpenGL REQUIRED)

# Core C++ library (static library for standalone testing)
add_library(rc_car_core STATIC
cpp/src/math_operations.cpp
cpp/src/renderer3d.cpp
)

target_include_directories(rc_car_core PUBLIC
${CMAKE_SOURCE_DIR}/cpp/include
)

target_link_libraries(rc_car_core PUBLIC OpenGL::GL)

# Python module (DLL/shared library)
pybind11_add_module(rc_car_cpp
cpp/src/bindings.cpp
)

target_link_libraries(rc_car_cpp PRIVATE rc_car_core)
target_link_libraries(rc_car_cpp PRIVATE rc_car_core OpenGL::GL)

target_include_directories(rc_car_cpp PRIVATE
${CMAKE_SOURCE_DIR}/cpp/include
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ GUI for RC car telemetry and controls with high-performance C++ extensions.
## Features
- PyQt6-based graphical interface for RC car control
- High-performance C++ extensions via Pybind11 for robotics calculations
- OpenGL-backed 3D visualization widget with Python bindings
- CMake-based build system with MSVC support
- Standalone C++ testing capabilities

Expand All @@ -20,6 +21,7 @@ GUI for RC car telemetry and controls with high-performance C++ extensions.
- CMake 3.15 or higher
- Microsoft Visual Studio (with C++ Desktop Development workload) on Windows
- GCC or Clang on Linux/macOS
- OpenGL development libraries (e.g., `libgl1-mesa-dev` and `libglu1-mesa-dev` on Linux)

## Installation

Expand Down
36 changes: 36 additions & 0 deletions cpp/include/renderer3d.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

#include <array>

namespace rc_car {

/**
* Minimal OpenGL-based renderer for showcasing 3D content.
*
* Rendering calls assume an active OpenGL context (provided by the caller).
*/
class Renderer3D {
public:
Renderer3D();

/**
* Set the clear color used before drawing.
*/
void setClearColor(float r, float g, float b, float a = 1.0f);

/**
* Render a colored cube using the current OpenGL context.
*
* @param angleXDeg Rotation around the X axis in degrees.
* @param angleYDeg Rotation around the Y axis in degrees.
* @param distance Camera distance from the object.
* @param width Viewport width in pixels.
* @param height Viewport height in pixels.
*/
void renderCube(float angleXDeg, float angleYDeg, float distance, int width, int height) const;

private:
std::array<float, 4> clearColor_;
};

} // namespace rc_car
26 changes: 20 additions & 6 deletions cpp/src/bindings.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <pybind11/pybind11.h>
#include "math_operations.h"
#include "renderer3d.h"

namespace py = pybind11;

Expand All @@ -15,12 +16,25 @@ PYBIND11_MODULE(rc_car_cpp, m) {
py::arg("x2"), py::arg("y2"), py::arg("z2"),
"Calculate angle between two 3D vectors in radians")
.def_static("normalize_vector",
[](double x, double y, double z) {
rc_car::MathOperations::normalizeVector(x, y, z);
return py::make_tuple(x, y, z);
},
py::arg("x"), py::arg("y"), py::arg("z"),
"Normalize a 3D vector, returns (x, y, z) tuple");
[](double x, double y, double z) {
rc_car::MathOperations::normalizeVector(x, y, z);
return py::make_tuple(x, y, z);
},
py::arg("x"), py::arg("y"), py::arg("z"),
"Normalize a 3D vector, returns (x, y, z) tuple");

py::class_<rc_car::Renderer3D>(m, "Renderer3D")
.def(py::init<>())
.def("set_clear_color", &rc_car::Renderer3D::setClearColor,
py::arg("r"), py::arg("g"), py::arg("b"), py::arg("a") = 1.0f,
"Set the clear color used for the framebuffer")
.def("render_cube", &rc_car::Renderer3D::renderCube,
py::arg("angle_x_deg") = 25.0f,
py::arg("angle_y_deg") = 35.0f,
py::arg("distance") = 4.0f,
py::arg("width") = 640,
py::arg("height") = 480,
"Render a colored cube in the active OpenGL context");

// Version information
m.attr("__version__") = "1.0.0";
Expand Down
102 changes: 102 additions & 0 deletions cpp/src/renderer3d.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include "renderer3d.h"
#include <algorithm>

#ifdef _WIN32
#include <windows.h>
#include <GL/gl.h>
#elif defined(__APPLE__)
#include <OpenGL/gl.h>
#else
#include <GL/gl.h>
#endif

namespace rc_car {

namespace {
float clamp01(float v) {
return std::max(0.0f, std::min(1.0f, v));
}
} // namespace

Renderer3D::Renderer3D() : clearColor_{0.05f, 0.09f, 0.14f, 1.0f} {}

void Renderer3D::setClearColor(float r, float g, float b, float a) {
clearColor_ = {
clamp01(r),
clamp01(g),
clamp01(b),
clamp01(a)
};
}

void Renderer3D::renderCube(float angleXDeg, float angleYDeg, float distance, int width, int height) const {
if (width <= 0 || height <= 0) {
return;
}

glViewport(0, 0, width, height);
glEnable(GL_DEPTH_TEST);

glClearColor(clearColor_[0], clearColor_[1], clearColor_[2], clearColor_[3]);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

const float aspect = static_cast<float>(width) / static_cast<float>(height);
glFrustum(-aspect, aspect, -1.0, 1.0, 1.5, 20.0);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -distance);
glRotatef(angleXDeg, 1.0f, 0.0f, 0.0f);
glRotatef(angleYDeg, 0.0f, 1.0f, 0.0f);

glBegin(GL_QUADS);

// Front face (red)
glColor3f(0.84f, 0.27f, 0.27f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);

// Back face (cyan)
glColor3f(0.16f, 0.67f, 0.84f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);

// Left face (green)
glColor3f(0.23f, 0.82f, 0.39f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);

// Right face (yellow)
glColor3f(0.96f, 0.82f, 0.26f);
glVertex3f(1.0f, -1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 1.0f);

// Top face (purple)
glColor3f(0.67f, 0.34f, 0.90f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);

// Bottom face (blue)
glColor3f(0.11f, 0.56f, 0.83f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);

glEnd();
}

} // namespace rc_car
15 changes: 15 additions & 0 deletions src/ui/MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ui.TelemetryWindow import VehicleTelemetryWindow
from ui.VideoStreamingWindow import VideoStreamingWindow
from ui.VisualizationWindow import VisualizationWindow
from ui.UIConsumer import BackendIface
from ui.FirmwareUpdateWindow import FirmwareUpdateWindow
from ui.theme import make_card
Expand Down Expand Up @@ -53,6 +54,7 @@ class SidePanel(QFrame):
showWelcome = pyqtSignal() # Show welcome
showTlm = pyqtSignal() # Show telemetry signal
showVideoStream = pyqtSignal() # Show video stream
showVisualizer = pyqtSignal() # Show 3D visualizer

def __init__(self, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -108,6 +110,10 @@ def __init__(self, parent=None):
self.btnVideo.setToolTip("Open video stream")
self.btnVideo.setIcon(QIcon("icons/video.svg"))

self.btn3d = GlowButton(" 3D View")
self.btn3d.setToolTip("Open 3D visualization")
self.btn3d.setIcon(QIcon("icons/rc-car.png"))

self.btnFw = GlowButton(" Firmware")
self.btnFw.setIcon(QIcon("icons/upgrade.svg"))
self.btnFw.setToolTip("Upload Firmware")
Expand All @@ -119,6 +125,7 @@ def __init__(self, parent=None):
layout.addWidget(self.btnWelcome)
layout.addWidget(self.btnTelem)
layout.addWidget(self.btnVideo)
layout.addWidget(self.btn3d)
layout.addWidget(self.btnFw)
layout.addWidget(self.btnGPS)

Expand Down Expand Up @@ -157,6 +164,7 @@ def __connectSignals(self) -> None:
self.btnWelcome.clicked.connect(lambda: self.showWelcome.emit())
self.btnTelem.clicked.connect(lambda: self.showTlm.emit())
self.btnVideo.clicked.connect(lambda: self.showVideoStream.emit())
self.btn3d.clicked.connect(lambda: self.showVisualizer.emit())


# ----------------------------
Expand Down Expand Up @@ -553,6 +561,7 @@ def __init__(self):
self.__tlmWindow = VehicleTelemetryWindow(self)
self.__streamWindow = VideoStreamingWindow()
self.__fwWindow = FirmwareUpdateWindow(self)
self.__vizWindow = VisualizationWindow(self)

# Side panel
self.side = SidePanel(self)
Expand Down Expand Up @@ -587,6 +596,7 @@ def __connectSignals(self):
self.side.showWelcome.connect(self.__showWelcome)
self.side.showTlm.connect(self.__showTlm)
self.side.showVideoStream.connect(self.__showVideo)
self.side.showVisualizer.connect(self.__showVisualizer)
self.__welcomeWindow.startRequested.connect(self.__onDiscoveryStart)
# When a device is discovered, show it on the welcome window
self.__consumer.deviceDiscovered.connect(self.__onDeviceDiscovered)
Expand Down Expand Up @@ -786,6 +796,11 @@ def __showVideo(self) -> None:
self.__setContent(self.__streamWindow)


def __showVisualizer(self) -> None:
"""Show the 3D visualization window."""
self.__setContent(self.__vizWindow)



def __showFirmware(self) -> None:
"""Show the firmware update panel in the central area."""
Expand Down
Loading