]> git.sesse.net Git - movit/blob - resample_effect.cpp
Factorize the code to compute sampling points for bilinear sampling into a shared...
[movit] / resample_effect.cpp
1 // Three-lobed Lanczos, the most common choice.
2 #define LANCZOS_RADIUS 3.0
3
4 #include <math.h>
5 #include <assert.h>
6
7 #include "resample_effect.h"
8 #include "effect_chain.h"
9 #include "util.h"
10 #include "opengl.h"
11
12 namespace {
13
14 float sinc(float x)
15 {
16         if (fabs(x) < 1e-6) {
17                 return 1.0f - fabs(x);
18         } else {
19                 return sin(x) / x;
20         }
21 }
22
23 float lanczos_weight(float x, float a)
24 {
25         if (fabs(x) > a) {
26                 return 0.0f;
27         } else {
28                 return sinc(M_PI * x) * sinc(M_PI * x / a);
29         }
30 }
31
32 // Euclid's algorithm, from Wikipedia.
33 unsigned gcd(unsigned a, unsigned b)
34 {
35         while (b != 0) {
36                 unsigned t = b;
37                 b = a % b;
38                 a = t;
39         }
40         return a;
41 }
42
43 }  // namespace
44
45 ResampleEffect::ResampleEffect()
46         : input_width(1280),
47           input_height(720)
48 {
49         register_int("width", &output_width);
50         register_int("height", &output_height);
51
52         // The first blur pass will forward resolution information to us.
53         hpass = new SingleResamplePassEffect(this);
54         hpass->set_int("direction", SingleResamplePassEffect::HORIZONTAL);
55         vpass = new SingleResamplePassEffect(NULL);
56         vpass->set_int("direction", SingleResamplePassEffect::VERTICAL);
57
58         update_size();
59 }
60
61 void ResampleEffect::rewrite_graph(EffectChain *graph, Node *self)
62 {
63         Node *hpass_node = graph->add_node(hpass);
64         Node *vpass_node = graph->add_node(vpass);
65         graph->connect_nodes(hpass_node, vpass_node);
66         graph->replace_receiver(self, hpass_node);
67         graph->replace_sender(self, vpass_node);
68         self->disabled = true;
69
70
71 // We get this information forwarded from the first blur pass,
72 // since we are not part of the chain ourselves.
73 void ResampleEffect::inform_input_size(unsigned input_num, unsigned width, unsigned height)
74 {
75         assert(input_num == 0);
76         assert(width != 0);
77         assert(height != 0);
78         input_width = width;
79         input_height = height;
80         update_size();
81 }
82                 
83 void ResampleEffect::update_size()
84 {
85         bool ok = true;
86         ok |= hpass->set_int("input_width", input_width);
87         ok |= hpass->set_int("input_height", input_height);
88         ok |= hpass->set_int("output_width", output_width);
89         ok |= hpass->set_int("output_height", input_height);
90
91         ok |= vpass->set_int("input_width", output_width);
92         ok |= vpass->set_int("input_height", input_height);
93         ok |= vpass->set_int("output_width", output_width);
94         ok |= vpass->set_int("output_height", output_height);
95
96         assert(ok);
97 }
98
99 bool ResampleEffect::set_float(const std::string &key, float value) {
100         if (key == "width") {
101                 output_width = value;
102                 update_size();
103                 return true;
104         }
105         if (key == "height") {
106                 output_height = value;
107                 update_size();
108                 return true;
109         }
110         return false;
111 }
112
113 SingleResamplePassEffect::SingleResamplePassEffect(ResampleEffect *parent)
114         : parent(parent),
115           direction(HORIZONTAL),
116           input_width(1280),
117           input_height(720),
118           last_input_width(-1),
119           last_input_height(-1),
120           last_output_width(-1),
121           last_output_height(-1)
122 {
123         register_int("direction", (int *)&direction);
124         register_int("input_width", &input_width);
125         register_int("input_height", &input_height);
126         register_int("output_width", &output_width);
127         register_int("output_height", &output_height);
128
129         glGenTextures(1, &texnum);
130 }
131
132 SingleResamplePassEffect::~SingleResamplePassEffect()
133 {
134         glDeleteTextures(1, &texnum);
135 }
136
137 std::string SingleResamplePassEffect::output_fragment_shader()
138 {
139         char buf[256];
140         sprintf(buf, "#define DIRECTION_VERTICAL %d\n", (direction == VERTICAL));
141         return buf + read_file("resample_effect.frag");
142 }
143
144 // Using vertical scaling as an example:
145 //
146 // Generally out[y] = w0 * in[yi] + w1 * in[yi + 1] + w2 * in[yi + 2] + ...
147 //
148 // Obviously, yi will depend on y (in a not-quite-linear way), but so will
149 // the weights w0, w1, w2, etc.. The easiest way of doing this is to encode,
150 // for each sample, the weight and the yi value, e.g. <yi, w0>, <yi + 1, w1>,
151 // and so on. For each y, we encode these along the x-axis (since that is spare),
152 // so out[0] will read from parameters <x,y> = <0,0>, <1,0>, <2,0> and so on.
153 //
154 // For horizontal scaling, we fill in the exact same texture;
155 // the shader just interprets is differently.
156 //
157 // TODO: Support optimization using free linear sampling, like in BlurEffect.
158 void SingleResamplePassEffect::update_texture(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num)
159 {
160         unsigned src_size, dst_size;
161         if (direction == SingleResamplePassEffect::HORIZONTAL) {
162                 assert(input_height == output_height);
163                 src_size = input_width;
164                 dst_size = output_width;
165         } else if (direction == SingleResamplePassEffect::VERTICAL) {
166                 assert(input_width == output_width);
167                 src_size = input_height;
168                 dst_size = output_height;
169         } else {
170                 assert(false);
171         }
172
173
174         // For many resamplings (e.g. 640 -> 1280), we will end up with the same
175         // set of samples over and over again in a loop. Thus, we can compute only
176         // the first such loop, and then ask the card to repeat the texture for us.
177         // This is both easier on the texture cache and lowers our CPU cost for
178         // generating the kernel somewhat.
179         num_loops = gcd(src_size, dst_size);
180         slice_height = 1.0f / num_loops;
181         unsigned dst_samples = dst_size / num_loops;
182
183         // Sample the kernel in the right place. A diagram with a triangular kernel
184         // (corresponding to linear filtering, and obviously with radius 1)
185         // for easier ASCII art drawing:
186         //
187         //                *
188         //               / \                      |
189         //              /   \                     |
190         //             /     \                    |
191         //    x---x---x   x   x---x---x---x
192         //
193         // Scaling up (in this case, 2x) means sampling more densely:
194         //
195         //                *
196         //               / \                      |
197         //              /   \                     |
198         //             /     \                    |
199         //   x-x-x-x-x-x x x x-x-x-x-x-x-x-x
200         //
201         // When scaling up, any destination pixel will only be influenced by a few
202         // (in this case, two) neighboring pixels, and more importantly, the number
203         // will not be influenced by the scaling factor. (Note, however, that the
204         // pixel centers have moved, due to OpenGL's center-pixel convention.)
205         // The only thing that changes is the weights themselves, as the sampling
206         // points are at different distances from the original pixels.
207         //
208         // Scaling down is a different story:
209         //
210         //                *
211         //               / \                      |
212         //              /   \                     |
213         //             /     \                    |
214         //    --x------ x     --x-------x--
215         //
216         // Again, the pixel centers have moved in a maybe unintuitive fashion,
217         // although when you consider that there are multiple source pixels around,
218         // it's not so bad as at first look:
219         //
220         //            *   *   *   *
221         //           / \ / \ / \ / \              |
222         //          /   X   X   X   \             |
223         //         /   / \ / \ / \   \            |
224         //    --x-------x-------x-------x--
225         //
226         // As you can see, the new pixels become averages of the two neighboring old
227         // ones (the situation for Lanczos is of course more complex).
228         //
229         // Anyhow, in this case we clearly need to look at more source pixels
230         // to compute the destination pixel, and how many depend on the scaling factor.
231         // Thus, the kernel width will vary with how much we scale.
232         float radius_scaling_factor = std::min(float(dst_size) / float(src_size), 1.0f);
233         int int_radius = lrintf(LANCZOS_RADIUS / radius_scaling_factor);
234         src_samples = int_radius * 2 + 1;
235         float *weights = new float[dst_samples * src_samples * 2];
236         for (unsigned y = 0; y < dst_samples; ++y) {
237                 // Find the point around which we want to sample the source image,
238                 // compensating for differing pixel centers as the scale changes.
239                 float center_src_y = (y + 0.5f) * float(src_size) / float(dst_size) - 0.5f;
240                 int base_src_y = lrintf(center_src_y);
241
242                 // Now sample <int_radius> pixels on each side around that point.
243                 for (int i = 0; i < src_samples; ++i) {
244                         int src_y = base_src_y + i - int_radius;
245                         float weight = lanczos_weight(radius_scaling_factor * (src_y - center_src_y), LANCZOS_RADIUS);
246                         weights[(y * src_samples + i) * 2 + 0] = weight * radius_scaling_factor;
247                         weights[(y * src_samples + i) * 2 + 1] = (src_y + 0.5) / float(src_size);
248                 }
249         }
250
251         // Encode as a two-component texture. Note the GL_REPEAT.
252         glActiveTexture(GL_TEXTURE0 + *sampler_num);
253         check_error();
254         glBindTexture(GL_TEXTURE_2D, texnum);
255         check_error();
256         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
257         check_error();
258         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
259         check_error();
260         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
261         check_error();
262         glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, src_samples, dst_samples, 0, GL_RG, GL_FLOAT, weights);
263         check_error();
264
265         delete[] weights;
266 }
267
268 void SingleResamplePassEffect::set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num)
269 {
270         Effect::set_gl_state(glsl_program_num, prefix, sampler_num);
271
272         if (input_width != last_input_width ||
273             input_height != last_input_height ||
274             output_width != last_output_width ||
275             output_height != last_output_height) {
276                 update_texture(glsl_program_num, prefix, sampler_num);
277                 last_input_width = input_width;
278                 last_input_height = input_height;
279                 last_output_width = output_width;
280                 last_output_height = output_height;
281         }
282
283         glActiveTexture(GL_TEXTURE0 + *sampler_num);
284         check_error();
285         glBindTexture(GL_TEXTURE_2D, texnum);
286         check_error();
287
288         set_uniform_int(glsl_program_num, prefix, "sample_tex", *sampler_num);
289         ++sampler_num;
290         set_uniform_int(glsl_program_num, prefix, "num_samples", src_samples);
291         set_uniform_float(glsl_program_num, prefix, "num_loops", num_loops);
292         set_uniform_float(glsl_program_num, prefix, "slice_height", slice_height);
293
294         // Instructions for how to convert integer sample numbers to positions in the weight texture.
295         set_uniform_float(glsl_program_num, prefix, "sample_x_scale", 1.0f / src_samples);
296         set_uniform_float(glsl_program_num, prefix, "sample_x_offset", 0.5f / src_samples);
297
298         // We specifically do not want mipmaps on the input texture;
299         // they break minification.
300         glActiveTexture(GL_TEXTURE0);
301         check_error();
302         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
303         check_error();
304 }