update battle water

This commit is contained in:
DramaticShape
2026-08-01 00:28:54 -04:00
parent 22b58e27a4
commit 980383bb92
5 changed files with 133 additions and 34 deletions
+26
View File
@@ -147,6 +147,32 @@
painted. The reflection lands a hair off the sprite's own depth and exactly
on its colour, which at a lake's worth of wave is the same picture.
### Changed
- **The waves arrive in sets now, and a little slower.** Three fixed trains
are an exactly periodic field -- every forty-odd pixels of sea wore the
same crest at the same height, which reads as wallpaper the moment a lake
is bigger than the repeat. Two long-wavelength fields now ride the
dominant train, four to five carrier wavelengths apiece so neither reads
as a wave itself: a SWELL that breathes its amplitude, so a few tall
crests march through and hand over to a lull that is itself moving, and a
BEND that bows its phase, so a crest line curves across the surface
instead of ruling itself over all of it. The two lesser trains stay
plain: they are texture rather than structure, and a third modulator is
the soup the train weights exist to avoid. The step beat comes down from
15 to 12 a second -- the crests were hurrying, and a big wave is slower
than a walk cycle -- still a clean divisor of the engine's 60, and still
exactly one world pixel of dominant-crest travel per step.
- **Staged battles draw their water plain, whatever the WATER row says.**
The reflective pass is tuned for the overworld's ladder of cameras; a
battle's camera is PLACED -- low, tilted, framed like a picture -- and
under it the pass read wrong: Fresnel opened all the way up, the leaned
sky landed on bands the framing never shows, and a lake-sized arena came
out as murk wearing the tile art. The battle is a stage set, and stage
water is painted: the flat animated tiles the mode always drew, with the
mons compositing over them like everything else on the set.
### Fixed
- **On Android the water stayed flat, as if the row were off -- and once it
+12 -21
View File
@@ -435,31 +435,22 @@ function BattleScene.render(state, arena, textures, token)
Voxel3D.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end
-- and the water over it, reflecting the arena and the hour's sky (see
-- VoxelScene.drawWater -- the arena fights on the same lakes). The two
-- mons are this shot's cast: painted into the reflection copy alone, so
-- a fight staged at the water's edge has both of them in the water while
-- they still composite over it, exactly as the overworld's walkers do.
local waterDraws = {}
if water then waterDraws[#waterDraws + 1] = { water, atlasFor(host) } end
-- and the water over it -- PLAIN, always: the flat animated tiles, never
-- the reflective pass, whatever the WATER row says. The reflection is
-- tuned for the overworld's ladder of cameras; this shot's is PLACED --
-- low, tilted and framed like a picture -- and under it the pass reads
-- wrong: Fresnel opens all the way up, the leaned sky lands on bands the
-- framing never shows, and a lake-sized arena comes out as murk wearing
-- the tile art. The battle is a stage set, and stage water is painted.
-- (No mirror also means the mons need no second draw into one -- they
-- just composite over the water below, like everything else on the set.)
if water then Voxel3D.draw(water, atlasFor(host)) end
for i, nb in ipairs(neighbors) do
if nbWater and nbWater[i] then
waterDraws[#waterDraws + 1] = { nbWater[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy) }
Voxel3D.draw(nbWater[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end
end
if #waterDraws > 0 then
VoxelScene.drawWater(waterDraws, function()
Voxel3D.seams(false)
Voxel3D.glass(false)
for _, card in ipairs(monCards(arena, groundY, textures)) do
Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
BattleBillboard.PULL, ShadowMap.snug(card.model))
end
Voxel3D.glass(true)
Voxel3D.seams(true)
end)
end
-- The mons, standing on their tiles. Depth-tested like everything else,
-- so a ledge or a tree between the camera and a Pokemon really is in
-- front of it, and the alpha discard cuts the sprite's own outline out of
+3 -2
View File
@@ -580,8 +580,9 @@ end
-- with the row OFF, no depth texture to read, or a shader that would not
-- build, the same meshes go through the ordinary scene shader and come out
-- as the flat animated water this mode always drew.
-- Shared with the overworld battle, which stages its fights on the same
-- lakes and wants the same water under them.
-- The overworld's alone: the staged battle draws its water plain, always --
-- its placed camera reads this pass wrong, and a stage set wants painted
-- water anyway (see BattleScene, where the choice is argued).
function VoxelScene.drawWater(draws, cast)
local plain = true
if Water.enabled() and Voxel3D.depthReadable() then
+70 -10
View File
@@ -237,14 +237,56 @@ Water.WAVE_TRAINS = {
{ -0.041, 0.033, 0.55, 0.11 },
}
-- ------- and what keeps them from reading as one pattern
--
-- Three fixed trains are still an exactly periodic field: every forty-odd
-- pixels of sea wears the same crest at the same height, and a lake's worth
-- of that reads as wallpaper. Real swell varies two ways a sum of sines
-- cannot: waves arrive in SETS -- a few tall ones, then a lull -- and a
-- crest line curves as it runs rather than ruling itself across the whole
-- surface. Both are put back with one long-wavelength field each, riding
-- the DOMINANT train only; the two lesser trains stay plain, because they
-- are texture rather than structure and three modulators is soup again.
--
-- Both wear the trains' own shape, { fx, fz, speed, x }: a direction whose
-- length is the spatial frequency, a phase rate, and what the field does.
-- Their wavelengths sit four to five times the carrier's, far enough apart
-- that neither reads as a wave itself -- the swell as slow weather over the
-- crests, the bend as the crests' own drift.
--
-- THE SWELL scales the dominant train's amplitude; `x` is the DEPTH of the
-- deepest lull, as the fraction of the train it takes away. It runs roughly
-- along the carrier's own direction and slower than it, which is a wave
-- group's honest habit (deep-water groups travel at about half the phase
-- speed) -- so sets of crests swell up, march a while, and hand over to a
-- calm patch that is itself moving.
Water.WAVE_SWELL = { 0.0325, 0.0134, 0.55, 0.35 }
-- THE BEND adds a slow wobble to the dominant train's phase; `x` is the
-- wobble's reach in RADIANS of carrier phase. 1.1 radians against a carrier
-- of about forty pixels bows a crest some seven pixels off its line over
-- the bend's own hundred-and-seventy-five -- a visible curve, not a
-- scribble -- and it runs ACROSS the carrier, which is the direction a
-- crest line actually wanders. What it costs is exactness in waveRate's
-- derivation: the carrier's local frequency now breathes around the number
-- the rate is derived from, so the one-pixel step is the average step
-- rather than every step's. The step CLOCK is untouched; only how far a
-- bowed stretch of crest moves on one tick varies, and by under a pixel.
Water.WAVE_BEND = { -0.0138, 0.0333, 0.35, 1.10 }
-- ------- and the beat they move on
--
-- The surface does not slide, it advances in STEPS -- 15 a second, off the
-- engine's own frame counter, which is the cadence hand-drawn pixel art is
-- animated at and the reason this reads as art rather than as a simulation
-- someone forgot to stylise. A surface built out of whole pixels that crawls
-- between them smoothly gives away that the quantisation is only skin deep.
Water.WAVE_FPS = 15
-- The surface does not slide, it advances in STEPS, off the engine's own
-- frame counter -- the move that makes this read as art rather than as a
-- simulation someone forgot to stylise. A surface built out of whole pixels
-- that crawls between them smoothly gives away that the quantisation is
-- only skin deep.
--
-- 12 a second, a shade under the 15 hand-drawn pixel art is usually
-- animated at: the crests were hurrying, and a big wave is slower than a
-- sprite's walk cycle. Still a clean divisor of the engine's 60, so every
-- step spans the same whole number of frames.
Water.WAVE_FPS = 12
-- How far the dominant train advances each of those steps, in WORLD PIXELS.
-- One is the honest choice for a stepped surface: the whole field shifts by
@@ -984,12 +1026,30 @@ Water._craterSource = craterSource -- named for the suite
-- and the wave trains, pasted in for the same reason: the rate is derived
-- from this table (Water.waveRate), so the field the shader sums has to be
-- the one that table describes rather than a copy of it kept in step by hand
-- the one that table describes rather than a copy of it kept in step by hand.
--
-- The dominant train carries the swell and the bend (see WAVE_SWELL): its
-- phase wobbles by the bend field and its amplitude breathes with the swell
-- envelope, both off the same tables the constants above document. One
-- statement per train either way, which is what the suite counts.
local function trainSource()
local out = {}
for _, t in ipairs(Water.WAVE_TRAINS) do
out[#out + 1] = (" h += sin(dot(q, vec2(%.4f, %.4f)) + waveT * %.4f)"
.. " * %.4f;"):format(t[1], t[2], t[3], t[4])
for i, t in ipairs(Water.WAVE_TRAINS) do
if i == 1 then
local s = Water.WAVE_SWELL
local b = Water.WAVE_BEND
out[#out + 1] = (
" h += sin(dot(q, vec2(%.4f, %.4f)) + waveT * %.4f\n"
.. " + %.4f * sin(dot(q, vec2(%.4f, %.4f)) + waveT * %.4f))\n"
.. " * %.4f * (1.0 - %.4f * (0.5 + 0.5 *\n"
.. " sin(dot(q, vec2(%.4f, %.4f)) + waveT * %.4f)));")
:format(t[1], t[2], t[3],
b[4], b[1], b[2], b[3],
t[4], s[4], s[1], s[2], s[3])
else
out[#out + 1] = (" h += sin(dot(q, vec2(%.4f, %.4f)) + waveT * %.4f)"
.. " * %.4f;"):format(t[1], t[2], t[3], t[4])
end
end
return table.concat(out, "\n")
end
+22 -1
View File
@@ -1542,7 +1542,10 @@ local function at(f)
end
local period = 60 / Water.WAVE_FPS
T.eq(period, 4, "15 steps a second is one every four engine frames")
T.eq(period, 5, "12 steps a second is one every five engine frames")
T.eq(math.floor(period), period,
"and the beat divides the engine's 60 exactly, so every step spans the "
.. "same whole number of frames")
-- inside one step nothing moves; crossing one, it does
T.eq(at(0), at(period - 1),
"every frame inside one wave step gets the same phase -- the surface "
@@ -1575,6 +1578,24 @@ T.eq(select(2, trains:gsub("h %+= sin", "")), #Water.WAVE_TRAINS,
T.check(trains:find(("%.4f"):format(t[1]), 1, true) ~= nil,
"at the frequency the table states")
-- the variation that keeps three periodic trains from reading as wallpaper:
-- the dominant train's amplitude breathes with the swell and its crests bow
-- with the bend, both pasted from their own tables like the trains are
T.check(trains:find(("%.4f"):format(Water.WAVE_SWELL[1]), 1, true) ~= nil
and trains:find(("%.4f"):format(Water.WAVE_BEND[1]), 1, true) ~= nil,
"the swell and the bend reach the shader off the tables that document "
.. "them, not off copies kept in step by hand")
T.check(Water.WAVE_SWELL[4] > 0 and Water.WAVE_SWELL[4] < 1,
"the swell's deepest lull thins the dominant train without deleting or "
.. "inverting it -- a sea with sets in it, not a sea that turns off")
for _, mod in ipairs({ Water.WAVE_SWELL, Water.WAVE_BEND }) do
local mf = math.sqrt(mod[1] * mod[1] + mod[2] * mod[2])
T.check(mf * 3.5 < freq,
"a modulator's wavelength sits several times the carrier's, far enough "
.. "apart that it reads as weather over the waves rather than as a "
.. "fourth wave -- which would be the soup the weights exist to avoid")
end
T.check(Water.WAVE_HEIGHT > -TileShapeHeights.water,
"the crests stand taller than the recess TileShape sinks water into -- "
.. "they are RELIEF inside the quad's own footprint, so a bar that reaches "