Implement a blur filter for canvas shadows

This commit is contained in:
Patrick Walton 2020-02-26 18:43:41 -08:00
parent d1c7da8bd2
commit 3a014d78eb
11 changed files with 422 additions and 13 deletions

View File

@ -1,6 +1,6 @@
// pathfinder/canvas/src/lib.rs
//
// Copyright © 2019 The Pathfinder Project Developers.
// Copyright © 2020 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
@ -12,7 +12,7 @@
use pathfinder_color::ColorU;
use pathfinder_content::dash::OutlineDash;
use pathfinder_content::effects::{BlendMode, CompositeOp, Effects, Filter};
use pathfinder_content::effects::{BlendMode, BlurDirection, CompositeOp, Effects, Filter};
use pathfinder_content::fill::FillRule;
use pathfinder_content::gradient::Gradient;
use pathfinder_content::outline::{ArcDirection, Contour, Outline};
@ -161,11 +161,34 @@ impl CanvasRenderingContext2D {
// Shadows
#[inline]
pub fn shadow_blur(&self) -> f32 {
self.current_state.shadow_blur
}
#[inline]
pub fn set_shadow_blur(&mut self, new_shadow_blur: f32) {
self.current_state.shadow_blur = new_shadow_blur;
}
#[inline]
pub fn shadow_color(&self) -> ColorU {
match self.current_state.shadow_paint {
Paint::Color(color) => color,
_ => panic!("Unexpected shadow paint!"),
}
}
#[inline]
pub fn set_shadow_color(&mut self, new_shadow_color: ColorU) {
self.current_state.shadow_paint = Paint::Color(new_shadow_color);
}
#[inline]
pub fn shadow_offset(&self) -> Vector2F {
self.current_state.shadow_offset
}
#[inline]
pub fn set_shadow_offset(&mut self, new_shadow_offset: Vector2F) {
self.current_state.shadow_offset = new_shadow_offset;
@ -235,7 +258,8 @@ impl CanvasRenderingContext2D {
let opacity = (self.current_state.global_alpha * 255.0) as u8;
if !self.current_state.shadow_paint.is_fully_transparent() {
let render_target_id = self.push_render_target_if_needed(composite_op);
let composite_render_target_id = self.push_render_target_if_needed(composite_op);
let shadow_blur_render_target_ids = self.push_shadow_blur_render_targets_if_needed();
let paint = self.current_state.resolve_paint(&self.current_state.shadow_paint);
let paint_id = self.scene.push_paint(&paint);
@ -249,7 +273,8 @@ impl CanvasRenderingContext2D {
path.set_opacity(opacity);
self.scene.push_path(path);
self.composite_render_target_if_needed(composite_op, render_target_id);
self.composite_shadow_blur_render_targets_if_needed(shadow_blur_render_target_ids);
self.composite_render_target_if_needed(composite_op, composite_render_target_id);
}
let render_target_id = self.push_render_target_if_needed(composite_op);
@ -274,6 +299,19 @@ impl CanvasRenderingContext2D {
Some(self.scene.push_render_target(RenderTarget::new(render_target_size, String::new())))
}
fn push_shadow_blur_render_targets_if_needed(&mut self) -> Option<[RenderTargetId; 2]> {
if self.current_state.shadow_blur == 0.0 {
return None;
}
let render_target_size = self.scene.view_box().size().ceil().to_i32();
let render_target_id_a =
self.scene.push_render_target(RenderTarget::new(render_target_size, String::new()));
let render_target_id_b =
self.scene.push_render_target(RenderTarget::new(render_target_size, String::new()));
Some([render_target_id_a, render_target_id_b])
}
fn composite_render_target_if_needed(&mut self,
composite_op: Option<CompositeOp>,
render_target_id: Option<RenderTargetId>) {
@ -287,6 +325,27 @@ impl CanvasRenderingContext2D {
Effects::new(Filter::Composite(composite_op)));
}
fn composite_shadow_blur_render_targets_if_needed(
&mut self,
render_target_ids: Option<[RenderTargetId; 2]>) {
let render_target_ids = match render_target_ids {
None => return,
Some(render_target_ids) => render_target_ids,
};
let sigma = self.current_state.shadow_blur * 0.5;
self.scene.pop_render_target();
self.scene.draw_render_target(render_target_ids[1], Effects::new(Filter::Blur {
direction: BlurDirection::X,
sigma,
}));
self.scene.pop_render_target();
self.scene.draw_render_target(render_target_ids[0], Effects::new(Filter::Blur {
direction: BlurDirection::Y,
sigma,
}));
}
// Transformations
#[inline]
@ -377,6 +436,7 @@ struct State {
fill_paint: Paint,
stroke_paint: Paint,
shadow_paint: Paint,
shadow_blur: f32,
shadow_offset: Vector2F,
text_align: TextAlign,
image_smoothing_enabled: bool,
@ -401,6 +461,7 @@ impl State {
fill_paint: Paint::black(),
stroke_paint: Paint::black(),
shadow_paint: Paint::transparent_black(),
shadow_blur: 0.0,
shadow_offset: Vector2F::default(),
text_align: TextAlign::Left,
image_smoothing_enabled: true,

View File

@ -57,6 +57,15 @@ pub enum Filter {
/// If this is enabled, stem darkening is advised.
gamma_correction: bool,
},
/// A blur operation in one direction, either horizontal or vertical.
///
/// To produce a full Gaussian blur, perform two successive blur operations, one in each
/// direction.
Blur {
direction: BlurDirection,
sigma: f32,
},
}
#[derive(Clone, Copy, Debug)]
@ -123,6 +132,12 @@ pub enum BlendMode {
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct DefringingKernel(pub [f32; 4]);
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BlurDirection {
X,
Y,
}
impl Default for CompositeOp {
#[inline]
fn default() -> CompositeOp {

View File

@ -156,6 +156,9 @@ impl GLDevice {
UniformData::Vec2(data) => {
gl::Uniform2f(uniform.location, data.x(), data.y()); ck();
}
UniformData::Vec3(data) => {
gl::Uniform3f(uniform.location, data[0], data[1], data[2]); ck();
}
UniformData::Vec4(data) => {
gl::Uniform4f(uniform.location, data.x(), data.y(), data.z(), data.w()); ck();
}

View File

@ -173,6 +173,7 @@ pub enum UniformData {
Mat2(F32x4),
Mat4([F32x4; 4]),
Vec2(F32x2),
Vec3([f32; 3]),
Vec4(F32x4),
TextureUnit(u32),
}

View File

@ -934,6 +934,11 @@ impl MetalDevice {
uniform_buffer_data.write_f32::<NativeEndian>(vector.x()).unwrap();
uniform_buffer_data.write_f32::<NativeEndian>(vector.y()).unwrap();
}
UniformData::Vec3(array) => {
uniform_buffer_data.write_f32::<NativeEndian>(array[0]).unwrap();
uniform_buffer_data.write_f32::<NativeEndian>(array[1]).unwrap();
uniform_buffer_data.write_f32::<NativeEndian>(array[2]).unwrap();
}
UniformData::Vec4(vector) => {
uniform_buffer_data.write_f32::<NativeEndian>(vector.x()).unwrap();
uniform_buffer_data.write_f32::<NativeEndian>(vector.y()).unwrap();

View File

@ -14,19 +14,21 @@ use crate::gpu::shaders::{AlphaTileBlendModeProgram, AlphaTileDodgeBurnProgram};
use crate::gpu::shaders::{AlphaTileHSLProgram, AlphaTileOverlayProgram};
use crate::gpu::shaders::{AlphaTileProgram, AlphaTileVertexArray, CopyTileProgram};
use crate::gpu::shaders::{CopyTileVertexArray, FillProgram, FillVertexArray, FilterBasicProgram};
use crate::gpu::shaders::{FilterBasicVertexArray, FilterTextProgram, FilterTextVertexArray};
use crate::gpu::shaders::{MAX_FILLS_PER_BATCH, MaskTileProgram, MaskTileVertexArray};
use crate::gpu::shaders::{ReprojectionProgram, ReprojectionVertexArray, SolidTileProgram};
use crate::gpu::shaders::{SolidTileVertexArray, StencilProgram, StencilVertexArray};
use crate::gpu::shaders::{FilterBasicVertexArray,FilterBlurProgram, FilterBlurVertexArray};
use crate::gpu::shaders::{FilterTextProgram, FilterTextVertexArray, MAX_FILLS_PER_BATCH};
use crate::gpu::shaders::{MaskTileProgram, MaskTileVertexArray, ReprojectionProgram};
use crate::gpu::shaders::{ReprojectionVertexArray, SolidTileProgram, SolidTileVertexArray};
use crate::gpu::shaders::{StencilProgram, StencilVertexArray};
use crate::gpu_data::{AlphaTile, FillBatchPrimitive, MaskTile, PaintData, PaintPageContents};
use crate::gpu_data::{PaintPageId, RenderCommand, SolidTileVertex};
use crate::options::BoundingQuad;
use crate::tiles::{TILE_HEIGHT, TILE_WIDTH};
use pathfinder_color::{self as color, ColorF};
use pathfinder_content::effects::{BlendMode, CompositeOp, DefringingKernel, Effects, Filter};
use pathfinder_content::effects::{BlendMode, BlurDirection, CompositeOp, DefringingKernel};
use pathfinder_content::effects::{Effects, Filter};
use pathfinder_content::fill::FillRule;
use pathfinder_content::pattern::RenderTargetId;
use pathfinder_geometry::vector::{Vector2I, Vector4F};
use pathfinder_geometry::vector::{Vector2F, Vector2I, Vector4F};
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::transform3d::Transform4F;
use pathfinder_gpu::resources::ResourceLoader;
@ -37,6 +39,7 @@ use pathfinder_gpu::{TextureFormat, TextureSamplingFlags, UniformData};
use pathfinder_simd::default::{F32x2, F32x4};
use std::cmp;
use std::collections::VecDeque;
use std::f32;
use std::mem;
use std::ops::{Add, Div};
use std::time::Duration;
@ -48,6 +51,9 @@ static QUAD_VERTEX_INDICES: [u32; 6] = [0, 1, 3, 1, 2, 3];
pub(crate) const MASK_TILES_ACROSS: u32 = 256;
pub(crate) const MASK_TILES_DOWN: u32 = 256;
// 1.0 / sqrt(2*pi)
const SQRT_2_PI_INV: f32 = 0.3989422804014327;
const TEXTURE_CACHE_SIZE: usize = 8;
// FIXME(pcwalton): Shrink this again!
@ -118,6 +124,8 @@ where
// Filter shaders
filter_basic_program: FilterBasicProgram<D>,
filter_basic_vertex_array: FilterBasicVertexArray<D>,
filter_blur_program: FilterBlurProgram<D>,
filter_blur_vertex_array: FilterBlurVertexArray<D>,
filter_text_program: FilterTextProgram<D>,
filter_text_vertex_array: FilterTextVertexArray<D>,
gamma_lut_texture: D::Texture,
@ -177,6 +185,7 @@ where
"tile_alpha_exclusion");
let alpha_tile_hsl_program = AlphaTileHSLProgram::new(&device, resources);
let filter_basic_program = FilterBasicProgram::new(&device, resources);
let filter_blur_program = FilterBlurProgram::new(&device, resources);
let filter_text_program = FilterTextProgram::new(&device, resources);
let stencil_program = StencilProgram::new(&device, resources);
let reprojection_program = ReprojectionProgram::new(&device, resources);
@ -276,6 +285,12 @@ where
&quad_vertex_positions_buffer,
&quad_vertex_indices_buffer,
);
let filter_blur_vertex_array = FilterBlurVertexArray::new(
&device,
&filter_blur_program,
&quad_vertex_positions_buffer,
&quad_vertex_indices_buffer,
);
let filter_text_vertex_array = FilterTextVertexArray::new(
&device,
&filter_text_program,
@ -361,6 +376,8 @@ where
filter_basic_program,
filter_basic_vertex_array,
filter_blur_program,
filter_blur_vertex_array,
filter_text_program,
filter_text_vertex_array,
gamma_lut_texture,
@ -1147,6 +1164,9 @@ where
defringing_kernel,
gamma_correction)
}
Filter::Blur { direction, sigma } => {
self.draw_blur_render_target(render_target_id, direction, sigma)
}
}
self.preserve_draw_framebuffer();
@ -1235,6 +1255,55 @@ where
});
}
fn draw_blur_render_target(&self,
render_target_id: RenderTargetId,
direction: BlurDirection,
sigma: f32) {
let clear_color = self.clear_color_for_draw_operation();
let source_framebuffer = &self.render_targets[render_target_id.0 as usize].framebuffer;
let source_texture = self.device.framebuffer_texture(source_framebuffer);
let source_texture_size = self.device.texture_size(source_texture);
let main_viewport = self.main_viewport();
let sigma_inv = 1.0 / sigma;
let gauss_coeff_x = SQRT_2_PI_INV * sigma_inv;
let gauss_coeff_y = f32::exp(-0.5 * sigma_inv * sigma_inv);
let gauss_coeff_z = gauss_coeff_y * gauss_coeff_y;
let src_offset = match direction {
BlurDirection::X => Vector2F::new(1.0, 0.0),
BlurDirection::Y => Vector2F::new(0.0, 1.0),
};
let src_offset_scale = src_offset / source_texture_size.to_f32();
let uniforms = vec![
(&self.filter_blur_program.framebuffer_size_uniform,
UniformData::Vec2(main_viewport.size().to_f32().0)),
(&self.filter_blur_program.src_uniform, UniformData::TextureUnit(0)),
(&self.filter_blur_program.src_offset_scale_uniform,
UniformData::Vec2(src_offset_scale.0)),
(&self.filter_blur_program.initial_gauss_coeff_uniform,
UniformData::Vec3([gauss_coeff_x, gauss_coeff_y, gauss_coeff_z])),
(&self.filter_blur_program.support_uniform,
UniformData::Int(f32::ceil(1.5 * sigma) as i32 * 2)),
];
self.device.draw_elements(6, &RenderState {
target: &self.draw_render_target(),
program: &self.filter_blur_program.program,
vertex_array: &self.filter_blur_vertex_array.vertex_array,
primitive: Primitive::Triangles,
textures: &[&source_texture],
uniforms: &uniforms,
viewport: main_viewport,
options: RenderOptions {
clear_ops: ClearOps { color: clear_color, ..ClearOps::default() },
blend: CompositeOp::SrcOver.to_blend_state(),
..RenderOptions::default()
},
});
}
fn blit_intermediate_dest_framebuffer_if_necessary(&mut self) {
if !self.flags.contains(RendererFlags::INTERMEDIATE_DEST_FRAMEBUFFER_NEEDED) {
return;

View File

@ -572,6 +572,68 @@ impl<D> FilterBasicVertexArray<D> where D: Device {
}
}
pub struct FilterBlurProgram<D> where D: Device {
pub program: D::Program,
pub framebuffer_size_uniform: D::Uniform,
pub src_uniform: D::Uniform,
pub src_offset_scale_uniform: D::Uniform,
pub initial_gauss_coeff_uniform: D::Uniform,
pub support_uniform: D::Uniform,
}
impl<D> FilterBlurProgram<D> where D: Device {
pub fn new(device: &D, resources: &dyn ResourceLoader) -> FilterBlurProgram<D> {
let program = device.create_program_from_shader_names(resources,
"filter_blur",
"filter",
"filter_blur");
let framebuffer_size_uniform = device.get_uniform(&program, "FramebufferSize");
let src_uniform = device.get_uniform(&program, "Src");
let src_offset_scale_uniform = device.get_uniform(&program, "SrcOffsetScale");
let initial_gauss_coeff_uniform = device.get_uniform(&program, "InitialGaussCoeff");
let support_uniform = device.get_uniform(&program, "Support");
FilterBlurProgram {
program,
framebuffer_size_uniform,
src_uniform,
src_offset_scale_uniform,
initial_gauss_coeff_uniform,
support_uniform,
}
}
}
pub struct FilterBlurVertexArray<D> where D: Device {
pub vertex_array: D::VertexArray,
}
impl<D> FilterBlurVertexArray<D> where D: Device {
pub fn new(
device: &D,
fill_blur_program: &FilterBlurProgram<D>,
quad_vertex_positions_buffer: &D::Buffer,
quad_vertex_indices_buffer: &D::Buffer,
) -> FilterBlurVertexArray<D> {
let vertex_array = device.create_vertex_array();
let position_attr = device.get_vertex_attr(&fill_blur_program.program, "Position")
.unwrap();
device.bind_buffer(&vertex_array, quad_vertex_positions_buffer, BufferTarget::Vertex);
device.configure_vertex_attr(&vertex_array, &position_attr, &VertexAttrDescriptor {
size: 2,
class: VertexAttrClass::Int,
attr_type: VertexAttrType::I16,
stride: 4,
offset: 0,
divisor: 0,
buffer_index: 0,
});
device.bind_buffer(&vertex_array, quad_vertex_indices_buffer, BufferTarget::Index);
FilterBlurVertexArray { vertex_array }
}
}
pub struct FilterTextProgram<D> where D: Device {
pub program: D::Program,
pub source_uniform: D::Uniform,

View File

@ -0,0 +1,71 @@
#version {{version}}
// Automatically generated from files in pathfinder/shaders/. Do not edit!
#extension GL_GOOGLE_include_directive : enable
precision highp float;
uniform sampler2D uSrc;
uniform vec2 uSrcOffsetScale;
uniform vec3 uInitialGaussCoeff;
uniform int uSupport;
in vec2 vTexCoord;
out vec4 oFragColor;
void main(){
vec3 gaussCoeff = uInitialGaussCoeff;
float gaussSum = gaussCoeff . x;
vec4 color = texture(uSrc, vTexCoord)* gaussCoeff . x;
gaussCoeff . xy *= gaussCoeff . yz;
for(int i = 1;i <= uSupport;i += 2){
float gaussPartialSum = gaussCoeff . x;
gaussCoeff . xy *= gaussCoeff . yz;
gaussPartialSum += gaussCoeff . x;
vec2 srcOffset = uSrcOffsetScale *(float(i)+ gaussCoeff . x / gaussPartialSum);
color +=(texture(uSrc, vTexCoord - srcOffset)+ texture(uSrc, vTexCoord + srcOffset))*
gaussPartialSum;
gaussSum += 2.0 * gaussPartialSum;
gaussCoeff . xy *= gaussCoeff . yz;
}
color /= gaussSum;
color . rgb *= color . a;
oFragColor = color;
}

View File

@ -0,0 +1,52 @@
// Automatically generated from files in pathfinder/shaders/. Do not edit!
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct spvDescriptorSetBuffer0
{
constant float3* uInitialGaussCoeff [[id(0)]];
texture2d<float> uSrc [[id(1)]];
sampler uSrcSmplr [[id(2)]];
constant int* uSupport [[id(3)]];
constant float2* uSrcOffsetScale [[id(4)]];
};
struct main0_out
{
float4 oFragColor [[color(0)]];
};
struct main0_in
{
float2 vTexCoord [[user(locn0)]];
};
fragment main0_out main0(main0_in in [[stage_in]], constant spvDescriptorSetBuffer0& spvDescriptorSet0 [[buffer(0)]])
{
main0_out out = {};
float3 gaussCoeff = (*spvDescriptorSet0.uInitialGaussCoeff);
float gaussSum = gaussCoeff.x;
float4 color = spvDescriptorSet0.uSrc.sample(spvDescriptorSet0.uSrcSmplr, in.vTexCoord) * gaussCoeff.x;
float2 _39 = gaussCoeff.xy * gaussCoeff.yz;
gaussCoeff = float3(_39.x, _39.y, gaussCoeff.z);
for (int i = 1; i <= (*spvDescriptorSet0.uSupport); i += 2)
{
float gaussPartialSum = gaussCoeff.x;
float2 _64 = gaussCoeff.xy * gaussCoeff.yz;
gaussCoeff = float3(_64.x, _64.y, gaussCoeff.z);
gaussPartialSum += gaussCoeff.x;
float2 srcOffset = (*spvDescriptorSet0.uSrcOffsetScale) * (float(i) + (gaussCoeff.x / gaussPartialSum));
color += ((spvDescriptorSet0.uSrc.sample(spvDescriptorSet0.uSrcSmplr, (in.vTexCoord - srcOffset)) + spvDescriptorSet0.uSrc.sample(spvDescriptorSet0.uSrcSmplr, (in.vTexCoord + srcOffset))) * gaussPartialSum);
gaussSum += (2.0 * gaussPartialSum);
float2 _108 = gaussCoeff.xy * gaussCoeff.yz;
gaussCoeff = float3(_108.x, _108.y, gaussCoeff.z);
}
color /= float4(gaussSum);
float3 _123 = color.xyz * color.w;
color = float4(_123.x, _123.y, _123.z, color.w);
out.oFragColor = color;
return out;
}

View File

@ -11,12 +11,13 @@ SHADERS=\
demo_ground.vs.glsl \
fill.fs.glsl \
fill.vs.glsl \
filter.vs.glsl \
filter_basic.fs.glsl \
filter_blur.fs.glsl \
filter_text.fs.glsl \
mask.vs.glsl \
mask_evenodd.fs.glsl \
mask_winding.fs.glsl \
filter.vs.glsl \
filter_basic.fs.glsl \
filter_text.fs.glsl \
reproject.fs.glsl \
reproject.vs.glsl \
stencil.fs.glsl \

View File

@ -0,0 +1,69 @@
#version 330
// pathfinder/shaders/filter_blur.fs.glsl
//
// Copyright © 2020 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// TODO(pcwalton): This could be significantly optimized by operating on a
// sparse per-tile basis.
// The technique here is "Incremental Computation of the Gaussian", GPU Gems 3, chapter 40:
// https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian
//
// It's the same technique WebRender uses.
#extension GL_GOOGLE_include_directive : enable
precision highp float;
#define SQRT_PI_2_INV 2.5066282746310002
uniform sampler2D uSrc;
uniform vec2 uSrcOffsetScale;
uniform vec3 uInitialGaussCoeff;
uniform int uSupport;
in vec2 vTexCoord;
out vec4 oFragColor;
void main() {
// Set up our incremental calculation.
vec3 gaussCoeff = uInitialGaussCoeff;
float gaussSum = gaussCoeff.x;
vec4 color = texture(uSrc, vTexCoord) * gaussCoeff.x;
gaussCoeff.xy *= gaussCoeff.yz;
// This is a common trick that lets us use the texture filtering hardware to evaluate two
// texels at a time. The basic principle is that, if c0 and c1 are colors of adjacent texels
// and k0 and k1 are arbitrary factors, the formula `k0 * c0 + k1 * c1` is equivalent to
// `(k0 + k1) * lerp(c0, c1, k1 / (k0 + k1))`. Linear interpolation, as performed by the
// texturing hardware when sampling adjacent pixels in one direction, evaluates
// `lerp(c0, c1, t)` where t is the offset from the texel with color `c0`. To evaluate the
// formula `k0 * c0 + k1 * c1`, therefore, we can use the texture hardware to perform linear
// interpolation with `t = k1 / (k0 + k1)`.
for (int i = 1; i <= uSupport; i += 2) {
float gaussPartialSum = gaussCoeff.x;
gaussCoeff.xy *= gaussCoeff.yz;
gaussPartialSum += gaussCoeff.x;
vec2 srcOffset = uSrcOffsetScale * (float(i) + gaussCoeff.x / gaussPartialSum);
color += (texture(uSrc, vTexCoord - srcOffset) + texture(uSrc, vTexCoord + srcOffset)) *
gaussPartialSum;
gaussSum += 2.0 * gaussPartialSum;
gaussCoeff.xy *= gaussCoeff.yz;
}
// Finish.
color /= gaussSum;
color.rgb *= color.a;
oFragColor = color;
}