Skip to content

Commit d3edf29

Browse files
committed
Add animation capture mode
1 parent 3fd0d97 commit d3edf29

File tree

5 files changed

+95
-6
lines changed

5 files changed

+95
-6
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,6 @@ $RECYCLE.BIN/
108108
/bin/stitch/*.png
109109
/bin/stitch/*.dzi
110110
/bin/stitch/*_files/
111-
/files/magic-numbers/generated.xml
111+
/files/magic-numbers/generated.xml
112+
113+
/bin/stitch/captures/*

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ After a few minutes the file `output.png` will be created.
8181
- `Spiral`: Will capture the world in a spiral.
8282
The center starting point of the spiral can either be your current viewport, the world center or some custom coordinates.
8383

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

8690
- `World seed`: If non empty, this will set the next new game to this seed.

files/capture.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,37 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
166166
MonitorStandby.ResetTimer()
167167
end
168168

169+
---Captures a screenshot of the current viewport.
170+
---This is used to capture animations, therefore the resulting image may not be suitable for stitching.
171+
---@param outputPixelScale number? The resulting image pixel to world pixel ratio.
172+
---@param frameNumber integer The frame number of the animation.
173+
local function captureScreenshotAnimation(outputPixelScale, frameNumber)
174+
if outputPixelScale == 0 or outputPixelScale == nil then
175+
outputPixelScale = Coords:PixelScale()
176+
end
177+
178+
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
179+
if not rectTopLeft or not rectBottomRight then
180+
error(string.format("couldn't determine capturing rectangle"))
181+
end
182+
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
183+
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
184+
end
185+
186+
local topLeftWorld, bottomRightWorld = Coords:ToWorld(rectTopLeft), Coords:ToWorld(rectBottomRight)
187+
188+
---We will use this to get our fame number into the filename.
189+
---@type Vec2
190+
local outputTopLeft = Vec2(frameNumber, 0)
191+
192+
if not ScreenCapture.Capture(rectTopLeft, rectBottomRight, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then
193+
error(string.format("failed to capture screenshot"))
194+
end
195+
196+
-- Reset monitor and PC standby every screenshot.
197+
MonitorStandby.ResetTimer()
198+
end
199+
169200
---Map capture process runner context error handler callback. Just rolls off the tongue.
170201
---@param err string
171202
---@param scope "init"|"do"|"end"
@@ -750,6 +781,56 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
750781
self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
751782
end
752783

784+
---Starts to capture an animation.
785+
---This stores sequences of images that can't be stitched, but can be rendered into a video instead.
786+
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
787+
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
788+
function Capture:StartCapturingAnimation(outputPixelScale)
789+
790+
---Queries the mod settings for the live capture parameters.
791+
---@return integer interval -- The interval length in frames.
792+
local function querySettings()
793+
local interval = 1--tonumber(ModSettingGet("noita-mapcap.live-interval")) or 30
794+
return interval
795+
end
796+
797+
-- Create file that signals that there are files in the output directory.
798+
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
799+
if file ~= nil then file:close() end
800+
801+
---Process main callback.
802+
---@param ctx ProcessRunnerCtx
803+
local function handleDo(ctx)
804+
Modification.SetCameraFree(false)
805+
806+
local frame = 0
807+
808+
repeat
809+
local interval = querySettings()
810+
811+
-- Wait until we are allowed to take a new screenshot.
812+
local delayFrames = 0
813+
repeat
814+
wait(0)
815+
delayFrames = delayFrames + 1
816+
until ctx:IsStopping() or delayFrames >= interval
817+
818+
captureScreenshotAnimation(outputPixelScale, frame)
819+
820+
frame = frame + 1
821+
until ctx:IsStopping()
822+
end
823+
824+
---Process end callback.
825+
---@param ctx ProcessRunnerCtx
826+
local function handleEnd(ctx)
827+
Modification.SetCameraFree()
828+
end
829+
830+
-- Run process, if there is no other running right now.
831+
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
832+
end
833+
753834
---Starts the capturing process based on user/mod settings.
754835
function Capture:StartCapturing()
755836
Message:CatchException("Capture:StartCapturing", function()
@@ -762,6 +843,8 @@ function Capture:StartCapturing()
762843
if mode == "live" then
763844
self:StartCapturingLive(outputPixelScale)
764845
self:StartCapturingPlayerPath(5, outputPixelScale) -- Capture player path with an interval of 5 frames.
846+
elseif mode == "animation" then
847+
self:StartCapturingAnimation(outputPixelScale)
765848
elseif mode == "area" then
766849
local area = ModSettingGet("noita-mapcap.area")
767850
if area == "custom" then

files/check.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function Check:Regular(interval)
120120
-- 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.
121121
local mode = ModSettingGet("noita-mapcap.capture-mode")
122122
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
123-
if mode ~= "live" and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
123+
if (mode ~= "live" and mode ~= "animation") and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
124124
Message:ShowGeneralSettingsProblem(
125125
"The virtual resolution is smaller than the capture grid size.",
126126
"This means that you will get black areas in your final stitched image.",

settings.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ modSettings = {
6767
{
6868
id = "capture-mode",
6969
ui_name = "Mode",
70-
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.",
70+
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.",
7171
value_default = "live",
72-
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" } },
72+
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" }, { "animation", "Animation"} },
7373
scope = MOD_SETTING_SCOPE_RUNTIME,
7474
},
7575
{
@@ -143,7 +143,7 @@ modSettings = {
143143
value_default = "512",
144144
allowed_characters = "0123456789",
145145
scope = MOD_SETTING_SCOPE_RUNTIME,
146-
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
146+
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
147147
},
148148
{
149149
id = "pixel-scale",
@@ -167,7 +167,7 @@ modSettings = {
167167
value_display_multiplier = 1,
168168
value_display_formatting = " $0 frames",
169169
scope = MOD_SETTING_SCOPE_RUNTIME,
170-
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
170+
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
171171
},
172172
{
173173
id = "custom-resolution-live",

0 commit comments

Comments
 (0)