From 971b905e7737b77c99e389f56f264dbc86c07ba2 Mon Sep 17 00:00:00 2001 From: TimLiu Date: Fri, 22 May 2026 15:31:56 +0800 Subject: [PATCH] [OpenClaw CTO Bot] feat: Convert 2D Bar Graph to 3D Bar Graph visualization Fixes #1 - Added Bar3DVisualization with perspective projection - X-axis: frequencies, Y-axis: amplitudes, Z-axis: time variation - 3D box rendering with 6-face shading (top brighter, bottom darker) - Auto-rotation + interactive mouse drag rotation - Scroll zoom support - History buffer (deque) for time-series FFT data - New vertexShader3D.glsl with mat4 MVP uniform - Added option 5 in main.cpp menu - Depth testing enabled for proper 3D occlusion --- src/main.cpp | 5 + src/visualizations/Bar3DVisualization.cpp | 299 ++++++++++++++++++++++ src/visualizations/Bar3DVisualization.h | 58 +++++ src/visualizations/_generation.json | 1 + src/visualizations/vertexShader3D.glsl | 12 + 5 files changed, 375 insertions(+) create mode 100644 src/visualizations/Bar3DVisualization.cpp create mode 100644 src/visualizations/Bar3DVisualization.h create mode 100644 src/visualizations/_generation.json create mode 100644 src/visualizations/vertexShader3D.glsl diff --git a/src/main.cpp b/src/main.cpp index 1acefc5..3015c88 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include "visualizations/BarVisualization.h" #include "visualizations/CircularBarVisualization.h" #include "visualizations/MountainVisualization.h" +#include "visualizations/Bar3DVisualization.h" #include #include @@ -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; @@ -43,6 +45,9 @@ int main() { case 4: visualization = make_unique(); break; + case 5: + visualization = make_unique(); + break; default: cout << "Invalid choice. Exiting.\n"; return -1; diff --git a/src/visualizations/Bar3DVisualization.cpp b/src/visualizations/Bar3DVisualization.cpp new file mode 100644 index 0000000..37ed211 --- /dev/null +++ b/src/visualizations/Bar3DVisualization.cpp @@ -0,0 +1,299 @@ +#define _USE_MATH_DEFINES +#include "Bar3DVisualization.h" +#include +#include +#include + +// ─── 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(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(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(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 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 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 Bar3DVisualization::multiplyMat4(const std::vector& a, const std::vector& b) { + std::vector 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 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& 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 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(); +} diff --git a/src/visualizations/Bar3DVisualization.h b/src/visualizations/Bar3DVisualization.h new file mode 100644 index 0000000..008fca8 --- /dev/null +++ b/src/visualizations/Bar3DVisualization.h @@ -0,0 +1,58 @@ +#ifndef BAR3D_VISUALIZATION_H +#define BAR3D_VISUALIZATION_H + +#include "..\ShaderUtils.h" +#include "BaseVisualization.h" +#include "ColorUtils.h" +#include +#include +#include +#include + +class Bar3DVisualization : public BaseVisualization { +public: + Bar3DVisualization(); + ~Bar3DVisualization(); + + bool initialize(int windowWidth, int windowHeight) override; + void render(const std::vector& 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> fftHistory; + + // Smoothed FFT for current frame + std::vector 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 perspectiveMatrix(float fov, float aspect, float near, float far); + std::vector lookAtMatrix(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ); + std::vector multiplyMat4(const std::vector& a, const std::vector& 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 diff --git a/src/visualizations/_generation.json b/src/visualizations/_generation.json new file mode 100644 index 0000000..c8ff685 --- /dev/null +++ b/src/visualizations/_generation.json @@ -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"} \ No newline at end of file diff --git a/src/visualizations/vertexShader3D.glsl b/src/visualizations/vertexShader3D.glsl new file mode 100644 index 0000000..49b0edd --- /dev/null +++ b/src/visualizations/vertexShader3D.glsl @@ -0,0 +1,12 @@ +#version 330 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aColor; + +out vec3 vertexColor; + +uniform mat4 uMVP; + +void main() { + gl_Position = uMVP * vec4(aPos, 1.0); + vertexColor = aColor; +}