WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
ScreenshotPanel.cpp
Go to the documentation of this file.
1#include "ScreenshotPanel.h"
2
3#include <cassert>
4
5#include <glad/gl.h>
6#include <glm/gtc/type_ptr.hpp>
7#include <glm/gtc/matrix_transform.hpp>
8
9#include "imgui.h"
10#include "imgui_stdlib.h"
11
12#include "Logger.h"
13#include "ViewportFBO.h"
14#include "OrbitCamera.h"
15#include "Renderer.h"
16#include "Attachment.h"
17
18#include "stb_image_write.h"
19
20#include <algorithm>
21#include <cstring>
22#include <format>
23#include <vector>
24
25namespace
26{
27
28// Capture the current FBO contents to a PNG file.
29void captureScreenshot(const std::string& path, ViewportFBO& fbo, std::string& status)
30{
31 if (fbo.width <= 0 || fbo.height <= 0 || !fbo.fbo)
32 {
33 status = "No viewport to capture.";
34 return;
35 }
36
37 const int w = fbo.width;
38 const int h = fbo.height;
39 std::vector<unsigned char> pixels(static_cast<size_t>(w) * h * 4);
40
41 glBindFramebuffer(GL_FRAMEBUFFER, fbo.fbo);
42 glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
43 glBindFramebuffer(GL_FRAMEBUFFER, 0);
44
45 // Flip vertically (OpenGL is bottom-up, PNG is top-down)
46 const size_t rowBytes = static_cast<size_t>(w) * 4;
47 std::vector<unsigned char> row(rowBytes);
48 for (int y = 0; y < h / 2; ++y)
49 {
50 unsigned char* top = pixels.data() + y * rowBytes;
51 unsigned char* bot = pixels.data() + (h - 1 - y) * rowBytes;
52 std::memcpy(row.data(), top, rowBytes);
53 std::memcpy(top, bot, rowBytes);
54 std::memcpy(bot, row.data(), rowBytes);
55 }
56
57 if (stbi_write_png(path.c_str(), w, h, 4, pixels.data(), static_cast<int>(rowBytes)))
58 {
59 status = "Saved: " + path;
60 LOG_INFO << "Screenshot saved to " << path;
61 }
62 else
63 {
64 status = "Failed to write: " + path;
65 LOG_ERROR << "Screenshot failed: " << path;
66 }
67}
68
69// Render the scene at a custom resolution into a temporary FBO and save as PNG.
70void captureAtResolution(const std::string& path, int cw, int ch,
71 Renderer& renderer, OrbitCamera& camera,
72 Attachment* root, float fov,
73 const glm::vec3& bgColor, bool drawGrid,
74 std::string& status)
75{
76 ViewportFBO tmpFbo;
77 renderer.renderScene(tmpFbo, cw, ch, camera, fov, bgColor, drawGrid,
78 [root]() {
79 if (!root) return;
80 glEnable(GL_LIGHTING);
81 glEnable(GL_TEXTURE_2D);
82 glEnable(GL_DEPTH_TEST);
83 glDepthFunc(GL_LEQUAL);
84 root->draw();
85 glEnable(GL_TEXTURE_2D);
86 glDisable(GL_LIGHTING);
87 glDepthMask(GL_FALSE);
88 glEnable(GL_BLEND);
89 root->drawParticles();
90 glDisable(GL_BLEND);
91 glDepthMask(GL_TRUE);
92 });
93
94 // Read pixels from the temp FBO
95 std::vector<unsigned char> pixels(static_cast<size_t>(cw) * ch * 4);
96 glBindFramebuffer(GL_FRAMEBUFFER, tmpFbo.fbo);
97 glReadPixels(0, 0, cw, ch, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
98 glBindFramebuffer(GL_FRAMEBUFFER, 0);
99 const size_t rowBytes = static_cast<size_t>(cw) * 4;
100 std::vector<unsigned char> row(rowBytes);
101 for (int y = 0; y < ch / 2; ++y)
102 {
103 unsigned char* top = pixels.data() + y * rowBytes;
104 unsigned char* bot = pixels.data() + (ch - 1 - y) * rowBytes;
105 std::memcpy(row.data(), top, rowBytes);
106 std::memcpy(top, bot, rowBytes);
107 std::memcpy(bot, row.data(), rowBytes);
108 }
109
110 if (stbi_write_png(path.c_str(), cw, ch, 4, pixels.data(), static_cast<int>(rowBytes)))
111 {
112 status = std::format("Saved ({}x{}): {}", cw, ch, path);
113 LOG_INFO << "Screenshot saved to " << path << " (" << cw << "x" << ch << ")";
114 }
115 else
116 {
117 status = "Failed to write: " + path;
118 }
119
120 tmpFbo.destroy();
121}
122
123} // anonymous namespace
124
126{
127 assert(ctx.screenshotPath && "DrawContext::screenshotPath must not be null");
128 assert(ctx.screenshotStatus && "DrawContext::screenshotStatus must not be null");
129 assert(ctx.useCanvasOverride && "DrawContext::useCanvasOverride must not be null");
130 assert(ctx.fbo && "DrawContext::fbo must not be null");
131 assert(ctx.camera && "DrawContext::camera must not be null");
132
133 ImGui::SeparatorText("Capture Viewport");
134 ImGui::Text("Output File:");
135 ImGui::SetNextItemWidth(-1);
136 ImGui::InputText("##screenshotPath", ctx.screenshotPath);
137
138 if (ImGui::Button("Save Screenshot", ImVec2(-1, 0)))
139 {
140 if (*ctx.useCanvasOverride && *ctx.canvasWidth > 0 && *ctx.canvasHeight > 0)
141 {
142 captureAtResolution(*ctx.screenshotPath,
143 *ctx.canvasWidth, *ctx.canvasHeight,
144 *ctx.renderer, *ctx.camera, ctx.root,
145 ctx.fov, ctx.bgColor, ctx.drawGrid,
146 *ctx.screenshotStatus);
147 }
148 else
149 {
150 captureScreenshot(*ctx.screenshotPath, *ctx.fbo, *ctx.screenshotStatus);
151 }
152 }
153
154 if (!ctx.screenshotStatus->empty())
155 {
156 bool isError = ctx.screenshotStatus->find("Failed") != std::string::npos ||
157 ctx.screenshotStatus->find("No ") != std::string::npos;
158 if (isError)
159 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", ctx.screenshotStatus->c_str());
160 else
161 ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s", ctx.screenshotStatus->c_str());
162 }
163
164 ImGui::SeparatorText("Canvas Size Override");
165 ImGui::Checkbox("Use custom resolution", ctx.useCanvasOverride);
166 if (*ctx.useCanvasOverride)
167 {
168 ImGui::SetNextItemWidth(100);
169 ImGui::InputInt("Width##canvas", ctx.canvasWidth, 0, 0);
170 ImGui::SameLine();
171 ImGui::SetNextItemWidth(100);
172 ImGui::InputInt("Height##canvas", ctx.canvasHeight, 0, 0);
173 *ctx.canvasWidth = std::max(1, std::min(*ctx.canvasWidth, 8192));
174 *ctx.canvasHeight = std::max(1, std::min(*ctx.canvasHeight, 8192));
175
176 ImGui::Text("Quick:");
177 ImGui::SameLine();
178 if (ImGui::SmallButton("1080p")) { *ctx.canvasWidth = 1920; *ctx.canvasHeight = 1080; }
179 ImGui::SameLine();
180 if (ImGui::SmallButton("1440p")) { *ctx.canvasWidth = 2560; *ctx.canvasHeight = 1440; }
181 ImGui::SameLine();
182 if (ImGui::SmallButton("4K")) { *ctx.canvasWidth = 3840; *ctx.canvasHeight = 2160; }
183 ImGui::SameLine();
184 if (ImGui::SmallButton("Square")) { *ctx.canvasWidth = 2048; *ctx.canvasHeight = 2048; }
185 }
186 else
187 {
188 ImGui::TextDisabled("Captures at current viewport resolution (%dx%d).",
189 ctx.fbo->width, ctx.fbo->height);
190 }
191}
#define LOG_ERROR
Definition Logger.h:11
#define LOG_INFO
Definition Logger.h:10
Scene-graph node that attaches a Displayable to a parent bone slot.
Definition Attachment.h:21
void drawParticles()
void draw()
Orbit camera that revolves around a target point.
Definition OrbitCamera.h:10
void renderScene(ViewportFBO &fbo, int w, int h, const OrbitCamera &camera, float fov, const glm::vec3 &clearColor, bool drawGrid, const std::function< void()> &drawObjects)
Definition Renderer.cpp:280
void draw(DrawContext &ctx)
Per-frame context for the screenshot panel.
Simple OpenGL framebuffer object wrapper for off-screen rendering.
Definition ViewportFBO.h:10
void destroy()
Release all GPU resources.
Definition ViewportFBO.h:59
int width
Current width in pixels.
Definition ViewportFBO.h:14
GLuint fbo
Framebuffer object handle.
Definition ViewportFBO.h:11
int height
Current height in pixels.
Definition ViewportFBO.h:15