DramaticShapeVoxelMod archival mirror · read-only
tests/voxel_acne_probe.lua

voxel_acne_probe.lua

tests/voxel_acne_probe.lua6.7 KB10064431ae673111
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
-- 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