mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:40:52 +02:00
mitigate diagonal shadow artifacts
This commit is contained in:
+43
-2
@@ -77,7 +77,44 @@ ShadowMap.HEIGHT = 160
|
||||
-- surface shadows itself in a moire of acne; too much and a shadow detaches
|
||||
-- from the foot of what casts it. The frustum is ~400 world pixels deep and
|
||||
-- the packed depth resolves under 0.01 of one, so there is room.
|
||||
ShadowMap.BIAS = 1.0
|
||||
--
|
||||
-- It cannot be ONE number, because what the comparison has to forgive is
|
||||
-- not fixed: the map stores one depth for a whole texel, so a lit surface
|
||||
-- reads its own depth wrong by however far it RAMPS across that texel --
|
||||
-- the texel's world size times the surface's slope in the light's frame.
|
||||
-- The texel swings from a third of a world pixel at the closest zoom to
|
||||
-- well over one at a maximised window on the widest, so a constant bias is
|
||||
-- generous at one end of the ladder and short at the other. Short shows up
|
||||
-- as diagonal bands of acne across big lit surfaces -- diagonal because
|
||||
-- the moire runs along neither the world grid nor the screen's, but along
|
||||
-- the depth ramp in the sun's own frame, and the sun sits southeast.
|
||||
--
|
||||
-- So: a floor for what does not scale (the packed depth's quantisation,
|
||||
-- and the two passes reaching the same world point by different matrices),
|
||||
-- plus a term in texels for what does.
|
||||
ShadowMap.BIAS = 0.5
|
||||
|
||||
-- World pixels of slack per world pixel of texel, for the steepest LIT
|
||||
-- surface here: a roof pitched 45 degrees and turned away from the sun,
|
||||
-- whose depth ramps about 3.1 world pixels per texel crossed on EITHER of
|
||||
-- the light frame's two axes (a vertical wall, by comparison, manages 1.7,
|
||||
-- flat ground 0.7, and anything steeper than that roof has its back to the
|
||||
-- sun and never reads the map at all). The 2x2 filter's taps sit half a
|
||||
-- texel out on both axes at once, so the worst a tap can disagree by is
|
||||
-- half the ramp along each -- which is where the halving that turns 6.2
|
||||
-- into 3.1 comes from, and why it is the SUM of the two components rather
|
||||
-- than their magnitude.
|
||||
--
|
||||
-- Measured against the artefact rather than trusted: the probe
|
||||
-- (tests/voxel_acne_probe.lua) counts isolated shadowed pixels on lit
|
||||
-- surfaces, and the banding stops at slack ~2.4 world px on the widest
|
||||
-- rung -- where this lands 3.1 * 0.83 + 0.5.
|
||||
ShadowMap.SLOPE = 3.1
|
||||
|
||||
-- The slack `fit` last worked out, in world pixels -- BIAS + SLOPE*texel.
|
||||
-- Read by probes; `ShadowMap.bias` is the same number as the [0,1] depth
|
||||
-- the map actually stores.
|
||||
ShadowMap.slack = ShadowMap.BIAS
|
||||
|
||||
local SHADER = [[
|
||||
varying float vDepth;
|
||||
@@ -316,9 +353,13 @@ local function fit(cx, cy, vw, vh)
|
||||
-- what the frustum ended up covering, for probes: the lateral extent in
|
||||
-- world pixels divided by RES is how fine a shadow edge can land
|
||||
ShadowMap.extent = { r - l, t - b, far - near }
|
||||
-- the slack the comparison needs, against the coarser of the two texel
|
||||
-- axes (the box is asymmetric, and one number has to cover both)
|
||||
ShadowMap.slack = ShadowMap.BIAS
|
||||
+ ShadowMap.SLOPE * math.max(w, h) / res
|
||||
-- the stored depth spans the frustum, so a world-pixel bias is that
|
||||
-- fraction of it
|
||||
ShadowMap.bias = ShadowMap.BIAS / math.max(1, far - near)
|
||||
ShadowMap.bias = ShadowMap.slack / math.max(1, far - near)
|
||||
end
|
||||
|
||||
-- Whether the map has to be redrawn for `sig` -- a caller-built stamp of
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
-- The sky, generated rather than shipped.
|
||||
--
|
||||
-- The overworld's, on every VOXEL rung. Wherever the diorama is drawn the void
|
||||
-- behind it is sky rather than a black plate: at 75 degrees the horizon is
|
||||
-- genuinely in frame and the bands run down to meet it, and at the steeper rungs
|
||||
-- the void that shows is the ground running out past the map edge, which gets
|
||||
-- the same sky above the same haze. A battle's placed camera keeps the flat fill
|
||||
-- it has always had -- its horizon is above the frame and its look is not this
|
||||
-- rung's to change.
|
||||
--
|
||||
-- THE RECIPE is the 8-bit skybox one: a short palette of blues painted as flat
|
||||
-- horizontal bands, deepest overhead, with a CHECKERBOARD of the next band
|
||||
-- dithered into the bottom of each one. Alternating two colours on a pixel grid
|
||||
-- is how a machine with four colours to a palette got a fifth, sixth and seventh
|
||||
-- out of them, and it is what keeps four bands reading as a gradient rather than
|
||||
-- as four stripes. No clouds, nothing moving.
|
||||
--
|
||||
-- NOTHING IS RESAMPLED, which is the whole of why it is drawn this way. There is
|
||||
-- no baked 160x144 picture scaled up to the window, no downsized buffer blown
|
||||
-- back up, no texture of any kind: one full-region rectangle through a shader
|
||||
-- that answers every pixel from its own canvas coordinate. A pixel of sky is
|
||||
-- computed at the size it is displayed at, so there is nothing for a filter to
|
||||
-- soften and nothing to go stale when the window or the zoom changes.
|
||||
--
|
||||
-- THE PIXEL GRID follows the zoom for the same reason. Bands and dither cells
|
||||
-- are measured in DIORAMA pixels -- the pass's own pixels-per-world-pixel, handed
|
||||
-- in fresh every frame -- so a chunky sky at 4x is a chunky sky at 12x, band
|
||||
-- edges land on the same grid the world's own texels do, and a ZOOM keypress is
|
||||
-- reflected in the frame that follows it rather than whenever something else
|
||||
-- happened to rebuild.
|
||||
--
|
||||
-- PALETTE ORDER, which is easy to get wrong. Stored LIGHTEST FIRST, because that
|
||||
-- is shade order: a display mode transforms a four-colour palette by replacing it
|
||||
-- outright (PaletteFX.effectiveColors hands back GRAYS or CLASSIC), and those are
|
||||
-- written light to dark. So the sky reads the list backwards -- shade 4 overhead,
|
||||
-- shade 1 at the horizon -- and GRAY gets four greys the right way up for
|
||||
-- nothing.
|
||||
|
||||
-- the mod namespace (see main.lua): V.require loads a sibling module
|
||||
local V = ...
|
||||
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
|
||||
local unpack = table.unpack or unpack
|
||||
|
||||
local Sky = {}
|
||||
|
||||
-- Lightest first. Every channel is a multiple of 8, which is where a five-bit
|
||||
-- GBC channel lands: these are colours the hardware could actually have shown,
|
||||
-- not blues picked off a 24-bit colour wheel.
|
||||
Sky.PALETTE = {
|
||||
{ 144, 192, 248 }, -- shade 1: pale, at the horizon
|
||||
{ 104, 160, 240 },
|
||||
{ 72, 128, 224 },
|
||||
{ 48, 96, 200 }, -- shade 4: deep, overhead
|
||||
}
|
||||
|
||||
-- The shader carries a fixed-size array, because a GLSL uniform array is a fixed
|
||||
-- size; four is also what a display mode has to give (it substitutes a palette),
|
||||
-- so this is the ceiling on the palette above rather than an arbitrary cap.
|
||||
Sky.MAX_BANDS = 4
|
||||
|
||||
-- The checkerboard between bands. DITHER_START is how far down a band it begins,
|
||||
-- as a fraction of that band: lower is a wider blend, and 1 switches it off. 0.6
|
||||
-- leaves the top of each band flat -- a band dithered all the way through reads
|
||||
-- as one averaged colour instead of as a step with a soft bottom edge.
|
||||
Sky.DITHER = true
|
||||
Sky.DITHER_START = 0.6
|
||||
|
||||
-- How much of the frame the bands cover when the horizon is NOT in it, as a
|
||||
-- fraction of the canvas height.
|
||||
--
|
||||
-- At the steeper rungs the camera looks down far enough that the ground plane's
|
||||
-- vanishing line is above the top edge -- there is no horizon to hang the pale
|
||||
-- end on, but there is still void up there where the map runs out, and it should
|
||||
-- read as sky. So the bands take the same slice of the frame the top rung's own
|
||||
-- horizon gives them, which keeps the sky looking like one sky across the whole
|
||||
-- ladder instead of changing character rung by rung.
|
||||
Sky.SPAN = 0.23
|
||||
|
||||
-- ------- the bands
|
||||
--
|
||||
-- Top first, each a { r, g, b } in 0..1, as the display mode has them.
|
||||
--
|
||||
-- Memoised, because this runs once a frame and the answer only moves when the
|
||||
-- mode does.
|
||||
local cache = { bands = nil, key = {} }
|
||||
|
||||
function Sky.bands()
|
||||
local shades = PaletteFX.effectiveColors(Sky.PALETTE) or Sky.PALETTE
|
||||
local n = math.min(#shades, #Sky.PALETTE, Sky.MAX_BANDS)
|
||||
local key, k = cache.key, 0
|
||||
local same = cache.bands ~= nil and #cache.bands == n
|
||||
for i = 1, n do
|
||||
local c = shades[i]
|
||||
for ch = 1, 3 do
|
||||
k = k + 1
|
||||
if key[k] ~= c[ch] then same = false end
|
||||
key[k] = c[ch]
|
||||
end
|
||||
end
|
||||
if same then return cache.bands end
|
||||
|
||||
local bands = {}
|
||||
for i = 1, n do
|
||||
-- backwards: the palette's darkest rung is the top band
|
||||
local c = shades[n - i + 1]
|
||||
bands[i] = { c[1] / 255, c[2] / 255, c[3] / 255 }
|
||||
end
|
||||
cache.bands = bands
|
||||
return bands
|
||||
end
|
||||
|
||||
-- Put the sky onto a flat descriptor: the bands to paint, plus the flat fill
|
||||
-- replaced by the palest of them. That fill is what the caller CLEARS to, so
|
||||
-- making it the bottom band's own colour means the haze below the sky and the
|
||||
-- bottom of the sky are one colour -- the join has no seam, and a frame that
|
||||
-- cannot paint the bands is a hazy sky rather than a wrong one.
|
||||
--
|
||||
-- Mutates the descriptor, which is a fresh table per frame from its caller.
|
||||
function Sky.dress(sky)
|
||||
local bands = Sky.bands()
|
||||
local haze = bands and bands[#bands]
|
||||
if not (sky and haze) then return sky end
|
||||
sky[1], sky[2], sky[3] = haze[1], haze[2], haze[3]
|
||||
sky.bands = bands
|
||||
return sky
|
||||
end
|
||||
|
||||
-- Where the sky's bottom edge goes, in canvas pixels: the camera's own horizon
|
||||
-- when that is in frame, and SPAN of the frame when it is not (see SPAN). nil
|
||||
-- when there is no room for any of it.
|
||||
function Sky.region(h, horizonY)
|
||||
if not (h and h > 0) then return nil end
|
||||
local edge = horizonY
|
||||
if not (edge and edge > 0) then edge = h * Sky.SPAN end
|
||||
edge = math.min(edge, h)
|
||||
if edge < 1 then return nil end
|
||||
return edge
|
||||
end
|
||||
|
||||
-- ------- the pass
|
||||
--
|
||||
-- One rectangle, one shader, no texture. Every pixel answers for itself from its
|
||||
-- canvas coordinate, so the sky is drawn at exactly the resolution it is
|
||||
-- displayed at -- there is no image being scaled and so nothing to be soft.
|
||||
--
|
||||
-- `cell` quantises BOTH the band edges and the dither: the y a pixel is judged
|
||||
-- by is the top of its own cell row, so a whole cell row is one colour and every
|
||||
-- edge in the sky lands on the diorama's pixel grid.
|
||||
local SHADER_SRC = [[
|
||||
#define MAXB %d
|
||||
uniform vec3 bands[MAXB];
|
||||
uniform int count;
|
||||
uniform float edge; // the sky's bottom, in canvas pixels
|
||||
uniform float cell; // the diorama's pixel size, in canvas pixels
|
||||
uniform float start; // where the checker begins inside a band
|
||||
uniform float alpha;
|
||||
|
||||
// Indexed through a loop counter, which every GLSL ES compiler accepts for a
|
||||
// uniform array; a bare bands[idx] is not portable.
|
||||
vec3 bandAt(int idx) {
|
||||
vec3 c = bands[0];
|
||||
for (int i = 1; i < MAXB; i++) {
|
||||
if (i == idx) { c = bands[i]; }
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||
float n = float(count);
|
||||
float row = floor(sc.y / cell) * cell; // top of this cell row
|
||||
float pos = clamp(row / max(edge, 1.0), 0.0, 0.999999) * n;
|
||||
float base = floor(pos);
|
||||
int idx = int(base);
|
||||
vec3 c = bandAt(idx);
|
||||
if (idx < count - 1 && (pos - base) > start) {
|
||||
float parity = mod(floor(sc.x / cell) + floor(sc.y / cell), 2.0);
|
||||
if (parity < 0.5) { c = bandAt(idx + 1); }
|
||||
}
|
||||
return vec4(c, alpha);
|
||||
}
|
||||
]]
|
||||
|
||||
local shader = nil -- nil = untried, false = unavailable
|
||||
|
||||
local function getShader()
|
||||
if shader == nil then
|
||||
shader = false
|
||||
if love.graphics and love.graphics.newShader then
|
||||
local ok, sh = pcall(love.graphics.newShader,
|
||||
SHADER_SRC:format(Sky.MAX_BANDS))
|
||||
if ok and sh then
|
||||
shader = sh
|
||||
elseif V and V.mod and V.mod.log then
|
||||
-- once, and only where it can be read: the fallback below is a sky
|
||||
-- without its dither, which is easy to look at and impossible to
|
||||
-- diagnose without this line
|
||||
V.mod.log:warn("sky shader did not compile: %s -- the bands draw flat, "
|
||||
.. "with no dither between them", tostring(sh))
|
||||
end
|
||||
end
|
||||
end
|
||||
return shader or nil
|
||||
end
|
||||
|
||||
Sky._getShader = getShader -- named for the suite
|
||||
|
||||
-- The flat fallback: the same bands as solid rectangles, no checker, on the same
|
||||
-- quantised edges. For a driver that could not compile the shader -- which is
|
||||
-- also every headless run.
|
||||
local function paintFlat(w, h, bands, edge, alpha, cell)
|
||||
local g = love.graphics
|
||||
local n = #bands
|
||||
local prev = 0
|
||||
for i = 1, n do
|
||||
local cut = (i == n) and math.min(h, math.ceil(edge))
|
||||
or math.floor(i / n * edge / cell + 0.5) * cell
|
||||
cut = math.max(prev, math.min(cut, math.min(h, math.ceil(edge))))
|
||||
if cut > prev then
|
||||
local c = bands[i]
|
||||
g.setColor(c[1], c[2], c[3], alpha)
|
||||
g.rectangle("fill", 0, prev, w, cut - prev)
|
||||
end
|
||||
prev = cut
|
||||
end
|
||||
end
|
||||
|
||||
-- Paint the sky into the bound canvas, filling it from the top edge down to
|
||||
-- `horizonY` (or to SPAN of the frame when the horizon is out of it).
|
||||
--
|
||||
-- `cell` is the diorama's pixel size in canvas pixels -- the pass's own
|
||||
-- pixels-per-world-pixel, handed in every frame so a zoom lands immediately.
|
||||
--
|
||||
-- Returns false when there is nothing to paint, in which case the caller's flat
|
||||
-- fill is the whole sky. That fill is the palest band, so a frame that declines
|
||||
-- this looks like a hazy day rather than like a bug.
|
||||
function Sky.paint(w, h, sky, horizonY, cell)
|
||||
local bands = sky and sky.bands
|
||||
if not (bands and bands[1]) then return false end
|
||||
if not (w and h and w > 0 and h > 0) then return false end
|
||||
local g = love.graphics
|
||||
if not (g and g.rectangle) then return false end
|
||||
local edge = Sky.region(h, horizonY)
|
||||
if not edge then return false end
|
||||
local alpha = sky[4] or 1
|
||||
cell = math.max(1, math.floor((cell or 1) + 0.5))
|
||||
|
||||
-- State to put aside. The scene's shader is one, and the blend mode another --
|
||||
-- a pass that left "replace" behind would make the fade-in strength meaningless
|
||||
-- -- but the DEPTH MODE is the one that would break the frame: a rectangle
|
||||
-- drawn under the pass's own ("lequal", true) stamps itself across the depth
|
||||
-- buffer at the near plane and hides the entire world behind the sky.
|
||||
local prevShader = g.getShader and g.getShader() or nil
|
||||
local cmp, write
|
||||
if g.getDepthMode then cmp, write = g.getDepthMode() end
|
||||
if g.setDepthMode then g.setDepthMode("always", false) end
|
||||
local blend, blendAlpha
|
||||
if g.getBlendMode then blend, blendAlpha = g.getBlendMode() end
|
||||
if g.setBlendMode then g.setBlendMode("alpha") end
|
||||
|
||||
local sh = getShader()
|
||||
if sh then
|
||||
local sent = pcall(function()
|
||||
-- one send per band would be one uniform lookup per band; the array takes
|
||||
-- them all at once, and it must be the LAST argument or Lua truncates the
|
||||
-- unpack to a single value
|
||||
sh:send("bands", unpack(bands))
|
||||
sh:send("count", #bands)
|
||||
sh:send("edge", edge)
|
||||
sh:send("cell", cell)
|
||||
sh:send("start", Sky.DITHER and Sky.DITHER_START or 2)
|
||||
sh:send("alpha", alpha)
|
||||
end)
|
||||
if sent then
|
||||
g.setShader(sh)
|
||||
g.setColor(1, 1, 1, 1)
|
||||
g.rectangle("fill", 0, 0, w, math.min(h, math.ceil(edge)))
|
||||
g.setShader()
|
||||
else
|
||||
sh = nil
|
||||
end
|
||||
end
|
||||
if not sh then paintFlat(w, h, bands, edge, alpha, cell) end
|
||||
g.setColor(1, 1, 1, 1)
|
||||
|
||||
if g.setBlendMode and blend then g.setBlendMode(blend, blendAlpha) end
|
||||
if g.setDepthMode then g.setDepthMode(cmp or "always", write or false) end
|
||||
if prevShader and g.setShader then g.setShader(prevShader) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Drop the compiled shader (window resize, hot reload), so a re-created graphics
|
||||
-- context builds a new one instead of drawing with a handle from the old.
|
||||
function Sky.invalidate()
|
||||
shader = nil
|
||||
end
|
||||
|
||||
return Sky
|
||||
@@ -0,0 +1,175 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user