diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 712bbe86..b87060bf 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -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 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, render_target_id: Option) { @@ -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, diff --git a/content/src/effects.rs b/content/src/effects.rs index db900795..fd4df276 100644 --- a/content/src/effects.rs +++ b/content/src/effects.rs @@ -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 { diff --git a/gl/src/lib.rs b/gl/src/lib.rs index 26681a9c..eb1caa1e 100644 --- a/gl/src/lib.rs +++ b/gl/src/lib.rs @@ -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(); } diff --git a/gpu/src/lib.rs b/gpu/src/lib.rs index 8f3f2064..35f94086 100644 --- a/gpu/src/lib.rs +++ b/gpu/src/lib.rs @@ -173,6 +173,7 @@ pub enum UniformData { Mat2(F32x4), Mat4([F32x4; 4]), Vec2(F32x2), + Vec3([f32; 3]), Vec4(F32x4), TextureUnit(u32), } diff --git a/metal/src/lib.rs b/metal/src/lib.rs index 3cf60ee8..a7fc26c3 100644 --- a/metal/src/lib.rs +++ b/metal/src/lib.rs @@ -934,6 +934,11 @@ impl MetalDevice { uniform_buffer_data.write_f32::(vector.x()).unwrap(); uniform_buffer_data.write_f32::(vector.y()).unwrap(); } + UniformData::Vec3(array) => { + uniform_buffer_data.write_f32::(array[0]).unwrap(); + uniform_buffer_data.write_f32::(array[1]).unwrap(); + uniform_buffer_data.write_f32::(array[2]).unwrap(); + } UniformData::Vec4(vector) => { uniform_buffer_data.write_f32::(vector.x()).unwrap(); uniform_buffer_data.write_f32::(vector.y()).unwrap(); diff --git a/renderer/src/gpu/renderer.rs b/renderer/src/gpu/renderer.rs index 49e664dd..493005e4 100644 --- a/renderer/src/gpu/renderer.rs +++ b/renderer/src/gpu/renderer.rs @@ -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, filter_basic_vertex_array: FilterBasicVertexArray, + filter_blur_program: FilterBlurProgram, + filter_blur_vertex_array: FilterBlurVertexArray, filter_text_program: FilterTextProgram, filter_text_vertex_array: FilterTextVertexArray, 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; diff --git a/renderer/src/gpu/shaders.rs b/renderer/src/gpu/shaders.rs index dbe7457c..efc856a3 100644 --- a/renderer/src/gpu/shaders.rs +++ b/renderer/src/gpu/shaders.rs @@ -572,6 +572,68 @@ impl FilterBasicVertexArray where D: Device { } } +pub struct FilterBlurProgram 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 FilterBlurProgram where D: Device { + pub fn new(device: &D, resources: &dyn ResourceLoader) -> FilterBlurProgram { + 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 where D: Device { + pub vertex_array: D::VertexArray, +} + +impl FilterBlurVertexArray where D: Device { + pub fn new( + device: &D, + fill_blur_program: &FilterBlurProgram, + quad_vertex_positions_buffer: &D::Buffer, + quad_vertex_indices_buffer: &D::Buffer, + ) -> FilterBlurVertexArray { + 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 where D: Device { pub program: D::Program, pub source_uniform: D::Uniform, diff --git a/resources/shaders/gl3/filter_blur.fs.glsl b/resources/shaders/gl3/filter_blur.fs.glsl new file mode 100644 index 00000000..d089fd30 --- /dev/null +++ b/resources/shaders/gl3/filter_blur.fs.glsl @@ -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; +} + diff --git a/resources/shaders/metal/filter_blur.fs.metal b/resources/shaders/metal/filter_blur.fs.metal new file mode 100644 index 00000000..b70ef414 --- /dev/null +++ b/resources/shaders/metal/filter_blur.fs.metal @@ -0,0 +1,52 @@ +// Automatically generated from files in pathfinder/shaders/. Do not edit! +#include +#include + +using namespace metal; + +struct spvDescriptorSetBuffer0 +{ + constant float3* uInitialGaussCoeff [[id(0)]]; + texture2d 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; +} + diff --git a/shaders/Makefile b/shaders/Makefile index f224ab14..7e672f04 100644 --- a/shaders/Makefile +++ b/shaders/Makefile @@ -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 \ diff --git a/shaders/filter_blur.fs.glsl b/shaders/filter_blur.fs.glsl new file mode 100644 index 00000000..dccc353c --- /dev/null +++ b/shaders/filter_blur.fs.glsl @@ -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 or the MIT license +// , 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; +} +