mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:01:09 +02:00
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user