feat: paint shader renderer
This commit is contained in:
1 parent
201f43ef34
commit
94cc30475f
4 files changed
+324
-3
No files matched your search
+263
@@ -0,0 +1,263 @@
|
|||||||
|
//! The paint shader on a Wayland surface: EGL + OpenGL ES 3, set up like
|
||||||
|
//! zoom's renderer. Optionally composites a premultiplied overlay (the
|
||||||
|
//! picker's cards) on top in the same pass.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use glow::HasContext;
|
||||||
|
use khronos_egl as egl;
|
||||||
|
use wayland_client::{Connection, Proxy, protocol::wl_surface::WlSurface};
|
||||||
|
|
||||||
|
use super::Palette;
|
||||||
|
use crate::zoom::gl::{Egl, config, open};
|
||||||
|
|
||||||
|
const VERTEX: &str = "#version 300 es
|
||||||
|
// One triangle that covers the screen.
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||||
|
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
const FRAGMENT: &str = include_str!("paint.frag");
|
||||||
|
|
||||||
|
/// What one frame shows.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Frame {
|
||||||
|
/// Seconds since the animation started.
|
||||||
|
pub time: f32,
|
||||||
|
/// 0 = calm flow, 1 = swirl around the centre.
|
||||||
|
pub motion: f32,
|
||||||
|
pub speed: f32,
|
||||||
|
/// Blob size; 1 is the default.
|
||||||
|
pub scale: f32,
|
||||||
|
pub palette: Palette,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
res: glow::UniformLocation,
|
||||||
|
t: glow::UniformLocation,
|
||||||
|
motion: glow::UniformLocation,
|
||||||
|
speed: glow::UniformLocation,
|
||||||
|
scale: glow::UniformLocation,
|
||||||
|
c1: glow::UniformLocation,
|
||||||
|
c2: glow::UniformLocation,
|
||||||
|
c3: glow::UniformLocation,
|
||||||
|
use_overlay: glow::UniformLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Paint {
|
||||||
|
gl: glow::Context,
|
||||||
|
u: Uniforms,
|
||||||
|
overlay: glow::Texture,
|
||||||
|
size: (u32, u32),
|
||||||
|
egl: Egl,
|
||||||
|
display: egl::Display,
|
||||||
|
context: egl::Context,
|
||||||
|
surface: egl::Surface,
|
||||||
|
// Destroyed after the EGL surface that draws into it.
|
||||||
|
window: wayland_egl::WlEglSurface,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint {
|
||||||
|
/// A GL surface of `size` buffer px on `surface`.
|
||||||
|
pub fn new(conn: &Connection, surface: &WlSurface, size: (u32, u32)) -> Result<Paint> {
|
||||||
|
let (egl, display) = open(conn)?;
|
||||||
|
egl.bind_api(egl::OPENGL_ES_API)
|
||||||
|
.context("no OpenGL ES in EGL")?;
|
||||||
|
let config = config(&egl, display)?;
|
||||||
|
let context = egl
|
||||||
|
.create_context(
|
||||||
|
display,
|
||||||
|
config,
|
||||||
|
None,
|
||||||
|
&[egl::CONTEXT_MAJOR_VERSION, 3, egl::NONE],
|
||||||
|
)
|
||||||
|
.context("could not create an OpenGL ES 3 context")?;
|
||||||
|
let window = wayland_egl::WlEglSurface::new(surface.id(), size.0 as i32, size.1 as i32)
|
||||||
|
.context("could not create a wl_egl_window")?;
|
||||||
|
let egl_surface = unsafe {
|
||||||
|
egl.create_window_surface(display, config, window.ptr() as egl::NativeWindowType, None)
|
||||||
|
}
|
||||||
|
.context("could not create the EGL window surface")?;
|
||||||
|
egl.make_current(display, Some(egl_surface), Some(egl_surface), Some(context))
|
||||||
|
.context("eglMakeCurrent failed")?;
|
||||||
|
// Paced by frame callbacks; swaps must not block.
|
||||||
|
egl.swap_interval(display, 0)
|
||||||
|
.context("eglSwapInterval failed")?;
|
||||||
|
let gl = unsafe {
|
||||||
|
glow::Context::from_loader_function(|name| {
|
||||||
|
egl.get_proc_address(name)
|
||||||
|
.map_or(std::ptr::null(), |f| f as *const _)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let (u, overlay) = unsafe { setup(&gl, size)? };
|
||||||
|
Ok(Paint {
|
||||||
|
gl,
|
||||||
|
u,
|
||||||
|
overlay,
|
||||||
|
size,
|
||||||
|
egl,
|
||||||
|
display,
|
||||||
|
context,
|
||||||
|
surface: egl_surface,
|
||||||
|
window,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The surface's buffer size changed.
|
||||||
|
pub fn resize(&mut self, size: (u32, u32)) {
|
||||||
|
if size != self.size {
|
||||||
|
self.window.resize(size.0 as i32, size.1 as i32, 0, 0);
|
||||||
|
unsafe { self.gl.viewport(0, 0, size.0 as i32, size.1 as i32) };
|
||||||
|
self.size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows `pixmap` (premultiplied RGBA, the surface's size) over the
|
||||||
|
/// paint, or nothing.
|
||||||
|
pub fn set_overlay(&mut self, pixmap: Option<&tiny_skia::Pixmap>) {
|
||||||
|
unsafe {
|
||||||
|
self.gl
|
||||||
|
.uniform_1_i32(Some(&self.u.use_overlay), pixmap.is_some() as i32);
|
||||||
|
if let Some(p) = pixmap {
|
||||||
|
self.gl.active_texture(glow::TEXTURE0);
|
||||||
|
self.gl.bind_texture(glow::TEXTURE_2D, Some(self.overlay));
|
||||||
|
self.gl.tex_image_2d(
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
glow::RGBA8 as i32,
|
||||||
|
p.width() as i32,
|
||||||
|
p.height() as i32,
|
||||||
|
0,
|
||||||
|
glow::RGBA,
|
||||||
|
glow::UNSIGNED_BYTE,
|
||||||
|
glow::PixelUnpackData::Slice(Some(p.data())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws `frame` and swaps (which commits the surface).
|
||||||
|
pub fn draw(&self, frame: &Frame) -> Result<()> {
|
||||||
|
let p = frame.palette;
|
||||||
|
unsafe {
|
||||||
|
let gl = &self.gl;
|
||||||
|
gl.uniform_2_f32(Some(&self.u.res), self.size.0 as f32, self.size.1 as f32);
|
||||||
|
gl.uniform_1_f32(Some(&self.u.t), frame.time);
|
||||||
|
gl.uniform_1_f32(Some(&self.u.motion), frame.motion);
|
||||||
|
gl.uniform_1_f32(Some(&self.u.speed), frame.speed);
|
||||||
|
gl.uniform_1_f32(Some(&self.u.scale), frame.scale);
|
||||||
|
gl.uniform_3_f32(Some(&self.u.c1), p.base[0], p.base[1], p.base[2]);
|
||||||
|
gl.uniform_3_f32(Some(&self.u.c2), p.body[0], p.body[1], p.body[2]);
|
||||||
|
gl.uniform_3_f32(
|
||||||
|
Some(&self.u.c3),
|
||||||
|
p.highlight[0],
|
||||||
|
p.highlight[1],
|
||||||
|
p.highlight[2],
|
||||||
|
);
|
||||||
|
gl.draw_arrays(glow::TRIANGLES, 0, 3);
|
||||||
|
}
|
||||||
|
self.egl
|
||||||
|
.swap_buffers(self.display, self.surface)
|
||||||
|
.context("eglSwapBuffers failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Paint {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.egl.make_current(self.display, None, None, None);
|
||||||
|
let _ = self.egl.destroy_surface(self.display, self.surface);
|
||||||
|
let _ = self.egl.destroy_context(self.display, self.context);
|
||||||
|
let _ = self.egl.terminate(self.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compiles the program and returns its uniforms and the overlay texture.
|
||||||
|
unsafe fn setup(gl: &glow::Context, size: (u32, u32)) -> Result<(Uniforms, glow::Texture)> {
|
||||||
|
unsafe {
|
||||||
|
let program = gl.create_program().map_err(|e| anyhow!(e))?;
|
||||||
|
for (kind, source) in [
|
||||||
|
(glow::VERTEX_SHADER, VERTEX),
|
||||||
|
(glow::FRAGMENT_SHADER, FRAGMENT),
|
||||||
|
] {
|
||||||
|
let shader = gl.create_shader(kind).map_err(|e| anyhow!(e))?;
|
||||||
|
gl.shader_source(shader, source);
|
||||||
|
gl.compile_shader(shader);
|
||||||
|
if !gl.get_shader_compile_status(shader) {
|
||||||
|
anyhow::bail!("paint shader: {}", gl.get_shader_info_log(shader));
|
||||||
|
}
|
||||||
|
gl.attach_shader(program, shader);
|
||||||
|
}
|
||||||
|
gl.link_program(program);
|
||||||
|
if !gl.get_program_link_status(program) {
|
||||||
|
anyhow::bail!("paint program: {}", gl.get_program_info_log(program));
|
||||||
|
}
|
||||||
|
gl.use_program(Some(program));
|
||||||
|
let vao = gl.create_vertex_array().map_err(|e| anyhow!(e))?;
|
||||||
|
gl.bind_vertex_array(Some(vao));
|
||||||
|
|
||||||
|
let overlay = gl.create_texture().map_err(|e| anyhow!(e))?;
|
||||||
|
gl.active_texture(glow::TEXTURE0);
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, Some(overlay));
|
||||||
|
gl.tex_parameter_i32(
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
glow::TEXTURE_MIN_FILTER,
|
||||||
|
glow::NEAREST as i32,
|
||||||
|
);
|
||||||
|
gl.tex_parameter_i32(
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
glow::TEXTURE_MAG_FILTER,
|
||||||
|
glow::NEAREST as i32,
|
||||||
|
);
|
||||||
|
gl.viewport(0, 0, size.0 as i32, size.1 as i32);
|
||||||
|
|
||||||
|
let uniform = |name: &str| {
|
||||||
|
gl.get_uniform_location(program, name)
|
||||||
|
.with_context(|| format!("no uniform {name}"))
|
||||||
|
};
|
||||||
|
if let Ok(sampler) = uniform("overlay") {
|
||||||
|
gl.uniform_1_i32(Some(&sampler), 0);
|
||||||
|
}
|
||||||
|
let u = Uniforms {
|
||||||
|
res: uniform("res")?,
|
||||||
|
t: uniform("t")?,
|
||||||
|
motion: uniform("motion")?,
|
||||||
|
speed: uniform("speed")?,
|
||||||
|
scale: uniform("scale")?,
|
||||||
|
c1: uniform("c1")?,
|
||||||
|
c2: uniform("c2")?,
|
||||||
|
c3: uniform("c3")?,
|
||||||
|
use_overlay: uniform("use_overlay")?,
|
||||||
|
};
|
||||||
|
gl.uniform_1_i32(Some(&u.use_overlay), 0);
|
||||||
|
Ok((u, overlay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shader_declares_its_uniforms() {
|
||||||
|
for name in [
|
||||||
|
"res",
|
||||||
|
"t",
|
||||||
|
"motion",
|
||||||
|
"speed",
|
||||||
|
"scale",
|
||||||
|
"c1",
|
||||||
|
"c2",
|
||||||
|
"c3",
|
||||||
|
"overlay",
|
||||||
|
"use_overlay",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
FRAGMENT
|
||||||
|
.lines()
|
||||||
|
.any(|l| l.starts_with("uniform") && l.contains(&format!(" {name};"))),
|
||||||
|
"{name} missing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
|
|
||||||
|
pub mod gl;
|
||||||
|
|
||||||
/// Three colours, dark to light, as linear 0–1 RGB.
|
/// Three colours, dark to light, as linear 0–1 RGB.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Palette {
|
pub struct Palette {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
uniform vec2 res;
|
||||||
|
uniform float t;
|
||||||
|
uniform float motion; // 0 flow, 1 swirl
|
||||||
|
uniform float speed;
|
||||||
|
uniform float scale; // blob size: 1 = the spike's
|
||||||
|
uniform vec3 c1;
|
||||||
|
uniform vec3 c2;
|
||||||
|
uniform vec3 c3;
|
||||||
|
uniform sampler2D overlay;
|
||||||
|
uniform bool use_overlay;
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
vec2 warp(vec2 p, float time) {
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
float fi = float(i);
|
||||||
|
p += 0.42 * sin(p.yx * vec2(1.55, 1.28) + time * vec2(0.27, 0.33) + fi * vec2(1.3, 2.1));
|
||||||
|
p = mat2(0.82, -0.57, 0.57, 0.82) * p;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 paint(vec2 frag) {
|
||||||
|
vec2 uv = (frag - 0.5 * res) / res.y;
|
||||||
|
float time = t * speed;
|
||||||
|
if (motion > 0.0) {
|
||||||
|
float r = length(uv);
|
||||||
|
float a = atan(uv.y, uv.x) + motion * (2.2 / (r + 0.35)) + time * 0.25 * motion;
|
||||||
|
uv = r * vec2(cos(a), sin(a));
|
||||||
|
}
|
||||||
|
vec2 p = warp(uv * 4.0 / scale, time);
|
||||||
|
p = warp(p * 1.7, time * 1.25 + 3.0);
|
||||||
|
float v = sin(p.x) * cos(p.y);
|
||||||
|
float w = sin(length(p) * 0.6 - time * 0.8);
|
||||||
|
float m = v * 0.75 + w * 0.25;
|
||||||
|
vec3 col = c1;
|
||||||
|
col = mix(col, c2, smoothstep(-0.32, -0.18, m));
|
||||||
|
col = mix(col, c3, smoothstep(0.38, 0.5, m) * 0.85);
|
||||||
|
float rim = 1.0 - smoothstep(0.0, 0.06, abs(m + 0.25));
|
||||||
|
col *= 1.0 - 0.3 * rim;
|
||||||
|
float gloss = pow(abs(cos(p.x * 1.3 + p.y)), 36.0);
|
||||||
|
col += gloss * 0.18 * (c3 + 0.25);
|
||||||
|
col *= 1.0 - 0.3 * dot(uv, uv);
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 frag = vec2(gl_FragCoord.x, res.y - gl_FragCoord.y);
|
||||||
|
vec3 col = paint(frag);
|
||||||
|
if (use_overlay) {
|
||||||
|
vec4 o = texture(overlay, frag / res);
|
||||||
|
col = o.rgb + col * (1.0 - o.a);
|
||||||
|
}
|
||||||
|
color = vec4(col, 1.0);
|
||||||
|
}
|
||||||
+3
-3
@@ -9,7 +9,7 @@ use wayland_client::{Connection, Proxy, protocol::wl_surface::WlSurface};
|
|||||||
use super::view::View;
|
use super::view::View;
|
||||||
use crate::frame::Bgrx;
|
use crate::frame::Bgrx;
|
||||||
|
|
||||||
type Egl = egl::DynamicInstance<egl::EGL1_5>;
|
pub(crate) type Egl = egl::DynamicInstance<egl::EGL1_5>;
|
||||||
|
|
||||||
/// `EGL_PLATFORM_WAYLAND_KHR`.
|
/// `EGL_PLATFORM_WAYLAND_KHR`.
|
||||||
const PLATFORM_WAYLAND: egl::Enum = 0x31D8;
|
const PLATFORM_WAYLAND: egl::Enum = 0x31D8;
|
||||||
@@ -47,7 +47,7 @@ void main() {
|
|||||||
";
|
";
|
||||||
|
|
||||||
/// Loads libEGL and opens the compositor's EGL display.
|
/// Loads libEGL and opens the compositor's EGL display.
|
||||||
fn open(conn: &Connection) -> Result<(Egl, egl::Display)> {
|
pub(crate) fn open(conn: &Connection) -> Result<(Egl, egl::Display)> {
|
||||||
let lib =
|
let lib =
|
||||||
unsafe { libloading::Library::new("libEGL.so.1") }.context("could not load libEGL.so.1")?;
|
unsafe { libloading::Library::new("libEGL.so.1") }.context("could not load libEGL.so.1")?;
|
||||||
let egl = unsafe { Egl::load_required_from(lib) }
|
let egl = unsafe { Egl::load_required_from(lib) }
|
||||||
@@ -65,7 +65,7 @@ fn open(conn: &Connection) -> Result<(Egl, egl::Display)> {
|
|||||||
Ok((egl, display))
|
Ok((egl, display))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(egl: &Egl, display: egl::Display) -> Result<egl::Config> {
|
pub(crate) fn config(egl: &Egl, display: egl::Display) -> Result<egl::Config> {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
let attributes = [
|
let attributes = [
|
||||||
egl::SURFACE_TYPE, egl::WINDOW_BIT,
|
egl::SURFACE_TYPE, egl::WINDOW_BIT,
|
||||||
|
|||||||
Reference in new issue
Block a user