Skip to content

Commit

Permalink
Add animation capture mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Dadido3 committed Jul 2, 2024
1 parent 3fd0d97 commit d3edf29
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 6 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ $RECYCLE.BIN/
/bin/stitch/*.png
/bin/stitch/*.dzi
/bin/stitch/*_files/
/files/magic-numbers/generated.xml
/files/magic-numbers/generated.xml

/bin/stitch/captures/*
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ After a few minutes the file `output.png` will be created.
- `Spiral`: Will capture the world in a spiral.
The center starting point of the spiral can either be your current viewport, the world center or some custom coordinates.

- `Animation`: Will capture an image sequence.
This will capture whatever you see frame by frame and stores it in the output folder by frame number.
You can't stitch the resulting images, but instead you can use something like ffmpeg to render the sequence into a video file.

### Advanced mod settings

- `World seed`: If non empty, this will set the next new game to this seed.
Expand Down
83 changes: 83 additions & 0 deletions files/capture.lua
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,37 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
MonitorStandby.ResetTimer()
end

---Captures a screenshot of the current viewport.
---This is used to capture animations, therefore the resulting image may not be suitable for stitching.
---@param outputPixelScale number? The resulting image pixel to world pixel ratio.
---@param frameNumber integer The frame number of the animation.
local function captureScreenshotAnimation(outputPixelScale, frameNumber)
if outputPixelScale == 0 or outputPixelScale == nil then
outputPixelScale = Coords:PixelScale()
end

local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
if not rectTopLeft or not rectBottomRight then
error(string.format("couldn't determine capturing rectangle"))
end
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
end

local topLeftWorld, bottomRightWorld = Coords:ToWorld(rectTopLeft), Coords:ToWorld(rectBottomRight)

---We will use this to get our fame number into the filename.
---@type Vec2
local outputTopLeft = Vec2(frameNumber, 0)

if not ScreenCapture.Capture(rectTopLeft, rectBottomRight, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then
error(string.format("failed to capture screenshot"))
end

-- Reset monitor and PC standby every screenshot.
MonitorStandby.ResetTimer()
end

---Map capture process runner context error handler callback. Just rolls off the tongue.
---@param err string
---@param scope "init"|"do"|"end"
Expand Down Expand Up @@ -750,6 +781,56 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
end

---Starts to capture an animation.
---This stores sequences of images that can't be stitched, but can be rendered into a video instead.
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingAnimation(outputPixelScale)

---Queries the mod settings for the live capture parameters.
---@return integer interval -- The interval length in frames.
local function querySettings()
local interval = 1--tonumber(ModSettingGet("noita-mapcap.live-interval")) or 30
return interval
end

-- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
if file ~= nil then file:close() end

---Process main callback.
---@param ctx ProcessRunnerCtx
local function handleDo(ctx)
Modification.SetCameraFree(false)

local frame = 0

repeat
local interval = querySettings()

-- Wait until we are allowed to take a new screenshot.
local delayFrames = 0
repeat
wait(0)
delayFrames = delayFrames + 1
until ctx:IsStopping() or delayFrames >= interval

captureScreenshotAnimation(outputPixelScale, frame)

frame = frame + 1
until ctx:IsStopping()
end

---Process end callback.
---@param ctx ProcessRunnerCtx
local function handleEnd(ctx)
Modification.SetCameraFree()
end

-- Run process, if there is no other running right now.
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end

---Starts the capturing process based on user/mod settings.
function Capture:StartCapturing()
Message:CatchException("Capture:StartCapturing", function()
Expand All @@ -762,6 +843,8 @@ function Capture:StartCapturing()
if mode == "live" then
self:StartCapturingLive(outputPixelScale)
self:StartCapturingPlayerPath(5, outputPixelScale) -- Capture player path with an interval of 5 frames.
elseif mode == "animation" then
self:StartCapturingAnimation(outputPixelScale)
elseif mode == "area" then
local area = ModSettingGet("noita-mapcap.area")
if area == "custom" then
Expand Down
2 changes: 1 addition & 1 deletion files/check.lua
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function Check:Regular(interval)
-- This is not perfect, as it doesn't take rounding and cropping into account, so the actual captured area may be a few pixels smaller.
local mode = ModSettingGet("noita-mapcap.capture-mode")
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
if mode ~= "live" and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
if (mode ~= "live" and mode ~= "animation") and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
Message:ShowGeneralSettingsProblem(
"The virtual resolution is smaller than the capture grid size.",
"This means that you will get black areas in your final stitched image.",
Expand Down
8 changes: 4 additions & 4 deletions settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ modSettings = {
{
id = "capture-mode",
ui_name = "Mode",
ui_description = "How the mod captures:\n- Live: Capture as you play along.\n- Area: Capture a defined area of the world.\n- Spiral: Capture in a spiral around a starting point indefinitely.",
ui_description = "How the mod captures:\n- Live: Capture as you play along.\n- Area: Capture a defined area of the world.\n- Spiral: Capture in a spiral around a starting point indefinitely.\n- Animation: Capture the screen frame by frame.",
value_default = "live",
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" } },
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" }, { "animation", "Animation"} },
scope = MOD_SETTING_SCOPE_RUNTIME,
},
{
Expand Down Expand Up @@ -143,7 +143,7 @@ modSettings = {
value_default = "512",
allowed_characters = "0123456789",
scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
},
{
id = "pixel-scale",
Expand All @@ -167,7 +167,7 @@ modSettings = {
value_display_multiplier = 1,
value_display_formatting = " $0 frames",
scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
},
{
id = "custom-resolution-live",
Expand Down

0 comments on commit d3edf29

Please sign in to comment.