1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: MIT |
3 | |
4 | use std::num::NonZeroU32; |
5 | use std::rc::Rc; |
6 | |
7 | slint::include_modules!(); |
8 | |
9 | use glow::HasContext; |
10 | |
11 | macro_rules! define_scoped_binding { |
12 | (struct $binding_ty_name:ident => $obj_name:path, $param_name:path, $binding_fn:ident, $target_name:path) => { |
13 | struct $binding_ty_name { |
14 | saved_value: Option<$obj_name>, |
15 | gl: Rc<glow::Context>, |
16 | } |
17 | |
18 | impl $binding_ty_name { |
19 | unsafe fn new(gl: &Rc<glow::Context>, new_binding: Option<$obj_name>) -> Self { |
20 | let saved_value = |
21 | NonZeroU32::new(gl.get_parameter_i32($param_name) as u32).map($obj_name); |
22 | |
23 | gl.$binding_fn($target_name, new_binding); |
24 | Self { saved_value, gl: gl.clone() } |
25 | } |
26 | } |
27 | |
28 | impl Drop for $binding_ty_name { |
29 | fn drop(&mut self) { |
30 | unsafe { |
31 | self.gl.$binding_fn($target_name, self.saved_value); |
32 | } |
33 | } |
34 | } |
35 | }; |
36 | (struct $binding_ty_name:ident => $obj_name:path, $param_name:path, $binding_fn:ident) => { |
37 | struct $binding_ty_name { |
38 | saved_value: Option<$obj_name>, |
39 | gl: Rc<glow::Context>, |
40 | } |
41 | |
42 | impl $binding_ty_name { |
43 | unsafe fn new(gl: &Rc<glow::Context>, new_binding: Option<$obj_name>) -> Self { |
44 | let saved_value = |
45 | NonZeroU32::new(gl.get_parameter_i32($param_name) as u32).map($obj_name); |
46 | |
47 | gl.$binding_fn(new_binding); |
48 | Self { saved_value, gl: gl.clone() } |
49 | } |
50 | } |
51 | |
52 | impl Drop for $binding_ty_name { |
53 | fn drop(&mut self) { |
54 | unsafe { |
55 | self.gl.$binding_fn(self.saved_value); |
56 | } |
57 | } |
58 | } |
59 | }; |
60 | } |
61 | |
62 | define_scoped_binding!(struct ScopedTextureBinding => glow::NativeTexture, glow::TEXTURE_BINDING_2D, bind_texture, glow::TEXTURE_2D); |
63 | define_scoped_binding!(struct ScopedFrameBufferBinding => glow::NativeFramebuffer, glow::DRAW_FRAMEBUFFER_BINDING, bind_framebuffer, glow::DRAW_FRAMEBUFFER); |
64 | define_scoped_binding!(struct ScopedVBOBinding => glow::NativeBuffer, glow::ARRAY_BUFFER_BINDING, bind_buffer, glow::ARRAY_BUFFER); |
65 | define_scoped_binding!(struct ScopedVAOBinding => glow::NativeVertexArray, glow::VERTEX_ARRAY_BINDING, bind_vertex_array); |
66 | |
67 | struct DemoTexture { |
68 | texture: glow::Texture, |
69 | width: u32, |
70 | height: u32, |
71 | fbo: glow::Framebuffer, |
72 | gl: Rc<glow::Context>, |
73 | } |
74 | |
75 | impl DemoTexture { |
76 | unsafe fn new(gl: &Rc<glow::Context>, width: u32, height: u32) -> Self { |
77 | let fbo = gl.create_framebuffer().expect("Unable to create framebuffer" ); |
78 | |
79 | let texture = gl.create_texture().expect("Unable to allocate texture" ); |
80 | |
81 | let _saved_texture_binding = ScopedTextureBinding::new(gl, Some(texture)); |
82 | |
83 | let old_unpack_alignment = gl.get_parameter_i32(glow::UNPACK_ALIGNMENT); |
84 | let old_unpack_row_length = gl.get_parameter_i32(glow::UNPACK_ROW_LENGTH); |
85 | let old_unpack_skip_pixels = gl.get_parameter_i32(glow::UNPACK_SKIP_PIXELS); |
86 | let old_unpack_skip_rows = gl.get_parameter_i32(glow::UNPACK_SKIP_ROWS); |
87 | |
88 | gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); |
89 | gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32); |
90 | gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32); |
91 | gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32); |
92 | gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32); |
93 | gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, width as i32); |
94 | gl.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0); |
95 | gl.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0); |
96 | |
97 | gl.tex_image_2d( |
98 | glow::TEXTURE_2D, |
99 | 0, |
100 | glow::RGBA as _, |
101 | width as _, |
102 | height as _, |
103 | 0, |
104 | glow::RGBA as _, |
105 | glow::UNSIGNED_BYTE as _, |
106 | None, |
107 | ); |
108 | |
109 | let _saved_fbo_binding = ScopedFrameBufferBinding::new(gl, Some(fbo)); |
110 | |
111 | gl.framebuffer_texture_2d( |
112 | glow::FRAMEBUFFER, |
113 | glow::COLOR_ATTACHMENT0, |
114 | glow::TEXTURE_2D, |
115 | Some(texture), |
116 | 0, |
117 | ); |
118 | |
119 | debug_assert_eq!( |
120 | gl.check_framebuffer_status(glow::FRAMEBUFFER), |
121 | glow::FRAMEBUFFER_COMPLETE |
122 | ); |
123 | |
124 | gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, old_unpack_alignment); |
125 | gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, old_unpack_row_length); |
126 | gl.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, old_unpack_skip_pixels); |
127 | gl.pixel_store_i32(glow::UNPACK_SKIP_ROWS, old_unpack_skip_rows); |
128 | |
129 | Self { texture, width, height, fbo, gl: gl.clone() } |
130 | } |
131 | |
132 | unsafe fn with_texture_as_active_fbo<R>(&self, callback: impl FnOnce() -> R) -> R { |
133 | let _saved_fbo = ScopedFrameBufferBinding::new(&self.gl, Some(self.fbo)); |
134 | callback() |
135 | } |
136 | } |
137 | |
138 | impl Drop for DemoTexture { |
139 | fn drop(&mut self) { |
140 | unsafe { |
141 | self.gl.delete_framebuffer(self.fbo); |
142 | self.gl.delete_texture(self.texture); |
143 | } |
144 | } |
145 | } |
146 | |
147 | struct DemoRenderer { |
148 | gl: Rc<glow::Context>, |
149 | program: glow::Program, |
150 | vbo: glow::Buffer, |
151 | vao: glow::VertexArray, |
152 | effect_time_location: glow::UniformLocation, |
153 | selected_light_color_position: glow::UniformLocation, |
154 | start_time: web_time::Instant, |
155 | displayed_texture: DemoTexture, |
156 | next_texture: DemoTexture, |
157 | } |
158 | |
159 | impl DemoRenderer { |
160 | fn new(gl: glow::Context) -> Self { |
161 | let gl = Rc::new(gl); |
162 | unsafe { |
163 | let program = gl.create_program().expect("Cannot create program" ); |
164 | |
165 | let (vertex_shader_source, fragment_shader_source) = ( |
166 | r#"#version 100 |
167 | attribute vec2 position; |
168 | varying vec2 frag_position; |
169 | void main() { |
170 | frag_position = position; |
171 | gl_Position = vec4(position, 0.0, 1.0); |
172 | }"# , |
173 | r#"#version 100 |
174 | precision highp float; |
175 | varying vec2 frag_position; |
176 | uniform vec3 selected_light_color; |
177 | uniform float iTime; |
178 | |
179 | float sdRoundBox(vec3 p, vec3 b, float r) |
180 | { |
181 | vec3 q = abs(p) - b; |
182 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r; |
183 | } |
184 | |
185 | vec3 rotateY(vec3 r, float angle) |
186 | { |
187 | mat3 rotation_matrix = mat3(cos(angle), 0, sin(angle), 0, 1, 0, -sin(angle), 0, cos(angle)); |
188 | return rotation_matrix * r; |
189 | } |
190 | |
191 | vec3 rotateZ(vec3 r, float angle) { |
192 | mat3 rotation_matrix = mat3(cos(angle), -sin(angle), 0, sin(angle), cos(angle), 0, 0, 0, 1); |
193 | return rotation_matrix * r; |
194 | } |
195 | |
196 | // Distance from the scene |
197 | float scene(vec3 r) |
198 | { |
199 | vec3 pos = rotateZ(rotateY(r + vec3(-1.0, -1.0, 4.0), iTime), iTime); |
200 | vec3 cube = vec3(0.5, 0.5, 0.5); |
201 | float edge = 0.1; |
202 | return sdRoundBox(pos, cube, edge); |
203 | } |
204 | |
205 | // https://iquilezles.org/articles/normalsSDF |
206 | vec3 normal( in vec3 pos ) |
207 | { |
208 | vec2 e = vec2(1.0,-1.0)*0.5773; |
209 | const float eps = 0.0005; |
210 | return normalize( e.xyy*scene( pos + e.xyy*eps ) + |
211 | e.yyx*scene( pos + e.yyx*eps ) + |
212 | e.yxy*scene( pos + e.yxy*eps ) + |
213 | e.xxx*scene( pos + e.xxx*eps ) ); |
214 | } |
215 | |
216 | #define ITERATIONS 90 |
217 | #define EPS 0.0001 |
218 | |
219 | vec4 render(vec2 fragCoord, vec3 light_color) |
220 | { |
221 | vec4 color = vec4(0, 0, 0, 1); |
222 | |
223 | vec3 camera = vec3(1.0, 2.0, 1.0); |
224 | vec3 p = vec3(fragCoord.x, fragCoord.y + 1.0, -1.0); |
225 | vec3 dir = normalize(p - camera); |
226 | |
227 | for(int i=0; i < ITERATIONS; i++) |
228 | { |
229 | float dist = scene(p); |
230 | if(dist < EPS) { |
231 | break; |
232 | } |
233 | p = p + dir * dist; |
234 | } |
235 | |
236 | vec3 surf_normal = normal(p); |
237 | |
238 | vec3 light_position = vec3(2.0, 4.0, -0.5); |
239 | float light = 7.0 + 2.0 * dot(surf_normal, light_position); |
240 | light /= 0.2 * pow(length(light_position - p), 3.5); |
241 | |
242 | return vec4(light * light_color.x, light * light_color.y, light * light_color.z, 1.0) * 2.0; |
243 | } |
244 | |
245 | /* |
246 | void mainImage(out vec4 fragColor, in vec2 fragCoord) |
247 | { |
248 | vec2 r = fragCoord.xy / iResolution.xy; |
249 | r.x *= (iResolution.x / iResolution.y); |
250 | fragColor = render(r, vec3(0.2, 0.5, 0.9)); |
251 | } |
252 | */ |
253 | |
254 | void main() { |
255 | vec2 r = vec2(0.5 * frag_position.x + 1.0, 0.5 - 0.5 * frag_position.y); |
256 | gl_FragColor = render(r, selected_light_color); |
257 | }"# , |
258 | ); |
259 | |
260 | let shader_sources = [ |
261 | (glow::VERTEX_SHADER, vertex_shader_source), |
262 | (glow::FRAGMENT_SHADER, fragment_shader_source), |
263 | ]; |
264 | |
265 | let mut shaders = Vec::with_capacity(shader_sources.len()); |
266 | |
267 | for (shader_type, shader_source) in shader_sources.iter() { |
268 | let shader = gl.create_shader(*shader_type).expect("Cannot create shader" ); |
269 | gl.shader_source(shader, shader_source); |
270 | gl.compile_shader(shader); |
271 | if !gl.get_shader_compile_status(shader) { |
272 | panic!("{}" , gl.get_shader_info_log(shader)); |
273 | } |
274 | gl.attach_shader(program, shader); |
275 | shaders.push(shader); |
276 | } |
277 | |
278 | gl.link_program(program); |
279 | if !gl.get_program_link_status(program) { |
280 | panic!("{}" , gl.get_program_info_log(program)); |
281 | } |
282 | |
283 | for shader in shaders { |
284 | gl.detach_shader(program, shader); |
285 | gl.delete_shader(shader); |
286 | } |
287 | |
288 | let effect_time_location = gl.get_uniform_location(program, "iTime" ).unwrap(); |
289 | let selected_light_color_position = |
290 | gl.get_uniform_location(program, "selected_light_color" ).unwrap(); |
291 | let position_location = gl.get_attrib_location(program, "position" ).unwrap(); |
292 | |
293 | let vbo = gl.create_buffer().expect("Cannot create buffer" ); |
294 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); |
295 | |
296 | let vertices = [-1.0f32, 1.0f32, -1.0f32, -1.0f32, 1.0f32, 1.0f32, 1.0f32, -1.0f32]; |
297 | |
298 | gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, vertices.align_to().1, glow::STATIC_DRAW); |
299 | |
300 | let vao = gl.create_vertex_array().expect("Cannot create vertex array" ); |
301 | gl.bind_vertex_array(Some(vao)); |
302 | gl.enable_vertex_attrib_array(position_location); |
303 | gl.vertex_attrib_pointer_f32(position_location, 2, glow::FLOAT, false, 8, 0); |
304 | |
305 | gl.bind_buffer(glow::ARRAY_BUFFER, None); |
306 | gl.bind_vertex_array(None); |
307 | |
308 | let displayed_texture = DemoTexture::new(&gl, 320, 200); |
309 | let next_texture = DemoTexture::new(&gl, 320, 200); |
310 | |
311 | Self { |
312 | gl, |
313 | program, |
314 | effect_time_location, |
315 | selected_light_color_position, |
316 | vbo, |
317 | vao, |
318 | start_time: web_time::Instant::now(), |
319 | displayed_texture, |
320 | next_texture, |
321 | } |
322 | } |
323 | } |
324 | } |
325 | |
326 | impl Drop for DemoRenderer { |
327 | fn drop(&mut self) { |
328 | unsafe { |
329 | self.gl.delete_program(self.program); |
330 | self.gl.delete_vertex_array(self.vao); |
331 | self.gl.delete_buffer(self.vbo); |
332 | } |
333 | } |
334 | } |
335 | |
336 | impl DemoRenderer { |
337 | fn render( |
338 | &mut self, |
339 | light_red: f32, |
340 | light_green: f32, |
341 | light_blue: f32, |
342 | width: u32, |
343 | height: u32, |
344 | ) -> slint::Image { |
345 | unsafe { |
346 | let gl = &self.gl; |
347 | |
348 | gl.use_program(Some(self.program)); |
349 | |
350 | let _saved_vbo = ScopedVBOBinding::new(gl, Some(self.vbo)); |
351 | let _saved_vao = ScopedVAOBinding::new(gl, Some(self.vao)); |
352 | |
353 | if self.next_texture.width != width || self.next_texture.height != height { |
354 | let mut new_texture = DemoTexture::new(gl, width, height); |
355 | std::mem::swap(&mut self.next_texture, &mut new_texture); |
356 | } |
357 | |
358 | self.next_texture.with_texture_as_active_fbo(|| { |
359 | let mut saved_viewport: [i32; 4] = [0, 0, 0, 0]; |
360 | gl.get_parameter_i32_slice(glow::VIEWPORT, &mut saved_viewport); |
361 | |
362 | gl.viewport(0, 0, self.next_texture.width as _, self.next_texture.height as _); |
363 | let elapsed = self.start_time.elapsed().as_millis() as f32 / 500.; |
364 | gl.uniform_1_f32(Some(&self.effect_time_location), elapsed); |
365 | |
366 | gl.uniform_3_f32( |
367 | Some(&self.selected_light_color_position), |
368 | light_red, |
369 | light_green, |
370 | light_blue, |
371 | ); |
372 | |
373 | gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4); |
374 | |
375 | gl.viewport( |
376 | saved_viewport[0], |
377 | saved_viewport[1], |
378 | saved_viewport[2], |
379 | saved_viewport[3], |
380 | ); |
381 | }); |
382 | |
383 | gl.use_program(None); |
384 | } |
385 | |
386 | let result_texture = unsafe { |
387 | slint::BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture( |
388 | self.next_texture.texture.0, |
389 | (self.next_texture.width, self.next_texture.height).into(), |
390 | ) |
391 | .build() |
392 | }; |
393 | |
394 | std::mem::swap(&mut self.next_texture, &mut self.displayed_texture); |
395 | |
396 | result_texture |
397 | } |
398 | } |
399 | |
400 | fn main() { |
401 | let app = App::new().unwrap(); |
402 | |
403 | let mut underlay = None; |
404 | |
405 | let app_weak = app.as_weak(); |
406 | |
407 | if let Err(error) = app.window().set_rendering_notifier(move |state, graphics_api| { |
408 | // eprintln!("rendering state {:#?}", state); |
409 | |
410 | match state { |
411 | slint::RenderingState::RenderingSetup => { |
412 | let context = match graphics_api { |
413 | slint::GraphicsAPI::NativeOpenGL { get_proc_address } => unsafe { |
414 | glow::Context::from_loader_function_cstr(|s| get_proc_address(s)) |
415 | }, |
416 | _ => return, |
417 | }; |
418 | underlay = Some(DemoRenderer::new(context)) |
419 | } |
420 | slint::RenderingState::BeforeRendering => { |
421 | if let (Some(underlay), Some(app)) = (underlay.as_mut(), app_weak.upgrade()) { |
422 | let texture = underlay.render( |
423 | app.get_selected_red(), |
424 | app.get_selected_green(), |
425 | app.get_selected_blue(), |
426 | app.get_requested_texture_width() as u32, |
427 | app.get_requested_texture_height() as u32, |
428 | ); |
429 | app.set_texture(slint::Image::from(texture)); |
430 | app.window().request_redraw(); |
431 | } |
432 | } |
433 | slint::RenderingState::AfterRendering => {} |
434 | slint::RenderingState::RenderingTeardown => { |
435 | drop(underlay.take()); |
436 | } |
437 | _ => {} |
438 | } |
439 | }) { |
440 | match error { |
441 | slint::SetRenderingNotifierError::Unsupported => eprintln!("This example requires the use of the GL backend. Please run with the environment variable SLINT_BACKEND=GL set." ), |
442 | _ => unreachable!() |
443 | } |
444 | std::process::exit(1); |
445 | } |
446 | |
447 | app.run().unwrap(); |
448 | } |
449 | |