Files
2026-07-29 15:35:28 -04:00

138 lines
5.8 KiB
Lua

-- Overworld battles: the two mons, as geometry standing on the map.
--
-- Not pics composited over a picture of the world -- quads INSIDE it, drawn
-- in the same 3D pass as the terrain, from the same camera, through the same
-- shader. Which means they get everything the world gets and nothing has to
-- be faked for them: the depth buffer decides what is in front of what, the
-- sun pass sees them and throws their real silhouettes across the ground,
-- and their size on screen is whatever standing on that tile at that
-- distance actually looks like.
--
-- One quad, standing upright with its feet on the cell and yawed to face the
-- camera. Upright rather than leaned back, unlike the free-roam mode's
-- character cards: those lean because that camera looks DOWN and a standing
-- card would foreshorten to nothing, and this one looks along the ground
-- from about a foot above it, where a card standing up is simply correct.
--
-- The shader's alpha discard cuts the mon's exact outline out of the quad,
-- so a Pokemon is its own silhouette against the world with no matte, no
-- billboard edge and no sorting to get wrong.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local Mat4 = V.require("Mat4")
local Voxel3D = V.require("Voxel3D")
local BattleBillboard = {}
-- How wide a full-size (7x7 tile) mon stands, in world pixels. One overworld
-- square, so a Pokemon covers the tile it is on and no more; a species whose
-- pic is smaller than the full buffer comes out proportionally smaller,
-- which is how the artwork's own size differences survive the trip.
BattleBillboard.FULL_W = 16
BattleBillboard.FULL_PIC = 56 -- the pic size FULL_W refers to
-- A hair of camera-ward bias, so a card standing ON the ground plane wins
-- the depth test against it instead of z-fighting the tile it is rooted to.
BattleBillboard.PULL = 1.5
local quad = nil -- nil = untried, false = unavailable
-- The unit card: x in -0.5..0.5, y in 0..1, z = 0, UV over the whole
-- texture. Feet on the model origin, so the model matrix only has to say
-- where the mon is standing and how big it is.
--
-- Which puts this card OFF the voxel grid, alone among the meshes in this
-- mode: the rest are built one unit per voxel in their own model space --
-- terrain in world pixels, a character's card in the sprite's own pixels --
-- and the wireframe is the integer planes of that space (see VoxelGrid).
-- One unit here is the whole card, so the only integer plane inside it is
-- x = 0, which is the pic's centre column: a single stray line straight
-- down the middle of every Pokemon and no seams anywhere else.
--
-- The card stays a unit card, because a mon's size on screen is decided by
-- the artwork's own dimensions and the distance it is standing at, and a
-- unit card is what lets one matrix say both. Whoever draws it turns the
-- wireframe off instead (Voxel3D.seams) -- a mesh that is not on the voxel
-- grid does not get a voxel grid drawn on it.
local function unitQuad()
if quad ~= nil then return quad or nil end
local verts = {
{ -0.5, 0, 0, 0, 1, 1 },
{ 0.5, 0, 0, 1, 1, 1 },
{ 0.5, 1, 0, 1, 0, 1 },
{ -0.5, 1, 0, 0, 0, 1 },
}
local indices = {}
Voxel3D.pushQuad(indices, 0)
local mesh = Voxel3D.newMesh(verts, indices)
quad = mesh or false
return quad or nil
end
BattleBillboard.mesh = unitQuad
-- The yaw that turns the card's face toward the eye. The quad's normal is
-- +Z before rotation, so this is just the bearing from the mon to the
-- camera -- flattened to the horizontal, because a card that also tipped to
-- face a camera above it would lift its feet off the floor.
function BattleBillboard.yawToward(x, z, eye)
if not eye then return 0 end
return math.atan2(eye[1] - x, eye[3] - z)
end
-- Stand a `w` x `h` card with its feet centred on world (x, y, z).
function BattleBillboard.matrix(x, y, z, w, h, yaw)
return Mat4.mul(Mat4.mul(Mat4.translate(x, y, z), Mat4.rotateY(yaw)),
Mat4.scale(w, h, 1))
end
-- The world size a pic of `pw` x `ph` texture pixels stands at.
function BattleBillboard.sizeFor(pw, ph)
if not (pw and ph and pw > 0 and ph > 0) then return 0, 0 end
local scale = BattleBillboard.FULL_W / BattleBillboard.FULL_PIC
return pw * scale, ph * scale
end
-- Draw one mon. `tex` is the pic already rendered to a texture (see
-- OverworldBattle, which lets the engine's own battler draw produce it, so
-- every faint slide, blink and squish comes along), `grow` the send-out
-- animation's scale or nil.
function BattleBillboard.draw(tex, x, y, z, grow)
local mesh = unitQuad()
if not (mesh and tex) then return false end
local pw, ph = tex:getDimensions()
local w, h = BattleBillboard.sizeFor(pw, ph)
if grow then w, h = w * grow, h * grow end
if w <= 0 or h <= 0 then return false end
local yaw = BattleBillboard.yawToward(x, z, Voxel3D.eye)
-- off the voxel grid, so no wireframe on it (see unitQuad)
Voxel3D.seams(false)
Voxel3D.draw(mesh, tex, BattleBillboard.matrix(x, y, z, w, h, yaw),
BattleBillboard.PULL)
Voxel3D.seams(true)
return true
end
-- The same card, as the SUN sees it: no camera-ward pull (that is a trick
-- for the view's own depth buffer and would drag the shadow off its owner)
-- and no draw call of its own, because the shadow pass has its own.
function BattleBillboard.caster(shadowMap, tex, x, y, z, grow)
local mesh = unitQuad()
if not (mesh and tex) then return false end
local pw, ph = tex:getDimensions()
local w, h = BattleBillboard.sizeFor(pw, ph)
if grow then w, h = w * grow, h * grow end
if w <= 0 or h <= 0 then return false end
local yaw = BattleBillboard.yawToward(x, z, Voxel3D.eye)
shadowMap.draw(mesh, tex, BattleBillboard.matrix(x, y, z, w, h, yaw))
return true
end
function BattleBillboard.invalidate()
quad = nil
end
return BattleBillboard