DramaticShapeVoxelMod archival mirror · read-only
6d27d914110bc7397433b31775cd6a9a977066da

fix glinting on windows and striping

DramaticShape · 2026-07-29 · parent 5ff56eab46

fix glinting on windows and striping

Changed files

CHANGELOG.md        | 31 +++++++++++++++++++------------
 lib/BattleScene.lua | 10 +++++++---
 lib/ShadowMap.lua   | 12 ++++++++++--
 lib/Voxel3D.lua     | 15 +++++++++++++--
 lib/VoxelScene.lua  | 28 +++++++++++++++++++---------
 5 files changed, 68 insertions(+), 28 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8056582..74eda1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -130,14 +130,18 @@
   at any angle, in free-roam and staged battles alike, with no geometry
   work.
 
-  By day a thin diagonal glint crosses the panes WHILE THE VIEW MOVES: the
-  sweep's phase is fed by the camera's own travel and its strength fades out
-  within a beat of standing still -- a reflection is something the viewpoint
-  does, so still camera means still glass. It lifts the texels toward
-  sky-white and leaves the diagonal shine art visible through it. The mask
-  is consulted only by meshes textured from the tileset atlas
-  (Voxel3D.glass), never by sprite sheets, whose coordinates would land on
-  the panes' atlas positions by accident.
+  By day a thin glint crosses the panes WHILE THE VIEW MOVES: the sweep's
+  phase is fed by the camera's own travel and its strength fades out within
+  a beat of standing still -- a reflection is something the viewpoint does,
+  so still camera means still glass. The sweep pattern lives in the pane's
+  OWN texels, not the screen's: a screen-anchored pattern has the world
+  sliding through it at zoom speed whenever the camera pans, which strobed
+  (worst walking against the sweep); anchored to the glass, panning moves
+  nothing and a step advances the glint a fraction of a texel, the same in
+  every direction. It lifts the texels toward sky-white and leaves the
+  shine art visible through it. The mask is consulted only by meshes
+  textured from the tileset atlas (Voxel3D.glass), never by sprite sheets,
+  whose coordinates would land on the panes' atlas positions by accident.
   After dark the panes are LIT: the texel's own pattern carried into a warm
   lamp colour, replacing the shaded answer entirely -- a lit window ignores
   the sun, every shadow and the hour's tint, exactly as a window with a lamp
@@ -202,10 +206,13 @@
   own ray (`ShadowMap.snug`): moving along the ray changes nothing about
   where a shadow falls, but storing the card shallower takes three quarters
   of the forgiveness back for the shadow it throws -- and for nothing else:
-  no terrain moved, so the acne margin is untouched where it matters, and
-  the quarter left keeps the card off the float-equality knife edge against
-  its own stored depth. The shadow root lands back under the feet at every
-  hour.
+  no terrain moved, so the acne margin is untouched where it matters. The
+  obligation that comes with it: every snugged caster's LIT draw hands the
+  same snugged transform to its own shadow lookup (Voxel3D.draw's
+  `sunModel`), so stored and lookup agree exactly and the compare keeps its
+  full margin -- read un-snugged, the missing nine tenths showed up as
+  diagonal moire bands crawling across every sprite. The shadow root lands
+  back under the feet at every hour.
 
 ### Changed
 
diff --git a/lib/BattleScene.lua b/lib/BattleScene.lua
index 6a64bf9..edb5f67 100644
--- a/lib/BattleScene.lua
+++ b/lib/BattleScene.lua
@@ -416,8 +416,10 @@ function BattleScene.render(state, arena, textures, token)
     -- tileset atlas, so the mask's coordinates mean nothing on them
     Voxel3D.glass(false)
     for _, card in ipairs(monCards(arena, groundY, textures)) do
+      -- the sun stored this card snugged (castShadows), so its own shadow
+      -- lookup must read the same snugged transform -- see ShadowMap.snug
       Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
-                   BattleBillboard.PULL)
+                   BattleBillboard.PULL, ShadowMap.snug(card.model))
     end
     Voxel3D.glass(true)
     Voxel3D.seams(true)
@@ -433,10 +435,12 @@ function BattleScene.render(state, arena, textures, token)
                    Mat4.translate(nb.ox, 0, nb.oy), pull)
     end
     local fpull = math.max(0, pull - 8 * math.sin(math.max(pitch, 0.05)))
-    Voxel3D.draw(ChunkMesher.flowers(host), atlasFor(host), nil, fpull)
+    Voxel3D.draw(ChunkMesher.flowers(host), atlasFor(host), nil, fpull,
+                 ShadowMap.snug(nil))
     for _, nb in ipairs(neighbors) do
       Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
-                   Mat4.translate(nb.ox, 0, nb.oy), fpull)
+                   Mat4.translate(nb.ox, 0, nb.oy), fpull,
+                   ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
     end
     local canvas = Voxel3D.endScene()
     if not canvas then return end
diff --git a/lib/ShadowMap.lua b/lib/ShadowMap.lua
index 4606d5a..9798462 100644
--- a/lib/ShadowMap.lua
+++ b/lib/ShadowMap.lua
@@ -390,8 +390,16 @@ ShadowMap.SNUG = 0.9
 -- terrain moved, so the acne margin the slack exists for is intact where
 -- it matters. For sprite cards and other thin stand-ins only.
 --
--- Valid between begin() and finish(), which is when `slack` matches the
--- frustum the map is being drawn with.
+-- ONE OBLIGATION comes with it: the caster's LIT draw must hand this same
+-- snugged transform to its shadow lookup (Voxel3D.draw's `sunModel`).
+-- Stored and lookup then agree exactly, as they did before snugging, and
+-- the compare keeps its full acne margin. A caster stored snugged but read
+-- un-snugged is 0.9 of the margin short, and the loss shows up as diagonal
+-- moire bands crawling across the card.
+--
+-- Valid between begin() and the next begin(): `slack` and the sun hold
+-- still between redraws of the map, so a lit frame that reuses last
+-- frame's map computes the same displacement it was stored with.
 function ShadowMap.snug(model)
   local f = sunDir()
   local s = -ShadowMap.slack * ShadowMap.SNUG
diff --git a/lib/Voxel3D.lua b/lib/Voxel3D.lua
index ef7889b..62e98be 100644
--- a/lib/Voxel3D.lua
+++ b/lib/Voxel3D.lua
@@ -205,6 +205,7 @@ local SHADER = [[
   uniform float ghost;        // 0 = shade normally, 1 = flatten to it
   uniform vec3 dayTint;       // the hour's light on the world; 1,1,1 = noon
   uniform Image glassMask;    // opaque where the atlas texel is window glass
+  uniform vec2 glassSize;     // the mask's dimensions: tc -> atlas texels
   uniform float glassNight;   // 0 = daylight .. 1 = the lamps are on
   uniform float glassPhase;   // the glint's phase: advances with TRAVEL
   uniform float glassGlint;   // and its strength: 0 while standing still
@@ -241,7 +242,13 @@ local SHADER = [[
     // cast with lamplight at night.
     float glass = Texel(glassMask, tc).a * glassOn;
     if (glass > 0.0) {
-      float sweep = sin((sc.x + sc.y) * 0.04 - glassPhase);
+      // the sweep lives in the PANE's own space (atlas texels), not the
+      // screen's: a pattern anchored to the screen has the world sliding
+      // through it at zoom speed whenever the camera pans, which strobed --
+      // worst where the pan and the phase ran opposite ways. Anchored to
+      // the glass, panning moves nothing; only the phase does, a fraction
+      // of a texel per step, the same in every walking direction.
+      float sweep = sin(tc.x * glassSize.x * 0.8 - glassPhase);
       float glint = pow(max(sweep, 0.0), 20.0) * 0.55 * glassGlint;
       vec3 pane = mix(rgb, vec3(0.93, 0.97, 1.0), glint * glass);
       float shine = dot(p.rgb, vec3(0.299, 0.587, 0.114));
@@ -607,7 +614,11 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
   -- 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
   local mask = Voxel3D.glassMask or GlassMask.blank()
-  if mask then pcall(sh.send, sh, "glassMask", mask) end
+  if mask then
+    pcall(sh.send, sh, "glassMask", mask)
+    local ok, mw, mh = pcall(mask.getDimensions, mask)
+    pcall(sh.send, sh, "glassSize", { ok and mw or 1, ok and mh or 1 })
+  end
   pcall(sh.send, sh, "glassNight", Voxel3D.glassNight or 0)
   pcall(sh.send, sh, "glassPhase", Voxel3D.glassPhase or 0)
   pcall(sh.send, sh, "glassGlint", Voxel3D.glassGlint or 0)
diff --git a/lib/VoxelScene.lua b/lib/VoxelScene.lua
index 4d401ea..01cfa0f 100644
--- a/lib/VoxelScene.lua
+++ b/lib/VoxelScene.lua
@@ -307,12 +307,13 @@ local function drawEntity(sprite, px, py, facing, phase, flip, gh, colors,
   -- drift): lets the leaned-back head win against the wall it leans
   -- OVER while a character genuinely BEHIND a building is dozens of
   -- pixels deeper and still loses, so real occlusion works.
-  -- the same card UNLEANED is what the sun saw (castShadows draws
-  -- exactly this mesh), so that is where each vertex asks whether the
-  -- light reached it -- see Voxel3D.draw
+  -- the same card UNLEANED -- and SNUGGED, exactly as the sun stored it
+  -- (castShadows draws this mesh through ShadowMap.snug) -- is where each
+  -- vertex asks whether the light reached it; see ShadowMap.snug for why
+  -- the lookup must match the stored transform to the letter
   Voxel3D.draw(mesh, tex, billboardMatrix(px, py, y, mirror),
                billboardPull(),
-               Voxel3D.casterMatrix(px, py, y, mirror))
+               ShadowMap.snug(Voxel3D.casterMatrix(px, py, y, mirror)))
   return true
 end
 
@@ -464,7 +465,11 @@ end
 -- with distance covered and its strength fades in over a few steps of
 -- walking and back out within a beat of standing still. Stand still and
 -- the glass is still; move and the light crosses it.
-VoxelScene.GLINT_RATE = 0.22     -- radians of sweep per world pixel travelled
+-- The rate is slow on purpose: the sweep pattern lives in the pane's own
+-- texels (see the scene shader), so this is a FRACTION of a texel per world
+-- pixel walked -- one full pass of the glint across a pane per eight or so
+-- cells of travel, with no frame ever jumping it far enough to strobe.
+VoxelScene.GLINT_RATE = 0.05     -- radians of sweep per world pixel travelled
 VoxelScene.GLINT_IN = 0.12      -- strength gained per moving frame
 VoxelScene.GLINT_OUT = 0.08     -- and lost per resting frame
 
@@ -699,11 +704,13 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
   -- wins the overlap, which is the order the flat game draws them in.
   local figPull = billboardPull()
   eachFigure(state.map, 0, 0, function(mesh, model, caster)
-    Voxel3D.draw(mesh, atlasFor(state.map), model, figPull, caster)
+    Voxel3D.draw(mesh, atlasFor(state.map), model, figPull,
+                 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, caster)
+      Voxel3D.draw(mesh, atlasFor(nb.map), model, figPull,
+                   ShadowMap.snug(caster))
     end)
   end
   -- and the seams are back on for the terrain art that follows: grass and
@@ -733,11 +740,14 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
   -- ON, while the nearest flower of the cell south (+20) stays in front
   -- and keeps overdrawing their feet.
   local fpull = math.max(0, pull - 8 * math.sin(math.max(Voxel.angle, 0.05)))
+  -- flowers are snugged casters too, so they read their own shadowing
+  -- through the same snugged transform the sun stored them with
   Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil,
-               fpull)
+               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)
+                 Mat4.translate(nb.ox, 0, nb.oy), fpull,
+                 ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
   end
 
   return Voxel3D.endScene()