mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 10:20:53 +02:00
196 lines
7.2 KiB
Lua
196 lines
7.2 KiB
Lua
-- Driver: one scene, once per rung of the AA row.
|
|
--
|
|
-- The AA row is the one setting in this mod whose whole effect is a pixel
|
|
-- wide, so it is also the one that cannot be judged from a description. This
|
|
-- renders the SAME frame at each rung and writes one PNG per rung; put two of
|
|
-- them side by side, magnified, and the row is either doing something or it
|
|
-- is not.
|
|
--
|
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/aa_shots.lua \
|
|
-- SHOT_DIR=<dir> lovec.exe .
|
|
--
|
|
-- knobs (env):
|
|
-- SHOT_DIR output directory (created if missing) (default "shots/aa")
|
|
-- AA_MAP map id (default VIRIDIAN_CITY)
|
|
-- AA_SPOT "x,y[,facing]" (default 20,26,up)
|
|
-- AA_RUNG the voxel camera rung (default 5, the 75 one)
|
|
--
|
|
-- The scene defaults to a town at the LOW camera on purpose: roof ridges, the
|
|
-- diagonal of a fence and a tree's silhouette against the sky are the edges
|
|
-- that stair-step, and 75 degrees is the rung that puts the most of them at an
|
|
-- angle to the pixel grid.
|
|
--
|
|
-- Determinism matters here for the same reason it does in voxel_shots_ab: the
|
|
-- three shots differ ONLY by the row under test, or comparing them means
|
|
-- nothing. The clock is pinned, the animated tile slots are frozen, the
|
|
-- townsfolk are stopped where they stand, and the tilt-shift is held at zero
|
|
-- (a gaussian over the frame would smear away the very edges being looked at).
|
|
--
|
|
-- Nothing here writes the player's options: the row is moved with
|
|
-- ModSetting:sync, which moves the cached index and persists nothing.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local Pipelines = require("src.render.Pipelines")
|
|
local OverworldState = require("src.world.OverworldController")
|
|
|
|
local ROOT = os.getenv("SHOT_DIR") or "shots/aa"
|
|
|
|
local handle = game.mods.exports["DRAMATIC_SHAPE"]
|
|
if not (handle and handle.lib) then
|
|
print("[aa] DRAMATIC_SHAPE mod not loaded -- nothing to shoot")
|
|
return
|
|
end
|
|
local V = handle.lib
|
|
local DayNight = V.require("DayNight")
|
|
local AntiAlias = V.require("AntiAlias")
|
|
local ChunkMesher = V.require("ChunkMesher")
|
|
local Voxel = V.require("VoxelState")
|
|
local ShadowMap = V.require("ShadowMap")
|
|
|
|
local MAP = os.getenv("AA_MAP") or "VIRIDIAN_CITY"
|
|
local SPOT = os.getenv("AA_SPOT") or "20,26,up"
|
|
local RUNG = math.floor(tonumber(os.getenv("AA_RUNG")) or 5)
|
|
local sx, sy, sf = SPOT:match("^(%-?%d+),%s*(%-?%d+),?%s*(%a*)$")
|
|
sx, sy = tonumber(sx) or 20, tonumber(sy) or 26
|
|
if sf == "" then sf = "up" end
|
|
|
|
OverworldState.rollEncounter = function() return nil end
|
|
|
|
local NPC = require("src.world.NPC")
|
|
if not NPC.dramaticShapeAaFreeze then
|
|
local inner = NPC.update
|
|
function NPC:update(...)
|
|
self.frozen = true
|
|
return inner(self, ...)
|
|
end
|
|
NPC.dramaticShapeAaFreeze = true
|
|
end
|
|
pcall(love.math.setRandomSeed, 20260801)
|
|
|
|
local TileRenderer = require("src.render.TileRenderer")
|
|
TileRenderer.tick = function() end
|
|
TileRenderer.animFrame = function() return 0 end
|
|
|
|
pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
|
|
pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
|
|
|
|
local Zoom = require("src.render.Zoom")
|
|
pcall(function()
|
|
game.save.options.zoom = 1
|
|
Zoom.applyOptions(game.save.options)
|
|
end)
|
|
|
|
local function cameraStill()
|
|
local o = game.overworld
|
|
local c = o and o.camera
|
|
if not c then return true end
|
|
local lx, ly, held = nil, nil, 0
|
|
for _ = 1, 300 do
|
|
if c.x == lx and c.y == ly then
|
|
held = held + 1
|
|
if held >= 10 then return true end
|
|
else
|
|
held = 0
|
|
lx, ly = c.x, c.y
|
|
end
|
|
U.wait(1)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- The camera PITCH, which is the one that caught this driver out. The tween
|
|
-- runs on wall-clock dt and Voxel.t reaching 1 is not the same instant the
|
|
-- angle stops moving, so the first shot of a run came out at 67 degrees
|
|
-- while the two after it were at 75 -- three frames that differ by the
|
|
-- camera, in a comparison whose entire subject is a pixel.
|
|
local function angleStill()
|
|
local last, held = nil, 0
|
|
for _ = 1, 600 do
|
|
if Voxel.angle == last then
|
|
held = held + 1
|
|
if held >= 10 then return true end
|
|
else
|
|
held = 0
|
|
last = Voxel.angle
|
|
end
|
|
U.wait(1)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function settle()
|
|
for _ = 1, 900 do
|
|
if ChunkMesher.pending() == 0 then break end
|
|
U.wait(1)
|
|
end
|
|
for _ = 1, 300 do
|
|
if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
|
|
U.wait(1)
|
|
end
|
|
angleStill()
|
|
cameraStill()
|
|
-- the sun map is only redrawn when its inputs move, and the AA row is not
|
|
-- one of them -- so force one pass at the settled camera rather than
|
|
-- comparing a frame against a map fitted a few hundredths of a pixel ago
|
|
if ShadowMap.forget then ShadowMap.forget() end
|
|
U.wait(20)
|
|
end
|
|
|
|
DayNight.setting:sync("day")
|
|
U.teleport(game, MAP, sx, sy, sf)
|
|
Pipelines.setLevel("voxel", RUNG)
|
|
Pipelines.setLevel("tiltshift", 0)
|
|
|
|
-- Warm up before the FIRST shot, not just between them.
|
|
--
|
|
-- Neighbour maps are requested from inside the render itself
|
|
-- (VoxelScene.prefetch), so an empty build queue right after a teleport
|
|
-- means "nothing has been asked for yet", not "everything is here". The
|
|
-- first capture of a run came out with the map beyond Viridian missing --
|
|
-- a whole tree line absent from one frame of a three-way comparison, which
|
|
-- looks exactly like the row under test doing something enormous. Settling
|
|
-- twice lets the first render request the neighbourhood and the second
|
|
-- drain it.
|
|
settle()
|
|
settle()
|
|
|
|
local shots, missed = 0, 0
|
|
for _, samples in ipairs({ 0, 2, 4 }) do
|
|
AntiAlias.setting:sync(samples)
|
|
settle()
|
|
-- AA_TRACE=1 prints the state each shot was taken in. When two shots of a
|
|
-- run disagree by more than the row could account for, this is what says
|
|
-- which input moved -- it is how the camera-tween and the neighbour-mesh
|
|
-- settles above were both found.
|
|
if os.getenv("AA_TRACE") == "1" then
|
|
local Voxel3D = V.require("Voxel3D")
|
|
local o = game.overworld
|
|
local cw, chh = Voxel3D.size()
|
|
print(("[aa] trace samples=%d angle=%.6f fov=%.6f cell=%.4f canvas=%dx%d cam=(%.3f,%.3f) eye=(%.2f,%.2f,%.2f) factor=%.4f")
|
|
:format(samples, Voxel.angle or -1, Voxel3D.fovY or -1,
|
|
Voxel3D.cell or -1, cw or 0, chh or 0,
|
|
o and o.camera and o.camera.x or -1,
|
|
o and o.camera and o.camera.y or -1,
|
|
(Voxel3D.eye or {})[1] or 0, (Voxel3D.eye or {})[2] or 0,
|
|
(Voxel3D.eye or {})[3] or 0, AntiAlias.factor()))
|
|
end
|
|
local path = ("%s/aa_%d.png"):format(ROOT, samples)
|
|
game.capturePath = path
|
|
U.wait(6)
|
|
local f = io.open(path, "rb")
|
|
if f then
|
|
f:close()
|
|
shots = shots + 1
|
|
print(("[aa] %s samples=%d"):format(path, samples))
|
|
else
|
|
missed = missed + 1
|
|
print("[aa] capture did not reach disk: " .. path)
|
|
end
|
|
end
|
|
-- left where it was found, so a run cannot leak a rung into the next one
|
|
AntiAlias.setting:sync(0)
|
|
|
|
print(("[aa] %d shots into %s (%d failed to reach disk)")
|
|
:format(shots, ROOT, missed))
|
|
end
|