mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:00:51 +02:00
176 lines
6.7 KiB
Lua
176 lines
6.7 KiB
Lua
-- Driver: isolate the diagonal banding on flat lit geometry.
|
|
--
|
|
-- Shoots the SAME stand point four ways in ONE launch, so palette, camera
|
|
-- and framing are identical and the only variable is the sun pass:
|
|
--
|
|
-- _on shadows as shipped
|
|
-- _off SHADOW_ALPHA = 0 -- the sun pass still runs, nothing reads it
|
|
-- _flatN a CONSTANT bias of N world px, i.e. SLOPE switched off
|
|
--
|
|
-- and prints, from the frustum the run actually fitted, how much depth
|
|
-- slack each face orientation NEEDS against the slack it is given. A face
|
|
-- whose need exceeds the slack shadows itself in a moire -- acne -- which
|
|
-- reads as diagonal banding, since the ramp it aliases against runs along
|
|
-- the light's frame and the sun sits southeast.
|
|
--
|
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/voxel_acne_probe.lua lovec .
|
|
--
|
|
-- knobs (env):
|
|
-- ACNE_MAP map id (default VIRIDIAN_CITY)
|
|
-- ACNE_SPOT "x,y[,facing]" (default 32,9,up -- the gym wall)
|
|
-- ACNE_LEVELS comma list of voxel levels (default "3")
|
|
-- ACNE_FLAT comma list of constant-bias values to compare against
|
|
-- (default "1" -- what shipped before SLOPE existed)
|
|
-- SHOT_DIR output directory, must exist (default "shots")
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local Pipelines = require("src.render.Pipelines")
|
|
|
|
local SPEED = math.max(1,
|
|
math.floor(tonumber(os.getenv("POKEPORT_SPEED")) or 1))
|
|
local function wait(n) U.wait(n * SPEED) end
|
|
|
|
local DIR = os.getenv("SHOT_DIR") or "shots"
|
|
local mapId = os.getenv("ACNE_MAP") or "VIRIDIAN_CITY"
|
|
local sx, sy, facing = (os.getenv("ACNE_SPOT") or "32,9,up")
|
|
:match("^%s*(%d+)%s*,%s*(%d+)%s*,?%s*(%a*)")
|
|
facing = (facing ~= "" and facing) or "up"
|
|
|
|
local handle = game.mods.exports["DRAMATIC_SHAPE"]
|
|
local V = assert(handle and handle.lib, "DRAMATIC_SHAPE exports missing")
|
|
local ShadowMap = V.require("ShadowMap")
|
|
local Voxel3D = V.require("Voxel3D")
|
|
local Mat4 = V.require("Mat4")
|
|
|
|
local levels = {}
|
|
for n in (os.getenv("ACNE_LEVELS") or "3"):gmatch("%d+") do
|
|
levels[#levels + 1] = tonumber(n)
|
|
end
|
|
local flats = {}
|
|
for n in (os.getenv("ACNE_FLAT") or "1"):gmatch("[%d.]+") do
|
|
flats[#flats + 1] = tonumber(n)
|
|
end
|
|
|
|
-- ---- what the frustum this run fitted asks of the bias, per face
|
|
local FACES = {
|
|
{ "top +Y", 0, 1, 0 },
|
|
{ "south +Z", 0, 0, 1 },
|
|
{ "east +X", 1, 0, 0 },
|
|
{ "west -X", -1, 0, 0 },
|
|
{ "north -Z", 0, 0, -1 },
|
|
{ "roof 45 N", 0, 0.7071, -0.7071 },
|
|
{ "roof 45 S", 0, 0.7071, 0.7071 },
|
|
}
|
|
|
|
local function report(tag)
|
|
local e = ShadowMap.extent
|
|
if not e then print("[acne] " .. tag .. ": no frustum yet") return end
|
|
local res = ShadowMap.res
|
|
local texX, texY, depth = e[1] / res, e[2] / res, e[3]
|
|
local d = ShadowMap.sunDir()
|
|
local view = Mat4.lookAt({ 0, 0, 0 }, d, { 0, 0, -1 })
|
|
local R = { view[1], view[2], view[3] }
|
|
local Up = { view[5], view[6], view[7] }
|
|
local F = { -view[9], -view[10], -view[11] }
|
|
print(("[acne] %s: res %d frustum %.0fx%.0f deep %.0f"
|
|
.. " texel %.2fx%.2f world px slack %.2f px (stored %.6f)")
|
|
:format(tag, res, e[1], e[2], depth, texX, texY,
|
|
ShadowMap.slack, ShadowMap.bias))
|
|
for _, fc in ipairs(FACES) do
|
|
local n = { fc[2], fc[3], fc[4] }
|
|
local nr = n[1]*R[1] + n[2]*R[2] + n[3]*R[3]
|
|
local nu = n[1]*Up[1] + n[2]*Up[2] + n[3]*Up[3]
|
|
local nf = n[1]*F[1] + n[2]*F[2] + n[3]*F[3]
|
|
-- a face the sun cannot see never samples its own depth, so it
|
|
-- cannot alias -- FACE_SHADE darkens those and the map is moot
|
|
if nf < 0 then
|
|
-- the plane's depth gradient in light space, times the half-texel
|
|
-- the 2x2 filter reaches out on each axis
|
|
local need = 0.5 * (math.abs(nr / nf) * texX
|
|
+ math.abs(nu / nf) * texY)
|
|
print(("[acne] %s cos %.3f slope %.2f needs %.2f px %s")
|
|
:format(fc[1], math.abs(nf),
|
|
math.sqrt((nr/nf)^2 + (nu/nf)^2), need,
|
|
need > ShadowMap.slack and "<-- ACNE" or "ok"))
|
|
else
|
|
print(("[acne] %s unlit (sun behind it)"):format(fc[1]))
|
|
end
|
|
end
|
|
end
|
|
|
|
local Zoom = require("src.render.Zoom")
|
|
Zoom.reset()
|
|
local steps = math.floor(tonumber(os.getenv("ACNE_ZOOM")) or 0)
|
|
for _ = 1, math.abs(steps) do
|
|
Zoom.step(steps > 0 and 1 or -1, game.renderer and game.renderer:fitScale())
|
|
end
|
|
|
|
Pipelines.setLevel("tiltshift", 0)
|
|
U.teleport(game, mapId, tonumber(sx), tonumber(sy), facing)
|
|
wait(20)
|
|
do
|
|
local vw, vh = game.renderer:worldViewSize()
|
|
print(("[acne] map %s at (%s,%s,%s) world view %dx%d px zoom %d")
|
|
:format(mapId, sx, sy, facing, vw, vh, Zoom.offset))
|
|
end
|
|
|
|
local shipped = Voxel3D.SHADOW_ALPHA
|
|
local shippedBias, shippedSlope = ShadowMap.BIAS, ShadowMap.SLOPE
|
|
|
|
-- ACNE_MATRIX=1: the four corners of (shadows on/off) x (voxel grid
|
|
-- on/off), so a band can be attributed to one of them rather than
|
|
-- guessed at. The grid is the other thing in this pass that draws
|
|
-- regular lines, and at a grazing angle it moires.
|
|
local VoxelGrid = V.require("VoxelGrid")
|
|
local matrix = os.getenv("ACNE_MATRIX") == "1"
|
|
|
|
local function shoot(name)
|
|
game.capturePath = ("%s/acne_%s.png"):format(DIR, name)
|
|
wait(4)
|
|
end
|
|
|
|
for _, level in ipairs(levels) do
|
|
Pipelines.setLevel("voxel", level)
|
|
wait(30) -- outlast the camera tween
|
|
local label = Pipelines.levelLabel("voxel", level) or level
|
|
print(("[acne] --- level %s shadowsActive=%s")
|
|
:format(label, tostring(Voxel3D.shadowsActive())))
|
|
report("as shipped")
|
|
shoot(("%s_v%s_on"):format(mapId, label))
|
|
|
|
if matrix then
|
|
for _, grid in ipairs({ true, false }) do
|
|
VoxelGrid.sync(grid)
|
|
for _, sun in ipairs({ true, false }) do
|
|
Voxel3D.SHADOW_ALPHA = sun and shipped or 0
|
|
wait(8)
|
|
shoot(("%s_v%s_grid%s_sun%s"):format(mapId, label,
|
|
grid and "1" or "0", sun and "1" or "0"))
|
|
end
|
|
end
|
|
Voxel3D.SHADOW_ALPHA = shipped
|
|
end
|
|
|
|
-- the sun pass still runs; the main pass simply stops reading it
|
|
Voxel3D.SHADOW_ALPHA = 0
|
|
wait(6)
|
|
shoot(("%s_v%s_off"):format(mapId, label))
|
|
Voxel3D.SHADOW_ALPHA = shipped
|
|
|
|
for _, b in ipairs(flats) do
|
|
ShadowMap.BIAS, ShadowMap.SLOPE = b, 0
|
|
ShadowMap.invalidate() -- force fit() to recompute ShadowMap.bias
|
|
wait(8)
|
|
report(("flat %.1f"):format(b))
|
|
shoot(("%s_v%s_flat%s"):format(mapId, label, tostring(b):gsub("%.", "p")))
|
|
end
|
|
ShadowMap.BIAS, ShadowMap.SLOPE = shippedBias, shippedSlope
|
|
ShadowMap.invalidate()
|
|
wait(6)
|
|
end
|
|
|
|
Pipelines.setLevel("voxel", 0)
|
|
wait(5)
|
|
print("[acne] done")
|
|
end
|