This repository was archived by the owner on Aug 14, 2026. It is now read-only.
forked from TeJota1337/DramaticShapeVoxelMod
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAntiAlias.lua
More file actions
244 lines (222 loc) · 10.5 KB
/
Copy pathAntiAlias.lua
File metadata and controls
244 lines (222 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
-- Voxel world mode: anti-aliasing, by supersampling.
--
-- Everything else in this mod is flat art blitted at whole pixels; this one
-- pass is real geometry seen through a perspective camera, and a polygon
-- edge that lands at an angle across the pixel grid is the one place in the
-- game where a hard stair-step is not a stylistic choice. A roof ridge, a
-- ledge lip, a tree's silhouette against the sky and the leaning card of a
-- character are all cut by an edge that has no reason to line up with
-- anything, and at the shallow rungs -- where the diorama reads most like a
-- photograph of a model -- they crawl as the camera drifts.
--
-- SUPERSAMPLING, not MSAA and not a filter over the finished frame, for two
-- reasons that both come out of what the pass already is:
--
-- MSAA would take the water with it. The reflections read the frame's own
-- DEPTH buffer as a texture (Voxel3D.beginWater), and a multisampled depth
-- attachment is not a thing a fragment shader in this dialect can sample.
-- The row would have quietly switched the other row off.
--
-- An edge filter (FXAA and its relatives) works from the finished colour
-- alone, and would be GUESSING where the edges are out of one sample per
-- pixel -- inventing detail it never rendered, and unable to tell a
-- geometry edge from the boundary between two texels of a tileset.
--
-- Rendering the pass larger and folding it back down has neither problem:
-- the depth buffer stays an ordinary texture, every pass in the frame keeps
-- working in the canvas it was handed, and the fold is an average of samples
-- that were each rendered honestly. It antialiases everything at once --
-- geometry, the alpha-cut outline of a sprite card, the wireframe, the
-- water's ray march -- because none of them know it is happening.
--
-- Be clear about what "everything" means: the artwork softens too. A tileset
-- texel out here is not a screen pixel, it is a quad in a perspective view,
-- and its boundary crosses the pixel grid at the same arbitrary angle a roof
-- ridge does -- so the fold averages across it exactly as it averages across
-- the ridge. That is what an honest extra sample says about that pixel, and
-- it is also the trade the row IS: the diorama comes out smoother, not
-- sharper. Which is why this is a row and not something that is simply on.
--
-- What it costs is pixels, which is the whole of why this is a row and not
-- something that is simply on: 2X is half again as many in each direction,
-- 4X is twice, and the scene pass is the most expensive thing in the frame.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local ModSetting = V.require("ModSetting")
local AntiAlias = {}
-- the key under options.modOptions.DRAMATIC_SHAPE, shared by the row in
-- OPTIONS and the mod manager's own settings page for this mod
AntiAlias.KEY = "aa"
AntiAlias.LABEL = "AA"
-- The ladder is SAMPLES PER DISPLAY PIXEL, which is how an AA setting reads
-- everywhere else, and the canvas scale each rung costs is its square root:
-- 2 samples is a canvas 1.41x wider and taller, 4 is one exactly twice the
-- size. OFF is the default -- this is a cost knob, and a mod should not
-- quietly spend four times the fill rate of the machine it lands on.
AntiAlias.setting = ModSetting.new(AntiAlias.KEY, AntiAlias.LABEL,
{ 0, 2, 4 }, { "OFF", "2X", "4X" })
-- The scale the pass currently open was actually expanded by (see expand).
-- 1 while there is no supersampling in force, which is also what every
-- reader gets on a frame that never opened a pass at all.
local live = 1
function AntiAlias.samples()
return tonumber(AntiAlias.setting:get()) or 0
end
-- What the row ASKS for. The scale in force is `factor()`, which is this
-- clamped to what the driver will actually allocate.
local function wanted()
local n = AntiAlias.samples()
if n <= 1 then return 1 end
return math.sqrt(n)
end
-- The biggest canvas this driver admits to, or nil where it will not say.
-- A 4K window at 4X asks for 7680 across, which is past the limit on plenty
-- of hardware and every phone -- and a refused canvas is not a softer
-- diorama, it is beginScene returning false and the whole mode falling back
-- to the flat 2D path.
local function textureLimit()
if not (love.graphics and love.graphics.getSystemLimits) then return nil end
local ok, limits = pcall(love.graphics.getSystemLimits)
return (ok and limits and limits.texturesize) or nil
end
-- The size to render `w` x `h` display pixels at, and the size everything
-- inside the pass then measures itself in.
--
-- Also where `live` is set, which is why this must be called once per pass
-- immediately before beginScene: the wireframe's line width and the FX
-- overlay's sprite scale are both quoted in DISPLAY pixels and have to be
-- multiplied up into canvas ones, and the honest multiplier is the one this
-- returned rather than the one the row asked for.
function AntiAlias.expand(w, h)
local s = wanted()
local max = textureLimit()
if max and max > 0 then
-- clamped rather than abandoned: a window too big for 4X can usually
-- still carry some of it, and half a rung of smoothing is worth more
-- than a row that silently does nothing at that size
s = math.min(s, max / math.max(1, w), max / math.max(1, h))
end
if not (s > 1.01) then
live = 1
return w, h
end
local ew, eh = math.floor(w * s + 0.5), math.floor(h * s + 0.5)
live = ew / math.max(1, w)
return ew, eh
end
-- The scale the open pass was expanded by; 1 when it was not.
function AntiAlias.factor()
return live
end
-- ------- the fold
--
-- One target per pass (the free-roam world and the battle's arena are alive
-- at different moments but reallocating on every battle entry and exit is
-- what the scene canvas's own slots exist to avoid), reallocated only when
-- that pass's DISPLAY size changes -- a window resize, or the row itself
-- moving, which changes the source and not this.
local targets = {}
local function targetFor(slot, w, h)
local t = targets[slot]
if not (t and t.w == w and t.h == h) then
local ok, c = pcall(love.graphics.newCanvas, w, h)
if not (ok and c) then return nil end
-- nearest, like the canvas it stands in for: this one is composited a
-- canvas pixel to a display pixel, and the smoothing has already happened
pcall(c.setFilter, c, "nearest", "nearest")
if t and t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
t = { canvas = c, w = w, h = h }
targets[slot] = t
end
return t.canvas
end
-- The box filter, and the whole of why it is a shader rather than a scaled
-- draw with linear filtering on.
--
-- The void this pass renders into is cleared to a TRANSPARENT BLACK, and at
-- the rungs below FULL a good deal of the frame is still that. Averaging a
-- straight-alpha edge against it drags the result toward black as well as
-- toward transparent, and then the engine's own composite multiplies by that
-- alpha a second time -- so every silhouette against the void would come out
-- ringed with a dark fringe, which is exactly the artefact the row is here to
-- remove.
--
-- So the taps are premultiplied before they are averaged and divided back out
-- after, which is the arithmetic that makes an edge pixel mean "half covered
-- by this colour" instead of "covered by half of this colour".
--
-- Four taps, half a source texel from the destination centre. At 4X those
-- land dead on the four texel centres the destination pixel covers, so it is
-- an exact 2x2 box; at 2X the source grid does not divide, and the bilinear
-- fetch under each tap widens the box a little rather than missing samples.
local SHADER = [[
uniform vec2 tap; // half a SOURCE texel, in uv
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
vec4 a = Texel(tex, tc + vec2(-tap.x, -tap.y));
vec4 b = Texel(tex, tc + vec2( tap.x, -tap.y));
vec4 c = Texel(tex, tc + vec2(-tap.x, tap.y));
vec4 d = Texel(tex, tc + vec2( tap.x, tap.y));
float al = (a.a + b.a + c.a + d.a) * 0.25;
if (al <= 0.0) return vec4(0.0);
vec3 sum = a.rgb * a.a + b.rgb * b.a + c.rgb * c.a + d.rgb * d.a;
return vec4(sum * 0.25 / al, al) * color;
}
]]
local shader = nil -- nil = untried, false = unavailable
local function getShader()
if shader == nil then
local ok, sh = pcall(love.graphics.newShader, SHADER)
shader = (ok and sh) or false
end
return shader or nil
end
-- Fold `canvas` down to `w` x `h` and hand back the result.
--
-- Returns the input untouched when there is nothing to fold -- the row is
-- off, or the canvas already IS that size -- so a caller can run it
-- unconditionally, and so can a headless test run. A target that would not
-- allocate is the same answer: the pass is lost either way if this hands back
-- something the wrong size, so it hands back the input and the frame draws at
-- the size it was rendered.
function AntiAlias.resolve(canvas, w, h, slot)
if not canvas then return canvas end
local ok, cw, ch = pcall(canvas.getDimensions, canvas)
if not ok or (cw == w and ch == h) then return canvas end
local target = targetFor(slot or "world", w, h)
if not target then return canvas end
local sh = getShader()
local prevBlend, prevAlpha = love.graphics.getBlendMode()
-- the scene canvas filters nearest for its usual 1:1 blit; the taps want
-- linear, put back below so every other pass finds what it expects
pcall(canvas.setFilter, canvas, "linear", "linear")
love.graphics.setColor(1, 1, 1, 1)
-- replace, not alpha-blend: this is an image-processing copy, and the alpha
-- the shader worked out has to land as itself rather than be composited
-- against whatever the target held
love.graphics.setBlendMode("replace", "premultiplied")
if sh then
love.graphics.setShader(sh)
pcall(sh.send, sh, "tap", { 0.5 / cw, 0.5 / ch })
end
local drew = pcall(function()
love.graphics.setCanvas(target)
love.graphics.clear(0, 0, 0, 0)
love.graphics.draw(canvas, 0, 0, 0, w / cw, h / ch)
end)
love.graphics.setCanvas()
love.graphics.setShader()
love.graphics.setBlendMode(prevBlend or "alpha", prevAlpha)
pcall(canvas.setFilter, canvas, "nearest", "nearest")
return drew and target or canvas
end
-- Drop the GPU objects (window resize, hot reload).
function AntiAlias.invalidate()
for slot, t in pairs(targets) do
if t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
targets[slot] = nil
end
end
function AntiAlias.row()
return AntiAlias.setting:row()
end
return AntiAlias