| 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 | glow::PixelUnpackData::Slice(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 | slint::BackendSelector::new() |
| 402 | .require_opengl_es() |
| 403 | .select() |
| 404 | .expect("Unable to create Slint backend with OpenGL ES renderer" ); |
| 405 | |
| 406 | let app = App::new().unwrap(); |
| 407 | |
| 408 | let mut underlay = None; |
| 409 | |
| 410 | let app_weak = app.as_weak(); |
| 411 | |
| 412 | app.window() |
| 413 | .set_rendering_notifier(move |state, graphics_api| { |
| 414 | // eprintln!("rendering state {:#?}", state); |
| 415 | |
| 416 | match state { |
| 417 | slint::RenderingState::RenderingSetup => { |
| 418 | let context = match graphics_api { |
| 419 | slint::GraphicsAPI::NativeOpenGL { get_proc_address } => unsafe { |
| 420 | glow::Context::from_loader_function_cstr(|s| get_proc_address(s)) |
| 421 | }, |
| 422 | _ => return, |
| 423 | }; |
| 424 | underlay = Some(DemoRenderer::new(context)) |
| 425 | } |
| 426 | slint::RenderingState::BeforeRendering => { |
| 427 | if let (Some(underlay), Some(app)) = (underlay.as_mut(), app_weak.upgrade()) { |
| 428 | let texture = underlay.render( |
| 429 | app.get_selected_red(), |
| 430 | app.get_selected_green(), |
| 431 | app.get_selected_blue(), |
| 432 | app.get_requested_texture_width() as u32, |
| 433 | app.get_requested_texture_height() as u32, |
| 434 | ); |
| 435 | app.set_texture(slint::Image::from(texture)); |
| 436 | app.window().request_redraw(); |
| 437 | } |
| 438 | } |
| 439 | slint::RenderingState::AfterRendering => {} |
| 440 | slint::RenderingState::RenderingTeardown => { |
| 441 | drop(underlay.take()); |
| 442 | } |
| 443 | _ => {} |
| 444 | } |
| 445 | }) |
| 446 | .expect("Unable to set rendering notifier" ); |
| 447 | |
| 448 | app.run().unwrap(); |
| 449 | } |
| 450 | |