Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "visualizations/BarVisualization.h"
#include "visualizations/CircularBarVisualization.h"
#include "visualizations/MountainVisualization.h"
#include "visualizations/Bar3DVisualization.h"

#include <iostream>
#include <memory>
Expand All @@ -25,6 +26,7 @@ int main() {
cout << "2. Bar Visualization\n";
cout << "3. Circular Bar Visualization\n";
cout << "4. Mountain Visualization\n";
cout << "5. 3D Bar Visualization\n";
cout << "Enter choice: ";
cin >> choice;

Expand All @@ -43,6 +45,9 @@ int main() {
case 4:
visualization = make_unique<MountainVisualization>();
break;
case 5:
visualization = make_unique<Bar3DVisualization>();
break;
default:
cout << "Invalid choice. Exiting.\n";
return -1;
Expand Down
299 changes: 299 additions & 0 deletions src/visualizations/Bar3DVisualization.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
#define _USE_MATH_DEFINES
#include "Bar3DVisualization.h"
#include <cmath>
#include <iostream>
#include <algorithm>

// ─── Constructor / Destructor ───────────────────────────────────────────────

Bar3DVisualization::Bar3DVisualization()
: window(nullptr), vbo(0), vao(0), shaderProgram(0),
cameraAngleX(25.0f), cameraAngleY(-35.0f), cameraDistance(8.0f), autoRotation(0.0f),
mouseDragging(false), lastMouseX(0), lastMouseY(0),
smoothedFFT(128, 0.0f) {}

Bar3DVisualization::~Bar3DVisualization() { cleanup(); }

// ─── Callbacks ──────────────────────────────────────────────────────────────

void Bar3DVisualization::framebufferSizeCallback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}

void Bar3DVisualization::mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
auto* self = static_cast<Bar3DVisualization*>(glfwGetWindowUserPointer(window));
if (!self) return;
if (button == GLFW_MOUSE_BUTTON_LEFT) {
self->mouseDragging = (action == GLFW_PRESS);
glfwGetCursorPos(window, &self->lastMouseX, &self->lastMouseY);
}
}

void Bar3DVisualization::cursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
auto* self = static_cast<Bar3DVisualization*>(glfwGetWindowUserPointer(window));
if (!self || !self->mouseDragging) return;
double dx = xpos - self->lastMouseX;
double dy = ypos - self->lastMouseY;
self->cameraAngleY += (float)dx * 0.3f;
self->cameraAngleX += (float)dy * 0.3f;
self->cameraAngleX = std::clamp(self->cameraAngleX, -89.0f, 89.0f);
self->lastMouseX = xpos;
self->lastMouseY = ypos;
}

void Bar3DVisualization::scrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
auto* self = static_cast<Bar3DVisualization*>(glfwGetWindowUserPointer(window));
if (!self) return;
self->cameraDistance -= (float)yoffset * 0.5f;
self->cameraDistance = std::clamp(self->cameraDistance, 3.0f, 20.0f);
}

// ─── Matrix Helpers ─────────────────────────────────────────────────────────

std::vector<float> Bar3DVisualization::perspectiveMatrix(float fov, float aspect, float nearP, float farP) {
float f = 1.0f / tanf(fov * 0.5f * (float)M_PI / 180.0f);
float rangeInv = 1.0f / (nearP - farP);
return {
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (nearP + farP) * rangeInv, -1,
0, 0, 2.0f * nearP * farP * rangeInv, 0
};
}

std::vector<float> Bar3DVisualization::lookAtMatrix(float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ) {
float fX = centerX - eyeX, fY = centerY - eyeY, fZ = centerZ - eyeZ;
float fLen = sqrtf(fX * fX + fY * fY + fZ * fZ);
fX /= fLen; fY /= fLen; fZ /= fLen;

float sX = fY * upZ - fZ * upY;
float sY = fZ * upX - fX * upZ;
float sZ = fX * upY - fY * upX;
float sLen = sqrtf(sX * sX + sY * sY + sZ * sZ);
sX /= sLen; sY /= sLen; sZ /= sLen;

float uX = sY * fZ - sZ * fY;
float uY = sZ * fX - sX * fZ;
float uZ = sX * fY - sY * fX;

return {
sX, uX, -fX, 0,
sY, uY, -fY, 0,
sZ, uZ, -fZ, 0,
-(sX * eyeX + sY * eyeY + sZ * eyeZ),
-(uX * eyeX + uY * eyeY + uZ * eyeZ),
(fX * eyeX + fY * eyeY + fZ * eyeZ),
1
};
}

std::vector<float> Bar3DVisualization::multiplyMat4(const std::vector<float>& a, const std::vector<float>& b) {
std::vector<float> result(16, 0.0f);
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
for (int k = 0; k < 4; k++) {
result[col * 4 + row] += a[k * 4 + row] * b[col * 4 + k];
}
}
}
return result;
}

// ─── Initialize ─────────────────────────────────────────────────────────────

bool Bar3DVisualization::initialize(int windowWidth, int windowHeight) {
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW." << std::endl;
return false;
}

window = glfwCreateWindow(windowWidth, windowHeight, "3D Bar Visualization", nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window." << std::endl;
glfwTerminate();
return false;
}

glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW." << std::endl;
return false;
}

glfwSetWindowUserPointer(window, this);
glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
glfwSetMouseButtonCallback(window, mouseButtonCallback);
glfwSetCursorPosCallback(window, cursorPosCallback);
glfwSetScrollCallback(window, scrollCallback);

// Enable depth testing for 3D
glEnable(GL_DEPTH_TEST);

glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);

glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// Position: location 0 (3 components for 3D)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// Color: location 1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glClearColor(0.05f, 0.05f, 0.1f, 1.0f);

// Use 3D shaders
shaderProgram = createShaderProgram("visualizations/vertexShader3D.glsl", "visualizations/fragmentShader.glsl");
if (shaderProgram == 0) {
std::cerr << "ERROR: Failed to create 3D shader program!" << std::endl;
return false;
}

glUseProgram(shaderProgram);

// Pre-fill history with empty data
std::vector<float> empty(64, 0.0f);
for (size_t i = 0; i < HISTORY_LENGTH; ++i) {
fftHistory.push_front(empty);
}

return true;
}

// ─── Render ─────────────────────────────────────────────────────────────────

void Bar3DVisualization::render(const std::vector<float>& fftMagnitudes) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);

if (fftMagnitudes.empty()) return;

// Number of frequency bars on x-axis
size_t numBars = fftMagnitudes.size() / 8;
if (numBars < 1) numBars = 1;

// Ensure smoothedFFT is the right size
if (smoothedFFT.size() != numBars) smoothedFFT.resize(numBars, 0.0f);

// ── Smooth current FFT data ──
float maxMagnitude = 0.0f;
for (size_t i = 0; i < numBars; ++i) {
if (i < fftMagnitudes.size() && fftMagnitudes[i] > maxMagnitude)
maxMagnitude = fftMagnitudes[i];
}
if (maxMagnitude < 1e-6f) maxMagnitude = 1.0f;

float decayFactor = 0.9f;
for (size_t i = 0; i < numBars; ++i) {
float rawMag = (i < fftMagnitudes.size()) ? fftMagnitudes[i] : 0.0f;
float normalizedMag = rawMag / maxMagnitude;
float logMag = log10(1.0f + normalizedMag * 10.0f) / log10(2.0f);
smoothedFFT[i] = smoothedFFT[i] * decayFactor + (1.0f - decayFactor) * logMag;
}

// Push to history (newest at front)
fftHistory.push_front(smoothedFFT);
if (fftHistory.size() > HISTORY_LENGTH) fftHistory.pop_back();

// ── Build 3D bar vertices ──
std::vector<float> vertices;
float barWidth = 0.7f;
float barDepth = 0.7f;
float spacing = 0.3f;
float zSpacing = 1.2f;

for (size_t z = 0; z < fftHistory.size(); ++z) {
const auto& frame = fftHistory[z];
float zPos = -(float)z * zSpacing;

for (size_t i = 0; i < numBars && i < frame.size(); ++i) {
float height = frame[i] * 2.5f;
if (height < 0.02f) height = 0.02f;

float xPos = (float)i * (barWidth + spacing) - (numBars * (barWidth + spacing)) * 0.5f;
float yPos = 0.0f;

// Color: unique per frequency, fading with z-distance
Color color = getColorFromMagnitude(frame[i], 0.0f, 1.0f);
float zFade = 1.0f - ((float)z / (float)HISTORY_LENGTH) * 0.5f;
float cr = color.r * zFade;
float cg = color.g * zFade;
float cb = color.b * zFade;

// Build 6 faces of a 3D box
float x0 = xPos, x1 = xPos + barWidth;
float y0 = yPos, y1 = yPos + height;
float z0 = zPos, z1 = zPos + barDepth;

// Front face
vertices.insert(vertices.end(), { x0,y0,z1, cr,cg,cb, x1,y0,z1, cr,cg,cb, x0,y1,z1, cr,cg,cb,
x1,y0,z1, cr,cg,cb, x1,y1,z1, cr,cg,cb, x0,y1,z1, cr,cg,cb });
// Back face
vertices.insert(vertices.end(), { x1,y0,z0, cr,cg,cb, x0,y0,z0, cr,cg,cb, x1,y1,z0, cr,cg,cb,
x0,y0,z0, cr,cg,cb, x0,y1,z0, cr,cg,cb, x1,y1,z0, cr,cg,cb });
// Left face
vertices.insert(vertices.end(), { x0,y0,z0, cr,cg,cb, x0,y0,z1, cr,cg,cb, x0,y1,z0, cr,cg,cb,
x0,y0,z1, cr,cg,cb, x0,y1,z1, cr,cg,cb, x0,y1,z0, cr,cg,cb });
// Right face
vertices.insert(vertices.end(), { x1,y0,z1, cr,cg,cb, x1,y0,z0, cr,cg,cb, x1,y1,z1, cr,cg,cb,
x1,y0,z0, cr,cg,cb, x1,y1,z0, cr,cg,cb, x1,y1,z1, cr,cg,cb });
// Top face (brighter)
float tr = std::min(cr * 1.3f, 1.0f), tg = std::min(cg * 1.3f, 1.0f), tb = std::min(cb * 1.3f, 1.0f);
vertices.insert(vertices.end(), { x0,y1,z1, tr,tg,tb, x1,y1,z1, tr,tg,tb, x0,y1,z0, tr,tg,tb,
x1,y1,z1, tr,tg,tb, x1,y1,z0, tr,tg,tb, x0,y1,z0, tr,tg,tb });
// Bottom face
float br2 = cr * 0.4f, bg2 = cg * 0.4f, bb2 = cb * 0.4f;
vertices.insert(vertices.end(), { x0,y0,z0, br2,bg2,bb2, x1,y0,z0, br2,bg2,bb2, x0,y0,z1, br2,bg2,bb2,
x1,y0,z0, br2,bg2,bb2, x1,y0,z1, br2,bg2,bb2, x0,y0,z1, br2,bg2,bb2 });
}
}

// ── Camera setup ──
autoRotation += 0.15f;
float totalAngleY = cameraAngleY + autoRotation;
float radX = cameraAngleX * (float)M_PI / 180.0f;
float radY = totalAngleY * (float)M_PI / 180.0f;

float eyeX = cameraDistance * cosf(radX) * sinf(radY);
float eyeY = cameraDistance * sinf(radX) + 2.0f;
float eyeZ = cameraDistance * cosf(radX) * cosf(radY);

int width, height;
glfwGetFramebufferSize(window, &width, &height);
float aspect = (height > 0) ? (float)width / (float)height : 1.0f;

auto proj = perspectiveMatrix(45.0f, aspect, 0.1f, 100.0f);
auto view = lookAtMatrix(eyeX, eyeY, eyeZ, 0, 1.0f, -HISTORY_LENGTH * 0.5f, 0, 1, 0);
auto mvp = multiplyMat4(proj, view);

// Upload MVP matrix
GLint mvpLoc = glGetUniformLocation(shaderProgram, "uMVP");
glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());

// Draw
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_DYNAMIC_DRAW);

glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(vertices.size() / 6));

glfwSwapBuffers(window);
glfwPollEvents();
}

// ─── Should Close / Cleanup ─────────────────────────────────────────────────

bool Bar3DVisualization::shouldClose() { return glfwWindowShouldClose(window); }

void Bar3DVisualization::cleanup() {
if (vbo != 0) glDeleteBuffers(1, &vbo);
if (vao != 0) glDeleteVertexArrays(1, &vao);
if (window) glfwDestroyWindow(window);
glfwTerminate();
}
58 changes: 58 additions & 0 deletions src/visualizations/Bar3DVisualization.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifndef BAR3D_VISUALIZATION_H
#define BAR3D_VISUALIZATION_H

#include "..\ShaderUtils.h"
#include "BaseVisualization.h"
#include "ColorUtils.h"
#include <vector>
#include <deque>
#include <GL/glew.h>
#include <GLFW/glfw3.h>

class Bar3DVisualization : public BaseVisualization {
public:
Bar3DVisualization();
~Bar3DVisualization();

bool initialize(int windowWidth, int windowHeight) override;
void render(const std::vector<float>& fftMagnitudes) override;
bool shouldClose() override;
void cleanup() override;

private:
GLFWwindow* window;
GLuint vbo, vao;
GLuint shaderProgram;

// History buffer for z-axis (time) dimension
static const size_t HISTORY_LENGTH = 16;
std::deque<std::vector<float>> fftHistory;

// Smoothed FFT for current frame
std::vector<float> smoothedFFT;

// Camera / projection parameters
float cameraAngleX;
float cameraAngleY;
float cameraDistance;
float autoRotation;

// Mouse state for interactive rotation
bool mouseDragging;
double lastMouseX, lastMouseY;

// Perspective projection matrix helper
std::vector<float> perspectiveMatrix(float fov, float aspect, float near, float far);
std::vector<float> lookAtMatrix(float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ);
std::vector<float> multiplyMat4(const std::vector<float>& a, const std::vector<float>& b);

// Callbacks
static void framebufferSizeCallback(GLFWwindow* window, int width, int height);
static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods);
static void cursorPosCallback(GLFWwindow* window, double xpos, double ypos);
static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset);
};

#endif
1 change: 1 addition & 0 deletions src/visualizations/_generation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"agent": "OpenClaw CTO Bot", "pre_task_context": "User: 悦姐,监控脚本发现一个 bounty issue — Convert 2D Bar Graph to 3D Bar Graph\n\nAssistant: 收到老板,分析一下。\n\nIssue #1: devlup-labs/MPV\n- C++ OpenGL 音频可视化项目\n- 需要把 2D 柱状图升级为 3D 透视投影\n- x 轴:频率,y 轴:振幅,z 轴:时间变化\n- 用 Perspective Projection 实现 3D 效果\n\n代码分析:\n- BarVisualization.cpp/h: 现有 2D 实现\n- vertexShader.glsl: 2D shader (vec2 position)\n- BaseVisualization.h: 抽象基类\n- ColorUtils.h: 颜色工具\n- main.cpp: 入口,用 switch 选择可视化类型\n\n实现方案:\n1. 新建 Bar3DVisualization.cpp/h 继承 BaseVisualization\n2. 新建 vertexShader3D.glsl 支持 mat4 MVP\n3. 实现透视投影 + lookAt 相机\n4. 用 deque 存储历史 FFT 数据作为 z 轴\n5. 绘制 3D 方块(6面体)\n6. 支持鼠标拖拽旋转 + 自动旋转\n7. 支持滚轮缩放\n8. 在 main.cpp 添加选项 5", "timestamp": "2026-05-22T15:30:00+08:00"}
Loading