Add playblast: viewport capture to H.264 MP4 via libavcodec

- TimelinePanel: Playblast button opens modal with output dir (native
  folder picker via IFileOpenDialog), custom resolution (HD/FHD/4K
  presets), frame range, and movie export toggle
- UsdSceneRenderer: CaptureFrame() reads pixels from resolved MSAA FBO;
  Render() stores last dimensions for off-screen re-render at capture res
- MovieEncoder: streams RGBA frames directly into MP4 using libavcodec/
  libswscale (H.264, tries libx264 → nvenc → qsv → amf → openh264)
- Application: CapturePlayblastFrame() re-renders at capture resolution
  each frame; opens/writes/closes MovieEncoder inline with the render loop
- FileDialog: BrowseFolder() using Vista-style IFileOpenDialog (COM)
- CMake: FindFFmpeg.cmake locates prebuilt BtbN GPL shared package in
  third_party/ffmpeg; links and copies DLLs for main and test targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:36:30 +08:00
parent c796c76561
commit 0538dd3243
12 changed files with 764 additions and 3 deletions
+37
View File
@@ -24,6 +24,7 @@
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <algorithm>
#include <cmath>
#include <unordered_set>
@@ -496,6 +497,8 @@ bool UsdSceneRenderer::PickObjectsInRect(
void UsdSceneRenderer::Render(int width, int height)
{
if (!m_stage || width <= 0 || height <= 0) return;
m_lastRenderWidth = width;
m_lastRenderHeight = height;
InitRenderer();
if (!m_renderer) return;
@@ -676,6 +679,40 @@ uint32_t UsdSceneRenderer::GetColorTextureID()
return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0;
}
bool UsdSceneRenderer::CaptureFrame(std::vector<uint8_t>& outRGBA)
{
if (!m_drawTarget || m_lastRenderWidth <= 0 || m_lastRenderHeight <= 0)
return false;
int w = m_lastRenderWidth;
int h = m_lastRenderHeight;
// Resolve MSAA so we read from the non-multisampled colour attachment.
m_drawTarget->Resolve();
outRGBA.resize(static_cast<size_t>(w) * h * 4);
// Save/restore the read-framebuffer binding so we don't upset ImGui.
GLint prevFbo = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevFbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, outRGBA.data());
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevFbo));
// OpenGL reads bottom-up; flip to top-down.
int stride = w * 4;
std::vector<uint8_t> row(stride);
for (int y = 0; y < h / 2; ++y) {
uint8_t* top = outRGBA.data() + y * stride;
uint8_t* bot = outRGBA.data() + (h - 1 - y) * stride;
std::copy(top, top + stride, row.data());
std::copy(bot, bot + stride, top);
std::copy(row.data(), row.data() + stride, bot);
}
return true;
}
// ===========================================================================
// Axis Overlay (stageView.DrawAxis port)
// ===========================================================================