// `@blend_src` needs this enable directive, which must precede every other
// declaration. Gated so only the LCD variant requires DUAL_SOURCE_BLENDING.
#ifdef EFFECT_SDF_LCD
enable dual_source_blending;
#endif
// One source for every Noesis shader. shader_preproc.rs strips the #ifdef
// branches using the define set from shader_defines::defines_for_shader().
//
// Ported from Shader.140.{vert,frag} in $NOESIS_SDK_DIR/Src/Packages/Render/
// GLRenderDevice/Src/; "GL ref" comments quote the GL code a block ports.
// ─── Uniforms ───
// cbuffer0_vs[16] (projection, 64B) + cbuffer1_vs[2] (glyph atlas size, 8B
// padded to vec4) packed into one struct so a single dynamic-offset bind group
// covers both. The SDF vertex shader needs cbuffer1_vs.xy to scale uv1 into
// glyph-atlas texel coords (`st1` below); other shaders ignore it.
struct VsUniforms {
projection: mat4x4<f32>,
glyph_size: vec4<f32>,
}
@group(0) @binding(0) var<uniform> vs_uniforms: VsUniforms;
// cbuffer0_ps[8]: values[0] is the fill color for EFFECT_RGBA, and
// values[0].x the opacity for the pattern and linear paints. PAINT_RADIAL
// documents its own layout.
struct PsUniforms0 {
values: array<vec4<f32>, 2>,
}
@group(1) @binding(0) var<uniform> ps_uniforms0: PsUniforms0;
// cbuffer1_ps: read by EFFECT_SHADOW / EFFECT_BLUR, and by custom shaders as
// their `Constants`. Noesis declares float[128]; the effects use at most the
// first 7 floats, so two vec4s (values[0] = cb[0..3], values[1] = cb[4..7]).
// Shares group(1) to stay within the downlevel 4-bind-group limit.
#ifdef HAS_CBUFFER1_PS
struct PsUniforms1 {
values: array<vec4<f32>, 2>,
}
@group(1) @binding(1) var<uniform> ps_uniforms1: PsUniforms1;
#endif
// A custom shader's constants. Its own source declares `struct Constants`.
#ifdef CUSTOM_CONSTANTS
@group(1) @binding(1) var<uniform> constants: Constants;
#endif
// The host's mip policy for pattern textures, from `set_pattern_lod`:
// x is the LOD bias, y the highest mip level sampled.
#ifdef PAINT_PATTERN
struct PatternLod {
values: vec4<f32>,
}
@group(1) @binding(2) var<uniform> pattern_lod: PatternLod;
#endif
// group(2): the paint texture. Holds `pattern` or `ramps` depending on the
// shader, the DOWNSAMPLE/UPSAMPLE source, or a custom effect's input. Shaders
// that don't read it get a dummy bind group.
#ifdef HAS_PAINT_TEXTURE
@group(2) @binding(0) var paint_texture: texture_2d<f32>;
@group(2) @binding(1) var paint_sampler: sampler;
#endif
// group(2) bindings 2-9: a custom shader's extra textures. Slots the shader
// doesn't use are bound to a 1x1 white texture.
#ifdef CUSTOM_SHADER
@group(2) @binding(2) var extra_texture0: texture_2d<f32>;
@group(2) @binding(3) var extra_sampler0: sampler;
@group(2) @binding(4) var extra_texture1: texture_2d<f32>;
@group(2) @binding(5) var extra_sampler1: sampler;
@group(2) @binding(6) var extra_texture2: texture_2d<f32>;
@group(2) @binding(7) var extra_sampler2: sampler;
@group(2) @binding(8) var extra_texture3: texture_2d<f32>;
@group(2) @binding(9) var extra_sampler3: sampler;
#endif
// group(3) bindings 0/1: `image`, the offscreen render of the layer being
// composited. Read by EFFECT_OPACITY, UPSAMPLE, SHADOW, and BLUR.
#ifdef HAS_IMAGE_TEXTURE
@group(3) @binding(0) var image_texture: texture_2d<f32>;
@group(3) @binding(1) var image_sampler: sampler;
#endif
// group(3) bindings 2/3: the blurred `shadow` pass, read by EFFECT_SHADOW and
// EFFECT_BLUR. Shares `image`'s group to stay within the 4-bind-group limit.
#ifdef HAS_SHADOW_TEXTURE
@group(3) @binding(2) var shadow_texture: texture_2d<f32>;
@group(3) @binding(3) var shadow_sampler: sampler;
#endif
// group(3) bindings 4/5: the SDF glyph atlas, apart from the paint texture so
// glyphs can be painted with a gradient or pattern.
#ifdef HAS_GLYPHS_TEXTURE
@group(3) @binding(4) var glyphs_texture: texture_2d<f32>;
@group(3) @binding(5) var glyphs_sampler: sampler;
#endif
// ─── Vertex I/O ───
// VsIn locations are the noesis_runtime VertexAttr indices (see
// vertex_layout.rs); each attribute is gated by its own HAS_* define.
struct VsIn {
@location(0) pos: vec2<f32>,
#ifdef HAS_COLOR
@location(1) color: vec4<f32>,
#endif
#ifdef HAS_UV0
@location(2) uv0: vec2<f32>,
#endif
#ifdef HAS_UV1
@location(3) uv1: vec2<f32>,
#endif
#ifdef HAS_COVERAGE
@location(4) coverage: f32,
#endif
#ifdef HAS_RECT
@location(5) rect: vec4<f32>,
#endif
#ifdef HAS_TILE
@location(6) tile: vec4<f32>,
#endif
#ifdef HAS_IMAGE_POS
@location(7) image_pos: vec4<f32>,
#endif
}
struct VsOut {
@builtin(position) clip_position: vec4<f32>,
#ifdef HAS_COLOR
// GL ref: `flat in vec4 color;`
@location(0) @interpolate(flat) color: vec4<f32>,
#endif
#ifdef HAS_UV0
@location(1) uv0: vec2<f32>,
#endif
#ifdef HAS_UV1
@location(2) uv1: vec2<f32>,
#endif
#ifdef HAS_COVERAGE
@location(4) coverage: f32,
#endif
#ifdef HAS_RECT
@location(5) @interpolate(flat) rect: vec4<f32>,
#endif
#ifdef HAS_TILE
@location(6) @interpolate(flat) tile: vec4<f32>,
#endif
#ifdef HAS_IMAGE_POS
@location(7) image_pos: vec4<f32>,
#endif
#ifdef HAS_ST1
// SDF only. uv1 in glyph-atlas texel space (uv1 × glyph_size). The
// fragment uses dFdx(st1) to size the AA window per fragment.
@location(3) st1: vec2<f32>,
#endif
#ifdef DOWNSAMPLE
// EFFECT_DOWNSAMPLE box-filters four taps; the vertex shader spreads
// attr_uv0 ± attr_uv1 into uv0..uv3 (GL Shader.140.vert DOWNSAMPLE block).
@location(8) uv2: vec2<f32>,
@location(9) uv3: vec2<f32>,
#endif
}
// ─── Vertex shader ───
@vertex
fn vs_main(in: VsIn) -> VsOut {
var out: VsOut;
// Noesis stores Matrix4 row-major (GetData()[i*4 + j] = row i, col j) and
// uploads it verbatim to cbuffer0_vs. WGSL's mat4x4<f32> loads the 16
// floats as column-major, which transposes the stored matrix relative
// to the logical one. Right-multiplying `v * M` recovers the logical
// transform (matches the GL 140 reference `vec4(pos, 0, 1) * mat4(...)`).
out.clip_position = vec4<f32>(in.pos, 0.0, 1.0) * vs_uniforms.projection;
#ifdef HAS_COLOR
out.color = in.color;
#endif
#ifdef DOWNSAMPLE
// 4-tap box filter offsets (GL Shader.140.vert DOWNSAMPLE): uv0 is the
// tap centre, uv1 carries the ±offset. Overrides the plain HAS_UV0/UV1
// pass-through below (guarded by #ifndef DOWNSAMPLE).
out.uv0 = in.uv0 + vec2<f32>(in.uv1.x, in.uv1.y);
out.uv1 = in.uv0 + vec2<f32>(in.uv1.x, -in.uv1.y);
out.uv2 = in.uv0 + vec2<f32>(-in.uv1.x, in.uv1.y);
out.uv3 = in.uv0 + vec2<f32>(-in.uv1.x, -in.uv1.y);
#endif
#ifndef DOWNSAMPLE
#ifdef HAS_UV0
out.uv0 = in.uv0;
#endif
#ifdef HAS_UV1
out.uv1 = in.uv1;
#endif
#endif
#ifdef HAS_COVERAGE
out.coverage = in.coverage;
#endif
#ifdef HAS_RECT
out.rect = in.rect;
#endif
#ifdef HAS_TILE
out.tile = in.tile;
#endif
#ifdef HAS_IMAGE_POS
out.image_pos = in.image_pos;
#endif
#ifdef HAS_ST1
// GL ref: `st1 = vec2(attr_uv1 * vec2(cbuffer1_vs[0], cbuffer1_vs[1]))`
out.st1 = in.uv1 * vs_uniforms.glyph_size.xy;
#endif
return out;
}
// ─── Pattern sampling ───
// wgpu samplers have no LOD bias, so the pattern paints scale the uv
// derivatives instead: the hardware then picks mip level
// min(lod + bias, max_level), where lod is the level it would pick unscaled.
#ifdef PAINT_PATTERN
fn sample_pattern_grad(uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
let size = vec2<f32>(textureDimensions(paint_texture));
let lod = log2(max(max(length(ddx * size), length(ddy * size)), 1e-6));
let biased = min(lod + pattern_lod.values.x, pattern_lod.values.y);
let scale = exp2(biased - lod);
return textureSampleGrad(paint_texture, paint_sampler, uv, ddx * scale, ddy * scale);
}
#endif
// ─── Custom shaders ───
// The functions a custom shader's source can call. fs_main fills the
// `custom_*` privates before it calls the shader's `main_brush` or
// `main_effect`. Every helper samples with explicit derivatives or level, so
// they work in non-uniform control flow too.
#ifdef CUSTOM_SHADER
var<private> custom_uv0: vec2<f32>;
var<private> custom_uv0_ddx: vec2<f32>;
var<private> custom_uv0_ddy: vec2<f32>;
// Samples extra texture `index` (0-3). The mip level follows uv0's rate of
// change on screen, which suits textures laid over the same area as uv0.
fn sample_texture(index: u32, uv: vec2<f32>) -> vec4<f32> {
let ddx = custom_uv0_ddx;
let ddy = custom_uv0_ddy;
var color: vec4<f32>;
switch index {
case 0u: {
color = textureSampleGrad(extra_texture0, extra_sampler0, uv, ddx, ddy);
}
case 1u: {
color = textureSampleGrad(extra_texture1, extra_sampler1, uv, ddx, ddy);
}
case 2u: {
color = textureSampleGrad(extra_texture2, extra_sampler2, uv, ddx, ddy);
}
default: {
color = textureSampleGrad(extra_texture3, extra_sampler3, uv, ddx, ddy);
}
}
return color;
}
#endif
// A brush shader's image, Noesis's `SampleImage`, under the host's pattern
// mip policy.
#ifdef CUSTOM_PATTERN
fn sample_image(uv: vec2<f32>) -> vec4<f32> {
return sample_pattern_grad(uv, custom_uv0_ddx, custom_uv0_ddy);
}
#endif
// An effect shader's input, after Noesis's EffectHelpers.h. The input may sit
// inside an atlas, so `input_coordinate` is only for sampling the input.
#ifdef EFFECT_CUSTOM
var<private> custom_rect: vec4<f32>;
var<private> custom_image_pos: vec4<f32>;
fn input_coordinate() -> vec2<f32> {
return custom_uv0;
}
// The input coordinate in 0..1 across the effect's area.
fn normalized_input_coordinate() -> vec2<f32> {
return (custom_uv0 - custom_rect.xy) / (custom_rect.zw - custom_rect.xy);
}
// The pixel's position in the effect's area, in pixels.
fn image_position() -> vec2<f32> {
return custom_image_pos.xy;
}
fn sample_input(uv: vec2<f32>) -> vec4<f32> {
return textureSampleLevel(paint_texture, paint_sampler, uv, 0.0);
}
fn get_input() -> vec4<f32> {
return sample_input(custom_uv0);
}
// Samples the input `offset` pixels away, clamped to the effect's area.
fn sample_input_at_offset(offset: vec2<f32>) -> vec4<f32> {
let uv = custom_uv0 + offset * custom_image_pos.zw;
return sample_input(clamp(uv, custom_rect.xy, custom_rect.zw));
}
fn sample_input_at_position(pos: vec2<f32>) -> vec4<f32> {
return sample_input_at_offset(pos - custom_image_pos.xy);
}
#endif
// ─── Fragment shader ───
//
// Each variant keeps one PAINT_* block (defining `paint` and `opacity`) and
// one EFFECT_* block that returns. The trailing `return vec4<f32>(0.0)` is
// unreachable; it keeps the function well-formed for every define set.
// EFFECT_SDF_LCD has its own dual-output `fs_main` below.
#ifndef EFFECT_SDF_LCD
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
#ifdef CUSTOM_SHADER
custom_uv0 = in.uv0;
custom_uv0_ddx = dpdx(in.uv0);
custom_uv0_ddy = dpdy(in.uv0);
#endif
#ifdef EFFECT_CUSTOM
custom_rect = in.rect;
custom_image_pos = in.image_pos;
#endif
#ifdef EFFECT_RGBA
return ps_uniforms0.values[0];
#endif
#ifdef EFFECT_MASK
return vec4<f32>(1.0);
#endif
#ifdef EFFECT_CLEAR
return vec4<f32>(0.0);
#endif
#ifdef PAINT_SOLID
let paint = in.color;
let opacity = 1.0;
#endif
// ─── Pattern paints ───
// The wrap variants (all but PAINT_PATTERN_PLAIN) read a `rect` attribute, the
// UV bounds of the pattern; fragments outside it contribute zero. REPEAT and
// MIRROR* also read `tile`, the tile origin (xy) and size (zw) for the wrap.
// GL ref: Shader.140.frag PAINT_PATTERN and its *_PATTERN subblocks.
#ifdef PAINT_PATTERN_PLAIN
// The sampler handles wrap and filtering.
let paint = sample_pattern_grad(in.uv0, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef CLAMP_PATTERN
// Masking to `rect` in-shader lets Noesis atlas several patterns in one
// texture, which a sampler wrap mode can't do.
let clamped_uv = clamp(in.uv0, in.rect.xy, in.rect.zw);
let inside = select(0.0, 1.0, all(in.uv0 == clamped_uv));
let paint = inside * sample_pattern_grad(in.uv0, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef REPEAT_PATTERN
// `tile` = (origin.xy, size.zw). Normalise uv into tile-local space,
// `fract` to wrap, then lift back into pattern UV space and clamp by
// `rect`. Sampling with uv0's derivatives keeps the mip choice right
// across the wrap's jumps.
let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
let wrap = fract(raw);
let uv = wrap * in.tile.zw + in.tile.xy;
let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
let inside = select(0.0, 1.0, all(uv == clamped_uv));
let paint = inside * sample_pattern_grad(uv, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef MIRRORU_PATTERN
// Triangle-wave on U, plain fract on V. `abs(x - 2*floor((x-1)/2) - 2)`
// is the SDK's branchless triangle-wave of period 2 in [0,2]; clamped
// back into tile space below.
let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
let wrap = vec2<f32>(
abs(raw.x - 2.0 * floor((raw.x - 1.0) * 0.5) - 2.0),
fract(raw.y),
);
let uv = wrap * in.tile.zw + in.tile.xy;
let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
let inside = select(0.0, 1.0, all(uv == clamped_uv));
let paint = inside * sample_pattern_grad(uv, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef MIRRORV_PATTERN
// Mirror V only.
let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
let wrap = vec2<f32>(
fract(raw.x),
abs(raw.y - 2.0 * floor((raw.y - 1.0) * 0.5) - 2.0),
);
let uv = wrap * in.tile.zw + in.tile.xy;
let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
let inside = select(0.0, 1.0, all(uv == clamped_uv));
let paint = inside * sample_pattern_grad(uv, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef MIRROR_PATTERN
// Mirror both axes.
let raw = (in.uv0 - in.tile.xy) / in.tile.zw;
let wrap = abs(raw - 2.0 * floor((raw - vec2<f32>(1.0)) * 0.5) - vec2<f32>(2.0));
let uv = wrap * in.tile.zw + in.tile.xy;
let clamped_uv = clamp(uv, in.rect.xy, in.rect.zw);
let inside = select(0.0, 1.0, all(uv == clamped_uv));
let paint = inside * sample_pattern_grad(uv, dpdx(in.uv0), dpdy(in.uv0));
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef CUSTOM_PATTERN
// The brush shader stands in for the pattern fetch (GL ref CUSTOM_PATTERN).
let paint = main_brush(in.uv0);
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef PAINT_LINEAR
// Noesis bakes per-gradient ramps into a 2D texture atlas, one row
// per gradient. `uv0.x` is the gradient parameter (0..1 along the
// gradient axis); `uv0.y` picks the atlas row. Matches the GL 140
// reference: `vec4 paint = texture(ramps, uv0); opacity_ = cbuffer0_ps[0];`
let paint = textureSample(paint_texture, paint_sampler, in.uv0);
let opacity = ps_uniforms0.values[0].x;
#endif
#ifdef PAINT_RADIAL
// GL ref Shader.140.frag PAINT_RADIAL. cbuffer0_ps layout
// (values[0] = cb[0..3], values[1] = cb[4..7]):
// cb[0..2]: coefficients for the gradient parameter `u`
// cb[3]: opacity
// cb[4..5]: focal-offset `dd` coefficients
// cb[6]: ramp atlas row
// uv0 is in focal-relative gradient space.
let cb0 = ps_uniforms0.values[0];
let cb1 = ps_uniforms0.values[1];
let dd = cb1.x * in.uv0.x - cb1.y * in.uv0.y;
let r = sqrt(in.uv0.x * in.uv0.x + in.uv0.y * in.uv0.y - dd * dd);
let u = cb0.x * in.uv0.x + cb0.y * in.uv0.y + cb0.z * r;
let paint = textureSample(paint_texture, paint_sampler, vec2<f32>(u, cb1.z));
let opacity = cb0.w;
#endif
#ifdef EFFECT_PATH
return opacity * paint;
#endif
#ifdef EFFECT_PATH_AA
return (opacity * in.coverage) * paint;
#endif
#ifdef EFFECT_OPACITY
// GL ref Shader.140.frag EFFECT_OPACITY:
// fragColor = texture(image, uv1) * (opacity_ * paint.a)
// Color comes from the layer; the paint only contributes its alpha
// (an opacity mask or a solid opacity).
return textureSample(image_texture, image_sampler, in.uv1)
* (opacity * paint.a);
#endif
#ifdef EFFECT_SHADOW
// GL ref Shader.140.frag EFFECT_SHADOW:
// shadowColor = cbuffer1_ps[0..3]
// offset = (cbuffer1_ps[4], -cbuffer1_ps[5])
// uv = clamp(uv1 - offset, rect.xy, rect.zw)
// alpha = mix(image(uv).a, shadow(uv).a, cbuffer1_ps[6])
// img = image(clamp(uv1, rect.xy, rect.zw))
// fragColor = (img + (1 - img.a) * (shadowColor * alpha)) * (opacity * paint.a)
// The shadow composites under the layer (premultiplied `1 - img.a`).
let shadow_color = ps_uniforms1.values[0];
let shadow_offset = vec2<f32>(ps_uniforms1.values[1].x, -ps_uniforms1.values[1].y);
let shadow_uv = clamp(in.uv1 - shadow_offset, in.rect.xy, in.rect.zw);
let shadow_alpha = mix(
textureSample(image_texture, image_sampler, shadow_uv).a,
textureSample(shadow_texture, shadow_sampler, shadow_uv).a,
ps_uniforms1.values[1].z,
);
let shadow_img = textureSample(
image_texture, image_sampler, clamp(in.uv1, in.rect.xy, in.rect.zw));
return (shadow_img + (1.0 - shadow_img.a) * (shadow_color * shadow_alpha))
* (opacity * paint.a);
#endif
#ifdef EFFECT_BLUR
// GL ref Shader.140.frag EFFECT_BLUR:
// fragColor = mix(image(uv1), shadow(uv1), cbuffer1_ps[0]) * (opacity * paint.a)
// cbuffer1_ps[0] crossfades from the unblurred layer (0) to the blur (1).
return mix(
textureSample(image_texture, image_sampler, in.uv1),
textureSample(shadow_texture, shadow_sampler, in.uv1),
ps_uniforms1.values[0].x,
) * (opacity * paint.a);
#endif
#ifdef EFFECT_DOWNSAMPLE
// GL ref Shader.140.frag EFFECT_DOWNSAMPLE: box filter of four taps of
// the group(2) source at the VS-computed UVs.
return (textureSample(paint_texture, paint_sampler, in.uv0)
+ textureSample(paint_texture, paint_sampler, in.uv1)
+ textureSample(paint_texture, paint_sampler, in.uv2)
+ textureSample(paint_texture, paint_sampler, in.uv3))
* 0.25;
#endif
#ifdef EFFECT_UPSAMPLE
// GL ref Shader.140.frag EFFECT_UPSAMPLE:
// mix(texture(image, uv1), texture(pattern, uv0), color.a)
// `image` (group 3) is the lower-resolution pass, group(2) the
// same-resolution source.
return mix(
textureSample(image_texture, image_sampler, in.uv1),
textureSample(paint_texture, paint_sampler, in.uv0),
in.color.a,
);
#endif
#ifdef EFFECT_SDF
// GL ref Shader.140.frag EFFECT_SDF:
// distance = SDF_SCALE * (texture(glyphs, uv1).r - SDF_BIAS)
// gradLen = length(dFdx(st1))
// scale = 1 / gradLen
// base = SDF_BASE_DEV * (1 - (clamp(scale, MIN, MAX) - MIN) / (MAX - MIN))
// range = SDF_AA_FACTOR * gradLen
// alpha = smoothstep(base - range, base + range, distance)
// fragColor = (alpha * opacity) * paint
let SDF_SCALE: f32 = 7.96875;
let SDF_BIAS: f32 = 0.50196078431;
let SDF_AA_FACTOR: f32 = 0.65;
let SDF_BASE_MIN: f32 = 0.125;
let SDF_BASE_MAX: f32 = 0.25;
let SDF_BASE_DEV: f32 = -0.65;
let glyph = textureSample(glyphs_texture, glyphs_sampler, in.uv1).r;
let distance = SDF_SCALE * (glyph - SDF_BIAS);
let gradLen = length(dpdx(in.st1));
let scale = 1.0 / gradLen;
let base = SDF_BASE_DEV
* (1.0 - (clamp(scale, SDF_BASE_MIN, SDF_BASE_MAX) - SDF_BASE_MIN)
/ (SDF_BASE_MAX - SDF_BASE_MIN));
let range = SDF_AA_FACTOR * gradLen;
let alpha = smoothstep(base - range, base + range, distance);
return (alpha * opacity) * paint;
#endif
#ifdef EFFECT_CUSTOM
// VK ref Shader.frag EFFECT_CUSTOM: main_effect() * (opacity_ * paint.a)
return main_effect() * (opacity * paint.a);
#endif
return vec4<f32>(0.0);
}
#endif
// ─── EFFECT_SDF_LCD: subpixel text via dual-source blending ───
//
// The SDK has no GL/VK reference for this path (those devices report no
// subpixel rendering). This is the usual single-channel SDF LCD technique:
// sample the distance field at three points a third of a screen pixel apart
// horizontally, and use each one's SDF_SOLID-style coverage for one of R/G/B.
//
// blend_src(0) is paint premultiplied by per-channel coverage; blend_src(1) is
// the coverage. The SrcOver_Dual blend (`cs + cd*(1 - src1)`) then gives
// `paint*cov + dst*(1 - cov)` per channel.
#ifdef EFFECT_SDF_LCD
struct FsLcdOut {
@location(0) @blend_src(0) color: vec4<f32>,
@location(0) @blend_src(1) coverage: vec4<f32>,
}
@fragment
fn fs_main(in: VsOut) -> FsLcdOut {
let paint = in.color;
let SDF_SCALE: f32 = 7.96875;
let SDF_BIAS: f32 = 0.50196078431;
let SDF_AA_FACTOR: f32 = 0.65;
let SDF_BASE_MIN: f32 = 0.125;
let SDF_BASE_MAX: f32 = 0.25;
let SDF_BASE_DEV: f32 = -0.65;
// AA window sizing, as in EFFECT_SDF.
let gradLen = length(dpdx(in.st1));
let scale = 1.0 / gradLen;
let base = SDF_BASE_DEV
* (1.0 - (clamp(scale, SDF_BASE_MIN, SDF_BASE_MAX) - SDF_BASE_MIN)
/ (SDF_BASE_MAX - SDF_BASE_MIN));
let range = SDF_AA_FACTOR * gradLen;
// One-third-of-a-pixel horizontal stagger in glyph-atlas UV space. dpdx(uv1)
// is the UV change per screen pixel in x; the R/G/B stripes sit at -1/3, 0,
// +1/3 pixel.
let duv = dpdx(in.uv1) * (1.0 / 3.0);
let dr = SDF_SCALE * (textureSample(glyphs_texture, glyphs_sampler, in.uv1 - duv).r - SDF_BIAS);
let dg = SDF_SCALE * (textureSample(glyphs_texture, glyphs_sampler, in.uv1).r - SDF_BIAS);
let db = SDF_SCALE * (textureSample(glyphs_texture, glyphs_sampler, in.uv1 + duv).r - SDF_BIAS);
let cov = vec3<f32>(
smoothstep(base - range, base + range, dr),
smoothstep(base - range, base + range, dg),
smoothstep(base - range, base + range, db),
);
let cov_a = (cov.r + cov.g + cov.b) * (1.0 / 3.0);
var out: FsLcdOut;
// Premultiplied so the dual blend's `One` src factor leaves it untouched.
out.color = vec4<f32>(paint.rgb * cov, paint.a * cov_a);
out.coverage = vec4<f32>(cov * paint.a, cov_a * paint.a);
return out;
}
#endif