From 153997e6bad7d9752a84eebe43df11bd1de50697 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Fri, 9 Aug 2024 14:42:16 +0200 Subject: [PATCH] Add `uv_transform` attribute to mesh, meshscatter, surface and image (#1406) * replay changes from master (add per-instance uv_scale) * define uv_transform instead of just scale * fix shader * add uv_transform attribute * add UVTransform and conversions * merge uv_scale into uv_transform * disable in WGLMakie * extend usage to mesh and surface * export UVTransform * update changelog * prototype Mat transforms * fix order of matrix dimensions * use only one buffer * update other shaders * update defaults * update image * switch to master defaults * fix patterns * add more named transforms * apply suggestions + cleanup * add symbol -> plot defaults * add tests for conversions * fix tests * drop support for rotations * add refimg * allow rotation in uv_transform() function only * document uv_transform() function * update attribute docs * update changelog * fix rotation direction of surface * update test * add uv_transforms to CairoMakie * fix tests * restore comment * actually use convert_arguments on uv_transform * get mesh, surface, image working in WGLMakie * fix heatmap * add textures to meshscatter with static uv_transform * implement per-instance uv_transform * add test for per-element uv_transform in meshscatter * allow operation chaining in uv_transform * fix chaining & add test * add docs example * update changelog * sample color in vertex shader for particles --------- Co-authored-by: Simon --- CHANGELOG.md | 2 + CairoMakie/src/cairo-extension.jl | 10 +++ CairoMakie/src/primitives.jl | 60 ++++++++----- CairoMakie/src/utils.jl | 5 +- GLMakie/assets/shader/mesh.frag | 17 +++- GLMakie/assets/shader/mesh.vert | 10 ++- GLMakie/assets/shader/particles.vert | 22 ++++- GLMakie/assets/shader/surface.vert | 10 ++- GLMakie/src/GLAbstraction/GLUniforms.jl | 4 +- GLMakie/src/drawing_primitives.jl | 12 ++- GLMakie/src/glshaders/mesh.jl | 2 +- GLMakie/src/glshaders/particles.jl | 20 ++++- GLMakie/src/glshaders/surface.jl | 2 +- MakieCore/src/basic_plots.jl | 35 ++++++++ ReferenceTests/src/tests/primitives.jl | 55 ++++++++++++ WGLMakie/assets/mesh.frag | 1 - WGLMakie/assets/mesh.vert | 7 +- WGLMakie/assets/particles.frag | 68 --------------- WGLMakie/assets/particles.vert | 59 ++++++++++++- WGLMakie/src/imagelike.jl | 26 ++++++ WGLMakie/src/meshes.jl | 12 ++- WGLMakie/src/particles.jl | 34 +++++++- docs/src/reference/plots/meshscatter.md | 28 ++++++ src/conversions.jl | 110 ++++++++++++++++++++++++ src/utilities/utilities.jl | 3 +- test/convert_attributes.jl | 48 +++++++++++ test/runtests.jl | 1 + 27 files changed, 540 insertions(+), 123 deletions(-) delete mode 100644 WGLMakie/assets/particles.frag create mode 100644 test/convert_attributes.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f7595744c..56605a840de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Added the `uv_transform` attribute for meshscatter, mesh, surface and image [#1406](https://github.com/MakieOrg/Makie.jl/pull/1406). +- Added the ability to use textures with `meshscatter` in WGLMakie [#1406](https://github.com/MakieOrg/Makie.jl/pull/1406). - Don't remove underlying VideoStream file when doing save() [#3883](https://github.com/MakieOrg/Makie.jl/pull/3883). - Fix label/legend for plotlist [#4079](https://github.com/MakieOrg/Makie.jl/pull/4079). - Fix wrong order for colors in RPRMakie [#4098](https://github.com/MakieOrg/Makie.jl/pull/4098). diff --git a/CairoMakie/src/cairo-extension.jl b/CairoMakie/src/cairo-extension.jl index 36b7e3f496a..34e620f7e2c 100644 --- a/CairoMakie/src/cairo-extension.jl +++ b/CairoMakie/src/cairo-extension.jl @@ -10,6 +10,16 @@ function get_font_matrix(ctx) return matrix end +function pattern_set_matrix(ctx, matrix) + ccall((:cairo_pattern_set_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) +end + +function pattern_get_matrix(ctx) + matrix = Cairo.CairoMatrix() + ccall((:cairo_pattern_get_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) + return matrix +end + function cairo_font_face_destroy(font_face) ccall( (:cairo_font_face_destroy, Cairo.libcairo), diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 5955f36d965..0f7ce2e02d0 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -821,17 +821,7 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: t = Makie.transform_func(primitive) identity_transform = (t === identity || t isa Tuple && all(x-> x === identity, t)) && (abs(model[1, 2]) < 1e-15) regular_grid = xs isa AbstractRange && ys isa AbstractRange - xy_aligned = let - # Only allow scaling and translation - pv = scene.camera.projectionview[] - M = Mat4f( - pv[1, 1], 0.0, 0.0, 0.0, - 0.0, pv[2, 2], 0.0, 0.0, - 0.0, 0.0, pv[3, 3], 0.0, - pv[1, 4], pv[2, 4], pv[3, 4], 1.0 - ) - pv ≈ M - end + xy_aligned = Makie.is_translation_scale_matrix(scene.camera.projectionview[]) if interpolate if !regular_grid @@ -850,6 +840,21 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: xymax = project_position(primitive, space, Point2(last.(imsize)), model) w, h = xymax .- xy + uv_transform = if primitive isa Image + val = to_value(get(primitive, :uv_transform, I)) + T = Makie.convert_attribute(val, Makie.key"uv_transform"(), Makie.key"image"()) + # Cairo uses pixel units so we need to transform those to a 0..1 range, + # then apply uv_transform, then scale them back to pixel units. + # Cairo also doesn't have the yflip we have in OpenGL, so we need to + # invert y. + T3 = Mat3f(T[1], T[2], 0, T[3], T[4], 0, T[5], T[6], 1) + T3 = Makie.uv_transform(Vec2f(size(image))) * T3 * + Makie.uv_transform(Vec2f(0, 1), 1f0 ./ Vec2f(size(image, 1), -size(image, 2))) + T3[Vec(1, 2), Vec(1,2,3)] + else + Mat{2, 3, Float32}(1,0,0,1,0,0) + end + can_use_fast_path = !(is_vector && !interpolate) && regular_grid && identity_transform && (interpolate || xy_aligned) use_fast_path = can_use_fast_path && !disable_fast_path @@ -874,8 +879,10 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: end filt = interpolate ? Cairo.FILTER_BILINEAR : Cairo.FILTER_NEAREST Cairo.pattern_set_filter(p, filt) + pattern_set_matrix(p, Cairo.CairoMatrix(uv_transform...)) Cairo.fill(ctx) Cairo.restore(ctx) + pattern_set_matrix(p, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0)) else # find projected image corners # this already takes care of flipping the image to correct cairo orientation @@ -940,7 +947,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki if !haskey(primitive, :faceculling) primitive[:faceculling] = Observable(-10) end - draw_mesh3D(scene, screen, primitive, mesh) + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"mesh"()) + draw_mesh3D(scene, screen, primitive, mesh; uv_transform = uv_transform) end return nothing end @@ -952,6 +960,11 @@ function draw_mesh2D(scene, screen, @nospecialize(plot), @nospecialize(mesh)) vs = project_position(scene, transform_func, space, decompose(Point, mesh), model) fs = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} uv = decompose_uv(mesh)::Union{Nothing, Vector{Vec2f}} + # Note: This assume the function is only called from mesh plots + uv_transform = Makie.convert_attribute(plot[:uv_transform][], Makie.key"uv_transform"(), Makie.key"mesh"()) + if uv isa Vector{Vec2f} && to_value(uv_transform) !== nothing + uv = map(uv -> uv_transform * to_ndim(Vec3f, uv, 1), uv) + end color = hasproperty(mesh, :color) ? to_color(mesh.color) : plot.calculated_colors[] cols = per_face_colors(color, nothing, fs, nothing, uv) return draw_mesh2D(screen, cols, vs, fs) @@ -1000,7 +1013,10 @@ end nan2zero(x) = !isnan(x) * x -function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I)) +function draw_mesh3D( + scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I), + uv_transform = Mat{2, 3, Float32}(1,0,0,1,0,0) + ) @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling)) matcap = to_value(get(attributes, :matcap, nothing)) @@ -1009,9 +1025,12 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f meshnormals = decompose_normals(mesh)::Vector{Vec3f} # note: can be made NaN-aware. meshuvs = texturecoordinates(mesh)::Union{Nothing, Vector{Vec2f}} + if meshuvs isa Vector{Vec2f} && to_value(uv_transform) !== nothing + meshuvs = map(uv -> uv_transform * to_ndim(Vec3f, uv, 1), meshuvs) + end + # Priorize colors of the mesh if present color = hasproperty(mesh, :color) ? mesh.color : to_value(attributes.calculated_colors) - per_face_col = per_face_colors(color, matcap, meshfaces, meshnormals, meshuvs) model = attributes.model[]::Mat4d @@ -1197,7 +1216,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki if !haskey(primitive, :faceculling) primitive[:faceculling] = Observable(-10) end - draw_mesh3D(scene, screen, primitive, mesh) + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"surface"()) + draw_mesh3D(scene, screen, primitive, mesh; uv_transform = uv_transform) primitive[:color] = old return nothing end @@ -1235,22 +1255,20 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki submesh[:model] = model scales = primitive[:markersize][] + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"meshscatter"()) for i in zorder p = pos[i] if color isa AbstractVector submesh[:calculated_colors] = color[i] end scale = markersize isa Vector ? markersize[i] : markersize - _rotation = if rotation isa Vector - Makie.rotationmatrix4(to_rotation(rotation[i])) - else - Makie.rotationmatrix4(to_rotation(rotation)) - end + _rotation = Makie.rotationmatrix4(to_rotation(Makie.sv_getindex(rotation, i))) + _uv_transform = Makie.sv_getindex(uv_transform, i) draw_mesh3D( scene, screen, submesh, marker, pos = p, scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), - rotation = _rotation + rotation = _rotation, uv_transform = _uv_transform ) end diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 859edc5009a..f429163aea9 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -261,11 +261,12 @@ function per_face_colors(_color, matcap, faces, normals, uv) # let next level extend and fill with CairoPattern return color elseif color isa AbstractMatrix{<: Colorant} && !isnothing(uv) - wsize = reverse(size(color)) + wsize = size(color) wh = wsize .- 1 + # nearest cvec = map(uv) do uv x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wsize) - return color[end - (y - 1), x] + return color[x, y] end # TODO This is wrong and doesn't actually interpolate # Inside the triangle sampling the color image diff --git a/GLMakie/assets/shader/mesh.frag b/GLMakie/assets/shader/mesh.frag index 5480da20008..077a390339e 100644 --- a/GLMakie/assets/shader/mesh.frag +++ b/GLMakie/assets/shader/mesh.frag @@ -12,6 +12,7 @@ in vec3 o_view_normal; in vec4 o_color; in vec2 o_uv; flat in uvec2 o_id; +flat in int o_InstanceID; {{matcap_type}} matcap; {{image_type}} image; @@ -79,18 +80,28 @@ vec4 get_color(sampler1D color, vec2 uv, vec2 color_norm, sampler1D color_map, s } uniform bool fetch_pixel; -uniform vec2 uv_scale; +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, int i, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, int i, vec2 uv){ return transform * vec3(uv, 1); } +vec2 apply_uv_transform(samplerBuffer transforms, int index, vec2 uv){ + // can't have matrices in a texture so we have 3x vec2 instead + mat3x2 transform; + transform[0] = texelFetch(transforms, 3 * index + 0).xy; + transform[1] = texelFetch(transforms, 3 * index + 1).xy; + transform[2] = texelFetch(transforms, 3 * index + 2).xy; + return transform * vec3(uv, 1); +} vec4 get_pattern_color(sampler1D color) { int size = textureSize(color, 0); - vec2 pos = gl_FragCoord.xy * uv_scale; + vec2 pos = apply_uv_transform(uv_transform, o_InstanceID, gl_FragCoord.xy); int idx = int(mod(pos.x, size)); return texelFetch(color, idx, 0); } vec4 get_pattern_color(sampler2D color){ ivec2 size = textureSize(color, 0); - vec2 pos = gl_FragCoord.xy * uv_scale; + vec2 pos = apply_uv_transform(uv_transform, o_InstanceID, gl_FragCoord.xy); return texelFetch(color, ivec2(mod(pos.x, size.x), mod(pos.y, size.y)), 0); } diff --git a/GLMakie/assets/shader/mesh.vert b/GLMakie/assets/shader/mesh.vert index 018248c0c5c..c8cd3fe8f14 100644 --- a/GLMakie/assets/shader/mesh.vert +++ b/GLMakie/assets/shader/mesh.vert @@ -20,8 +20,9 @@ void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange); uniform uint objectid; + flat out uvec2 o_id; -uniform vec2 uv_scale; +flat out int o_InstanceID; out vec2 o_uv; out vec4 o_color; @@ -31,6 +32,10 @@ vec3 to_3d(vec3 v){return v;} vec2 to_2d(float v){return vec2(v, 0);} vec2 to_2d(vec2 v){return v;} +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, vec2 uv){ return transform * vec3(uv, 1); } + vec4 to_color(vec3 c, Nothing color_map, Nothing color_norm){ return vec4(c, 1); } @@ -59,8 +64,9 @@ void main() { o_id = uvec2(objectid, gl_VertexID+1); vec2 tex_uv = to_2d(texturecoordinates); - o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; + o_uv = apply_uv_transform(uv_transform, tex_uv); o_color = to_color(vertex_color, color_map, color_norm); + o_InstanceID = 0; vec3 v = to_3d(vertices); render(model * vec4(v, 1), normals, view, projection); } diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert index 4fc672ac4bb..fe7434408e2 100644 --- a/GLMakie/assets/shader/particles.vert +++ b/GLMakie/assets/shader/particles.vert @@ -33,6 +33,7 @@ uniform uint objectid; uniform int len; flat out uvec2 o_id; +flat out int o_InstanceID; out vec4 o_color; out vec2 o_uv; @@ -92,8 +93,22 @@ vec4 get_particle_color(sampler2D color, Nothing intensity, Nothing color_map, N void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); -vec2 get_uv(Nothing x){return vec2(0.0);} -vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, int i, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, int i, vec2 uv){ return transform * vec3(uv, 1); } +vec2 apply_uv_transform(samplerBuffer transforms, int index, vec2 uv){ + // can't have matrices in a texture so we have 3x vec2 instead + mat3x2 transform; + transform[0] = texelFetch(transforms, 3 * index + 0).xy; + transform[1] = texelFetch(transforms, 3 * index + 1).xy; + transform[2] = texelFetch(transforms, 3 * index + 2).xy; + return transform * vec3(uv, 1); +} + +vec2 get_uv(int index, Nothing uv){ return vec2(0.0); } +vec2 get_uv(int index, vec2 uv){ + return apply_uv_transform(uv_transform, index, uv); +} void main(){ int index = gl_InstanceID; @@ -105,7 +120,8 @@ void main(){ {{position_calc}} o_color = get_particle_color(color, intensity, color_map, color_norm, index, len); o_color = o_color * to_color(vertex_color); - o_uv = get_uv(texturecoordinates); + o_uv = get_uv(index, texturecoordinates); + o_InstanceID = index; rotate(rotation, index, V, N); render(model * vec4(pos + V, 1), N, view, projection); } diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index b8b16eb98d6..0a5021a0d70 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -30,7 +30,6 @@ uniform vec4 nan_color; vec4 color_lookup(float intensity, sampler1D color, vec2 norm); uniform vec3 scale; - uniform mat4 view, model, projection; // See util.vert for implementations @@ -41,6 +40,9 @@ vec2 linear_index(ivec2 dims, int index); vec2 linear_index(ivec2 dims, int index, vec2 offset); vec4 linear_texture(sampler2D tex, int index, vec2 offset); +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, vec2 uv){ return transform * vec3(uv, 1); } // Normal generation @@ -147,8 +149,8 @@ vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ } uniform uint objectid; -uniform vec2 uv_scale; flat out uvec2 o_id; +flat out int o_InstanceID; // dummy for compat with meshscatter in mesh.frag out vec4 o_color; out vec2 o_uv; @@ -162,7 +164,9 @@ void main() {{position_calc}} o_id = uvec2(objectid, index1D+1); - o_uv = index01 * uv_scale; + o_InstanceID = 0; + // match up with mesh + o_uv = apply_uv_transform(uv_transform, vec2(index01.x, 1 - index01.y)); vec3 normalvec = {{normal_calc}}; o_color = vec4(0.0); diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index a51af4024bc..3da13dec89c 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -30,7 +30,7 @@ function uniformfunc(typ::DataType, dims::Tuple{Int}) end function uniformfunc(typ::DataType, dims::Tuple{Int, Int}) M, N = dims - Symbol(string("glUniformMatrix", M == N ? "$M" : "$(M)x$(N)", opengl_postfix(typ))) + Symbol(string("glUniformMatrix", M == N ? "$M" : "$(N)x$(M)", opengl_postfix(typ))) end gluniform(location::Integer, x::Nothing) = nothing @@ -105,7 +105,7 @@ end function glsl_typename(t::Type{T}) where T <: Mat M, N = size(t) - string(opengl_prefix(eltype(t)), "mat", M==N ? M : string(M, "x", N)) + string(opengl_prefix(eltype(t)), "mat", M==N ? M : string(N, "x", M)) end toglsltype_string(t::Observable) = toglsltype_string(to_value(t)) toglsltype_string(x::T) where {T<:Union{Real, Mat, StaticVector, Texture, Colorant, TextureBuffer, Nothing}} = "uniform $(glsl_typename(x))" diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index c97d6b2152b..60ea6959655 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -647,9 +647,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Image) gl_attributes[:vertices] = apply_transform_and_f32_conversion(scene, plot, position) rect = Rect2f(0, 0, 1, 1) gl_attributes[:faces] = decompose(GLTriangleFace, rect) - gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv - return 1.0f0 .- Vec2f(uv[2], uv[1]) - end + gl_attributes[:texturecoordinates] = decompose_uv(rect) get!(gl_attributes, :shading, NoShading) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest @@ -677,6 +675,14 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= img = lift(x -> el32convert(Makie.to_image(x)), plot, color) gl_attributes[:image] = ShaderAbstractions.Sampler(img, x_repeat=:repeat, minfilter=:nearest) get!(gl_attributes, :fetch_pixel, true) + # different default with Patterns (no swapping and flipping of axes) + gl_attributes[:uv_transform] = map(plot, plot.attributes[:uv_transform]) do uv_transform + if uv_transform === Makie.automatic + return Mat{2,3,Float32}(1,0,0,1,0,0) + else + return convert_attribute(uv_transform, key"uv_transform"()) + end + end elseif to_value(color) isa AbstractMatrix{<:Colorant} gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 877ddf90dea..89e9d3b1db6 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -50,7 +50,7 @@ function draw_mesh(screen, data::Dict) color_norm = nothing fetch_pixel = false texturecoordinates = Vec2f(0) => GLBuffer - uv_scale = Vec2f(1) + uv_transform = Vec4f(1, 1, 0, 0) transparency = false interpolate_in_fragment_shader = true shader = GLVisualizeShader( diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 0991b0f8c7b..c4c3f03e620 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -75,6 +75,25 @@ function draw_mesh_particle(screen, p, data) texturecoordinates = nothing end + # TODO: use instance attributes + if to_value(data[:uv_transform]) isa Vector + transforms = pop!(data, :uv_transform) + @gen_defaults! data begin + uv_transform = map(transforms) do transforms + # 3x Vec2 should match the element order of glsl mat3x2 + output = Vector{Vec2f}(undef, 3 * length(transforms)) + for i in eachindex(transforms) + output[3 * (i-1) + 1] = transforms[i][Vec(1, 2)] + output[3 * (i-1) + 2] = transforms[i][Vec(3, 4)] + output[3 * (i-1) + 3] = transforms[i][Vec(5, 6)] + end + return output + end => TextureBuffer + end + else + # handled automatically + end + shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin color_map = nothing => Texture @@ -86,7 +105,6 @@ function draw_mesh_particle(screen, p, data) matcap = nothing => Texture fetch_pixel = false interpolate_in_fragment_shader = false - uv_scale = Vec2f(1) backlight = 0f0 instances = const_lift(length, position) diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index ef7810a96f8..d9cda988098 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -142,7 +142,7 @@ function draw_surface(screen, main, data::Dict) highclip = RGBAf(0, 0, 0, 0) lowclip = RGBAf(0, 0, 0, 0) - uv_scale = Vec2f(1) + uv_transform = Vec4f(1, 1, 0, 0) instances = const_lift(x->(size(x,1)-1) * (size(x,2)-1), main) => "number of planes used to render the surface" transparency = false shader = GLVisualizeShader( diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index 0c45e806237..417229293fd 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -209,6 +209,14 @@ Plots an image on a rectangle bounded by `x` and `y` (defaults to size of image) mixin_generic_plot_attributes()... mixin_colormap_attributes()... fxaa = false + """ + Sets a transform for uv coordinates, which controls how the image is mapped to its rectangular area. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic colormap = [:black, :white] end @@ -300,6 +308,14 @@ Plots a surface, where `(x, y)` define a grid whose heights are the entries in ` invert_normals = false "[(W)GLMakie only] Specifies whether the surface matrix gets sampled with interpolation." interpolate = true + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to a surface. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... @@ -393,6 +409,14 @@ Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [Geome interpolate = true cycle = [:color => :patchcolor] matcap = nothing + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to a mesh. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... @@ -472,6 +496,17 @@ Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar "Sets the rotation of the mesh. A numeric rotation is around the z-axis, a `Vec3f` causes the mesh to rotate such that the the z-axis is now that vector, and a quaternion describes a general rotation. This can be given as a Vector to apply to each scattered mesh individually." rotation = 0.0 cycle = [:color] + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to the scattered mesh. + Note that the mesh needs to include uv coordinates for this, which is not the case by default + for geometry primitives. You can use `GeometryBasics.uv_normal_mesh(prim)` with, for example `prim = Rect2f(0, 0, 1, 1)`. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + It can also be set per scattered mesh by passing a `Vector` of any of the above and operations + can be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index f26434c8e32..afbb0994be8 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -719,6 +719,61 @@ end fig end +@reference_test "uv_transform" begin + fig = Figure(size = (400, 400)) + img = [RGBf(1,0,0) RGBf(0,1,0); RGBf(0,0,1) RGBf(1,1,1)] + + function create_block(f, gl, args...; kwargs...) + ax, p = f(gl[1, 1], args..., uv_transform = I; kwargs...) + hidedecorations!(ax) + ax, p = f(gl[1, 2], args..., uv_transform = :rotr90; kwargs...) + hidedecorations!(ax) + ax, p = f(gl[2, 1], args..., uv_transform = (Vec2f(0.5), Vec2f(0.5)); kwargs...) + hidedecorations!(ax) + ax, p = f(gl[2, 2], args..., uv_transform = Makie.Mat{2,3,Float32}(-1,0,0,-1,1,1); kwargs...) + hidedecorations!(ax) + end + + gl = fig[1, 1] = GridLayout() + create_block(mesh, gl, Rect2f(0, 0, 1, 1), color = img) + + gl = fig[1, 2] = GridLayout() + create_block(surface, gl, 0..1, 0..1, zeros(10, 10), color = img) + + gl = fig[2, 1] = GridLayout() + create_block( + meshscatter, gl, Point2f[(0,0), (0,1), (1,0), (1,1)], color = img, + marker = Makie.uv_normal_mesh(Rect2f(0,0,1,1)), markersize = 1.0) + + gl = fig[2, 2] = GridLayout() + create_block(image, gl, 0..1, 0..1, img) + + fig +end + +@testset "per element uv_transform" begin + cow = loadasset("cow.png") + + N = 8; M = 10 + f = Figure(size = (500, 400)) + a, p = meshscatter( + f[1, 1], + [Point2f(x, y) for x in 1:M for y in 1:N], + color = cow, + uv_transform = [ + Makie.uv_transform(:rotl90) * + Makie.uv_transform(Vec2f(x, y+1/N), Vec2f(1/M, -1/N)) + for x in range(0, 1, length = M+1)[1:M] + for y in range(0, 1, length = N+1)[1:N] + ], + markersize = Vec3f(0.9, 0.9, 1), + marker = uv_normal_mesh(Rect2f(-0.5, -0.5, 1, 1)) + ) + hidedecorations!(a) + xlims!(a, 0.3, M+0.7) + ylims!(a, 0.3, N+0.7) + f +end @reference_test "Scatter with FastPixel" begin f = Figure() row = [(1, :pixel, 20), (2, :data, 0.5)] diff --git a/WGLMakie/assets/mesh.frag b/WGLMakie/assets/mesh.frag index efe137428e3..68b202a9b59 100644 --- a/WGLMakie/assets/mesh.frag +++ b/WGLMakie/assets/mesh.frag @@ -1,6 +1,5 @@ in vec2 frag_uv; in vec4 frag_color; -flat in int sample_frag_color; in vec3 o_normal; in vec3 o_camdir; diff --git a/WGLMakie/assets/mesh.vert b/WGLMakie/assets/mesh.vert index 14341fbe452..b7a3fc4c51d 100644 --- a/WGLMakie/assets/mesh.vert +++ b/WGLMakie/assets/mesh.vert @@ -61,6 +61,10 @@ vec4 vertex_color(float value, vec2 colorrange, sampler2D colormap){ } } +// TODO: enable +// vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3 transform, vec2 uv){ return (transform * vec3(uv, 1)).xy; } + void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { // normal in world space @@ -84,8 +88,7 @@ void main(){ vec4 position_world = model * vec4(vertex_position, 1); render(position_world, get_normals(), view, projection); - frag_uv = get_uv(); - frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); + frag_uv = apply_uv_transform(get_uv_transform(), get_uv()); frag_color = vertex_color(get_color(), get_colorrange(), colormap); frag_instance_id = uint(gl_VertexID); diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag deleted file mode 100644 index 262a1fd9538..00000000000 --- a/WGLMakie/assets/particles.frag +++ /dev/null @@ -1,68 +0,0 @@ -in vec4 frag_color; -in vec3 frag_normal; -in vec3 frag_position; -in vec3 o_camdir; - -// Smoothes out edge around 0 light intensity, see GLMakie -float smooth_zero_max(float x) { - const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; - const float shift = 1.0 + xswap - yswap; - float pow8 = x + shift; - pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; - return x < yswap ? c * pow8 : x; -} - -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float backlight = get_backlight(); - float diff_coeff = smooth_zero_max(dot(L, -N)) + - backlight * smooth_zero_max(dot(L, N)); - - // specular coefficient - vec3 H = normalize(L + V); - - float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + - backlight * pow(max(dot(H, N), 0.0), get_shininess()); - if (diff_coeff <= 0.0) - spec_coeff = 0.0; - - // final lighting model - return get_light_color() * vec3( - get_diffuse() * diff_coeff * color + - get_specular() * spec_coeff - ); -} - -flat in uint frag_instance_id; -vec4 pack_int(uint id, uint index) { - vec4 unpack; - unpack.x = float((id & uint(0xff00)) >> 8) / 255.0; - unpack.y = float((id & uint(0x00ff)) >> 0) / 255.0; - unpack.z = float((index & uint(0xff00)) >> 8) / 255.0; - unpack.w = float((index & uint(0x00ff)) >> 0) / 255.0; - return unpack; -} - -void main() { - vec3 L, N, light, color; - if (get_shading()) { - L = get_light_direction(); - N = normalize(frag_normal); - light = blinnphong(N, normalize(o_camdir), L, frag_color.rgb); - color = get_ambient() * frag_color.rgb + light; - } else { - color = frag_color.rgb; - } - - - if (picking) { - if (frag_color.a > 0.1) { - fragment_color = pack_int(object_id, frag_instance_id); - } - return; - } - - if (frag_color.a <= 0.0){ - discard; - } - fragment_color = vec4(color, frag_color.a); -} diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index 495c475579d..ce986674392 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -4,9 +4,9 @@ uniform mat4 projection; uniform mat4 view; uniform vec3 eyeposition; -out vec3 frag_normal; -out vec3 frag_position; +out vec3 o_normal; out vec4 frag_color; +out vec2 frag_uv; out vec3 o_camdir; vec3 qmul(vec4 q, vec3 v){ @@ -24,6 +24,56 @@ vec4 to_vec4(vec4 v4){return v4;} vec3 to_vec3(vec2 v3){return vec3(v3, 0.0);} vec3 to_vec3(vec3 v4){return v4;} + +vec4 get_color_from_cmap(float value, sampler2D color_map, vec2 colorrange) { + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return get_lowclip(); + } else if (value > cmax) { + return get_highclip(); + } else { + // isnan is broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return get_nan_color(); + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(color_map, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(color_map, vec2(i01, 0.0)); +} + +vec4 vertex_color(vec3 color, bool colorrange, bool colormap){ + return vec4(color, 1.0); +} +vec4 vertex_color(vec4 color, bool colorrange, bool colormap){ + return color; +} +vec4 vertex_color(bool color, bool colorrange, bool colormap){ + // color sampling happens in fragment shader + return vec4(0.0, 0.0, 0.0, 0.0); +} +vec4 vertex_color(bool value, vec2 colorrange, sampler2D colormap){ + // color sampling happens in fragment shader + return vec4(0.0, 0.0, 0.0, 0.0); +} +vec4 vertex_color(float value, vec2 colorrange, sampler2D colormap){ + if (get_interpolate_in_fragment_shader()) { + return vec4(value, 0.0, 0.0, 0.0); + } else { + return get_color_from_cmap(value, colormap, colorrange); + } +} + +// TODO: enable +// vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3 transform, vec2 uv){ return (transform * vec3(uv, 1)).xy; } +// TODO: per element + flat out uint frag_instance_id; void main(){ @@ -34,8 +84,9 @@ void main(){ rotate(get_rotation(), vertex_position, N); vertex_position = to_vec3(get_offset()) + vertex_position; vec4 position_world = model * vec4(vertex_position, 1); - frag_normal = N; - frag_color = to_vec4(get_color()); + o_normal = N; + frag_color = vertex_color(get_color(), get_colorrange(), colormap); + frag_uv = apply_uv_transform(get_uv_transform(), get_uv()); // direction to camera o_camdir = position_world.xyz / position_world.w - eyeposition; // screen space coordinates of the position diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index cd2a43bccc4..a0b24289eed 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -39,6 +39,17 @@ function create_shader(mscene::Scene, plot::Surface) per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) uniforms = Dict(:uniform_color => color, :color => false) + # TODO: allow passing Mat{2, 3, Float32} (and nothing) + uniforms[:uv_transform] = map(plot, plot[:uv_transform]) do x + M = convert_attribute(x, Key{:uv_transform}(), Key{:surface}()) + # Why transpose? + if M === nothing + return Mat3f(0,1,0, 1,0,0, 0,0,1) + else + return Mat3f(0,1,0, 1,0,0, 0,0,1) * Mat3f(M[1], M[2], 0, M[3], M[4], 0, M[5], M[6], 1) + end + end + return draw_mesh(mscene, per_vertex, plot, uniforms) end @@ -52,6 +63,21 @@ function create_shader(mscene::Scene, plot::Union{Heatmap, Image}) :shininess => 0.0f0, :backlight => 0.0f0, ) + + # TODO: allow passing Mat{2, 3, Float32} (and nothing) + if plot isa Image + uniforms[:uv_transform] = map(plot, plot[:uv_transform]) do x + M = convert_attribute(x, Key{:uv_transform}(), Key{:image}()) + # Why transpose? + if M === nothing + return Mat3f(0,1,0, 1,0,0, 0,0,1) + else + return Mat3f(0,1,0, 1,0,0, 0,0,1) * Mat3f(M[1], M[2], 0, M[3], M[4], 0, M[5], M[6], 1) + end + end + else + uniforms[:uv_transform] = Observable(Mat3f(0,1,0, -1,0,0, 1,0,1)) + end return draw_mesh(mscene, mesh, plot, uniforms) end diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl index 207ae02a05e..43e90e7d8b2 100644 --- a/WGLMakie/src/meshes.jl +++ b/WGLMakie/src/meshes.jl @@ -49,6 +49,7 @@ function handle_color!(plot, uniforms, buffers, uniform_color_name = :uniform_co get!(uniforms, uniform_color_name, false) get!(uniforms, :colormap, false) get!(uniforms, :colorrange, false) + get!(uniforms, :pattern, false) get!(uniforms, :highclip, RGBAf(0, 0, 0, 0)) get!(uniforms, :lowclip, RGBAf(0, 0, 0, 0)) get!(uniforms, :nan_color, RGBAf(0, 0, 0, 0)) @@ -62,7 +63,6 @@ function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) filter!(kv -> !(kv[2] isa Function), uniforms) handle_color!(plot, uniforms, per_vertex; permute_tex=permute_tex) - get!(uniforms, :pattern, false) get!(uniforms, :ambient, Vec3f(1)) get!(uniforms, :light_direction, Vec3f(1)) get!(uniforms, :light_color, Vec3f(1)) @@ -119,6 +119,16 @@ function create_shader(scene::Scene, plot::Makie.Mesh) end end + # TODO: allow passing Mat{2, 3, Float32} (and nothing) + uniforms[:uv_transform] = map(plot, plot[:uv_transform]) do x + M = convert_attribute(x, Key{:uv_transform}(), Key{:mesh}()) + if M === nothing + return Mat3f(I) + else + return Mat3f(M[1], M[2], 0, M[3], M[4], 0, M[5], M[6], 1) + end + end + faces = facebuffer(mesh_signal) positions = vertexbuffer(mesh_signal, plot) attributes[:faces] = faces diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index 535105ee82d..63c182579b3 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -40,7 +40,7 @@ const IGNORE_KEYS = Set([ :visible, :transformation, :alpha, :linewidth, :transparency, :marker, :light_direction, :light_color, :cycle, :label, :inspector_clear, :inspector_hover, - :inspector_label, :axis_cyclerr, :dim_conversions, :material + :inspector_label, :axis_cyclerr, :dim_conversions, :material, # TODO add model here since we generally need to apply patch_model? ]) @@ -69,9 +69,15 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[k] = lift_convert(k, v, plot) end - handle_color!(plot, uniform_dict, per_instance, :color) - handle_color_getter!(uniform_dict, per_instance) + handle_color!(plot, uniform_dict, per_instance) + # handle_color_getter!(uniform_dict, per_instance) instance = convert_attribute(plot.marker[], key"marker"(), key"meshscatter"()) + uniform_dict[:interpolate_in_fragment_shader] = get(plot, :interpolate_in_fragment_shader, false) + + if haskey(uniform_dict, :color) && haskey(per_instance, :color) + to_value(uniform_dict[:color]) isa Bool && delete!(uniform_dict, :color) + to_value(per_instance[:color]) isa Bool && delete!(per_instance, :color) + end if !hasproperty(instance, :uv) uniform_dict[:uv] = Vec2f(0) @@ -95,7 +101,27 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) - return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), + # TODO: allow passing Mat{2, 3, Float32} (and nothing) + uv_transform = map(plot, plot[:uv_transform]) do x + M = convert_attribute(x, Key{:uv_transform}(), Key{:meshscatter}()) + # why transpose? + T = Mat3f(0,1,0, 1,0,0, 0,0,1) + if M === nothing + return T + elseif M isa Mat + return T * Mat3f(M[1], M[2], 0, M[3], M[4], 0, M[5], M[6], 1) + elseif M isa Vector + return [T * Mat3f(m[1], m[2], 0, m[3], m[4], 0, m[5], m[6], 1) for m in M] + end + end + + if to_value(uv_transform) isa Vector + per_instance[:uv_transform] = Buffer(uv_transform) + else + uniform_dict[:uv_transform] = uv_transform + end + + return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("mesh.frag"), instance, VertexArray(; per_instance...), uniform_dict) end diff --git a/docs/src/reference/plots/meshscatter.md b/docs/src/reference/plots/meshscatter.md index 78de8dc0a58..491f9e8d132 100644 --- a/docs/src/reference/plots/meshscatter.md +++ b/docs/src/reference/plots/meshscatter.md @@ -15,6 +15,34 @@ zs = LinRange(0, 3, length(xs)) meshscatter(xs, ys, zs, markersize = 0.1, color = zs) ``` +```@figure backend=GLMakie +using FileIO, GeometryBasics +cow = FileIO.load(joinpath(pkgdir(Makie), "assets", "cow.png")) + +N = 8; M = 10 +f = Figure(size = (500, 400)) +a, p = meshscatter( + f[1, 1], + [Point2f(x, y) for x in 1:M for y in 1:N], + color = cow, + uv_transform = [ + # 1. undo y flip of uvs relative to pos + # 2. grab relevant section from image + # 3. rotate to match view + (:rotl90, (Vec2f(x, y), Vec2f(1/M, 1/N)), :flip_y) + for x in range(0, 1, length = M+1)[1:M] + for y in range(0, 1, length = N+1)[1:N] + ], + markersize = Vec3f(0.9, 0.9, 1), + marker = uv_normal_mesh(Rect2f(-0.5, -0.5, 1, 1)) +) +hidedecorations!(a) +xlims!(a, 0.4, M+0.6) +ylims!(a, 0.4, N+0.6) +f +``` + + ## Attributes ```@attrdocs diff --git a/src/conversions.jl b/src/conversions.jl index 929a9130888..942b8949b66 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -896,6 +896,116 @@ to_3d_scale(x::VecTypes) = to_ndim(Vec3f, x, 1) to_3d_scale(x::AbstractVector) = to_3d_scale.(x) +## UV Transforms + +# defaults that place a cow image the same way as the image +# convert_attribute(::Automatic, ::key"uv_transform", ::key"meshscatter") = Mat{2, 3, Float32}(0, 1, 1, 0, 0, 0) +# convert_attribute(::Automatic, ::key"uv_transform", ::key"mesh") = Mat{2, 3, Float32}(0, 1, 1, 0, 0, 0) +# convert_attribute(::Automatic, ::key"uv_transform", ::key"surface") = Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0) +# convert_attribute(::Automatic, ::key"uv_transform", ::key"image") = Mat{2, 3, Float32}(0, 1, 1, 0, 0, 0) + +# defaults matching master +# Note - defaults with Patterns should be identity (handled in backends) +convert_attribute(::Automatic, ::key"uv_transform", ::key"meshscatter") = Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0) +convert_attribute(::Automatic, ::key"uv_transform", ::key"mesh") = Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0) +convert_attribute(::Automatic, ::key"uv_transform", ::key"surface") = Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1) +convert_attribute(::Automatic, ::key"uv_transform", ::key"image") = Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1) + +convert_attribute(x::Vector, k::key"uv_transform") = convert_attribute.(x, (k,)) +convert_attribute(x, k::key"uv_transform") = convert_attribute(uv_transform(x), k) +convert_attribute(x::Mat3f, ::key"uv_transform") = x[Vec(1,2), Vec(1,2,3)] +convert_attribute(x::Mat{2, 3, Float32}, ::key"uv_transform") = x +convert_attribute(x::Nothing, ::key"uv_transform") = x + +function convert_attribute(angle::Real, ::key"uv_transform") + # To rotate an image with uvs in the 0..1 range we need to translate, + # rotate, translate back. + # For patterns and in terms of what's actually happening to the uvs we + # should not translate at all. + error("A uv_transform corresponding to a rotation by $(angle)rad is not implemented directly. Use :rotr90, :rotl90, :rot180 or Makie.uv_transform(angle).") +end + +""" + uv_transform(args::Tuple) + uv_transform(args...) + +Returns a 3x3 uv transformation matrix combinign all the given arguments. This +lowers to `mapfoldl(uv_transform, *, args)` so operations act from right to left +like matrices `(op3, op2, op1)`. + +Note that `Tuple{VecTypes{2, <:Real}, VecTypes{2, <:Real}}` maps to +`uv_transform(translation, scale)` as a special case. +""" +uv_transform(packed::Tuple) = mapfoldl(uv_transform, *, packed) +uv_transform(packed...) = uv_transform(packed) +uv_transform(::UniformScaling) = Mat{3, 3, Float32}(I) + + +# prefer scale as single argument since it may be useful for patterns +# while just translation is mostly useless +""" + uv_transform(scale::VecTypes{2}) + uv_transform(translation::VecTypes{2}, scale::VecTypes{2}) + uv_transform(angle::Real) + +Creates a 3x3 uv transformation matrix based on the given translation and scale +or rotation angle (around z axis). +""" +uv_transform(x::Tuple{VecTypes{2, <:Real}, VecTypes{2, <:Real}}) = uv_transform(x[1], x[2]) +uv_transform(scale::VecTypes{2, <: Real}) = uv_transform(Vec2f(0), scale) +function uv_transform(translation::VecTypes{2, <: Real}, scale::VecTypes{2, <: Real}) + return Mat3f( + scale[1], 0, 0, + 0, scale[2], 0, + translation[1], translation[2], 1 + ) +end +function uv_transform(angle::Real) + return Mat3f( + cos(angle), sin(angle), 0, + -sin(angle), cos(angle), 0, + 0, 0, 1 + ) +end + +""" + uv_transform(action::Symbol) + +Creates a 3x3 uv transformation matrix from a given named action. They assume +`0 < uv < 1` and thus may not work correctly with Patterns. The actions include +- `:rotr90` corresponding to `rotr90(texture)` +- `:rotl90` corresponding to `rotl90(texture)` +- `:rot180` corresponding to `rot180(texture)` +- `:swap_xy, :transpose` which corresponds to transposing the texture +- `:flip_x, :flip_y, :flip_xy` which flips the x/y/both axis of a texture +- `:mesh, :meshscatter, :surface, :image` which grabs the default of the corresponding plot type +""" +function uv_transform(action::Symbol) + # TODO: do some explicitly named operations + if action == :rotr90 + return Mat3f(0,-1,0, 1,0,0, 0,1,1) + elseif action == :rotl90 + return Mat3f(0,1,0, -1,0,0, 1,0,1) + elseif action == :rot180 + return Mat3f(-1,0,0, 0,-1,0, 1,1,1) + elseif action in (:swap_xy, :transpose) + return Mat3f(0,1,0, 1,0,0, 0,0,1) + elseif action in (:flip_x, :invert_x) + return Mat3f(-1,0,0, 0,1,0, 1,0,1) + elseif action in (:flip_y, :invert_y) + return Mat3f(1,0,0, 0,-1,0, 0,1,1) + elseif action in (:flip_xy, :invert_xy) + return Mat3f(-1,0,0, 0,-1,0, 1,1,1) + elseif action in (:meshscatter, :mesh, :image, :surface) + M = convert_attribute(automatic, key"uv_transform"(), Key{action}()) + return Mat3f(M[1,1], M[2,1], 0, M[1,2], M[2,2], 0, M[1,3], M[2,3], 1) + # elseif action == :surface + # return Mat3f(I) + else + error("Transformation :$action not recognized.") + end +end + convert_attribute(x, ::key"uv_offset_width") = Vec4f(x) convert_attribute(x::AbstractVector{Vec4f}, ::key"uv_offset_width") = x diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 3d0e1d8c5f4..1408106aabd 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -396,7 +396,8 @@ function surface2mesh(xs, ys, zs::AbstractMatrix, transform_func = identity, spa # and remove quads that contain a NaN coordinate to avoid drawing triangles faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) # create the uv (texture) vectors - uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) + # uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) + uv = decompose_uv(rect) # return a mesh with known uvs and normals. return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = nan_aware_normals(ps, faces)), faces, ) end diff --git a/test/convert_attributes.jl b/test/convert_attributes.jl new file mode 100644 index 00000000000..7bad5471edd --- /dev/null +++ b/test/convert_attributes.jl @@ -0,0 +1,48 @@ +using Makie: Mat, Mat3f, convert_attribute, uv_transform, automatic + +@testset "uv_transform" begin + key = Makie.key"uv_transform"() + + # defaults matching previous Makie versions + @test convert_attribute(automatic, key, Makie.key"meshscatter"()) == Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0) + @test convert_attribute(automatic, key, Makie.key"mesh"()) == Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0) + @test convert_attribute(automatic, key, Makie.key"surface"()) == Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1) + @test convert_attribute(automatic, key, Makie.key"image"()) == Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1) + + # General Pipeline + # Each should work as a value or as a vector element + for wrap in (identity, x -> [x]) + M = Mat{2, 3, Float32}(1,2,3,4,5,6) + @test convert_attribute(wrap(M), key) == wrap(M) + + M3 = Mat3f(1,2,0, 3,4,0, 5,6,0) + @test convert_attribute(wrap(M3), key) == wrap(M) + + # transformationmatrix-like + @test convert_attribute(wrap(Vec2f(2,3)), key) == wrap(Mat{2, 3, Float32}(2,0,0,3,0,0)) + @test convert_attribute(wrap((Vec2f(-1,-2), Vec2f(2,3))), key) == + wrap(Mat{2, 3, Float32}(2,0,0,3,-1,-2)) + @test convert_attribute(wrap(I), key) == wrap(Mat{2, 3, Float32}(1,0,0,1,0,0)) + + # Named + @test convert_attribute(wrap(:rotr90), key) == wrap(Mat{2, 3, Float32}(0, -1, 1, 0, 0, 1)) + @test convert_attribute(wrap(:rotl90), key) == wrap(Mat{2, 3, Float32}(0, 1, -1, 0, 1, 0)) + @test convert_attribute(wrap(:swap_xy), key) == wrap(Mat{2, 3, Float32}(0, 1, 1, 0, 0, 0)) + @test convert_attribute(wrap(:flip_x), key) == wrap(Mat{2, 3, Float32}(-1, 0, 0, 1, 1, 0)) + @test convert_attribute(wrap(:flip_y), key) == wrap(Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1)) + @test convert_attribute(wrap(:flip_xy), key) == wrap(Mat{2, 3, Float32}(-1, 0, 0, -1, 1, 1)) + + # Chaining + @test convert_attribute(wrap((:flip_x, :flip_xy, :flip_y)), key) == wrap(Mat{2, 3, Float32}(1, 0, 0, 1, 0, 0)) + @test convert_attribute(wrap((:rotr90, :swap_xy)), key) == wrap(Mat{2, 3, Float32}(1, 0, 0, -1, 0, 1)) + @test convert_attribute(wrap((:rotl90, (Vec2f(0.5, 0.5), Vec2f(0.5, 0.5)), :flip_y)), key) == wrap(Mat{2, 3, Float32}(0, 0.5, 0.5, 0, 0, 0.5)) + end + + @test convert_attribute(nothing, key) === nothing + + # Not meant to be used via convert_attribute, util for uv_transform + @test uv_transform(:meshscatter)[Vec(1,2), Vec(1,2,3)] == convert_attribute(automatic, key, Makie.key"meshscatter"()) + @test uv_transform(:mesh)[Vec(1,2), Vec(1,2,3)] == convert_attribute(automatic, key, Makie.key"mesh"()) + @test uv_transform(:image)[Vec(1,2), Vec(1,2,3)] == convert_attribute(automatic, key, Makie.key"image"()) + @test uv_transform(:surface)[Vec(1,2), Vec(1,2,3)] == convert_attribute(automatic, key, Makie.key"surface"()) +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 1616c19fac3..3f8e3d4d797 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,6 +39,7 @@ using Makie: volume include("convert_arguments.jl") # from here include("conversions.jl") + include("convert_attributes.jl") include("float32convert.jl") include("dim-converts.jl")