aa_shots.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
-- 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