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 pathTiltShift.lua
More file actions
179 lines (160 loc) · 6.81 KB
/
Copy pathTiltShift.lua
File metadata and controls
179 lines (160 loc) · 6.81 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
-- Voxel world mode: tilt-shift post-process (the miniature-diorama look).
--
-- A depth-of-field fake that reads perfectly on a tilted voxel scene: a
-- horizontal band through the view centre (where the camera focuses -- the
-- player) stays sharp, and the frame blurs progressively toward the top
-- and bottom edges, with a slight saturation lift to sell the model-photo
-- feel. It runs on the finished voxel canvas as two separable gaussian
-- passes, so it costs two fullscreen draws and touches nothing in the 3D
-- pass itself.
--
-- This is a render pipeline in its own right -- a worldPresent pass, which
-- is the stage that post-processes the finished world BEFORE the UI
-- composites over it. That placement is the whole point: a miniature-photo
-- blur belongs on the diorama, not on the dialog box in front of it.
--
-- The engine owns the level: the T-SHIFT options row, its hotkey, the
-- OFF -> 1 -> 2 -> 3 -> OFF ladder and persistence in
-- save.options.pipelines all come from the render_pipelines record in
-- main.lua, and the current level arrives through update(). Each step
-- narrows the sharp band, deepens the blur and pushes the saturation a
-- little further.
--
-- Because worldPresent only runs when some pipeline rendered the world,
-- this draws over the voxel scene and nothing else. With it off (or on any
-- failure -- headless, no shader support) apply() hands the canvas back
-- untouched, so every other path is byte-for-byte what it always was.
local V = ...
local TiltShift = {}
TiltShift.level = 0
TiltShift.LABELS = { "OFF", "1", "2", "3" }
-- The strength ladder. `spacing` is the gap between the gaussian's taps
-- at full blur, as a fraction of the canvas height (so the miniature
-- reads the same in a window and fullscreen; the blur's reach is 4x the
-- spacing per pass, and the two passes compound). `band` is the
-- half-height of the fully sharp zone and `range` the ramp to full blur,
-- both in canvas-uv units. The focus line sits at the vertical centre
-- because that is where the voxel camera parks the player.
TiltShift.FOCUS_Y = 0.5
TiltShift.PRESETS = {
[1] = { spacing = 0.0016, band = 0.14, range = 0.42, saturation = 1.10 },
[2] = { spacing = 0.0028, band = 0.10, range = 0.36, saturation = 1.18 },
[3] = { spacing = 0.0042, band = 0.07, range = 0.30, saturation = 1.28 },
}
local SHADER = [[
uniform vec2 dir; // one texel step along the axis being blurred
uniform float focusY;
uniform float band;
uniform float range;
uniform float spacing; // gap between taps at full blur, in texels
uniform float boost; // 0 = plain blur pass, 1 = final pass (color pop)
uniform float saturation;
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
float d = abs(tc.y - focusY) - band;
float s = clamp(d / range, 0.0, 1.0);
s = s * s; // ease in, so the band edge has no visible seam
// 9 taps a small step apart: dense enough that even the strongest
// preset blurs smoothly instead of ghosting into streaks
vec2 o = dir * (s * spacing);
vec4 sum = Texel(tex, tc) * 0.2270270270;
sum += (Texel(tex, tc + o) + Texel(tex, tc - o)) * 0.1945945946;
sum += (Texel(tex, tc + 2.0 * o) + Texel(tex, tc - 2.0 * o)) * 0.1216216216;
sum += (Texel(tex, tc + 3.0 * o) + Texel(tex, tc - 3.0 * o)) * 0.0540540541;
sum += (Texel(tex, tc + 4.0 * o) + Texel(tex, tc - 4.0 * o)) * 0.0162162162;
if (boost > 0.5) {
float luma = dot(sum.rgb, vec3(0.299, 0.587, 0.114));
sum.rgb = mix(vec3(luma), sum.rgb, saturation);
}
return sum * color;
}
]]
local shader = nil -- nil = untried, false = unavailable
local ping, pong, cw, ch = nil, nil, 0, 0
local function getShader()
if shader == nil then
local ok, sh = pcall(function() return love.graphics.newShader(SHADER) end)
shader = (ok and sh) or false
end
return shader or nil
end
local function getCanvases(w, h)
if not ping or cw ~= w or ch ~= h then
local PixelCanvas = V.require("PixelCanvas")
local ok, a = PixelCanvas.new(w, h)
if not ok then return nil end
local okB, b = PixelCanvas.new(w, h)
if not okB then return nil end
-- the gaussian's fractional tap offsets need linear filtering
a:setFilter("linear", "linear")
b:setFilter("linear", "linear")
ping, pong, cw, ch = a, b, w, h
end
return ping, pong
end
function TiltShift.setLevel(level)
level = math.floor(tonumber(level) or 0)
if level < 0 then level = 0 end
if level > 3 then level = 3 end
TiltShift.level = level
end
-- The pipeline's per-frame tick: the engine hands over the level the
-- player has the T-SHIFT row set to. No tween -- unlike the camera angle,
-- a blur strength has nothing to ease through.
function TiltShift.update(_, level)
TiltShift.setLevel(level)
end
function TiltShift.levelLabel(level)
return TiltShift.LABELS[(level or TiltShift.level) + 1] or "OFF"
end
function TiltShift.active()
return TiltShift.level > 0
end
function TiltShift.reset()
TiltShift.level = 0
end
-- Run the effect over `canvas` and return the processed canvas. Returns
-- the input unchanged when the effect is off or cannot run (headless, no
-- shader support), so the caller composites exactly what it was handed.
function TiltShift.apply(canvas)
local preset = TiltShift.PRESETS[TiltShift.level]
if not (preset and canvas) then return canvas end
local sh = getShader()
if not sh then return canvas end
local w, h = canvas:getDimensions()
local a, b = getCanvases(w, h)
if not a then return canvas end
local spacing = math.max(0.75, math.min(3, h * preset.spacing))
local prevBlend, prevAlpha = love.graphics.getBlendMode()
-- the voxel canvas filters nearest for its 1:1 blit; the blur taps need
-- linear, restored below so the composite path sees what it expects
canvas:setFilter("linear", "linear")
love.graphics.setShader(sh)
love.graphics.setColor(1, 1, 1, 1)
-- replace, not alpha-blend: these are image-processing copies
love.graphics.setBlendMode("replace", "premultiplied")
pcall(sh.send, sh, "focusY", TiltShift.FOCUS_Y)
pcall(sh.send, sh, "band", preset.band)
pcall(sh.send, sh, "range", preset.range)
pcall(sh.send, sh, "spacing", spacing)
pcall(sh.send, sh, "saturation", preset.saturation)
local ok = pcall(function()
love.graphics.setCanvas(a)
pcall(sh.send, sh, "dir", { 1 / w, 0 })
pcall(sh.send, sh, "boost", 0)
love.graphics.draw(canvas)
love.graphics.setCanvas(b)
pcall(sh.send, sh, "dir", { 0, 1 / h })
pcall(sh.send, sh, "boost", 1)
love.graphics.draw(a)
end)
love.graphics.setCanvas()
love.graphics.setShader()
love.graphics.setBlendMode(prevBlend or "alpha", prevAlpha)
canvas:setFilter("nearest", "nearest")
return ok and b or canvas
end
-- Drop the GPU objects (window resize, hot reload).
function TiltShift.invalidate()
ping, pong, cw, ch = nil, nil, 0, 0
end
return TiltShift