فهرست منبع

app/gfx: hardcode a CRT startup fade in special effect for cooolness

darkfi 2 روز پیش
والد
کامیت
6d024a83f6

+ 187 - 3
bin/app/src/gfx/mod.rs

@@ -24,8 +24,8 @@ use darkfi_serial::{
 use miniquad::native::egl;
 use miniquad::{
     conf, window, Bindings, BufferSource, BufferType, BufferUsage, EventHandler, KeyCode, KeyMods,
-    MouseButton, PassAction, Pipeline, RenderingBackend, TextureFormat, TextureKind, TextureParams,
-    TextureWrap, TouchPhase, UniformType,
+    MouseButton, PassAction, Pipeline, RenderPass, RenderingBackend, TextureFormat, TextureKind,
+    TextureParams, TextureWrap, TouchPhase, UniformType,
 };
 use std::{
     collections::HashMap,
@@ -588,6 +588,27 @@ impl<'a> RenderContext<'a> {
 
 type DcId = u64;
 
+/// CRT post-processing parameters as set at app start. They are faded to
+/// zero (brightness to 1.0) over CRT_FADE_SECS and the pass is then dropped.
+#[derive(Clone, Copy, Debug)]
+struct CrtParams {
+    chromatic_aberration: f32,
+    blur_amount: f32,
+    blur_radius: f32,
+    glow_intensity: f32,
+    brightness: f32,
+}
+
+const CRT_START: CrtParams = CrtParams {
+    chromatic_aberration: 0.008,
+    blur_amount: 4.,
+    blur_radius: 8.,
+    glow_intensity: 10.,
+    brightness: 4.08,
+};
+
+const CRT_FADE_SECS: f32 = 1.;
+
 struct Stage {
     ctx: Box<dyn RenderingBackend>,
     #[cfg(target_os = "android")]
@@ -596,6 +617,15 @@ struct Stage {
     white_texture: miniquad::TextureId,
     draw_calls: HashMap<DcId, GfxDrawCall>,
 
+    /// CRT post-processing
+    crt_pipeline: Pipeline,
+    crt_pass: Option<RenderPass>,
+    crt_texture: Option<miniquad::TextureId>,
+    crt_vertex_buffer: Option<miniquad::BufferId>,
+    crt_index_buffer: Option<miniquad::BufferId>,
+    crt_size: (u32, u32),
+    crt_start: Option<std::time::Instant>,
+
     textures: Box<HashMap<TextureId, miniquad::TextureId>>,
     buffers: Box<HashMap<BufferId, miniquad::BufferId>>,
     anims: Box<HashMap<AnimId, GfxSeqAnim>>,
@@ -632,6 +662,7 @@ impl Stage {
 
         let rgb_pipeline = shader::create_rgb_pipeline(&mut ctx);
         let yuv_pipeline = shader::create_yuv_pipeline(&mut ctx);
+        let crt_pipeline = shader::create_crt_pipeline(&mut ctx);
 
         #[cfg(target_os = "android")]
         let libegl = egl::LibEgl::try_load().expect("Cant load LibEGL");
@@ -642,6 +673,13 @@ impl Stage {
             libegl,
             loaded_pipelines: [rgb_pipeline, yuv_pipeline],
             white_texture,
+            crt_pipeline,
+            crt_pass: None,
+            crt_texture: None,
+            crt_vertex_buffer: None,
+            crt_index_buffer: None,
+            crt_size: (0, 0),
+            crt_start: None,
             draw_calls: HashMap::from([(
                 0,
                 GfxDrawCall { instrs: vec![], dcs: vec![], z_index: 0 },
@@ -908,6 +946,129 @@ impl Stage {
             crate::android::request_apply_insets();
         }
     }
+
+    /// Release the offscreen render target and quad of the CRT pass
+    fn free_crt_resources(&mut self) {
+        if let Some(pass) = self.crt_pass.take() {
+            self.ctx.delete_render_pass(pass);
+        }
+        if let Some(texture) = self.crt_texture.take() {
+            self.ctx.delete_texture(texture);
+        }
+        if let Some(buffer) = self.crt_vertex_buffer.take() {
+            self.ctx.delete_buffer(buffer);
+        }
+        if let Some(buffer) = self.crt_index_buffer.take() {
+            self.ctx.delete_buffer(buffer);
+        }
+        self.crt_size = (0, 0);
+    }
+
+    /// Current CRT parameters, interpolated from CRT_START towards zero with
+    /// smoothstep easing. None once the fade has finished.
+    fn crt_current_params(&mut self) -> Option<CrtParams> {
+        let start = *self.crt_start.get_or_insert_with(std::time::Instant::now);
+        let t = (start.elapsed().as_secs_f32() / CRT_FADE_SECS).clamp(0., 1.);
+        if t >= 1. {
+            return None
+        }
+        let s = 1. - t * t * (3. - 2. * t);
+        Some(CrtParams {
+            chromatic_aberration: CRT_START.chromatic_aberration * s,
+            blur_amount: CRT_START.blur_amount * s,
+            blur_radius: CRT_START.blur_radius * s,
+            glow_intensity: CRT_START.glow_intensity * s,
+            brightness: 1. + (CRT_START.brightness - 1.) * s,
+        })
+    }
+
+    /// (Re)create the offscreen render target and fullscreen quad used by
+    /// the CRT post-processing pass. Recreates on window resize.
+    fn ensure_crt_pass(&mut self) {
+        let (w, h) = window::screen_size();
+        let w = (w.round() as u32).max(1);
+        let h = (h.round() as u32).max(1);
+
+        if self.crt_size == (w, h) && self.crt_pass.is_some() {
+            return
+        }
+
+        self.free_crt_resources();
+
+        let texture = self.ctx.new_render_texture(TextureParams {
+            width: w,
+            height: h,
+            format: TextureFormat::RGBA8,
+            wrap: TextureWrap::Clamp,
+            min_filter: miniquad::FilterMode::Linear,
+            mag_filter: miniquad::FilterMode::Linear,
+            ..Default::default()
+        });
+        let pass = self.ctx.new_render_pass(texture, None);
+
+        // Screen resolution is passed to the shader via vertex colors.
+        // GL render targets store the first row at v=0 (bottom) so flip v,
+        // Metal stores it at v=0 (top) so keep it as-is.
+        let is_gl = self.ctx.info().backend == miniquad::Backend::OpenGl;
+        let (v_top, v_bottom) = if is_gl { (1., 0.) } else { (0., 1.) };
+        let verts = [
+            Vertex { pos: [0., 0.], color: [w as f32, h as f32, 0., 1.], uv: [0., v_top] },
+            Vertex { pos: [1., 0.], color: [w as f32, h as f32, 0., 1.], uv: [1., v_top] },
+            Vertex { pos: [1., 1.], color: [w as f32, h as f32, 0., 1.], uv: [1., v_bottom] },
+            Vertex { pos: [0., 1.], color: [w as f32, h as f32, 0., 1.], uv: [0., v_bottom] },
+        ];
+        let vertex_buffer = self.ctx.new_buffer(
+            BufferType::VertexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&verts),
+        );
+
+        let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
+        let index_buffer = self.ctx.new_buffer(
+            BufferType::IndexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&indices),
+        );
+
+        self.crt_texture = Some(texture);
+        self.crt_pass = Some(pass);
+        self.crt_vertex_buffer = Some(vertex_buffer);
+        self.crt_index_buffer = Some(index_buffer);
+        self.crt_size = (w, h);
+    }
+
+    /// Blit the offscreen scene to the screen through the CRT shader
+    fn draw_crt(&mut self, params: CrtParams) {
+        self.ctx.begin_default_pass(PassAction::clear_color(0., 0., 0., 1.));
+        self.ctx.apply_pipeline(&self.crt_pipeline);
+
+        // Same top-left (0, 0) bottom-right (1, 1) projection, identity model
+        let proj = glam::Mat4::from_translation(glam::Vec3::new(-1., 1., 0.)) *
+            glam::Mat4::from_scale(glam::Vec3::new(2., -2., 1.));
+        let model = glam::Mat4::IDENTITY;
+
+        // Two mat4s followed by the five CRT parameter floats
+        let mut uniforms_data = [0u8; 148];
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&proj) };
+        uniforms_data[0..64].copy_from_slice(&data);
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&model) };
+        uniforms_data[64..128].copy_from_slice(&data);
+        uniforms_data[128..132].copy_from_slice(&params.chromatic_aberration.to_ne_bytes());
+        uniforms_data[132..136].copy_from_slice(&params.blur_amount.to_ne_bytes());
+        uniforms_data[136..140].copy_from_slice(&params.blur_radius.to_ne_bytes());
+        uniforms_data[140..144].copy_from_slice(&params.glow_intensity.to_ne_bytes());
+        uniforms_data[144..148].copy_from_slice(&params.brightness.to_ne_bytes());
+        self.ctx.apply_uniforms_from_bytes(uniforms_data.as_ptr(), uniforms_data.len());
+
+        let bindings = Bindings {
+            vertex_buffers: vec![self.crt_vertex_buffer.unwrap()],
+            index_buffer: self.crt_index_buffer.unwrap(),
+            images: vec![self.crt_texture.unwrap()],
+        };
+        self.ctx.apply_bindings(&bindings);
+        self.ctx.draw(0, 6, 1);
+        self.ctx.end_render_pass();
+    }
 }
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -998,7 +1159,24 @@ impl EventHandler for Stage {
     }
 
     fn draw(&mut self) {
-        self.ctx.begin_default_pass(PassAction::clear_color(0., 0., 0., 1.));
+        let crt_params = self.crt_current_params();
+
+        // Keep the offscreen pass alive only while the effect is fading in
+        // strength; drop it once the parameters have reached zero
+        if crt_params.is_some() {
+            self.ensure_crt_pass();
+        } else if self.crt_pass.is_some() {
+            self.free_crt_resources();
+        }
+        let crt_pass = self.crt_pass;
+
+        // Render the scene into the offscreen CRT render target when available,
+        // otherwise straight to the default framebuffer
+        if let Some(pass) = crt_pass {
+            self.ctx.begin_pass(Some(pass), PassAction::clear_color(0., 0., 0., 1.));
+        } else {
+            self.ctx.begin_default_pass(PassAction::clear_color(0., 0., 0., 1.));
+        }
 
         // Apply default RGB pipeline
         self.ctx.apply_pipeline(&self.loaded_pipelines[GraphicPipeline::RGB as usize]);
@@ -1038,6 +1216,12 @@ impl EventHandler for Stage {
         render_ctx.draw();
 
         self.ctx.end_render_pass();
+
+        // Post-process the offscreen scene through the CRT shader
+        if let (Some(_), Some(params)) = (crt_pass, crt_params) {
+            self.draw_crt(params);
+        }
+
         self.ctx.commit_frame();
     }
 

+ 59 - 0
bin/app/src/gfx/shader/gl_fragment_crt.frag

@@ -0,0 +1,59 @@
+#version 100
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+
+varying vec2 uv;
+varying vec2 resolution;
+
+uniform sampler2D tex;
+
+// CRT effect parameters, driven from Rust
+uniform float chromatic_aberration;
+uniform float blur_amount;
+uniform float blur_radius;
+uniform float glow_intensity;
+uniform float brightness;
+
+const float GLOW_THRESHOLD = 0.55;
+
+// Sample with chromatic aberration: RGB channels split towards the edges
+vec3 sample_scene(vec2 coord) {
+    vec2 dir = coord - 0.5;
+    float r = texture2D(tex, coord + dir * chromatic_aberration).r;
+    float g = texture2D(tex, coord).g;
+    float b = texture2D(tex, coord - dir * chromatic_aberration).b;
+    return vec3(r, g, b);
+}
+
+// Soft blur plus extra glow on highlights, sharing the same taps
+vec3 blur_sample(vec2 coord, vec2 texel, out vec3 highlights) {
+    vec3 sum = texture2D(tex, coord).rgb * 4.0;
+    vec3 bright = max(texture2D(tex, coord).rgb - GLOW_THRESHOLD, 0.0) * 4.0;
+    for (int i = 0; i < 8; i++) {
+        float a = float(i) * 0.78539816;
+        vec2 d = vec2(cos(a), sin(a));
+        vec3 inner = texture2D(tex, coord + d * texel * blur_radius).rgb;
+        vec3 outer = texture2D(tex, coord + d * texel * blur_radius * 2.4).rgb;
+        sum += inner + outer;
+        bright += max(inner - GLOW_THRESHOLD, 0.0);
+        bright += max(outer - GLOW_THRESHOLD, 0.0);
+    }
+    highlights = bright * (glow_intensity / 20.0);
+    return sum / 20.0;
+}
+
+void main() {
+    vec2 coord = uv;
+
+    vec2 texel = 1.0 / resolution;
+
+    vec3 color = sample_scene(coord);
+    vec3 highlights;
+    vec3 blurred = blur_sample(coord, texel, highlights);
+    color = mix(color, blurred, blur_amount) + highlights;
+
+    gl_FragColor = vec4(color * brightness, 1.0);
+}

+ 16 - 0
bin/app/src/gfx/shader/gl_vertex_crt.vert

@@ -0,0 +1,16 @@
+#version 100
+attribute vec2 in_pos;
+attribute vec4 in_color;
+attribute vec2 in_uv;
+
+varying vec2 uv;
+varying vec2 resolution;
+
+uniform mat4 Projection;
+uniform mat4 Model;
+
+void main() {
+    gl_Position = Projection * Model * vec4(in_pos, 0, 1);
+    uv = in_uv;
+    resolution = in_color.xy;
+}

+ 99 - 0
bin/app/src/gfx/shader/metal_crt.metal

@@ -0,0 +1,99 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+struct Uniforms
+{
+    float4x4 Projection;
+    float4x4 Model;
+    float chromatic_aberration;
+    float blur_amount;
+    float blur_radius;
+    float glow_intensity;
+    float brightness;
+};
+
+struct Vertex
+{
+    float2 in_pos   [[attribute(0)]];
+    float4 in_color [[attribute(1)]];
+    float2 in_uv    [[attribute(2)]];
+};
+
+struct RasterizerData
+{
+    float4 position [[position]];
+    float2 uv [[user(locn0)]];
+    float2 resolution [[user(locn1)]];
+};
+
+vertex RasterizerData vertexShader(Vertex v [[stage_in]], constant Uniforms& uniforms [[buffer(0)]])
+{
+    RasterizerData out;
+
+    out.position = uniforms.Projection * uniforms.Model * float4(v.in_pos, 0.0, 1.0);
+    out.uv = v.in_uv;
+    out.resolution = v.in_color.xy;
+
+    return out;
+}
+
+constant float GLOW_THRESHOLD = 0.55;
+
+float3 sample_scene(texture2d<float> tex, sampler texSmplr, float2 coord, float chromatic_aberration)
+{
+    float2 dir = coord - 0.5;
+    float r = tex.sample(texSmplr, coord + dir * chromatic_aberration).r;
+    float g = tex.sample(texSmplr, coord).g;
+    float b = tex.sample(texSmplr, coord - dir * chromatic_aberration).b;
+    return float3(r, g, b);
+}
+
+// Soft blur plus extra glow on highlights, sharing the same taps
+float3 blur_sample(
+    texture2d<float> tex,
+    sampler texSmplr,
+    float2 coord,
+    float2 texel,
+    float blur_radius,
+    float glow_intensity,
+    thread float3& highlights
+)
+{
+    float3 center = tex.sample(texSmplr, coord).rgb;
+    float3 sum = center * 4.0;
+    float3 bright = max(center - GLOW_THRESHOLD, 0.0) * 4.0;
+    for (int i = 0; i < 8; i++) {
+        float a = float(i) * 0.78539816;
+        float2 d = float2(cos(a), sin(a));
+        float3 inner = tex.sample(texSmplr, coord + d * texel * blur_radius).rgb;
+        float3 outer = tex.sample(texSmplr, coord + d * texel * blur_radius * 2.4).rgb;
+        sum += inner + outer;
+        bright += max(inner - GLOW_THRESHOLD, 0.0);
+        bright += max(outer - GLOW_THRESHOLD, 0.0);
+    }
+    highlights = bright * (glow_intensity / 20.0);
+    return sum / 20.0;
+}
+
+fragment float4 fragmentShader(
+    RasterizerData in [[stage_in]],
+    texture2d<float> tex [[texture(0)]],
+    sampler texSmplr [[sampler(0)]],
+    constant Uniforms& uniforms [[buffer(0)]]
+)
+{
+    float2 coord = in.uv;
+
+    float2 texel = 1.0 / in.resolution;
+
+    float3 color = sample_scene(tex, texSmplr, coord, uniforms.chromatic_aberration);
+    float3 highlights;
+    float3 blurred = blur_sample(
+        tex, texSmplr, coord, texel,
+        uniforms.blur_radius, uniforms.glow_intensity, highlights
+    );
+    color = mix(color, blurred, uniforms.blur_amount) + highlights;
+
+    return float4(color * uniforms.brightness, 1.0);
+}

+ 59 - 11
bin/app/src/gfx/shader/mod.rs

@@ -19,10 +19,13 @@
 use miniquad::*;
 
 pub const GL_VERTEX: &str = include_str!("gl_vertex.vert");
+pub const GL_VERTEX_CRT: &str = include_str!("gl_vertex_crt.vert");
 pub const GL_FRAGMENT_RGB: &str = include_str!("gl_fragment_rgb.frag");
 pub const GL_FRAGMENT_YUV: &str = include_str!("gl_fragment_yuv.frag");
+pub const GL_FRAGMENT_CRT: &str = include_str!("gl_fragment_crt.frag");
 pub const METAL_RGB: &str = include_str!("metal_rgb.metal");
 pub const METAL_YUV: &str = include_str!("metal_yuv.metal");
+pub const METAL_CRT: &str = include_str!("metal_crt.metal");
 
 pub fn meta_rgb() -> ShaderMeta {
     ShaderMeta {
@@ -46,7 +49,16 @@ pub fn create_rgb_pipeline(ctx: &mut Box<dyn RenderingBackend>) -> Pipeline {
         Backend::Metal => ShaderSource::Msl { program: METAL_RGB },
     };
 
-    create_pipeline_with_meta(ctx, shader_source, shader_meta)
+    let params = PipelineParams {
+        color_blend: Some(BlendState::new(
+            Equation::Add,
+            BlendFactor::Value(BlendValue::SourceAlpha),
+            BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
+        )),
+        ..Default::default()
+    };
+
+    create_pipeline_with_meta(ctx, shader_source, shader_meta, params)
 }
 
 pub fn create_yuv_pipeline(ctx: &mut Box<dyn RenderingBackend>) -> Pipeline {
@@ -57,28 +69,64 @@ pub fn create_yuv_pipeline(ctx: &mut Box<dyn RenderingBackend>) -> Pipeline {
         Backend::Metal => ShaderSource::Msl { program: METAL_YUV },
     };
 
-    create_pipeline_with_meta(ctx, shader_source, shader_meta)
+    let params = PipelineParams {
+        color_blend: Some(BlendState::new(
+            Equation::Add,
+            BlendFactor::Value(BlendValue::SourceAlpha),
+            BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
+        )),
+        ..Default::default()
+    };
+
+    create_pipeline_with_meta(ctx, shader_source, shader_meta, params)
+}
+
+pub fn create_crt_pipeline(ctx: &mut Box<dyn RenderingBackend>) -> Pipeline {
+    let shader_meta = ShaderMeta {
+        images: vec!["tex".to_string()],
+        uniforms: UniformBlockLayout {
+            uniforms: vec![
+                UniformDesc::new("Projection", UniformType::Mat4),
+                UniformDesc::new("Model", UniformType::Mat4),
+                UniformDesc::new("chromatic_aberration", UniformType::Float1),
+                UniformDesc::new("blur_amount", UniformType::Float1),
+                UniformDesc::new("blur_radius", UniformType::Float1),
+                UniformDesc::new("glow_intensity", UniformType::Float1),
+                UniformDesc::new("brightness", UniformType::Float1),
+            ],
+        },
+    };
+
+    let shader_source = match ctx.info().backend {
+        Backend::OpenGl => ShaderSource::Glsl { vertex: GL_VERTEX_CRT, fragment: GL_FRAGMENT_CRT },
+        Backend::Metal => ShaderSource::Msl { program: METAL_CRT },
+    };
+
+    let shader = ctx.new_shader(shader_source, shader_meta).unwrap();
+
+    ctx.new_pipeline(
+        &[BufferLayout::default()],
+        &[
+            VertexAttribute::new("in_pos", VertexFormat::Float2),
+            VertexAttribute::new("in_color", VertexFormat::Float4),
+            VertexAttribute::new("in_uv", VertexFormat::Float2),
+        ],
+        shader,
+        PipelineParams::default(),
+    )
 }
 
 fn create_pipeline_with_meta(
     ctx: &mut Box<dyn RenderingBackend>,
     shader_source: ShaderSource,
     mut shader_meta: ShaderMeta,
+    params: PipelineParams,
 ) -> Pipeline {
     shader_meta.uniforms.uniforms.push(UniformDesc::new("Projection", UniformType::Mat4));
     shader_meta.uniforms.uniforms.push(UniformDesc::new("Model", UniformType::Mat4));
 
     let shader = ctx.new_shader(shader_source, shader_meta).unwrap();
 
-    let params = PipelineParams {
-        color_blend: Some(BlendState::new(
-            Equation::Add,
-            BlendFactor::Value(BlendValue::SourceAlpha),
-            BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
-        )),
-        ..Default::default()
-    };
-
     ctx.new_pipeline(
         &[BufferLayout::default()],
         &[