Merge pull request #139 from DramaticShape/stadium_fixes

1.7.1
This commit is contained in:
DramaticShape
2026-08-07 21:05:19 -04:00
committed by GitHub
12 changed files with 774 additions and 75 deletions
+5
View File
@@ -207,6 +207,11 @@ end
local function volume(kind, x, y, z, r, fade)
return { x = x, y = y, z = z, r = r,
-- The BOX kind is rectangular in the shader, because the flat
-- screen's box is the WINDOW's own footprint and a window is not
-- square (lib/ViewBox). A headset's model has no window to be
-- shaped like, so this one is: the same half-size twice.
rx = r, rz = r,
-- a zero band is a hard edge: half a pixel of ramp, which is
-- one pixel of antialiasing rather than a stair
invFade = 1 / math.max(fade or 0, 0.5), kind = kind }
+30 -20
View File
@@ -51,10 +51,11 @@ local floor, sqrt, min, max = math.floor, math.sqrt, math.min, math.max
local ForestAtmos = {}
-- The diorama's viewport, as both shaders here take it (see Voxel3D.cull:
-- the field is set for a headset's diorama frame and nil for every other,
-- where kind 0 means "no cut"). Read through V.require rather than held as
-- an upvalue because this file loads before Voxel3D on some paths.
-- The viewport, as both shaders here take it (see Voxel3D.cull: the field
-- is set for a headset's diorama frame and for an orbit rung's window box,
-- and nil for every other, where kind 0 means "no cut"). Read through
-- V.require rather than held as an upvalue because this file loads before
-- Voxel3D on some paths.
local function cullAt()
local c = V.require("Voxel3D").cull
return c and { c.x, c.y, c.z } or { 0, 0, 0 }
@@ -65,6 +66,11 @@ local function cullShape()
return c and { c.r, c.invFade, c.kind } or { 0, 0, 0 }
end
local function cullRect()
local c = V.require("Voxel3D").cull
return c and { c.rx or c.r, c.rz or c.r } or { 0, 0 }
end
-- FULL is the point; LOW halves the march and drops the particles, for
-- hardware that minds a per-pixel loop under 4X supersampling.
--
@@ -430,24 +436,25 @@ local RAY_SHADER = [[
uniform vec3 sunward; // unit, toward the unseen sun
uniform vec2 wind; // leaf-field drift, uv per second
uniform float time;
// the diorama's viewport, as the scene shader takes it (see Voxel3D):
// air outside the model is not air, so a sample out there contributes
// nothing and the beams end with the world they fall through
// the viewport, as the scene shader takes it (see Voxel3D): air outside
// the model is not air, so a sample out there contributes nothing and
// the beams end with the world they fall through
uniform vec3 cullAt;
uniform vec3 cullShape;
uniform vec2 cullRect; // the box's half-extents in x and z
float dioramaCull(vec3 p) {
if (cullShape.z <= 0.5) return 1.0;
vec3 cd = p - cullAt;
float d;
if (cullShape.z < 1.5) {
d = max(abs(cd.x), abs(cd.z)); // the box
float inside;
if (cullShape.z < 1.5) { // the box
inside = min(cullRect.x - abs(cd.x), cullRect.y - abs(cd.z));
} else if (cullShape.z < 2.5) {
d = length(cd); // the ball
inside = cullShape.x - length(cd); // the ball
} else {
d = length(cd.xz); // the fight's pillar
inside = cullShape.x - length(cd.xz); // the fight's pillar
}
return clamp((cullShape.x - d) * cullShape.y, 0.0, 1.0);
return clamp(inside * cullShape.y, 0.0, 1.0);
}
float sunDepth(vec2 uv) {
@@ -621,8 +628,9 @@ local PART_SHADER = [[
#ifdef VERTEX
uniform mat4 vp;
uniform vec3 curve;
uniform vec3 cullAt; // the diorama's viewport (see Voxel3D.cull):
uniform vec3 cullShape; // a mote outside the model is not in the air
uniform vec3 cullAt; // the viewport (see Voxel3D.cull): a mote
uniform vec3 cullShape; // outside the model is not in the air
uniform vec2 cullRect; // the box's half-extents in x and z
uniform vec3 axisR; // the camera's right, world space
uniform vec3 axisU; // and its up: the billboard's own frame
uniform float time;
@@ -646,15 +654,15 @@ local PART_SHADER = [[
// rather than having to cut one in half
if (cullShape.z > 0.5) {
vec3 cd = base - cullAt;
float cdd;
float inside;
if (cullShape.z < 1.5) {
cdd = max(abs(cd.x), abs(cd.z));
inside = min(cullRect.x - abs(cd.x), cullRect.y - abs(cd.z));
} else if (cullShape.z < 2.5) {
cdd = length(cd);
inside = cullShape.x - length(cd);
} else {
cdd = length(cd.xz);
inside = cullShape.x - length(cd.xz);
}
vGlow *= clamp((cullShape.x - cdd) * cullShape.y, 0.0, 1.0);
vGlow *= clamp(inside * cullShape.y, 0.0, 1.0);
}
vCorner = AtmosData.xy;
vec4 w = vec4(base + axisR * (AtmosData.x * size)
@@ -815,6 +823,7 @@ function ForestAtmos.draw(map)
Voxel3D.curveK or 0 })
pcall(sh.send, sh, "cullAt", cullAt())
pcall(sh.send, sh, "cullShape", cullShape())
pcall(sh.send, sh, "cullRect", cullRect())
pcall(sh.send, sh, "screen", { w, h })
pcall(sh.send, sh, "fogW",
{ f.fog.density, f.fog.heightK,
@@ -847,6 +856,7 @@ function ForestAtmos.draw(map)
Voxel3D.curveK or 0 })
pcall(psh.send, psh, "cullAt", cullAt())
pcall(psh.send, psh, "cullShape", cullShape())
pcall(psh.send, psh, "cullRect", cullRect())
pcall(psh.send, psh, "axisR", axisR)
pcall(psh.send, psh, "axisU", axisU)
pcall(psh.send, psh, "time", ForestAtmos.time)
+321
View File
@@ -0,0 +1,321 @@
-- RENDER DIST: how much of the map the orbit rungs bother to draw.
--
-- lib/Diorama cuts a headset's model out of the world with a box the
-- player opens and closes with their hands. This is the same cut on the
-- flat screen, asked the other way round: not "how much world do I want to
-- be holding" but "how much world can this camera actually SEE" -- so that
-- nothing off screen is drawn, and nothing on screen is missing.
--
-- THE FOOTPRINT IS NOT THE WINDOW. That is the whole difficulty, and the
-- first cut of this file got it wrong: it took the flat game's own vw-by-vh
-- rectangle about the view centre, which is exactly right at 0 degrees and
-- wrong at every rung the mode actually has. Tilt the camera and the ground
-- it frames stops being that rectangle and becomes a TRAPEZOID -- reaching
-- much further north (the far edge of the frame is further away, so it
-- covers more ground per pixel), flaring much wider there for the same
-- reason, and pulling IN at the south edge, which is nearer the eye than
-- the focus is. A window-sized box cuts the north field and both far
-- corners off a world that is plainly on screen: gaps at the top and down
-- the sides, with sky showing through them.
--
-- So the footprint is derived from the camera rather than guessed. The
-- orbit is one number (see Voxel3D.viewProjection): eye at distance
-- FOCAL*vh, pitched `a` off straight down, looking at the view centre,
-- with a fov chosen so a straight-down camera frames exactly vh. Cast the
-- frame's own corner rays at the ground plane and the trapezoid falls out
-- in closed form -- see footprint() for the derivation, which is three
-- lines of algebra and no tuning at all.
--
-- The CUT is still a rectangle (the shader's box kind), so what is stored
-- is the trapezoid's bounding rect: never narrower than the picture, so it
-- can never take a bite out of it. It is off-centre in z, because the
-- trapezoid is -- the box sits north of the view centre at every rung but
-- the top one.
--
-- THE HORIZON IS WHY THERE IS A ROW AT ALL. Past about 63 degrees (exactly
-- atan(2*FOCAL), where the top of the frame lifts off the ground plane) the
-- trapezoid stops being finite: the camera can see to the horizon, and "all
-- the ground on screen" is an infinite answer. Something has to name a
-- distance, and that is what RENDER DIST names -- MAX_REACH view heights,
-- times the row's own multiplier. Below that pitch the row does nothing to
-- the picture at all, because the honest footprint is already smaller than
-- the reach; at 75 it is what decides where the world ends.
--
-- AND IT PAYS FOR ITSELF. A cut this file can describe in world pixels is
-- one VoxelScene can test a whole neighbour map against BEFORE drawing it
-- -- see shows() -- so a connected map that lands entirely outside the box
-- costs no terrain mesh, no water, no grass, no flowers and no shadow
-- pass. Most of the win is at the HIGH rungs, where the camera is nearly
-- overhead and the footprint is barely bigger than the window; at 75 it
-- sees half the region and skips almost nothing, which is the truth about
-- that rung rather than a shortcoming of the test.
--
-- WHAT IT DOES NOT TOUCH. The free-roam rungs (1ST and 3RD): the player is
-- standing IN the world there, and a box around a walking eye is a
-- fog-of-war circle rather than a model on a table. The rung tween into
-- them opens the box out with the blend rather than dropping it on a
-- frame, so diving into a head does not pop the sides away. A headset's
-- frame is not touched either -- lib/Diorama owns the cut there, and VR's
-- STANDARD rungs are a tabletop already.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local ModSetting = V.require("ModSetting")
local ViewBox = {}
ViewBox.KEY = "viewbox"
-- RENDER DIST rather than a V- name like the rows either side of it: what
-- the player is choosing is HOW MUCH WORLD gets drawn, which is the thing
-- every game with this row calls a render distance. The mode read -- the
-- slab with sides -- is what that buys, not what the row is asking.
ViewBox.LABEL = "RENDER DIST"
-- The ladder, as a multiplier on the footprint the camera actually frames.
-- Rung 0 is FIT and it is the default, and FIT means exactly that: the
-- ground on screen and no more. It cannot open a gap -- 1.0 times the
-- honest answer is the honest answer -- so the only thing the wider rungs
-- buy below the horizon pitch is margin around a cut nobody can see.
--
-- Where they DO decide the picture is at 75, and at the tween rungs either
-- side of it, where the footprint is infinite and MAX_REACH below stands in
-- for it: there the ladder is a real render distance and FIT is the closest
-- horizon of the four.
--
-- Geometric rather than even, for the reason WorldCurve's ladder is: what
-- the player sees change between two rungs is the AREA inside the box, and
-- that goes as the square -- even steps bunch the whole ladder at the
-- near end.
--
-- The last rung is 0, which is no cut at all. It sits at the TOP because
-- "everything" is where the ladder is going: FIT, wider, wider, wider,
-- all of it.
ViewBox.FRACS = { 1.0, 1.5, 2.25, 3.5, 0 }
ViewBox.setting = ModSetting.new(ViewBox.KEY, ViewBox.LABEL,
{ 0, 1, 2, 3, 4 },
{ "FIT", "WIDE", "WIDER", "WIDEST", "OFF" })
-- How far the world may reach when the camera can see the HORIZON and the
-- honest footprint is infinite, in view heights. Generous on purpose: this
-- is a backstop for an unbounded answer, not a curtain to draw across the
-- middle distance, and it wants to land well past the edge of the loaded
-- neighbourhood so the world runs out before the cut does. Multiplied by
-- the row, so a player who can see the seam can push it away.
ViewBox.MAX_REACH = 6
-- The rim under V-CURVE, as a fraction of the shorter half-extent, and for
-- the reason Diorama.FADE_FRAC exists: a bent world has no straight sides,
-- so a hard edge across one is a lie about what is being looked at. Flat,
-- the box keeps its hard edge -- that IS the sides.
ViewBox.FADE_FRAC = 0.16
-- How far outside the box a map may still have geometry inside it: the
-- border ring ChunkMesher meshes around a body (RING = 3 blocks of 32
-- world pixels), which is the one thing a map draws beyond its own
-- rectangle. A neighbour kept by this margin that turns out to be entirely
-- outside is drawn and then cut per fragment, which is what would have
-- happened without the test -- the margin can only cost a draw, never a
-- hole.
ViewBox.PAD = 96
function ViewBox.level()
return ViewBox.setting:get() or 0
end
-- The multiplier in force, or nil for OFF -- which is also every caller's
-- "there is no cut this frame" answer.
function ViewBox.frac()
local f = ViewBox.FRACS[ViewBox.level() + 1]
if not f or f <= 0 then return nil end
return f
end
-- Whether this rung is one the box is about: an ORBIT rung, which is every
-- level the mode has except OFF (level 0, where there is no 3D pass to cut)
-- and the two free-roam rungs (see the header).
function ViewBox.appliesTo(level)
local ok, applies = pcall(function()
local Voxel = V.require("VoxelState")
local l = level or Voxel.level or 0
return l > 0 and not Voxel.isFreeCam(l)
end)
return ok and applies or false
end
-- ------- what the live frame is
--
-- Set by VoxelScene for the length of one flat frame and cleared with it,
-- exactly as Diorama's is for a headset's. Nothing else writes it, and
-- every reader -- the shader uniforms, the neighbour skip -- hangs off this
-- one field being nil or not.
ViewBox.cull = nil -- { x, y, z, r, rx, rz, invFade, kind }
local function curved()
local ok, on = pcall(function()
return V.require("WorldCurve").active()
end)
return ok and on or false
end
-- How far out of the orbit and into a walking head the rung tween has got,
-- 0..1. The box opens out by one over what is LEFT of the orbit, so it has
-- grown past every edge of the frame by the time the eye arrives in the
-- head and the cut is dropped -- rather than the sides vanishing on the
-- frame the rung number changed, which is a pop in the middle of a move.
local function orbitLeft()
local ok, blend = pcall(function()
return V.require("FirstPerson").blendEased()
end)
if not ok or type(blend) ~= "number" then return 1 end
return 1 - math.max(0, math.min(1, blend))
end
-- ------- the ground this camera frames
--
-- The orbit (Voxel3D.viewProjection's else branch) is: eye at distance
-- k = FOCAL*vh, pitched `a` off straight down and due south of the focus;
-- focus on the ground at the view centre; a symmetric frustum whose
-- half-tangents are tanY = 1/(2*FOCAL) vertically and tanY*(vw/vh)
-- horizontally. Screen coordinates run sx, sy in [-1, 1] with sy = +1 the
-- TOP of the frame, which is north.
--
-- The ray through a screen point is forward + right*sx*tanX + up*sy*tanY,
-- and with the orbit's basis (right = +x, forward = (0, -cos a, -sin a),
-- up = (0, sin a, -cos a)) that comes out as
--
-- d = ( sx*tanX, -cos a + sy*tanY*sin a, -sin a - sy*tanY*cos a )
--
-- Drop it to the ground plane from an eye at height k*cos a and the whole
-- trapezoid collapses to ONE denominator,
--
-- D(sy) = cos a - sy*tanY*sin a
--
-- with (the sin^2 + cos^2 cancels most of the algebra away):
--
-- north of centre : (vh/2) * sy / D(sy)
-- half-width : (vw/2) * cos a / D(sy)
--
-- because k*tanY is exactly vh/2 and tanX*k is exactly vw/2, whatever FOCAL
-- is. At a = 0 both reduce to vh/2 and vw/2 -- the flat window, which is
-- the case the first cut of this file mistook for all of them.
--
-- D shrinks as sy climbs, so BOTH grow toward the top of the frame, and
-- both blow up where D reaches zero: sy* = cot(a)/tanY, the row the horizon
-- sits on. Past 63 degrees that row is inside the frame and the answer is
-- infinite -- which is what `reach` is for.
--
-- Returns three DISTANCES from the view centre, all positive: how far the
-- picture runs north, how far south, and how far to each side.
function ViewBox.footprint(a, vw, vh, reach)
local Voxel = V.require("VoxelState")
local halfW, halfH = (vw or 320) * 0.5, (vh or 288) * 0.5
local tanY = 1 / (2 * (Voxel.FOCAL or 1))
-- the orbit never reaches level (75 degrees is the last rung) but a tween
-- reads a live angle, and a cos of zero is a horizon through the middle
-- of the frame rather than a number
local ca = math.max(math.cos(a or 0), 1e-3)
local sa = math.max(math.sin(a or 0), 0)
-- The screen row the far edge is taken at: the TOP of the frame, or the
-- row whose ray lands `reach` out, whichever comes first. Inverting the
-- north formula for sy gives
--
-- sy = reach*cos a / (vh/2 + reach*tanY*sin a)
--
-- which is always strictly below the horizon row (it approaches it from
-- underneath as reach grows), so D below is always positive -- with the
-- horizon in frame this never even reaches 1 and the clamp is inert.
local sy = reach * ca / (halfH + reach * tanY * sa)
if sy > 1 then sy = 1 end
local D = ca - sy * tanY * sa
if D < 1e-3 then D = 1e-3 end
return halfH * sy / D, -- north
halfH / (ca + tanY * sa), -- south: the sy = -1 row
halfW * ca / D -- and the widest row is the far one
end
-- Open the frame's cut: the bounding rectangle of the ground this camera
-- frames, times the row's multiplier. Returns the cut, or nil when this
-- frame has none -- which is the row at OFF, a rung the box is not about,
-- and a camera that has finished its dive into a head.
function ViewBox.frame(cx, cy, vw, vh, level)
ViewBox.cull = nil
local frac = ViewBox.frac()
if not (frac and ViewBox.appliesTo(level)) then return nil end
local left = orbitLeft()
if left <= 0.001 then return nil end
frac = frac / left
local Voxel = V.require("VoxelState")
local north, south, side = ViewBox.footprint(
Voxel.angle or 0, vw, vh, ViewBox.MAX_REACH * (vh or 288))
north, south, side = north * frac, south * frac, side * frac
-- The rectangle around it. Off-centre in z, because the trapezoid is:
-- the camera looks NORTH from south of its focus, so there is far more
-- picture ahead of the view centre than behind it -- at 35 degrees
-- roughly twice as much, at 75 the whole frame.
--
-- The same floor Diorama.radius keeps: a box smaller than a couple of
-- tiles is not a viewport, it is a hole the player is standing in.
local rx = math.max(24, side)
local rz = math.max(24, (north + south) * 0.5)
local bent = curved()
local fade = bent and math.max(1, math.min(rx, rz) * ViewBox.FADE_FRAC) or 0
ViewBox.cull = {
x = cx, y = 0, z = cy - (north - south) * 0.5,
-- `r` is what the ball and the pillar kinds are sized by and the box
-- is not; carried so the cut table has one shape whoever made it
r = math.max(rx, rz), rx = rx, rz = rz,
-- a zero band is a hard edge: half a pixel of ramp, which is one pixel
-- of antialiasing rather than a stair (Diorama says the same)
invFade = 1 / math.max(fade, 0.5),
kind = V.require("Diorama").BOX,
}
return ViewBox.cull
end
function ViewBox.stop()
ViewBox.cull = nil
end
-- ------- the coarse half of the cut
--
-- Whether anything inside the world-pixel rectangle (x0, z0)-(x1, z1) can
-- be inside this frame's box. True whenever there is no box, so a caller
-- may guard every draw with it unconditionally.
function ViewBox.shows(x0, z0, x1, z1)
local c = ViewBox.cull
if not c then return true end
local pad = ViewBox.PAD
return x0 - pad <= c.x + c.rx and x1 + pad >= c.x - c.rx
and z0 - pad <= c.z + c.rz and z1 + pad >= c.z - c.rz
end
-- The same question about a connected neighbour, in the shape VoxelScene
-- keeps them: { map, ox, oy } with the offset in world pixels and the map's
-- own size in blocks of 32 (the shape prefetch's masks are built from).
function ViewBox.showsMap(nb)
if not (nb and nb.map and nb.map.def) then return true end
return ViewBox.shows(nb.ox or 0, nb.oy or 0,
(nb.ox or 0) + (nb.map.def.width or 0) * 32,
(nb.oy or 0) + (nb.map.def.height or 0) * 32)
end
-- What the shadow pass has to notice: WHICH neighbours it drew is now a
-- function of the row, and the row is the one input to that the sun's own
-- signature does not already carry (the centre, the view size and the
-- rung are all in it). Widening the box brings a neighbour back into the
-- light's frustum, and a map recorded without it must be redrawn.
function ViewBox.signature()
return ViewBox.frac() or 0
end
function ViewBox.row()
return ViewBox.setting:row()
end
function ViewBox.sync(value)
ViewBox.setting:sync(value)
end
return ViewBox
+22 -10
View File
@@ -176,26 +176,35 @@ local SHADER = [[
uniform vec3 cullAt; // the viewport's centre, in world pixels
uniform vec3 cullShape; // half-size, 1/fade, kind: 1 box, 2 ball,
// 3 the staged fight's pillar
uniform vec2 cullRect; // the BOX's half-extents in x and z, which
// the round kinds have no use for. Two
// numbers because the flat screen's box is
// the WINDOW's own footprint and a window is
// not square (lib/ViewBox); a headset's is,
// and lib/Diorama sends the same half-size
// twice.
// 1 well inside the viewport, 0 outside it, and the rim in between --
// which is a HARD edge for the box (its band is half a pixel wide, so
// the ramp is just the antialiasing) and a dissolve for the other two.
//
// The box and the pillar are unbounded upward and downward on purpose:
// what is wanted is a square (or round) piece cut OUT OF THE MAP, and a
// cut with a lid would take the tops off the trees standing in it.
// Every kind is unbounded upward and downward on purpose: what is wanted
// is a rectangular (or round) piece cut OUT OF THE MAP, and a cut with a
// lid would take the tops off the trees standing in it.
float dioramaCull(vec3 p) {
if (cullShape.z <= 0.5) return 1.0;
vec3 cd = p - cullAt;
float d;
// how far INSIDE the cut this point is, in world pixels: the nearest
// side for the rectangle, the rim for the two round kinds
float inside;
if (cullShape.z < 1.5) {
d = max(abs(cd.x), abs(cd.z)); // the box: a square of map
inside = min(cullRect.x - abs(cd.x), cullRect.y - abs(cd.z));
} else if (cullShape.z < 2.5) {
d = length(cd); // the ball, under V-CURVE
inside = cullShape.x - length(cd); // the ball, under V-CURVE
} else {
d = length(cd.xz); // the fight's pillar
inside = cullShape.x - length(cd.xz); // the fight's pillar
}
return clamp((cullShape.x - d) * cullShape.y, 0.0, 1.0);
return clamp(inside * cullShape.y, 0.0, 1.0);
}
#endif
uniform Image sunMap;
@@ -1068,13 +1077,16 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
pcall(sh.send, sh, "fogInfo", fog and
{ fog.density or 0, fog.start or 0, fog.heightK or 0, 0 }
or { 0, 0, 0, 0 })
-- and the diorama's viewport (see Voxel3D.cull), kind 0 when there is
-- none -- which is every frame that is not a headset's diorama
-- and the viewport (see Voxel3D.cull), kind 0 when there is none --
-- which is every frame that neither a headset's diorama (lib/Diorama)
-- nor an orbit rung's window box (lib/ViewBox) has cut
local cull = Voxel3D.cull
pcall(sh.send, sh, "cullAt",
cull and { cull.x, cull.y, cull.z } or { 0, 0, 0 })
pcall(sh.send, sh, "cullShape",
cull and { cull.r, cull.invFade, cull.kind } or { 0, 0, 0 })
pcall(sh.send, sh, "cullRect",
cull and { cull.rx or cull.r, cull.rz or cull.r } or { 0, 0 })
-- the window glass: the tileset's mask (or the blank -- the sampler is
-- declared either way, and unbound is a driver-dependent crash), how lit
-- the panes are, and the movement-fed glint as the caller last set it
+72 -22
View File
@@ -28,6 +28,7 @@ local FirstPerson = V.require("FirstPerson")
local BattleBillboard = V.require("BattleBillboard")
local Pokedex = V.require("Pokedex")
local Diorama = V.require("Diorama")
local ViewBox = V.require("ViewBox")
local PaletteFX = require("src.render.PaletteFX")
local Map = require("src.world.Map")
@@ -623,10 +624,12 @@ local function drawCast(state, posed, atlasFor)
ShadowMap.snug(caster))
end)
for _, nb in ipairs(state.neighbors or {}) do
eachFigure(nb.map, nb.ox, nb.oy, function(mesh, model, caster)
Voxel3D.draw(mesh, atlasFor(nb.map), model, figPull,
ShadowMap.snug(caster))
end)
if ViewBox.showsMap(nb) then
eachFigure(nb.map, nb.ox, nb.oy, function(mesh, model, caster)
Voxel3D.draw(mesh, atlasFor(nb.map), model, figPull,
ShadowMap.snug(caster))
end)
end
end
-- and the seams are back on for the terrain art that follows: grass and
-- flowers are the world's own drawing, not people
@@ -770,6 +773,11 @@ local function shadowSignature(terrain, nbMesh, posed, cx, cy, vw, vh)
-- and the sprite cards swap frames as it circles them, so a turn on the
-- spot re-fits and redraws exactly like a camera move ("" outside 1ST)
put(FirstPerson.signature())
-- and the window box, because WHICH neighbours went into the light is a
-- function of it (see ViewBox.signature): opening the row out brings a
-- map back inside the cut, and a sun map recorded without it would leave
-- that map standing in its own unlit shadow
put(ViewBox.signature())
put(tostring(terrain))
for i = 1, #nbMesh do put(tostring(nbMesh[i])) end
for _, p in ipairs(posed) do
@@ -803,9 +811,16 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
if not ShadowMap.begin(cx, cy, vw, vh) then return end
ShadowMap.draw(terrain, atlasFor(state.map), nil)
-- The window box's coarse cut, here and at every neighbour loop below
-- (lib/ViewBox): a connected map lying entirely outside this frame's
-- viewport has nothing inside it that could reach the picture, so it is
-- not submitted at all. True for every map whenever there is no box,
-- which is every frame the row is OFF and every headset frame.
for i, nb in ipairs(state.neighbors or {}) do
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
if ViewBox.showsMap(nb) then
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end
end
-- The water surface, which the terrain mesh no longer carries (it is its
-- own reflective pass now -- see Water). The sun still has to see it, or
@@ -813,8 +828,10 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
-- far plane answers for the surface a shoreline tree's shadow falls on.
ShadowMap.draw(water, atlasFor(state.map), nil)
for i, nb in ipairs(state.neighbors or {}) do
ShadowMap.draw(nbWater and nbWater[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
if ViewBox.showsMap(nb) then
ShadowMap.draw(nbWater and nbWater[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end
end
-- flower billboards live outside the terrain mesh (they draw after the
-- characters, pulled -- see render), but the sun still sees them: a
@@ -825,8 +842,10 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
ShadowMap.draw(ChunkMesher.flowers(state.map), atlasFor(state.map),
ShadowMap.snug(nil))
for _, nb in ipairs(state.neighbors or {}) do
ShadowMap.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
if ViewBox.showsMap(nb) then
ShadowMap.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
end
end
-- From here down it is the CAST, marked as such in the map (see
-- ShadowMap.sprites) so water can decline them: everything the world casts
@@ -839,9 +858,11 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
ShadowMap.draw(mesh, atlasFor(state.map), ShadowMap.snug(caster))
end)
for _, nb in ipairs(state.neighbors or {}) do
eachFigure(nb.map, nb.ox, nb.oy, function(mesh, _, caster)
ShadowMap.draw(mesh, atlasFor(nb.map), ShadowMap.snug(caster))
end)
if ViewBox.showsMap(nb) then
eachFigure(nb.map, nb.ox, nb.oy, function(mesh, _, caster)
ShadowMap.draw(mesh, atlasFor(nb.map), ShadowMap.snug(caster))
end)
end
end
for _, p in ipairs(posed) do
local def = p.sprite.def
@@ -960,6 +981,25 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
cx, cy = eyes.cx, eyes.cy
end
-- and the ORBIT RUNGS' own viewport (lib/ViewBox): the flat screen's
-- answer to the same question the diorama's box asks -- the map cut to
-- the window that frames it, so a tilted world reads as a model with
-- sides rather than a map running off every edge. Flat frames only: a
-- headset's cut is Diorama's above, and the two must never both be live.
--
-- After the first-person block, so the box is centred on the camera
-- actually in charge and opens out with a dive into a head rather than
-- vanishing on the frame the rung changed.
--
-- Ahead of castShadows, deliberately: the sun draws the same neighbours
-- the eye does (both ask ViewBox.showsMap), so a map skipped out here is
-- skipped out there and nothing is left casting a shadow it cannot own.
if not eyes then
Voxel3D.cull = ViewBox.frame(cx, cy, vw, vh)
else
ViewBox.stop()
end
-- A staged fight, seen by the VR eyes: the flat screen draws the battle
-- SCREEN while one is up (this pass never runs), but the headset keeps
-- looking at the world, so the world had better have the fight on it.
@@ -990,9 +1030,14 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
local function drawScene()
Voxel3D.draw(terrain, atlasFor(state.map), nil)
-- the window box's coarse cut, exactly as the sun pass took it: the same
-- test on the same maps, so the light and the eye can never disagree
-- about which neighbours are in this frame (see ViewBox.showsMap)
for i, nb in ipairs(state.neighbors or {}) do
Voxel3D.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
if ViewBox.showsMap(nb) then
Voxel3D.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end
end
-- Without a shadow map (headless, or a driver that could not make the
@@ -1024,7 +1069,7 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
waterDraws[#waterDraws + 1] = { water, atlasFor(state.map), nil }
end
for i, nb in ipairs(state.neighbors or {}) do
if nbWater and nbWater[i] then
if nbWater and nbWater[i] and ViewBox.showsMap(nb) then
waterDraws[#waterDraws + 1] = { nbWater[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy) }
end
@@ -1143,8 +1188,10 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
local pull = VoxelScene.pull(lean)
Voxel3D.draw(ChunkMesher.grass(state.map), atlasFor(state.map), nil, pull)
for _, nb in ipairs(state.neighbors or {}) do
Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy), pull)
if ViewBox.showsMap(nb) then
Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy), pull)
end
end
-- flower billboards: pulled like the characters and the grass, MINUS
-- the depth of 8 world pixels along the view (8 sin a -- the camera
@@ -1162,9 +1209,11 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil,
fpull, ShadowMap.snug(nil))
for _, nb in ipairs(state.neighbors or {}) do
Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy), fpull,
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
if ViewBox.showsMap(nb) then
Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy), fpull,
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
end
end
-- The map's atmosphere -- god rays down from the invisible canopy, and
@@ -1208,10 +1257,11 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
end -- drawScene
-- the two diorama fields are this function's for the length of this
-- the viewport fields are this function's for the length of this
-- function, whichever way it leaves (see where they are set)
local function done(result)
Voxel3D.cull, Voxel3D.keyColor = nil, nil
ViewBox.stop()
return result
end
+16 -12
View File
@@ -463,26 +463,27 @@ uniform float pxAngle; // radians of view one screen pixel subtends
// sample to get back to the screen. Declared in both stages, like `vp`, and
// both are highp here.
uniform vec3 curve; // xy = the focus in world XZ, z = k; 0 = off
// The diorama's viewport, exactly as the scene shader takes it: centre in
// world pixels, then half-size / one-over-fade / kind (0 off, 1 box, 2
// ball, 3 the staged fight's pillar). Water is world like anything else,
// and a lake left lying outside the model would be the one thing floating
// in the sky.
// The viewport, exactly as the scene shader takes it: centre in world
// pixels, then half-size / one-over-fade / kind (0 off, 1 box, 2 ball, 3
// the staged fight's pillar), plus the box's two half-extents. Water is
// world like anything else, and a lake left lying outside the model would
// be the one thing floating in the sky.
uniform vec3 cullAt;
uniform vec3 cullShape;
uniform vec2 cullRect;
float dioramaCull(vec3 p) {
if (cullShape.z <= 0.5) return 1.0;
vec3 cd = p - cullAt;
float d;
float inside;
if (cullShape.z < 1.5) {
d = max(abs(cd.x), abs(cd.z));
inside = min(cullRect.x - abs(cd.x), cullRect.y - abs(cd.z));
} else if (cullShape.z < 2.5) {
d = length(cd);
inside = cullShape.x - length(cd);
} else {
d = length(cd.xz);
inside = cullShape.x - length(cd.xz);
}
return clamp((cullShape.x - d) * cullShape.y, 0.0, 1.0);
return clamp(inside * cullShape.y, 0.0, 1.0);
}
// How far the bend has pushed the world down at world XZ `q` -- the vertex
@@ -1274,12 +1275,15 @@ function Water.begin(ctx)
send("vp", "row", ctx.vp)
send("eye", ctx.eye)
send("curve", ctx.curve)
-- the diorama's viewport, as beginScene sent it to the scene shader;
-- kind 0 -- every frame that is not a headset's diorama -- is "no cut"
-- the viewport, as beginScene sent it to the scene shader; kind 0 --
-- every frame neither the diorama nor the orbit's box has cut -- is
-- "no cut"
local cull = V.require("Voxel3D").cull
send("cullAt", cull and { cull.x, cull.y, cull.z } or { 0, 0, 0 })
send("cullShape", cull and { cull.r, cull.invFade, cull.kind }
or { 0, 0, 0 })
send("cullRect", cull and { cull.rx or cull.r, cull.rz or cull.r }
or { 0, 0 })
send("screen", { ctx.screen[1], ctx.screen[2] })
send("cell", math.max(1, ctx.cell or 1))
-- how much of the view one screen pixel is worth: what sets the relief