mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 10:00:50 +02:00
lets go
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
-- A Poke Ball as real geometry: the prop the LET'S GO capture mode throws.
|
||||
--
|
||||
-- The mod has never drawn a ball in 3D -- the one the engine tosses is a 2D
|
||||
-- sprite inside the battle's move-animation layer. This is a ball that can
|
||||
-- fly through the arena, hang in the air in front of the camera, hinge its
|
||||
-- lid open, drink a Pokemon in, click shut, rock on the ground and burst
|
||||
-- back open -- all of it depth-tested, sun-shadowed and hour-tinted like
|
||||
-- everything else in the diorama, because it is a mesh in Voxel3D's own
|
||||
-- format going through Voxel3D's own shader.
|
||||
--
|
||||
-- ------- how it is built
|
||||
--
|
||||
-- Two lat/long hemisphere shells that meet at the equator -- the WHITE base
|
||||
-- and the coloured LID -- each carrying its half of the black band as a
|
||||
-- slightly bulged latitude belt, so the two halves separate exactly where
|
||||
-- the real ball separates. The button is a little cylinder standing out of
|
||||
-- the base's front; the interior is sealed with two pale discs so an open
|
||||
-- ball shows a shell with a floor rather than a view through to the far
|
||||
-- wall's backface. Colour is a palette texture one texel per material and
|
||||
-- one ROW per ball tier (POKE/GREAT/ULTRA/MASTER/SAFARI), exactly the
|
||||
-- HordeGun/Pokedex scheme -- so GREAT is blue and ULTRA wears its yellow
|
||||
-- band without a second mesh, just a different V coordinate.
|
||||
--
|
||||
-- Shade is baked per vertex from the surface normal with StadiumStage's
|
||||
-- fitted constants, which is this mod's answer for anything curved: the
|
||||
-- ball's sun side and belly read as a sphere under the same southeastern
|
||||
-- sun the roofs are lit by.
|
||||
--
|
||||
-- ------- how it animates
|
||||
--
|
||||
-- The HordeGun way: a handful of scalar timers advanced by update(dt) and
|
||||
-- consumed as matrix terms at draw time. No skeleton, no keyframes --
|
||||
-- lid is a hinge matrix about the back of the equator, the wobble is a
|
||||
-- decaying rotateZ about the ground contact point, the caught click is a
|
||||
-- squash pulse, the stars are one shared quad drawn a few times facing the
|
||||
-- eye. The ball owns its POSE only; where it IS (the throw arc, the drop)
|
||||
-- is the caller's problem, which is what keeps this file a prop and not a
|
||||
-- game mode.
|
||||
--
|
||||
-- Nothing here touches love.* until something has to be drawn, so the
|
||||
-- module loads and the state machine runs headless -- the test suite
|
||||
-- exercises the phases without a GPU.
|
||||
|
||||
-- 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 Pokeball = {}
|
||||
Pokeball.__index = Pokeball
|
||||
|
||||
-- ------- the ball's measurements, in world pixels
|
||||
--
|
||||
-- A map cell is 16 and a full-size mon card is 16 wide, so a 4.4-pixel ball
|
||||
-- sits in the hand and against a Pokemon at about the proportion the games
|
||||
-- draw: unmissable in the foreground, believable at the far cell.
|
||||
Pokeball.R = 2.2
|
||||
|
||||
-- the black belt: half-height as a latitude angle, and how far the belt
|
||||
-- bulges past the shell so it reads as a band and not a painted stripe
|
||||
local BAND_LAT = 0.16
|
||||
local BAND_R = 1.045
|
||||
|
||||
-- lid hinge: at the BACK of the equator (-Z), opening backward. 2.0 rad is
|
||||
-- past upright -- the mouth gapes at the sky, which is the capture pose.
|
||||
local HINGE_Z = -0.86 -- as a fraction of R
|
||||
local LID_OPEN = 2.0
|
||||
|
||||
-- tessellation: enough that the silhouette is round at held-ball size,
|
||||
-- cheap enough that six of these would not show on a phone's frame budget
|
||||
local LON = 14
|
||||
local LAT = 5
|
||||
|
||||
-- pose timing
|
||||
local LID_RATE = 6.5 -- lid open/close, in lid-fractions per second
|
||||
local BURST_RATE = 14 -- the breakout pop is a violent open
|
||||
local WOBBLE_T = 0.85 -- one rock, seconds
|
||||
local WOBBLE_A = 0.38 -- how far it tips, radians
|
||||
local PULSE_T = 0.14 -- the caught click's squash pulse
|
||||
local STAR_T = 0.9 -- the caught stars' life
|
||||
local GLOW_DECAY = 2.2 -- additive glow, per second
|
||||
|
||||
-- ------- palette
|
||||
--
|
||||
-- One texel per material (columns), one row per ball tier. Alpha stays 1
|
||||
-- everywhere: the voxel shader discards below 0.5 (Voxel3D's SHADER), so a
|
||||
-- translucent texel is an invisible one.
|
||||
local SLOTS = { TOP = 1, BOTTOM = 2, BAND = 3, RING = 4, FACE = 5,
|
||||
INNER = 6, GLOW = 7, STAR = 8 }
|
||||
local SLOT_N = 8
|
||||
|
||||
local TIERS = { "POKE_BALL", "GREAT_BALL", "ULTRA_BALL", "MASTER_BALL",
|
||||
"SAFARI_BALL" }
|
||||
|
||||
local COLORS = {
|
||||
POKE_BALL = { top = { 0.86, 0.16, 0.16 }, band = { 0.12, 0.12, 0.13 },
|
||||
bottom = { 0.93, 0.93, 0.95 } },
|
||||
GREAT_BALL = { top = { 0.25, 0.45, 0.88 }, band = { 0.12, 0.12, 0.13 },
|
||||
bottom = { 0.93, 0.93, 0.95 } },
|
||||
ULTRA_BALL = { top = { 0.22, 0.22, 0.26 }, band = { 0.85, 0.70, 0.18 },
|
||||
bottom = { 0.93, 0.93, 0.95 } },
|
||||
MASTER_BALL = { top = { 0.48, 0.22, 0.66 }, band = { 0.16, 0.13, 0.19 },
|
||||
bottom = { 0.93, 0.93, 0.95 },
|
||||
glow = { 1.0, 0.72, 0.92 } },
|
||||
SAFARI_BALL = { top = { 0.47, 0.52, 0.26 }, band = { 0.36, 0.27, 0.16 },
|
||||
bottom = { 0.90, 0.88, 0.80 } },
|
||||
}
|
||||
|
||||
local SHARED = {
|
||||
ring = { 0.28, 0.28, 0.30 },
|
||||
face = { 0.96, 0.96, 0.97 },
|
||||
inner = { 0.72, 0.70, 0.68 },
|
||||
glow = { 1.00, 0.92, 0.65 },
|
||||
star = { 1.00, 0.85, 0.25 },
|
||||
}
|
||||
|
||||
local function tierRow(ball)
|
||||
for i, id in ipairs(TIERS) do
|
||||
if id == ball then return i end
|
||||
end
|
||||
return 1 -- an unknown ball is a plain POKE BALL
|
||||
end
|
||||
|
||||
-- palette texel centres
|
||||
local function uvFor(slot, row)
|
||||
return (slot - 0.5) / SLOT_N, (row - 0.5) / #TIERS
|
||||
end
|
||||
|
||||
local palette = nil
|
||||
local function paletteTexture()
|
||||
if palette ~= nil then return palette or nil end
|
||||
local ok, img = pcall(function()
|
||||
local data = love.image.newImageData(SLOT_N, #TIERS)
|
||||
for row, id in ipairs(TIERS) do
|
||||
local c = COLORS[id]
|
||||
local function put(slot, rgb)
|
||||
data:setPixel(slot - 1, row - 1, rgb[1], rgb[2], rgb[3], 1)
|
||||
end
|
||||
put(SLOTS.TOP, c.top)
|
||||
put(SLOTS.BOTTOM, c.bottom)
|
||||
put(SLOTS.BAND, c.band)
|
||||
put(SLOTS.RING, SHARED.ring)
|
||||
put(SLOTS.FACE, SHARED.face)
|
||||
put(SLOTS.INNER, SHARED.inner)
|
||||
put(SLOTS.GLOW, c.glow or SHARED.glow)
|
||||
put(SLOTS.STAR, SHARED.star)
|
||||
end
|
||||
local tex = love.graphics.newImage(data)
|
||||
tex:setFilter("nearest", "nearest")
|
||||
return tex
|
||||
end)
|
||||
palette = ok and img or false
|
||||
return palette or nil
|
||||
end
|
||||
|
||||
-- ------- shade
|
||||
--
|
||||
-- StadiumStage's fitted form of Voxel3D.FACE_SHADE: the same southeastern
|
||||
-- sun, answered for an arbitrary normal instead of one of six faces.
|
||||
local function shadeFor(nx, ny, nz)
|
||||
local s = 0.7725 + nx * 0.06 + ny * 0.225 + nz * 0.11
|
||||
return math.max(0.30, math.min(1.00, s))
|
||||
end
|
||||
|
||||
-- ------- mesh building
|
||||
--
|
||||
-- Everything below appends {x,y,z, u,v, shade} rows plus triangle indices.
|
||||
-- Quads go through the shared corner order; the discs use a degenerate
|
||||
-- fourth vertex, which the rasteriser drops as the zero-area triangle it is.
|
||||
local function quad(verts, map, a, b, c, d)
|
||||
local n = #verts
|
||||
verts[n + 1], verts[n + 2], verts[n + 3], verts[n + 4] = a, b, c, d
|
||||
Voxel3D.pushQuad(map, n / 4)
|
||||
end
|
||||
|
||||
local R = Pokeball.R
|
||||
local TAU = math.pi * 2
|
||||
|
||||
-- a latitude zone of the sphere between phi0 and phi1 (radians from the
|
||||
-- equator, north positive), at radiusK times the shell radius
|
||||
local function zone(verts, map, phi0, phi1, rows, slot, row, radiusK)
|
||||
local u, v = uvFor(slot, row)
|
||||
local r = R * (radiusK or 1)
|
||||
for i = 0, rows - 1 do
|
||||
local pa = phi0 + (phi1 - phi0) * (i / rows)
|
||||
local pb = phi0 + (phi1 - phi0) * ((i + 1) / rows)
|
||||
for j = 0, LON - 1 do
|
||||
local ta = TAU * (j / LON)
|
||||
local tb = TAU * ((j + 1) / LON)
|
||||
local function corner(phi, th)
|
||||
local nx = math.cos(phi) * math.sin(th)
|
||||
local ny = math.sin(phi)
|
||||
local nz = math.cos(phi) * math.cos(th)
|
||||
return { nx * r, ny * r, nz * r, u, v, shadeFor(nx, ny, nz) }
|
||||
end
|
||||
quad(verts, map, corner(pa, ta), corner(pa, tb),
|
||||
corner(pb, tb), corner(pb, ta))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- a disc in a y-plane, sealed with fan quads about the centre
|
||||
local function disc(verts, map, y, radius, slot, row, up)
|
||||
local u, v = uvFor(slot, row)
|
||||
local sh = shadeFor(0, up and 1 or -1, 0)
|
||||
local centre = { 0, y, 0, u, v, sh }
|
||||
for j = 0, LON - 1 do
|
||||
local ta = TAU * (j / LON)
|
||||
local tb = TAU * ((j + 1) / LON)
|
||||
local a = { radius * math.sin(ta), y, radius * math.cos(ta), u, v, sh }
|
||||
local b = { radius * math.sin(tb), y, radius * math.cos(tb), u, v, sh }
|
||||
quad(verts, map, centre, a, b, centre)
|
||||
end
|
||||
end
|
||||
|
||||
-- the button: a ring wall and its face, standing out of the shell along +Z
|
||||
local function button(verts, map, row)
|
||||
local BLON = 10
|
||||
local function ringWall(rad, z0, z1, slot)
|
||||
local u, v = uvFor(slot, row)
|
||||
for j = 0, BLON - 1 do
|
||||
local ta = TAU * (j / BLON)
|
||||
local tb = TAU * ((j + 1) / BLON)
|
||||
local function at(th, z)
|
||||
local nx, ny = math.cos(th), math.sin(th)
|
||||
return { rad * nx, rad * ny, z, u, v, shadeFor(nx, ny, 0) }
|
||||
end
|
||||
quad(verts, map, at(ta, z0), at(tb, z0), at(tb, z1), at(ta, z1))
|
||||
end
|
||||
end
|
||||
local function faceDisc(rad, z, slot)
|
||||
local u, v = uvFor(slot, row)
|
||||
local sh = shadeFor(0, 0, 1)
|
||||
local centre = { 0, 0, z, u, v, sh }
|
||||
for j = 0, BLON - 1 do
|
||||
local ta = TAU * (j / BLON)
|
||||
local tb = TAU * ((j + 1) / BLON)
|
||||
local a = { rad * math.cos(ta), rad * math.sin(ta), z, u, v, sh }
|
||||
local b = { rad * math.cos(tb), rad * math.sin(tb), z, u, v, sh }
|
||||
quad(verts, map, centre, a, b, centre)
|
||||
end
|
||||
end
|
||||
-- the wall starts inside the shell so the junction never shows a gap
|
||||
ringWall(0.75, R * 0.90, R + 0.30, SLOTS.RING)
|
||||
faceDisc(0.75, R + 0.30, SLOTS.RING)
|
||||
ringWall(0.45, R + 0.30, R + 0.42, SLOTS.RING)
|
||||
faceDisc(0.45, R + 0.42, SLOTS.FACE)
|
||||
end
|
||||
|
||||
-- one tier's meshes, memoised: { base = , lid = , spark = }
|
||||
--
|
||||
-- spark is a shared unit card (x -0.5..0.5, y 0..1, z 0) wearing one texel;
|
||||
-- the glow disc, the beam and every star are that card under a matrix.
|
||||
local meshes = {}
|
||||
local function meshesFor(ball)
|
||||
local row = tierRow(ball)
|
||||
local hit = meshes[row]
|
||||
if hit ~= nil then return hit or nil end
|
||||
|
||||
local ok, built = pcall(function()
|
||||
local bv, bm = {}, {}
|
||||
-- the base: white bowl from the south pole up to the band, its half of
|
||||
-- the band, the interior floor and the button on the front
|
||||
zone(bv, bm, -math.pi / 2, -BAND_LAT, LAT, SLOTS.BOTTOM, row)
|
||||
zone(bv, bm, -BAND_LAT, 0, 1, SLOTS.BAND, row, BAND_R)
|
||||
disc(bv, bm, -0.06, R * 0.97, SLOTS.INNER, row, true)
|
||||
button(bv, bm, row)
|
||||
|
||||
local lv, lm = {}, {}
|
||||
-- the lid: its half of the band up to the coloured dome, and its pale
|
||||
-- underside, which is what shows once the hinge tips it back
|
||||
zone(lv, lm, 0, BAND_LAT, 1, SLOTS.BAND, row, BAND_R)
|
||||
zone(lv, lm, BAND_LAT, math.pi / 2, LAT, SLOTS.TOP, row)
|
||||
disc(lv, lm, 0.06, R * 0.97, SLOTS.INNER, row, false)
|
||||
|
||||
local base = Voxel3D.newMesh(bv, bm)
|
||||
local lid = Voxel3D.newMesh(lv, lm)
|
||||
if not (base and lid) then return nil end
|
||||
|
||||
local function card(slot)
|
||||
local u, v = uvFor(slot, row)
|
||||
local cv, cm = {}, {}
|
||||
quad(cv, cm, { -0.5, 0, 0, u, v, 1 }, { 0.5, 0, 0, u, v, 1 },
|
||||
{ 0.5, 1, 0, u, v, 1 }, { -0.5, 1, 0, u, v, 1 })
|
||||
return Voxel3D.newMesh(cv, cm)
|
||||
end
|
||||
return { base = base, lid = lid,
|
||||
glow = card(SLOTS.GLOW), star = card(SLOTS.STAR) }
|
||||
end)
|
||||
meshes[row] = (ok and built) or false
|
||||
return meshes[row] or nil
|
||||
end
|
||||
|
||||
-- dropped so a lost GL context (Android resume) rebuilds everything
|
||||
function Pokeball.invalidate()
|
||||
meshes = {}
|
||||
palette = nil
|
||||
end
|
||||
|
||||
-- ------- an instance: one ball with a pose
|
||||
--
|
||||
-- Loads and runs without graphics; only draw() and cast() want a GPU.
|
||||
function Pokeball.new(ball)
|
||||
return setmetatable({
|
||||
ball = ball or "POKE_BALL",
|
||||
pos = { 0, 0, 0 }, -- world pixels, the ball's CENTRE
|
||||
yaw = 0, -- which way the button faces
|
||||
scale = 1,
|
||||
spin = 0, -- visual spin about the vertical, rad/s
|
||||
tumble = 0, -- end-over-end in flight, rad/s
|
||||
roll = 0, -- SCREEN-PLANE spin, rad/s: rotation about
|
||||
-- the axis out of the ball's face, which
|
||||
-- with the yaw at the camera reads as the
|
||||
-- ball turning clockwise/counter-clockwise
|
||||
-- to the viewer -- the curveball wind-up
|
||||
spinAngle = 0, tumbleAngle = 0, rollAngle = 0,
|
||||
lid = 0, lidTarget = 0, lidRate = LID_RATE,
|
||||
wobbleT = nil, wobbleDir = 1,
|
||||
pulse = nil, -- the caught click's squash
|
||||
glow = 0,
|
||||
stars = nil, -- caught celebration, or nil
|
||||
visible = true,
|
||||
}, Pokeball)
|
||||
end
|
||||
|
||||
-- ------- the verbs the capture flow speaks
|
||||
|
||||
function Pokeball:open()
|
||||
self.lidTarget, self.lidRate = 1, LID_RATE
|
||||
self.glow = 1
|
||||
end
|
||||
|
||||
function Pokeball:close()
|
||||
self.lidTarget, self.lidRate = 0, LID_RATE
|
||||
end
|
||||
|
||||
-- one rock on the ground; dir alternates shakes. Returns how long it takes,
|
||||
-- so the caller can sequence the pauses between shakes.
|
||||
function Pokeball:rock(dir)
|
||||
self.wobbleT = 0
|
||||
self.wobbleDir = dir or 1
|
||||
return WOBBLE_T
|
||||
end
|
||||
|
||||
-- the caught click: squash pulse, a soft flash, and the stars
|
||||
function Pokeball:catchClick()
|
||||
self.pulse = 0
|
||||
self.glow = 0.6
|
||||
local stars = {}
|
||||
for i = 1, 6 do
|
||||
stars[i] = { t = -0.04 * (i - 1), th = TAU * (i - 1) / 6 + 0.4 }
|
||||
end
|
||||
self.stars = stars
|
||||
end
|
||||
|
||||
-- the breakout: the lid blown open and a hard flash
|
||||
function Pokeball:burst()
|
||||
self.lidTarget, self.lidRate = 1, BURST_RATE
|
||||
self.glow = 1
|
||||
end
|
||||
|
||||
function Pokeball:busy()
|
||||
return self.wobbleT ~= nil or self.pulse ~= nil
|
||||
or math.abs(self.lid - self.lidTarget) > 0.02
|
||||
end
|
||||
|
||||
function Pokeball:update(dt)
|
||||
-- lid toward its target, at whatever violence was asked for
|
||||
local d = self.lidTarget - self.lid
|
||||
if d ~= 0 then
|
||||
local step = self.lidRate * dt
|
||||
if math.abs(d) <= step then
|
||||
-- arriving CLOSED from open is the shut click: the squash pulse
|
||||
if self.lid > self.lidTarget then self.pulse = self.pulse or 0 end
|
||||
self.lid = self.lidTarget
|
||||
else
|
||||
self.lid = self.lid + (d > 0 and step or -step)
|
||||
end
|
||||
end
|
||||
if self.wobbleT then
|
||||
self.wobbleT = self.wobbleT + dt
|
||||
if self.wobbleT >= WOBBLE_T then self.wobbleT = nil end
|
||||
end
|
||||
if self.pulse then
|
||||
self.pulse = self.pulse + dt
|
||||
if self.pulse >= PULSE_T then self.pulse = nil end
|
||||
end
|
||||
if self.stars then
|
||||
local live = false
|
||||
for _, s in ipairs(self.stars) do
|
||||
s.t = s.t + dt
|
||||
if s.t < STAR_T then live = true end
|
||||
end
|
||||
if not live then self.stars = nil end
|
||||
end
|
||||
self.glow = math.max(0, self.glow - GLOW_DECAY * dt)
|
||||
self.spinAngle = self.spinAngle + self.spin * dt
|
||||
self.tumbleAngle = self.tumbleAngle + self.tumble * dt
|
||||
self.rollAngle = self.rollAngle + self.roll * dt
|
||||
end
|
||||
|
||||
-- ------- pose as a matrix
|
||||
|
||||
local function smooth(t)
|
||||
if t <= 0 then return 0 end
|
||||
if t >= 1 then return 1 end
|
||||
return t * t * (3 - 2 * t)
|
||||
end
|
||||
|
||||
function Pokeball:matrix()
|
||||
local m = Mat4.mul(Mat4.translate(self.pos[1], self.pos[2], self.pos[3]),
|
||||
Mat4.rotateY(self.yaw))
|
||||
if self.wobbleT then
|
||||
-- a decaying rock about the ground contact: tip, cross through centre,
|
||||
-- tip the other way, settle
|
||||
local t = self.wobbleT / WOBBLE_T
|
||||
local a = WOBBLE_A * math.sin(TAU * t) * (1 - t) * self.wobbleDir
|
||||
m = Mat4.mul(m, Mat4.mul(Mat4.translate(0, -R * self.scale, 0),
|
||||
Mat4.mul(Mat4.rotateZ(a),
|
||||
Mat4.translate(0, R * self.scale, 0))))
|
||||
end
|
||||
if self.spinAngle ~= 0 then m = Mat4.mul(m, Mat4.rotateY(self.spinAngle)) end
|
||||
if self.tumbleAngle ~= 0 then
|
||||
m = Mat4.mul(m, Mat4.rotateX(self.tumbleAngle))
|
||||
end
|
||||
-- the roll turns about the ball's own face axis, so with the yaw aimed
|
||||
-- at the camera it reads as clockwise/counter-clockwise on screen
|
||||
if self.rollAngle ~= 0 then
|
||||
m = Mat4.mul(m, Mat4.rotateZ(self.rollAngle))
|
||||
end
|
||||
local k = self.scale
|
||||
if self.pulse then
|
||||
-- the click: a quick squash and back, more felt than seen
|
||||
local p = math.sin((self.pulse / PULSE_T) * math.pi) * 0.14
|
||||
m = Mat4.mul(m, Mat4.scale(k * (1 + p), k * (1 - p), k * (1 + p)))
|
||||
elseif k ~= 1 then
|
||||
m = Mat4.mul(m, Mat4.scale(k, k, k))
|
||||
end
|
||||
return m
|
||||
end
|
||||
|
||||
-- the hinge: the lid's own extra transform about the back of the equator
|
||||
local function lidMatrix(open)
|
||||
if open <= 0 then return nil end
|
||||
local a = -LID_OPEN * smooth(open)
|
||||
local hz = HINGE_Z * R
|
||||
return Mat4.mul(Mat4.translate(0, 0, hz),
|
||||
Mat4.mul(Mat4.rotateX(a), Mat4.translate(0, 0, -hz)))
|
||||
end
|
||||
|
||||
-- where the open mouth is, for aiming the capture beam
|
||||
function Pokeball:mouth()
|
||||
return self.pos[1], self.pos[2] + R * 0.4 * self.scale, self.pos[3]
|
||||
end
|
||||
|
||||
-- ------- drawing
|
||||
--
|
||||
-- Assumes a live Voxel3D scene (between beginScene and endScene), exactly
|
||||
-- like Stadium.draw. Seams and glass are off for the duration: the ball is
|
||||
-- not on the voxel grid and does not wear the tileset atlas.
|
||||
local function eyeYaw(x, z)
|
||||
local eye = Voxel3D.eye
|
||||
if not eye then return 0 end
|
||||
return math.atan2(eye[1] - x, eye[3] - z)
|
||||
end
|
||||
|
||||
function Pokeball:draw(pull)
|
||||
if not self.visible then return end
|
||||
local m = meshesFor(self.ball)
|
||||
local pal = paletteTexture()
|
||||
if not (m and pal) then return end
|
||||
Voxel3D.seams(false)
|
||||
Voxel3D.glass(false)
|
||||
local model = self:matrix()
|
||||
Voxel3D.draw(m.base, pal, model, pull)
|
||||
local lidM = lidMatrix(self.lid)
|
||||
Voxel3D.draw(m.lid, pal, lidM and Mat4.mul(model, lidM) or model, pull)
|
||||
|
||||
-- the additive dressing: the open-mouth glow and the caught stars.
|
||||
-- Depth writes are off under "add" (Voxel3D.blend), so these can never
|
||||
-- punch holes for later draws.
|
||||
local anythingAdd = (self.glow > 0.05 and self.lid > 0.1) or self.stars
|
||||
if anythingAdd then
|
||||
Voxel3D.blend("add")
|
||||
if self.glow > 0.05 and self.lid > 0.1 then
|
||||
-- a pulsing octahedron of light standing in the mouth: two crossed
|
||||
-- cards read from every seat in the house
|
||||
local gx, gy, gz = self:mouth()
|
||||
local s = R * (1.1 + 0.25 * self.glow) * self.scale
|
||||
for i = 0, 1 do
|
||||
local card = Mat4.mul(Mat4.translate(gx, gy, gz),
|
||||
Mat4.mul(Mat4.rotateY(eyeYaw(gx, gz) + i * math.pi / 2),
|
||||
Mat4.scale(s, s, 1)))
|
||||
Voxel3D.draw(m.glow, pal, card, pull)
|
||||
end
|
||||
end
|
||||
if self.stars then
|
||||
for _, s in ipairs(self.stars) do
|
||||
if s.t > 0 and s.t < STAR_T then
|
||||
local t = s.t / STAR_T
|
||||
local rr = (R + 4.5 * t) * self.scale
|
||||
local sx = self.pos[1] + math.sin(s.th) * rr
|
||||
local sz = self.pos[3] + math.cos(s.th) * rr
|
||||
local sy = self.pos[2] + (R + 7 * t - 5 * t * t) * self.scale
|
||||
local sc = 1.1 * (1 - t)
|
||||
local card = Mat4.mul(Mat4.translate(sx, sy, sz),
|
||||
Mat4.mul(Mat4.rotateY(eyeYaw(sx, sz)),
|
||||
Mat4.mul(Mat4.rotateZ(TAU * t * 0.5),
|
||||
Mat4.scale(sc, sc, 1))))
|
||||
Voxel3D.draw(m.star, pal, card, pull)
|
||||
end
|
||||
end
|
||||
end
|
||||
Voxel3D.blend(nil)
|
||||
end
|
||||
Voxel3D.glass(true)
|
||||
Voxel3D.seams(true)
|
||||
end
|
||||
|
||||
-- the capture beam: a crossed pair of additive cards stretched from the
|
||||
-- ball's mouth to the mon it is drinking in. Separate from draw() because
|
||||
-- the caller owns the far end and the fade.
|
||||
function Pokeball:drawBeam(tx, ty, tz, width, strength, pull)
|
||||
if not self.visible then return end
|
||||
local m = meshesFor(self.ball)
|
||||
local pal = paletteTexture()
|
||||
if not (m and pal) then return end
|
||||
local x, y, z = self:mouth()
|
||||
local dx, dy, dz = tx - x, ty - y, tz - z
|
||||
local len = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
if len < 0.5 then return end
|
||||
dx, dy, dz = dx / len, dy / len, dz / len
|
||||
-- two perpendiculars to the beam axis
|
||||
local ux, uy, uz
|
||||
if math.abs(dy) < 0.94 then
|
||||
ux, uy, uz = -dz, 0, dx -- cross(d, worldUp), unnormalised
|
||||
local l = math.sqrt(ux * ux + uz * uz)
|
||||
ux, uz = ux / l, uz / l
|
||||
else
|
||||
ux, uy, uz = 1, 0, 0
|
||||
end
|
||||
local vx = dy * uz - dz * uy
|
||||
local vy = dz * ux - dx * uz
|
||||
local vz = dx * uy - dy * ux
|
||||
local w = (width or R) * (strength or 1)
|
||||
Voxel3D.seams(false)
|
||||
Voxel3D.glass(false)
|
||||
Voxel3D.blend("add")
|
||||
-- the unit card is x -0.5..0.5, y 0..1: columns map its x to a
|
||||
-- perpendicular and its y to the full run of the axis
|
||||
local a = { ux * w, dx * len, vx, x,
|
||||
uy * w, dy * len, vy, y,
|
||||
uz * w, dz * len, vz, z,
|
||||
0, 0, 0, 1 }
|
||||
local b = { vx * w, dx * len, ux, x,
|
||||
vy * w, dy * len, uy, y,
|
||||
vz * w, dz * len, uz, z,
|
||||
0, 0, 0, 1 }
|
||||
Voxel3D.draw(m.glow, pal, a, pull)
|
||||
Voxel3D.draw(m.glow, pal, b, pull)
|
||||
Voxel3D.blend(nil)
|
||||
Voxel3D.glass(true)
|
||||
Voxel3D.seams(true)
|
||||
end
|
||||
|
||||
-- ------- the sun's view
|
||||
--
|
||||
-- The same two shells under the same matrix, so the shadow on the ground is
|
||||
-- the pose the camera sees. The caller folds a term into the shadow
|
||||
-- signature while a ball is live (the sun pass is cached -- see
|
||||
-- BattleScene.shadowSignature) or this freezes on its first frame.
|
||||
function Pokeball:cast(shadowMap)
|
||||
if not self.visible then return end
|
||||
local m = meshesFor(self.ball)
|
||||
local pal = paletteTexture()
|
||||
if not (m and pal) then return end
|
||||
local model = self:matrix()
|
||||
shadowMap.draw(m.base, pal, model)
|
||||
local lidM = lidMatrix(self.lid)
|
||||
shadowMap.draw(m.lid, pal, lidM and Mat4.mul(model, lidM) or model)
|
||||
end
|
||||
|
||||
-- a term for the arena's cached shadow signature: quantised, so the cache
|
||||
-- only re-renders when the ball has visibly moved
|
||||
function Pokeball:signature()
|
||||
if not self.visible then return "" end
|
||||
return table.concat({ math.floor(self.pos[1] * 4), math.floor(self.pos[2] * 4),
|
||||
math.floor(self.pos[3] * 4), math.floor(self.lid * 8),
|
||||
self.wobbleT and math.floor(self.wobbleT * 30) or -1 },
|
||||
",")
|
||||
end
|
||||
|
||||
return Pokeball
|
||||
Reference in New Issue
Block a user