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

diorama_shots.lua

tests/diorama_shots.lua6.2 KB100644feafb2049e
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
-- Driver: the DIORAMA mode's own frame, without a headset.
--
-- The diorama is drawn through VoxelScene's `eyes` path, and that path only
-- ever runs from lib/VR -- so with no OpenXR runtime on the machine there is
-- nothing to look at and nothing to check. This builds ONE eye by hand (the
-- same VRRig mapping the headset would have built), opens a diorama frame,
-- and encodes the eye canvas straight to a PNG.
--
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/diorama_shots.lua \
--   SHOT_DIR=<dir> lovec.exe .
--
-- knobs (env):
--   SHOT_DIR    output directory under the LOVE save dir (default "diorama")
--   DIO_MAP     map id                        (default VIRIDIAN_CITY)
--   DIO_SPOT    "x,y[,facing]"                (default 20,26,up)
--   DIO_RUNG    the voxel camera rung         (default 5, the 75 one)
--
-- It also prints whether each shader the mode touches actually COMPILED,
-- which is the part a headless suite cannot answer: the cull is new source
-- in the scene shader, the water shader and both forest ones, and a shader
-- that will not build fails soft everywhere in this mod -- the picture just
-- quietly loses a pass.
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local Pipelines = require("src.render.Pipelines")

  local ROOT = os.getenv("SHOT_DIR") or "diorama"

  local handle = game.mods.exports["DRAMATIC_SHAPE"]
  if not (handle and handle.lib) then
    print("[dio] DRAMATIC_SHAPE mod not loaded -- nothing to shoot")
    return
  end
  local V = handle.lib
  local Voxel = V.require("VoxelState")
  local Voxel3D = V.require("Voxel3D")
  local VoxelScene = V.require("VoxelScene")
  local VRRig = V.require("VRRig")
  local Diorama = V.require("Diorama")
  local Water = V.require("Water")
  local ChunkMesher = V.require("ChunkMesher")
  local DayNight = V.require("DayNight")
  local VR = V.require("VR")
  local Curve = V.require("WorldCurve")

  local MAP = os.getenv("DIO_MAP") or "VIRIDIAN_CITY"
  local SPOT = os.getenv("DIO_SPOT") or "20,26,up"
  local RUNG = math.floor(tonumber(os.getenv("DIO_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

  require("src.world.OverworldController").rollEncounter = function() return nil end
  DayNight.setting:sync("day")
  U.teleport(game, MAP, sx, sy, sf)
  Pipelines.setLevel("voxel", RUNG)
  Pipelines.setLevel("tiltshift", 0)

  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
    U.wait(20)
  end
  settle()
  settle()

  print(("[dio] scene shader %s | grid variant %s | water %s")
    :format(tostring(Voxel3D.shader() ~= nil),
            tostring(Voxel3D.shader(true) ~= nil),
            tostring(Water.shader(false) ~= nil)))

  -- one eye, straight ahead from the resting head: the mapping lib/VR would
  -- have built for this rung, with no pose to read off a runtime
  local POSE = { pos = { 0, 0, 0 }, quat = { 0, 0, 0, 1 } }
  local FOV = { angleLeft = -0.8, angleRight = 0.8,
                angleUp = 0.7, angleDown = -0.7 }

  local function shoot(name, opts)
    local ow = game.overworld
    local vw, vh = 320, 288
    pcall(function() vw, vh = game.renderer:worldViewSize() end)
    local cx = ow.camera.x + vw / 2
    local cy = ow.camera.y + vh / 2

    Diorama.begin(opts.mode or "diorama")
    Diorama.zoom = opts.zoom or 1
    Diorama.yaw = opts.yaw or 0
    -- the cut's SHAPE is the V-CURVE row's (box while flat, ball while
    -- bent), so the driver throws the row rather than asking for a shape
    Curve.setting:sync(opts.curve or 0)
    if opts.arena then
      Diorama.pillar(opts.arena)
    else
      Diorama.viewport(cx, cy, vh)
    end
    local pivot = VRRig.dioramaPivot(Diorama.cull.x, Diorama.cull.z)
    local anchor = VRRig.dioramaAnchor(Voxel.angle, Diorama.offset)
    -- the same framing lib/VR picks: the view ordinarily, the DISC while a
    -- fight is staged
    local frame = opts.arena and (Diorama.cull.r * 2.6) or vh
    local scale = VRRig.dioramaScale(frame, Voxel.FOCAL)
    -- the bend the diorama asks its eyes for (lib/VR does the same)
    local curveK = Curve.k(vh)
    local eyes = {
      { camera = VRRig.eyeCamera(POSE, FOV, pivot, anchor, scale,
                                 opts.yaw ~= 0 and opts.yaw or nil, curveK),
        w = 720, h = 720, slot = "vrL", adopt = false },
      cx = pivot[1], cy = pivot[3],
    }
    VoxelScene.spriteLean = math.rad(75)
    local ok, out = pcall(VoxelScene.render, ow, 0, 0, vw, vh,
                          VR.paletteFor, eyes)
    VoxelScene.spriteLean = nil
    if not (ok and out and out[1]) then
      print("[dio] " .. name .. ": no frame (" .. tostring(out) .. ")")
      Diorama.stop()
      return
    end
    local okE, err = pcall(function()
      love.filesystem.createDirectory(ROOT)
      local data = out[1]:newImageData()
      data:encode("png", ROOT .. "/" .. name .. ".png")
    end)
    print("[dio] " .. name
          .. (okE and " written" or (" ENCODE FAILED: " .. tostring(err))))
    Diorama.stop()
  end

  shoot("box", { mode = "diorama" })
  shoot("box-turned", { mode = "diorama", yaw = math.rad(35) })
  shoot("box-tight", { mode = "diorama", zoom = 0.5 })
  shoot("ball-3", { mode = "diorama", curve = 3 })
  shoot("ball-4", { mode = "diorama", curve = 4 })
  shoot("ball-5", { mode = "diorama", curve = 5 })
  shoot("ball-wide", { mode = "diorama", curve = 5, zoom = 1.9 })
  shoot("keyed", { mode = "diorama-mr" })
  shoot("keyed-ball", { mode = "diorama-mr", curve = 3 })
  do
    -- a fight's disc, cut about the arena the map would actually stage on
    local BattleArena = V.require("BattleArena")
    local ow = game.overworld
    local arena = BattleArena.find(ow.map, ow.player.cellX, ow.player.cellY,
                                   false)
    if arena then
      shoot("arena-disc", { mode = "diorama", arena = arena })
    else
      print("[dio] no arena on this map -- disc shot skipped")
    end
  end

  Diorama.stop()
  print("[dio] done; PNGs are under the LOVE save directory / " .. ROOT)
  love.event.quit()
end