diff --git a/.gitignore b/.gitignore index 45fbd5d..317fb86 100644 --- a/.gitignore +++ b/.gitignore @@ -108,4 +108,6 @@ $RECYCLE.BIN/ /bin/stitch/*.png /bin/stitch/*.dzi /bin/stitch/*_files/ -/files/magic-numbers/generated.xml \ No newline at end of file +/files/magic-numbers/generated.xml + +/bin/stitch/captures/* \ No newline at end of file diff --git a/README.md b/README.md index b7ea8fe..1cb0c50 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/files/capture.lua b/files/capture.lua index be24578..c59e1af 100644 --- a/files/capture.lua +++ b/files/capture.lua @@ -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" @@ -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() @@ -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 diff --git a/files/check.lua b/files/check.lua index 9098b10..e4fda8d 100644 --- a/files/check.lua +++ b/files/check.lua @@ -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.", diff --git a/settings.lua b/settings.lua index 0790b49..89c9f66 100644 --- a/settings.lua +++ b/settings.lua @@ -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, }, { @@ -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", @@ -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",