mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 10:00:50 +02:00
add day/night filter to 2d
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
-- The hour's light on the FLAT world.
|
||||
--
|
||||
-- The clock already reaches everything the 3D pass draws: VoxelScene and
|
||||
-- BattleScene multiply the whole scene by DayNight.tint, so walking around a
|
||||
-- route at dusk warms the diorama and midnight turns it blue. Switch voxel
|
||||
-- mode off and none of that happens -- the tint is a uniform in a shader the
|
||||
-- flat tile path never runs -- so the same evening that fell on the diorama
|
||||
-- left the 2D world at permanent noon. One clock, two worlds, one of them
|
||||
-- ignoring it.
|
||||
--
|
||||
-- So the flat composite gets the same multiply, painted as one rectangle.
|
||||
--
|
||||
-- ------- WHERE, which is the only difficult part
|
||||
--
|
||||
-- Not on the world canvas. In a colorized mode that canvas is grayscale art
|
||||
-- and the blit that puts it on screen runs it through the palette shader,
|
||||
-- which classifies each pixel into a shade BY ITS RED CHANNEL. Multiply a
|
||||
-- night blue over it first and every shade lands in the wrong bucket -- the
|
||||
-- world would not darken, it would change colour into whatever the palette
|
||||
-- said the wrong bucket was.
|
||||
--
|
||||
-- So it goes on AFTER that pass, on the composited world. And not after the
|
||||
-- whole frame either: the UI blit is next, and the dialog boxes, the menus and
|
||||
-- the HUD are paper held up in front of the world rather than part of it --
|
||||
-- the same reason the tilt-shift blur is a worldPresent and not a present.
|
||||
--
|
||||
-- Which leaves one instant: between the world blit and the UI blit, inside
|
||||
-- Renderer:endFrame. There is no seam there -- worldPresent, the engine's own
|
||||
-- hook for exactly this, only runs when a PIPELINE produced the world, which
|
||||
-- in flat mode is the one thing that did not happen. So endFrame is wrapped
|
||||
-- and the UI canvas's own draw call is watched for: `blit` passes the canvas
|
||||
-- as the first argument, so the first draw of Renderer.canvas IS the boundary,
|
||||
-- by identity rather than by counting or guessing.
|
||||
--
|
||||
-- The shader and scissor that call arrives under belong to the UI blit already
|
||||
-- in progress, so both are put aside for the rectangle and handed straight
|
||||
-- back -- otherwise the tint would be palette-remapped and clipped to a zone.
|
||||
--
|
||||
-- ------- WHEN
|
||||
--
|
||||
-- Outdoors, on the flat path, when the hour is not neutral. Each of those is
|
||||
-- load-bearing:
|
||||
--
|
||||
-- the flat path a pipeline that rendered the world already applied the
|
||||
-- tint inside its own shader; painting it again would apply
|
||||
-- the hour twice. worldOverride is exactly "a pipeline drew
|
||||
-- this frame".
|
||||
-- outdoors a room has no sky to take its light from, which is the
|
||||
-- same answer DayNight.tint gives on its own and the same
|
||||
-- one applyRig gives the sun.
|
||||
-- not neutral midday is a multiply by white. Skipped rather than drawn,
|
||||
-- so a game with the clock at DAY issues not one extra call.
|
||||
|
||||
-- the mod namespace (see main.lua): V.require loads a sibling module
|
||||
local V = ...
|
||||
|
||||
local DayNight = V.require("DayNight")
|
||||
|
||||
local DayTint = {}
|
||||
|
||||
-- Below this the tint is close enough to white that the rectangle would not
|
||||
-- change a pixel, and the frame is left exactly as it was.
|
||||
DayTint.NEUTRAL = 0.999
|
||||
|
||||
local function outdoorNow()
|
||||
local ok, Game = pcall(require, "src.core.Game")
|
||||
if not ok then return false end
|
||||
local ow = Game and Game.overworld
|
||||
local map = ow and ow.map
|
||||
if not map then return false end
|
||||
local okMap, Map = pcall(require, "src.world.Map")
|
||||
if not okMap then return false end
|
||||
local outdoor = map.def and Map.isOutdoor(map.def) or false
|
||||
-- a canopy floor takes the hour's colour and nothing else of it, exactly as
|
||||
-- it does in the 3D pass (BattleScene, VoxelScene)
|
||||
return outdoor or DayNight.isCanopy(map)
|
||||
end
|
||||
|
||||
-- The colour this frame's world should be multiplied by, or nil to leave the
|
||||
-- frame alone.
|
||||
function DayTint.forFrame(renderer)
|
||||
if not renderer then return nil end
|
||||
if renderer.worldOverride then return nil end -- a pipeline drew, and tinted
|
||||
if not renderer.worldActive then return nil end -- no world on screen at all
|
||||
if not outdoorNow() then return nil end
|
||||
local tint = DayNight.tint(true)
|
||||
if not tint then return nil end
|
||||
local r, g, b = tint[1] or 1, tint[2] or 1, tint[3] or 1
|
||||
if r > DayTint.NEUTRAL and g > DayTint.NEUTRAL and b > DayTint.NEUTRAL then
|
||||
return nil
|
||||
end
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
-- One rectangle over the window, multiplied into whatever is under it.
|
||||
--
|
||||
-- The whole window rather than the world's own rect, which is what the
|
||||
-- engine's warp fade does from the same place and for the same reason: the
|
||||
-- border fill, the letterbox bars and the world are all "the world" as far as
|
||||
-- the hour is concerned, and black multiplied by anything is still black.
|
||||
-- Every read of the graphics state is optional, because a headless driver
|
||||
-- ships some of these and not others -- the same reason TerrainAtlas reads the
|
||||
-- engine's seams guarded. What cannot be read cannot be put back either, and a
|
||||
-- missing accessor must cost the tint rather than the frame.
|
||||
local function saved(name, ...)
|
||||
local fn = love.graphics[name]
|
||||
if not fn then return nil end
|
||||
local ok, a, b, c, d = pcall(fn, ...)
|
||||
if not ok then return nil end
|
||||
return a, b, c, d
|
||||
end
|
||||
|
||||
function DayTint.paint(r, g, b)
|
||||
local gfx = love.graphics
|
||||
local shader = saved("getShader")
|
||||
local sx, sy, sw, sh = saved("getScissor")
|
||||
local blend, alpha = saved("getBlendMode")
|
||||
local pr, pg, pb, pa = saved("getColor")
|
||||
local w, h = gfx.getDimensions()
|
||||
|
||||
if gfx.setShader then gfx.setShader() end
|
||||
if gfx.setScissor then gfx.setScissor() end
|
||||
gfx.setBlendMode("multiply", "premultiplied")
|
||||
gfx.setColor(r, g, b, 1)
|
||||
gfx.rectangle("fill", 0, 0, w, h)
|
||||
|
||||
gfx.setBlendMode(blend or "alpha", alpha)
|
||||
gfx.setColor(pr or 1, pg or 1, pb or 1, pa or 1)
|
||||
if gfx.setScissor then
|
||||
if sx then gfx.setScissor(sx, sy, sw, sh) else gfx.setScissor() end
|
||||
end
|
||||
if shader and gfx.setShader then gfx.setShader(shader) end
|
||||
end
|
||||
|
||||
function DayTint.install()
|
||||
local Renderer = require("src.render.Renderer")
|
||||
if Renderer.dramaticShapeTintHook then return end
|
||||
local inner = Renderer.endFrame
|
||||
|
||||
function Renderer:endFrame(zones, worldZones)
|
||||
local r, g, b = DayTint.forFrame(self)
|
||||
if not r then return inner(self, zones, worldZones) end
|
||||
|
||||
local gfx = love.graphics
|
||||
local draw = gfx.draw
|
||||
local ui = self.canvas
|
||||
local painted = false
|
||||
gfx.draw = function(tex, ...)
|
||||
-- the UI canvas reaching the screen: the world is finished, the paper
|
||||
-- in front of it has not started. Restored FIRST so the rectangle's own
|
||||
-- drawing cannot re-enter this, and so a UI blit that draws one quad per
|
||||
-- SGB zone only triggers it once.
|
||||
if not painted and tex == ui then
|
||||
painted = true
|
||||
gfx.draw = draw
|
||||
DayTint.paint(r, g, b)
|
||||
end
|
||||
return draw(tex, ...)
|
||||
end
|
||||
|
||||
local ok, err = pcall(inner, self, zones, worldZones)
|
||||
gfx.draw = draw
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
Renderer.dramaticShapeTintHook = true
|
||||
end
|
||||
|
||||
return DayTint
|
||||
Reference in New Issue
Block a user