]> git.sesse.net Git - nageru/blob - theme.lua
Hook up a third input, the SBS mix. Required hooking up ResizeEffect.
[nageru] / theme.lua
1 -- The theme is what decides what's actually shown on screen, what kind of
2 -- transitions are available (if any), and what kind of inputs there are,
3 -- if any. In general, it drives the entire display logic by creating Movit
4 -- chains, setting their parameters and then deciding which to show when.
5 --
6 -- Themes are written in Lua, which reflects a simplified form of the Movit API
7 -- where all the low-level details (such as texture formats) are handled by the
8 -- C++ side and you generally just build chains.
9 io.write("hello from lua\n")
10
11 local zoom_start = -2.0
12 local zoom_end = -1.0
13 local zoom_src = 0.0
14 local zoom_dst = 1.0
15
16 local live_signal_num = 0
17 local preview_signal_num = 1
18
19 -- The main live chain.
20 function make_sbs_chain(hq)
21         local chain = EffectChain.new(16, 9)
22         local input0 = chain:add_live_input()
23         input0:connect_signal(0)
24         local input1 = chain:add_live_input()
25         input1:connect_signal(1)
26
27         local resample_effect = nil
28         local resize_effect = nil
29         if (hq) then
30                 resample_effect = chain:add_effect(ResampleEffect.new(), input0)
31         else
32                 resize_effect = chain:add_effect(ResizeEffect.new(), input0)
33         end
34
35         local padding_effect = chain:add_effect(IntegralPaddingEffect.new())
36         padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 1.0)
37
38         local resample2_effect = nil
39         local resize2_effect = nil
40         if (hq) then
41                 resample2_effect = chain:add_effect(ResampleEffect.new(), input1)
42         else
43                 resize2_effect = chain:add_effect(ResizeEffect.new(), input1)
44         end
45         -- Effect *saturation_effect = chain->add_effect(new SaturationEffect())
46         -- CHECK(saturation_effect->set_float("saturation", 0.3f))
47         local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
48         wb_effect:set_float("output_color_temperature", 3500.0)
49         local padding2_effect = chain:add_effect(IntegralPaddingEffect.new())
50
51         chain:add_effect(OverlayEffect.new(), padding_effect, padding2_effect)
52         chain:finalize(hq)
53
54         return {
55                 chain = chain,
56                 input0 = {
57                         input = input0,
58                         resample_effect = resample_effect,
59                         resize_effect = resize_effect,
60                         padding_effect = padding_effect
61                 },
62                 input1 = {
63                         input = input1,
64                         resample_effect = resample2_effect,
65                         resize_effect = resize2_effect,
66                         padding_effect = padding2_effect
67                 }
68         }
69 end
70
71 local main_chain_hq = make_sbs_chain(true)
72 local main_chain_lq = make_sbs_chain(false)
73
74 -- A chain to show a single input on screen (HQ version).
75 local simple_chain_hq = EffectChain.new(16, 9)
76 local simple_chain_hq_input = simple_chain_hq:add_live_input()
77 simple_chain_hq_input:connect_signal(0);  -- First input card. Can be changed whenever you want.
78 simple_chain_hq:finalize(true)
79
80 -- A chain to show a single input on screen (LQ version).
81 local simple_chain_lq = EffectChain.new(16, 9)
82 local simple_chain_lq_input = simple_chain_lq:add_live_input()
83 simple_chain_lq_input:connect_signal(0);  -- First input card. Can be changed whenever you want.
84 simple_chain_lq:finalize(false)
85
86 -- Returns the number of outputs in addition to the live (0) and preview (1).
87 -- Called only once, at the start of the program.
88 function num_channels()
89         return 3
90 end
91
92 -- Called every frame.
93 function get_transitions()
94         return {"Cut", "Fade", "Zoom!"}
95 end
96
97 function transition_clicked(num, t)
98         -- local temp = live_signal_num
99         -- live_signal_num = preview_signal_num
100         -- preview_signal_num = temp
101
102         zoom_start = t
103         zoom_end = t + 1.0
104
105         local temp = zoom_src
106         zoom_src = zoom_dst
107         zoom_dst = temp
108 end
109
110 function channel_clicked(num, t)
111         -- Presumably change the preview here.
112         io.write("STUB: channel_clicked\n")
113 end
114
115 -- Called every frame. Get the chain for displaying at input <num>,
116 -- where 0 is live, 1 is preview, 2 is the first channel to display
117 -- in the bottom bar, and so on up to num_channels()+1. t is the 
118 -- current time in seconds. width and height are the dimensions of
119 -- the output, although you can ignore them if you don't need them
120 -- (they're useful if you want to e.g. know what to resample by).
121 --
122 -- You should return two objects; the chain itself, and then a
123 -- function (taking no parameters) that is run just before rendering.
124 -- The function needs to call connect_signal on any inputs, so that
125 -- it gets updated video data for the given frame. (You are allowed
126 -- to switch which input your input is getting from between frames,
127 -- but not calling connect_signal results in undefined behavior.)
128 -- If you want to change any parameters in the chain, this is also
129 -- the right place.
130 --
131 -- NOTE: The chain returned must be finalized with the Y'CbCr flag
132 -- if and only if num==0.
133 function get_chain(num, t, width, height)
134         if num == 0 then  -- Live.
135                 if t > zoom_end and zoom_dst == 1.0 then
136                         -- Special case: Show only the single image on screen.
137                         prepare = function()
138                                 simple_chain_hq_input:connect_signal(live_signal_num)
139                         end
140                         return simple_chain_hq, prepare
141                 end
142                 prepare = function()
143                         if t < zoom_start then
144                                 prepare_sbs_chain(main_chain_hq, zoom_src, width, height)
145                         elseif t > zoom_end then
146                                 prepare_sbs_chain(main_chain_hq, zoom_dst, width, height)
147                         else
148                                 local tt = (t - zoom_start) / (zoom_end - zoom_start)
149                                 -- Smooth it a bit.
150                                 tt = math.sin(tt * 3.14159265358 * 0.5)
151                                 prepare_sbs_chain(main_chain_hq, zoom_src + (zoom_dst - zoom_src) * tt, width, height)
152                         end
153                 end
154                 return main_chain_hq.chain, prepare
155         end
156         if num == 1 then  -- Preview.
157                 prepare = function()
158                         simple_chain_lq_input:connect_signal(preview_signal_num)
159                 end
160                 return simple_chain_lq, prepare
161         end
162         if num == 2 then
163                 prepare = function()
164                         simple_chain_lq_input:connect_signal(0)
165                 end
166                 return simple_chain_lq, prepare
167         end
168         if num == 3 then
169                 prepare = function()
170                         simple_chain_lq_input:connect_signal(1)
171                 end
172                 return simple_chain_lq, prepare
173         end
174         if num == 4 then
175                 prepare = function()
176                         prepare_sbs_chain(main_chain_lq, 0.0, width, height)
177                 end
178                 return main_chain_lq.chain, prepare
179         end
180 end
181
182 function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height)
183         local srcx0 = 0.0
184         local srcx1 = 1.0
185         local srcy0 = 0.0
186         local srcy1 = 1.0
187
188         -- Cull.
189         if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
190                 resample_effect:set_int("width", 1)
191                 resample_effect:set_int("height", 1)
192                 resample_effect:set_float("zoom_x", screen_width)
193                 resample_effect:set_float("zoom_y", screen_height)
194                 padding_effect:set_int("left", screen_width + 100)
195                 padding_effect:set_int("top", screen_height + 100)
196                 return
197         end
198
199         -- Clip. (TODO: Clip on upper/left sides, too.)
200         if x1 > screen_width then
201                 srcx1 = (screen_width - x0) / (x1 - x0)
202                 x1 = screen_width
203         end
204         if y1 > screen_height then
205                 srcy1 = (screen_height - y0) / (y1 - y0)
206                 y1 = screen_height
207         end
208
209         if resample_effect ~= nil then
210                 -- High-quality resampling.
211                 local x_subpixel_offset = x0 - math.floor(x0)
212                 local y_subpixel_offset = y0 - math.floor(y0)
213
214                 -- Resampling must be to an integral number of pixels. Round up,
215                 -- and then add an extra pixel so we have some leeway for the border.
216                 local width = math.ceil(x1 - x0) + 1
217                 local height = math.ceil(y1 - y0) + 1
218                 resample_effect:set_int("width", width)
219                 resample_effect:set_int("height", height)
220
221                 -- Correct the discrepancy with zoom. (This will leave a small
222                 -- excess edge of pixels and subpixels, which we'll correct for soon.)
223                 local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
224                 local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
225                 resample_effect:set_float("zoom_x", zoom_x)
226                 resample_effect:set_float("zoom_y", zoom_y)
227                 resample_effect:set_float("zoom_center_x", 0.0)
228                 resample_effect:set_float("zoom_center_y", 0.0)
229
230                 -- Padding must also be to a whole-pixel offset.
231                 padding_effect:set_int("left", math.floor(x0))
232                 padding_effect:set_int("top", math.floor(y0))
233
234                 -- Correct _that_ discrepancy by subpixel offset in the resampling.
235                 resample_effect:set_float("left", -x_subpixel_offset / zoom_x)
236                 resample_effect:set_float("top", -y_subpixel_offset / zoom_y)
237
238                 -- Finally, adjust the border so it is exactly where we want it.
239                 padding_effect:set_float("border_offset_left", x_subpixel_offset)
240                 padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
241                 padding_effect:set_float("border_offset_top", y_subpixel_offset)
242                 padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
243         else
244                 -- Lower-quality simple resizing.
245                 local width = round(x1 - x0)
246                 local height = round(y1 - y0)
247                 resize_effect:set_int("width", width)
248                 resize_effect:set_int("height", height)
249
250                 -- Padding must also be to a whole-pixel offset.
251                 padding_effect:set_int("left", math.floor(x0))
252                 padding_effect:set_int("top", math.floor(y0))
253         end
254 end
255
256 -- This is broken, of course (even for positive numbers), but Lua doesn't give us access to real rounding.
257 function round(x)
258         return math.floor(x + 0.5)
259 end
260
261 function prepare_sbs_chain(chain, t, screen_width, screen_height)
262         chain.input0.input:connect_signal(live_signal_num)
263         chain.input1.input:connect_signal(1)
264
265         -- First input is positioned (16,48) from top-left.
266         local width0 = round(848 * screen_width/1280.0)
267         local height0 = round(width0 * 9.0 / 16.0)
268
269         local top0 = 48 * screen_height/720.0
270         local left0 = 16 * screen_width/1280.0
271         local bottom0 = top0 + height0
272         local right0 = left0 + width0
273
274         -- Second input is positioned (16,48) from the bottom-right.
275         local width1 = 384 * screen_width/1280.0
276         local height1 = 216 * screen_height/720.0
277
278         local bottom1 = screen_height - 48 * screen_height/720.0
279         local right1 = screen_width - 16 * screen_width/1280.0
280         local top1 = bottom1 - height1
281         local left1 = right1 - width1
282
283         -- Interpolate between the fullscreen and side-by-side views.
284         local scale0 = 1.0 + t * (1280.0 / 848.0 - 1.0)
285         local tx0 = 0.0 + t * (-left0 * scale0)
286         local ty0 = 0.0 + t * (-top0 * scale0)
287
288         top0 = top0 * scale0 + ty0
289         bottom0 = bottom0 * scale0 + ty0
290         left0 = left0 * scale0 + tx0
291         right0 = right0 * scale0 + tx0
292
293         top1 = top1 * scale0 + ty0
294         bottom1 = bottom1 * scale0 + ty0
295         left1 = left1 * scale0 + tx0
296         right1 = right1 * scale0 + tx0
297         place_rectangle(chain.input0.resample_effect, chain.input0.resize_effect, chain.input0.padding_effect, left0, top0, right0, bottom0, screen_width, screen_height)
298         place_rectangle(chain.input1.resample_effect, chain.input1.resize_effect, chain.input1.padding_effect, left1, top1, right1, bottom1, screen_width, screen_height)
299 end