Skip to content

Commit

Permalink
Fix blurry text (#1494)
Browse files Browse the repository at this point in the history
* correctly include glyph padding

* let the user set glyph resolution

* fix glyph scale and offset in scatter

* fix glyph scaling in WGLMakie

* include short_test_13 (char scatter)

* include short_test13 in CairoMakie (char marker)
  • Loading branch information
ffreyer authored Dec 6, 2021
1 parent cb9087e commit f233cda
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 45 deletions.
2 changes: 1 addition & 1 deletion CairoMakie/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ excludes = Set([
"Depth Shift",
"Order Independent Transparency"
])
excludes2 = Set(["short_tests_90", "short_tests_111", "short_tests_35", "short_tests_13", "short_tests_3"])
excludes2 = Set(["short_tests_90", "short_tests_111", "short_tests_35", "short_tests_3"])

functions = [:volume, :volume!, :uv_mesh]
database = database_filtered(excludes, excludes2, functions=functions)
Expand Down
6 changes: 4 additions & 2 deletions GLMakie/assets/shader/distance_shape.frag
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ flat in vec4 f_uv_texture_bbox;
// These versions of aastep assume that `dist` is a signed distance function
// which has been scaled to be in units of pixels.
float aastep(float threshold1, float dist) {
return min(1.0, f_viewport_from_u_scale)*smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist);
return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist);
}
float aastep(float threshold1, float threshold2, float dist) {
return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist) -
Expand Down Expand Up @@ -154,10 +154,12 @@ void main(){
// But note that this may interfere with object picking.
//if (final_color == f_bg_color)
// discard;

write2framebuffer(final_color, f_id);

// Debug tools:
// * Show the background of the sprite.
// write2framebuffer(mix(final_color, vec4(1,0,0,1), 0.2), f_id);
// write2framebuffer(mix(final_color, vec4(1,0,0,1), 0.2), f_id);
// * Show the antialiasing border around glyphs
// write2framebuffer(vec4(vec3(abs(signed_distance)),1), f_id);
}
59 changes: 51 additions & 8 deletions GLMakie/src/GLVisualize/visualize/particles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,35 @@ function _default(
sprites(p, s, data)
end


# To map (scale, scale_x, scale_y, scale_z) -> scale
combine_scales(scale, x::Nothing, y::Nothing, z::Nothing) = scale
combine_scales(s::Nothing, x, y, z::Nothing) = Vec2f.(x, y)
combine_scales(s::Nothing, x, y, z) = Vec3f.(x, y, z)

function char_scale_factor(char, font)
# uv * size(ta.data) / Makie.PIXELSIZE_IN_ATLAS[] is the padded glyph size
# normalized to the size the glyph was generated as.
ta = Makie.get_texture_atlas()
lbrt = glyph_uv_width!(ta, char, font)
width = Vec(lbrt[3] - lbrt[1], lbrt[4] - lbrt[2])
width * Vec2f(size(ta.data)) / Makie.PIXELSIZE_IN_ATLAS[]
end

# This works the same for x being widths and offsets
rescale_glyph(char::Char, font, x) = x * char_scale_factor(char, font)
function rescale_glyph(char::Char, font, xs::Vector)
f = char_scale_factor(char, font)
map(xs -> f * x, xs)
end
function rescale_glyph(str::String, font, x)
[x * char_scale_factor(char, font) for char in collect(str)]
end
function rescale_glyph(str::String, font, xs::Vector)
map((char, x) -> x * char_scale_factor(char, font), collect(str), xs)
end


"""
Main assemble functions for sprite particles.
Sprites are anything like distance fields, images and simple geometries
Expand All @@ -281,6 +310,23 @@ function sprites(p, s, data)
rot = vec2quaternion(rot)
delete!(data, :rotation)

# Rescale to include glyph padding and shape
if isa(to_value(p[1]), Char)
scale = map(combine_scales,
pop!(data, :scale, Observable(nothing)),
pop!(data, :scale_x, Observable(nothing)),
pop!(data, :scale_y, Observable(nothing)),
pop!(data, :scale_z, Observable(nothing))
)
font = get(data, :font, Observable(Makie.defaultfont()))
offset = get(data, :offset, Observable(Vec2f(0)))

# The same scaling that needs to be applied to scale also needs to apply
# to offset.
data[:offset] = map(rescale_glyph, p[1], font, offset)
data[:scale] = map(rescale_glyph, p[1], font, scale)
end

@gen_defaults! data begin
shape = const_lift(x-> Int32(primitive_shape(x)), p[1])
position = p[2] => GLBuffer
Expand All @@ -296,10 +342,6 @@ function sprites(p, s, data)
rotation = rot => GLBuffer
image = nothing => Texture
end
# TODO don't make this dependant on some shady type dispatch
if isa(to_value(p[1]), Char) && !isa(to_value(scale), Union{StaticVector, AbstractVector{<: StaticVector}}) # correct dimensions
data[:scale] = const_lift(correct_scale, p[1], scale)
end

@gen_defaults! data begin
offset = primitive_offset(p[1], scale) => GLBuffer
Expand Down Expand Up @@ -365,7 +407,6 @@ function _default(main::TOrSignal{S}, s::Style, data::Dict) where S <: AbstractS
font = to_font("default")
scale_primitive = true
position = const_lift(calc_position, main, start_position, relative_scale, font, atlas)
offset = const_lift(calc_offset, main, relative_scale, font, atlas)
prerender = () -> begin
glDisable(GL_DEPTH_TEST)
glDepthMask(GL_TRUE)
Expand All @@ -375,10 +416,12 @@ function _default(main::TOrSignal{S}, s::Style, data::Dict) where S <: AbstractS
uv_offset_width = const_lift(main) do str
Vec4f[glyph_uv_width!(atlas, c, font) for c = str]
end
scale = const_lift(main, relative_scale) do str, s
Vec2f[glyph_scale!(atlas, c, font, s) for c = str]
end
end

# Rescale to include glyph padding and shape
data[:offset] = map(rescale_glyph, main, data[:font], data[:offset])
data[:scale] = map(rescale_glyph, main, data[:font], data[:scale])

delete!(data, :font)
_default((DISTANCEFIELD, position), s, data)
end
1 change: 0 additions & 1 deletion GLMakie/src/gl_backend.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ using .GLAbstraction
const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}()

function get_texture!(atlas)
Makie.set_glyph_resolution!(Makie.High)
# clean up dead context!
filter!(atlas_texture_cache) do (ctx, tex_func)
if GLAbstraction.context_alive(ctx)
Expand Down
32 changes: 30 additions & 2 deletions WGLMakie/src/particles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,45 @@ primitive_shape(::Type{<:Rect2}) = Cint(RECTANGLE)
primitive_shape(::Type{T}) where {T} = error("Type $(T) not supported")
primitive_shape(x::Shape) = Cint(x)

function char_scale_factor(char, font)
# uv * size(ta.data) / Makie.PIXELSIZE_IN_ATLAS[] is the padded glyph size
# normalized to the size the glyph was generated as.
ta = Makie.get_texture_atlas()
lbrt = glyph_uv_width!(ta, char, font)
width = Vec(lbrt[3] - lbrt[1], lbrt[4] - lbrt[2])
width * Vec2f(size(ta.data)) / Makie.PIXELSIZE_IN_ATLAS[]
end

# This works the same for x being widths and offsets
rescale_glyph(char::Char, font, x) = x * char_scale_factor(char, font)
function rescale_glyph(char::Char, font, xs::Vector)
f = char_scale_factor(char, font)
map(xs -> f * x, xs)
end
function rescale_glyph(str::String, font, x)
[x * char_scale_factor(char, font) for char in collect(str)]
end
function rescale_glyph(str::String, font, xs::Vector)
map((char, x) -> x * char_scale_factor(char, font), collect(str), xs)
end

using Makie: to_spritemarker

function scatter_shader(scene::Scene, attributes)
# Potentially per instance attributes
per_instance_keys = (:offset, :rotations, :markersize, :color, :intensity,
:uv_offset_width, :marker_offset)
uniform_dict = Dict{Symbol,Any}()

if haskey(attributes, :marker) && attributes[:marker][] isa Union{Char, Vector{Char},String}
font = get(attributes, :font, Observable(Makie.defaultfont()))
attributes[:markersize] = map(rescale_glyph, attributes[:marker], font, attributes[:markersize])
attributes[:marker_offset] = map(rescale_glyph, attributes[:marker], font, attributes[:marker_offset])
end

if haskey(attributes, :marker) && attributes[:marker][] isa Union{Vector{Char},String}
x = pop!(attributes, :marker)
attributes[:uv_offset_width] = lift(x -> Makie.glyph_uv_width!.(collect(x)),
x)
attributes[:uv_offset_width] = lift(x -> Makie.glyph_uv_width!.(collect(x)), x)
uniform_dict[:shape_type] = Cint(3)
end

Expand Down
2 changes: 1 addition & 1 deletion WGLMakie/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ excludes = Set([
"Record Video"
])

excludes2 = Set(["short_tests_83", "short_tests_78", "short_tests_40", "short_tests_13", "short_tests_5", "short_tests_41"])
excludes2 = Set(["short_tests_83", "short_tests_78", "short_tests_40", "short_tests_5", "short_tests_41"])
database = database_filtered(excludes, excludes2)

recorded = joinpath(@__DIR__, "recorded")
Expand Down
11 changes: 7 additions & 4 deletions src/layouting/layouting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,14 @@ function text_quads(positions, glyphs::AbstractVector, fonts::AbstractVector, te
offsets = Vec2f[]
uv = Vec4f[]
scales = Vec2f[]
pad = GLYPH_PADDING[] / PIXELSIZE_IN_ATLAS[]
broadcast_foreach(positions, glyphs, fonts, textsizes) do offs, c, font, pixelsize
# for (c, font, pixelsize) in zipx(glyphs, fonts, textsizes)
push!(uv, glyph_uv_width!(atlas, c, font))
glyph_bb, extent = FreeTypeAbstraction.metrics_bb(c, font, pixelsize)
push!(scales, widths(glyph_bb))
push!(offsets, minimum(glyph_bb))

push!(scales, widths(glyph_bb) .+ pixelsize * 2pad)
push!(offsets, minimum(glyph_bb) .- pixelsize * pad)
end
return positions, offsets, uv, scales
end
Expand All @@ -326,13 +328,14 @@ function text_quads(positions, glyphs, fonts, textsizes::Vector{<:ScalarOrVector
offsets = Vec2f[]
uv = Vec4f[]
scales = Vec2f[]
pad = GLYPH_PADDING[] / PIXELSIZE_IN_ATLAS[]

broadcast_foreach(positions, glyphs, fonts, textsizes) do positions, glyphs, fonts, textsizes
broadcast_foreach(positions, glyphs, fonts, textsizes) do offs, c, font, pixelsize
push!(uv, glyph_uv_width!(atlas, c, font))
glyph_bb, extent = FreeTypeAbstraction.metrics_bb(c, font, pixelsize)
push!(scales, widths(glyph_bb))
push!(offsets, minimum(glyph_bb))
push!(scales, widths(glyph_bb) .+ pixelsize * 2pad)
push!(offsets, minimum(glyph_bb) .- pixelsize * pad)
end
end

Expand Down
65 changes: 39 additions & 26 deletions src/utilities/texture_atlas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,29 @@ Base.size(atlas::TextureAtlas) = size(atlas.data)

@enum GlyphResolution High Low

const TEXTURE_RESOLUTION = Ref((2048, 2048))
const CACHE_RESOLUTION_PREFIX = Ref("high")

const HIGH_PIXELSIZE = 64
const LOW_PIXELSIZE = 32

const CACHE_RESOLUTION_PREFIX = Ref("high")
const TEXTURE_RESOLUTION = Ref((2048, 2048))
const PIXELSIZE_IN_ATLAS = Ref(HIGH_PIXELSIZE)
# Small padding results in artifacts during downsampling. It seems like roughly
# 1.5px padding is required for a scaled glyph to be displayed without artifacts.
# E.g. for textsize = 8px we need 1.5/8 * 64 = 12px+ padding (in each direction)
# for things to look clear with a 64px glyph size.
const GLYPH_PADDING = Ref(12)

function set_glyph_resolution!(res::GlyphResolution)
if res == High
TEXTURE_RESOLUTION[] = (2048, 2048)
CACHE_RESOLUTION_PREFIX[] = "high"
PIXELSIZE_IN_ATLAS[] = HIGH_PIXELSIZE
GLYPH_PADDING[] = 12
else
TEXTURE_RESOLUTION[] = (1024, 1024)
CACHE_RESOLUTION_PREFIX[] = "low"
PIXELSIZE_IN_ATLAS[] = LOW_PIXELSIZE
GLYPH_PADDING[] = 6
end
end

Expand Down Expand Up @@ -176,37 +182,44 @@ function glyph_uv_width!(c::Char)
return glyph_uv_width!(get_texture_atlas(), c, defaultfont())
end

function glyph_boundingbox(c::Char, font::NativeFont, pixelsize)
if FT_Get_Char_Index(font, c) == 0
for afont in alternativefonts()
if FT_Get_Char_Index(afont, c) != 0
font = afont
break
end
end
end
bb, ext = FreeTypeAbstraction.metrics_bb(c, font, pixelsize)
return bb
end
# function glyph_boundingbox(c::Char, font::NativeFont, pixelsize)
# if FT_Get_Char_Index(font, c) == 0
# for afont in alternativefonts()
# if FT_Get_Char_Index(afont, c) != 0
# font = afont
# break
# end
# end
# end
# bb, ext = FreeTypeAbstraction.metrics_bb(c, font, pixelsize)
# return bb
# end

function insert_glyph!(atlas::TextureAtlas, glyph::Char, font::NativeFont)
return get!(atlas.mapping, (glyph, FreeTypeAbstraction.fontname(font))) do
downsample = 5 # render font 5x larger, and then downsample back to desired pixelsize
pad = 8 # padd rendered font by 6 pixel in each direction
# We save glyphs as signed distance fields, i.e. we save the distance
# a pixel is away from the edge of a symbol (continuous at the edge).
# To get accurate distances we want to draw the symbol at high
# resolution and then downsample to the PIXELSIZE_IN_ATLAS.
downsample = 5
# To draw a symbol from a sdf we essentially do `color * (sdf > 0)`. For
# antialiasing we smooth out the step function `sdf > 0`. That means we
# need a few values outside the symbol. To guarantee that we have those
# at all relevant scales we add padding to the rendered bitmap and the
# resulting sdf.
pad = GLYPH_PADDING[]

uv_pixel = render(atlas, glyph, font, downsample, pad)
tex_size = Vec2f(size(atlas.data) .- 1) # starts at 1

idx_left_bottom = minimum(uv_pixel)# 0 based!!!
# 0 based
idx_left_bottom = minimum(uv_pixel)
idx_right_top = maximum(uv_pixel)

# include padding
left_bottom_pad = idx_left_bottom .+ pad .- 1
# -1 for indexing offset
right_top_pad = idx_right_top .- pad


# transform to normalized texture coordinates
uv_left_bottom_pad = (left_bottom_pad) ./ tex_size
uv_right_top_pad = (right_top_pad) ./ tex_size
# -1 for indexing offset
uv_left_bottom_pad = (idx_left_bottom) ./ tex_size
uv_right_top_pad = (idx_right_top .- 1) ./ tex_size

uv_offset_rect = Vec4f(uv_left_bottom_pad..., uv_right_top_pad...)
i = atlas.index
Expand Down

0 comments on commit f233cda

Please sign in to comment.