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

voxel_shadow_probe.lua

tests/voxel_shadow_probe.lua4.9 KB1006445cf9178e5b
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
-- Driver: prove out the voxel sun pass.
--
-- Reports whether the shadow map can run, dumps the map itself as a PNG
-- (the packed depth reads as a red/green gradient -- what matters is that
-- it is populated and the right way up), and screenshots a stand point at
-- every camera pitch so the cast shadows can be eyeballed against the flat
-- ground truth.
--
--   POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_shadow_probe.lua lovec .
--
-- knobs (env):
--   SHADOW_MAP    map id                        (default PALLET_TOWN)
--   SHADOW_SPOT   "x,y[,facing]"                (default 5,6,down)
--   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("SHADOW_MAP") or "PALLET_TOWN"
  local sx, sy, facing = (os.getenv("SHADOW_SPOT") or "5,6,down")
    :match("^%s*(%d+)%s*,%s*(%d+)%s*,?%s*(%a*)")
  facing = (facing ~= "" and facing) or "down"

  -- reach the mod's lib namespace the way a companion mod would
  local handle = game.mods.exports["DRAMATIC_SHAPE"]
  local V = handle and handle.lib
  assert(V, "DRAMATIC_SHAPE exports not reachable")
  local ShadowMap = V.require("ShadowMap")
  local Voxel3D = V.require("Voxel3D")
  local VoxelGrid = V.require("VoxelGrid")

  -- SHADOW_GRID=1 forces the voxel wireframe on for the run, and
  -- SHADOW_CURVE=n the curved-world rung -- neither touches the player's
  -- persisted setting
  if os.getenv("SHADOW_GRID") == "1" then
    VoxelGrid.sync(true)
    print("[shadow] voxel grid forced on; shader built="
          .. tostring(Voxel3D.shader(true) ~= nil))
  end
  local WorldCurve = V.require("WorldCurve")
  local curve = math.floor(tonumber(os.getenv("SHADOW_CURVE")) or 0)
  if curve > 0 then
    WorldCurve.sync(curve)
    print(("[shadow] world curve rung %d (amount %.2f)")
      :format(curve, WorldCurve.AMOUNTS[curve + 1] or 0))
  end

  -- SHADOW_SUN="kx,kz" retunes the bearing for one run, so a sun can be
  -- compared against another without an edit-rebuild cycle
  local skx, skz = (os.getenv("SHADOW_SUN") or ""):match("^(-?[%d.]+),(-?[%d.]+)$")
  if skx then
    ShadowMap.KX, ShadowMap.KZ = tonumber(skx), tonumber(skz)
    Voxel3D.SHADOW_KX, Voxel3D.SHADOW_KZ = ShadowMap.KX, ShadowMap.KZ
  end

  print(("[shadow] sun shear kx=%.2f kz=%.2f  alpha=%.2f  res=%d")
    :format(ShadowMap.KX, ShadowMap.KZ, Voxel3D.SHADOW_ALPHA, ShadowMap.res))

  -- pin the zoom: the world view size drives the light frustum, and a run
  -- that inherits whatever the player left in options.lua is not
  -- comparable with the one before it
  local Zoom = require("src.render.Zoom")
  Zoom.reset()
  local steps = math.floor(tonumber(os.getenv("SHADOW_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(("[shadow] world view %dx%d px, zoom offset %d")
      :format(vw, vh, Zoom.offset))
  end

  Pipelines.setLevel("voxel", 0)
  wait(25)
  game.capturePath = ("%s/shadow_%s_flat.png"):format(DIR, mapId)
  wait(3)

  print("[shadow] available=" .. tostring(ShadowMap.available()))

  for level = 1, Pipelines.maxLevel("voxel") do
    Pipelines.setLevel("voxel", level)
    wait(30)                      -- outlast the 0.25s camera tween
    local label = Pipelines.levelLabel("voxel", level) or level
    game.capturePath = ("%s/shadow_%s_v%s.png"):format(DIR, mapId, label)
    wait(3)
    local e = ShadowMap.extent or { 0, 0, 0 }
    print(("[shadow] level %s active=%s bias=%.6f frustum=%.0fx%.0f deep %.0f"
           .. " (%.2f world px/texel)")
      :format(label, tostring(ShadowMap.active()), ShadowMap.bias,
              e[1], e[2], e[3], e[1] / ShadowMap.res))
  end

  -- the map itself: packed depth, so red is the high byte of "how far the
  -- sun got". A blank (all-white) dump means nothing was drawn into it.
  local canvas = ShadowMap.texture()
  if canvas and canvas.newImageData then
    local ok, err = pcall(function()
      local data = canvas:newImageData()
      local w, h = data:getDimensions()
      local hit = 0
      for y = 0, h - 1, 8 do
        for x = 0, w - 1, 8 do
          local r = data:getPixel(x, y)
          if r < 0.999 then hit = hit + 1 end
        end
      end
      print(("[shadow] map %dx%d, %d/%d sampled texels written")
        :format(w, h, hit, (w / 8) * (h / 8)))
      data:encode("png", "shadowmap.png")
    end)
    print("[shadow] dump " .. (ok and "ok (save dir/shadowmap.png)"
                               or tostring(err)))
  end

  Pipelines.setLevel("voxel", 0)
  wait(5)
  print("[shadow] done")
end