From 83a2387d31084a5a896c225f0f88ddbcd929b4c2 Mon Sep 17 00:00:00 2001 From: Tom Rottier <39778698+TomRottier@users.noreply.github.com> Date: Thu, 22 Jun 2023 17:59:37 +0100 Subject: [PATCH 01/14] add option to flip x direction for bezier path (#3024) --- src/bezier.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bezier.jl b/src/bezier.jl index 924f0d675bc..41d4cd27582 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -240,12 +240,15 @@ function BezierPath(poly::Polygon) return BezierPath(commands) end -function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = false, keep_aspect = true) +function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = false, flipx = false, keep_aspect = true) commands = parse_bezier_commands(svg) p = BezierPath(commands) if flipy p = scale(p, Vec(1, -1)) end + if flipx + p = scale(p, Vec(-1, 1)) + end if fit if bbox === nothing p = fit_to_bbox(p, Rect2f((-0.5, -0.5), (1.0, 1.0)), keep_aspect = keep_aspect) From 04eca18a8476a8b52ff80a693d72d254930eb70a Mon Sep 17 00:00:00 2001 From: Tom Rottier <39778698+TomRottier@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:40:53 +0100 Subject: [PATCH 02/14] update docs (#3026) update docs for bezier path for xflip --- docs/examples/plotting_functions/scatter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/plotting_functions/scatter.md b/docs/examples/plotting_functions/scatter.md index 31fc226540f..947e1c06c81 100644 --- a/docs/examples/plotting_functions/scatter.md +++ b/docs/examples/plotting_functions/scatter.md @@ -243,7 +243,7 @@ scatter(1:5, #### Construction from svg path strings You can also create a bezier path from an [svg path specification string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands). -You can automatically resize the path and flip the y-axis (svgs usually have a coordinate system where y increases downwards) with the keywords `fit` and `yflip`. +You can automatically resize the path and flip the y- and x-axes (svgs usually have a coordinate system where y increases downwards) with the keywords `fit`, `yflip`, and `xflip`. By default, the bounding box for the fitted path is a square of width 1 centered on zero. You can pass a different bounding `Rect` with the `bbox` keyword argument. By default, the aspect of the path is left intact, and if it's not matching the new bounding box, the path is centered so it fits inside. From a490d20c7f51ae38e2910f376b7e582c199d265e Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:19:42 +0200 Subject: [PATCH 03/14] Fix links (#3021) Update screen.jl --- CairoMakie/src/screen.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 67117ddde9f..ed8cd516697 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -76,8 +76,8 @@ end to_cairo_antialias(aa::Int) = aa """ -* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). -* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). +* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/stable/documentation/figure_size/). +* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/stable/documentation/figure_size/). * `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`. * `visible::Bool`: if true, a browser/image viewer will open to display rendered output. """ From ddd1fd2b1009a2019b8641cfd69bee79a260549f Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Tue, 27 Jun 2023 10:14:42 +0200 Subject: [PATCH 04/14] Adjust stroke/glow/AA scaling in GLMakie (#2950) * scale sdf based on minimum xy scale * fix rect scaling * add ellipse sdf * add NEWS entry * clean up comment [skip ci] --------- Co-authored-by: Simon --- GLMakie/assets/shader/distance_shape.frag | 43 ++++++++++++++++++++--- GLMakie/assets/shader/sprites.geom | 4 ++- GLMakie/src/glshaders/particles.jl | 20 ++++++++++- NEWS.md | 2 ++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index 3e16d4a4e70..1221978a1be 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -14,6 +14,7 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d #define ROUNDED_RECTANGLE 2 #define DISTANCEFIELD 3 #define TRIANGLE 4 +#define ELLIPSE 5 #define M_SQRT_2 1.4142135 @@ -37,6 +38,7 @@ flat in uvec2 f_id; flat in int f_primitive_index; in vec2 f_uv; // f_uv.{x,y} are in the interval [-a, 1+a] flat in vec4 f_uv_texture_bbox; +flat in vec2 f_sprite_scale; // These versions of aastep assume that `dist` is a signed distance function // which has been scaled to be in units of pixels. @@ -65,15 +67,46 @@ float triangle(vec2 P){ return -max(r1,r2); } float circle(vec2 uv){ - return 0.5-length(uv-vec2(0.5)); + return 0.5 - length(uv - vec2(0.5)); } float rectangle(vec2 uv){ - vec2 d = max(-uv, uv-vec2(1)); + vec2 s = f_sprite_scale / min(f_sprite_scale.x, f_sprite_scale.y); + vec2 d = s * max(-uv, uv-vec2(1)); return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))); } float rounded_rectangle(vec2 uv, vec2 tl, vec2 br){ - vec2 d = max(tl-uv, uv-br); - return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))-tl.x); + vec2 s = f_sprite_scale / min(f_sprite_scale.x, f_sprite_scale.y); + vec2 d = s * max(tl-uv, uv-br); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y))) - s.x * tl.x); +} +// See https://iquilezles.org/articles/ellipsedist/ +float ellipse(vec2 uv, vec2 scale) +{ + // to central coordinates, use symmetry (quarter ellipse, 0 <= p <= wh) + vec2 wh = scale / min(scale.x, scale.y); + vec2 p = wh * abs(uv - vec2(0.5)); + wh = wh * 0.5; + + // initial value + vec2 q = wh * (p - wh); + vec2 cs = normalize( (q.x1.0) ? -d : d; } void fill(vec4 fillcolor, Nothing image, vec2 uv, float infill, inout vec4 color){ @@ -141,6 +174,8 @@ void main(){ signed_distance = rectangle(f_uv); else if(shape == TRIANGLE) signed_distance = triangle(f_uv); + else if(shape == ELLIPSE) + signed_distance = ellipse(f_uv, f_sprite_scale); // See notes in geometry shader where f_viewport_from_u_scale is computed. signed_distance *= f_viewport_from_u_scale; diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 47da9a6657c..d34d131a187 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -60,6 +60,7 @@ flat out vec4 f_glow_color; flat out uvec2 f_id; out vec2 f_uv; flat out vec4 f_uv_texture_bbox; +flat out vec2 f_sprite_scale; uniform mat4 projection, view, model; @@ -88,6 +89,7 @@ void emit_vertex(vec4 vertex, vec2 uv) f_stroke_color = g_stroke_color[0]; f_glow_color = g_glow_color[0]; f_id = g_id[0]; + f_sprite_scale = g_offset_width[0].zw; EmitVertex(); } @@ -157,7 +159,7 @@ void main(void) // any calculation based on them will not be a distance function.) // * For sampled distance fields, we need to consistently choose the *x* // for the scaling in get_distancefield_scale(). - float sprite_from_u_scale = abs(o_w.z); + float sprite_from_u_scale = min(abs(o_w.z), abs(o_w.w)); f_viewport_from_u_scale = viewport_from_sprite_scale * sprite_from_u_scale; f_distancefield_scale = get_distancefield_scale(distancefield); diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 7b8bd663363..9bc4d3b4fae 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -33,6 +33,14 @@ struct PointSizeRender end (x::PointSizeRender)() = glPointSize(to_pointsize(x.size[])) +# For switching between ellipse method and faster circle method in shader +is_all_equal_scale(o::Observable) = is_all_equal_scale(o[]) +is_all_equal_scale(::Real) = true +is_all_equal_scale(::Vector{Real}) = true +is_all_equal_scale(v::Vec2f) = v[1] == v[2] # could use ≈ too +is_all_equal_scale(vs::Vector{Vec2f}) = all(is_all_equal_scale, vs) + + @nospecialize @@ -164,7 +172,7 @@ function draw_scatter(screen, (marker, position), data) rot = get!(data, :rotation, Vec4f(0, 0, 0, 1)) rot = vec2quaternion(rot) delete!(data, :rotation) - + @gen_defaults! data begin shape = Cint(0) position = position => GLBuffer @@ -174,6 +182,16 @@ function draw_scatter(screen, (marker, position), data) image = nothing => Texture end + data[:shape] = map( + convert(Observable{Int}, pop!(data, :shape)), data[:scale] + ) do shape, scale + if shape == 0 && !is_all_equal_scale(scale) + return Cint(5) # scaled CIRCLE -> ELLIPSE + else + return shape + end + end + @gen_defaults! data begin quad_offset = Vec2f(0) => GLBuffer intensity = nothing => GLBuffer diff --git a/NEWS.md b/NEWS.md index da5ec9734c5..9128a116a20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ ## master +- Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950) +- Fix broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953) - Scale errorbar whiskers and bracket correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) - Update bracket when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) From 77823ff524415f080390240c18d11ae6403e942d Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 28 Jun 2023 15:09:22 +0200 Subject: [PATCH 05/14] fix meshscatter for 2d marker and fix shading (#3031) fix meshscatter for 2d marker + shading --- WGLMakie/assets/particles.frag | 16 +++++++++++----- WGLMakie/assets/particles.vert | 2 +- WGLMakie/src/particles.jl | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag index 69c7e040868..5615fb356da 100644 --- a/WGLMakie/assets/particles.frag +++ b/WGLMakie/assets/particles.frag @@ -32,11 +32,17 @@ vec4 pack_int(uint id, uint index) { } void main() { - vec3 L = normalize(frag_lightdir); - vec3 N = normalize(frag_normal); - vec3 light1 = blinnphong(N, frag_position, L, frag_color.rgb); - vec3 light2 = blinnphong(N, frag_position, -L, frag_color.rgb); - vec3 color = get_ambient() * frag_color.rgb + light1 + get_backlight() * light2; + vec3 L, N, light1, light2, color; + if (get_shading()) { + L = normalize(frag_lightdir); + N = normalize(frag_normal); + light1 = blinnphong(N, frag_position, L, frag_color.rgb); + light2 = blinnphong(N, frag_position, -L, frag_color.rgb); + color = get_ambient() * frag_color.rgb + light1 + get_backlight() * light2; + } else { + color = frag_color.rgb; + } + if (picking) { if (frag_color.a > 0.1) { diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index fefb693af48..f2785d2aed4 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -30,7 +30,7 @@ flat out uint frag_instance_id; void main(){ // get_* gets the global inputs (uniform, sampler, position array) // those functions will get inserted by the shader creation pipeline - vec3 vertex_position = get_markersize() * get_position(); + vec3 vertex_position = get_markersize() * to_vec3(get_position()); vec3 lightpos = vec3(20,20,20); vec3 N = get_normals(); rotate(get_rotations(), vertex_position, N); diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index d12ad0c9fce..702a16d3df3 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -92,6 +92,7 @@ function create_shader(scene::Scene, plot::MeshScatter) # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms uniform_dict[:picking] = false uniform_dict[:object_id] = UInt32(0) + uniform_dict[:shading] = plot.shading return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) From 393c08892b2f1a7c5e1fd32930f68d1605445f58 Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Wed, 28 Jun 2023 15:26:07 -0400 Subject: [PATCH 06/14] fix error message (#3029) was getting this error: ``` ERROR: UndefVarError: `intens` not defined Stacktrace: [1] color_and_colormap!(plot::Scatter{Tuple{Vector{Point{3, Float32}}}}, intensity::Observable{Any}) @ Makie ~/.julia/packages/Makie/iECbF/src/interfaces.jl:26 ``` --- src/interfaces.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index bc159b80cd0..f08358613bf 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -24,7 +24,7 @@ function color_and_colormap!(plot, intensity = plot[:color]) get!(plot, :nan_color, RGBAf(0,0,0,0)) if intensity[] isa Number plot[:colorrange][] isa Automatic && - error("Cannot determine a colorrange automatically for single number color value $intens. Pass an explicit colorrange.") + error("Cannot determine a colorrange automatically for single number color value $intensity. Pass an explicit colorrange.") args = @converted_attribute plot (colorrange, lowclip, highclip, nan_color) plot[:color] = lift(numbers_to_colors, plot, intensity, colormap, args...) delete!(plot, :colorrange) From d93343d2a2f9cbc8891cbdda5404c06f24c1b4cf Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Wed, 28 Jun 2023 15:28:32 -0400 Subject: [PATCH 07/14] WGLMakie supports `pick` now (#3033) see https://github.com/MakieOrg/Makie.jl/issues/2258#issuecomment-1611730997 --- docs/documentation/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/events.md b/docs/documentation/events.md index 593fead36c3..d477c32bd88 100644 --- a/docs/documentation/events.md +++ b/docs/documentation/events.md @@ -225,7 +225,7 @@ scene ## Point Picking -Makie provides a function `pick(x[, position = events(x).mouseposition[]])` to get the plot displayed at a certain position with `x` being a `Figure`, `Axis`, `FigureAxisPlot` or `Scene`. This is currently a **GLMakie** only feature. The function returns a primitive plot and an index. The primitive plots are the base plots drawable in backends: +Makie provides a function `pick(x[, position = events(x).mouseposition[]])` to get the plot displayed at a certain position with `x` being a `Figure`, `Axis`, `FigureAxisPlot` or `Scene`. The function returns a primitive plot and an index. The primitive plots are the base plots drawable in backends: - scatter - text From c8039e272db3bcb2d1e789a89be179fbbe3fe0a5 Mon Sep 17 00:00:00 2001 From: Daniel VandenHeuvel <95613936+DanielVandH@users.noreply.github.com> Date: Fri, 30 Jun 2023 02:37:18 +1000 Subject: [PATCH 08/14] Use DelaunayTriangulation.jl for tricontourf (#2896) * Use DelaunayTriangulation.jl * Example wasn't passing edges * Update to v0.6.1 with removed constprop * Change DelaunayTriangulation to 0.6.2 and fix doc code * Add DelaunayTriangulation to doc depdencies * Support passing Triangulation objects into tricontourf * Fix scoping issue * Make the default form of tricontourf tricontourf(tri::DelTri.Triangulation, zs) * Add reference tests * Fix missing DelaunayTriangulation dep * Remove MiniQhull dependency * Accidentally left in deved packages * PrecompileTools is needed * Remove boundary_nodes and edges kwargs * Update the DelaunayTriangulation compat * small docs fixes --------- Co-authored-by: Simon Co-authored-by: Julius Krumbiegel Co-authored-by: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> --- Project.toml | 4 +- ReferenceTests/Project.toml | 1 + ReferenceTests/src/ReferenceTests.jl | 1 + ReferenceTests/src/tests/examples2d.jl | 67 ++++++++++++ ReferenceTests/src/tests/refimages.jl | 1 + docs/Project.toml | 1 + .../plotting_functions/tricontourf.md | 103 ++++++++++++++++++ src/Makie.jl | 2 +- src/basic_recipes/tricontourf.jl | 62 +++++++---- 9 files changed, 217 insertions(+), 25 deletions(-) diff --git a/Project.toml b/Project.toml index 89a3bb6f529..0153527c67d 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" @@ -34,7 +35,6 @@ MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" MathTeXEngine = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" -MiniQhull = "978d7f02-9e05-4691-894f-ae31a51d76ca" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Packing = "19eb6ba3-879d-56ad-ad62-d5c202156566" @@ -64,6 +64,7 @@ ColorSchemes = "3.5" ColorTypes = "0.8, 0.9, 0.10, 0.11" Colors = "0.9, 0.10, 0.11, 0.12" Contour = "0.5, 0.6" +DelaunayTriangulation = "0.6.2, 0.7" Distributions = "0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25" DocStringExtensions = "0.8, 0.9" FFMPEG = "0.2, 0.3, 0.4" @@ -83,7 +84,6 @@ MacroTools = "0.5" MakieCore = "=0.6.3" Match = "1.1" MathTeXEngine = "0.5" -MiniQhull = "0.4" Observables = "0.5.3" OffsetArrays = "1" Packing = "0.5" diff --git a/ReferenceTests/Project.toml b/ReferenceTests/Project.toml index 39ba457065e..7ba378ebc8e 100644 --- a/ReferenceTests/Project.toml +++ b/ReferenceTests/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" diff --git a/ReferenceTests/src/ReferenceTests.jl b/ReferenceTests/src/ReferenceTests.jl index fc7cadedc13..fe844401fa1 100644 --- a/ReferenceTests/src/ReferenceTests.jl +++ b/ReferenceTests/src/ReferenceTests.jl @@ -27,6 +27,7 @@ using Colors using LaTeXStrings using GeometryBasics using DelimitedFiles +using DelaunayTriangulation basedir(files...) = normpath(joinpath(@__DIR__, "..", files...)) loadasset(files...) = FileIO.load(assetpath(files...)) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 902329173bf..836977ecbde 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -685,6 +685,73 @@ end f end +@reference_test "tricontourf with boundary nodes" begin + n = 20 + angles = range(0, 2pi, length = n+1)[1:end-1] + x = [cos.(angles); 2 .* cos.(angles .+ pi/n)] + y = [sin.(angles); 2 .* sin.(angles .+ pi/n)] + z = (x .- 0.5).^2 + (y .- 0.5).^2 .+ 0.5.* RNG.randn.() + + inner = [n:-1:1; n] # clockwise inner + outer = [(n+1):(2n); n+1] # counter-clockwise outer + boundary_nodes = [[outer], [inner]] + tri = DelaunayTriangulation.triangulate([x'; y'], boundary_nodes = boundary_nodes) + f, ax, _ = tricontourf(tri, z) + scatter!(x, y, color = z, strokewidth = 1, strokecolor = :black) + f +end + +@reference_test "tricontourf with boundary nodes and edges" begin + curve_1 = [ + [(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (15.0, 0.0), (20.0, 0.0), (25.0, 0.0)], + [(25.0, 0.0), (25.0, 5.0), (25.0, 10.0), (25.0, 15.0), (25.0, 20.0), (25.0, 25.0)], + [(25.0, 25.0), (20.0, 25.0), (15.0, 25.0), (10.0, 25.0), (5.0, 25.0), (0.0, 25.0)], + [(0.0, 25.0), (0.0, 20.0), (0.0, 15.0), (0.0, 10.0), (0.0, 5.0), (0.0, 0.0)] + ] + curve_2 = [ + [(4.0, 6.0), (4.0, 14.0), (4.0, 20.0), (18.0, 20.0), (20.0, 20.0)], + [(20.0, 20.0), (20.0, 16.0), (20.0, 12.0), (20.0, 8.0), (20.0, 4.0)], + [(20.0, 4.0), (16.0, 4.0), (12.0, 4.0), (8.0, 4.0), (4.0, 4.0), (4.0, 6.0)] + ] + curve_3 = [ + [(12.906, 10.912), (16.0, 12.0), (16.16, 14.46), (16.29, 17.06), + (13.13, 16.86), (8.92, 16.4), (8.8, 10.9), (12.906, 10.912)] + ] + curves = [curve_1, curve_2, curve_3] + points = [ + (3.0, 23.0), (9.0, 24.0), (9.2, 22.0), (14.8, 22.8), (16.0, 22.0), + (23.0, 23.0), (22.6, 19.0), (23.8, 17.8), (22.0, 14.0), (22.0, 11.0), + (24.0, 6.0), (23.0, 2.0), (19.0, 1.0), (16.0, 3.0), (10.0, 1.0), (11.0, 3.0), + (6.0, 2.0), (6.2, 3.0), (2.0, 3.0), (2.6, 6.2), (2.0, 8.0), (2.0, 11.0), + (5.0, 12.0), (2.0, 17.0), (3.0, 19.0), (6.0, 18.0), (6.5, 14.5), + (13.0, 19.0), (13.0, 12.0), (16.0, 8.0), (9.8, 8.0), (7.5, 6.0), + (12.0, 13.0), (19.0, 15.0) + ] + boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) + edges = Set(((1, 19), (19, 12), (46, 4), (45, 12))) + + tri = triangulate(points; boundary_nodes = boundary_nodes, edges = edges, check_arguments = false) + z = [(x - 1) * (y + 1) for (x, y) in each_point(tri)] + f, ax, _ = tricontourf(tri, z, levels = 30) + f +end + +@reference_test "tricontourf with provided triangulation" begin + θ = [LinRange(0, 2π * (1 - 1/19), 20); 0] + xy = Vector{Vector{Vector{NTuple{2,Float64}}}}() + cx = [0.0, 3.0] + for i in 1:2 + push!(xy, [[(cx[i] + cos(θ), sin(θ)) for θ in θ]]) + push!(xy, [[(cx[i] + 0.5cos(θ), 0.5sin(θ)) for θ in reverse(θ)]]) + end + boundary_nodes, points = convert_boundary_points_to_indices(xy) + tri = triangulate(points; boundary_nodes=boundary_nodes, check_arguments=false) + z = [(x - 3/2)^2 + y^2 for (x, y) in each_point(tri)] + + f, ax, tr = tricontourf(tri, z, colormap = :matter) + f +end + @reference_test "contour labels 2D" begin paraboloid = (x, y) -> 10(x^2 + y^2) diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 74faac9c3a3..73ce26f4a42 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -10,6 +10,7 @@ using ReferenceTests.LaTeXStrings using ReferenceTests.DelimitedFiles using ReferenceTests.Test using ReferenceTests.Colors: RGB, N0f8 +using ReferenceTests.DelaunayTriangulation using Makie: Record, volume @testset "primitives" begin diff --git a/docs/Project.toml b/docs/Project.toml index e7d571969bd..d05280df457 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,6 +6,7 @@ Chain = "8be319e6-bccf-4806-a6f7-6fae938471bc" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/examples/plotting_functions/tricontourf.md b/docs/examples/plotting_functions/tricontourf.md index 3adfadd50df..abc18e24168 100644 --- a/docs/examples/plotting_functions/tricontourf.md +++ b/docs/examples/plotting_functions/tricontourf.md @@ -44,6 +44,8 @@ f #### Triangulation modes +Manual triangulations can be passed as a 3xN matrix of integers, where each column of three integers specifies the indices of the corners of one triangle in the vector of points. + \begin{examplefigure}{svg = true} ```julia using CairoMakie @@ -74,6 +76,107 @@ f ``` \end{examplefigure} +By default, `tricontourf` performs unconstrained triangulations. +Greater control over the triangulation, such as allowing for enforced boundaries, can be achieved by using [DelaunayTriangulation.jl](https://github.com/DanielVandH/DelaunayTriangulation.jl) and passing the resulting triangulation as the first argument of `tricontourf`. +For example, the above annulus can also be plotted as follows: + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide +using Random + +Random.seed!(123) + +n = 20 +angles = range(0, 2pi, length = n+1)[1:end-1] +x = [cos.(angles); 2 .* cos.(angles .+ pi/n)] +y = [sin.(angles); 2 .* sin.(angles .+ pi/n)] +z = (x .- 0.5).^2 + (y .- 0.5).^2 .+ 0.5.*randn.() + +inner = [n:-1:1; n] # clockwise inner +outer = [(n+1):(2n); n+1] # counter-clockwise outer +boundary_nodes = [[outer], [inner]] +points = [x'; y'] +tri = triangulate(points; boundary_nodes = boundary_nodes) +f, ax, _ = tricontourf(tri, z; + axis = (; aspect = 1, title = "Constrained triangulation\nvia DelaunayTriangulation.jl")) +scatter!(x, y, color = z, strokewidth = 1, strokecolor = :black) +f +``` +\end{examplefigure} + +Boundary nodes make it possible to support more complicated regions, possibly with holes, than is possible by only providing points themselves. + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide + +## Start by defining the boundaries, and then convert to the appropriate interface +curve_1 = [ + [(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (15.0, 0.0), (20.0, 0.0), (25.0, 0.0)], + [(25.0, 0.0), (25.0, 5.0), (25.0, 10.0), (25.0, 15.0), (25.0, 20.0), (25.0, 25.0)], + [(25.0, 25.0), (20.0, 25.0), (15.0, 25.0), (10.0, 25.0), (5.0, 25.0), (0.0, 25.0)], + [(0.0, 25.0), (0.0, 20.0), (0.0, 15.0), (0.0, 10.0), (0.0, 5.0), (0.0, 0.0)] +] # outer-most boundary: counter-clockwise +curve_2 = [ + [(4.0, 6.0), (4.0, 14.0), (4.0, 20.0), (18.0, 20.0), (20.0, 20.0)], + [(20.0, 20.0), (20.0, 16.0), (20.0, 12.0), (20.0, 8.0), (20.0, 4.0)], + [(20.0, 4.0), (16.0, 4.0), (12.0, 4.0), (8.0, 4.0), (4.0, 4.0), (4.0, 6.0)] +] # inner boundary: clockwise +curve_3 = [ + [(12.906, 10.912), (16.0, 12.0), (16.16, 14.46), (16.29, 17.06), + (13.13, 16.86), (8.92, 16.4), (8.8, 10.9), (12.906, 10.912)] +] # this is inside curve_2, so it's counter-clockwise +curves = [curve_1, curve_2, curve_3] +points = [ + (3.0, 23.0), (9.0, 24.0), (9.2, 22.0), (14.8, 22.8), (16.0, 22.0), + (23.0, 23.0), (22.6, 19.0), (23.8, 17.8), (22.0, 14.0), (22.0, 11.0), + (24.0, 6.0), (23.0, 2.0), (19.0, 1.0), (16.0, 3.0), (10.0, 1.0), (11.0, 3.0), + (6.0, 2.0), (6.2, 3.0), (2.0, 3.0), (2.6, 6.2), (2.0, 8.0), (2.0, 11.0), + (5.0, 12.0), (2.0, 17.0), (3.0, 19.0), (6.0, 18.0), (6.5, 14.5), + (13.0, 19.0), (13.0, 12.0), (16.0, 8.0), (9.8, 8.0), (7.5, 6.0), + (12.0, 13.0), (19.0, 15.0) +] +boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) +edges = Set(((1, 19), (19, 12), (46, 4), (45, 12))) + +## Extract the x, y +tri = triangulate(points; boundary_nodes = boundary_nodes, edges = edges, check_arguments = false) +z = [(x - 1) * (y + 1) for (x, y) in each_point(tri)] # note that each_point preserves the index order +f, ax, _ = tricontourf(tri, z, levels = 30; axis = (; aspect = 1)) +f +``` +\end{examplefigure} + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide + +using Random +Random.seed!(1234) + +θ = [LinRange(0, 2π * (1 - 1/19), 20); 0] +xy = Vector{Vector{Vector{NTuple{2,Float64}}}}() +cx = [0.0, 3.0] +for i in 1:2 + push!(xy, [[(cx[i] + cos(θ), sin(θ)) for θ in θ]]) + push!(xy, [[(cx[i] + 0.5cos(θ), 0.5sin(θ)) for θ in reverse(θ)]]) +end +boundary_nodes, points = convert_boundary_points_to_indices(xy) +tri = triangulate(points; boundary_nodes=boundary_nodes, check_arguments=false) +z = [(x - 3/2)^2 + y^2 for (x, y) in each_point(tri)] # note that each_point preserves the index order + +f, ax, tr = tricontourf(tri, z, colormap = :matter) +f +``` +\end{examplefigure} + #### Relative mode Sometimes it's beneficial to drop one part of the range of values, usually towards the outer boundary. diff --git a/src/Makie.jl b/src/Makie.jl index c0ec3d157ae..64a19ed55e8 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -49,7 +49,7 @@ import ImageIO import FileIO import SparseArrays import TriplotBase -import MiniQhull +import DelaunayTriangulation as DelTri import Setfield import REPL import MacroTools diff --git a/src/basic_recipes/tricontourf.jl b/src/basic_recipes/tricontourf.jl index 9b98a876cbb..b4f3808a6b9 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -1,10 +1,12 @@ struct DelaunayTriangulation end """ + tricontourf(triangles::Triangulation, zs; kwargs...) tricontourf(xs, ys, zs; kwargs...) -Plots a filled tricontour of the height information in `zs` at horizontal positions `xs` -and vertical positions `ys`. +Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and +vertical positions `ys`. A `Triangulation` from DelaunayTriangulation.jl can also be provided instead of `xs` and `ys` +for specifying the triangles, otherwise an unconstrained triangulation of `xs` and `ys` is computed. ## Attributes @@ -14,7 +16,7 @@ and vertical positions `ys`. - `mode = :normal` sets the way in which a vector of levels is interpreted, if it's set to `:relative`, each number is interpreted as a fraction between the minimum and maximum values of `zs`. For example, `levels = 0.1:0.1:1.0` would exclude the lower 10% of data. - `extendlow = nothing`. This sets the color of an optional additional band from `minimum(zs)` to the lowest value in `levels`. If it's `:auto`, the lower end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. - `extendhigh = nothing`. This sets the color of an optional additional band from the highest value of `levels` to `maximum(zs)`. If it's `:auto`, the high end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. -- `triangulation = DelaunayTriangulation()`. The mode with which the points in `xs` and `ys` are triangulated. Passing `DelaunayTriangulation()` performs a delaunay triangulation. You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` with size (3, n), where each column specifies the vertex indices of one triangle. +- `triangulation = DelaunayTriangulation()`. The mode with which the points in `xs` and `ys` are triangulated. Passing `DelaunayTriangulation()` performs a Delaunay triangulation. You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` with size (3, n), where each column specifies the vertex indices of one triangle, or as a `Triangulation` from DelaunayTriangulation.jl. ### Generic @@ -41,12 +43,33 @@ $(ATTRIBUTES) nan_color = :transparent, inspectable = theme(scene, :inspectable), transparency = false, - triangulation = DelaunayTriangulation() + triangulation = DelaunayTriangulation(), + edges = nothing, ) end -function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}) - map(x -> elconvert(Float32, x), (x, y, z)) +function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}) + return (:triangulation,) +end + +function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; + triangulation=DelaunayTriangulation()) + z = elconvert(Float32, z) + points = [x'; y'] + if triangulation isa DelaunayTriangulation + tri = DelTri.triangulate(points) + elseif !(triangulation isa DelTri.Triangulation) + # Wrap user's provided triangulation into a Triangulation. Their triangulation must be such that DelTri.add_triangle! is defined. + if typeof(triangulation) <: AbstractMatrix{<:Int} && size(triangulation, 1) != 3 + triangulation = triangulation' + end + tri = DelTri.Triangulation(points) + triangles = DelTri.get_triangles(tri) + for τ in DelTri.each_solid_triangle(triangulation) + DelTri.add_triangle!(triangles, τ) + end + end + return (tri, z) end function compute_contourf_colormap(levels, cmap, elow, ehigh) @@ -90,8 +113,8 @@ function compute_highcolor(eh, cmap) end end -function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractVector{<:Real},<:AbstractVector{<:Real}}}) - xs, ys, zs = c[1:3] +function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVector{<:Real}}}) + tri, zs = c[1:2] c.attributes[:_computed_levels] = lift(c, zs, c.levels, c.mode) do zs, levels, mode return _get_isoband_levels(Val(mode), levels, vec(zs)) @@ -117,7 +140,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV polys = Observable(PolyType[]) colors = Observable(Float64[]) - function calculate_polys(xs, ys, zs, levels::Vector{Float32}, is_extended_low, is_extended_high, triangulation) + function calculate_polys(triangulation, zs, levels::Vector{Float32}, is_extended_low, is_extended_high) empty!(polys[]) empty!(colors[]) @@ -131,7 +154,10 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV lows = levels[1:end-1] highs = levels[2:end] - trianglelist = compute_triangulation(triangulation, xs, ys) + xs = [DelTri.getx(p) for p in DelTri.each_point(triangulation)] # each_point preserves indices + ys = [DelTri.gety(p) for p in DelTri.each_point(triangulation)] + + trianglelist = compute_triangulation(triangulation) filledcontours = filled_tricontours(xs, ys, zs, trianglelist, levels) levelcenters = (highs .+ lows) ./ 2 @@ -154,10 +180,10 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV return end - onany(calculate_polys, c, xs, ys, zs, c._computed_levels, is_extended_low, is_extended_high, c.triangulation) + onany(calculate_polys, c, tri, zs, c._computed_levels, is_extended_low, is_extended_high) # onany doesn't get called without a push, so we call # it on a first run! - calculate_polys(xs[], ys[], zs[], c._computed_levels[], is_extended_low[], is_extended_high[], c.triangulation[]) + calculate_polys(tri[], zs[], c._computed_levels[], is_extended_low[], is_extended_high[]) poly!(c, polys, @@ -175,16 +201,8 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV ) end -function compute_triangulation(::DelaunayTriangulation, xs, ys) - vertices = [xs'; ys'] - return MiniQhull.delaunay(vertices) -end - -function compute_triangulation(triangulation::AbstractMatrix{<:Int}, xs, ys) - if size(triangulation, 1) != 3 - throw(ArgumentError("Triangulation matrix must be of size (3, n) but is of size $(size(triangulation)).")) - end - triangulation +function compute_triangulation(tri) + return [T[j] for T in DelTri.each_solid_triangle(tri), j in 1:3]' end # FIXME: TriplotBase augments levels so here the implementation is just repeated without that step From 41fc3ee628e7f6ce2610c607ec564b832987422d Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Fri, 30 Jun 2023 12:42:33 +0200 Subject: [PATCH 09/14] Update NEWS.md (#3036) --- NEWS.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9128a116a20..4cd6ff4b3a1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,29 +2,30 @@ ## master -- Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950) -- Fix broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953) -- Scale errorbar whiskers and bracket correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) -- Update bracket when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) +- Added the ability to use custom triangulations from DelaunayTriangulation.jl [#2896](https://github.com/MakieOrg/Makie.jl/pull/2896). +- Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950). +- Fixed broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). +- Scaled `errorbar` whiskers and `bracket` correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). +- Updated `bracket` when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). ## v0.19.6 -- Fix broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). -- Fix WGLMakie JS popup [#2976](https://github.com/MakieOrg/Makie.jl/pull/2976). -- Fix legendelements when children have no elements [#2982](https://github.com/MakieOrg/Makie.jl/pull/2982). -- Bump compat for StatsBase to 0.34 [#2915](https://github.com/MakieOrg/Makie.jl/pull/2915). +- Fixed broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). +- Fixed WGLMakie JS popup [#2976](https://github.com/MakieOrg/Makie.jl/pull/2976). +- Fixed `legendelements` when children have no elements [#2982](https://github.com/MakieOrg/Makie.jl/pull/2982). +- Bumped compat for StatsBase to 0.34 [#2915](https://github.com/MakieOrg/Makie.jl/pull/2915). - Improved thread safety [#2840](https://github.com/MakieOrg/Makie.jl/pull/2840). ## v0.19.5 -- Add `loop` option for GIF outputs when recording videos with `record` [#2891](https://github.com/MakieOrg/Makie.jl/pull/2891). -- More fixes for line rendering in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). +- Added `loop` option for GIF outputs when recording videos with `record` [#2891](https://github.com/MakieOrg/Makie.jl/pull/2891). +- Fixed line rendering issues in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). - Fixed incorrect line alpha in dense lines in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). -- Change `scene.clear` to an observable and make changes in `Scene` Observables trigger renders in GLMakie [#2929](https://github.com/MakieOrg/Makie.jl/pull/2929). +- Changed `scene.clear` to an observable and made changes in `Scene` Observables trigger renders in GLMakie [#2929](https://github.com/MakieOrg/Makie.jl/pull/2929). - Added contour labels [#2496](https://github.com/MakieOrg/Makie.jl/pull/2496). -- Allow rich text to be used in Legends [#2902](https://github.com/MakieOrg/Makie.jl/pull/2902). -- More support for zero length Geometries [#2917](https://github.com/MakieOrg/Makie.jl/pull/2917). -- Make CairoMakie drawing for polygons with holes order independent [#2918](https://github.com/MakieOrg/Makie.jl/pull/2918). +- Allowed rich text to be used in Legends [#2902](https://github.com/MakieOrg/Makie.jl/pull/2902). +- Added more support for zero length Geometries [#2917](https://github.com/MakieOrg/Makie.jl/pull/2917). +- Made CairoMakie drawing for polygons with holes order independent [#2918](https://github.com/MakieOrg/Makie.jl/pull/2918). - Fixes for `Makie.inline!()`, allowing now for `Makie.inline!(automatic)` (default), which is better at automatically opening a window/ inlining a plot into plotpane when needed [#2919](https://github.com/MakieOrg/Makie.jl/pull/2919) [#2937](https://github.com/MakieOrg/Makie.jl/pull/2937). - Block/Axis doc improvements [#2940](https://github.com/MakieOrg/Makie.jl/pull/2940) [#2932](https://github.com/MakieOrg/Makie.jl/pull/2932) [#2894](https://github.com/MakieOrg/Makie.jl/pull/2894). From aac647c754e5931b90352e1175ae083fa892ba00 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Fri, 30 Jun 2023 12:57:06 +0200 Subject: [PATCH 10/14] Improve `set_close_to!` docstring (#3037) * add function signature to set_close_to! docstring see https://github.com/MakieOrg/Makie.jl/issues/794#issuecomment-1613872576 * slight correction --------- Co-authored-by: Ben Arthur --- src/makielayout/blocks/slider.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/makielayout/blocks/slider.jl b/src/makielayout/blocks/slider.jl index 51941b1be6a..28ebc809db4 100644 --- a/src/makielayout/blocks/slider.jl +++ b/src/makielayout/blocks/slider.jl @@ -191,7 +191,11 @@ function closest_index_inexact(sliderrange, value) end """ + set_close_to!(slider, value) -> closest_value + Set the `slider` to the value in the slider's range that is closest to `value` and return this value. +This function should be used to set a slider to a value programmatically, rather than +mutating its value observable directly, which doesn't update the slider visually. """ function set_close_to!(slider::Slider, value) closest = closest_index(slider.range[], value) From 80735a26211bbdc60243d13b7ed0663b3758d8c4 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 3 Jul 2023 04:47:15 -0400 Subject: [PATCH 11/14] Better error message when the GL status is not `GL_FRAMEBUFFER_COMPLETE` (#3014) * Better error message when the GL status is not `GL_FRAMEBUFFER_COMPLETE` For full effect, this should probably use a lookup table/Dict. Ref #3010 * improve error messages --------- Co-authored-by: SimonDanisch --- GLMakie/src/glwindow.jl | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index d643f609c5b..df0836b308b 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -56,6 +56,36 @@ function attach_colorbuffer!(fb::GLFramebuffer, key::Symbol, t::Texture{T, 2}) w return next_color_id end +function enum_to_error(s) + s == GL_FRAMEBUFFER_COMPLETE && return + s == GL_FRAMEBUFFER_UNDEFINED && + error("GL_FRAMEBUFFER_UNDEFINED: The specified framebuffer is the default read or draw framebuffer, but the default framebuffer does not exist.") + s == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: At least one of the framebuffer attachment points is incomplete.") + s == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: The framebuffer does not have at least one image attached to it.") + s == GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: The value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for any color attachment point(s) specified by GL_DRAW_BUFFERi.") + s == GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: GL_READ_BUFFER is not GL_NONE and the value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for the color attachment point specified by GL_READ_BUFFER.") + s == GL_FRAMEBUFFER_UNSUPPORTED && + error("GL_FRAMEBUFFER_UNSUPPORTED: The combination of internal formats of the attached images violates a driver implementation-dependent set of restrictions. Check your OpenGL driver!") + s == GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && + error("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: The value of GL_RENDERBUFFER_SAMPLES is not the same for all attached renderbuffers; +if the value of GL_TEXTURE_SAMPLES is not the same for all attached textures; or, if the attached images consist of a mix of renderbuffers and textures, + the value of GL_RENDERBUFFER_SAMPLES does not match the value of GL_TEXTURE_SAMPLES. + GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE is also returned if the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not consistent across all attached textures; + or, if the attached images include a mix of renderbuffers and textures, the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not set to GL_TRUE for all attached textures.") + s == GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS && + error("GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: Any framebuffer attachment is layered, and any populated attachment is not layered, or if all populated color attachments are not from textures of the same target.") + return error("Unknown framebuffer completion error code: $s") +end + +function check_framebuffer() + status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + return enum_to_error(status) +end + function GLFramebuffer(fb_size::NTuple{2, Int}) # Create framebuffer frambuffer_id = glGenFramebuffers() @@ -92,8 +122,7 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) attach_framebuffer(depth_buffer, GL_DEPTH_ATTACHMENT) attach_framebuffer(depth_buffer, GL_STENCIL_ATTACHMENT) - status = glCheckFramebufferStatus(GL_FRAMEBUFFER) - @assert status == GL_FRAMEBUFFER_COMPLETE + check_framebuffer() fb_size_node = Observable(fb_size) From a82e8a52e935270336c034cdf89a231b18e8da21 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Mon, 3 Jul 2023 10:51:11 +0200 Subject: [PATCH 12/14] Fix bezier marker stroke (#2961) * add missing ClosePath * update NEWS [skip ci] * test bezier stroke [skip ci] * fix stroke/glow/AA scaling * remove extra padding in Bezier path renders * mention GLMakie changes * fix dot --------- Co-authored-by: Simon --- NEWS.md | 3 ++- ReferenceTests/src/tests/primitives.jl | 21 +++++++++++++++++++++ WGLMakie/test/runtests.jl | 3 ++- src/bezier.jl | 25 +++++++++++++------------ src/utilities/texture_atlas.jl | 7 +++---- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4cd6ff4b3a1..24d6b6024f8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,12 +2,13 @@ ## master +- Fix incomplete stroke with some Bezier markers in CairoMakie and blurry strokes in GLMakie [#2961](https://github.com/MakieOrg/Makie.jl/pull/2961) - Added the ability to use custom triangulations from DelaunayTriangulation.jl [#2896](https://github.com/MakieOrg/Makie.jl/pull/2896). - Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950). -- Fixed broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). - Scaled `errorbar` whiskers and `bracket` correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). - Updated `bracket` when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). + ## v0.19.6 - Fixed broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 211f350862b..4327bf8ab7e 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -253,6 +253,27 @@ end f end +@reference_test "BezierPath marker stroke" begin + f = Figure(resolution = (800, 800)) + ax = Axis(f[1, 1]) + + # Same as above + markers = [ + :rect, :circle, :cross, :x, :utriangle, :rtriangle, :dtriangle, :ltriangle, :pentagon, + :hexagon, :octagon, :star4, :star5, :star6, :star8, :vline, :hline, 'x', 'X' + ] + + for (i, marker) in enumerate(markers) + scatter!( + Point2f.(1:5, i), marker = marker, + markersize = range(10, 30, length = 5), color = :orange, + strokewidth = 2, strokecolor = :black + ) + end + + f +end + @reference_test "complex_bezier_markers" begin f = Figure(resolution = (800, 800)) diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 667b8d19bc1..20b317aecaa 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -54,7 +54,8 @@ excludes = Set([ "scatter with stroke", "scatter with glow", "lines and linestyles", - "Textured meshscatter" # not yet implemented + "Textured meshscatter", # not yet implemented + "BezierPath marker stroke", # not yet implemented ]) Makie.inline!(Makie.automatic) diff --git a/src/bezier.jl b/src/bezier.jl index 41d4cd27582..d9074dcb7dd 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -201,7 +201,8 @@ function bezier_ngon(n, radius, angle) for a in range(0, 2pi, length = n+1)[1:end-1]] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]) + LineTo.(points[2:end]); + ClosePath() ]) end @@ -212,7 +213,8 @@ function bezier_star(n, inner_radius, outer_radius, angle) for (i, a) in enumerate(range(0, 2pi, length = 2n+1)[1:end-1])] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]) + LineTo.(points[2:end]); + ClosePath() ]) end @@ -493,24 +495,23 @@ function render_path(path, bitmap_size_px = 256) # in the outline, 1 unit = 1/64px scale_factor = bitmap_size_px * 64 - # we transform the path into the unit square and we can - # scale and translate this to a 4096x4096 grid, which is 64px x 64px - # when rendered to bitmap + # We transform the path into a rectangle of size (aspect, 1) or (1, aspect) + # such that aspect ≤ 1. We then scale that rectangle up to a size of 4096 by + # 4096 * aspect, which results in at most a 64px by 64px bitmap # freetype has no ClosePath and EllipticalArc, so those need to be replaced path_replaced = replace_nonfreetype_commands(path) - path_unit_square = fit_to_unit_square(path_replaced, false) + aspect = widths(bbox(path)) / maximum(widths(bbox(path))) + path_unit_rect = fit_to_bbox(path_replaced, Rect2f(Point2f(0), aspect)) - path_transformed = Makie.scale( - path_unit_square, - scale_factor, - ) + path_transformed = Makie.scale(path_unit_rect, scale_factor) outline_ref = make_outline(path_transformed) - w = bitmap_size_px - h = bitmap_size_px + # Adjust bitmap size to match path aspect + w = ceil(Int, bitmap_size_px * aspect[1]) + h = ceil(Int, bitmap_size_px * aspect[2]) pitch = w * 1 # 8 bit gray pixelbuffer = zeros(UInt8, h * pitch) bitmap_ref = Ref{FT_Bitmap}() diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 7dfb92c9c04..bc413a87273 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -70,7 +70,7 @@ function Base.show(io::IO, atlas::TextureAtlas) println(io, " font_render_callback: ", length(atlas.font_render_callback)) end -const SERIALIZATION_FORMAT_VERSION = "v1" +const SERIALIZATION_FORMAT_VERSION = "v2" # basically a singleton for the textureatlas function get_cache_path(resolution::Int, pix_per_glyph::Int) @@ -486,8 +486,7 @@ end function marker_scale_factor(atlas::TextureAtlas, path::BezierPath) # padded_width = (unpadded_target_width + unpadded_target_width * pad_per_unit) - path_width = widths(Makie.bbox(path)) - return (1f0 .+ bezierpath_pad_scale_factor(atlas, path)) .* path_width + return (1f0 .+ bezierpath_pad_scale_factor(atlas, path)) .* widths(Makie.bbox(path)) end function rescale_marker(atlas::TextureAtlas, pathmarker::BezierPath, font, markersize) @@ -512,7 +511,7 @@ end function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2, markeroffset::Vec2) bb = bbox(bp) - pad_offset = (origin(bb) .- 0.5f0 .* bezierpath_pad_scale_factor(atlas, bp) .* widths(bb)) + pad_offset = origin(bb) .- 0.5f0 .* bezierpath_pad_scale_factor(atlas, bp) .* widths(bb) return markersize .* pad_offset end From ea54d0f7679a98ce47fb8e2d27042db8fbd4b6ca Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Mon, 3 Jul 2023 16:05:45 +0200 Subject: [PATCH 13/14] Fix transformation handling in DataInspector (#3002) * copy inspector changes from ff/camera * fix most transforms * fix volumeslices * fix dublicate transform_func application * clean up function signature * fix band * fix bad indicator clearing + transform_func * fix meshscatter * fix surface * fix mesh * respect space & fix contourf * update NEWS [skip ci] * rename and reorganize ray casting functionality * fix error on failed pick * fix scene offset of mouse position * add some tests + fixes * add value tests for position_on_plot() --------- Co-authored-by: Simon --- NEWS.md | 1 + src/Makie.jl | 1 + src/interaction/inspector.jl | 357 ++++++++++------------------- src/interaction/ray_casting.jl | 390 ++++++++++++++++++++++++++++++++ src/layouting/transformation.jl | 30 ++- test/ray_casting.jl | 163 +++++++++++++ test/runtests.jl | 1 + 7 files changed, 707 insertions(+), 236 deletions(-) create mode 100644 src/interaction/ray_casting.jl create mode 100644 test/ray_casting.jl diff --git a/NEWS.md b/NEWS.md index 24d6b6024f8..3053736b4c9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ ## master +- Fixed DataInspector interaction with transformations [#3002](https://github.com/MakieOrg/Makie.jl/pull/3002) - Fix incomplete stroke with some Bezier markers in CairoMakie and blurry strokes in GLMakie [#2961](https://github.com/MakieOrg/Makie.jl/pull/2961) - Added the ability to use custom triangulations from DelaunayTriangulation.jl [#2896](https://github.com/MakieOrg/Makie.jl/pull/2896). - Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950). diff --git a/src/Makie.jl b/src/Makie.jl index 64a19ed55e8..cb3e0f56d59 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -176,6 +176,7 @@ include("stats/hexbin.jl") # Interactiveness include("interaction/events.jl") include("interaction/interactive_api.jl") +include("interaction/ray_casting.jl") include("interaction/inspector.jl") # documentation and help functions diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 414e182f7fd..849c43f8392 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -73,53 +73,6 @@ function closest_point_on_line(A::Point2f, B::Point2f, P::Point2f) A .+ AB * dot(AP, AB) / dot(AB, AB) end -function view_ray(scene) - inv_projview = inv(camera(scene).projectionview[]) - view_ray(inv_projview, events(scene).mouseposition[], pixelarea(scene)[]) -end -function view_ray(inv_view_proj, mpos, area::Rect2) - # This figures out the camera view direction from the projectionview matrix (?) - # and computes a ray from a near and a far point. - # Based on ComputeCameraRay from ImGuizmo - mp = 2f0 .* (mpos .- minimum(area)) ./ widths(area) .- 1f0 - v = inv_view_proj * Vec4f(0, 0, -10, 1) - reversed = v[3] < v[4] - near = reversed ? 1f0 - 1e-6 : 0f0 - far = reversed ? 0f0 : 1f0 - 1e-6 - - origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) - origin = origin[Vec(1, 2, 3)] ./ origin[4] - - p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) - p = p[Vec(1, 2, 3)] ./ p[4] - - dir = normalize(p .- origin) - return origin, dir -end - - -# These work in 2D and 3D -function closest_point_on_line(A, B, origin, dir) - closest_point_on_line( - to_ndim(Point3f, A, 0), - to_ndim(Point3f, B, 0), - to_ndim(Point3f, origin, 0), - to_ndim(Vec3f, dir, 0) - ) -end -function closest_point_on_line(A::Point3f, B::Point3f, origin::Point3f, dir::Vec3f) - # See: - # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection - AB_norm = norm(B .- A) - u_AB = (B .- A) / AB_norm - u_dir = normalize(dir) - u_perp = normalize(cross(u_dir, u_AB)) - # e_RD, e_perp defines a plane with normal n - n = normalize(cross(u_dir, u_perp)) - t = dot(origin .- A, n) / dot(u_AB, n) - A .+ clamp(t, 0.0, AB_norm) * u_AB -end - function point_in_triangle(A::Point2, B::Point2, C::Point2, P::Point2, ϵ = 1e-6) # adjusted from ray_triangle_intersection AO = A .- P @@ -132,39 +85,6 @@ function point_in_triangle(A::Point2, B::Point2, C::Point2, P::Point2, ϵ = 1e-6 return (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) end -function ray_triangle_intersection(A, B, C, origin, dir, ϵ = 1e-6) - # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html - AO = A .- origin - BO = B .- origin - CO = C .- origin - A1 = 0.5 * dot(cross(BO, CO), dir) - A2 = 0.5 * dot(cross(CO, AO), dir) - A3 = 0.5 * dot(cross(AO, BO), dir) - - e = 1e-3 - if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) - Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) - else - Point3f(NaN) - end -end - - -### Surface positions -######################################## - -surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) -surface_x(xs, i, j, N) = xs[i] -surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] - -surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) -surface_y(ys, i, j, N) = ys[j] -surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] - -function surface_pos(xs, ys, zs, i, j) - N, M = size(zs) - Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) -end ### Mapping mesh vertex indices to Vector{Polygon} index @@ -216,20 +136,20 @@ function point_in_quad_parameter( # Our initial guess is that P is in the center of the quad (in terms of AB and DC) f = 0.5 - - for i in 0:iterations + AB = B - A + DC = C - D + for _ in 0:iterations # vector between top and bottom point of the current line dir = (D + f * (C - D)) - (A + f * (B - A)) - DC = C - D - AB = B - A # solves P + _ * dir = A + f1 * (B - A) (intersection point of ray & line) f1, _ = inv(Mat2f(AB..., dir...)) * (P - A) f2, _ = inv(Mat2f(DC..., dir...)) * (P - D) # next fraction estimate should be between f1 and f2 # adding 2f to this helps avoid jumping between low and high values + old_f = f f = 0.25 * (2f + f1 + f2) - if abs(f2 - f1) < epsilon + if abs(old_f - f) < epsilon return f end end @@ -241,11 +161,13 @@ end ## Shifted projection ######################################## -function shift_project(scene, plot, pos) +@deprecate shift_project(scene, plot, pos) shift_project(scene, pos) false + +function shift_project(scene, pos) project( camera(scene).projectionview[], Vec2f(widths(pixelarea(scene)[])), - apply_transform(transform_func(plot), pos, to_value(get(plot, :space, :data))) + pos ) .+ Vec2f(origin(pixelarea(scene)[])) end @@ -417,8 +339,8 @@ function show_data_recursion(inspector, plot, idx) show_data(inspector, plot, idx) end - if processed - inspector.selection = plot + if processed && inspector.selection != plot + clear_temporary_plots!(inspector, plot) end return processed @@ -439,8 +361,8 @@ function show_data_recursion(inspector, plot::AbstractPlot, idx, source) show_data(inspector, plot, idx, source) end - if processed - inspector.selection = plot + if processed && inspector.selection != plot + clear_temporary_plots!(inspector, plot) end return processed @@ -500,13 +422,14 @@ function show_data(inspector::DataInspector, plot::Scatter, idx) tt = inspector.plot scene = parent_scene(plot) - proj_pos = shift_project(scene, plot, to_ndim(Point3f, plot[1][][idx], 0)) + pos = position_on_plot(plot, idx) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, plot[1][][idx]) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else - tt.text[] = position2string(plot[1][][idx]) + tt.text[] = position2string(pos) end tt.offset[] = ifelse( a.apply_tooltip_offset[], @@ -526,11 +449,9 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) scene = parent_scene(plot) if a.enable_indicators[] - T = transformationmatrix( - plot[1][][idx], - _to_scale(plot.markersize[], idx), - _to_rotation(plot.rotations[], idx) - ) + translation = apply_transform_and_model(plot, plot[1][][idx]) + rotation = _to_rotation(plot.rotations[], idx) + scale = _to_scale(plot.markersize[], idx) if inspector.selection != plot clear_temporary_plots!(inspector, plot) @@ -545,9 +466,12 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) bbox = Rect{3, Float32}(convert_attribute( plot.marker[], Key{:marker}(), Key{Makie.plotkey(plot)}() )) + T = Transformation( + identity; translation = translation, rotation = rotation, scale = scale + ) p = wireframe!( - scene, bbox, model = T, color = a.indicator_color, + scene, bbox, transformation = T, color = a.indicator_color, linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false ) @@ -558,21 +482,21 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) elseif !isempty(inspector.temp_plots) p = inspector.temp_plots[1] - p.model[] = T - + transform!(p, translation = translation, scale = scale, rotation = rotation) end a.indicator_visible[] = true end - proj_pos = shift_project(scene, plot, to_ndim(Point3f, plot[1][][idx], 0)) + pos = position_on_plot(plot, idx) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, plot[1][][idx]) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else - tt.text[] = position2string(plot[1][][idx]) + tt.text[] = position2string(pos) end tt.visible[] = true @@ -586,11 +510,9 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i scene = parent_scene(plot) # cast ray from cursor into screen, find closest point to line - p0, p1 = plot[1][][idx-1:idx] - origin, dir = view_ray(scene) - pos = closest_point_on_line(p0, p1, origin, dir) + pos = position_on_plot(plot, idx) - proj_pos = shift_project(scene, plot, to_ndim(Point3f, pos, 0)) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) tt.offset[] = ifelse( @@ -600,9 +522,9 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i ) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, typeof(p0)(pos)) + tt.text[] = plot[:inspector_label][](plot, idx, eltype(plot[1][])(pos)) else - tt.text[] = position2string(typeof(p0)(pos)) + tt.text[] = position2string(eltype(plot[1][])(pos)) end tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) @@ -616,7 +538,14 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) tt = inspector.plot scene = parent_scene(plot) - bbox = boundingbox(plot) + # Manual boundingbox including transfunc + bbox = let + points = point_iterator(plot) + trans_func = transform_func(plot) + model = plot.model[] + iter = iterate_transformed(points, model, to_value(get(plot, :space, :data)), trans_func) + limits_from_transformed_points(iter) + end proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) @@ -632,7 +561,8 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) end p = wireframe!( - scene, bbox, color = a.indicator_color, + scene, bbox, color = a.indicator_color, + transformation = Transformation(), linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false ) @@ -663,43 +593,11 @@ end function show_data(inspector::DataInspector, plot::Surface, idx) a = inspector.attributes tt = inspector.plot - scene = parent_scene(plot) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) - xs = plot[1][] - ys = plot[2][] - zs = plot[3][] - w, h = size(zs) - _i = mod1(idx, w); _j = div(idx-1, w) - - # This isn't the most accurate so we include some neighboring faces - origin, dir = view_ray(scene) - pos = Point3f(NaN) - for i in _i-1:_i+1, j in _j-1:_j+1 - (1 <= i <= w) && (1 <= j < h) || continue - - if i - 1 > 0 - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i-1, j), - surface_pos(xs, ys, zs, i, j+1), - origin, dir - ) - end - - if i + 1 <= w && isnan(pos) - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i, j+1), - surface_pos(xs, ys, zs, i+1, j+1), - origin, dir - ) - end - - isnan(pos) || break - end + pos = position_on_plot(plot, idx) if !isnan(pos) tt[1][] = proj_pos @@ -730,20 +628,22 @@ function show_imagelike(inspector, plot, name, edge_based) a = inspector.attributes tt = inspector.plot scene = parent_scene(plot) - mpos = mouseposition(scene) - if plot.interpolate[] - i, j, z = _interpolated_getindex(plot[1][], plot[2][], plot[3][], mpos) - x, y = mpos - else - i, j, z = _pixelated_getindex(plot[1][], plot[2][], plot[3][], mpos, edge_based) - x = i; y = j + pos = position_on_plot(plot, -1, apply_transform = false)[Vec(1, 2)] # index irrelevant + + # Not on image/heatmap + if isnan(pos) + a.indicator_visible[] = false + tt.visible[] = false + return true end - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), Point3f(mpos[1], mpos[2], z)) + if plot.interpolate[] + i, j, z = _interpolated_getindex(plot[1][], plot[2][], plot[3][], pos) + x, y = pos else - tt.text[] = color2text(name, x, y, z) + i, j, z = _pixelated_getindex(plot[1][], plot[2][], plot[3][], pos, edge_based) + x = i; y = j end # in case we hover over NaN values @@ -753,6 +653,12 @@ function show_imagelike(inspector, plot, name, edge_based) return true end + if haskey(plot, :inspector_label) + tt.text[] = plot[:inspector_label][](plot, (i, j), Point3f(pos[1], pos[2], z)) + else + tt.text[] = color2text(name, x, y, z) + end + a._color[] = if z isa AbstractFloat interpolated_getindex( to_colormap(plot.colormap[]), z, @@ -762,41 +668,44 @@ function show_imagelike(inspector, plot, name, edge_based) z end - position = to_ndim(Point3f, mpos, 0) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) if a.enable_indicators[] if plot.interpolate[] - if inspector.selection != plot + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + !(inspector.temp_plots[1] isa Scatter) clear_temporary_plots!(inspector, plot) p = scatter!( - scene, position, color = a._color, + scene, pos, color = a._color, visible = a.indicator_visible, - inspectable = false, - marker=:rect, markersize = map(r -> 3r, a.range), + inspectable = false, model = plot.model, + # TODO switch to Rect with 2r-1 or 2r-2 markersize to have + # just enough space to always detect the underlying image + marker=:rect, markersize = map(r -> 2r, a.range), strokecolor = a.indicator_color, - strokewidth = a.indicator_linewidth + strokewidth = a.indicator_linewidth, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) - elseif !isempty(inspector.temp_plots) + else p = inspector.temp_plots[1] - p[1].val[1] = position + p[1].val[1] = pos notify(p[1]) end else bbox = _pixelated_image_bbox(plot[1][], plot[2][], plot[3][], i, j, edge_based) - if inspector.selection != plot + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + !(inspector.temp_plots[1] isa Wireframe) clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, bbox, color = a.indicator_color, + scene, bbox, color = a.indicator_color, model = plot.model, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, - visible = a.indicator_visible, inspectable = false + visible = a.indicator_visible, inspectable = false, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) - elseif !isempty(inspector.temp_plots) + else p = inspector.temp_plots[1] p[1][] = bbox end @@ -902,8 +811,8 @@ function show_data(inspector::DataInspector, plot::BarPlot, idx) tt = inspector.plot scene = parent_scene(plot) - pos = plot[1][][idx] - proj_pos = shift_project(scene, plot, to_ndim(Point3f, pos, 0)) + pos = apply_transform_and_model(plot, plot[1][][idx]) + proj_pos = shift_project(scene, to_ndim(Point3f, pos, 0)) update_tooltip_alignment!(inspector, proj_pos) if a.enable_indicators[] @@ -942,9 +851,10 @@ function show_data(inspector::DataInspector, plot::Arrows, idx, ::LineSegments) return show_data(inspector, plot, div(idx+1, 2), nothing) end function show_data(inspector::DataInspector, plot::Arrows, idx, source) - a = inspector.plot.attributes + a = inspector.attributes tt = inspector.plot - pos = plot[1][][idx] + pos = apply_transform_and_model(plot, plot[1][][idx]) + mpos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, mpos) @@ -953,7 +863,7 @@ function show_data(inspector::DataInspector, plot::Arrows, idx, source) tt[1][] = mpos if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, mpos) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else tt.text[] = "Position:\n $p\nDirection:\n $v" end @@ -967,7 +877,7 @@ end # backend handle picking colors from a colormap function show_data(inspector::DataInspector, plot::Contourf, idx, source::Mesh) tt = inspector.plot - idx = show_poly(inspector, plot.plots[1], idx, source) + idx = show_poly(inspector, plot, plot.plots[1], idx, source) level = plot.plots[1].color[][idx] mpos = Point2f(mouseposition_px(inspector.root)) @@ -993,14 +903,13 @@ end # return true # end -function show_poly(inspector, plot, idx, source) +function show_poly(inspector, plot, poly, idx, source) a = inspector.attributes - idx = vertexindex2poly(plot[1][], idx) + idx = vertexindex2poly(poly[1][], idx) if a.enable_indicators[] - - line_collection = copy(convert_arguments(PointBased(), plot[1][][idx].exterior)[1]) - for int in plot[1][][idx].interiors + line_collection = copy(convert_arguments(PointBased(), poly[1][][idx].exterior)[1]) + for int in poly[1][][idx].interiors push!(line_collection, Point2f(NaN)) append!(line_collection, convert_arguments(PointBased(), int)[1]) end @@ -1010,11 +919,11 @@ function show_poly(inspector, plot, idx, source) clear_temporary_plots!(inspector, plot) p = lines!( - scene, line_collection, color = a.indicator_color, + scene, line_collection, color = a.indicator_color, + transformation = Transformation(source), strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, - visible = a.indicator_visible, inspectable = false + visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) elseif !isempty(inspector.temp_plots) @@ -1030,68 +939,46 @@ end function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Heatmap) a = inspector.attributes tt = inspector.plot - scene = parent_scene(plot) - proj_pos = Point2f(mouseposition_px(inspector.root)) - update_tooltip_alignment!(inspector, proj_pos) + pos = position_on_plot(child, -1, apply_transform = false)[Vec(1, 2)] # index irrelevant - qs = extrema(child[1][]) - ps = extrema(child[2][]) - data = child[3][] - T = child.transformation.model[] - - vs = [ # clockwise - Point3f(T * Point4f(qs[1], ps[1], 0, 1)), - Point3f(T * Point4f(qs[1], ps[2], 0, 1)), - Point3f(T * Point4f(qs[2], ps[2], 0, 1)), - Point3f(T * Point4f(qs[2], ps[1], 0, 1)) - ] - - origin, dir = view_ray(scene) - pos = Point3f(NaN) - pos = ray_triangle_intersection(vs[1], vs[2], vs[3], origin, dir) + # Not on heatmap if isnan(pos) - pos = ray_triangle_intersection(vs[3], vs[4], vs[1], origin, dir) + a.indicator_visible[] && (a.indicator_visible[] = false) + tt.visible[] = false + return true end - if !isnan(pos) - child_idx = findfirst(isequal(child), plot.plots) - if child_idx == 2 - x = pos[2]; y = pos[3] - elseif child_idx == 3 - x = pos[1]; y = pos[3] - else - x = pos[1]; y = pos[2] - end - i = clamp(round(Int, (x - qs[1]) / (qs[2] - qs[1]) * size(data, 1) + 0.5), 1, size(data, 1)) - j = clamp(round(Int, (y - ps[1]) / (ps[2] - ps[1]) * size(data, 2) + 0.5), 1, size(data, 2)) - val = data[i, j] + i, j, val = _pixelated_getindex(child[1][], child[2][], child[3][], pos, true) - tt[1][] = proj_pos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), pos) - else - tt.text[] = @sprintf( - "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", - pos[1], pos[2], pos[3], val - ) - end - tt.visible[] = true + proj_pos = Point2f(mouseposition_px(inspector.root)) + update_tooltip_alignment!(inspector, proj_pos) + tt[1][] = proj_pos + + world_pos = apply_transform_and_model(child, pos) + + if haskey(plot, :inspector_label) + tt.text[] = plot[:inspector_label][](plot, (i, j), world_pos) else - tt.visible[] = false + tt.text[] = @sprintf( + "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", + world_pos[1], world_pos[2], world_pos[3], val + ) end + + tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) return true end -function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) +function show_data(inspector::DataInspector, plot::Band, idx::Integer, mesh::Mesh) scene = parent_scene(plot) tt = inspector.plot a = inspector.attributes - pos = Point2f(mouseposition(scene)) + pos = Point2f(position_on_plot(mesh, idx, apply_transform = false)) #Point2f(mouseposition(scene)) ps1 = plot.converted[1][] ps2 = plot.converted[2][] @@ -1112,22 +999,20 @@ function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) # Draw the line if a.enable_indicators[] - model = plot.model[] - - if inspector.selection != plot || isempty(inspector.temp_plots) + # Why does this sometimes create 2+ plots + if inspector.selection != plot || (length(inspector.temp_plots) != 1) clear_temporary_plots!(inspector, plot) p = lines!( - scene, [P1, P2], model = model, + scene, [P1, P2], transformation = Transformation(plot.transformation), color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, - visible = a.indicator_visible, inspectable = false + visible = a.indicator_visible, inspectable = false, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[])) push!(inspector.temp_plots, p) elseif !isempty(inspector.temp_plots) p = inspector.temp_plots[1] p[1][] = [P1, P2] - p.model[] = model end a.indicator_visible[] = true @@ -1139,6 +1024,8 @@ function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) if haskey(plot, :inspector_label) tt.text[] = plot[:inspector_label][](plot, right, (P1, P2)) else + P1 = apply_transform_and_model(mesh, P1, Point2f) + P2 = apply_transform_and_model(mesh, P2, Point2f) tt.text[] = @sprintf("(%0.3f, %0.3f) .. (%0.3f, %0.3f)", P1[1], P1[2], P2[1], P2[2]) end tt.visible[] = true diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl new file mode 100644 index 00000000000..f87ffe4bae3 --- /dev/null +++ b/src/interaction/ray_casting.jl @@ -0,0 +1,390 @@ +################################################################################ +### Ray Generation +################################################################################ + +struct Ray + origin::Point3f + direction::Vec3f +end + +""" + ray_at_cursor(fig/ax/scene) + +Returns a Ray into the scene starting at the current cursor position. +""" +ray_at_cursor(x) = ray_at_cursor(get_scene(x)) +function ray_at_cursor(scene::Scene) + return Ray(scene, mouseposition_px(scene)) +end + +""" + Ray(scene[, cam = cameracontrols(scene)], xy) + +Returns a `Ray` into the given `scene` passing through pixel position `xy`. Note +that the pixel position should be relative to the origin of the scene, as it is +when calling `mouseposition_px(scene)`. +""" +Ray(scene::Scene, xy::VecTypes{2}) = Ray(scene, cameracontrols(scene), xy) + + +function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos + + u_z = normalize(viewdir) + u_x = normalize(cross(u_z, cam.upvector[])) + u_y = normalize(cross(u_x, u_z)) + + px_width, px_height = widths(scene.px_area[]) + aspect = px_width / px_height + rel_pos = 2 .* xy ./ (px_width, px_height) .- 1 + + if cam.attributes.projectiontype[] === Perspective + dir = (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) * tand(0.5 * cam.fov[]) + u_z + return Ray(cam.eyeposition[], normalize(dir)) + else + # Orthographic has consistent direction, but not starting point + origin = norm(viewdir) * (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) + return Ray(origin, normalize(viewdir)) + end +end + +function Ray(scene::Scene, cam::Camera2D, xy::VecTypes{2}) + rel_pos = xy ./ widths(scene.px_area[]) + pv = scene.camera.projectionview[] + m = Vec2f(pv[1, 1], pv[2, 2]) + b = Vec2f(pv[1, 4], pv[2, 4]) + origin = (2 * rel_pos .- 1 - b) ./ m + return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) +end + +function Ray(::Scene, ::PixelCamera, xy::VecTypes{2}) + return Ray(to_ndim(Point3f, xy, 10_000f0), Vec3f(0,0,-1)) +end + +function Ray(scene::Scene, ::RelativeCamera, xy::VecTypes{2}) + origin = xy ./ widths(scene.px_area[]) + return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) +end + +Ray(scene::Scene, cam, xy::VecTypes{2}) = ray_from_projectionview(scene, xy) + +# This method should always work +function ray_from_projectionview(scene::Scene, xy::VecTypes{2}) + inv_view_proj = inv(camera(scene).projectionview[]) + area = pixelarea(scene)[] + + # This figures out the camera view direction from the projectionview matrix + # and computes a ray from a near and a far point. + # Based on ComputeCameraRay from ImGuizmo + mp = 2f0 .* xy ./ widths(area) .- 1f0 + v = inv_view_proj * Vec4f(0, 0, -10, 1) + reversed = v[3] < v[4] + near = reversed ? 1f0 - 1e-6 : 0f0 + far = reversed ? 0f0 : 1f0 - 1e-6 + + origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) + origin = origin[Vec(1, 2, 3)] ./ origin[4] + + p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) + p = p[Vec(1, 2, 3)] ./ p[4] + + dir = normalize(p .- origin) + + return Ray(origin, dir) +end + + +function transform(M::Mat4f, ray::Ray) + p4d = M * to_ndim(Point4f, ray.origin, 1f0) + dir = normalize(M[Vec(1,2,3), Vec(1,2,3)] * ray.direction) + return Ray(p4d[Vec(1,2,3)] / p4d[4], dir) +end + + +################################################################################ +### Ray - object intersections +################################################################################ + + +# These work in 2D and 3D +function closest_point_on_line(A::VecTypes, B::VecTypes, ray::Ray) + return closest_point_on_line(to_ndim(Point3f, A, 0), to_ndim(Point3f, B, 0), ray) +end +function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) + # See: + # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection + AB_norm = norm(B .- A) + u_AB = (B .- A) / AB_norm + u_perp = normalize(cross(ray.direction, u_AB)) + # e_RD, e_perp defines a plane with normal n + n = normalize(cross(ray.direction, u_perp)) + t = dot(ray.origin .- A, n) / dot(u_AB, n) + return A .+ clamp(t, 0.0, AB_norm) * u_AB +end + + +function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) + # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html + # Alternative: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm + AO = A .- ray.origin + BO = B .- ray.origin + CO = C .- ray.origin + A1 = 0.5 * dot(cross(BO, CO), ray.direction) + A2 = 0.5 * dot(cross(CO, AO), ray.direction) + A3 = 0.5 * dot(cross(AO, BO), ray.direction) + + # all positive or all negative + if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) + return Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) + else + return Point3f(NaN) + end +end + +function ray_rect_intersection(rect::Rect2f, ray::Ray) + possible_hit = ray.origin - ray.origin[3] / ray.direction[3] * ray.direction + min = minimum(rect); max = maximum(rect) + if all(min <= possible_hit[Vec(1,2)] <= max) + return possible_hit + end + return Point3f(NaN) +end + + +function ray_rect_intersection(rect::Rect3f, ray::Ray) + mins = (minimum(rect) - ray.origin) ./ ray.direction + maxs = (maximum(rect) - ray.origin) ./ ray.direction + x, y, z = min.(mins, maxs) + possible_hit = max(x, y, z) + if possible_hit < minimum(max.(mins, maxs)) + return ray.origin + possible_hit * ray.direction + end + return Point3f(NaN) +end + +function is_point_on_ray(p::Point3f, ray::Ray) + diff = ray.origin - p + return abs(dot(diff, ray.direction)) ≈ abs(norm(diff)) +end + + +################################################################################ +### Ray casting (positions from ray-plot intersections) +################################################################################ + + +""" + ray_assisted_pick(fig/ax/scene[, xy = events(fig/ax/scene).mouseposition[], apply_transform = true]) + +This function performs a `pick` at the given pixel position `xy` and returns the +picked `plot`, `index` and world or input space `position::Point3f`. It is equivalent to +``` +plot, idx = pick(fig/ax/scene, xy) +ray = Ray(parent_scene(plot), xy .- minimum(pixelarea(parent_scene(plot))[])) +position = position_on_plot(plot, idx, ray, apply_transform = true) +``` +See [`position_on_plot`](@ref) for more information. +""" +function ray_assisted_pick(obj, xy = events(obj).mouseposition[]; apply_transform = true) + plot, idx = pick(get_scene(obj), xy) + isnothing(plot) && return (plot, idx, Point3f(NaN)) + scene = parent_scene(plot) + ray = Ray(scene, xy .- minimum(pixelarea(scene)[])) + pos = position_on_plot(plot, idx, ray, apply_transform = apply_transform) + return (plot, idx, pos) +end + + +""" + position_on_plot(plot, index[, ray::Ray; apply_transform = true]) + +This function calculates the world or input space position of a ray - plot +intersection with the result `plot, idx = pick(...)` and a ray cast from the +picked position. If there is no intersection `Point3f(NaN)` will be returned. + +This should be called as +``` +plot, idx = pick(ax, px_pos) +pos_in_ax = position_on_plot(plot, idx, Ray(ax, px_pos .- minimum(pixelarea(ax.scene)[]))) +``` +or more simply `plot, idx, pos_in_ax = ray_assisted_pick(ax, px_pos)`. + +You can switch between getting a position in world space (after applying +transformations like `log`, `translate!()`, `rotate!()` and `scale!()`) and +input space (the raw position data of the plot) by adjusting `apply_transform`. + +Note that `position_on_plot` is only implemented for primitive plot types, i.e. +the possible return types of `pick`. Depending on the plot type the calculation +differs: +- `scatter` and `meshscatter` return the position of the picked marker/mesh +- `text` is excluded, always returning `Point3f(NaN)` +- `volume` calculates the ray - rect intersection for its bounding box +- `lines` and `linesegments` return the closest point on the line to the ray +- `mesh` and `surface` check for ray-triangle intersections for every triangle containing the picked vertex +- `image` and `heatmap` check for ray-rect intersection +""" +function position_on_plot(plot::AbstractPlot, idx::Integer; apply_transform = true) + return position_on_plot( + plot, idx, ray_at_cursor(parent_scene(plot)); + apply_transform = apply_transform + ) +end + + +function position_on_plot(plot::Union{Scatter, MeshScatter}, idx, ray::Ray; apply_transform = true) + pos = to_ndim(Point3f, plot[1][][idx], 0f0) + if apply_transform && !isnan(pos) + return apply_transform_and_model(plot, pos) + else + return pos + end +end + +function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply_transform = true) + p0, p1 = apply_transform_and_model(plot, plot[1][][idx-1:idx]) + + pos = closest_point_on_line(p0, p1, ray) + + if apply_transform + return pos + else + p4d = inv(plot.model[]) * to_ndim(Point4f, pos, 1f0) + p3d = p4d[Vec(1, 2, 3)] / p4d[4] + itf = inverse_transform(transform_func(plot)) + return Makie.apply_transform(itf, p3d, get(plot, :space, :data)) + end +end + +function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_transform = true) + # Heatmap and Image are always a Rect2f. The transform function is currently + # not allowed to change this, so applying it should be fine. Applying the + # model matrix may add a z component to the Rect2f, which we can't represent. + # So we instead inverse-transform the ray + space = to_value(get(plot, :space, :data)) + p0, p1 = map(Point2f.(extrema(plot.x[]), extrema(plot.y[]))) do p + return Makie.apply_transform(transform_func(plot), p, space) + end + ray = transform(inv(plot.model[]), ray) + pos = ray_rect_intersection(Rect2f(p0, p1 - p0), ray) + + if apply_transform + p4d = plot.model[] * to_ndim(Point4f, to_ndim(Point3f, pos, 0), 1) + return p4d[Vec(1, 2, 3)] / p4d[4] + else + pos = Makie.apply_transform(inverse_transform(transform_func(plot)), pos, space) + return to_ndim(Point3f, pos, 0) + end +end + +function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) + positions = coordinates(plot.mesh[]) + ray = transform(inv(plot.model[]), ray) + tf = transform_func(plot) + space = to_value(get(plot, :space, :data)) + + for f in faces(plot.mesh[]) + if idx in f + p1, p2, p3 = positions[f] + p1, p2, p3 = Makie.apply_transform.(tf, (p1, p2, p3), space) + pos = ray_triangle_intersection(p1, p2, p3, ray) + if pos !== Point3f(NaN) + if apply_transform + p4d = plot.model[] * to_ndim(Point4f, pos, 1) + return Point3f(p4d) / p4d[4] + else + return Makie.apply_transform(inverse_transform(tf), pos, space) + end + end + end + end + + @info "Did not find $idx" + + return Point3f(NaN) +end + +# Handling indexing into different surface input types +surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) +surface_x(xs, i, j, N) = xs[i] +surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] + +surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) +surface_y(ys, i, j, N) = ys[j] +surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] + +function surface_pos(xs, ys, zs, i, j) + N, M = size(zs) + return Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) +end + +function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) + xs = plot[1][] + ys = plot[2][] + zs = plot[3][] + w, h = size(zs) + _i = mod1(idx, w); _j = div(idx-1, w) + + ray = transform(inv(plot.model[]), ray) + tf = transform_func(plot) + space = to_value(get(plot, :space, :data)) + + # This isn't the most accurate so we include some neighboring faces + pos = Point3f(NaN) + for i in _i-1:_i+1, j in _j-1:_j+1 + (1 <= i <= w) && (1 <= j < h) || continue + + if i - 1 > 0 + # transforms only apply to x and y coordinates of surfaces + A = surface_pos(xs, ys, zs, i, j) + B = surface_pos(xs, ys, zs, i-1, j) + C = surface_pos(xs, ys, zs, i, j+1) + A, B, C = map((A, B, C)) do p + xy = Makie.apply_transform(tf, Point2f(p), space) + Point3f(xy[1], xy[2], p[3]) + end + pos = ray_triangle_intersection(A, B, C, ray) + end + + if i + 1 <= w && isnan(pos) + A = surface_pos(xs, ys, zs, i, j) + B = surface_pos(xs, ys, zs, i, j+1) + C = surface_pos(xs, ys, zs, i+1, j+1) + A, B, C = map((A, B, C)) do p + xy = Makie.apply_transform(tf, Point2f(p), space) + Point3f(xy[1], xy[2], p[3]) + end + pos = ray_triangle_intersection(A, B, C, ray) + end + + isnan(pos) || break + end + + if apply_transform + p4d = plot.model[] * to_ndim(Point4f, pos, 1) + return p4d[Vec(1, 2, 3)] / p4d[4] + else + xy = Makie.apply_transform(inverse_transform(tf), Point2f(pos), space) + return Point3f(xy[1], xy[2], pos[3]) + end +end + +function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true) + min, max = Point3f.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) + + if apply_transform + min = apply_transform_and_model(plot, min) + max = apply_transform_and_model(plot, max) + return ray_rect_intersection(Rect3f(min, max .- min), ray) + else + min = Makie.apply_transform(transform_func(plot), min, get(plot, :space, :data)) + max = Makie.apply_transform(transform_func(plot), max, get(plot, :space, :data)) + ray = transform(inv(plot.model[]), ray) + pos = ray_rect_intersection(Rect3f(min, max .- min), ray) + return Makie.apply_transform(inverse_transform(plot), pos, get(plot, :space, :data)) + end +end + +position_on_plot(plot::Text, args...; kwargs...) = Point3f(NaN) +position_on_plot(plot::Nothing, args...; kwargs...) = Point3f(NaN) \ No newline at end of file diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index 9454f8b50fb..6ddc7cd53d0 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -205,6 +205,34 @@ transformation(x::Attributes) = x.transformation[] transform_func(x) = transform_func_obs(x)[] transform_func_obs(x) = transformation(x).transform_func +""" + apply_transform_and_model(plot, pos, output_type = Point3f) + apply_transform_and_model(model, transfrom_func, pos, output_type = Point3f) + + +Applies the transform function and model matrix (i.e. transformations from +`translate!`, `rotate!` and `scale!`) to the given input +""" +function apply_transform_and_model(plot::AbstractPlot, pos, output_type = Point3f) + return apply_transform_and_model( + plot.model[], transform_func(plot), pos, + to_value(get(plot, :space, :data)), + output_type + ) +end +function apply_transform_and_model(model::Mat4f, f, pos::VecTypes, space = :data, output_type = Point3f) + transformed = apply_transform(f, pos, space) + p4d = to_ndim(Point4f, to_ndim(Point3f, transformed, 0), 1) + p4d = model * p4d + p4d = p4d ./ p4d[4] + return to_ndim(output_type, p4d, NaN) +end +function apply_transform_and_model(model::Mat4f, f, positions::Vector, space = :data, output_type = Point3f) + return map(positions) do pos + apply_transform_and_model(model, f, pos, space, output_type) + end +end + """ apply_transform(f, data, space) Apply the data transform func to the data if the space matches one @@ -268,7 +296,7 @@ function apply_transform(f::PointTrans{N1}, point::Point{N2}) where {N1, N2} end function apply_transform(f, data::AbstractArray) - map(point-> apply_transform(f, point), data) + map(point -> apply_transform(f, point), data) end function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2}) diff --git a/test/ray_casting.jl b/test/ray_casting.jl new file mode 100644 index 00000000000..ec88508ba18 --- /dev/null +++ b/test/ray_casting.jl @@ -0,0 +1,163 @@ +@testset "Ray Casting" begin + @testset "View Rays" begin + scene = Scene() + xy = 0.5 * widths(pixelarea(scene)[]) + + orthographic_cam3d!(x) = cam3d!(x, perspectiveprojection = Makie.Orthographic) + + for set_cam! in (cam2d!, cam_relative!, campixel!, cam3d!, orthographic_cam3d!) + @testset "$set_cam!" begin + set_cam!(scene) + ray = Makie.Ray(scene, xy) + ref_ray = Makie.ray_from_projectionview(scene, xy) + # Direction matches and is normalized + @test ref_ray.direction ≈ ray.direction + @test norm(ray.direction) ≈ 1f0 + # origins are on the same ray + @test Makie.is_point_on_ray(ray.origin, ref_ray) + end + end + end + + + # transform() is used to apply a translation-rotation-scale matrix to rays + # instead of point like data + # Generate random point + transform + rot = Makie.rotation_between(rand(Vec3f), rand(Vec3f)) + model = Makie.transformationmatrix(rand(Vec3f), rand(Vec3f), rot) + point = Point3f(1) + rand(Point3f) + + # Generate rate that passes through transformed point + transformed = Point3f(model * Point4f(point..., 1)) + direction = (1 + 10*rand()) * rand(Vec3f) + ray = Makie.Ray(transformed + direction, normalize(direction)) + + @test Makie.is_point_on_ray(transformed, ray) + transformed_ray = Makie.transform(inv(model), ray) + @test Makie.is_point_on_ray(point, transformed_ray) + + + @testset "Intersections" begin + p = rand(Point3f) + v = rand(Vec3f) + ray = Makie.Ray(p + 10v, normalize(v)) + + # ray - line + w = cross(v, rand(Vec3f)) + A = p - 5w + B = p + 5w + result = Makie.closest_point_on_line(A, B, ray) + @test result ≈ p + + # ray - triangle + w2 = cross(v, w) + A = p - 5w - 5w2 + B = p + 5w + C = p + 5w2 + result = Makie.ray_triangle_intersection(A, B, C, ray) + @test result ≈ p + + # ray - rect3 + rect = Rect(Vec(A), 10w + 10w2 + 10v) + result = Makie.ray_rect_intersection(rect, ray) + @test Makie.is_point_on_ray(result, ray) + + # ray - rect2 + p2 = Point2f(ray.origin - ray.origin[3] / ray.direction[3] * ray.direction) + w = rand(Vec2f) + rect = Rect2f(p2 - 5w, 10w) + result = Makie.ray_rect_intersection(rect, ray) + @test result ≈ Point3f(p2..., 0) + end + + + # Note that these tests depend on the exact placement of plots and may + # error when cameras are adjusted + @testset "position_on_plot()" begin + + # Lines (2D) & Linesegments (3D) + ps = [exp(-0.01phi) * Point2f(cos(phi), sin(phi)) for phi in range(0, 20pi, length = 501)] + scene = Scene(resolution = (400, 400)) + p = lines!(scene, ps) + cam2d!(scene) + ray = Makie.Ray(scene, (325.0, 313.0)) + pos = Makie.position_on_plot(p, 157, ray) + @test pos ≈ Point3f(0.6087957666683925, 0.5513198993583837, 0.0) + + scene = Scene(resolution = (400, 400)) + p = linesegments!(scene, ps) + cam3d!(scene) + ray = Makie.Ray(scene, (238.0, 233.0)) + pos = Makie.position_on_plot(p, 178, ray) + @test pos ≈ Point3f(-0.7850463447725504, -0.15125213957100314, 0.0) + + + # Heatmap (2D) & Image (3D) + scene = Scene(resolution = (400, 400)) + p = heatmap!(scene, 0..1, -1..1, rand(10, 10)) + cam2d!(scene) + ray = Makie.Ray(scene, (228.0, 91.0)) + pos = Makie.position_on_plot(p, 0, ray) + @test pos ≈ Point3f(0.13999999, -0.54499996, 0.0) + + scene = Scene(resolution = (400, 400)) + p = image!(scene, -1..1, -1..1, rand(10, 10)) + cam3d!(scene) + ray = Makie.Ray(scene, (309.0, 197.0)) + pos = Makie.position_on_plot(p, 3, ray) + @test pos ≈ Point3f(-0.7830243, 0.8614166, 0.0) + + + # Mesh (3D) + scene = Scene(resolution = (400, 400)) + p = mesh!(scene, Rect3f(Point3f(0), Vec3f(1))) + cam3d!(scene) + ray = Makie.Ray(scene, (201.0, 283.0)) + pos = Makie.position_on_plot(p, 15, ray) + @test pos ≈ Point3f(0.029754717, 0.043159597, 1.0) + + # Surface (3D) + scene = Scene(resolution = (400, 400)) + p = surface!(scene, -2..2, -2..2, [sin(x) * cos(y) for x in -10:10, y in -10:10]) + cam3d!(scene) + ray = Makie.Ray(scene, (52.0, 238.0)) + pos = Makie.position_on_plot(p, 57, ray) + @test pos ≈ Point3f(0.80910987, -1.6090667, 0.137722) + + # Volume (3D) + scene = Scene(resolution = (400, 400)) + p = volume!(scene, rand(10, 10, 10)) + cam3d!(scene) + center!(scene) + ray = Makie.Ray(scene, (16.0, 306.0)) + pos = Makie.position_on_plot(p, 0, ray) + @test pos ≈ Point3f(10.0, 0.18444633, 9.989262) + end + + # For recreating the above: + #= + # Scene setup from tests: + scene = Scene(resolution = (400, 400)) + p = surface!(scene, -2..2, -2..2, [sin(x) * cos(y) for x in -10:10, y in -10:10]) + cam3d!(scene) + + pos = Observable(Point3f(0.5)) + on(events(scene).mousebutton, priority = 100) do event + if event.button == Mouse.left && event.action == Mouse.press + mp = events(scene).mouseposition[] + _p, idx = pick(scene, mp, 10) + pos[] = Makie.position_on_plot(p, idx) + println(_p == p) + println("ray = Makie.Ray(scene, $mp)") + println("pos = Makie.position_on_plot(p, $idx, ray)") + println("@test pos ≈ Point3f(", pos[][1], ", ", pos[][2], ", ", pos[][3], ")") + end + end + + # Optional - show selected positon + # This may change the camera, so don't use it for test values + # scatter!(scene, pos) + + scene + =# +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5d46b19a92e..1e2e257d552 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,4 +32,5 @@ using Makie: volume include("events.jl") include("text.jl") include("boundingboxes.jl") + include("ray_casting.jl") end From eaf1028b885fd8c1e6786adc93ba39d043ad84d2 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Jul 2023 19:31:34 +0200 Subject: [PATCH 14/14] Updates for Northstar & v3 (#2786) * allow multi gpu * use northstar way of life * improvements * update version & test examples --- RPRMakie/Project.toml | 2 +- RPRMakie/examples/bars.jl | 4 +-- RPRMakie/examples/lego.jl | 3 +- RPRMakie/examples/lines.jl | 23 +++++++++++--- RPRMakie/examples/material_x.jl | 25 +++++++++++++++ RPRMakie/examples/materials.jl | 2 +- RPRMakie/examples/opengl_interop.jl | 7 +++-- RPRMakie/examples/volume.jl | 14 +++++++++ RPRMakie/src/RPRMakie.jl | 2 +- RPRMakie/src/lines.jl | 8 ++--- RPRMakie/src/meshes.jl | 10 +----- RPRMakie/src/scene.jl | 1 + RPRMakie/src/volume.jl | 48 +++++++++++++++++------------ src/camera/camera3d.jl | 1 - 14 files changed, 101 insertions(+), 49 deletions(-) create mode 100644 RPRMakie/examples/material_x.jl create mode 100644 RPRMakie/examples/volume.jl diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 46151afff69..e0aea02aeb2 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -18,7 +18,7 @@ Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" Makie = "=0.19.6" -RadeonProRender = "0.2.15" +RadeonProRender = "0.3.0" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/RPRMakie/examples/bars.jl b/RPRMakie/examples/bars.jl index 61514ecc546..a6be9abe0e6 100644 --- a/RPRMakie/examples/bars.jl +++ b/RPRMakie/examples/bars.jl @@ -2,7 +2,7 @@ using GeometryBasics, RPRMakie using Colors, FileIO, ImageShow using Colors: N0f8 -RPRMakie.activate!(plugin=RPR.Northstar) +RPRMakie.activate!(plugin=RPR.Northstar, resource=RPR.GPU0) fig = Figure(; resolution=(800, 600), fontsize=26) radiance = 10000 lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), @@ -25,4 +25,4 @@ cam.eyeposition[] = Float32[5, 22, 12] cam.lookat[] = Float32[5, 5, -0.5] cam.upvector[] = Float32[0.0, 0.0, 1.0] cam.fov[] = 14.0 -ax.scene +@time display(ax.scene) diff --git a/RPRMakie/examples/lego.jl b/RPRMakie/examples/lego.jl index b5655a0d681..cb364038271 100644 --- a/RPRMakie/examples/lego.jl +++ b/RPRMakie/examples/lego.jl @@ -88,8 +88,7 @@ nsteps = length(angles); #Number of animation steps translations = LinRange(0, total_translation, nsteps) s -Makie.record(s, "lego_walk.mp4", zip(translations, angles)) do (translation, angle) - +@time Makie.record(s, "lego_walk.mp4", zip(translations, angles)) do (translation, angle) # Rotate right arm + hand for name in ["arm_left", "arm_right", "leg_left", "leg_right"] rotate!(figure[name], rotation_axes[name], angle) diff --git a/RPRMakie/examples/lines.jl b/RPRMakie/examples/lines.jl index 4b6a103fd44..b3f2624e7f9 100644 --- a/RPRMakie/examples/lines.jl +++ b/RPRMakie/examples/lines.jl @@ -2,19 +2,32 @@ using GeometryBasics, RPRMakie using Colors, FileIO using Colors: N0f8 +function box!(ax, size) + orig = Vec3f(-2, -2, 0) + mesh!(ax, Rect3f(orig, Vec3f(size, size, 0.1)); color=:white, + material=(reflection_color=Vec4f(1), reflection_weight=10f0)) + mesh!(ax, Rect3f(orig, Vec3f(0.1, size, size)); color=:white) + mesh!(ax, Rect3f(orig, Vec3f(size, 0.1, size)); color=:white) + return +end + begin - RPRMakie.activate!(plugin=RPR.Tahoe) fig = Figure(; resolution=(1000, 1000)) - ax = LScene(fig[1, 1]) + radiance = 100 + lights = Makie.AbstractLight[PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] + ax = LScene(fig[1, 1]; scenekw=(; lights=lights), show_axis=false) points = Point3f[] for i in 4:10 n = i + 1 y = LinRange(0, i, n) y2 = (y ./ 2) .- 2 xyz = Point3f.((i - 5) ./ 2, y2, sin.(y) .+ 1) - lines!(ax, xyz; linewidth=5, color=:red) + lp = lines!(ax, xyz; linewidth=10, color=:white) append!(points, xyz) end - meshscatter!(ax, points, color=:green) - ax.scene + mat = (; emission_color=:red, emission_weight=Vec3f(5.0f0)) + meshscatter!(ax, points; material=mat) + box!(ax, 5) + RPRMakie.activate!(plugin = RPR.Northstar, iterations = 500, resource = RPR.GPU0) + ax.scene |> display end diff --git a/RPRMakie/examples/material_x.jl b/RPRMakie/examples/material_x.jl new file mode 100644 index 00000000000..415f87c7e7a --- /dev/null +++ b/RPRMakie/examples/material_x.jl @@ -0,0 +1,25 @@ +# download material from: https://matlib.gpuopen.com/main/materials/all?material=8686536a-8041-445b-97f1-f249f4c3b0af +using RPRMakie, ImageShow + +material = "Pinwheel_Pattern_Marble_Tiles_4k_16b" # folder you downloaded & extracted + +img = begin + radiance = 1000 + lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), + PointLight(Vec3f(5), RGBf(radiance, radiance, radiance * 1.1))] + fig = Figure(; resolution=(1500, 700)) + ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) + screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=500, resource=RPR.GPU0) + matsys = screen.matsys + marble_tiles = RPR.Matx(matsys, joinpath(material, "Pinwheel_Pattern_Marble_Tiles.mtlx")) + + mesh!(ax, load(Makie.assetpath("matball_floor.obj")); color=:white) + matball!(ax, marble_tiles; color=nothing) + cam = cameracontrols(ax.scene) + cam.eyeposition[] = Vec3f(0.0, -2, 1) + cam.lookat[] = Vec3f(0) + cam.upvector[] = Float32[0.0, -0.01, 1.0] + update_cam!(ax.scene, cam) + # TODO, material doesn't show up? + colorbuffer(screen) +end diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index 3e5e7639dda..ccdcb2209d7 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -8,7 +8,7 @@ img = begin PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] fig = Figure(; resolution=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) - screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar) + screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=1000) matsys = screen.matsys emissive = RPR.EmissiveMaterial(matsys) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index e43fe2df392..04e7c2ccd10 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -77,9 +77,10 @@ cam.lookat[] = Vec3f(0, 0, -1) cam.upvector[] = Vec3f(0, 0, 1) cam.fov[] = 30 -display(fig) - -context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh) +GLMakie.activate!(inline=false) +display(fig; inline=false, backend=GLMakie) +RPRMakie.activate!(iterations=1, plugin=RPR.Northstar, resource=RPR.GPU0) +context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh); # Change light parameters interactively begin diff --git a/RPRMakie/examples/volume.jl b/RPRMakie/examples/volume.jl new file mode 100644 index 00000000000..7c4a86ddb63 --- /dev/null +++ b/RPRMakie/examples/volume.jl @@ -0,0 +1,14 @@ +using RPRMakie +using Makie, NIfTI, FileIO +using GLMakie +r = LinRange(-1, 1, 100) +cube = [(x .^ 2 + y .^ 2 + z .^ 2) for x = r, y = r, z = r] + +brain = Float32.(niread(Makie.assetpath("brain.nii.gz")).raw) +radiance = 5000 +lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), + PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] +fig = Figure(; resolution=(1000, 1000)) +ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) +Makie.volume!(ax, 0..3, 0..3.78, 0..3.18, brain, algorithm=:absorption, absorption=0.3) +display(ax.scene; iterations=5000) diff --git a/RPRMakie/src/RPRMakie.jl b/RPRMakie/src/RPRMakie.jl index 57e415bfae3..74bf5fc7d4d 100644 --- a/RPRMakie/src/RPRMakie.jl +++ b/RPRMakie/src/RPRMakie.jl @@ -30,7 +30,7 @@ function ScreenConfig(iterations::Int, max_recursion::Int, render_resource, rend iterations, max_recursion, Int32(render_resource isa Makie.Automatic ? RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 : render_resource), - render_plugin isa Makie.Automatic ? RPR.Tahoe : render_plugin + render_plugin isa Makie.Automatic ? RPR.Northstar : render_plugin ) end diff --git a/RPRMakie/src/lines.jl b/RPRMakie/src/lines.jl index 467ce80572e..a6c91665dd4 100644 --- a/RPRMakie/src/lines.jl +++ b/RPRMakie/src/lines.jl @@ -24,8 +24,8 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Lines) indices = line2segments(points) radius = [plot.linewidth[] / 1000] curve = RPR.Curve(context, points, indices, radius, [Vec2f(0.0)], [length(indices) ÷ 4]) - material = RPR.MaterialNode(matsys, RPR.RPR_MATERIAL_NODE_DIFFUSE) - set!(material, RPR.RPR_MATERIAL_INPUT_COLOR, to_color(plot.color[])) + material = extract_material(matsys, plot) + material.color = to_color(plot.color[]) set!(curve, material) return curve end @@ -57,7 +57,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.LineSegments) curve = RPR.Curve(context, points, indices, radius, Vec2f.(0.0, LinRange(0, 1, nsegments)), fill(1, nsegments)) - material = RPR.DiffuseMaterial(matsys) + material = extract_material(matsys, plot) color = to_color(plot.color[]) function set_color!(colorvec) @@ -78,6 +78,6 @@ function to_rpr_object(context, matsys, scene, plot::Makie.LineSegments) else material.color = to_color(color) end - set!(curve, material.node) + set!(curve, material) return curve end diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index 4e31520ed6f..c94f3611a97 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -68,15 +68,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) instances = [marker] n_instances = length(positions) RPR.rprShapeSetObjectID(marker, 0) - material = if haskey(plot, :material) - if plot.material isa Attributes - RPR.Material(matsys, Dict(map(((k,v),)-> k => to_value(v), plot.material))) - else - plot.material[] - end - else - RPR.DiffuseMaterial(matsys) - end + material = extract_material(matsys, plot) set!(marker, material) for i in 1:(n_instances-1) inst = RPR.Shape(context, marker) diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 6a5902bfea8..07e2d491961 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -5,6 +5,7 @@ function update_rpr_camera!(oldvals, camera, cam_controls, cam) c = cam_controls l, u, p, fov = c.lookat[], c.upvector[], c.eyeposition[], c.fov[] far, near, res = c.far[], c.near[], cam.resolution[] + fov = 45f0 # The current camera ignores fov updates new_vals = (; l, u, p, fov, far, near, res) new_vals == oldvals && return oldvals wd = norm(l - p) diff --git a/RPRMakie/src/volume.jl b/RPRMakie/src/volume.jl index d3766f82a09..197c56ed1a0 100644 --- a/RPRMakie/src/volume.jl +++ b/RPRMakie/src/volume.jl @@ -1,29 +1,37 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Volume) volume = plot.volume[] - xyz = plot.x[], plot.y[], plot.z[] - mini_maxi = extrema.(xyz) - mini = first.(mini_maxi) - maxi = last.(mini_maxi) + cube = RPR.VolumeCube(context) - vol_cube = RadeonProRender.Shape(context, Rect3f(mini, maxi .- mini)) - color_lookup = to_colormap(plot.colormap[]) - density_lookup = [Vec3f(plot.absorption[])] + function update_cube(m, xyz...) + mi = minimum.(xyz) + maxi = maximum.(xyz) + w = maxi .- mi + m2 = Mat4f(w[1], 0, 0, 0, 0, w[2], 0, 0, 0, 0, w[3], 0, mi[1], mi[2], mi[3], 1) + mat = convert(Mat4f, m) * m2 + transform!(cube, mat) + return + end + onany(update_cube, plot.model, plot.x, plot.y, plot.z) + update_cube(plot.model[], plot.x[], plot.y[], plot.z[]) mini, maxi = extrema(volume) - grid = RPR.VoxelGrid(context, (volume .- mini) ./ (maxi - mini)) - rpr_vol = RPR.HeteroVolume(context) + vol_normed = (volume .- mini) ./ (maxi - mini) + grid = RPR.VoxelGrid(context, vol_normed) + gridsampler = RPR.GridSamplerMaterial(matsys) + gridsampler.data = grid - RPR.set_albedo_grid!(rpr_vol, grid) - RPR.set_albedo_lookup!(rpr_vol, color_lookup) + color_sampler = RPR.ImageTextureMaterial(matsys) + color_sampler.data = RPR.Image(context, reverse(to_colormap(plot.colormap[]))) + gridsampler2 = RPR.GridSamplerMaterial(matsys) + color_sampler.uv = gridsampler - RPR.set_density_grid!(rpr_vol, grid) - RPR.set_density_lookup!(rpr_vol, density_lookup) + volmat = RPR.VolumeMaterial(matsys) + on(plot.absorption; update=true) do absorption + return volmat.density = Vec4f(absorption, 0.0, 0.0, 0.0) + end + volmat.densitygrid = gridsampler + volmat.color = color_sampler + set!(cube, volmat) - mat = RPR.TransparentMaterial(matsys) - mat.color = Vec4f(1) - - set!(vol_cube, rpr_vol) - set!(vol_cube, mat) - - return [vol_cube, rpr_vol] + return [cube] end diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index d7a8b6dace2..2b695aa0043 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -174,7 +174,6 @@ function Camera3D(scene::Scene; kwargs...) on(camera(scene), events(scene).keyboardbutton) do event if event.action in (Keyboard.press, Keyboard.repeat) && cam.pulser[] == -1.0 && attr.selected[] && any(key -> ispressed(scene, attr[key][]), keynames) - cam.pulser[] = time() return Consume(true) end