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:
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
@@ -35,6 +36,7 @@ void TimelinePanel::SetStage(pxr::UsdStageRefPtr stage)
|
||||
// ---------------------------------------------------------------------------
|
||||
void TimelinePanel::Update(float deltaTime)
|
||||
{
|
||||
if (m_isPlayblasting) return; // time is stepped externally during capture
|
||||
if (!m_playing) return;
|
||||
|
||||
double playStart = m_startFrame;
|
||||
@@ -204,6 +206,117 @@ void TimelinePanel::Render()
|
||||
ImGui::Text("%.6g fps", m_fps);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Auto-Key", &m_autoKey)) NotifyTimeChanged();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
|
||||
// ---- Playblast button / progress ----
|
||||
if (m_isPlayblasting) {
|
||||
int total = m_playblastSettings.endFrame - m_playblastSettings.startFrame + 1;
|
||||
ImGui::Text("Capturing %d / %d", m_playblastFrameIdx, total);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel##pb")) AbortPlayblast();
|
||||
} else {
|
||||
if (ImGui::Button("Playblast")) {
|
||||
m_playblastSettings.startFrame = int(m_startFrame);
|
||||
m_playblastSettings.endFrame = int(m_endFrame);
|
||||
m_showPlayblastDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Playblast settings dialog ----
|
||||
if (m_showPlayblastDialog) {
|
||||
ImGui::OpenPopup("Playblast##dlg");
|
||||
m_showPlayblastDialog = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Playblast##dlg", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
// ── Output path ──────────────────────────────────────────────────────
|
||||
ImGui::Text("Output directory:");
|
||||
ImGui::SetNextItemWidth(340.f);
|
||||
ImGui::InputText("##outdir", m_playblastSettings.outputDir,
|
||||
sizeof(m_playblastSettings.outputDir));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse...##outdir")) {
|
||||
if (OnBrowseFolder) {
|
||||
std::string dir = OnBrowseFolder();
|
||||
if (!dir.empty()) {
|
||||
strncpy(m_playblastSettings.outputDir, dir.c_str(),
|
||||
sizeof(m_playblastSettings.outputDir) - 1);
|
||||
m_playblastSettings.outputDir[sizeof(m_playblastSettings.outputDir) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("File prefix:");
|
||||
ImGui::SetNextItemWidth(200.f);
|
||||
ImGui::InputText("##prefix", m_playblastSettings.filePrefix,
|
||||
sizeof(m_playblastSettings.filePrefix));
|
||||
|
||||
// ── Frame range ───────────────────────────────────────────────────────
|
||||
ImGui::Separator();
|
||||
ImGui::SetNextItemWidth(90.f);
|
||||
ImGui::InputInt("Start frame##pb", &m_playblastSettings.startFrame);
|
||||
ImGui::SetNextItemWidth(90.f);
|
||||
ImGui::InputInt("End frame##pb", &m_playblastSettings.endFrame);
|
||||
if (m_playblastSettings.endFrame < m_playblastSettings.startFrame)
|
||||
m_playblastSettings.endFrame = m_playblastSettings.startFrame;
|
||||
|
||||
// ── Capture resolution ────────────────────────────────────────────────
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Capture resolution:");
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
ImGui::InputInt("W##pbw", &m_playblastSettings.captureWidth);
|
||||
if (m_playblastSettings.captureWidth < 16) m_playblastSettings.captureWidth = 16;
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("x");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
ImGui::InputInt("H##pbh", &m_playblastSettings.captureHeight);
|
||||
if (m_playblastSettings.captureHeight < 16) m_playblastSettings.captureHeight = 16;
|
||||
|
||||
// Common presets
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("HD")) { m_playblastSettings.captureWidth = 1280; m_playblastSettings.captureHeight = 720; }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("FHD")) { m_playblastSettings.captureWidth = 1920; m_playblastSettings.captureHeight = 1080; }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("4K")) { m_playblastSettings.captureWidth = 3840; m_playblastSettings.captureHeight = 2160; }
|
||||
|
||||
// ── Movie export ──────────────────────────────────────────────────────
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Export movie (H.264 MP4 via libavcodec)", &m_playblastSettings.exportMovie);
|
||||
if (m_playblastSettings.exportMovie) {
|
||||
ImGui::Indent();
|
||||
ImGui::Checkbox("Also save BMP frames alongside movie", &m_playblastSettings.keepFrames);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Writes: <dir>\\<prefix>.NNNN.bmp%s",
|
||||
m_playblastSettings.exportMovie ? " → <prefix>.mp4" : "");
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Cancel##dlg")) ImGui::CloseCurrentPopup();
|
||||
ImGui::SameLine();
|
||||
bool canStart = m_playblastSettings.outputDir[0] != '\0' &&
|
||||
m_playblastSettings.filePrefix[0] != '\0';
|
||||
ImGui::BeginDisabled(!canStart);
|
||||
if (ImGui::Button("Start##dlg")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
m_playblastWasPlaying = m_playing;
|
||||
m_playing = false;
|
||||
m_playblastSettings.fps = m_fps;
|
||||
m_playblastFrame = double(m_playblastSettings.startFrame);
|
||||
m_playblastFrameIdx = 0;
|
||||
m_playblastSkipFirst = true;
|
||||
m_isPlayblasting = true;
|
||||
SetCurrentFrame(m_playblastFrame);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---- Slider row: [start] [----slider----] [end] ----
|
||||
{
|
||||
@@ -268,4 +381,23 @@ pxr::UsdTimeCode TimelinePanel::EditTime() const
|
||||
: pxr::UsdTimeCode::Default();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
bool TimelinePanel::AdvancePlayblast()
|
||||
{
|
||||
m_playblastFrame += 1.0;
|
||||
m_playblastFrameIdx++;
|
||||
if (m_playblastFrame > double(m_playblastSettings.endFrame)) {
|
||||
AbortPlayblast();
|
||||
return false;
|
||||
}
|
||||
SetCurrentFrame(m_playblastFrame);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TimelinePanel::AbortPlayblast()
|
||||
{
|
||||
m_isPlayblasting = false;
|
||||
m_playing = m_playblastWasPlaying;
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
|
||||
Reference in New Issue
Block a user