Welcome to this step-by-step tutorial on rendering a 3D rotating sphere using the Raspberry Pi Pico with HDMI output! If you're a hobbyist or a student eager to dive into graphics programming on microcontrollers, you're in the right place. We'll walk you through a program that not only displays a rotating 3D sphere but also performs a benchmark to determine how many polygons it can render at 30 frames per second (FPS).
By the end of this tutorial, you will:
To see the program in action immediately, copy the following code and paste it into your Arduino IDE:
// Include necessary headers
#include <PicoDVI.h>
#include <math.h>
#include <vector>
#include <algorithm> // For sorting polygons
// ------------------------------ Constants and Macros ------------------------------
// Screen dimensions
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
// Perspective projection parameters
#define FOV 60.0 // Field of view for perspective projection
#define ASPECT_RATIO (SCREEN_WIDTH / (float)SCREEN_HEIGHT) // Aspect ratio of the display
#define NEAR_PLANE 0.1f // Near clipping plane
#define FAR_PLANE 1000.0f // Far clipping plane
// ------------------------------ Display Initialization ------------------------------
// Create display object with specified resolution and settings
DVIGFX8 display(DVI_RES_320x240p60, true, pico_sock_cfg);
// ------------------------------ Helper Structs and Functions ------------------------------
/**
* @brief Represents a 3D vector or point
*/
struct Vector3 {
float x, y, z;
};
/**
* @brief Represents a 2D point on the screen
*/
struct Point2D {
float x, y;
};
/**
* @brief Rotates a 3D point by given rotation angles around the X, Y, and Z axes
* @param point The 3D point to rotate
* @param angles The rotation angles (in radians) around the X, Y, and Z axes
* @return The rotated 3D point
*/
Vector3 rotatePoint(Vector3 point, Vector3 angles) {
// Rotate around X axis
float cosX = cos(angles.x);
float sinX = sin(angles.x);
float y = point.y * cosX - point.z * sinX;
float z = point.y * sinX + point.z * cosX;
point.y = y;
point.z = z;
// Rotate around Y axis
float cosY = cos(angles.y);
float sinY = sin(angles.y);
float x = point.z * sinY + point.x * cosY;
point.z = point.z * cosY - point.x * sinY;
point.x = x;
// Rotate around Z axis
float cosZ = cos(angles.z);
float sinZ = sin(angles.z);
x = point.x * cosZ - point.y * sinZ;
y = point.x * sinZ + point.y * cosZ;
point.x = x;
point.y = y;
return point;
}
// ------------------------------ Classes ------------------------------
/**
* @brief Camera class to handle camera position and rotation
*/
class Camera {
public:
Vector3 position; // Camera position in world space
Vector3 rotation; // Camera rotation angles
/**
* @brief Constructor for the Camera class
* @param position Initial position of the camera
* @param rotation Initial rotation of the camera
*/
Camera(Vector3 position, Vector3 rotation) : position(position), rotation(rotation) {}
/**
* @brief Projects a 3D point onto the 2D screen using perspective projection
* @param v The 3D point in world space
* @return The 2D point on the screen
*/
Point2D project(Vector3 v) {
// Translate point to camera space (relative to camera position)
Vector3 translated = {v.x - position.x, v.y - position.y, v.z - position.z};
// Apply camera rotation
Vector3 rotated = rotatePoint(translated, rotation);
// Apply perspective projection
if (rotated.z <= NEAR_PLANE) rotated.z = NEAR_PLANE; // Avoid division by zero or negative depth
float factor = FOV / (FOV + rotated.z);
return Point2D{
rotated.x * factor * ASPECT_RATIO + SCREEN_WIDTH / 2.0f,
rotated.y * factor + SCREEN_HEIGHT / 2.0f
};
}
};
/**
* @brief Polygon class representing a quadrilateral face of the sphere
*/
class Polygon {
private:
Vector3 vertices[4]; // The four vertices of the polygon (quadrilateral)
uint8_t outlineColor; // Color index for the outline
uint8_t fillColor; // Color index for the fill
public:
/**
* @brief Constructor for the Polygon class
* @param v0, v1, v2, v3 The four vertices of the quadrilateral
* @param outlineColor The color index for the outline
* @param fillColor The color index for the fill
*/
Polygon(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, uint8_t outlineColor, uint8_t fillColor)
: outlineColor(outlineColor), fillColor(fillColor) {
vertices[0] = v0;
vertices[1] = v1;
vertices[2] = v2;
vertices[3] = v3;
}
/**
* @brief Computes the average depth (z-value) of the polygon after rotation
* @param rotation The rotation angles to apply
* @param objectCenter The center of the object (sphere)
* @return The average depth value
*/
float averageDepth(Vector3 rotation, Vector3 objectCenter) {
float avgZ = 0;
for (int i = 0; i < 4; i++) {
// Compute vertex position relative to object center
Vector3 relativeVertex = {
vertices[i].x - objectCenter.x,
vertices[i].y - objectCenter.y,
vertices[i].z - objectCenter.z
};
// Rotate the vertex
Vector3 rotatedVertex = rotatePoint(relativeVertex, rotation);
// Accumulate the z-value
avgZ += rotatedVertex.z;
}
return avgZ / 4.0f;
}
/**
* @brief Draws the polygon onto the display using the given camera and rotation
* @param camera The camera object for projection
* @param rotation The rotation angles to apply
* @param objectCenter The center of the object (sphere)
*/
void draw(Camera& camera, Vector3 rotation, Vector3 objectCenter) {
// Rotate all vertices around the object's center
Vector3 rotatedVertices[4];
for (int i = 0; i < 4; i++) {
// Compute vertex position relative to object center
Vector3 relativeVertex = {
vertices[i].x - objectCenter.x,
vertices[i].y - objectCenter.y,
vertices[i].z - objectCenter.z
};
// Rotate the vertex
rotatedVertices[i] = rotatePoint(relativeVertex, rotation);
// Translate back to world position
rotatedVertices[i].x += objectCenter.x;
rotatedVertices[i].y += objectCenter.y;
rotatedVertices[i].z += objectCenter.z;
}
// Project the rotated vertices into 2D screen space
Point2D projected[4];
for (int i = 0; i < 4; i++) {
projected[i] = camera.project(rotatedVertices[i]);
}
// Draw the polygon as two triangles to fill the quadrilateral
display.fillTriangle(
projected[0].x, projected[0].y,
projected[1].x, projected[1].y,
projected[2].x, projected[2].y,
fillColor
);
display.fillTriangle(
projected[2].x, projected[2].y,
projected[3].x, projected[3].y,
projected[0].x, projected[0].y,
fillColor
);
// Draw the outline of the polygon
display.drawLine(projected[0].x, projected[0].y, projected[1].x, projected[1].y, outlineColor);
display.drawLine(projected[1].x, projected[1].y, projected[2].x, projected[2].y, outlineColor);
display.drawLine(projected[2].x, projected[2].y, projected[3].x, projected[3].y, outlineColor);
display.drawLine(projected[3].x, projected[3].y, projected[0].x, projected[0].y, outlineColor);
}
};
/**
* @brief Sphere class representing a 3D sphere made up of polygons
*/
class Sphere {
private:
std::vector faces; // Collection of polygon faces composing the sphere
public:
/**
* @brief Constructor for the Sphere class
* @param center The center position of the sphere
* @param radius The radius of the sphere
* @param slices Number of longitudinal slices (divisions around Y-axis)
* @param stacks Number of latitudinal stacks (divisions along Y-axis)
* @param faceColors Vector of color indices for face fill colors
* @param outlineColors Vector of color indices for face outline colors
*/
Sphere(Vector3 center, float radius, int slices, int stacks, std::vector faceColors, std::vector outlineColors) {
// Generate sphere faces
for (int i = 0; i < stacks; ++i) {
// Calculate latitude angles
float theta1 = (i * M_PI) / stacks - M_PI / 2;
float theta2 = ((i + 1) * M_PI) / stacks - M_PI / 2;
for (int j = 0; j < slices; ++j) {
// Calculate longitude angles
float phi1 = (j * 2 * M_PI) / slices;
float phi2 = ((j + 1) * 2 * M_PI) / slices;
// Compute vertices of the quadrilateral
Vector3 v0 = {
center.x + radius * cos(theta1) * cos(phi1),
center.y + radius * sin(theta1),
center.z + radius * cos(theta1) * sin(phi1)
};
Vector3 v1 = {
center.x + radius * cos(theta1) * cos(phi2),
center.y + radius * sin(theta1),
center.z + radius * cos(theta1) * sin(phi2)
};
Vector3 v2 = {
center.x + radius * cos(theta2) * cos(phi2),
center.y + radius * sin(theta2),
center.z + radius * cos(theta2) * sin(phi2)
};
Vector3 v3 = {
center.x + radius * cos(theta2) * cos(phi1),
center.y + radius * sin(theta2),
center.z + radius * cos(theta2) * sin(phi1)
};
// Create the polygon for this face
faces.emplace_back(Polygon(
v0, v1, v2, v3,
outlineColors[i % outlineColors.size()],
faceColors[i % faceColors.size()]
));
}
}
}
/**
* @brief Draws the sphere onto the display using the given camera and rotation
* @param camera The camera object for projection
* @param rotation The rotation angles to apply
*/
void draw(Camera& camera, Vector3 rotation) {
Vector3 sphereCenter = {0, 0, 0}; // Sphere's center
// Sort the faces based on their average depth (farther faces drawn first)
std::sort(faces.begin(), faces.end(), [&](Polygon& a, Polygon& b) {
return a.averageDepth(rotation, sphereCenter) > b.averageDepth(rotation, sphereCenter);
});
// Draw the sorted faces
for (auto& face : faces) {
face.draw(camera, rotation, sphereCenter);
}
}
};
// ------------------------------ Global Variables ------------------------------
// Initialize the camera with a position and rotation
Camera camera(
Vector3{0, 0, -150}, // Camera positioned along the negative Z-axis
Vector3{0, 0, 0} // No initial rotation
);
// Define face and outline colors for the sphere
std::vector faceColors = {4, 6, 8, 10, 12, 14}; // Red, Green, Blue, Yellow, Magenta, Cyan
std::vector outlineColors = {16, 17, 18, 19, 20, 21}; // Dark Red, Dark Green, Dark Blue, Dark Yellow, Dark Magenta, Dark Cyan
// Initialize the sphere with initial slices and stacks
Sphere sphere(
Vector3{0, 0, 0}, // Sphere centered at the origin
40, // Radius of the sphere
4, // Initial number of slices
4, // Initial number of stacks
faceColors,
outlineColors
);
// Variables for rotation angles
float angleX = 0;
float angleY = 0;
float angleZ = 0;
// Benchmarking variables
unsigned long startTime = millis();
int frameCount = 0;
int slices = 4;
int stacks = 4;
bool benchmarkComplete = false;
float avgFPS = 0;
unsigned long lastFPSTime = millis();
int fpsFrameCount = 0;
float fps = 0;
// ------------------------------ Setup Function ------------------------------
/**
* @brief Arduino setup function called once at startup
*/
void setup() {
// Initialize the display
if (!display.begin()) {
// If initialization fails, blink the built-in LED as an error indicator
pinMode(LED_BUILTIN, OUTPUT);
for (;;) digitalWrite(LED_BUILTIN, (millis() / 500) & 1);
}
// Define a 32-color VGA palette
display.setColor(0, 0, 0, 0); // Color 0: Black
display.setColor(1, 255, 255, 255); // Color 1: White
display.setColor(2, 128, 128, 128); // Color 2: Dark Gray
display.setColor(3, 192, 192, 192); // Color 3: Light Gray
display.setColor(4, 255, 0, 0); // Color 4: Red
display.setColor(5, 255, 128, 128); // Color 5: Light Red
display.setColor(6, 0, 255, 0); // Color 6: Green
display.setColor(7, 128, 255, 128); // Color 7: Light Green
display.setColor(8, 0, 0, 255); // Color 8: Blue
display.setColor(9, 128, 128, 255); // Color 9: Light Blue
display.setColor(10, 255, 255, 0); // Color 10: Yellow
display.setColor(11, 255, 255, 128); // Color 11: Light Yellow
display.setColor(12, 255, 0, 255); // Color 12: Magenta
display.setColor(13, 255, 128, 255); // Color 13: Light Magenta
display.setColor(14, 0, 255, 255); // Color 14: Cyan
display.setColor(15, 128, 255, 255); // Color 15: Light Cyan
// Additional 16 colors
display.setColor(16, 128, 0, 0); // Color 16: Dark Red
display.setColor(17, 0, 128, 0); // Color 17: Dark Green
display.setColor(18, 0, 0, 128); // Color 18: Dark Blue
display.setColor(19, 128, 128, 0); // Color 19: Dark Yellow
display.setColor(20, 128, 0, 128); // Color 20: Dark Magenta
display.setColor(21, 0, 128, 128); // Color 21: Dark Cyan
display.setColor(22, 192, 0, 0); // Color 22: Darker Red
display.setColor(23, 0, 192, 0); // Color 23: Darker Green
display.setColor(24, 0, 0, 192); // Color 24: Darker Blue
display.setColor(25, 192, 192, 0); // Color 25: Darker Yellow
display.setColor(26, 192, 0, 192); // Color 26: Darker Magenta
display.setColor(27, 0, 192, 192); // Color 27: Darker Cyan
display.setColor(28, 64, 0, 0); // Color 28: Very Dark Red
display.setColor(29, 0, 64, 0); // Color 29: Very Dark Green
display.setColor(30, 0, 0, 64); // Color 30: Very Dark Blue
display.setColor(31, 64, 64, 64); // Color 31: Very Dark Gray
// Enable double buffering for smooth animations
display.swap(false, true);
}
// ------------------------------ Main Loop Function ------------------------------
/**
* @brief Arduino main loop function called repeatedly after setup()
*/
void loop() {
unsigned long currentTime = millis();
// Update rotation angles
angleX += 0.02f;
angleY += 0.01f;
angleZ += 0.015f;
Vector3 rotation = {angleX, angleY, angleZ};
// Clear the screen to black
display.fillScreen(0);
// Render the sphere
sphere.draw(camera, rotation);
// Increment frame counts for FPS calculation
frameCount++;
fpsFrameCount++;
// Calculate FPS every second
if (currentTime - lastFPSTime >= 1000) {
fps = fpsFrameCount * 1000.0f / (currentTime - lastFPSTime);
fpsFrameCount = 0;
lastFPSTime = currentTime;
}
// Display the current FPS on the screen
display.setCursor(240, 5); // Position at the top-right corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("FPS: %.2f", fps);
if (!benchmarkComplete) {
// Display countdown timer during benchmarking
int timeLeft = 10 - ((currentTime - startTime) / 1000);
if (timeLeft < 0) timeLeft = 0;
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("Time Left: %d s", timeLeft);
// After 10 seconds, calculate average FPS
if ((currentTime - startTime) >= 10000) {
avgFPS = (float)frameCount * 1000.0f / (currentTime - startTime);
if (avgFPS < 30.0f) {
// Benchmark complete
benchmarkComplete = true;
int numPolygons = slices * stacks * 2; // Each face is two polygons
// Replace "Time Left" with the final result
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("Max Polygons at 30 FPS: %d", numPolygons);
} else {
// Increase slices and stacks to increase complexity
slices++;
stacks++;
// Recreate sphere with increased complexity
sphere = Sphere(
Vector3{0, 0, 0}, // Center of the sphere
40, // Radius of the sphere
slices,
stacks,
faceColors,
outlineColors
);
// Reset frame counts and start time for the next benchmark
frameCount = 0;
startTime = currentTime;
}
}
} else {
// Benchmark is complete; display the final result
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
int numPolygons = slices * stacks * 2; // Each face is two polygons
display.printf("Max Polygons at 30 FPS: %d", numPolygons);
}
// Swap buffers to display the rendered frame
display.swap();
}
Let's delve into the code to understand how it works. We'll go through each section, explaining the purpose and functionality in simple terms.
First, it's essential to understand that the Raspberry Pi Pico doesn't output true HDMI signals. Instead, it uses DVI (Digital Visual Interface) signals, which are compatible with HDMI connectors. This means you can connect your Pico to an HDMI display, but the signal it's sending is actually DVI. The PicoDVI library handles this conversion, allowing us to output graphics to modern displays.
#include <PicoDVI.h>
#include <math.h>
#include <vector>
#include <algorithm> // For sorting polygons
std::sort
, which we'll use to sort polygons based on depth for proper rendering.#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FOV 60.0
#define ASPECT_RATIO (SCREEN_WIDTH / (float)SCREEN_HEIGHT)
#define NEAR_PLANE 0.1f
#define FAR_PLANE 1000.0f
DVIGFX8 display(DVI_RES_320x240p60, true, pico_sock_cfg);
struct Vector3 {
float x, y, z;
};
struct Point2D {
float x, y;
};
Vector3 rotatePoint(Vector3 point, Vector3 angles) {
// Rotate around X axis
float cosX = cos(angles.x);
float sinX = sin(angles.x);
float y = point.y * cosX - point.z * sinX;
float z = point.y * sinX + point.z * cosX;
point.y = y;
point.z = z;
// Rotate around Y axis
float cosY = cos(angles.y);
float sinY = sin(angles.y);
float x = point.z * sinY + point.x * cosY;
point.z = point.z * cosY - point.x * sinY;
point.x = x;
// Rotate around Z axis
float cosZ = cos(angles.z);
float sinZ = sin(angles.z);
x = point.x * cosZ - point.y * sinZ;
y = point.x * sinZ + point.y * cosZ;
point.x = x;
point.y = y;
return point;
}
Purpose: Rotates a 3D point around the X, Y, and Z axes by the specified angles.
Process:
Uses: Applies rotation transformations to points in our 3D space.
class Camera {
public:
Vector3 position; // Camera position in world space
Vector3 rotation; // Camera rotation angles
// Constructor for the Camera class
Camera(Vector3 position, Vector3 rotation) : position(position), rotation(rotation) {}
// Projects a 3D point onto the 2D screen using perspective projection
Point2D project(Vector3 v) {
// Translate point to camera space (relative to camera position)
Vector3 translated = {v.x - position.x, v.y - position.y, v.z - position.z};
// Apply camera rotation
Vector3 rotated = rotatePoint(translated, rotation);
// Apply perspective projection
if (rotated.z <= NEAR_PLANE) rotated.z = NEAR_PLANE; // Avoid division by zero or negative depth
float factor = FOV / (FOV + rotated.z);
return Point2D{
rotated.x * factor * ASPECT_RATIO + SCREEN_WIDTH / 2.0f,
rotated.y * factor + SCREEN_HEIGHT / 2.0f
};
}
};
class Polygon {
private:
Vector3 vertices[4]; // The four vertices of the polygon (quadrilateral)
uint8_t outlineColor; // Color index for the outline
uint8_t fillColor; // Color index for the fill
public:
// Constructor for the Polygon class
Polygon(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, uint8_t outlineColor, uint8_t fillColor)
: outlineColor(outlineColor), fillColor(fillColor) {
vertices[0] = v0;
vertices[1] = v1;
vertices[2] = v2;
vertices[3] = v3;
}
// Computes the average depth (z-value) of the polygon after rotation
float averageDepth(Vector3 rotation, Vector3 objectCenter) {
float avgZ = 0;
for (int i = 0; i < 4; i++) {
// Compute vertex position relative to object center
Vector3 relativeVertex = {
vertices[i].x - objectCenter.x,
vertices[i].y - objectCenter.y,
vertices[i].z - objectCenter.z
};
// Rotate the vertex
Vector3 rotatedVertex = rotatePoint(relativeVertex, rotation);
// Accumulate the z-value
avgZ += rotatedVertex.z;
}
return avgZ / 4.0f;
}
// Draws the polygon onto the display using the given camera and rotation
void draw(Camera& camera, Vector3 rotation, Vector3 objectCenter) {
// Rotate all vertices around the object's center
Vector3 rotatedVertices[4];
for (int i = 0; i < 4; i++) {
// Compute vertex position relative to object center
Vector3 relativeVertex = {
vertices[i].x - objectCenter.x,
vertices[i].y - objectCenter.y,
vertices[i].z - objectCenter.z
};
// Rotate the vertex
rotatedVertices[i] = rotatePoint(relativeVertex, rotation);
// Translate back to world position
rotatedVertices[i].x += objectCenter.x;
rotatedVertices[i].y += objectCenter.y;
rotatedVertices[i].z += objectCenter.z;
}
// Project the rotated vertices into 2D screen space
Point2D projected[4];
for (int i = 0; i < 4; i++) {
projected[i] = camera.project(rotatedVertices[i]);
}
// Draw the polygon as two triangles to fill the quadrilateral
display.fillTriangle(
projected[0].x, projected[0].y,
projected[1].x, projected[1].y,
projected[2].x, projected[2].y,
fillColor
);
display.fillTriangle(
projected[2].x, projected[2].y,
projected[3].x, projected[3].y,
projected[0].x, projected[0].y,
fillColor
);
// Draw the outline of the polygon
display.drawLine(projected[0].x, projected[0].y, projected[1].x, projected[1].y, outlineColor);
display.drawLine(projected[1].x, projected[1].y, projected[2].x, projected[2].y, outlineColor);
display.drawLine(projected[2].x, projected[2].y, projected[3].x, projected[3].y, outlineColor);
display.drawLine(projected[3].x, projected[3].y, projected[0].x, projected[0].y, outlineColor);
}
};
fillTriangle
and drawLine
functions to render the filled polygon and its outline.class Sphere {
private:
std::vector faces; // Collection of polygon faces composing the sphere
public:
// Constructor for the Sphere class
Sphere(Vector3 center, float radius, int slices, int stacks, std::vector faceColors, std::vector outlineColors) {
// Generate sphere faces
for (int i = 0; i < stacks; ++i) {
// Calculate latitude angles
float theta1 = (i * M_PI) / stacks - M_PI / 2;
float theta2 = ((i + 1) * M_PI) / stacks - M_PI / 2;
for (int j = 0; j < slices; ++j) {
// Calculate longitude angles
float phi1 = (j * 2 * M_PI) / slices;
float phi2 = ((j + 1) * 2 * M_PI) / slices;
// Compute vertices of the quadrilateral
Vector3 v0 = {
center.x + radius * cos(theta1) * cos(phi1),
center.y + radius * sin(theta1),
center.z + radius * cos(theta1) * sin(phi1)
};
Vector3 v1 = {
center.x + radius * cos(theta1) * cos(phi2),
center.y + radius * sin(theta1),
center.z + radius * cos(theta1) * sin(phi2)
};
Vector3 v2 = {
center.x + radius * cos(theta2) * cos(phi2),
center.y + radius * sin(theta2),
center.z + radius * cos(theta2) * sin(phi2)
};
Vector3 v3 = {
center.x + radius * cos(theta2) * cos(phi1),
center.y + radius * sin(theta2),
center.z + radius * cos(theta2) * sin(phi1)
};
// Create the polygon for this face
faces.emplace_back(Polygon(
v0, v1, v2, v3,
outlineColors[i % outlineColors.size()],
faceColors[i % faceColors.size()]
));
}
}
}
// Draws the sphere onto the display using the given camera and rotation
void draw(Camera& camera, Vector3 rotation) {
Vector3 sphereCenter = {0, 0, 0}; // Sphere's center
// Sort the faces based on their average depth (farther faces drawn first)
std::sort(faces.begin(), faces.end(), [&](Polygon& a, Polygon& b) {
return a.averageDepth(rotation, sphereCenter) > b.averageDepth(rotation, sphereCenter);
});
// Draw the sorted faces
for (auto& face : faces) {
face.draw(camera, rotation, sphereCenter);
}
}
};
draw
method of each Polygon object.// Initialize the camera with a position and rotation
Camera camera(
Vector3{0, 0, -150}, // Camera positioned along the negative Z-axis
Vector3{0, 0, 0} // No initial rotation
);
// Define face and outline colors for the sphere
std::vector faceColors = {4, 6, 8, 10, 12, 14}; // Red, Green, Blue, Yellow, Magenta, Cyan
std::vector outlineColors = {16, 17, 18, 19, 20, 21}; // Dark Red, Dark Green, Dark Blue, Dark Yellow, Dark Magenta, Dark Cyan
// Initialize the sphere with initial slices and stacks
Sphere sphere(
Vector3{0, 0, 0}, // Sphere centered at the origin
40, // Radius of the sphere
4, // Initial number of slices
4, // Initial number of stacks
faceColors,
outlineColors
);
// Variables for rotation angles
float angleX = 0;
float angleY = 0;
float angleZ = 0;
// Benchmarking variables
unsigned long startTime = millis();
int frameCount = 0;
int slices = 4;
int stacks = 4;
bool benchmarkComplete = false;
float avgFPS = 0;
unsigned long lastFPSTime = millis();
int fpsFrameCount = 0;
float fps = 0;
void setup() {
// Initialize the display
if (!display.begin()) {
// If initialization fails, blink the built-in LED as an error indicator
pinMode(LED_BUILTIN, OUTPUT);
for (;;) digitalWrite(LED_BUILTIN, (millis() / 500) & 1);
}
// Define a 32-color VGA palette
display.setColor(0, 0, 0, 0); // Color 0: Black
display.setColor(1, 255, 255, 255); // Color 1: White
display.setColor(2, 128, 128, 128); // Color 2: Dark Gray
display.setColor(3, 192, 192, 192); // Color 3: Light Gray
display.setColor(4, 255, 0, 0); // Color 4: Red
display.setColor(5, 255, 128, 128); // Color 5: Light Red
display.setColor(6, 0, 255, 0); // Color 6: Green
display.setColor(7, 128, 255, 128); // Color 7: Light Green
display.setColor(8, 0, 0, 255); // Color 8: Blue
display.setColor(9, 128, 128, 255); // Color 9: Light Blue
display.setColor(10, 255, 255, 0); // Color 10: Yellow
display.setColor(11, 255, 255, 128); // Color 11: Light Yellow
display.setColor(12, 255, 0, 255); // Color 12: Magenta
display.setColor(13, 255, 128, 255); // Color 13: Light Magenta
display.setColor(14, 0, 255, 255); // Color 14: Cyan
display.setColor(15, 128, 255, 255); // Color 15: Light Cyan
// Additional 16 colors
display.setColor(16, 128, 0, 0); // Color 16: Dark Red
display.setColor(17, 0, 128, 0); // Color 17: Dark Green
display.setColor(18, 0, 0, 128); // Color 18: Dark Blue
display.setColor(19, 128, 128, 0); // Color 19: Dark Yellow
display.setColor(20, 128, 0, 128); // Color 20: Dark Magenta
display.setColor(21, 0, 128, 128); // Color 21: Dark Cyan
display.setColor(22, 192, 0, 0); // Color 22: Darker Red
display.setColor(23, 0, 192, 0); // Color 23: Darker Green
display.setColor(24, 0, 0, 192); // Color 24: Darker Blue
display.setColor(25, 192, 192, 0); // Color 25: Darker Yellow
display.setColor(26, 192, 0, 192); // Color 26: Darker Magenta
display.setColor(27, 0, 192, 192); // Color 27: Darker Cyan
display.setColor(28, 64, 0, 0); // Color 28: Very Dark Red
display.setColor(29, 0, 64, 0); // Color 29: Very Dark Green
display.setColor(30, 0, 0, 64); // Color 30: Very Dark Blue
display.setColor(31, 64, 64, 64); // Color 31: Very Dark Gray
// Enable double buffering for smooth animations
display.swap(false, true);
}
Display Initialization:
display.swap(false, true);
to prevent flickering by drawing frames off-screen before displaying them.void loop() {
unsigned long currentTime = millis();
// Update rotation angles
angleX += 0.02f;
angleY += 0.01f;
angleZ += 0.015f;
Vector3 rotation = {angleX, angleY, angleZ};
// Clear the screen to black
display.fillScreen(0);
// Render the sphere
sphere.draw(camera, rotation);
// Increment frame counts for FPS calculation
frameCount++;
fpsFrameCount++;
// Calculate FPS every second
if (currentTime - lastFPSTime >= 1000) {
fps = fpsFrameCount * 1000.0f / (currentTime - lastFPSTime);
fpsFrameCount = 0;
lastFPSTime = currentTime;
}
// Display the current FPS on the screen
display.setCursor(240, 5); // Position at the top-right corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("FPS: %.2f", fps);
if (!benchmarkComplete) {
// Display countdown timer during benchmarking
int timeLeft = 10 - ((currentTime - startTime) / 1000);
if (timeLeft < 0) timeLeft = 0;
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("Time Left: %d s", timeLeft);
// After 10 seconds, calculate average FPS
if ((currentTime - startTime) >= 10000) {
avgFPS = (float)frameCount * 1000.0f / (currentTime - startTime);
if (avgFPS < 30.0f) {
// Benchmark complete
benchmarkComplete = true;
int numPolygons = slices * stacks * 2; // Each face is two polygons
// Replace "Time Left" with the final result
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
display.printf("Max Polygons at 30 FPS: %d", numPolygons);
} else {
// Increase slices and stacks to increase complexity
slices++;
stacks++;
// Recreate sphere with increased complexity
sphere = Sphere(
Vector3{0, 0, 0}, // Center of the sphere
40, // Radius of the sphere
slices,
stacks,
faceColors,
outlineColors
);
// Reset frame counts and start time for the next benchmark
frameCount = 0;
startTime = currentTime;
}
}
} else {
// Benchmark is complete; display the final result
display.setCursor(5, 5); // Position at the top-left corner
display.setTextColor(1); // White color
display.setTextSize(1);
int numPolygons = slices * stacks * 2; // Each face is two polygons
display.printf("Max Polygons at 30 FPS: %d", numPolygons);
}
// Swap buffers to display the rendered frame
display.swap();
}
Functionality:
By understanding each part of the code, you now have a solid grasp of how 3D rendering works on a microcontroller like the Raspberry Pi Pico using the PicoDVI library. Feel free to experiment with different values for slices, stacks, rotation speeds, and camera positions to see how they affect the rendering and performance. Happy coding!
Happy Benchmarking with Your PicoGamer v1.2 DIY Edition!